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
  1. Neograničen ZFS ARC. zfs_arc_max je 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.

  2. 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.

  3. 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 free prikazuje pod used, a ne pod buff/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_buffers fiksan 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