Padroneggiare la Politica OOM: Ottimizzare la Risposta di Systemd agli Eventi di Memoria Esaurita

Impara a controllare il comportamento del killer Out-of-Memory (OOM) di Linux usando systemd. Questa guida esplora le direttive `OOMScoreAdjust` e `OOMPolicy` per proteggere i servizi critici influenzando quali processi vengono terminati durante condizioni di bassa memoria. Padroneggia l'ottimizzazione OOM di systemd per una maggiore stabilità e resilienza del sistema.

Padroneggiare la Politica OOM: Ottimizzare la Risposta di Systemd agli Eventi di Memoria Esaurita

I guasti per memoria esaurita raramente accadono in un momento opportuno. Un'importazione batch riceve un file più grande del solito, un servizio perde memoria durante la notte, un backup si sovrappone a un picco di traffico, o un deploy raddoppia il numero di processi worker. Quando Linux non riesce a liberare memoria sufficiente per un'allocazione, il kernel può invocare il killer OOM e terminare un processo in modo che la macchina possa continuare a funzionare.

La parte scomoda è che la vittima predefinita potrebbe non essere il servizio che avresti scelto. Su un host condiviso, potresti preferire che un worker di coda riprovabile muoia prima dell'API principale. Su un server di database, potresti volere che SSH e il monitoraggio rimangano attivi per poter recuperare la macchina. Systemd ti offre due manopole per questo tipo di decisione: OOMScoreAdjust= e OOMPolicy=.

OOMScoreAdjust= influenza quale processo viene selezionato. OOMPolicy= controlla cosa fa systemd dopo che un processo nel servizio è stato ucciso. Risolvono problemi diversi, e confonderli porta a runbook scadenti.

Cosa Sta Valutando il Kernel

Ogni processo Linux ha un punteggio OOM, visibile in /proc/<pid>/oom_score. Un punteggio più alto significa che il processo è una vittima OOM più probabile. Il kernel deriva quel punteggio dall'uso della memoria e da altro contesto, quindi applica il valore di aggiustamento da /proc/<pid>/oom_score_adj.

OOMScoreAdjust= di systemd scrive quell'aggiustamento per i processi che avvia. L'intervallo è da -1000 a 1000.

  • -1000 fornisce la protezione più forte e disabilita efficacemente l'uccisione OOM per quel processo.
  • I valori negativi rendono il processo meno probabile da uccidere.
  • I valori positivi rendono il processo più probabile da uccidere.
  • 0 lascia l'aggiustamento neutro.

L'approccio più sicuro di solito non è "proteggi tutto ciò che è importante." Se ogni servizio è protetto, il kernel ha meno scelte utili quando l'host è già a corto di memoria. Proteggi un piccolo numero di servizi e rendi il lavoro sacrificabile più facile da uccidere.

Per un servizio API primario, un aggiustamento moderato è spesso sufficiente:

[Service]
OOMScoreAdjust=-300

Per un worker di coda che può riprovare i lavori:

[Service]
OOMScoreAdjust=500

Quel worker potrebbe morire per primo durante la pressione della memoria, ma questo è il punto. Un lavoro fallito può tornare nella coda. Un database morto o un host irraggiungibile è un incidente più grande.

Cosa Fa Realmente OOMPolicy

OOMPolicy= non segna un'unità come "critica," e non sceglie il primo processo da uccidere. I valori supportati sono continue, stop e kill.

  • continue: systemd registra l'evento OOM e lascia l'unità in esecuzione se rimangono dei processi.
  • stop: systemd registra l'evento e ferma l'unità in modo pulito.
  • kill: se un processo nell'unità viene ucciso da OOM, i processi rimanenti in quell'unità vengono uccisi come gruppo.

Usa questa impostazione per evitare servizi mezzo vivi. Se un servizio web multi-processo perde un worker e continua ad accettare traffico in uno stato rotto, continue può nascondere il fallimento. OOMPolicy=kill rende il fallimento ovvio e permette a Restart=on-failure di riportare il servizio in uno stato pulito.

[Service]
OOMPolicy=kill
Restart=on-failure
RestartSec=5s

Per un lavoro batch con processi helper, stop può essere meno brusco per i processi rimanenti:

[Service]
OOMPolicy=stop

Il processo scelto dal kernel è già sparito. stop influisce solo su ciò che systemd fa al resto del servizio, quindi non fare affidamento su di esso come punto di salvataggio elegante. I lavori a lunga esecuzione dovrebbero fare checkpoint del proprio lavoro.

Un Modello Pratico di Ottimizzazione

Inizia suddividendo i servizi in tre gruppi.

Primo, identifica i servizi che mantengono l'host recuperabile: SSH, rete, monitoraggio e il carico di lavoro primario. Dai solo ai più importanti aggiustamenti negativi modesti.

Secondo, identifica i servizi che possono essere riprovati: worker, importatori, generatori di report, processori di immagini, cache warmer, helper di sviluppo. Dai a questi aggiustamenti positivi.

Terzo, decidi se ogni servizio può continuare a funzionare in sicurezza dopo che un processo è stato ucciso. Se no, usa OOMPolicy=kill e una politica di riavvio.

