Quand Linux indique que votre CPU est « inactif » mais que votre machine donne l’impression d’être submergée, vous êtes généralement face au même coupable : l’attente I/O. Vos cœurs ne sont pas occupés à calculer ; ils font la queue, en attendant que le stockage réponde. Pendant ce temps, un conteneur Docker mâche des écritures comme s’il était payé au fsync, et tout l’hôte se transforme en tragédie au ralenti.
C’est la partie inconfortable de la densité de conteneurs : un voisin bruyant peut ralentir tout le monde. La bonne nouvelle, c’est que Linux vous fournit les outils pour identifier le responsable puis le brider de façon chirurgicale — sans redémarrer, sans deviner, et sans « ajoutons des disques » comme première réponse.
Un modèle mental : ce que signifie vraiment l’attente I/O sur un hôte de conteneurs
Sur Linux, l’attente I/O est le temps pendant lequel le CPU reste inactif alors qu’ il existe au moins une requête I/O en attente dans le système. Ce n’est pas une mesure de l’utilisation du disque en soi. C’est une mesure du fait que « le CPU voulait exécuter quelque chose, mais des tâches sont bloquées sur de l’I/O, donc le planificateur enregistre de l’attente ».
Sur un hôte Docker, ce « bloqué sur I/O » signifie souvent :
- Des écritures sur l’overlay filesystem amplifiées en plusieurs écritures réelles.
- Du spam de logs forçant des écritures synchrones sur un système de fichiers chaud.
- Une base de données effectuant des fsync intensifs en boucle serrée.
- Un job de sauvegarde qui lit en streaming et affame les écritures (ou l’inverse) selon le planificateur et le périphérique.
- Des tempêtes de métadonnées : des millions de petits fichiers, des recherches de répertoires, des mises à jour d’inodes, pression sur le journal.
Les conteneurs n’ont pas de stockage magique. Ils partagent les mêmes périphériques bloc de l’hôte, la même file d’attente, et souvent le même journal de système de fichiers. Si un workload martèle la file avec une I/O profonde et sans mécanismes d’équité, tous les autres workloads commencent à faire la queue aussi. Le noyau tentera d’être équitable, mais équité ne veut pas dire « votre latence p99 reste acceptable ».
Les deux variantes de « l’attente I/O infernale »
Enfer de latence : les IOPS ne sont pas très élevés, mais la latence explose. Pensez : bugs firmware NVMe, vidages de cache d’un contrôleur RAID, étouffement du journal de système de fichiers, ou écritures heavy sync. Les utilisateurs ressentent cela comme « tout est bloqué » même si les graphes de débit paraissent modestes.
Enfer de profondeur de file : un conteneur garde le périphérique saturé avec de nombreuses requêtes en vol. Le périphérique est « occupé » et le débit moyen semble impressionnant. Pendant ce temps, les tâches interactives et les autres conteneurs patientent derrière une montagne de requêtes.
Une citation à garder sur vous
Idée paraphrasée : « L’espoir n’est pas une stratégie ; il vous faut un plan et des boucles de rétroaction. »
— Gene Kranz (esprit opérations de mission, paraphrasé)
C’est l’attitude à adopter ici. Ne minimisez pas le problème. Mesurez, attribuez, puis appliquez un contrôle.
Playbook de diagnostic rapide (vérifiez ceci en premier)
Si votre hôte fond, vous n’avez pas le temps pour de la danse interprétative. Faites ceci dans l’ordre.
1) Confirmez que c’est la latence/la file du stockage, pas le CPU ou la mémoire
- Vérifiez la charge système et l’attente I/O.
- Vérifiez la latence disque et la profondeur de file.
- Vérifiez la pression mémoire (les tempêtes de swap peuvent ressembler à des tempêtes I/O parce qu’elles le sont).
2) Identifiez les processus top I/O sur l’hôte
- Utilisez
iotop(taux lecture/écriture par processus). - Utilisez
pidstat -d(statistiques I/O par processus dans le temps). - Utilisez
lsofpour voir quels fichiers sont martelés (logs ? fichiers de base de données ? diff overlay ?).
3) Faites le lien entre ces processus et les conteneurs
- Trouvez l’ID du conteneur via les cgroups (
/proc/<pid>/cgroup). - Ou mappez les chemins de fichiers vers
/var/lib/docker/overlay2et les répertoires diff.
4) Appliquez la limitation la moins pire
- Si vous êtes en cgroup v2 :
io.maxetio.weight. - Si vous êtes en cgroup v1 : throttling blkio et poids (fonctionne mieux sur périphériques bloc directs, moins magique sur fichiers empilés).
- Réduisez aussi les dégâts : limitez les logs, déplacez les chemins chauds sur des disques séparés, et corrigez le comportement de l’application (fréquence de flush, batching, etc.).
Blague #1 : Si votre attente I/O est à 60 %, vos CPU ne sont pas « paresseux » — ils sont juste coincés dans la file d’attente la plus lente du monde.
Faits et contexte intéressants (pourquoi ce problème revient)
- « iowait » de Linux n’est pas le temps passé par le disque. C’est le temps CPU inactif pendant que l’I/O est en attente, qui peut augmenter même sur des périphériques rapides si des tâches se bloquent de façon synchrone.
- Les cgroups pour le contrôle I/O sont arrivés après les contrôles CPU/mémoire. Les premières configurations de conteneurs géraient bien le CPU et la mémoire, et très mal l’équité de stockage.
- Le throttling blkio fonctionnait historiquement mieux avec un accès direct aux périphériques bloc. Quand tout passe par une couche système de fichiers (comme overlay2), l’attribution et le contrôle deviennent plus flous.
- Le planificateur Completely Fair Queuing (CFQ) était autrefois le défaut d’équité pour les médias rotatifs. Les noyaux modernes penchent vers des planificateurs comme BFQ ou mq-deadline selon la classe de périphérique et les objectifs.
- L’amplification d’écriture d’OverlayFS est réelle. Une « petite écriture » dans un conteneur peut déclencher des copy-up et des écritures métadonnées sur le système de fichiers hôte.
- Le journal ext4 peut devenir un goulot sous des workloads riches en métadonnées. Beaucoup de créations/suppressions de fichiers peuvent stresser le journal même si le débit de données est faible.
- La journalisation est un récidiviste depuis l’ère des démons. La seule chose plus infinie que l’optimisme humain est un fichier de log sans limite sur un disque partagé.
- NVMe n’est pas à l’abri des pics de latence. Firmware, throttling thermique, gestion d’énergie et garbage collection au niveau du périphérique peuvent encore produire des latences pénibles en queue.
- La « load average » inclut les tâches en sommeil ininterruptible (état D). Les blocages de stockage peuvent gonfler la charge même quand l’usage CPU semble correct.
Tâches pratiques : commandes, sorties et décisions (12+)
Voici des outils de terrain. Chaque tâche inclut : une commande, une sortie typique, ce que ça signifie, et la décision que vous prenez ensuite. Exécutez-les en root ou avec les privilèges suffisants si nécessaire.
Task 1: Confirmer les symptômes d’iowait et de charge
cr0x@server:~$ uptime
14:22:05 up 21 days, 6:11, 2 users, load average: 28.12, 24.77, 19.03
cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (server) 01/02/2026 _x86_64_ (32 CPU)
14:22:07 CPU %usr %nice %sys %iowait %irq %soft %steal %idle
14:22:08 all 3.21 0.00 1.18 62.44 0.00 0.09 0.00 33.08
14:22:09 all 2.97 0.00 1.11 64.02 0.00 0.07 0.00 31.83
14:22:10 all 3.45 0.00 1.29 61.88 0.00 0.08 0.00 33.30
Ce que ça signifie : la charge moyenne est énorme, mais l’usage user+system CPU est faible et l’iowait est ~60 %. Beaucoup de tâches sont bloquées sur le stockage.
Décision : traitez cela comme un problème de latence/file du stockage. Passez aux métriques au niveau disque ; ne perdez pas de temps à tuner le CPU.
Task 2: Vérifier la latence disque et la profondeur de file avec iostat
cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (server) 01/02/2026 _x86_64_ (32 CPU)
Device r/s w/s rkB/s wkB/s await r_await w_await aqu-sz %util
nvme0n1 120.0 980.0 5120.0 82400.0 86.32 14.22 95.01 22.41 99.50
Ce que ça signifie : le périphérique est pratiquement saturé (%util ~99.5). La latence d’écriture moyenne est catastrophique (~95ms) et la profondeur de file (aqu-sz) est élevée.
Décision : vous avez besoin d’attribution. Trouvez quels processus génèrent l’I/O.
Task 3: Vérifier si des tâches sont bloquées en état D (attente stockage)
cr0x@server:~$ ps -eo state,pid,comm,wchan:32 --sort=state | head -n 15
D 18342 postgres io_schedule
D 19011 containerd-shim io_schedule
D 19102 python3 ext4_sync_file
D 24410 java io_schedule
R 1322 ksoftirqd/3 -
R 31201 iostat -
S 1 systemd ep_poll
Ce que ça signifie : des processus en état D sont bloqués dans des chemins I/O du noyau (io_schedule, ext4_sync_file).
Décision : trouvez lesquels appartiennent aux conteneurs bruyants.
Task 4: Obtenir les taux I/O par processus (liste de blâme rapide)
cr0x@server:~$ iotop -oPa -d 1 -n 3
Total DISK READ: 5.00 M/s | Total DISK WRITE: 90.00 M/s
PID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
19102 be/4 root 0.00 B/s 45.00 M/s 0.00 % 95.00 % python3 /app/worker.py
18342 be/4 postgres 0.00 B/s 20.00 M/s 0.00 % 80.00 % postgres: checkpointer
24410 be/4 app 0.00 B/s 12.00 M/s 0.00 % 60.00 % java -jar service.jar
Ce que ça signifie : un worker Python domine les écritures ; le checkpointer Postgres est aussi actif (peut-être normal, peut-être poussé par autre chose).
Décision : mappez le PID 19102 à un conteneur et inspectez ce qu’il écrit.
Task 5: Mapper un PID à un conteneur Docker via les cgroups
cr0x@server:~$ cat /proc/19102/cgroup
0::/docker/4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d
Ce que ça signifie : le processus est à l’intérieur du conteneur 4c3f1f8f2a7b....
Décision : identifiez le nom humain, l’image et le rôle de ce conteneur.
Task 6: Résoudre l’ID du conteneur en nom et inspecter la configuration
cr0x@server:~$ docker ps --no-trunc --filter id=4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d acme/etl:2.8.1 "python3 /app/wo..." 3 hours ago Up 3 hours etl-worker-07
cr0x@server:~$ docker inspect -f '{{.HostConfig.LogConfig.Type}} {{json .HostConfig.LogConfig.Config}}' etl-worker-07
json-file {"max-file":"0","max-size":"0"}
Ce que ça signifie : le conteneur est un worker ETL et utilise la journalisation json-file par défaut avec aucune rotation (max-size 0, max-file 0).
Décision : suspecter des écritures induites par les logs et/ou des écritures sur un chemin monté chaud. Vérifiez quels fichiers sont actifs.
Task 7: Voir quels fichiers le PID bruyant écrit
cr0x@server:~$ lsof -p 19102 | awk '{print $4, $9}' | head -n 15
1u /dev/null
2u /dev/null
3w /var/lib/docker/containers/4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d/4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d-json.log
4w /data/spool/outgoing/events.ndjson
5w /data/spool/outgoing/events.ndjson
Ce que ça signifie : il écrit dans le fichier de log json de Docker et aussi dans un chemin monté sur l’hôte /data/spool/outgoing/events.ndjson.
Décision : vérifiez l’utilisation du système de fichiers, les options de montage, et si ce chemin partage le même périphérique que tout le reste.
Task 8: Déterminer quel système de fichiers/périphérique soutient le chemin chaud
cr0x@server:~$ df -T /var/lib/docker /data/spool/outgoing
Filesystem Type 1K-blocks Used Available Use% Mounted on
/dev/nvme0n1p2 ext4 960379012 712883120 198472344 79% /
/dev/nvme0n1p2 ext4 960379012 712883120 198472344 79% /
Ce que ça signifie : la storage Docker et le répertoire spool partagent le même système de fichiers ext4 sur le même périphérique.
Décision : toute tempête d’écriture punira l’hôte entier. Si vous ne pouvez pas déplacer immédiatement les chemins vers un périphérique différent, le throttling devient votre frein d’urgence.
Task 9: Vérifier le driver de stockage de Docker (le comportement d’overlay2 compte)
cr0x@server:~$ docker info | sed -n '1,40p'
Client:
Version: 25.0.3
Server:
Containers: 47
Running: 39
Storage Driver: overlay2
Backing Filesystem: extfs
Logging Driver: json-file
Cgroup Driver: systemd
Cgroup Version: 2
Ce que ça signifie : overlay2 sur ext4, logs json-file, cgroup v2 avec driver systemd. C’est une bonne base pour utiliser io.max/io.weight en cgroup v2.
Décision : bridez via cgroup v2 (préféré) et corrigez la journalisation.
Task 10: Vérifier la pression mémoire et le swap (ne pas manquer l’évidence)
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 125Gi 96Gi 2.1Gi 1.2Gi 27Gi 18Gi
Swap: 16Gi 9.5Gi 6.5Gi
cr0x@server:~$ vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 18 9961472 2200000 210000 22400000 80 150 1200 92000 1200 3100 3 1 34 62 0
1 20 9961600 2180000 210000 22390000 60 120 1100 89000 1180 3000 3 1 32 64 0
Ce que ça signifie : le swap est utilisé et il y a I/O de swap (si/so). Ça peut amplifier la douleur disque, mais le problème dominant est une grosse sortie bloc (bo) et un wa élevé.
Décision : gardez le swap en tête, mais poursuivez l’attribution I/O. Si le swap thrash, envisagez aussi de limiter la mémoire du coupable.
Task 11: Identifier le chemin cgroup du conteneur (driver systemd, cgroup v2)
cr0x@server:~$ systemctl status docker | sed -n '1,12p'
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-12-10 08:14:10 UTC; 3 weeks 2 days ago
Docs: man:docker(1)
Main PID: 1420 (dockerd)
cr0x@server:~$ cat /proc/19102/cgroup
0::/system.slice/docker-4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d.scope
Ce que ça signifie : avec systemd + cgroup v2, le conteneur vit dans une unité scope systemd sous /sys/fs/cgroup/system.slice/.
Décision : appliquez des limites I/O à cette unité scope via les fichiers cgroup (ou les propriétés systemd), pas au hasard.
Task 12: Localiser le major:minor du périphérique bloc pour le système de fichiers
cr0x@server:~$ findmnt -no SOURCE,TARGET,FSTYPE /var/lib/docker
/dev/nvme0n1p2 / ext4
cr0x@server:~$ lsblk -o NAME,MAJ:MIN,SIZE,TYPE,MOUNTPOINT /dev/nvme0n1p2
NAME MAJ:MIN SIZE TYPE MOUNTPOINT
nvme0n1p2 259:2 915G part /
Ce que ça signifie : votre périphérique bloc cible est major:minor 259:2. Les contrôles I/O cgroup v2 exigent cet identifiant.
Décision : définissez io.max et/ou io.weight pour ce périphérique sur le cgroup du conteneur.
Task 13: Appliquer une limitation de bande passante cgroup v2 (confinement immédiat)
cr0x@server:~$ CG=/sys/fs/cgroup/system.slice/docker-4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d.scope
cr0x@server:~$ echo "259:2 wbps=20971520 rbps=10485760" | sudo tee $CG/io.max
259:2 wbps=20971520 rbps=10485760
cr0x@server:~$ cat $CG/io.max
259:2 rbps=10485760 wbps=20971520
Ce que ça signifie : lectures plafonnées à 10 MiB/s, écritures plafonnées à 20 MiB/s pour ce conteneur contre le périphérique racine.
Décision : surveillez la latence et la réactivité du système. Si l’hôte se rétablit, vous avez confirmé le « voisin bruyant » et gagné du temps pour une vraie correction.
Task 14: Vérifier l’amélioration immédiatement
cr0x@server:~$ iostat -x 1 3
Device r/s w/s rkB/s wkB/s await aqu-sz %util
nvme0n1 110.0 260.0 4800.0 21000.0 12.40 2.10 78.00
cr0x@server:~$ mpstat 1 3 | tail -n 4
14:25:08 all 6.01 0.00 2.10 8.33 0.00 0.12 0.00 83.44
Ce que ça signifie : la latence du périphérique a chuté (await ~12ms) et l’iowait a fortement diminué (~8%). L’hôte respire à nouveau.
Décision : gardez la limite comme garde-fou temporaire, puis corrigez la cause racine : journalisation, batching, emplacement des données, ou conception de l’application.
Task 15: Mettre en place la rotation des logs (arrêter l’hémorragie)
cr0x@server:~$ docker update --log-opt max-size=50m --log-opt max-file=3 etl-worker-07
etl-worker-07
cr0x@server:~$ docker inspect -f '{{json .HostConfig.LogConfig}}' etl-worker-07
{"Type":"json-file","Config":{"max-file":"3","max-size":"50m"}}
Ce que ça signifie : les logs seront roulés et ne grossiront plus sans limite.
Décision : si les logs étaient un contributeur majeur, vous pourrez réduire la limite I/O ou la supprimer après confirmation de la stabilité.
Task 16: Trouver quels conteneurs génèrent le plus de churn sur la couche écrivable
cr0x@server:~$ docker ps -q | while read c; do
> name=$(docker inspect -f '{{.Name}}' $c | sed 's#^/##')
> rw=$(docker inspect -f '{{.GraphDriver.Data.UpperDir}}' $c 2>/dev/null)
> if [ -n "$rw" ]; then
> sz=$(sudo du -sh "$rw" 2>/dev/null | awk '{print $1}')
> echo "$sz $name"
> fi
> done | sort -h | tail -n 8
3.1G etl-worker-07
4.8G api-gateway
6.2G report-builder
9.7G search-indexer
Ce que ça signifie : les couches écrivables (UpperDir) montrent un churn important. Tout le churn n’est pas mauvais, mais c’est un signe.
Décision : déplacez les chemins à écriture intensive vers des volumes ou bind mounts, et réduisez les écritures dans la couche écrivable du conteneur.
Faire le lien entre la douleur I/O et le conteneur coupable
La partie la plus difficile de « Docker cause une attente I/O » est que le disque ne sait pas ce qu’est un conteneur. Le noyau connaît les PID, inodes, périphériques bloc et cgroups. Votre travail consiste à relier ces points.
Commencez du disque et remontez
Si iostat montre un périphérique unique avec un await horrible et un %util élevé, vous faites face soit à une saturation soit à une latence au niveau du périphérique. À partir de là :
- Utilisez
iotoppour identifier les PID qui écrivent le plus. - Mappez PID → cgroup → ID de conteneur.
- Utilisez
lsofpour comprendre le chemin : logs, volume, UpperDir d’overlay, fichiers de base de données.
Commencez du conteneur et descendez
Parfois vous suspectez déjà le coupable (« c’est le job batch, non ? »). Validez sans vous auto-illusionner :
- Inspectez la configuration des logs du conteneur et les mounts de volumes.
- Vérifiez la taille de son fichier de log json et de sa couche écrivable.
- Vérifiez s’il s’agit d’une application sync-heavy (bases de données, brokers de messages, tout ce qui valorise la durabilité).
Comprendre la marque spéciale de chaos d’overlay2
overlay2 est efficace pour de nombreux workloads, mais les patterns à écriture intensive peuvent coûter cher. Les écritures sur des fichiers présents dans la couche inférieure (image) déclenchent un copy-up dans la couche supérieure. Les opérations métadonnées peuvent aussi exploser, surtout pour des workloads touchant beaucoup de petits fichiers.
Si vous voyez de lourdes écritures sous /var/lib/docker/overlay2, ne cherchez pas à « optimiser overlay2 ». Le bon mouvement est généralement d’arrêter d’écrire là. Placez les données mutables sur des volumes ou bind mounts où vous pouvez contrôler les systèmes de fichiers, les options de montage et l’isolation I/O.
Blague #2 : Les systèmes de fichiers overlay sont comme la politique de bureau — tout semble simple jusqu’à ce que vous changiez une petite chose et soudain trois départements sont impliqués.
Options de limitation qui fonctionnent réellement (et quoi éviter)
Vous avez trois classes de contrôles : niveau conteneur (cgroups), niveau hôte (planificateur I/O et choix de système de fichiers), et niveau application (comment le workload écrit). Si vous ne faites qu’une chose, commencez par les cgroups pour arrêter le rayon d’impact.
Option A : cgroup v2 io.max (plafonds durs)
Sur les distributions modernes et les configurations Docker utilisant cgroup v2, io.max est votre levier le plus direct : définissez une bande passante maximale lecture/écriture (et des limites IOPS sur certaines configurations) par périphérique bloc.
Quand l’utiliser : l’hôte est non réactif et vous avez besoin d’un confinement immédiat ; un job batch peut être ralenti sans casser la cohérence.
Compromis : c’est brutal. Si vous plafonnez trop bas, vous pouvez provoquer des timeouts dans le conteneur limité. Cela peut néanmoins être préférable à la mise hors service de tout le reste.
Option B : cgroup v2 io.weight (équité relative)
io.weight est plus doux qu’un plafond dur : il indique au noyau « ce groupe a moins/plus d’importance ». Si vous avez plusieurs workloads et que vous voulez qu’ils partagent équitablement, les poids peuvent être meilleurs que des limites strictes.
Quand l’utiliser : vous souhaitez protéger des services sensibles à la latence tout en laissant les jobs batch utiliser la capacité résiduelle.
Compromis : si le périphérique est saturé par un job et que les autres sont légers, le poids peut ne pas suffire ; vous pourriez quand même avoir besoin d’un plafond.
Option C : flags legacy blkio de Docker (ère cgroup v1)
Les options Docker --blkio-weight, --device-read-bps, --device-write-bps et consorts ont été conçues pour cgroup v1. Elles peuvent encore aider selon votre noyau, périphérique et pilote, mais elles sont moins prévisibles une fois que des systèmes de fichiers empilés et des sous-systèmes bloc multi-queue modernes sont impliqués.
Conseil d’opinion : si vous êtes en cgroup v2, préférez io.max/io.weight sur le système de fichiers cgroup. Traitez les flags blkio Docker comme « fonctionnent en laboratoire » à moins d’avoir validé sur votre pile stockage et noyau.
Option D : propriétés systemd pour les scopes Docker (automatisation plus propre)
Si les conteneurs apparaissent en tant qu’unités scope systemd, vous pouvez définir des propriétés via systemctl set-property. Cela évite d’écrire manuellement dans les fichiers cgroup, et cela survit mieux à certains workflows opérationnels.
Option E : corriger la cause racine (la seule victoire permanente)
Le throttling sert à la contention. Les gains permanents ressemblent à :
- Déplacer les chemins à écriture intensive vers des volumes dédiés sur des périphériques séparés.
- Limiter et router les logs (rotation, compression, ou expédition hors hôte).
- Réduire la fréquence de sync : batcher les écritures, utiliser des réglages de group commit (avec prudence), ou changer explicitement la posture de durabilité.
- Choisir des systèmes de fichiers adaptés au workload (ou au moins des options de montage adéquates).
- Cesser d’écrire des millions de petits fichiers si vous pouvez les stocker en segments plus gros.
Ce qu’il faut éviter
- Ne « corrigez » pas l’iowait en ajoutant du CPU. C’est comme acheter une voiture plus rapide pour rester coincé dans un embouteillage plus dense.
- Ne désactivez pas le journaling ou les fonctionnalités de durabilité à la légère. Vous pouvez absolument améliorer la performance — jusqu’à ce que vous découvriez ce qu’est une coupure d’alimentation.
- Ne blâmez pas Docker en tant que concept. Docker n’est que le messager. Le véritable ennemi est le stockage partagé non gouverné.
Trois mini-récits d’entreprise tirés des mines I/O
1) Incident causé par une mauvaise hypothèse : « C’est dans un conteneur, donc c’est isolé »
L’équipe avait un hôte chargé : services API, quelques jobs en arrière-plan, et un conteneur importateur « temporaire » qui récupérait des données partenaires la nuit. L’importateur a été déployé sans limites explicites. Personne ne s’en est soucié. Après tout, c’était dans Docker.
La première nuit où il a tourné sur l’hôte de production partagé, des alertes de latence ont touché tout le monde. Pas seulement l’importateur. Les APIs ont ralenti, la pipeline métriques a pris du retard, les sessions SSH ont gelé en plein commande. Les graphes CPU semblaient « normaux », ce qui rend ce mode de défaillance si déconcertant : les CPU attendaient poliment que le stockage réponde.
L’ingénieur d’astreinte a cherché les logs applicatifs et les dépendances en amont pendant vingt minutes parce que les symptômes ressemblaient à une panne distribuée. Ce n’est qu’après avoir vérifié iostat que le tableau est devenu clair : un périphérique NVMe fixé près de 100% d’utilisation, avec une latence d’écriture en pic. Puis iotop a pointé un seul processus enchaînant des écritures constantes.
La mauvaise hypothèse était subtile : « les conteneurs isolent les ressources par défaut. » Ils ne le font pas. Le CPU et la mémoire peuvent être limités facilement, mais le stockage est partagé à moins que vous n’appliquiez des contrôles ou ne sépariez les périphériques sous-jacents. La correction de la nuit a été un plafond cgroup d’urgence. La correction à long terme était encore moins excitante : l’importateur a eu son propre volume sur un périphérique séparé, plus une limite I/O comme ceinture de sécurité.
2) Optimisation qui s’est retournée : « Plus de concurrence signifie des imports plus rapides »
Une équipe plateforme data a essayé d’accélérer un job de transformation. Ils ont doublé la concurrence des workers et sont passés à des fichiers découpés plus petits pour augmenter le parallélisme. Sur le papier, ça aurait dû être plus rapide : plus de workers, plus de débit, moins de temps mort.
En production, c’est devenu une grenade de latence. Le job produisait des milliers de petits fichiers et effectuait fréquemment des appels fsync pour « être sûr ». Le journal du système de fichiers a commencé à dominer. Le débit n’a pas doublé ; il s’est effondré. Pire, le reste de l’hôte a souffert parce que le job gardait la file de stockage saturée de petites opérations qui sont du poison pour la latence en queue.
Le retour de bâton n’était pas seulement « trop de charge ». C’était la forme de la charge. Beaucoup de petites opérations synchrones sont une bête différente des grosses écritures séquentielles. Sur du stockage moderne, vous pouvez quand même vous noyer dans des mises à jour de métadonnées et des flushs barrières. Votre NVMe rapide peut devenir un disque tournant très cher si vous l’utilisez comme sac de frappe d’écritures aléatoires.
Ils ont récupéré en réduisant la concurrence, en batchant les sorties en segments plus gros, et en écrivant sur un volume sur un système de fichiers séparé et tuné pour le workload. Puis ils ont ajouté un plafond I/O pour que de futures « optimisations » ne puissent pas prendre l’hôte en otage. La note finale du postmortem était franche : optimiser un job sans définir les dommages collatéraux acceptables n’est pas de l’optimisation ; c’est du jeu.
3) Pratique ennuyeuse mais correcte qui a sauvé la mise : QoS par service et disques séparés
Une autre organisation exploitait une flotte mixte : services web, caches, quelques bases de données stateful, et analytics périodique. Ils avaient déjà été brûlés, alors ils ont bâti un règlement ennuyeux.
Les services stateful ont obtenu des volumes dédiés sur des périphériques séparés. Aucune exception. Les workloads batch tournaient dans des conteneurs avec des poids et plafonds I/O prédéfinis. La journalisation était limitée par défaut. L’équipe plateforme maintenait même un simple runbook commençant par iostat, iotop et « mapper PID à cgroup ». Rien de fancy.
Un après-midi, une nouvelle image conteneur est sortie avec la journalisation de debug activée. Elle a commencé à écrire agressivement, mais l’hôte n’a pas fondu. Le conteneur de logs a atteint son plafond, s’est ralenti, et le reste de la flotte a continué à servir. L’astreinte a quand même dû corriger la mauvaise configuration, mais c’était un incident contenu, pas une panne.
La pratique n’était pas glamour : allouer le stockage intentionnellement, appliquer la QoS, et imposer des valeurs par défaut. Mais c’est à quoi ressemble la fiabilité — moins d’héroïsme, plus de garde-fous.
Erreurs courantes : symptôme → cause racine → correction
1) Symptom : la charge moyenne est énorme, l’utilisation CPU est faible
Cause racine : tâches bloquées en état D sur le stockage ; la load inclut le sommeil ininterruptible.
Correction : confirmez avec ps/vmstat ; identifiez les PID top I/O ; mappez au conteneur ; appliquez io.max ou des poids ; puis adressez le comportement d’écriture.
2) Symptom : disque à 100% utilisé mais le débit n’est pas impressionnant
Cause racine : I/O aléatoire petite taille, tempêtes de métadonnées, contention du journal, ou flushs fréquents causant un fort overhead par opération.
Correction : réduisez le churn des fichiers ; batcher les écritures ; déplacez les chemins chauds sur un volume tuné ; envisagez BFQ pour l’équité sur certains périphériques ; plafonnez le coupable.
3) Symptom : un seul conteneur « a l’air ok » sur CPU/mémoire mais l’hôte est inutilisable
Cause racine : absence d’isolation I/O ; le conteneur martèle le périphérique partagé (logs, fichiers temporaires, checkpoints de base de données).
Correction : imposez la QoS I/O via les cgroups ; plafonnez les logs ; placez les chemins à écriture intensive sur des volumes séparés.
4) Symptom : docker logs est lent et /var/lib/docker grossit vite
Cause racine : journalisation json-file sans rotation ; fichier de log énorme provoquant des écritures et mises à jour métadonnées supplémentaires.
Correction : définissez max-size et max-file ; expédiez les logs ailleurs ; désactivez la journalisation debug en production par défaut.
5) Symptom : après « throttling », l’app commence à timeout
Cause racine : plafonds durs trop bas pour les exigences latence/durabilité du workload, ou le workload attend des bursts I/O.
Correction : augmentez les limites jusqu’à ce que les SLO se rétablissent ; préférez les poids aux plafonds stricts pour les workloads mixtes ; corrigez le batching et le backpressure applicatif.
6) Symptom : iowait est élevé mais iostat semble normal
Cause racine : l’I/O peut se produire sur un périphérique différent (loopback, stockage réseau), ou le goulot est dans des locks/metadonnées du système de fichiers pas visibles clairement dans des stats périphériques simples.
Correction : vérifiez les montages avec findmnt ; vérifiez tous les périphériques avec iostat -x ; utilisez pidstat -d et lsof pour localiser les chemins ; vérifiez séparément les volumes réseau.
7) Symptom : un conteneur lit beaucoup et les écritures sont affamées (ou inversement)
Cause racine : comportement du planificateur et contention de la file ; des patterns mixtes lecture/écriture peuvent provoquer de l’injustice et des pics de queue.
Correction : appliquez des plafonds séparés lecture/écriture dans io.max ; isolez les workloads sur des périphériques séparés ; planifiez les lectures batch hors-peak.
Listes de contrôle / plan étape par étape
Confinement d’urgence (15 minutes)
- Confirmez l’iowait et la latence du périphérique :
mpstat,iostat -x. - Trouvez les PID top I/O :
iotop -oPa,pidstat -d 1. - Mappez PID → conteneur via
/proc/<pid>/cgroupetdocker ps. - Appliquez un plafond
io.maxcgroup v2 pour le conteneur sur le périphérique incriminé. - Vérifiez l’amélioration : iowait en baisse, await en baisse, réactivité de l’hôte rétablie.
- Communiquez clairement : « Nous avons bridé le conteneur X pour stabiliser l’hôte ; le workload Y peut tourner plus lentement. »
Stabilisation (même jour)
- Corrigez la journalisation : limitez les logs json-file ou passez à un driver/agent qui n’affecte pas le disque racine.
- Déplacez les données à écriture intensive hors de la couche écrivable overlay vers un volume ou bind mount.
- Vérifiez le remplissage du système de fichiers : espace libre et usage d’inodes. Les disques pleins provoquent des griefs de performance.
- Confirmez le comportement du swap ; si le swap est massif, ajoutez des limites mémoire au coupable ou ajustez le workload.
- Documentez précisément les réglages cgroup appliqués pour pouvoir reproduire et revenir en arrière.
Durcissement (ce sprint)
- Définissez des limites de journalisation par défaut pour tous les conteneurs (policy, pas une suggestion).
- Définissez des classes I/O : services sensibles à la latence vs batch/ETL ; appliquez poids/plafonds en conséquence.
- Séparez le stockage pour les services stateful ; n’hébergez pas les fichiers de base de données sur le stockage runtime du conteneur si possible.
- Rédigez un runbook : « mapper PID à conteneur » avec les commandes exactes pour votre mode cgroup.
- Ajoutez des alertes sur la latence disque (
await) et la profondeur de file, pas seulement sur le débit et le %util.
FAQ
1) Pourquoi une forte iowait fait-elle geler SSH et les commandes simples ?
Parce que ces commandes ont aussi besoin du disque : lecture des binaires, écriture de l’historique shell, mise à jour des logs, toucher des fichiers, pagination mémoire. Quand la file disque est profonde ou que la latence monte, tout ce qui a besoin d’I/O se bloque.
2) L’iowait est-il toujours le signe que le disque est « lent » ?
Non. Cela peut indiquer un comportement applicatif sync-heavy, une contention du système de fichiers, une activité swap, ou une saturation de la file du périphérique. Confirmez avec des métriques de latence/profondeur comme await et aqu-sz.
3) J’ai plafonné la bande écriture d’un conteneur et l’hôte s’est amélioré. Est-ce la preuve que le conteneur est la cause racine ?
Cela prouve qu’il contribue largement à la file du périphérique. La cause racine peut être architecturale : disques partagés, paramètres de log par défaut, ou un design qui convertit de petites écritures en tempêtes de flush.
4) Dois-je utiliser des limites IOPS ou de bande passante ?
Les limites de bande passante sont un bon outil brutal pour les workloads en streaming. Les limites IOPS peuvent mieux convenir aux I/O aléatoires et workloads riches en métadonnées. Utilisez ce qui correspond à la douleur : si la latence explose sur de petites opérations, les caps IOPS peuvent aider davantage.
5) Les flags blkio de Docker fonctionnent-ils avec overlay2 ?
Parfois, mais pas de façon fiable au point d’y laisser votre budget d’incident. Avec cgroup v2, préférez io.max/io.weight sur le scope du conteneur. Validez dans votre environnement.
6) Si je déplace des données sur un volume, pourquoi cela aide-t-il ?
Vous évitez l’amplification d’écriture d’overlay et vous gagnez du contrôle : vous pouvez placer le volume sur un périphérique différent, choisir les options de système de fichiers, et isoler l’I/O plus proprement.
7) Puis-je résoudre ça en changeant le planificateur I/O ?
Parfois, vous pouvez améliorer l’équité ou les queues de latence, mais les réglages de planificateur ne vous sauveront pas d’un écrivain sans limites sur un disque partagé. Bridez d’abord ; tunez ensuite.
8) Pourquoi les logs causent-ils autant de dégâts ?
Parce que ce sont des écritures que vous n’avez pas budgétées, souvent semi-synchrones, souvent sans fin, et par défaut sur le même disque que tout le reste. Faites-les tourner et expédiez-les ailleurs.
9) Comment savoir si le goulot est le journal du système de fichiers ?
Vous verrez une forte latence avec beaucoup de petites écritures, de nombreuses tâches dans des wait channels comme ext4_sync_file, et une activité métadonnées lourde. La correction est généralement la forme du workload (batching) et le placement des données, pas des drapeaux noyau mystiques.
10) Le throttling est-il « sûr » pour les bases de données ?
Ça dépend. Pour une base de données servant du trafic production, des plafonds durs peuvent provoquer des timeouts et des défaillances en cascade. Préférez des poids et une isolation de stockage appropriée. Si vous devez plafonner, faites-le avec précaution et surveillez.
Étapes suivantes (ce que vous faites lundi)
Quand un conteneur traîne votre hôte dans le purgatoire de l’attente I/O, la bonne action n’est pas de deviner. C’est l’attribution et le contrôle : mesurez la latence du périphérique, identifiez les PID top I/O, mappez-les aux conteneurs, et appliquez des limites I/O cgroup qui gardent l’hôte en vie.
Puis faites la partie responsable : cessez d’écrire sur les couches overlay, limitez la journalisation par défaut, et séparez le stockage pour les workloads qui n’ont pas à partager une file. Gardez les throttles comme garde-fous, pas comme substitut d’architecture. Votre futur vous, de l’astreinte, vous remerciera d’être ennuyeusement préparé.