Kako smo riješili OOM livelock na ZFS + KVM hostu: ARC cap, swappiness i Patroni balloon
🇬🇧 In English: How we fixed an OOM livelock on a ZFS + KVM host: ARC cap, swappiness and a Patroni balloon
Virtualizacijski host nam se zamrznuo — potpuno neresponzivan, bez SSH-a, bez konzole — i morali smo ga ručno restartovati (hard reset). Riječ je o NixOS hostu sa ZFS-om i desetak KVM/QEMU gostiju. Ovo je priča kako smo iz logova rekonstruisali šta se desilo i koje smo četiri promjene primijenili da se ne ponovi.
Tema je univerzalna za svakoga ko vrti ZFS + KVM na istom serveru: kombinacija neograničenog ZFS ARC-a, prenapučene (over-committed) VM memorije i swap-a na ZFS zvol-u je recept za zamrzavanje.
Simptom: beskonačna petlja umjesto čistog OOM-a
Prvo pravilo nakon ovakvog incidenta — pogledaj prethodni boot:
journalctl --list-boots
Log prethodnog boota se naglo prekida, a posljednje što vidimo je 2039 identičnih linija u jednoj sekundi:
kernel: Purging GPU memory, 0 pages freed, 0 pages still pinned, 1 pages left available.
…i onda ništa. Ova poruka dolazi iz i915 GPU shrinkera integrisane Intel grafike. Kernel pod ekstremnim pritiskom memorije zove sve “shrinkere” da oslobode RAM; na headless serveru i915 nema šta osloboditi, pa se vrti u prazno. To je klasičan out-of-memory livelock — sistem nije pao na čist OOM, nego se zaglavio pokušavajući (bezuspješno) osloboditi memoriju.
Bitan detalj: u trenutku zamrzavanja OOM-killer se uopšte nije aktivirao. Da jeste, ubio bi jedan proces i sistem bi preživio. Umjesto toga — livelock i totalno zamrzavanje.
Analiza uzroka: tri faktora koja se sabiraju
# ARC bez gornje granice
grep -E '^(size|c_max)' /proc/spl/kstat/zfs/arcstats
# c_max 61.7 GB <-- na hostu sa 62 GiB RAM-a!
# swap je ZFS zvol
swapon --show
# /dev/zd0 partition 8G
-
Neograničen ZFS ARC.
zfs_arc_maxje bio na defaultu, pa je ARC smio narasti praktično do cijelog RAM-a. ARC je teoretski “reclaimable”, ali pod naglim pritiskom VM alokacija ne stigne se evictovati dovoljno brzo. -
Over-commit VM memorije. Zbir alociranog RAM-a gostiju bio je gotovo jednak fizičkom RAM-u hosta. Nakon ~53 dana uptime-a, RSS gostiju se “ugrijao” prema tim alokacijama (page cache unutar gosta se puni, a KVM rijetko vraća stranice hostu), pa je realna potrošnja gmizala prema plafonu.
-
Swap na ZFS zvol-u. Da bi upisao stranicu u swap na zvol-u, ZFS mora alocirati memoriju za svoj write path — što je nemoguće kad memorije nema. Reclaim ne napreduje → livelock umjesto oporavka. Zato je 8 GB swap-a bilo praktično beskorisno u kritičnom trenutku.
Zanimljivo: noćni backup nije bio krivac, iako se na prvi pogled tako činilo. Tek pažljivo čitanje logova pokazalo je da je backup završio uredno sati prije zamrzavanja. Pravi okidač je bila alokacija memorije jednog gosta uz već zategnut RAM.
Rješenje: četiri zahvata
1. Ograniči ZFS ARC (glavni popravak)
Pošto je root na ZFS-u, granicu postavljamo preko kernel parametra da se primijeni već pri učitavanju modula u initrd-u:
boot.kernelParams = [ "zfs.zfs_arc_max=17179869184" ]; # 16 GiB
Uživo, bez reboota:
echo 17179869184 > /sys/module/zfs/parameters/zfs_arc_max
Ovo je suštinski popravak: uklanja “odbjeglog potrošača” i garantuje da OOM-killer može čisto odraditi posao (ubije jedan proces) umjesto da se sistem zaglavi.
Korisno znati: na Linuxu ZFS ARC se u
freeprikazuje podused, a ne podbuff/cache— pa “iskorištena” memorija izgleda veća nego što stvarno jeste. ~16 GB toga je reclaimable cache.
2. Smanji vm.swappiness
boot.kernel.sysctl."vm.swappiness" = 10; # default je 60
Tjera kernel da prvo oslobađa page cache / ARC, a tek onda ulazi u opasni zvol write-path. Primjenjuje se odmah (sysctl je runtime, ne treba reboot).
Napomena: smanjivanje swap-a sa 8 na 4 GB ne rješava deadlock — veličina swap-a nije uzrok, nego činjenica da write-path treba memoriju. Pravi fix bi bio swap van ZFS-a; swappiness je ono što stvarno smanjuje izloženost.
3. Pravilno dimenzioniraj prenapučene VM-ove
Jedan web-server VM imao je alociran višestruko više RAM-a nego što ikad koristi. Spustili smo mu alokaciju i tako vratili dragocjeni headroom hostu. Promjena memorije zahtijeva hladni restart gosta (max memorija se ne mijenja na živom domenu).
4. Patroni-aware balloon servis (live, bez reboota)
Najzanimljiviji dio. Imamo PostgreSQL HA klaster (Patroni) od više čvorova-VM-ova. Lider radi sav write i treba pun RAM; replike samo “replejaju” WAL i troše manje. Umjesto fiksne alokacije, napravili smo mali servis na hostu koji svaki sat:
- pročita topologiju klastera preko Patroni REST API-ja (
/cluster), - preko live libvirt balloona (
virsh setmem … --live, bez reboota) postavi:- lider → pun RAM,
- zdrave streaming replike → manji RAM.
Ključne sigurnosne odluke:
- Grow-before-shrink: prvo poveća (prema lideru), pa tek onda smanji — da svježe promovisani lider nikad nije ni na tren prikraćen.
- Fail-open: ako je Patroni nedostupan ili nema lidera, servis ne dira ništa — VM-ovi ostaju na punoj (boot-default) memoriji. Najgori scenario je “nema uštede”, nikad “lider premali”.
- Sigurno jer je
shared_buffersfiksan i postavljen za donju granicu; balloon oslobađa samo slobodnu memoriju gosta, nikad stranice koje PostgreSQL koristi.
Skraćena logika (Python, stdlib):
for vm, target_kib in sorted(targets.items(), key=lambda kv: -kv[1]): # grow prije shrink
if current_kib(vm) != target_kib:
subprocess.check_call(["virsh", "setmem", vm, str(target_kib), "--live"])
Rezultat: lider ostaje pun, replike se skupljaju, a host dobije nekoliko gigabajta headroom-a — sve uživo, bez ijednog reboota baze.
Lekcije
- Na ZFS + KVM hostu uvijek ograniči ARC (
zfs_arc_max). Default “do skoro svega” se direktno bije sa memorijom gostiju. - Pazi na over-commit i “warm-up”. Svjež VM troši malo; nakon sedmica uptime-a RSS gmiže prema alociranom. Metrika koju treba pratiti je committed memorija, ne trenutni
free. - Swap na ZFS zvol-u je deadlock-prone. Ako mora tako, bar spusti
swappiness; idealno swap drži van ZFS-a. - Livelock nije isto što i OOM. Ako vidiš beskonačni
Purging GPU memory(ili sličan shrinker u petlji), to je memorijski livelock — cilj je da OOM-killer može čisto odraditi posao. - Čitaj logove do kraja prije zaključka. Prvi osumnjičeni (backup) nije bio krivac; tek hronologija je otkrila pravi okidač.
- Balloon + orkestracija (ovdje: Patroni rola → libvirt balloon) omogućava dinamičku raspodjelu RAM-a između VM-ova bez reboota.
Napomena
Generisano od strane Claude 🤖
Ernad Husremović, hernad@bring.out.ba