Un override realistico per un worker potrebbe assomigliare a questo:

# /etc/systemd/system/image-worker.service.d/oom.conf
[Service]
OOMScoreAdjust=500
OOMPolicy=kill
Restart=on-failure
RestartSec=10s

Un servizio applicativo primario potrebbe assomigliare a questo:

# /etc/systemd/system/api.service.d/oom.conf
[Service]
OOMScoreAdjust=-300
OOMPolicy=kill
Restart=on-failure
RestartSec=5s

Eviterei OOMScoreAdjust=-1000 a meno che tu non abbia testato la modalità di fallimento. Se quel servizio protetto è quello che perde memoria, la macchina ha ancora bisogno di un modo per recuperare.

Applicare e Verificare la Modifica

Usa drop-in invece di modificare i file di unità confezionati:

sudo systemctl edit api.service

Dopo aver salvato l'override, ricarica systemd e riavvia il servizio:

sudo systemctl daemon-reload
sudo systemctl restart api.service

Controlla l'unità unita e i valori che systemd vede:

systemctl cat api.service
systemctl show api.service -p OOMPolicy -p OOMScoreAdjust

Poi ispeziona il processo in esecuzione:

PID=$(systemctl show api.service -p MainPID --value)
cat /proc/$PID/oom_score_adj
cat /proc/$PID/oom_score

oom_score_adj dovrebbe corrispondere al tuo aggiustamento configurato. oom_score può cambiare man mano che il processo usa più o meno memoria.

Dopo un incidente, controlla sia i log dell'unità che il log del kernel:

journalctl -u api.service --since "1 hour ago"
journalctl -k --since "1 hour ago" | grep -i oom

Su sistemi che usano systemd-oomd, controlla anche:

systemctl status systemd-oomd
oomctl

La Politica OOM Non È Pianificazione della Capacità

L'ottimizzazione OOM è un'ultima linea di difesa. Hai ancora bisogno di limiti di memoria, avvisi e spazio sufficiente per i picchi normali. Per servizi con confini prevedibili, considera i controlli di memoria cgroup:

[Service]
MemoryHigh=1500M
MemoryMax=2G

MemoryHigh= applica pressione prima del limite rigido. MemoryMax= è un tetto massimo. Il comportamento esatto dipende dalla versione di systemd e dalla configurazione cgroup, ma l'idea operativa è semplice: contenere un servizio prima che consumi l'host.

Lo swap merita lo stesso tipo di considerazione. Nessuno swap può far sì che picchi brevi si trasformino in uccisioni OOM improvvise. Troppo swap lento può mantenere l'host vivo mentre la latenza diventa inutile. Rivedi la politica OOM insieme a swap, limiti di memoria, comportamento di riavvio e avvisi.

Esempio: Un Host, Tre Servizi

Supponiamo che un piccolo host di produzione esegua un'API, una cache Redis e un worker di report in background. Il worker di report è utile, ma può riprovare il lavoro. Redis migliora la latenza, ma l'applicazione può comunque servire alcune richieste andando al database. L'API è il servizio rivolto ai clienti.

Un primo tentativo ragionevole potrebbe essere:

# api.service
[Service]
OOMScoreAdjust=-300
OOMPolicy=kill
Restart=on-failure
# redis.service drop-in, se questa istanza Redis è solo cache
[Service]
OOMScoreAdjust=0
OOMPolicy=kill
# report-worker.service
[Service]
OOMScoreAdjust=600
OOMPolicy=kill
Restart=on-failure

Questo non garantisce che il worker muoia per primo in ogni caso possibile, ma rende chiara la tua intenzione. Se il worker di report cresce troppo, è un bersaglio più facile. Se l'API perde uno dei suoi processi, systemd uccide il resto e lo riavvia in modo pulito. Se Redis è solo una cache, potresti scegliere di non proteggerlo pesantemente; se Redis è il tuo archivio dati primario, prenderesti una decisione diversa.

Questo è il motivo per cui la politica OOM dovrebbe essere legata al ruolo del servizio, non al nome del prodotto. "Redis" non è automaticamente critico o sacrificabile. "La cache che possiamo ricostruire" e "l'unica copia dello stato della sessione" sono oggetti operativi diversi.

Testare Senza Creare un Disastro

Non hai bisogno di crashare un server di produzione per sapere se le impostazioni sono applicate. Inizia con l'ispezione:

systemctl show report-worker.service -p OOMScoreAdjust -p OOMPolicy
systemctl status report-worker.service

Poi controlla il processo in esecuzione:

PID=$(systemctl show report-worker.service -p MainPID --value)
cat /proc/$PID/oom_score_adj

Per test più approfonditi, usa un host di staging o una macchina virtuale usa e getta con la stessa versione di systemd e la stessa modalità cgroup. Esegui lì uno strumento controllato di pressione della memoria, non su un box di produzione condiviso. L'obiettivo è confermare il comportamento generale: il worker è più facile da uccidere, il servizio principale non rimane mezzo vivo, e il comportamento di riavvio è visibile nel journal.

