Tempêtes de swap Docker : pourquoi les conteneurs « fonctionnent » pendant que l’hôte fond (et corrections)

Cet article vous a aidé ?

La page est accessible. Les pods sont verts. Les journaux des conteneurs ont l’air ennuyeux. Pendant ce temps, votre hôte crie :
charge moyenne dans la stratosphère, SSH met 30 secondes pour afficher un caractère, et kswapd dévore le CPU comme si chaque cycle était payé.

C’est un type particulier d’incident en production où tout « fonctionne » jusqu’à ce que le métier remarque la latence,
les timeouts et des bases de données mystérieusement « lentes ». Bienvenue dans la tempête de swap : pas un crash unique, mais une fonte lente et inéluctable.

Ce qu’est une tempête de swap (et pourquoi elle vous trompe)

Une tempête de swap est une pression mémoire soutenue qui pousse le noyau à évincer continuellement des pages de la RAM vers le swap,
puis à les recharger, de manière répétée. Ce n’est pas seulement « un peu de swap utilisé ». C’est le système qui passe tellement de temps
à déplacer des pages que le travail utile devient secondaire.

Le pire, c’est que de nombreuses applications continuent à « fonctionner ». Elles répondent, lentement. Elles retentent. Elles expirent et sont relancées.
Votre orchestrateur voit des processus encore vivants, des checks de santé à peine passants, et pense que tout va bien.
Ce sont les humains qui remarquent en premier : tout semble collant.

Deux signaux qui distinguent « swap utilisé » d’une « tempête de swap »

  • Les fautes de page majeures augmentent en pic (lecture de pages depuis un swap basé sur disque).
  • La PSI de mémoire montre des blocages soutenus (tâches en attente de reclaim mémoire / IO).

Si vous ne regardez que le « pourcentage de swap utilisé », on peut vous mentir. Le swap peut être à 20 % et stable pendant des semaines sans drame.
À l’inverse, le swap peut être « seulement » à 5 % et pourtant en tempête si le working set churn.

Faits intéressants et contexte historique (parce que ce bazar a une histoire)

  1. Le comportement OOM précoce de Linux était notoirement brutal. Le killer OOM du noyau a évolué sur des décennies ; il surprend encore les gens sous pression.
  2. Les cgroups sont apparus pour arrêter les « voisins bruyants ». Ils ont été conçus pour des systèmes partagés bien avant que les conteneurs ne deviennent à la mode.
  3. La comptabilisation du swap dans les cgroups a été controversée. Elle ajoute un overhead et a eu des bugs en production ; de nombreuses plateformes l’ont désactivée par défaut.
  4. Kubernetes a historiquement déconseillé le swap. Pas parce que le swap est intrinsèquement mauvais, mais parce que l’isolation mémoire prévisible est difficile quand le swap entre en jeu.
  5. Le chiffre « mémoire libre » est mal compris depuis toujours. Linux utilise la RAM pour le cache de pages de façon agressive ; un « free » faible est souvent sain.
  6. Pressure Stall Information (PSI) est relativement récent. C’est l’un des meilleurs outils modernes pour voir « attente sur la mémoire » sans deviner.
  7. Le swap sur SSD a rendu les tempêtes plus silencieuses, pas plus sûres. Un swap plus rapide réduit la douleur… jusqu’à masquer le problème et provoquer l’amplification d’écriture et des cliffs de latence.
  8. Les valeurs par défaut d’overcommit sont un artefact culturel. Linux suppose que beaucoup de programmes allouent plus qu’ils n’utilisent ; c’est vrai jusqu’à ce que ça ne le soit plus.

Pourquoi les conteneurs semblent sains pendant que l’hôte meurt

Les conteneurs n’ont pas leur propre noyau. Ce sont des processus groupés par des cgroups et des namespaces.
La pression mémoire est gérée par le noyau de l’hôte, et c’est le noyau de l’hôte qui fait le reclaim et le swap.

Voici l’illusion : un conteneur peut continuer à tourner et répondre alors que l’hôte swappe intensément, car
les processus du conteneur sont toujours planifiés et continuent de progresser—mais à un coût terrible.
L’utilisation CPU du conteneur peut même sembler inférieure parce qu’il est bloqué sur de l’IO (swap-in), au lieu de brûler du CPU.

Les principaux modes de défaillance qui créent le schéma « conteneurs OK, hôte en fusion »

  • Pas de limites mémoire (ou limites incorrectes). Un conteneur grossit jusqu’à ce que l’hôte fasse du reclaim et swappe tout le monde.
  • Limites définies mais swap non contraint. Le conteneur reste sous son plafond RAM mais pousse quand même la pression dans le reclaim global via des patterns de cache de pages et des ressources partagées.
  • Le page cache + IO filesystem domine. Des conteneurs effectuant de l’IO peuvent saturer le cache, forçant le reclaim et le swap pour les autres charges.
  • Overcommit + pics. Beaucoup de services allouent agressivement en même temps ; vous n’atteignez pas l’OOM immédiatement, vous churnez.
  • La politique OOM évite de tuer. Le système swappe au lieu d’échouer rapidement, échangeant la correction contre une « disponibilité » catastrophique.

