Votre service fonctionnait correctement. Puis il a cessé de l’être. Une boucle de redémarrages. Un trou dans les métriques. Quelqu’un suggère « peut-être un déploiement ? »
Vous vérifiez la timeline et ne trouvez rien — si ce n’est la main froide et efficace du tueur OOM.
Sur Ubuntu 24.04, vous pouvez perdre un processus à cause soit du tueur OOM du noyau soit du gestionnaire OOM en espace utilisateur de systemd. Si vous ne prouvez pas lequel l’a fait (et pourquoi), vous « corrigerez » la mauvaise couche et
l’incident reviendra, à l’heure dite, souvent à 02:17.
Ce que « OOM » signifie réellement dans Linux 2025
« Out of memory » sonne comme binaire, comme si le système avait manqué de RAM. En production c’est plutôt un problème d’ordonnancement qui devient un combat sanglant : des requêtes d’allocation arrivent, le reclaim et la compaction peinent,
la logique OOM du noyau ou de l’espace utilisateur décide que le progrès nécessite un sacrifice, et votre service devient ce sacrifice.
Sur Ubuntu 24.04 vous traitez typiquement avec :
- Le tueur OOM du noyau (classique) : déclenché quand le noyau ne peut satisfaire des allocations mémoire et que le reclaim échoue ; il sélectionne une victime selon un score de « badness ».
- Application des limites cgroup v2 : les limites mémoire peuvent déclencher un OOM au sein d’un cgroup même si l’hôte a de la RAM libre ; la victime est choisie à l’intérieur de ce cgroup.
- systemd-oomd : démon en espace utilisateur qui tue de façon proactive des processus ou des tranches (slices) lorsque le PSI (pressure stall information) indique une pression mémoire soutenue, souvent avant que le noyau n’atteigne un OOM strict.
Six à dix faits qui vous rendent meilleur sur le sujet
- Le tueur OOM du noyau est antérieur aux conteneurs ; il a été conçu pour « un grand système », puis adapté aux cgroups où « OOM dans une boîte » est devenu normal.
- PSI (pressure stall information) est relativement récent dans l’histoire de Linux et a changé la donne : on peut mesurer le « temps passé bloqué en attente de mémoire », pas seulement les « octets libres ».
- L’histoire du swap par défaut sur Ubuntu a évolué. Certaines flottes sont passées aux swapfiles par défaut ; d’autres ont désactivé le swap sur des hôtes conteneurisés, ce qui rend les OOM plus nets et plus rapides.
- Le cache de pages compte aussi comme mémoire. La mémoire « libre » peut être faible alors que le système est sain, parce que le cache est récupérable. L’astuce est de savoir quand le reclaim cesse de fonctionner.
- OOM ne concerne pas que les fuites. On peut OOM à cause d’un pic légitime de charge, d’un thundering herd, ou d’une seule requête construisant une grande table en mémoire.
- cgroup v2 a changé la sémantique. Dans une hiérarchie unifiée, memory.current, memory.max et PSI sont des éléments de premier plan. Si vous raisonnez encore en termes cgroup v1, vous vous tromperez de diagnostic.
- systemd-oomd peut tuer « la mauvaise chose » selon votre point de vue car il cible des unités/slices selon la pression et les politiques configurées, pas selon l’impact métier.
- L’overcommit est une politique, pas de la magie. Linux peut promettre plus de mémoire virtuelle qu’il n’en existe ; la facture arrive plus tard, souvent au pic de trafic.
Une citation à garder sous les yeux :
L’espoir n’est pas une stratégie.
— idée paraphrasée souvent entendue en opérations et fiabilité.
Le point est clair : vous avez besoin de preuves, puis de contrôles.
Petite blague #1 : Le tueur OOM, c’est comme la saison des budgets — tout le monde est surpris, et il choisit quand même une victime.
Playbook de diagnostic rapide (première/deuxième/troisième étapes)
Quand un service meurt et que vous suspectez un OOM, vous voulez une certitude rapide, pas une fouille archéologique de deux heures.
Voici l’ordre de triage qui permet de gagner du temps.
Première étape : confirmer un kill et identifier le tueur
- Cherchez des lignes de kernel OOM dans
dmesg/ le journal : « Out of memory », « Killed process ». - Cherchez les actions de systemd-oomd : « Killed … due to memory pressure » dans le journal.
- Vérifiez si le service est dans un cgroup avec memory.max défini : runtime de conteneur, limite d’unité systemd, QoS Kubernetes, etc.
Deuxième étape : confirmer que c’était de la pression mémoire, pas un crash déguisé
- Code de sortie du service : 137 (SIGKILL) est courant pour les kills liés à OOM (surtout dans les conteneurs), mais pas exclusif.
- Vérifiez les compteurs
oom_killdansmemory.eventsdu cgroup. - Corrélez avec les pics PSI mémoire « some/full ».
Troisième étape : localiser la source de la pression
- Était-ce un seul processus qui croissait ? (croissance du RSS, fuite de heap, threads incontrôlés)
- Était-ce plusieurs processus ? (fork bomb, thundering herd, fanout de logs, sidecars)
- Était-ce un échec de reclaim ? (pages dirty, blocage IO, thrashing de swap, fragmentation mémoire)
- Était-ce une limite ? (limite de conteneur trop basse, MemoryMax d’unité, mauvaise classe QoS)
Si vous ne faites qu’une chose : décidez si c’était kernel OOM, cgroup OOM ou
systemd-oomd. La prévention change complètement selon le cas.
Prouvez-le à partir des logs : noyau vs systemd-oomd vs cgroup
Empreintes du tueur OOM du noyau
Le tueur OOM du noyau laisse des traces très distinctives : contexte d’allocation, processus victime, « oom_score_adj »,
et souvent une liste de tâches avec statistiques mémoire. Vous n’avez pas besoin de conjectures ; vous avez juste à lire au bon endroit.
Empreintes de systemd-oomd
systemd-oomd est plus discret mais toujours auditable. Il enregistrera ses décisions et l’unité qu’il a ciblée. Il agit
sur des signaux de pression soutenue et des politiques configurées, pas sur un « échec d’allocation ».
Empreintes d’un OOM dû à une limite cgroup
Si vous êtes dans des conteneurs ou des services systemd avec des limites mémoire, vous pouvez être tué alors que l’hôte est
correct. Vos graphes d’hôte montrent beaucoup de RAM libre. Votre service meurt quand même. C’est une histoire de cgroup.
La preuve se trouve dans memory.events et consorts.
Tâches pratiques (commandes + signification des sorties + décisions)
Ce sont les tâches que j’exécute réellement pendant les incidents. Chacune inclut ce que la sortie signifie et la décision
que vous en tirez. Exécutez-les en root ou avec sudo quand nécessaire.
Tâche 1 : Confirmer que le service est mort par SIGKILL (souvent OOM)
cr0x@server:~$ systemctl status myservice --no-pager
● myservice.service - My Service
Loaded: loaded (/etc/systemd/system/myservice.service; enabled; preset: enabled)
Active: failed (Result: signal) since Mon 2025-12-29 10:41:02 UTC; 3min ago
Main PID: 21477 (code=killed, signal=KILL)
Memory: 0B
CPU: 2min 11.203s
Signification : Le processus principal a été tué avec SIGKILL. L’OOM est un suspect principal car le noyau (et oomd)
utilisent typiquement SIGKILL, mais un opérateur a aussi pu exécuter kill -9.
Décision : Passez aux preuves du journal. Ne « corrigez » rien pour l’instant.
Tâche 2 : Rechercher les lignes kernel OOM autour de l’événement
cr0x@server:~$ journalctl -k --since "2025-12-29 10:35" --until "2025-12-29 10:45" | egrep -i "out of memory|oom|killed process|oom-killer"
Dec 29 10:41:01 server kernel: Out of memory: Killed process 21477 (myservice) total-vm:3281440kB, anon-rss:1512200kB, file-rss:1200kB, shmem-rss:0kB, UID:110 pgtables:4120kB oom_score_adj:0
Dec 29 10:41:01 server kernel: oom_reaper: reaped process 21477 (myservice), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Signification : Il s’agit d’un kill par le noyau OOM. Il nomme la victime et fournit des statistiques mémoire.
Décision : Concentrez-vous sur pourquoi le noyau a atteint l’OOM : exhaustion mémoire globale, swap désactivé, reclaim bloqué, ou pic.
systemd-oomd n’est pas le coupable ici.
Tâche 3 : Si les logs du noyau sont silencieux, recherchez les actions de systemd-oomd
cr0x@server:~$ journalctl --since "2025-12-29 10:35" --until "2025-12-29 10:45" -u systemd-oomd --no-pager
Dec 29 10:40:58 server systemd-oomd[783]: Memory pressure high for /system.slice/myservice.service, killing 1 process(es) in this unit.
Dec 29 10:40:58 server systemd-oomd[783]: Killed /system.slice/myservice.service (myservice), pid=21477, uid=110, total_vm=3281440kB, rss=1513408kB
Signification : systemd-oomd a agi, ciblant une unité systemd parce que la pression mémoire est restée élevée.
Décision : Vous devez inspecter la configuration d’oomd et le PSI, et envisager de modifier les protections ou politiques de l’unité.
Ajouter de la RAM peut aider, mais la politique peut toujours vous tuer tôt.
Tâche 4 : Vérifier si systemd-oomd est activé et actif
cr0x@server:~$ systemctl is-enabled systemd-oomd && systemctl is-active systemd-oomd
enabled
active
Signification : oomd est en jeu sur cet hôte.
Décision : Quand vous voyez SIGKILL sans logs noyau OOM, considérez oomd comme un suspect de première classe.
Tâche 5 : Identifier le chemin cgroup de votre service et vérifier s’il a une limite mémoire
cr0x@server:~$ systemctl show -p ControlGroup -p MemoryMax -p MemoryHigh myservice
ControlGroup=/system.slice/myservice.service
MemoryMax=infinity
MemoryHigh=infinity
Signification : Cette unité n’a pas de limite mémoire explicite systemd. Si vous avez toujours un OOM de cgroup, cela peut venir d’une slice parente,
d’une limite de conteneur, ou d’une autre frontière de contrôleur.
Décision : Inspectez les slices parentes et les fichiers cgroup v2 directement.
Tâche 6 : Lire memory.events du cgroup pour une preuve irréfutable de cgroup OOM
cr0x@server:~$ cgpath=$(systemctl show -p ControlGroup --value myservice); cat /sys/fs/cgroup${cgpath}/memory.events
low 0
high 12
max 3
oom 1
oom_kill 1
Signification : oom_kill 1 est l’arme fumante : un kill est intervenu à cause de la politique mémoire du cgroup.
max 3 indique que le cgroup a frappé sa limite stricte plusieurs fois ; il n’a pas toujours tué, mais il a heurté le plafond.
Décision : Corrigez la limite ou le comportement mémoire. Ne perdez pas de temps sur les graphes d’hôte ; c’est local au cgroup.
Tâche 7 : Inspecter l’usage courant vs la limite au niveau du cgroup
cr0x@server:~$ cat /sys/fs/cgroup${cgpath}/memory.current; cat /sys/fs/cgroup${cgpath}/memory.max
1634328576
2147483648
Signification : Le service utilise ~1.52 GiB avec un plafond à 2 GiB. Si vous voyez des kills avec une utilisation proche du plafond, vous avez un vrai problème de marge.
Décision : Soit augmenter la limite, soit réduire l’empreinte, soit ajouter du retour de pression pour qu’il ne fonce pas dans le mur.
Tâche 8 : Vérifier le PSI mémoire pour voir si l’hôte est en train de se bloquer
cr0x@server:~$ cat /proc/pressure/memory
some avg10=0.48 avg60=0.92 avg300=1.22 total=39203341
full avg10=0.09 avg60=0.20 avg300=0.18 total=8123402
Signification : PSI montre que le système passe un temps mesurable bloqué sur la mémoire. « full » signifie que des tâches sont complètement bloquées en attente de mémoire.
Si « full » n’est pas négligeable, vous ne manquez pas seulement de RAM libre — vous perdez du temps de travail.
Décision : Si le PSI est élevé et soutenu, poursuivez des corrections systémiques : comportement de reclaim, stratégie de swap, façonnage de charge, ou plus de RAM.
Tâche 9 : Confirmer l’état du swap et si vous opérez sans filet
cr0x@server:~$ swapon --show
NAME TYPE SIZE USED PRIO
/swapfile file 8G 512M -2
Signification : Le swap existe et est utilisé. Cela peut acheter du temps et éviter des OOM brutaux, mais cela peut aussi cacher des fuites jusqu’à l’effondrement de latence.
Décision : Si le swap est désactivé sur un hôte générique, envisagez d’activer un modestes swapfile. Si le swap est activé et fortement utilisé,
enquêtez sur la croissance mémoire et les risques de blocage IO.
Tâche 10 : Rechercher les détails de sélection du noyau (badness, contraintes)
cr0x@server:~$ journalctl -k --since "2025-12-29 10:40" --no-pager | egrep -i "oom_score_adj|constraint|MemAvailable|Killed process" | head -n 20
Dec 29 10:41:01 server kernel: myservice invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
Dec 29 10:41:01 server kernel: Constraint: CONSTRAINT_NONE, nodemask=(null), cpuset=/, mems_allowed=0
Dec 29 10:41:01 server kernel: Killed process 21477 (myservice) total-vm:3281440kB, anon-rss:1512200kB, file-rss:1200kB, shmem-rss:0kB, UID:110 pgtables:4120kB oom_score_adj:0
Signification : CONSTRAINT_NONE suggère qu’il s’agit d’une pression mémoire globale (pas contrainte à un cpuset/mems).
Décision : Regardez tout l’hôte : principaux consommateurs mémoire, reclaim, swap et IO. Si vous attendiez une limite cgroup, votre hypothèse est fausse.
Tâche 11 : Identifier les plus gros consommateurs de mémoire au moment de l’événement (ou maintenant, si présents)
cr0x@server:~$ ps -eo pid,ppid,comm,rss,vsz,oom_score_adj --sort=-rss | head -n 12
PID PPID COMMAND RSS VSZ OOM_SCORE_ADJ
30102 1 java 4123456 7258120 0
21477 1 myservice 1512200 3281440 0
9821 1 postgres 812344 1623340 0
1350 1 prometheus 402112 912440 0
Signification : RSS est la mémoire résidente réelle. VSZ est l’espace d’adresses virtuel (souvent trompeur). Si quelque chose écrase votre service,
la victime peut avoir été « malchanceuse » plutôt que « la plus grosse ».
Décision : Décidez s’il faut plafonner ou déplacer le processus lourd, ou protéger votre service via oom_score_adj et politiques d’unité.
Tâche 12 : Vérifier les paramètres d’overcommit mémoire (politique qui change le comportement OOM)
cr0x@server:~$ sysctl vm.overcommit_memory vm.overcommit_ratio
vm.overcommit_memory = 0
vm.overcommit_ratio = 50
Signification : Le mode overcommit 0 est heuristique. Il peut autoriser des allocations qui déclenchent plus tard un OOM sous charge.
Décision : Pour certaines classes de systèmes (bases de données, réservations mémoire), envisagez une politique plus stricte (mode 2).
Pour de nombreux serveurs applicatifs, le mode 0 est acceptable ; concentrez-vous d’abord sur limites et fuites.
Tâche 13 : Vérifier les problèmes de reclaim/IO qui déforment la notion de « mémoire disponible »
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 524288 112344 30240 8123448 0 4 120 320 912 2100 11 4 83 2 0
3 1 524288 80320 30188 8051120 0 64 128 4096 1102 2600 18 6 62 14 0
4 2 524288 60220 30152 7983340 0 256 140 8192 1200 2901 22 7 48 23 0
2 1 524288 93210 30160 8041200 0 32 110 1024 980 2400 14 5 74 7 0
1 0 524288 104220 30180 8100100 0 0 100 512 900 2200 10 4 84 2 0
Signification : Un so élevé (swap out) plus un wa élevé (IO wait) peuvent signaler du thrash de swap ou une contention stockage.
Cela peut pousser le système dans des blocages mémoire puis des décisions OOM.
Décision : Si le IO wait pique, traitez le stockage comme partie intégrante de l’incident mémoire. Corrigez la saturation IO, le comportement des pages dirty,
et envisagez un backing swap plus rapide ou une stratégie différente.
Tâche 14 : Inspecter les protections d’unité systemd qui influencent le choix des kills
cr0x@server:~$ systemctl show myservice -p OOMScoreAdjust -p ManagedOOMMemoryPressure -p ManagedOOMMemoryPressureLimit
OOMScoreAdjust=0
ManagedOOMMemoryPressure=auto
ManagedOOMMemoryPressureLimit=0
Signification : Avec ManagedOOMMemoryPressure=auto, systemd peut opter pour la gestion selon les défauts et la version.
Décision : Pour des services critiques, choisissez explicitement : soit les protéger (désactiver la gestion oomd pour l’unité)
soit appliquer une politique de slice qui tue d’abord le travail moins important.
Tâche 15 : Vérifier si votre service tourne dans un conteneur avec ses propres limites
cr0x@server:~$ cat /proc/21477/cgroup
0::/system.slice/myservice.service
Signification : Cet exemple montre un service systemd directement sur l’hôte. Dans Docker/Kubernetes vous verriez des chemins indiquant
des scopes/slices de conteneur.
Décision : Si le chemin pointe vers des scopes de conteneur, allez au cgroup du conteneur et lisez memory.max et memory.events là-bas.
Tâche 16 : Si vous avez un service cœur métier, définissez un ajustement d’OOM score délibéré
cr0x@server:~$ sudo systemctl edit myservice
# (editor opens)
# Add:
# [Service]
# OOMScoreAdjust=-500
cr0x@server:~$ sudo systemctl daemon-reload && sudo systemctl restart myservice
Signification : Des valeurs plus faibles (plus négatives) rendent le noyau moins susceptible de choisir le processus comme victime OOM.
Cela n’empêche pas l’OOM ; cela change qui se fait tirer dessus.
Décision : N’utilisez ceci que si vous avez aussi un plan pour ce qui doit mourir en remplacement (jobs batch, caches, workers best-effort).
Sinon vous déplacez simplement la zone d’impact ailleurs.
Pourquoi c’est arrivé : modes de défaillance probables
1) L’illusion « l’hôte a de la mémoire » (OOM dû à cgroup)
L’hôte peut avoir des gigaoctets libres et votre service être tué si son cgroup atteint memory.max.
C’est courant sur des nœuds Kubernetes, hôtes Docker, et services systemd où quelqu’un a défini MemoryMax
des mois plus tôt et l’a oublié.
La preuve n’est pas dans free -h. Elle est dans memory.events pour ce cgroup.
2) La politique « pas de swap pour la performance » (OOM global brutal)
Désactiver le swap peut être valable pour certains environnements sensibles à la latence, mais cela échange une dégradation graduelle contre une mort soudaine.
Si vous le faites, vous devez avoir des limites strictes, un contrôle d’admission et une excellente planification de capacité. Beaucoup d’équipes n’ont rien de tout cela.
3) Une fuite qui n’apparaît qu’en charge réelle
Le classique. Un cache sans bornes. Une explosion d’étiquettes métriques. Un chemin de requête qui retient des références.
Tout semble bien en staging parce que le staging ne subit pas la diversité réelle des utilisateurs.
4) Le reclaim ne fonctionne pas parce que le stockage est le goulot caché
Le reclaim mémoire dépend souvent de l’IO : écriture de pages dirty, swap out, relecture. Quand le stockage est saturé,
la pression mémoire devient des blocages puis des kills.
Règle SRE : si les incidents mémoire se corrèlent avec IO wait, vous n’avez pas un « problème mémoire ». Vous avez un problème système.
5) systemd-oomd fait ce que vous avez demandé (ou ce que « auto » a décidé)
oomd est conçu pour tuer plus tôt que le noyau, afin d’éviter que tout le système devienne inutilisable.
C’est une bonne chose. C’est aussi surprenant quand il tue une unité que vous croyiez protégée.
Si vous exécutez des nœuds multi-tenant, oomd peut être votre ami. Si vous exécutez des nœuds à usage unique, il peut être du bruit à moins d’être configuré.
Petite blague #2 : Désactiver le swap pour « éviter la latence » c’est comme enlever l’alarme incendie pour « éviter le bruit ».
Trois mini-récits d’entreprise (anonymisés, plausibles, techniquement exacts)
Mini-récit 1 : Incident causé par une fausse hypothèse
Une SaaS de taille moyenne exécutait une API de reporting sur une pool d’hôtes Ubuntu. L’on-call a vu une série de redémarrages et a fait la chose habituelle :
vérifier les graphes mémoire de l’hôte. Tout avait l’air correct — beaucoup de RAM libre, pas d’utilisation swap, pas de preuve évidente. L’équipe a blâmé
des « crashes aléatoires » et a rollbacké une mise à jour de bibliothèque inoffensive.
Les redémarrages ont continué. Un ingénieur senior a finalement extrait memory.events pour l’unité systemd et trouvé
oom_kill qui augmentait. Le service avait un MemoryMax hérité d’une slice parente utilisée pour les « apps non critiques ».
Personne ne se souvenait de l’avoir défini car c’était fait pendant une période d’économies, et cela vivait dans un dépôt d’infrastructure
que l’équipe API ne consultait jamais.
La fausse hypothèse était simple : « Si l’hôte a de la RAM libre, cela ne peut pas être un OOM. » Dans un monde cgroup, cette hypothèse est morte.
L’hôte allait bien ; le service était enfermé.
La correction n’était pas dramatique : augmenter la limite mémoire de l’unité pour correspondre aux pics réels, ajouter une alerte sur memory.events du cgroup pour high/max avant oom_kill, et documenter la politique de slice.
Le postmortem n’a pas blâmé le noyau. Il a blâmé l’absence de propriété et les contraintes invisibles.
Mini-récit 2 : Une optimisation qui s’est retournée contre l’équipe
Une plateforme de paiements voulait réduire le p99. Quelqu’un a proposé de désactiver le swap sur les nœuds applicatifs parce que « swap est lent ».
Ils ont aussi ajusté la JVM pour utiliser un heap plus gros afin de réduire la fréquence de GC. La première semaine fut belle — dashboards propres et latence tail légèrement meilleure.
Puis une campagne de trafic a frappé. L’utilisation mémoire a augmenté à mesure que les caches chauffaient et que la concurrence de requêtes grimpait. Sans swap,
il n’y avait pas de tampon pour les pics transitoires. Le noyau a atteint l’OOM rapidement et a tué des workers Java au hasard. Certains nœuds ont survécu,
d’autres non, créant des charges inégales et des tempêtes de retry.
L’équipe a tenté de réparer en augmentant encore les tailles de heap, ce qui a empiré la situation : le heap a mangé le cache fichier et
réduit les options de reclaim. Quand le système a eu des ennuis, il avait moins de ressources où se rabattre.
La correction finale fut ennuyeuse mais très efficace : réactiver un swapfile modeste, réduire le heap à une fraction plus sûre de la RAM, et appliquer des limites mémoire par worker via des slices systemd pour qu’un seul worker ne puisse pas consommer tout le nœud.
Ils ont gardé les gains de latence en réduisant les pics de concurrence au lieu de supprimer le filet de sécurité du système.
Mini-récit 3 : Une pratique ennuyeuse mais correcte qui a sauvé la mise
Un service d’ingestion de données traitait de gros fichiers clients. Ce n’était pas glamour : surtout du streaming IO, un peu de décompression,
un peu de parsing. L’équipe plateforme avait une politique stricte : chaque unité de service doit déclarer des attentes mémoire avec
MemoryHigh et MemoryMax, et doit émettre un indicateur « RSS courant ». Les équipes se plaignaient que c’était bureaucratique.
Un soir, un nouveau client a envoyé un fichier malformé qui a déclenché un comportement pathologique dans une bibliothèque de parsing.
Le RSS a augmenté progressivement. Le service n’a pas planté immédiatement ; il continuait juste à demander de la mémoire. Sur un hôte sans limites,
il aurait entraîné tout le reste.
À la place, MemoryHigh a provoqué des signaux de throttling tôt (la pression de reclaim a augmenté, la performance s’est dégradée mais est restée fonctionnelle),
et MemoryMax a empêché l’épuisement total du nœud. Le worker d’ingestion a été tué à l’intérieur de son propre cgroup,
sans emporter les sidecars base de données ou les node exporters.
L’on-call a vu l’alerte : memory.events high qui augmentait. Ils ont pu la corréler à un job client unique,
le mettre en quarantaine, et expédier un correctif de parser le lendemain. La politique ennuyeuse a transformé un incident global en un job échoué isolé.
Personne n’a applaudi. C’est justement le but.
Prévention efficace : limites, swap, tuning et discipline d’exécution
Décidez quelle couche doit échouer en premier
Vous ne prévenez pas l’épuisement mémoire en espérant que le noyau « gère ». Vous le prévenez en concevant un ordre d’échec :
quelles charges de travail sont compressées, lesquelles sont tuées, et lesquelles sont protégées.
- Protéger : bases de données, services de coordination, agents nodaux qui gardent l’hôte gérable.
- Best-effort : jobs batch, caches reconstruisables, workers asynchrones qui peuvent réessayer.
- Jamais sans bornes : tout ce qui peut se fan-out avec une entrée utilisateur (regex, décompression, parsing JSON, couches de cache).
Utilisez les limites systemd intentionnellement (MemoryHigh + MemoryMax)
MemoryMax est un mur dur. Utile, mais brutal : le frapper peut entraîner des kills. MemoryHigh est un seuil de pression :
il déclenche reclaim et throttling et vous donne une chance de récupérer avant la mort.
Un schéma pragmatique pour les services longue durée :
- Fixer
MemoryHighà un niveau où la dégradation de performance est acceptable mais où les alertes sont fortes. - Fixer
MemoryMaxassez haut pour autoriser les pics connus, mais assez bas pour protéger l’hôte.
cr0x@server:~$ sudo systemctl edit myservice
# Add:
# [Service]
# MemoryHigh=3G
# MemoryMax=4G
cr0x@server:~$ sudo systemctl daemon-reload && sudo systemctl restart myservice
Configurez oomd plutôt que de faire comme s’il n’existait pas
Si systemd-oomd est activé, vous avez besoin d’une politique explicite. « auto » n’est pas une politique ; c’est une valeur par défaut qui finira par vous surprendre à la pire heure.
Approches typiques :
- Nœuds multi-tenant : gardez oomd, utilisez des slices, mettez les workloads best-effort dans une slice qu’oomd pourra tuer en premier.
- Nœuds à usage unique : envisagez de désactiver la gestion oomd pour l’unité critique si elle se fait tuer prématurément, mais laissez le noyau OOM comme dernier recours.
Swap : choisissez une stratégie et assumez-la
Le swap n’est pas « mauvais ». Le swap non géré est mauvais. Si vous activez le swap, surveillez les taux swap-in/out et le IO wait. Si vous désactivez le swap,
acceptez que les OOM seront soudains et fréquents sauf si vous avez des limites strictes et des workloads prévisibles.
Stoppez les pics mémoire à la frontière applicative
La façon la plus rapide d’éviter un OOM est d’arrêter d’accepter du travail qui conduit à une croissance mémoire non bornée.
Habitudes de production :
- Borner les caches (taille + TTL) et mesurer les taux d’éviction.
- Limiter la concurrence. La plupart des services n’ont pas besoin de « autant de threads que possible », ils ont besoin de « autant que vous pouvez garder en cache et en RAM ».
- Limiter les tailles de payload et appliquer le parsing en streaming.
- Utiliser le backpressure : files limitées, load shedding, coupe-circuits.
Connaître la différence entre RSS et « ça a l’air gros »
Les ingénieurs aiment blâmer des « fuites mémoire » en se basant sur VSZ. C’est ainsi que l’on finit par « corriger » des réservations mmap qui n’ont jamais été résidentes.
Utilisez RSS, PSS (si possible), et cgroup memory.current. Et corrélez avec la pression (PSI), pas seulement des octets.
Note pour les ingénieurs stockage : les incidents mémoire sont souvent des incidents IO déguisés
Si votre système reclaim, swappe ou écrit des pages dirty sous pression, la latence stockage devient partie de votre histoire mémoire.
Un disque lent ou saturé peut transformer une « pression récupérable » en « tuer quelque chose maintenant ».
Si vous voyez un IO wait élevé pendant la pression, corrigez le chemin stockage : profondeur de file, volumes voisins bruyants, throttling, paramètres de writeback,
et le backing du swap.
Erreurs courantes : symptôme → cause racine → correctif
1) Symptom : « Le service a reçu SIGKILL, mais rien dans dmesg »
Cause racine : systemd-oomd l’a tué d’après le PSI, ou le kill est intervenu à l’intérieur d’un conteneur/cgroup et vous regardez le mauvais périmètre de journal.
Fix : Vérifiez journalctl -u systemd-oomd et memory.events de l’unité. Confirmez le chemin cgroup et lisez les bons fichiers.
2) Symptom : « L’hôte montre 30% de RAM libre, mais le conteneur OOMe quand même »
Cause racine : memory.max du cgroup est trop bas (limite du conteneur), ou les pics mémoire dépassent la marge disponible.
Fix : Inspectez memory.max et memory.events dans le cgroup du conteneur. Augmentez la limite ou réduisez les pics.
3) Symptom : « Tout ralentit pendant des minutes, puis un processus meurt »
Cause racine : Thrashement reclaim et swap ; IO wait rend les blocages mémoire insupportables.
Fix : Vérifiez vmstat pour swap/wa. Réduisez la pression dirty writeback, améliorez les performances stockage, envisagez une stratégie de swap raisonnable,
et plafonnez les plus gros coupables.
4) Symptom : « OOM tue un petit processus, pas le gros consommateur »
Cause racine : scoring OOM, différences oom_score_adj, considérations de mémoire partagée, ou le gros est protégé par une politique.
Fix : Inspectez les logs pour oom_score_adj ; définissez volontairement OOMScoreAdjust pour les services critiques et contraignez le travail best-effort
avec des limites cgroup pour que le noyau ait de meilleurs choix.
5) Symptom : « L’OOM arrive juste après un déploiement, mais pas tout le temps »
Cause racine : Caches froids + concurrence accrue + nouveaux chemins d’allocation. Courant aussi : un nouveau chemin code alloue selon l’entrée utilisateur.
Fix : Ajoutez des tests canary qui simulent cold start, bornez les caches, limitez la taille des requêtes, et fixez MemoryHigh pour faire remonter la pression tôt.
6) Symptom : « Nous avons ajouté de la RAM, l’OOM arrive encore »
Cause racine : La limite est dans un cgroup, pas au niveau hôte. Ou une fuite s’adapte pour remplir ce que vous achetez.
Fix : Montrez où se situe la limite (memory.max). Puis mesurez la pente de croissance dans le temps pour confirmer fuite vs charge.
Listes de vérification / plan pas-à-pas
Checklist incident (15 minutes, sans héroïsme)
- Obtenez l’heure exacte de la mort et le signal :
systemctl statusou le statut du runtime de conteneur. - Vérifiez le log noyau autour de cette heure pour « Killed process ».
- Si le noyau est silencieux, vérifiez le journal
systemd-oomd. - Lisez
memory.eventspour le cgroup du service et sa slice parente. - Capturez le PSI mémoire :
/proc/pressure/memory(hôte) et le PSI cgroup si pertinent. - Capturez les plus gros consommateurs RSS et leurs oom_score_adj.
- Vérifiez l’état du swap et
vmstatpour l’interaction swap/IO wait. - Notez : kernel OOM vs oomd vs OOM limite cgroup. Ne laissez pas l’ambiguïté.
Plan de stabilisation (même jour)
- Si OOM cgroup : augmentez
MemoryMax(ou la limite du conteneur) pour arrêter l’hémorragie. - Si OOM global noyau : ajoutez du swap si approprié, réduisez la concurrence, et plafonnez temporairement les workloads non critiques.
- Si kill oomd : ajustez la politique ManagedOOM ou placez les workloads best-effort dans une slice tuable.
- Configurez des alertes sur
memory.events« high » et PSI « full » pour détecter la pression avant les kills. - Protégez les unités critiques avec
OOMScoreAdjust, mais seulement après avoir prévu une classe de victimes sûre.
Plan de prévention (cette semaine)
- Définissez des budgets mémoire par service : RSS steady-state attendu et pic worst-case.
- Fixez
MemoryHighetMemoryMaxen conséquence ; documentez la propriété. - Ajoutez des garde-fous applicatifs : limites de taille de requête, caches bornés, plafonds de concurrence.
- Instrumentez la mémoire : jauges RSS, métriques de heap si applicable, et contrôles périodiques de fuite.
- Réalisez un test de charge contrôlé simulant cold start + pic de concurrence.
- Revoyez swap et chemin IO : assurez-vous que le reclaim a un endroit où aller sans faire fondre le stockage.
FAQ
1) Comment distinguer rapidement un kernel OOM d’un systemd-oomd ?
Le kernel OOM affiche « Out of memory » et « Killed process » dans journalctl -k. systemd-oomd affiche ses décisions de kill dans
journalctl -u systemd-oomd. Si les deux sont silencieux, vérifiez memory.events du cgroup pour oom_kill.
2) Que signifie le code de sortie 137 ?
Cela signifie généralement que le processus a reçu SIGKILL (128 + 9). L’OOM est une raison courante, surtout dans les conteneurs, mais un opérateur ou un watchdog peut aussi envoyer SIGKILL.
Corrélez toujours avec les logs et les événements cgroup.
3) Pourquoi le tueur OOM a-t-il choisi mon service important ?
Le noyau choisit selon le score de badness et les contraintes. Si tout est critique et sans bornes, quelque chose de critique mourra.
Utilisez OOMScoreAdjust pour protéger des unités clés, mais créez aussi des classes tuables (batch, cache) avec des limites.
4) Puis-je simplement désactiver systemd-oomd ?
Vous pouvez, mais ne le faites pas par réflexe. Sur des nœuds multi-tenant, oomd peut empêcher l’effondrement total de l’hôte en agissant tôt.
Si vous le désactivez, soyez sûr que vos limites cgroup et vos contrôles de workload empêchent la pression globale.
5) Quelle est la différence entre MemoryHigh et MemoryMax ?
MemoryHigh applique une pression de reclaim et du throttling lorsqu’il est dépassé — un avertissement précoce et un contrôle souple.
MemoryMax est une limite stricte ; la dépasser peut déclencher des kills OOM à l’intérieur du cgroup.
6) Pourquoi la mémoire « free » semble faible alors que le système va bien ?
Linux utilise la RAM pour le page cache. Une faible valeur « free » n’est pas intrinsèquement mauvaise. Regardez la mémoire « available » et, mieux, la pression PSI
pour savoir si le système se bloque.
7) Le swap est-il toujours recommandé sur les serveurs Ubuntu 24.04 ?
Pas toujours. Le swap peut réduire la fréquence des kills OOM durs, mais peut augmenter la latence sous pression.
Pour des hôtes généralistes, un swapfile modeste est souvent un gain net. Pour des workloads basse-latence stricts, vous pouvez désactiver le swap —
mais alors vous devez appliquer des limites mémoire strictes et un contrôle d’admission.
8) Comment prouver un OOM de conteneur vs un OOM hôte ?
Vérifiez le cgroup du conteneur : memory.events avec oom_kill indique un OOM cgroup.
Un OOM hôte affichera des entrées noyau « Killed process » et affectera souvent plusieurs services.
9) Quel est le meilleur indicateur d’alerte précoce ?
Le PSI mémoire (/proc/pressure/memory) combiné à la montée de memory.events high du cgroup. Les octets vous disent « combien ».
La pression vous dit « à quel point c’est grave ».
Prochaines étapes (ce qu’il faut faire avant la prochaine page)
Ne traitez pas l’OOM comme la météo. C’est de l’ingénierie. Le gain immédiat est de prouver le tueur : kernel OOM, cgroup OOM, ou systemd-oomd.
Une fois que vous savez cela, les correctifs cessent d’être de la superstition.
Faites ces trois choses cette semaine :
- Ajoutez une alerte sur
memory.events(highetoom_kill) pour vos services principaux. - Définissez explicitement des budgets
MemoryHigh/MemoryMaxpour les services importants, et placez le travail best-effort dans une slice tuable. - Décidez de votre stratégie swap et surveillez-la — car « nous avons désactivé le swap une fois » n’est pas un plan, c’est une rumeur.
La prochaine fois qu’un service disparaîtra, vous devriez pouvoir répondre « qui l’a tué ? » en moins de cinq minutes.
Ensuite vous pourrez passer le reste du temps à empêcher la répétition, au lieu de discuter à partir d’une capture d’écran de dashboard.