Se usi container, testa nella stessa forma in cui deploy. Un servizio eseguito direttamente sotto systemd non si comporta esattamente come un processo all'interno di un container con il proprio limite di memoria. Il kernel potrebbe applicare il limite del container prima che l'host sia globalmente a corto di memoria. In quel caso, il tuo runtime container, Kubernetes o le impostazioni cgroup potrebbero essere il primo strato che decide cosa muore.

Leggere l'Incidente Dopo

Dopo un evento OOM, evita di saltare direttamente a "abbiamo bisogno di più RAM." A volte sì. A volte una cache ha dimenticato i TTL. A volte un deploy ha cambiato la concorrenza dei worker. A volte l'attività di persistenza o backup ha causato un picco di memoria copy-on-write.

Cerca tre cose:

journalctl -k --since "2026-05-24 01:00" | grep -i oom
journalctl -u api.service --since "2026-05-24 01:00"
systemctl show api.service -p Result -p NRestarts

Il log del kernel di solito ti dice quale processo è stato ucciso. Il log dell'unità ti dice come ha reagito systemd. I contatori di riavvio ti dicono se il servizio si è ripreso in modo pulito o ha flappato.

Poi confronta il processo ucciso con la tua priorità intenzionale. Se un servizio protetto è morto prima di un worker sacrificabile, controlla se il worker era effettivamente in esecuzione sotto l'unità che hai ottimizzato, se l'override era stato caricato e se un altro limite di memoria è scattato prima. Se la vittima scelta corrisponde alla politica ma l'incidente ha comunque danneggiato gli utenti, la tua classificazione dei servizi potrebbe dover cambiare.

Documenta la Ragione, Non Solo il Valore

Le impostazioni OOM sono facili da dimenticare perché rimangono silenziose nei drop-in delle unità fino a un brutto giorno. Lascia un breve commento nell'override o nel tuo repository di infrastruttura che spiega la ragione dell'aggiustamento.

[Service]
# Worker di coda riprovabile. Preferisci uccidere questo prima di api.service durante la pressione dell'host.
OOMScoreAdjust=600
OOMPolicy=kill

Quel commento fa risparmiare tempo durante una revisione dell'incidente. Senza di esso, qualcuno potrebbe vedere un punteggio OOM positivo e "correggerlo" a zero senza rendersi conto che era una decisione di priorità intenzionale.

Registra anche quando hai rivisto l'impostazione l'ultima volta. Un servizio può cambiare ruolo nel tempo. Un worker che una volta gestiva miniature sacrificabili potrebbe successivamente elaborare pagamenti, esportazioni o lavori visibili ai clienti. La politica OOM dovrebbe seguire il rischio attuale, non lo scopo originale del servizio.

Configurazioni Errate Comuni

Una configurazione errata è proteggere il database, l'API, il worker, la cache, il log shipper e l'agente di monitoraggio tutti insieme. Sembra attento, ma dà al kernel meno opzioni. Scegli le priorità.

Un'altra configurazione errata è impostare OOMPolicy=continue su un servizio che non può tollerare processi figli mancanti. Un process manager, un web server o un demone personalizzato potrebbe mantenere l'unità attiva anche dopo che parte del carico di lavoro è sparita. Se il tuo load balancer controlla solo se la porta è aperta, il traffico può continuare a fluire verso un servizio degradato.

Una terza configurazione errata è l'aggiustamento positivo senza comportamento di riprova. Se rendi un servizio facile da uccidere, assicurati che ucciderlo sia accettabile. Per un worker di coda, ciò significa che i lavori vengono riconosciuti solo dopo l'elaborazione riuscita. Per un lavoro batch, ciò significa checkpoint. Per un cache warmer, ciò significa che la cache può essere ricostruita in seguito.

Infine, evita di nascondere gli eventi OOM con soli riavvii automatici. Riavviare un servizio che perde memoria può guadagnare tempo, ma può anche creare un ciclo in cui la memoria sale, il servizio muore e gli utenti vedono fallimenti periodici. Aggiungi avvisi sul conteggio dei riavvii e sulla crescita della memoria, non solo sullo stato del processo.

Un Breve Runbook

Quando ottimizzi un server reale, usa una checklist ripetibile:

  1. Elenca i servizi necessari per il recupero e il traffico utente.
  2. Elenca i servizi riprovabili che possono essere uccisi per primi.
  3. Aggiungi valori positivi di OOMScoreAdjust al lavoro sacrificabile.
  4. Aggiungi valori negativi moderati solo ai pochi servizi che meritano protezione.
  5. Usa OOMPolicy=kill per i servizi che non dovrebbero funzionare parzialmente.
  6. Verifica i valori applicati tramite systemctl show e /proc.
  7. Avvisa sulla pressione della memoria prima che si verifichino eventi OOM.

L'obiettivo non è rendere gli eventi OOM innocui. L'obiettivo è renderli comprensibili. OOMScoreAdjust= aiuta a scegliere la vittima. OOMPolicy= aiuta a definire cosa succede al resto dell'unità. Insieme, danno un ordine di fallimento più prevedibile quando la memoria è già esaurita.