Encore un twist : la télémétrie au niveau conteneur peut induire en erreur. Certains outils rapportent l’usage mémoire du cgroup mais pas
la douleur du reclaim au niveau hôte. Vous verrez des conteneurs « dans les limites » pendant que l’hôte passe sa journée à mélanger des pages.

Blague #1 : Le swap, c’est comme un garde-meuble — vous avez l’impression d’être organisé jusqu’au moment où vous réalisez que vous payez chaque mois pour stocker des trucs dont vous avez encore besoin tous les jours.

Notions de base sur la mémoire Linux utiles

Vous n’avez pas besoin de mémoriser le code du noyau. Vous avez besoin de quelques concepts pour raisonner sur les tempêtes de swap sans superstition.

Working set vs mémoire allouée

La plupart des applis allouent de la mémoire qu’elles ne touchent pas activement. Le noyau ne se soucie pas de « alloué », il se soucie de
« récemment utilisé ». Votre working set est l’ensemble de pages que vous touchez assez fréquemment pour que les évincer fasse mal.

Les tempêtes de swap surviennent quand le working set ne tient pas, ou quand il tient mais que le noyau est forcé de churner des pages à cause
de demandes concurrentes (page cache, autres cgroups, ou un seul coupable qui salit sans cesse la mémoire).

Mémoire anonyme vs mémoire liée aux fichiers

  • Anonyme : heap, pile—pouvant être swappée.
  • Liée à un fichier : page cache—évinçable sans swap (on peut relire depuis le fichier) sauf si les pages sont dirty.

Quand vous exécutez bases de données, caches, JVM et services très verbeux en logs sur le même hôte, la récupération d’anonyme et de fichier
interagit de façons divertissantes. « Divertissantes » signifie ici « un postmortem que vous lirez à 2h du matin ».

Reclaim, kswapd et reclaim direct

Le noyau essaie de récupérer de la mémoire en arrière-plan (kswapd). Sous forte pression, les processus peuvent entrer eux-mêmes
en direct reclaim—ils se bloquent en tentant de libérer de la mémoire. C’est là que la latence va mourir.

Pourquoi les tempêtes de swap ressemblent à des problèmes CPU

Le reclaim consomme du CPU. La compression peut consommer du CPU (zswap/zram). Le faulting des pages brûle CPU et IO.
Et vos threads applicatifs peuvent être bloqués, rendant les graphiques d’utilisation confus : faible CPU applicatif, fort CPU système, fort IO wait.

cgroups, Docker et les angles vifs autour du swap

Docker utilise les cgroups pour contraindre les ressources. Mais les « contraintes mémoire » sont un fourre-tout selon la version du noyau,
cgroup v1 vs v2, et la configuration de Docker.

cgroup v1 vs v2 : différences pratiques pour les tempêtes de swap

En cgroup v1, la mémoire et le swap étaient gérés par des réglages séparés (memory.limit_in_bytes, memory.memsw.limit_in_bytes),
et la comptabilisation du swap pouvait être désactivée. En cgroup v2, la mémoire est plus unifiée et l’interface est plus propre :
memory.max, memory.swap.max, memory.high, plus des métriques de pression.

Si vous êtes en cgroup v2 et que vous n’utilisez pas memory.high, vous manquez l’un des meilleurs outils pour empêcher un seul cgroup de transformer
l’hôte en grille-pain propulsé par le swap.

Flags mémoire Docker : ce qu’ils signifient réellement

  • --memory : limite stricte. Si dépassée, le cgroup tentera de reclaim ; s’il n’y parvient pas, vous aurez un OOM (à l’intérieur du cgroup).
  • --memory-swap : dans beaucoup de configurations, limite totale mémoire+swap. La sémantique varie ; sur certains systèmes c’est ignoré sans comptabilisation du swap.
  • --oom-kill-disable : presque toujours une mauvaise idée en production. Cela encourage l’hôte à souffrir plus longtemps.

Le fait que le conteneur « fonctionne » pendant que l’hôte fond résulte souvent d’une décision de politique :
on a dit au système « ne tuez pas, essayez plus fort ». Le noyau a obéi.

Une citation à tatouer sur vos runbooks

« L’espoir n’est pas une stratégie. » — idée paraphrasée souvent attribuée dans les cercles ingénierie/ops ; le point reste valide.

Mode opératoire de diagnostic rapide

C’est l’ordre que j’utilise quand quelqu’un signale « l’hôte est lent » et que je suspecte une pression mémoire. Il est conçu pour
vous amener rapidement à une décision : tuer, limiter, déplacer ou tuner.

Première étape : confirmer qu’il s’agit bien d’une tempête de swap (et pas seulement de « swap utilisé »)

  1. Vérifier l’activité swap actuelle (taux swap-in/out) et les fautes de page majeures.
  2. Vérifier la PSI mémoire pour des blocages soutenus.
  3. Vérifier si le sous-système IO est saturé (le swap dépend de l’IO).

Deuxième étape : trouver le cgroup/conteneur responsable

  1. Comparer l’usage mémoire par conteneur, y compris RSS et cache.
  2. Vérifier quels cgroups déclenchent des OOM ou un reclaim élevé.
  3. Rechercher un motif de charge (croissance de heap JVM, job batch, rafale de logs, compaction, rebuild d’index).

