Linux : diagnostiquer un service tué par l’OOM killer sans désactiver la protection mémoire
Un service disparaît, systemd le relance, puis tout semble revenir à la normale. Quelques heures plus tard, il retombe. Si le journal contient « Out of memory », « Killed process » ou « oom-kill », le noyau a probablement sacrifié un processus pour que la machine continue de fonctionner.
Le mauvais réflexe consiste à désactiver l’OOM killer ou à ajouter de la mémoire au hasard. Avant de corriger, il faut savoir où la mémoire a manqué : sur l’hôte entier, dans un service limité par systemd ou à l’intérieur d’un conteneur. Ce ne sont pas les mêmes incidents, même si le résultat visible est identique.
L’OOM killer n’est pas la panne : c’est le dernier garde-fou
Quand Linux ne parvient plus à satisfaire une allocation mémoire après les tentatives de récupération, il choisit un processus à terminer afin de libérer des ressources. La sélection tient notamment compte de la consommation et du score OOM du processus. Dans un cgroup v2, une limite memory.max peut aussi déclencher un OOM local : le noyau tue alors un processus de ce groupe sans que toute la machine soit nécessairement à court de RAM.
Il faut également distinguer l’OOM du noyau de systemd-oomd. Ce dernier agit en espace utilisateur et peut intervenir plus tôt à partir de la pression mémoire et de la consommation de swap. Les journaux et l’unité tuée permettent généralement d’identifier lequel a agi.
1. Établir la chronologie avant de redémarrer partout
Notez l’heure exacte de la disparition du service, puis regardez son état et ses derniers messages :
systemctl status mon-service.service
journalctl -u mon-service.service --since "30 minutes ago"
Un code de sortie 137 signifie généralement que le processus a reçu SIGKILL, mais il ne prouve pas à lui seul un OOM. Un administrateur, un orchestrateur ou un autre mécanisme peut aussi envoyer ce signal. Il faut donc chercher la confirmation côté noyau ou gestionnaire de mémoire.
2. Chercher la preuve dans le journal du noyau
journalctl -k -b --since "2 hours ago" |
grep -Ei 'out of memory|oom-kill|killed process'
# Si l’incident a précédé le dernier redémarrage
journalctl -k -b -1 |
grep -Ei 'out of memory|oom-kill|killed process'
Les lignes utiles donnent souvent le nom et le PID du processus tué, sa mémoire anonyme, son cgroup et le contexte de l’allocation. Conservez plusieurs lignes autour de l’événement : isoler uniquement « Killed process » peut faire perdre l’information qui indique s’il s’agit d’un OOM global ou d’une limite de groupe.
Si systemd-oomd est actif, consultez aussi son journal :
systemctl status systemd-oomd
journalctl -u systemd-oomd --since "2 hours ago"
oomctl
3. Vérifier les limites réelles du service
Une machine peut afficher plusieurs gigaoctets disponibles et tuer malgré tout un service enfermé dans un cgroup trop petit. Pour une unité systemd :
systemctl show mon-service.service -p Result -p ExecMainCode -p ExecMainStatus -p MemoryCurrent -p MemoryPeak -p MemoryHigh -p MemoryMax -p ControlGroup
Avec cgroup v2, MemoryHigh correspond à un seuil de pression : le service est ralenti et poussé à récupérer de la mémoire, mais ce seuil ne déclenche pas directement l’OOM killer. MemoryMax est la limite dure. Si elle est atteinte et que la récupération échoue, un OOM peut être déclenché dans le groupe.
Si le cgroup existe encore, ses compteurs donnent une preuve supplémentaire :
CG=$(systemctl show -p ControlGroup --value mon-service.service)
cat "/sys/fs/cgroup$CG/memory.current"
cat "/sys/fs/cgroup$CG/memory.max"
cat "/sys/fs/cgroup$CG/memory.events"
Dans memory.events, oom compte les situations où la limite a été atteinte et où une allocation allait échouer. oom_kill compte les processus du cgroup effectivement tués par un mécanisme OOM. Attention : ces compteurs peuvent disparaître si le cgroup est détruit et recréé. Une collecte régulière reste plus fiable qu’une inspection longtemps après l’incident.
4. Dans Docker, contrôler le conteneur et l’hôte
Un conteneur peut atteindre sa propre limite, ou être victime d’un manque de mémoire global sur l’hôte. Vérifiez son état, sa limite configurée et sa consommation :
docker inspect --format 'oom={{.State.OOMKilled}} exit={{.State.ExitCode}} limit={{.HostConfig.Memory}}' mon-conteneur
docker stats --no-stream mon-conteneur
OOMKilled=true est une indication forte, mais ne remplace pas le journal du noyau et les compteurs cgroup. Vérifiez aussi la configuration Compose ou la commande de lancement : une limite ancienne peut être devenue trop faible après une évolution de l’application ou une hausse de la concurrence.
5. Mesurer la pression, pas seulement la RAM libre
Une photographie prise après le redémarrage du service peut sembler normale. Il faut observer ce qui se passe avant la chute :
free -h
swapon --show
vmstat 1
cat /proc/pressure/memory
PSI, exposé dans /proc/pressure/memory, mesure le temps pendant lequel des tâches sont bloquées par le manque de mémoire. La ligne some indique qu’au moins certaines tâches attendent ; full signale que toutes les tâches non inactives sont bloquées simultanément. Une valeur ponctuelle ne suffit pas : ce sont l’évolution et les pointes avant l’incident qui permettent de repérer le thrashing.
Les causes à départager
- Fuite mémoire : la consommation augmente continuellement à charge comparable.
- Pic de concurrence : chaque requête reste raisonnable, mais trop de travaux s’exécutent en même temps.
- Cache ou file interne sans borne : l’application conserve plus d’objets qu’elle ne peut en évacuer.
- Limite cgroup trop basse : le service est correctement dimensionné pour l’hôte, mais pas pour la limite qui lui est attribuée.
- Pression globale : plusieurs services acceptables séparément épuisent ensemble la machine.
- Swap absente ou saturée : la machine perd un tampon possible lors d’un pic, sans que la swap puisse corriger une fuite durable.
Corriger sans déplacer le problème
Commencez par la cause : corriger la fuite, borner les caches, réduire la concurrence ou planifier les traitements lourds. Ensuite seulement, ajustez les ressources. Pour un service contrôlé par cgroup, un seuil MemoryHigh surveillé peut révéler la pression avant la limite dure MemoryMax. Dimensionnez ces valeurs à partir de mesures de pointe et gardez une marge explicite.
Ajouter de la swap peut amortir un pic sur un stockage adapté, mais cela augmente la latence et ne soigne pas une croissance sans limite. Ajouter de la RAM ou relever MemoryMax est pertinent si la charge utile a réellement grandi ; sinon, cela ne fait souvent que retarder le prochain incident.
Ce qu’il vaut mieux éviter
- désactiver globalement l’OOM killer ou protéger tous les services avec un score extrême ;
- activer
panic_on_oomsur un serveur ordinaire sans stratégie de cluster et de collecte de crash ; - utiliser
--oom-kill-disablepour masquer un conteneur mal dimensionné ; - vider périodiquement les caches du noyau avec une tâche cron ;
- redémarrer automatiquement sans conserver journaux, compteurs et métriques de l’incident.
Docker déconseille explicitement de contourner les protections OOM avec des réglages extrêmes. Le rôle du mécanisme est de contenir les dégâts. La bonne réponse consiste à instrumenter la pression mémoire, fixer des limites cohérentes et traiter la consommation qui les déclenche.
Conclusion
Un service tué par OOM ne signifie pas automatiquement « il faut plus de RAM ». La preuve se construit en croisant le journal du noyau, l’état systemd, les compteurs cgroup, les limites du conteneur et la pression PSI. Une fois le périmètre identifié, la correction devient beaucoup plus nette : réduire une consommation, lisser un pic, ajuster une limite ou renforcer l’hôte.
Si l’incident touche une machine ou un service en production et que la chronologie reste floue, FuretSplick peut intervenir dans le cadre d’un diagnostic et d’une remise en service, sans commencer par modifier les limites au hasard.
