OOM Killer

OOM Killer

Există un scenariu pe care orice administrator de sistem îl întâlnește mai devreme sau mai târziu: sistemul rămâne fără memorie, swap-ul e plin, și niciun proces nu eliberează RAM. În acest punct, kernelul Linux activează un mecanism de ultimă instanță numit Out of Memory Killer - sau simplu, OOM Killer. Rolul său este brutal de simplu: alege unul sau mai multe procese și le termină forțat pentru a elibera memorie și a menține sistemul funcțional.

Cum ajunge sistemul în stare OOM

Linux folosește implicit o strategie numită memory overcommit - permite proceselor să aloce mai multă memorie decât există fizic disponibil, bazându-se pe faptul că majoritatea proceselor nu folosesc simultan toată memoria alocată. De exemplu, când un proces apelează malloc() pentru 1GB, kernelul îi promite acea memorie fără să o aloce efectiv - paginile fizice sunt alocate abia când procesul scrie în ele (lazy allocation).

Această strategie funcționează bine în practică, dar în scenarii extreme - un proces care începe să consume rapid memorie, mai multe procese care cresc simultan, sau un leak de memorie - sistemul poate ajunge în situația în care promisiunile depășesc cu mult realitatea. Când swap-ul e plin și RAM-ul e epuizat, kernelul nu mai poate onora alocările și OOM Killer intră în acțiune.

Cum alege OOM Killer victima

OOM Killer nu termină procese la întâmplare. Kernelul calculează pentru fiecare proces un oom_score între 0 și 1000, unde un scor mai mare înseamnă o probabilitate mai mare de a fi ales ca victimă. Algoritmul ia în considerare mai mulți factori:

  • Consumul de memorie - procesele care folosesc mult RAM au scor mai mare
  • Durata de rulare - procesele nou-pornite au scor ușor mai mare decât cele vechi
  • Procesele copil - un proces cu mulți copii care consumă memorie are scor mai mare
  • Prioritatea - procesele cu prioritate mare au scor ușor mai mic

Scopul algoritmului este să elibereze cât mai multă memorie cu un singur proces terminat, minimizând impactul asupra sistemului.

Verifici scorul OOM al oricărui proces:

cat /proc/PID/oom_score

unde PID este ID-ul procesului. Pentru a vedea scorurile tuturor proceselor, sortate descrescător:

for proc in /proc/[0-9]*/; do
  pid=$(basename $proc)
  score=$(cat $proc/oom_score 2>/dev/null)
  name=$(cat $proc/comm 2>/dev/null)
  echo "$score $pid $name"
done | sort -rn | head -20

Cum identifici dacă OOM Killer a acționat

Primul semn că OOM Killer a acționat este de obicei un serviciu care dispare fără urmă sau un server care devine brusc iresponsiv. Kernelul înregistrează întotdeauna acțiunile OOM Killer în log-urile de sistem.

Pe sisteme cu systemd:

journalctl -k | grep -i "oom\|killed process\|out of memory"

sau pentru log-urile kernel din ultima pornire:

journalctl -k -b 0 | grep -i oom

Pe sisteme fără systemd sau dacă preferi dmesg:

dmesg | grep -i "oom\|killed"

Un mesaj tipic OOM arată astfel:

Out of memory: Killed process 1234 (nginx) total-vm:524288kB, anon-rss:102400kB, file-rss:4096kB

Mesajul include numele procesului, PID-ul și câtă memorie folosea în momentul terminării. Înainte de acest mesaj vei găsi de obicei un dump complet al stării memoriei în momentul evenimentului, util pentru diagnostic.

Controlul comportamentului OOM

oom_score_adj

Fiecare proces are un fișier /proc/PID/oom_score_adj care permite ajustarea scorului OOM calculat de kernel. Valoarea poate fi între -1000 și 1000:

  • -1000 înseamnă că procesul este complet imun la OOM Killer
  • 0 înseamnă comportament implicit
  • 1000 înseamnă că procesul va fi primul ales ca victimă

Verifici valoarea curentă pentru un proces:

cat /proc/PID/oom_score_adj

Modifici valoarea pentru un proces care rulează deja:

echo -500 > /proc/PID/oom_score_adj

Protejarea proceselor critice

Dacă rulezi servicii critice - un server de baze de date, un server web principal, un daemon de monitorizare - poți reduce semnificativ șansa ca OOM Killer să le termine setând un oom_score_adj negativ.

Pentru procese gestionate de systemd, adaugi în fișierul de serviciu (/etc/systemd/system/nume-serviciu.service sau prin systemctl edit):

[Service]
OOMScoreAdjust=-500

Reîncarci configurația și repornești serviciul:

systemctl daemon-reload
systemctl restart nume-serviciu