Troisième étape : décider la mitigation

  • Si la latence prime sur l’achèvement : échouer vite (limites strictes, autoriser l’OOM, redémarrer proprement).
  • Si l’achèvement prime sur la latence : isoler (nœuds dédiés, réduire swappiness, swap contrôlé, plus lent mais stable).
  • Si vous êtes aveugle : ajoutez PSI + métriques mémoire par cgroup d’abord. Tuner sans visibilité, c’est jouer à la roulette.

Tâches pratiques : commandes, sorties, décisions

Voici les commandes que j’exécute réellement sur un hôte. Chaque tâche inclut ce que la sortie signifie et la décision qu’elle permet de prendre.
Ajustez les noms d’interface et les chemins selon votre environnement.

Tâche 1 : Confirmer la présence du swap et la quantité utilisée

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:            62Gi        54Gi       1.2Gi       1.1Gi       6.8Gi       2.3Gi
Swap:           16Gi        12Gi       4.0Gi

Sens : Le swap est fortement utilisé (12Gi) et la mémoire disponible est faible (2.3Gi). Ce n’est pas la preuve d’une tempête, mais c’est suspect.
Décision : Passer aux métriques d’activité ; le swap utilisé seul n’implique pas une action immédiate.

Tâche 2 : Mesurer l’activité swap et la pression de fautes de page

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
 3  1 12453248 312000  68000 512000   60  210   120   980 1200 2400 12 18 42 28  0
 2  2 12454800 298000  66000 500000  180  640   200  1800 1800 3200  8 22 30 40  0
 4  2 12456000 286000  64000 490000  220  710   260  2100 1900 3300  9 23 24 44  0
 3  3 12456800 280000  62000 482000  240  680   300  2000 2000 3500 10 24 22 44  0
 2  2 12458000 276000  60000 475000  210  650   280  1900 1950 3400  9 23 25 43  0

Sens : si/so non négligeables (swap-in/out) chaque seconde et wa élevé (IO wait). C’est du paging actif.
Décision : Traiter comme une tempête. Ensuite : déterminer si l’IO est saturé et quel cgroup pousse la mémoire.

Tâche 3 : Vérifier la PSI pour les blocages mémoire (niveau hôte)

cr0x@server:~$ cat /proc/pressure/memory
some avg10=18.40 avg60=12.12 avg300=8.50 total=192003210
full avg10=6.20 avg60=3.90 avg300=2.10 total=48200321

Sens : La pression full indique que les tâches sont fréquemment bloquées parce que le reclaim mémoire n’arrive pas à suivre. Cela corrèle fortement avec les pics de latence.
Décision : Arrêtez de chercher des « bugs CPU ». C’est de la pression mémoire. Trouvez le coupable et limitez/tuez/isolez.

Tâche 4 : Identifier si vous êtes en cgroup v1 ou v2

cr0x@server:~$ stat -fc %T /sys/fs/cgroup
cgroup2fs

Sens : cgroup v2 est actif. Vous pouvez utiliser memory.high et memory.swap.max.
Décision : Préférez les contrôles v2 ; évitez les astuces d’époque v1 qui ne s’appliquent pas.

Tâche 5 : Voir les plus gros consommateurs de mémoire par conteneur

cr0x@server:~$ docker stats --no-stream
CONTAINER ID   NAME              CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O     PIDS
a1b2c3d4e5f6   api-prod          35.20%    1.8GiB / 2GiB         90.00%    2.1GB / 1.9GB  120MB / 3GB  210
b2c3d4e5f6g7   search-indexer     4.10%    7.4GiB / 8GiB         92.50%    150MB / 90MB   30GB / 2GB   65
c3d4e5f6g7h8   metrics-agent      0.50%    220MiB / 512MiB       42.97%    20MB / 18MB    2MB / 1MB    14

Sens : search-indexer approche sa limite mémoire et effectue d’importantes IO bloc (30GB reads/writes), ce qui peut être du churn de page cache, une compaction ou un spill.
Décision : Approfondir les métriques du cgroup de ce conteneur (reclaim, swap, événements OOM).

Tâche 6 : Inspecter les limites mémoire + swap d’un conteneur suspect (v2)

cr0x@server:~$ CID=b2c3d4e5f6g7
cr0x@server:~$ CG=$(docker inspect -f '{{.HostConfig.CgroupParent}}' "$CID")
cr0x@server:~$ docker inspect -f '{{.Id}} {{.Name}}' "$CID"
b2c3d4e5f6g7h8i9j0 /search-indexer
cr0x@server:~$ cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.max
8589934592
cr0x@server:~$ cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.swap.max
max

Sens : La limite RAM est 8GiB, mais le swap est illimité (max). Sous pression, ce cgroup peut pousser très fort sur le swap.
Décision : Définir memory.swap.max ou configurer Docker pour borner le swap pour les conteneurs qui ne doivent pas paginer.

Tâche 7 : Vérifier les événements par cgroup : atteignez-vous reclaim/OOM ?

