La page sonne à 03:12. « Latence API en hausse, nœuds en swapping, un container tué par OOM. » Vous vous connectez et voyez le familier : la mémoire « utilisée » est élevée, les dirigeants sont réveillés, et quelqu’un a déjà suggéré « ajoutez juste de la RAM ».
Les fuites mémoire sont le pire type d’incident en lent mouvement : tout fonctionne… jusqu’à ce que ça ne fonctionne plus, et ça casse au pire moment. L’astuce sur Debian 13 n’est pas l’héroïsme. C’est collecter des preuves sans transformer la production en laboratoire, faire de petits changements réversibles, et toujours séparer les vraies fuites du comportement mémoire attendu.
Faits intéressants et contexte (rapide, concret)
- Linux ne « libère » pas la mémoire de la façon dont on le voudrait. Le noyau utilise agressivement la RAM pour le page cache ;
MemAvailableest la métrique qui compte plus queMemFree. - Les cgroups ont changé la donne à deux reprises. cgroups v1 permettait une configuration par contrôleur ; v2 unifie cela, et systemd en fait le plan de contrôle par défaut pour les services.
- /proc est plus ancien que la plupart des runbooks d’incident. C’est l’interface canonique pour les statistiques de processus depuis les débuts de Linux, et elle reste la meilleure pour une visibilité à faible perturbation.
- « OOM killer » n’est pas un seul type d’événement. Il y a l’OOM système, l’OOM cgroup, et le « on est mort parce que malloc a échoué » côté utilisateur. Ils se ressemblent dans les tableaux de bord et sont très différents dans les logs.
- Le comportement de malloc est une politique, pas de la physique. L’allocateur de glibc utilise des arenas et peut ne pas rendre la mémoire à l’OS rapidement ; cela ressemble souvent à une fuite alors que ce n’en est pas une.
- Overcommit est une fonctionnalité. Linux peut promettre plus de mémoire virtuelle qu’il n’en existe ; c’est normal jusqu’à ce que ça ne le soit plus. Le mode d’échec dépend des réglages d’overcommit et de la charge.
- eBPF a rendu « observer sans arrêter » courant. Vous pouvez tracer les allocations et les fautes avec beaucoup moins de pénalité de performance que les anciennes approches lourdes basées sur ptrace.
- Java a inventé de nouvelles catégories de « fuite ». Les fuites de heap sont une chose ; le suivi de la mémoire native existe parce que les direct buffers, les stacks de thread et le JNI peuvent croître silencieusement.
- Smaps est le héros méconnu. /proc/<pid>/smaps est verbeux, assez coûteux à lire à grande échelle, et absolument décisif quand vous devez savoir ce qu’est réellement la mémoire.
Ce que « fuite mémoire » signifie vraiment sous Linux
En production, « fuite mémoire » est utilisé pour quatre problèmes différents. Un seul est une fuite classique.
Si vous ne nommez pas correctement le problème, vous « corrigerez » la mauvaise chose et vous vous sentirez productif pendant que la page continue de s’empirer.
1) Vraie fuite : la mémoire devient inatteignable et n’est jamais récupérée
C’est le manuel : les allocations continuent, les frees ne suivent pas, et l’ensemble vivant croît sans limite.
Vous verrez une montée monotone du RSS ou de l’usage du heap qui ne corrèle pas avec la charge.
2) Mémoire retenue : accessible, mais conservée involontairement
Caches sans éviction, maps non bornées indexées par des IDs d’utilisateurs, contextes de requêtes stockés globalement. Techniquement pas « fuité » parce que le programme peut encore y accéder, mais pratiquement le même résultat.
3) Comportement de l’allocateur : mémoire libérée en interne mais pas rendue à l’OS
malloc de glibc, fragmentation, arenas par thread et effets de « haut niveau d’utilisation » peuvent garder le RSS élevé même après le pic de charge.
L’application peut être saine. Vos graphiques l’accuseront quand même.
4) Pression sur le noyau/page cache : la RAM est utilisée, mais pas par votre processus
« La mémoire utilisée » monte parce que le noyau met en cache des pages de fichiers. Sous pression, elle devrait baisser.
Si elle ne baisse pas, vous pouvez avoir de la congestion de pages sales, un IO lent, ou des règles de reclaim de cgroup qui rendent le cache collant.
Le travail consiste à déterminer dans quelle catégorie vous vous trouvez avec le plus petit rayon d’impact. Déboguer par panique coûte cher.
Une idée paraphrasée de Werner Vogels (CTO d’Amazon) : Tout finit par échouer ; concevez des systèmes et des habitudes qui rendent les échecs survivables.
Playbook de diagnostic rapide (premier/deuxième/troisième)
Premier : confirmez que c’est un problème de processus, pas « Linux qui fait Linux »
- Regardez MemAvailable, l’activité de swap et les fautes majeures. Si
MemAvailableest sain et que le swap est calme, vous n’avez probablement pas de fuite urgente. - Identifiez les principaux consommateurs de RSS et si la croissance est monotone.
- Vérifiez si la mémoire est dominée par anon (heap) ou file (cache, mmaps).
Deuxième : décidez si vous êtes à la frontière d’un cgroup/systemd
- Le service est-il contraint par la propriété systemd
MemoryMax? Si oui, les fuites se manifesteront comme un cgroup OOM, pas un OOM système global. - Collectez
memory.current,memory.eventset le RSS par processus sous le cgroup de l’unité.
Troisième : choisissez la source de preuve la moins perturbatrice
- /proc/<pid>/smaps_rollup pour une vue rapide PSS/RSS/Swap.
- statistiques cgroup v2 pour le suivi au niveau du service et les événements OOM.
- profileurs natifs au langage (pprof, JVM NMT, Python tracemalloc) si vous pouvez les activer sans redémarrage.
- échantillonnage eBPF quand vous ne pouvez pas toucher l’app mais avez besoin d’attribution.
Blague #1 : Les fuites mémoire sont comme les snacks de bureau — petites au début, puis soudain tout disparaît et personne n’admet rien.
Tâches pratiques : commandes, signification des sorties et décisions (12+)
Celles-ci sont adaptées à la production. La plupart sont en lecture seule. Quelques-unes modifient la configuration, et celles-là sont signalées avec la décision que vous prenez.
Utilisez-les dans l’ordre ; ne sautez pas aux « outils cool » avant d’avoir accumulé des preuves de base.
Tâche 1 : Vérifier si le noyau est réellement sous pression mémoire
cr0x@server:~$ grep -E 'Mem(Total|Free|Available)|Swap(Total|Free)' /proc/meminfo
MemTotal: 65843064 kB
MemFree: 1234560 kB
MemAvailable: 18432000 kB
SwapTotal: 4194300 kB
SwapFree: 4096000 kB
Ce que cela signifie : MemFree est bas (normal), MemAvailable est encore ~18 GB (bon), swap majoritairement libre (bon).
Décision : Si MemAvailable est sain et que le swap ne décroît pas, vous n’avez probablement pas une fuite aiguë. Passez à la confirmation au niveau processus avant de réveiller tout le monde.
Tâche 2 : Chercher un swapping actif et un churn de reclaim
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 0 1234560 81234 21000000 0 0 5 12 820 1600 12 4 83 1 0
1 0 0 1200000 80000 21100000 0 0 0 8 790 1550 11 3 85 1 0
3 1 0 1100000 79000 21200000 0 0 0 120 900 1700 15 6 74 5 0
5 2 0 900000 78000 21300000 0 0 0 800 1200 2200 18 8 60 14 0
4 2 0 850000 77000 21350000 0 0 0 900 1180 2100 16 7 62 13 0
Ce que cela signifie : si/so sont à zéro (pas d’entrée/sortie de swap), mais wa (IO wait) augmente, et b (bloqué) est non nul.
Décision : Si le swapping est actif (si/so > 0 de façon soutenue), vous êtes en territoire d’urgence. Sinon, le problème peut venir de blocages IO ou d’un seul processus qui gonfle sans encore forcer le swap.
Tâche 3 : Identifier les principaux consommateurs de RSS (triage rapide)
cr0x@server:~$ ps -eo pid,ppid,comm,%mem,rss --sort=-rss | head -n 10
PID PPID COMMAND %MEM RSS
4123 1 api-service 18.4 12288000
2877 1 search-worker 12.1 8050000
1990 1 postgres 9.7 6450000
911 1 nginx 1.2 780000
Ce que cela signifie : api-service est le principal consommateur résident.
Décision : Choisissez le principal suspect et restez concentré. Si plusieurs processus croissent ensemble, suspectez une pression de cache partagée, une croissance de tmpfs ou un changement de charge.
Tâche 4 : Confirmer une croissance monotone (ne vous fiez pas à un seul instantané)
cr0x@server:~$ pid=4123; for i in 1 2 3 4 5; do date; awk '/VmRSS|VmSize/ {print}' /proc/$pid/status; sleep 30; done
Mon Dec 30 03:12:01 UTC 2025
VmSize: 18934264 kB
VmRSS: 12288000 kB
Mon Dec 30 03:12:31 UTC 2025
VmSize: 18959000 kB
VmRSS: 12340000 kB
Mon Dec 30 03:13:01 UTC 2025
VmSize: 19001000 kB
VmRSS: 12420000 kB
Mon Dec 30 03:13:31 UTC 2025
VmSize: 19042000 kB
VmRSS: 12510000 kB
Mon Dec 30 03:14:01 UTC 2025
VmSize: 19090000 kB
VmRSS: 12605000 kB
Ce que cela signifie : VmSize et VmRSS augmentent régulièrement. C’est une fuite ou un schéma de rétention, pas un pic ponctuel.
Décision : Commencez à collecter de l’attribution (smaps_rollup, métriques de heap, stats de l’allocateur). Planifiez aussi une atténuation (redémarrage, montée en charge) car vous avez maintenant un temps avant l’échec.
Tâche 5 : Déterminer anon vs file-backed memory (smaps_rollup)
cr0x@server:~$ pid=4123; cat /proc/$pid/smaps_rollup | egrep 'Rss:|Pss:|Private|Shared|Swap:'
Rss: 12631240 kB
Pss: 12590010 kB
Shared_Clean: 89240 kB
Shared_Dirty: 1024 kB
Private_Clean: 120000 kB
Private_Dirty: 12450000 kB
Swap: 0 kB
Ce que cela signifie : Majoritairement Private_Dirty anon. C’est typiquement le heap, les stacks ou des mmaps anonymes.
Décision : Concentrez-vous sur les allocations et la rétention applicatives. Si au contraire la mémoire file-backed dominait, vous inspecteriez les mmaps, caches et schémas d’IO.
Tâche 6 : Vérifier les événements OOM de cgroup sous systemd (schémas par défaut de Debian 13)
cr0x@server:~$ systemctl status api-service.service --no-pager
● api-service.service - API Service
Loaded: loaded (/etc/systemd/system/api-service.service; enabled; preset: enabled)
Active: active (running) since Mon 2025-12-30 02:10:11 UTC; 1h 2min ago
Main PID: 4123 (api-service)
Tasks: 84 (limit: 12288)
Memory: 12.4G (peak: 12.6G)
CPU: 22min 9.843s
CGroup: /system.slice/api-service.service
└─4123 /usr/local/bin/api-service
Ce que cela signifie : systemd suit la mémoire courante et le pic ; c’est déjà précieux pour comprendre la forme de la fuite.
Décision : Si vous voyez « Memory: … (limit: …) » ou des messages OOM récents, passez aux statistiques du cgroup ensuite.
Tâche 7 : Lire les compteurs mémoire cgroup v2 et les événements OOM
cr0x@server:~$ cg=$(systemctl show -p ControlGroup --value api-service.service); echo $cg; cat /sys/fs/cgroup$cg/memory.current; cat /sys/fs/cgroup$cg/memory.events
/system.slice/api-service.service
13518778368
low 0
high 0
max 0
oom 0
oom_kill 0
Ce que cela signifie : Le service utilise ~13.5 GB et n’a pas encore atteint l’OOM du cgroup.
Décision : Si oom_kill s’incrémente, vous ne poursuivez pas un « crash mystère » — vous atteignez une limite connue. Ajustez MemoryMax, corrigez la fuite ou montez en charge.
Tâche 8 : Détecter si la mémoire est dans tmpfs ou des logs échappés (coupables sournois)
cr0x@server:~$ df -h | egrep 'tmpfs|/run|/dev/shm'
tmpfs 32G 1.2G 31G 4% /run
tmpfs 32G 18G 14G 57% /dev/shm
Ce que cela signifie : /dev/shm est énorme. C’est de la mémoire, pas du disque.
Décision : Si /dev/shm ou /run gonfle, inspectez ce qui écrit là (segments de mémoire partagée, caches de type navigateur, IPC). Ce n’est pas une fuite de heap ; redémarrer le service peut ne pas aider si c’est un autre processus.
Tâche 9 : Trouver les principaux consommateurs à l’intérieur du cgroup du service (unités multi-processus)
cr0x@server:~$ cg=$(systemctl show -p ControlGroup --value api-service.service); for p in $(cat /sys/fs/cgroup$cg/cgroup.procs | head -n 20); do awk -v p=$p '/VmRSS/ {print p, $2 "kB"}' /proc/$p/status 2>/dev/null; done | sort -k2 -n | tail
4123 12605000kB
Ce que cela signifie : Un processus principal représente presque tout le RSS.
Décision : Bien : l’attribution est plus simple. Si plusieurs processus partagent la croissance, suspectez des workers, un schéma de fork ou une explosion des arenas d’allocateur entre threads.
Tâche 10 : Vérifier le taux de fautes de page pour distinguer « toucher nouvelle mémoire » vs « réutiliser »
cr0x@server:~$ pid=4123; awk '{print "minflt="$10, "majflt="$12}' /proc/$pid/stat
minflt=48392011 majflt=42
Ce que cela signifie : Les fautes mineures sont élevées (normal pour les allocations), les fautes majeures faibles (pas encore de thrashing sur disque).
Décision : Si les fautes majeures montent rapidement, vous êtes déjà en effondrement de performance. Priorisez l’atténuation (redémarrage/scale) avant le profilage approfondi.
Tâche 11 : Voir les régions mappées par taille (les gros mmaps se repèrent)
cr0x@server:~$ pid=4123; awk '{print $1, $2, $6}' /proc/$pid/maps | head
55fdb5a6b000-55fdb5b2e000 r--p /usr/local/bin/api-service
55fdb5b2e000-55fdb5e7e000 r-xp /usr/local/bin/api-service
55fdb5e7e000-55fdb5f24000 r--p /usr/local/bin/api-service
7f01b4000000-7f01b8000000 rw-p
7f01b8000000-7f01bc000000 rw-p
Ce que cela signifie : De grandes régions anonymes (rw-p sans fichier) suggèrent des arenas de heap ou des mmap explicites.
Décision : Si vous voyez d’énormes mmaps file-backed (par ex. fichiers de données), la « fuite » pourrait venir d’une stratégie de mapping. Correction différente.
Tâche 12 : Capturer un instantané pmap (lecture seule, utile pour les diffs de tendance)
cr0x@server:~$ pid=4123; sudo pmap -x $pid | tail -n 5
---------------- ------- ------- ------- -------
total kB 18990000 12631240 12591000 0
Ce que cela signifie : Confirme les totaux ; pmap est un bon artefact « avant/après » pour des tickets.
Décision : Sauvegardez cette sortie pendant la timeline de l’incident. Si la mémoire baisse après un changement de config ou une bascule de trafic, vous avez des indices de causalité.
Tâche 13 : Inspecter journald pour des preuves OOM et de kills de cgroup
cr0x@server:~$ sudo journalctl -k --since "1 hour ago" | egrep -i 'oom|out of memory|killed process|memory cgroup' | tail -n 20
Dec 30 02:55:10 server kernel: Memory cgroup out of memory: Killed process 2877 (search-worker) total-vm:9132000kB, anon-rss:7800000kB, file-rss:12000kB, shmem-rss:0kB
Dec 30 02:55:10 server kernel: oom_reaper: reaped process 2877 (search-worker), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Ce que cela signifie : Le noyau a tué un processus à cause d’un OOM de cgroup. Ce n’est pas un segfault ; c’est l’application d’une politique.
Décision : Décidez si la limite est incorrecte (trop basse pour les pics) ou si la charge est erronée (fuite/rétention). Souvent c’est les deux.
Tâche 14 : Obtenir la vue de systemd sur les limites et l’accounting mémoire (et corriger ce qui manque)
cr0x@server:~$ systemctl show api-service.service -p MemoryAccounting -p MemoryMax -p MemoryHigh -p OOMPolicy
MemoryAccounting=yes
MemoryMax=infinity
MemoryHigh=infinity
OOMPolicy=continue
Ce que cela signifie : L’accounting est activé, mais il n’y a pas de plafond. OOMPolicy=continue signifie que systemd ne va pas arrêter l’unité lui-même sur OOM.
Décision : Si vous exécutez des hôtes multi-tenant, définissez MemoryHigh/MemoryMax pour protéger les voisins. Si c’est un nœud dédié, vous pouvez préférer pas de plafond et compter sur l’autoscaling + alerting.
Tâche 15 : Mettre une limite temporaire « tripwire » (prudemment) pour forcer un échec plus précoce avec de meilleures preuves
cr0x@server:~$ sudo systemctl set-property api-service.service MemoryHigh=14G MemoryMax=16G
cr0x@server:~$ systemctl show api-service.service -p MemoryHigh -p MemoryMax
MemoryHigh=15032385536
MemoryMax=17179869184
Ce que cela signifie : Vous avez appliqué une limite en direct (systemd écrit dans le cgroup). MemoryHigh déclenche la pression de reclaim ; MemoryMax est la limite dure.
Décision : Ne faites cela que si vous pouvez tolérer que le service soit tué plus tôt. C’est un compromis : meilleure contention et signaux plus clairs versus impact utilisateur potentiel. Sur des hôtes partagés, c’est souvent le choix responsable.
Tâche 16 : Si c’est Java, activer Native Memory Tracking (faible perturbation si planifié)
cr0x@server:~$ jcmd 4123 VM.native_memory summary
4123:
Native Memory Tracking:
Total: reserved=13540MB, committed=12620MB
- Java Heap (reserved=8192MB, committed=8192MB)
- Class (reserved=512MB, committed=480MB)
- Thread (reserved=1024MB, committed=920MB)
- Code (reserved=256MB, committed=220MB)
- GC (reserved=1200MB, committed=1100MB)
- Internal (reserved=2300MB, committed=1700MB)
Ce que cela signifie : Le processus n’est pas seulement « heap ». Les threads et allocations internes peuvent dominer.
Décision : Si le heap est stable mais que la mémoire native/interne croît, concentrez-vous sur les buffers hors-heap, le JNI, la création de threads ou la fragmentation de l’allocateur — pas sur le tuning du GC.
Tâche 17 : Si c’est Go, prendre un snapshot pprof du heap (perturbation minimale si le endpoint existe)
cr0x@server:~$ curl -sS localhost:6060/debug/pprof/heap?seconds=30 | head
\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff...
Ce que cela signifie : C’est un profil pprof gzippé. Ce n’est pas lisible en clair dans le terminal.
Décision : Sauvegardez-le et analysez hors-nœud. Si vous ne pouvez pas exposer pprof en toute sécurité, ne créez pas de trous en prod pendant un incident — utilisez un tunnel SSH ou liez à localhost uniquement.
Tâche 18 : Si c’est Python, utiliser tracemalloc (meilleur quand activé tôt)
cr0x@server:~$ python3 -c 'import tracemalloc; tracemalloc.start(); a=[b"x"*1024 for _ in range(10000)]; print(tracemalloc.get_traced_memory())'
(10312960, 10312960)
Ce que cela signifie : tracemalloc rapporte les allocations suivies actuelles et le pic (octets).
Décision : Dans de vrais services, vous activez tracemalloc au démarrage ou via un feature flag. S’il n’est pas activé, ne prétendez pas pouvoir reconstruire l’historique des allocations d’objets Python à partir du seul RSS.
Mini-histoire 1 : la mauvaise hypothèse (le piège « RSS = fuite »)
Une entreprise SaaS de taille moyenne exploitait une flotte Debian hébergeant une API multi-tenant. Chaque lundi matin, un nœud augmentait progressivement sa mémoire jusqu’à atteindre le swap, puis la latence devenait géométrique. L’on-call a fait ce que font les on-calls : a trouvé le processus au plus grand RSS et a ouvert un ticket « fuite mémoire » contre l’équipe API.
L’équipe API a répondu par l’art défensif habituel : « ça marche en staging », quelques graphiques de heap, et la croyance sincère que leur garbage collector était innocent. L’opération pointait le RSS. Le débat est devenu religieux : « Linux met en cache » contre « votre service fuit ».
La mauvaise hypothèse était simple : que l’augmentation du RSS signifie une augmentation des données vivantes. Ce n’est pas le cas. Le service utilisait une bibliothèque qui mappait en mémoire de grands jeux de données en lecture seule pour des recherches rapides. Le lundi, un job batch faisait pivoter de nouveaux jeux de données et le service mappait les nouveaux avant de démapper les anciens. Pendant un temps, les deux ensembles coexistaient. Le RSS a sauté. Pas une fuite ; un pattern de déploiement.
La correction n’était pas un profileur. C’était coordonner la rotation des jeux de données et forcer une stratégie de mapping qui échangeait le pointeur une fois la nouvelle map prête, puis démappait immédiatement l’ancienne région. Après cela, le RSS montait encore les lundis—mais moins et sur des fenêtres plus courtes. Ils ont aussi changé l’alerte de « RSS > seuil » vers « tendance MemAvailable en baisse + fautes majeures + latence ».
Personne n’aime la morale de cette histoire parce qu’elle est ennuyante : mesurez la bonne chose avant d’imputer des responsabilités. Mais c’est moins coûteux qu’un procès hebdomadaire.
Mini-histoire 2 : l’optimisation qui a mal tourné (les réglages d’allocateur face au trafic réel)
Une autre organisation, un autre trimestre, une autre « initiative d’efficacité des coûts ». Ils exécutaient un service C++ sur Debian et voulaient réduire l’utilisation mémoire. Un ingénieur bien intentionné a lu sur les arenas de glibc et a décidé de réduire l’empreinte mémoire en définissant MALLOC_ARENA_MAX=2 sur toute la flotte.
Le changement a fonctionné en pré-production. Le RSS a chuté sous charge synthétique. Les graphiques étaient magnifiques. Puis la production est arrivée : les schémas de trafic avaient plus de connexions longue durée, plus de pics de concurrence par requête, et surtout, des durées de vie d’objets différentes. La latence a commencé à saccader. Le CPU est monté. La mémoire n’est pas devenue stable ; elle est devenue contestée.
Avec trop peu d’arenas, les threads se sont battus pour les verrous de l’allocateur. Certaines requêtes ont ralenti, les files ont grandi, et le service retenait la mémoire plus longtemps parce qu’il était occupé à être lent. L’on-call a vu le RSS monter et a de nouveau accusé une fuite. Ils mesuraient un effet, pas la cause.
Le rollback a corrigé la latence. La mémoire a de nouveau grimpé—mais elle s’est comportée de façon prévisible. Ils sont finalement passés à jemalloc pour ce service avec du profiling activé en staging et un déploiement en production contrôlé. La leçon n’était pas « ne jamais tuner malloc ». C’était « les réglages d’allocateur sont spécifiques au workload et peuvent transformer des problèmes mémoire en problèmes de performance ».
Blague #2 : Tuner malloc en production, c’est comme réorganiser le garde-manger pendant un dîner — techniquement possible, socialement discutable.
Mini-histoire 3 : la pratique ennuyeuse mais correcte qui a sauvé la journée (budgétisation, limites et runbooks réchauffés)
Une équipe de services financiers exploitait des nœuds Debian 13 pour du traitement en arrière-plan. Le service n’était pas glamour : un consommateur de queue qui transformait des documents et stockait les résultats. Il avait aussi un historique de pics de mémoire occasionnels liés à des bibliothèques tierces de parsing.
Ils ont fait deux choses ennuyeuses. D’abord, ils ont utilisé systemd MemoryAccounting avec MemoryHigh réglé à une valeur qui forçait la pression de reclaim avant que l’hôte ne soit en danger. Ensuite, ils ont construit un job hebdomadaire de « répétition de fuite » en staging : montée de charge, vérification que la mémoire plafonne, et capture d’instantanés smaps_rollup à intervalles fixes.
Une nuit, une mise à jour d’une librairie fournisseur a changé le comportement et retenu d’énormes buffers. La mémoire a commencé à monter. Le service n’a pas emporté tout le nœud parce que la limite du cgroup l’a contenu. Le nœud est resté sain ; seul le groupe de workers a été impacté.
L’on-call n’a pas eu besoin de deviner. Les alertes incluaient memory.current et memory.events. Le runbook disait déjà : si memory.current monte et que le privé dirty domine, redémarrez l’unité, figez la version du paquet et ouvrez un incident pour la cause racine. Ils ont suivi cela, sont repartis dormir, et ont corrigé la fuite proprement le lendemain.
Rien d’héroïque n’est arrivé. C’était le but. La pratique « ennuyeuse » a transformé un incident potentiel de flotte en un simple hoquet de service.
Techniques à faible perturbation qui fonctionnent réellement
Commencez par des artefacts que vous pouvez collecter sans changer le processus
Vos outils les plus sûrs sont : /proc, systemd et les compteurs cgroup. Ils ne nécessitent pas de redémarrage. Ils n’attachent pas de débogueur. Ils n’introduisent pas l’effet Heisenbug où votre profileur « corrige » le timing et cache la fuite.
- /proc/<pid>/smaps_rollup vous dit si la mémoire est private dirty (type heap) versus file-backed.
- /sys/fs/cgroup/…/memory.current vous dit si l’unité entière croît, même si elle a plusieurs PID.
- journalctl -k vous indique si vous voyez des kills OOM de cgroup, un OOM global, ou autre chose entièrement.
Privilégiez l’échantillonnage plutôt que le tracing quand vous êtes sous charge
Le tracing complet des allocations peut être coûteux et fausser le comportement. Les profileurs par échantillonnage et les instantanés périodiques sont le défaut en production.
Si vous avez besoin des sites d’allocation précis, faites-le brièvement, et avec un plan de rollback.
Contenez les dégâts avec des limites cgroup et des redémarrages propres
« Faible perturbation » ne veut pas dire « ne jamais redémarrer ». Cela signifie redémarrer intentionnellement : drainer le trafic, rouler un seul instance, vérifier que la mémoire plateau, puis continuer.
Un redémarrage contrôlé qui évite les tempêtes de swap au niveau du nœud est souvent l’acte le plus gentil que vous puissiez faire pour vos utilisateurs.
Cherchez la corrélation temporelle avec les changements de charge
Les fuites se corrèlent souvent avec un type de requête spécifique, un job cron, un déploiement, ou un changement de forme de données.
Si la mémoire croît seulement quand une queue particulière est active, vous avez déjà réduit la recherche plus que n’importe quel outil générique.
systemd et cgroups v2 : utilisez-les, ne les combattez pas
Debian 13 mise sur systemd et cgroups v2. Ce n’est pas une déclaration idéologique ; c’est la réalité que vous devez déboguer.
Si vous traitez l’hôte comme « un tas de processus » et ignorez les cgroups, vous passerez à côté de la vraie frontière d’application des règles.
Pourquoi la pensée au niveau cgroup compte
Un service peut être tué alors que l’hôte a encore de la mémoire libre parce qu’il a atteint sa limite de cgroup. Inversement, un hôte peut être en danger même si le service semble correct parce qu’un autre cgroup monopolise la mémoire.
La télémétrie au niveau service vous évite de débattre quel processus « a l’air gros » et répond plutôt : quelle unité est responsable de la pression.
Utilisez MemoryHigh avant MemoryMax
MemoryHigh introduit la pression de reclaim ; MemoryMax est une barrière dure de kill. En pratique, MemoryHigh est un avertissement plus doux qui achète du temps.
Si vous ne définissez que MemoryMax, votre premier signal pourrait être un kill. C’est comme découvrir que votre jauge à carburant est cassée quand le moteur s’arrête.
Ayez une OOMPolicy explicite pour l’unité
Si systemd tue quelque chose pour OOM, que voulez-vous qu’il fasse ? Redémarrer ? Arrêter ? Continuer ?
Décidez en fonction du comportement du service : les APIs sans état peuvent redémarrer ; les workers avec état peuvent nécessiter un drain et une logique de retry.
cr0x@server:~$ sudo systemctl edit api-service.service
cr0x@server:~$ sudo cat /etc/systemd/system/api-service.service.d/override.conf
[Service]
MemoryAccounting=yes
MemoryHigh=14G
MemoryMax=16G
OOMPolicy=restart
Restart=always
RestartSec=5
Ce que cela signifie : Vous avez défini la frontière de contention et automatisé la récupération.
Décision : Si les redémarrages sont sûrs et que votre fuite est lente, cela transforme une « mort du nœud à 3h » en « recyclage contrôlé d’instance », achetant du temps pour le travail de cause racine.
Ne confondez pas « mémoire utilisée » et « mémoire facturée »
Les cgroups facturent la mémoire différemment pour l’anonyme et le cache de fichiers, et le comportement exact dépend des versions du noyau et des réglages.
Le but n’est pas de mémoriser chaque détail ; c’est d’observer les tendances et de savoir quels compteurs vous utilisez.
Recherche de fuites selon le langage (Java, Go, Python, C/C++)
Java : séparez le monde en heap vs natif
Si le RSS croît mais que l’usage du heap est stable, arrêtez d’accuser le GC. Vous êtes en mémoire native : direct buffers, stacks de threads, JNI, code cache ou fragmentation d’allocateur.
NMT (Native Memory Tracking) est votre ami, mais il est préférable de l’activer intentionnellement (flags de démarrage) pour un faible overhead.
Approche à faible perturbation : collectez des résumés NMT périodiquement, plus les logs GC ou événements JFR s’ils sont déjà activés. Si vous avez besoin d’un heap dump, préférez une fenêtre contrôlée et assurez-vous d’avoir de l’espace disque et de la bande passante IO ; les heap dumps peuvent être perturbateurs.
Go : traitez les profils de heap comme des preuves, pas des opinions
Le runtime Go vous fournit pprof et des métriques runtime qui sont étonnamment bonnes. Le risque est l’exposition : un endpoint de debug accessible depuis le mauvais réseau est un incident à lui seul.
Maintenez pprof lié à localhost et tunnelisez si nécessaire.
Cherchez : heap en usage croissant, nombre d’objets en hausse, ou un comptage de goroutines qui grimpe (une fuite d’un autre type).
Python : les fuites peuvent être « natives » aussi
Les services Python peuvent fuir en objets Python purs (suivis par tracemalloc), mais aussi dans des extensions natives qui allouent en dehors du suivi d’objets Python.
Si tracemalloc est ok et que le RSS monte malgré tout, suspectez des libs natives, des buffers et des patterns mmap.
C/C++ : décidez si vous avez besoin de télémétrie d’allocateur ou d’outils au niveau code
En C/C++, une vraie fuite est courante, mais il est aussi fréquent de « ne pas rendre la mémoire à l’OS ».
Si vous le pouvez, remplacer malloc de glibc par jemalloc dans un rollout contrôlé peut fournir du profiling et souvent un RSS plus stable. Mais ne considérez pas le remplacement d’allocateur comme une panacée.
Le chemin le moins perturbateur : capturer smaps_rollup, instantanés pmap, et si besoin, un échantillonnage court eBPF des piles d’allocation plutôt que du tracing toujours actif.
Erreurs courantes : symptôme → cause racine → correction
« La mémoire utilisée est à 95%, on fuit »
Cause racine : le page cache fait son travail ; le système a encore un MemAvailable sain.
Correction : alertez sur la tendance de MemAvailable, l’activité swap, les fautes majeures et la latence. Ne sonnez pas les humains parce que Linux utilise la RAM.
« Le RSS augmente, donc fuite de heap »
Cause racine : mmaps file-backed, fragmentation de l’allocateur, ou croissance de la mémoire native (direct buffers JVM, extensions Python).
Correction : smaps_rollup pour séparer private dirty vs file-backed ; utilisez les outils du langage (JVM NMT, pprof) pour confirmer.
« Le service est mort, ça doit être un segfault »
Cause racine : kill OOM de cgroup (souvent silencieux au niveau de l’app) ou OOM système.
Correction : journalctl -k pour les lignes OOM ; vérifiez memory.events dans le cgroup du service ; décidez d’une stratégie MemoryMax/OOMPolicy.
« On va corriger en ajoutant du swap »
Cause racine : le swap masque les fuites et les transforme en incidents de latence.
Correction : gardez le swap modeste, surveillez l’activité swap, et traitez un swap-in/out soutenu comme un P1. Utilisez le confinement et les redémarrages, pas un swap infini.
« On a activé du profilage lourd et la fuite a disparu »
Cause racine : effet observateur ; changement de timing ; modèles d’allocation différents.
Correction : privilégiez l’échantillonnage ; collectez plusieurs courtes fenêtres ; corrélez avec la charge ; reproduisez en staging avec une forme de trafic réaliste.
« On a défini MemoryMax et maintenant ça redémarre aléatoirement »
Cause racine : limite trop basse pour les pics légitimes, ou MemoryHigh absent donc mort soudaine.
Correction : définissez MemoryHigh en dessous de MemoryMax, mesurez les pics et ajustez. Utilisez des hooks d’arrêt gracieux et l’autoscaling si possible.
« Le container a une limite correcte ; l’hôte a quand même fait OOM »
Cause racine : autres cgroups sans limites ; pression mémoire du noyau ; cache de fichiers et pages sales ; ou accounting mal configuré.
Correction : inspectez memory.current sur les cgroups de haut niveau ; assurez-vous que l’accounting est activé ; mettez des limites sensées pour les voisins bruyants.
Listes de contrôle / plan étape par étape
Checklist A : confirmation en 10 minutes (sans redémarrage, sans nouveaux agents)
- Vérifier
/proc/meminfo:MemAvailablebaisse-t-il avec le temps ? - Lancer
vmstat 1: le swap est-il actif (si/so), l’IO wait monte-t-il ? - Identifier le processus au plus grand RSS via
ps --sort=-rss. - Confirmer la croissance avec des lectures répétées de
/proc/<pid>/status(VmRSS). - Utiliser
/proc/<pid>/smaps_rollup: private dirty vs file-backed. - Vérifier la mémoire de l’unité avec
systemctl statusetmemory.currentdu cgroup. - Rechercher dans les logs du noyau les événements OOM et les victimes.
Checklist B : confinement sans drame (quand la fuite est réelle)
- Estimer le temps avant l’échec : taux de croissance du RSS actuel vs marge disponible.
- Décider de la frontière de contention :
MemoryHigh/MemoryMaxpar service ou montée en charge du nœud. - Définir d’abord
MemoryHigh, puisMemoryMaxsi nécessaire. Préférer des changements itératifs et modestes. - S’assurer que
OOMPolicyet le comportement de Restart correspondent au type de service. - Planifier une fenêtre de redémarrage en rolling ; drainer le trafic si applicable.
- Capturer les artefacts « avant redémarrage » : smaps_rollup, totaux pmap,
memory.current,memory.events. - Après redémarrage : vérifier la stabilisation de la mémoire sous la même charge.
Checklist C : travail de cause racine (quand les utilisateurs sont en sécurité)
- Choisir le bon outil : pprof/JFR/NMT/tracemalloc/profiling de l’allocateur.
- Corréler la fuite à la charge : endpoints, types de jobs, tailles de payload.
- Reproduire en staging avec une concurrence et des données proches de la production.
- Corriger la rétention : ajouter éviction, bornes, timeouts et backpressure.
- Ajouter des dashboards :
memory.current, part privée dirty, événements OOM, compteurs de redémarrage. - Ajouter un test de non-régression : exécuter la charge suspectée assez longtemps pour montrer la pente.
FAQ
1) Comment distinguer « fuite » et « allocateur qui ne rend pas la mémoire » ?
Regardez la croissance monotone de la mémoire private dirty (smaps_rollup) corrélée aux compteurs d’objets ou aux métriques de heap. Si les données applicatives chutent mais que le RSS reste élevé, suspectez le comportement/fragmentation de l’allocateur.
2) Pourquoi le RSS continue d’augmenter alors que la charge est stable ?
Vraie fuite, rétention de cache, jobs en arrière-plan, ou une tâche lente « une fois par heure ». Confirmez avec des lectures répétées de VmRSS et corrélez avec les logs de requêtes/jobs. Une charge stable n’implique pas une forme de charge stable.
3) Quel est le fichier /proc le plus utile pour cela ?
/proc/<pid>/smaps_rollup. Il est assez compact pour être saisi pendant les incidents et donne des signaux de répartition décisifs.
4) Dois-je définir MemoryMax pour chaque service systemd ?
Sur des hôtes partagés : oui, presque toujours. Sur des nœuds dédiés : peut-être. Les limites empêchent les voisins bruyants de prendre la machine, mais elles introduisent aussi un comportement de défaut dur que vous devez gérer.
5) Comment savoir si l’OOM killer a touché mon service ?
Vérifiez journalctl -k pour des lignes « Killed process » et consultez les compteurs memory.events du cgroup du service (oom/oom_kill). Ne devinez pas.
6) Ajouter du swap est-ce une mitigation valable ?
C’est une béquille à court terme, pas une solution. Le swap peut empêcher un OOM immédiat mais transforme souvent les échecs en pics de latence et tempêtes IO. Utilisez-le parcimonieusement, surveillez-le agressivement.
7) Puis-je déboguer des fuites sans redémarrage ?
Parfois. /proc et les stats cgroup ne nécessitent pas de redémarrage. L’échantillonnage eBPF nécessite souvent seulement des privilèges, pas de redémarrage. Les outils niveau langage varient : Go et JVM peuvent souvent s’attacher ; tracemalloc est mieux activé au démarrage.
8) Quelle est la manière la moins perturbatrice de collecter des preuves « avant/après » ?
Prenez des instantanés périodiques : smaps_rollup, totaux pmap, memory.current et memory.events. Stockez-les avec des horodatages. Cela vous donne une pente et un journal de changements sans instrumentation lourde.
9) Pourquoi systemd affiche « Memory: 12G » mais ps montre un RSS différent ?
systemd reporte l’usage mémoire du cgroup entier (incluant les enfants et le cache facturé au cgroup). ps montre le RSS par processus. Ils répondent à des questions différentes ; utilisez les deux.
10) Quand dois-je arrêter le débogage et juste rollbacker ?
Si la fuite a commencé après un déploiement et que vous avez un rollback sûr, faites-le tôt. La cause racine peut attendre. Les utilisateurs ne se soucient pas que vous ayez trouvé le site d’allocation parfait à 04:40.
Conclusion : actions suivantes que vous pouvez faire aujourd’hui
Les fuites mémoire dans les services ne s’expliquent pas par des impressions. Sur Debian 13, la voie la moins perturbatrice est constante : confirmer la pression avec MemAvailable et le swap, identifier l’unité croissante via les cgroups, classifier la mémoire avec smaps_rollup, et seulement ensuite choisir des outils plus profonds.
Étapes suivantes qui rapportent immédiatement :
- Ajouter des dashboards pour memory.current, memory.events et les compteurs de redémarrage par unité.
- Définir MemoryHigh (et parfois MemoryMax) pour les services bruyants, plus une OOMPolicy explicite.
- Mettre à jour les alertes pour se concentrer sur la tendance de MemAvailable, l’activité swap, les fautes majeures et la latence, pas la « mémoire utilisée » brute.
- Prendre l’habitude de capturer des instantanés smaps_rollup pendant les incidents pour arrêter les débats et commencer les corrections.
Vous ne pouvez pas prévenir chaque fuite. Vous pouvez empêcher que la plupart des incidents de fuite deviennent des catastrophes au niveau hôte. Voilà le vrai gain.