Valoarea -500 reduce semnificativ șansa de a fi ales, fără să acorde imunitate completă. Imunitatea completă (-1000) e rezervată proceselor cu adevărat critice - dacă protejezi prea multe procese, OOM Killer nu va mai putea elibera suficientă memorie și sistemul va îngheța în loc să termine un proces.

Procesele cu imunitate implicită

Kernelul acordă automat imunitate (-1000) câtorva procese esențiale pentru funcționarea sistemului, cum ar fi procesele din spațiul kernel. Procesul init (PID 1, systemd sau echivalentul) are de asemenea protecție implicită.

Comportamentul la OOM: panic sau kill

Implicit, când kernelul detectează o situație OOM, activează OOM Killer. Există însă un parametru care schimbă acest comportament și provoacă un kernel panic (oprire imediată a sistemului) în loc să termine procese:

cat /proc/sys/vm/panic_on_oom

Valoarea 0 înseamnă că OOM Killer e activ (implicit). Valoarea 1 sau 2 provoacă kernel panic. Pe servere de producție unde consecințele unui serviciu terminat sunt mai grave decât o repornire, kernel panic + watchdog poate fi preferabil - sistemul repornește controlat în loc să ruleze într-o stare degradată.

# Activezi kernel panic la OOM (temporar)
sysctl vm.panic_on_oom=1
 
# Permanent în /etc/sysctl.conf
echo "vm.panic_on_oom=1" >> /etc/sysctl.conf

Un parametru companion este kernel.panic, care setează numărul de secunde după care sistemul repornește automat după un panic:

kernel.panic=10

Overcommit: controlul alocărilor de memorie

Comportamentul de overcommit al kernelului este controlat de vm.overcommit_memory:

Valoare Comportament
0 Overcommit euristic (implicit) - kernelul permite overcommit rezonabil
1 Overcommit fără limite - orice alocare este acceptată
2 Fără overcommit - alocările sunt limitate la RAM + swap

Modul 2 previne complet situațiile OOM prin refuzarea alocărilor care ar depăși memoria disponibilă, dar poate cauza erori de tip Cannot allocate memory în aplicații care se bazează pe comportamentul standard de overcommit (multe programe C, JVM, etc.).

# Verifici modul curent
cat /proc/sys/vm/overcommit_memory
 
# Modifici temporar
sysctl vm.overcommit_memory=2
 
# Permanent
echo "vm.overcommit_memory=2" >> /etc/sysctl.conf

Când overcommit_memory=2, limita totală de memorie este controlată de vm.overcommit_ratio (implicit 50), care reprezintă procentul din RAM fizic adăugat la swap pentru calculul limitei:

limita = swap + (overcommit_ratio / 100) * RAM_fizic

Prevenirea situațiilor OOM

OOM Killer este ultima linie de apărare, nu o soluție. Dacă sistemul tău activează OOM Killer regulat, problema reală este că rulezi mai multe servicii decât poate susține RAM-ul disponibil.

Monitorizarea proactivă este primul pas - urmărești consumul de memorie și primești alerte înainte de a ajunge în situație critică:

# Verifici rapid memoria disponibilă
free -h
 
# Urmărești evoluția în timp real
watch -n 5 free -h

Limitele de memorie per container sunt esențiale dacă rulezi Docker - fără limite, un container poate consuma tot RAM-ul disponibil și declanșa OOM Killer pentru alte procese. Adaugi în docker-compose.yml:

services:
  aplicatie:
    mem_limit: 512m
    memswap_limit: 512m

earlyoom este un daemon din spațiul utilizator care monitorizează memoria și termină procese înainte ca kernelul să ajungă în stare OOM critică, oferind un control mai fin și notificări:

# Ubuntu/Debian
apt install earlyoom
 
# Arch Linux
pacman -S earlyoom
 
systemctl enable --now earlyoom

earlyoom acționează când memoria disponibilă scade sub un prag configurabil (implicit 10%) și poate fi configurat să trimită notificări sau să execute un script înainte de a termina procese.

Diagnosticare post-OOM

Când OOM Killer a acționat, e important să înțelegi de ce pentru a preveni recurența. Colectezi informațiile complete din log-uri:

journalctl -k -b -1 | grep -A 50 "Out of memory"

-b -1 se referă la boot-ul anterior (util dacă sistemul s-a restartat după OOM). Dump-ul kernel include consumul de memorie per proces în momentul evenimentului, ceea ce îți permite să identifici care proces a consumat RAM excesiv.

Dacă vrei să simulezi o situație OOM în mod controlat pentru testare (într-un mediu de test, nu pe producție):

# Alocă memorie rapid până la epuizare - folosește cu MARE grijă
python3 -c "x = []; [x.append(' ' * 10**6) for _ in range(10000)]"

Resurse suplimentare

  • Swap pe Linux - gestionarea memoriei virtuale, partiții și fișiere swap
  • zram și zswap - alternative moderne la swap-ul clasic pe disc