cr0x@server:~$ cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.events
low 0
high 1224
max 18
oom 2
oom_kill 2

Sens : Le cgroup a atteint high fréquemment et a subi des kills OOM. Ce n’est pas une « instabilité aléatoire » ; c’est un problème de dimensionnement.
Décision : Soit augmenter la mémoire, soit réduire le working set, soit accepter des redémarrages mais empêcher le thrash au niveau hôte en limitant le swap et en utilisant memory.high.

Tâche 8 : Observer l’usage du swap par processus (trouver le vrai coupable)

cr0x@server:~$ sudo smem -rs swap | head -n 8
  PID User     Command                         Swap      USS      PSS      RSS
18231 root     java -jar indexer.jar          6144M    4096M    4200M    7000M
 9132 root     python3 /app/worker.py          820M     600M     650M    1200M
 2210 root     dockerd                          90M      60M      70M     180M
 1987 root     containerd                       40M      25M      30M      90M
 1544 root     /usr/bin/prometheus              10M     900M     920M     980M
 1123 root     /usr/sbin/sshd                    1M       2M       3M       8M

Sens : L’indexeur Java a 6GiB swapés. Cela explique le « conteneur vivant mais lent » : il faute des pages constamment.
Décision : Si cette charge ne doit pas swapper, limitez-la et forcez OOM/restart. Si elle doit swapper, isolez-la sur un hôte avec swap plus rapide et moins de contention.

Tâche 9 : Vérifier la saturation disque (le swap, c’est de l’IO ; l’IO, c’est de la latence)

cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (server) 	01/02/2026 	_x86_64_	(16 CPU)

avg-cpu:  %user %nice %system %iowait  %steal %idle
          10.21  0.00   22.11   41.90    0.00  25.78

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   wrqm/s  %wrqm w_await wareq-sz  aqu-sz  %util
nvme0n1        120.0   12800.0     2.0   1.64    18.2   106.7    210.0   24400.0     8.0   3.67   32.5   116.2    9.80  99.20

Sens : %util proche de 100 % et forte latence d’attente. Le NVMe est saturé ; swap-in/out va s’aligner en file, provoquant des blocages partout.
Décision : Atténuation immédiate : réduire la pression mémoire (tuer le coupable, baisser la concurrence). À plus long terme : séparer le swap de l’IO de charge ou utiliser un stockage plus rapide.

Tâche 10 : Voir quels processus sont bloqués en reclaim ou en attente IO

cr0x@server:~$ ps -eo pid,stat,wchan:20,comm --sort=stat | head -n 12
  PID STAT WCHAN                COMMAND
18231 D    io_schedule          java
19102 D    io_schedule          java
 9132 D    io_schedule          python3
24011 D    balance_pgdat        postgres
24022 D    balance_pgdat        postgres
 2210 Ssl  ep_poll              dockerd
 1987 Ssl  ep_poll              containerd

Sens : État D + io_schedule indique un sommeil non interrompu en attente d’IO. balance_pgdat suggère du direct reclaim.
Décision : Votre latence est due à des attentes au niveau noyau. Arrêtez d’augmenter le trafic ; cela empirera la file. Réduisez la charge ou arrêtez le coupable.

Tâche 11 : Vérifier les logs kernel pour OOM et avertissements de reclaim

cr0x@server:~$ sudo dmesg -T | tail -n 12
[Thu Jan  2 10:14:22 2026] Memory cgroup out of memory: Killed process 18231 (java) total-vm:12422392kB, anon-rss:7023120kB, file-rss:10244kB, shmem-rss:0kB
[Thu Jan  2 10:14:22 2026] oom_reaper: reaped process 18231 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
[Thu Jan  2 10:14:25 2026] Out of memory: Killed process 9132 (python3) total-vm:2048320kB, anon-rss:1192200kB, file-rss:9120kB, shmem-rss:0kB

Sens : Des kills OOM au niveau cgroup ont eu lieu. C’est en fait préférable à une tempête de swap au niveau hôte—si votre service peut redémarrer proprement.
Décision : Confirmez les policies de restart, ajustez les limites et fixez les attentes : un OOM contrôlé vaut mieux qu’un effondrement incontrôlé de l’hôte.

Tâche 12 : Inspecter le swappiness et la posture d’overcommit de l’hôte

cr0x@server:~$ sysctl vm.swappiness vm.overcommit_memory vm.overcommit_ratio
vm.swappiness = 60
vm.overcommit_memory = 0
vm.overcommit_ratio = 50

Sens : Swappiness 60 est proche de la valeur par défaut et peut être trop agressif sur des hôtes conteneurisés mixtes. L’overcommit est heuristique (0).
Décision : Si cet hôte accueille des services sensibles à la latence, envisagez d’abaisser swappiness et de resserrer l’overcommit, mais seulement après avoir mis en place des limites par cgroup.

Tâche 13 : Vérifier zswap/zram (utile parfois, masque toujours)

cr0x@server:~$ grep -H . /sys/module/zswap/parameters/enabled /sys/block/zram0/disksize 2>/dev/null
/sys/module/zswap/parameters/enabled:Y

Sens : zswap est activé ; les pages swappées peuvent être compressées en RAM. Cela réduit l’IO mais augmente le CPU et peut masquer la pression jusqu’à ce qu’il soit trop tard.
Décision : Gardez-le si cela apporte de la stabilité. Ne le prenez pas comme une permission pour exécuter des charges sans limites mémoire.

Tâche 14 : Vérifier les hypothèses du daemon Docker et la comptabilisation mémoire du noyau

cr0x@server:~$ docker info | sed -n '1,40p'
Client:
 Version:    26.1.0
 Context:    default
 Debug Mode: false

Server:
 Containers: 38
  Running: 33
  Paused: 0
  Stopped: 5
 Server Version: 26.1.0
 Storage Driver: overlay2
 Cgroup Driver: systemd
 Cgroup Version: 2
 Kernel Version: 6.5.0
 Operating System: Ubuntu 24.04 LTS
 OSType: linux

Sens : systemd + cgroup v2. Bien. Vos contrôles existent ; il suffit de les utiliser.
Décision : Mettre en place des politiques compatibles cgroup v2 (memory.high, memory.swap.max) plutôt que des conseils hérités.

Trois mini-récits d’entreprise issus des tranchées mémoire

Mini-récit 1 : L’incident causé par une mauvaise hypothèse

Une entreprise SaaS de taille moyenne exécutait une flotte de workers d’indexation de recherche dans Docker sur quelques hôtes costauds. Ils avaient le swap activé « pour la sécurité »,
et ils avaient défini des limites mémoire par conteneur. Tout le monde se sentait responsable. Tout le monde dormait.

Durant une semaine chargée, le backlog d’indexation a augmenté et un ingénieur a augmenté la concurrence des workers à l’intérieur du conteneur.
Le conteneur restait sous son plafond --memory la plupart du temps, mais le pattern d’allocation de la charge est devenu en rafales :
gros buffers transitoires, IO disque intensif et cache agressif. L’hôte a commencé à swapper.

L’hypothèse erronée était subtile : « Si les conteneurs ont des limites mémoire, l’hôte ne swappe pas jusqu’à l’oubli. »
En réalité, les limites empêchent un seul cgroup d’utiliser une RAM infinie, mais elles ne garantissent pas automatiquement l’équité au niveau hôte
ni n’empêchent la douleur du reclaim global—surtout quand le swap est illimité et que le chemin IO est partagé.

Les symptômes étaient classiques. Latence API doublée, puis triplée. Les connexions SSH saccadaient. La surveillance indiquait « mémoire conteneur dans les limites »,
si bien que l’équipe a passé des heures à chasser des réglages réseau et base de données. Finalement, quelqu’un a lancé cat /proc/pressure/memory
et l’histoire s’est écrite d’elle-même.

La correction n’était pas exotique. Ils ont défini memory.swap.max pour les workers d’indexation, ajouté memory.high pour les throttler avant l’OOM,
et déplacé la charge d’indexation vers des nœuds dédiés avec leur propre budget IO. L’amélioration majeure est venue de la partie ennuyeuse :
noter explicitement que « les limites mémoire ne protègent pas l’hôte » et en faire une condition de déploiement.

Mini-récit 2 : L’optimisation qui s’est retournée contre eux

Une autre organisation avait un pipeline de logging multi-tenant. Pour réduire la charge d’écriture disque, ils ont activé zswap et augmenté la taille du swap.
Les résultats initiaux semblaient excellents : moins de pics d’écriture, des graphes IO plus lisses, moins d’OOM immédiats.

Puis survint un incident mineur : un client a activé le logging verbeux et la compression côté application.
Le volume de logs a explosé, le CPU a monté et la pression mémoire a augmenté. Avec zswap, le noyau compressait d’abord les pages swappées en RAM.
Cela a réduit l’IO du swap, mais augmenté le temps CPU passé en compression et en reclaim.

Dans les tableaux de bord, cela ressemblait à une « saturation CPU », pas à une « pression mémoire ». L’équipe a retouché les limites CPU, ajouté des cœurs et agrandi l’hôte.
Le système s’est empiré. Plus de RAM signifiait plus de caches et donc plus à churner ; plus de CPU signifiait que zswap pouvait compresser davantage, retardant l’échec évident.
Le jitter de latence est devenu permanent.

L’effet néfaste n’était pas zswap lui-même. C’était de le traiter comme une optimisation de performance plutôt que comme un tampon de pression.
Le vrai problème était la croissance mémoire non contrôlée dans une étape de parsing et l’absence de throttling memory.high. Le swap était le symptôme,
zswap l’amplificateur qui rendait le symptôme plus difficile à voir.

Ils ont corrigé cela en imposant des plafonds mémoire stricts par étape, en ajoutant du backpressure dans le pipeline, et en n’utilisant zswap que sur des nœuds
dédiés au batch où la latence n’avait pas d’importance. Ils ont aussi changé l’alerte : PSI mémoire soutenue > seuil est devenue une page-worthy alert.

Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une équipe de services financiers exécutait un mélange d’APIs orientées client et d’un job de réconciliation nocturne sur le même cluster Kubernetes.
Ils étaient douloureusement conservateurs : demandes/limites par service définies, seuils d’éviction révisés chaque trimestre,
et chaque pool de nœuds avait une « politique swap » écrite. Ce n’était pas excitant. C’est aussi pourquoi ils n’ont pas eu d’incidents excitants.

Une nuit, un job batch a commencé à utiliser plus de mémoire après une mise à jour d’une bibliothèque fournisseur. Il a grandi régulièrement, pas de façon explosive.
Sur une équipe moins disciplinée, cela devient une lente tempête de swap et un rapport d’incident contenant les mots « nous ne savons pas ».

Leur système a fait quelque chose de peu glamour : le job a atteint sa limite mémoire, a été tué par l’OOM, et a redémarré avec un réglage de parallélisme réduit
(fallback pré-défini). Le batch a tourné plus lentement. Les APIs sont restées rapides. L’astreinte a reçu une alerte spécifique :
« batch job OOMKilled ; PSI hôte normal. »

Le lendemain, ils ont revert la bibliothèque, soumis le bug upstream, et ajusté la requête mémoire du job définitivement.
Personne n’a écrit un message héroïque sur Slack. C’est le but. La bonne pratique—limites plus échec contrôlé—a empêché qu’un événement « hôte en fusion » n’existe du tout.

Corrections durables : limites, OOM, swappiness et surveillance

1) Imposer des limites mémoire sur chaque conteneur important

Pas de limite, c’est une décision. C’est décider que l’hôte sera votre limite. L’hôte est une mauvaise limite car il échoue collectivement : toutes les charges souffrent, puis tout s’effondre.

Utilisez des limites par service dimensionnées sur le working set, pas sur des fantasmes de pics d’allocation. Pour les JVM, cela signifie fixer le heap
intentionnellement et laisser de la marge pour l’off-heap et les allocations natives.

2) Utiliser memory.high (cgroup v2) pour throttler avant l’OOM

memory.max est une falaise. memory.high est une limitation de vitesse. Quand vous définissez memory.high, le noyau commence à reclaimer
et à throttler les allocations une fois que le cgroup le dépasse, ce qui tend à réduire le thrash au niveau hôte.

Pour les services en rafales, memory.high légèrement en dessous de memory.max produit souvent un système qui est plus lent sous pression mais
contrôlable, au lieu de chaotique.

3) Bornez le swap par workload, ou désactivez-le pour les services sensibles à la latence

Le swap illimité est la façon dont on se retrouve avec un serveur « up » mais inutilisable. Pour les services qui doivent être réactifs,
préférez soit aucune utilisation du swap soit une très petite autorisation de swap.

Si vous avez besoin de swap pour des charges batch, isolez-les. Le swap n’est pas gratuit ; c’est un compromis latence vs achèvement.
Mélanger « doit être rapide » et « peut être lent » sur le même nœud supporté par swap est une expérience sociologique.

4) Baisser la swappiness (prudemment) sur les hôtes conteneurisés mixtes

vm.swappiness contrôle l’agressivité avec laquelle le noyau swappe la mémoire anonyme versus récupérer le page cache.
Sur des hôtes exécutant des bases de données ou des services basse latence, une valeur plus basse (comme 10 ou 1) peut réduire le swap des pages chaudes.

Ne faites pas un copier-coller de vm.swappiness=1 partout. Si votre hôte compte sur le swap pour éviter des OOM et que vous baissez la swappiness sans corriger le dimensionnement,
vous échangerez des tempêtes de swap contre des tempêtes d’OOM.

5) Préférer l’OOM contrôlé au thrash incontrôlé

C’est la partie que les gens détestent émotionnellement mais aiment opérationnellement : pour beaucoup de services, redémarrer coûte moins cher que paginer.
Si un service ne peut pas fonctionner dans son budget, le tuer, c’est de l’honnêteté.

Évitez de désactiver l’OOM kill pour les conteneurs sauf si vous comprenez profondément les conséquences. Désactiver l’OOM kill est la façon de
transformer un mauvais processus en incident de niveau hôte.

Blague #2 : Désactiver le killer OOM, c’est comme retirer l’alarme incendie parce qu’elle est bruyante—maintenant vous pouvez admirer les flammes en paix.

6) Surveillez PSI et les métriques de reclaim, pas seulement la « mémoire utilisée »

Les alertes les plus utiles sont celles qui vous disent « le noyau bloque les tâches ».
PSI vous donne cela directement. Associez-le aux taux swap-in/out et à la latence IO.

7) Séparez les chemins IO quand le swap est inévitable

Si le swap et votre charge principale partagent le même disque, vous avez créé une boucle de rétroaction : le swap provoque l’encombrement IO,
l’encombrement IO ralentit les applis, les applis lentes gardent la mémoire plus longtemps, la pression mémoire augmente, le swap augmente. Félicitations, vous avez construit un carrousel.

Sur des systèmes sérieux, le swap appartient à un stockage rapide, parfois à des dispositifs séparés, ou vous utilisez zram pour une marge d’urgence bornée
(en tenant compte du CPU nécessaire).

8) Intégrez la budgétisation mémoire au déploiement, pas au postmortem

Réalité d’entreprise : les équipes n’« oublieront pas de mettre des limites plus tard ». Elles vont livrer aujourd’hui. Votre travail est d’en faire un garde-fou :
vérifications CI pour Compose, contrôles d’admission pour Kubernetes, et alertes runtime pour « pas de limite définie ».

Erreurs courantes : symptômes → cause racine → correction

1) Symptomatique : la charge moyenne de l’hôte est énorme ; l’usage CPU semble modéré

Cause racine : Threads bloqués en IO wait lors de swap-in ou de reclaim direct. La charge compte les tâches exécutables + non interruptibles.

Correction : Confirmer avec vmstat/iostat/ps (état D). Réduire immédiatement la pression mémoire ; limiter/tuer le coupable ; ajouter memory.high.

2) Symptomatique : les conteneurs affichent « dans la limite mémoire », mais l’hôte swappe fortement

Cause racine : Les limites existent mais le swap est illimité, churn du page cache force un reclaim global, ou plusieurs conteneurs dépassent collectivement la capacité hôte.

Correction : Définir des limites de swap par cgroup (memory.swap.max) et des budgets mémoire réalistes. Ne pas sur-allouer sans politique explicite.

3) Symptomatique : pics de latence aléatoires sur des services sans lien

Cause racine : Reclaim global et encombrement IO créent du couplage inter-services. Un coupable mémoire punis tout le monde.

Correction : Isoler les charges bruyantes sur des nœuds/pools séparés ; appliquer des limites ; surveiller PSI.

4) Symptomatique : « On a ajouté du swap et c’était pire »

Cause racine : Plus de swap augmente le temps pendant lequel un hôte peut rester dans un état de pagination dégradé, amplifiant la latence queue tail et la confusion opérationnelle.

Correction : Traiter le swap comme un tampon d’urgence, pas une capacité. Préférer OOM/restart pour les services sensibles, ou isoler les jobs batch.

5) Symptomatique : des kills OOM surviennent mais l’hôte reste lent ensuite

Cause racine : Le swap reste peuplé ; le reclaim et le refaulting continuent ; la file IO est toujours en drainage ; les caches sont fragmentés.

Correction : Après suppression du coupable, laisser du temps pour la récupération ; réduire la pression IO ; envisager temporairement de vider les caches seulement en dernier recours et en comprenant bien le rayon d’impact.

6) Symptomatique : « La mémoire libre » est proche de zéro ; quelqu’un panique

Cause racine : Linux utilise la RAM pour le page cache ; peu de mémoire libre est normal quand la mémoire disponible est saine et la PSI basse.

Correction : Éduquer les équipes à utiliser available et PSI, pas free. Alerter sur la pression et le churn, pas sur l’esthétique.

7) Symptomatique : après activation de zswap/zram, le CPU a augmenté et le débit a chuté

Cause racine : Surcharge de compression plus pression mémoire continue. Vous avez déplacé le coût du disque vers le CPU.

Correction : N’activer que si vous avez du CPU disponible ; borner l’usage du swap ; corriger le budget mémoire réel.

8) Symptomatique : « Swappiness=1 a résolu (jusqu’à la semaine suivante) »

Cause racine : La réduction du swap a masqué un mauvais dimensionnement mémoire temporairement ; la pression existe toujours et peut maintenant provoquer des OOM abrupts.

Correction : Dimensionner les charges, définir des limites, ajouter memory.high/backpressure. Tuner swappiness en finition, pas en acte d’ouverture.

Listes de vérification / plan pas à pas

Réponse immédiate à l’incident (15 minutes)

  1. Exécutez vmstat 1 et cat /proc/pressure/memory. Confirmez paging actif + blocages.
  2. Exécutez iostat -xz 1. Confirmez la saturation disque / await.
  3. Trouvez le coupable : docker stats, puis swap par processus avec smem ou inspectez les événements cgroup.
  4. Atténuez :
    • Réduire la concurrence / le trafic.
    • Arrêter le conteneur coupable ou le redémarrer avec une empreinte mémoire réduite.
    • Si besoin, déplacer temporairement le coupable sur un hôte dédié.
  5. Vérifiez la récupération : les taux swap-in/out diminuent, full PSI approche ~0, la latence IO revient à la normale.

Plan de stabilisation (même jour)

  1. Définir des limites mémoire pour tout conteneur sans limite.
  2. En cgroup v2 : ajouter memory.high pour les workloads rafales afin de réduire le thrash.
  3. Borner le swap là où c’est approprié (memory.swap.max), surtout pour les services sensibles à la latence.
  4. Revoir l’usage de --oom-kill-disable ; le retirer sauf raison très spécifique.
  5. Réajuster la séparation des rôles de nœuds : batch vs services sensibles ne devraient pas partager le même destin mémoire/IO.

Plan de durcissement (prochain sprint)

  1. Ajouter des alertes sur PSI mémoire (some et full) avec seuils soutenus.
  2. Ajouter des alertes sur taux swap-in/out, fautes majeures et await disque.
  3. Implémenter policy-as-code : linter pour Compose ou contrôles d’admission exigeant des limites mémoire.
  4. Documenter les budgets mémoire par service et le comportement au restart (que se passe-t-il en OOM, quelle est la récupération).
  5. Tester en charge avec pression mémoire : lancer des pics de concurrence et vérifier que l’hôte reste interactif.

FAQ

1) Le swap est-il toujours mauvais pour les hôtes Docker ?

Non. Le swap est un outil. Il est utile comme tampon d’urgence et pour les workloads batch. Il est dangereux quand il devient béquille pour
des services sous-dimensionnés et rend l’hôte « up » mais inutilisable.

2) Pourquoi mes conteneurs affichent-ils un CPU faible alors que le système est lent ?

Parce qu’ils sont bloqués. Dans les tempêtes de swap, les threads sont souvent en IO wait ou en reclaim direct. Votre graphe CPU ne montrera pas « attente disque »
à moins de regarder l’iowait, la PSI, ou les tâches bloquées.

3) Dois-je désactiver le swap pour éviter les tempêtes ?

Si vous exécutez des services sensibles à la latence et que vous avez de bonnes limites, désactiver le swap peut améliorer la prévisibilité.
Mais cela rend aussi les OOM plus probables. L’approche correcte est généralement : définir les limites et les politiques d’abord, puis décider de la posture swap.

4) Quelle est la différence d’impact utilisateur entre OOM et thrash de swap ?

L’OOM est abrupt et bruyant : un processus meurt et redémarre. Le thrash de swap est prolongé et discret : tout est lent, les timeouts s’enchaînent,
et vous obtenez des défaillances secondaires. Dans beaucoup de systèmes, un OOM contrôlé est le moindre mal.

5) Pourquoi ajouter de la RAM ne règle parfois pas le problème ?

Plus de RAM n’aide que si le working set tient et si vous réduisez le churn. Si la charge grandit pour remplir la mémoire (caches, heaps, compactions),
vous pouvez retrouver la même pression, simplement sur une toile plus grande.

6) Comment définir des limites de swap pour des conteneurs Docker en cgroup v2 ?

Les flags Docker peuvent être incohérents selon les environnements. La manière fiable est souvent d’utiliser les contrôles cgroup v2 directement via des scopes systemd
ou la configuration runtime, en s’assurant que memory.swap.max est défini pour le cgroup du conteneur. Validez en lisant le fichier dans /sys/fs/cgroup.

7) Pourquoi la « mémoire libre » est-elle basse même quand le système va bien ?

Linux utilise la RAM disponible pour le cache. Ce cache est reclaimable. Regardez la mémoire « available » et les métriques de pression, pas « free ».
Peu de mémoire libre + faible PSI est généralement normal.

8) Quelles métriques devrais-je surveiller pour détecter tôt les tempêtes de swap ?

PSI mémoire soutenue (/proc/pressure/memory), taux swap-in/out, fautes de page majeures, await/%util disque,
et événements mémoire par cgroup (high/max/oom). Les alertes doivent se déclencher sur des conditions soutenues, pas sur des pics d’une seconde.

9) Overlay2 ou le comportement du filesystem peuvent-ils contribuer aux tempêtes de swap ?

Indirectement, oui. Un churn important de métadonnées du filesystem et l’amplification d’écriture peuvent saturer l’IO, rendant le swap-in/out bien plus douloureux.
Cela ne crée pas la pression mémoire, mais transforme la pression mémoire en outage système plus rapidement.

10) zram est-il meilleur que le swap disque pour les conteneurs ?

zram évite l’IO disque en compressant dans la RAM. Il peut adoucir les tempêtes si vous avez du CPU libre, mais c’est toujours du swap—la latence augmente,
et cela peut masquer des problèmes de capacité. Utilisez-le comme tampon, pas comme permission.

Prochaines étapes pratiques

Si votre hôte Docker fond pendant que les conteneurs « fonctionnent », cessez de débattre de la moralité du swap. Traitez-le comme ce qu’il est :
un instrument de dette de performance avec un taux d’intérêt variable et un modèle compossé dangereux.

Faites ceci ensuite, dans cet ordre :

  1. Instrumentez la pression : ajoutez des alertes et dashboards basés sur PSI, en parallèle avec l’activité swap et la latence IO.
  2. Budgétez la mémoire par service : posez des limites réalistes ; retirez « illimité » comme valeur par défaut.
  3. Contrôlez le comportement du swap : bornez le swap par workload (surtout pour les sensibles à la latence) et envisagez le throttling memory.high.
  4. Préférez l’échec contrôlé : autorisez l’OOM cgroup + redémarrage pour les services qui peuvent récupérer, plutôt que le thrash hôte.
  5. Isolez les fauteurs de troubles : indexation batch, compaction et tout ce qui aime grossir ne doivent pas partager des nœuds avec des APIs basse latence.

Votre objectif n’est pas « ne jamais utiliser le swap ». Votre objectif est « ne jamais laisser le swap décider de votre timeline d’incident ». Le noyau fera ce que vous lui demandez.
Demandez quelque chose de vivable.

← Précédent
VM Windows sur Proxmox sans réseau : correctifs de pilote VirtIO qui fonctionnent
Suivant →
Debian 13 : timers systemd vs cron — migrer pour la fiabilité (et éviter les pièges fréquents)

Laisser un commentaire