Le symptôme : vous déployez un conteneur de base de données, et soudain tout l’hôte ressemble à un disque USB de 2009. SSH met des secondes à afficher les caractères. D’autres conteneurs expirent. Le CPU est « correct ». La mémoire est « correcte ». Pourtant tout est pénible.
La cause : contention de stockage. Plus précisément, une charge consomme (ou déclenche) la majeure partie du budget IOPS et augmente la latence pour tout le monde. Docker n’a pas « cassé » votre serveur. Vous avez juste découvert, à la dure, que les disques sont partagés, que la mise en file est réelle, et que les sémantiques de synchronisation des bases de données ne négocient pas.
À quoi ressemble la privation d’IOPS sur un hôte Docker
La privation d’IOPS n’est pas « le disque est plein ». C’est « le disque est occupé ». Plus précisément : le chemin de stockage est saturé de sorte que la latence moyenne des requêtes explose, et chaque charge partageant ce chemin paie la taxe.
Sur des hôtes Linux exécutant Docker, cela se manifeste généralement par :
- Un iowait élevé (mais pas toujours). Si vous ne regardez que le pourcentage CPU, vous pouvez le manquer.
- Des pics de latence pour les lectures et/ou écritures. Les bases de données détestent la latence d’écriture parce que
fsyncest un contrat, pas une suggestion. - Une accumulation dans les files. Les requêtes s’empilent dans la couche bloc ou sur le périphérique, et tout devient « lent », y compris des services non liés.
- Des symptômes indirects et étranges : DNS lent dans les conteneurs, logs lents, services systemd qui expirent, hoquets du démon Docker, voire des vérifications de santé « aléatoires » qui échouent.
Pourquoi SSH rame-t-il ? Parce que votre terminal écrit dans un PTY, les shells touchent l’historique sur disque, les logs se flushent, et le noyau est occupé à orchestrer les E/S. Le système n’est pas mort. Il attend que la ressource partagée la plus lente revienne de pause.
Pourquoi un seul conteneur peut tout dégrader
Les conteneurs Docker sont de l’isolation de processus plus un peu de magie filesystem. Ils ne sont pas une isolation physique. Si plusieurs conteneurs partagent :
- le même périphérique bloc (même volume root, même disque EBS, même groupe RAID, même LUN SAN),
- le même système de fichiers,
- les mêmes files de l’ordonnanceur d’E/S,
- et souvent le même comportement de writeback et de journalisation,
…alors un conteneur peut absolument dégrader tous les autres. C’est le cas classique du « voisin bruyant ».
Les bases de données amplifient la latence
La plupart des moteurs de BD effectuent beaucoup de petites E/S aléatoires. Ils font aussi fsync (ou équivalent) pour garantir la durabilité. Si le stockage est lent, la BD n’« accélère » pas en faisant plus d’efforts ; elle bloque et attend. Ce blocage peut se propager aux pools de connexions, aux threads applicatifs et déclencher des tempêtes de retry.
Et voici ce que Docker aggrave : la pile de stockage par défaut peut ajouter de l’overhead quand la BD écrit beaucoup de petits fichiers ou modifie beaucoup de métadonnées.
Les systèmes de fichiers overlay peuvent amplifier les écritures
Beaucoup d’hôtes Docker utilisent overlay2 pour la couche inscriptible des conteneurs. Les systèmes overlay sont excellents pour le layering d’images et l’ergonomie développeur. Les bases de données se moquent de votre ergonomie ; elles exigent une latence prévisible.
Si votre BD écrit dans la couche inscriptible du conteneur (au lieu d’un volume dédié), vous pouvez déclencher des opérations supplémentaires sur les métadonnées, des comportements de copy-up, et des motifs d’écriture moins favorables. Parfois ça passe. Parfois c’est une scène de crime de performance avec preuves à l’appui.
« Mais le disque n’utilise que 20 % du débit » est un piège
Le débit (MB/s) n’est pas toute l’histoire. Les IOPS et la latence importent plus pour beaucoup de charges BD. Vous pouvez avoir un disque faisant 5 MB/s et être complètement saturé en IOPS avec des écritures aléatoires 4K.
C’est pourquoi votre monitoring qui suit « la bande passante disque » dit que tout va bien alors que l’hôte pleure discrètement en iowait.
Blague #1 : Quand une base de données dit qu’elle « attend le disque », elle n’est pas passive-agressive. Elle est factuelle.
Faits intéressants et contexte historique
- Les IOPS sont devenues un métrique mainstream à cause de l’OLTP : les bases transactionnelles ont poussé l’industrie à mesurer « combien d’opérations petites par seconde », pas seulement MB/s.
- L’ordonnanceur CFQ était autrefois le défaut pour l’équité sur beaucoup de distributions ; les systèmes modernes utilisent souvent
mq-deadlineounonepour NVMe, changeant la perception de l’équité en contention. - Les cgroups v1 avaient tôt des contrôles blkio (poids et throttling). cgroups v2 est passé à une interface différente (
io.max,io.weight), ce qui surprend les équipes en migration. - Overlayfs a été conçu pour les mounts en union et les couches, pas pour des fichiers de base de données à fort turnover. Il s’est beaucoup amélioré, mais le décalage de charge se voit toujours aux pires moments : les queues de latence.
- Les barrières d’écriture et les flush existent parce que les caches mentent : les périphériques réordonnent les écritures pour la performance, donc l’OS utilise des flushes/FUA pour forcer l’ordre quand les applications exigent la durabilité.
- Les systèmes de fichiers journaux échangent des écritures supplémentaires contre la consistance : ext4 et XFS sont fiables, mais le comportement du journal peut augmenter la charge IO sous fort churn de métadonnées.
- Le stockage bloc cloud a souvent des crédits de burst : vous pouvez être rapide un moment, puis soudainement lent. Le conteneur n’a pas changé ; c’est le niveau de stockage.
- NVMe a amélioré massivement le parallélisme avec plusieurs files, mais n’a pas supprimé la contention ; elle a juste été déplacée vers d’autres files et limites.
- Les « fsync storms » sont un mode de défaillance classique : de nombreux threads appellent fsync, les files se remplissent, la latence explose, et un système qui paraissait correct au p50 se désintègre au p99.
Playbook de diagnostic rapide
Voici la séquence « j’ai cinq minutes et la production est en feu ». Ne débattez pas d’architecture pendant que l’hôte se fige. Mesurez, identifiez le goulet, puis décidez si vous devez brider, déplacer ou isoler.
Premier point : confirmez que c’est la latence IO, pas le CPU ou la mémoire
- Vérifiez la charge, l’iowait et la file d’attente d’exécution.
- Vérifiez la latence disque et la profondeur de file.
- Vérifiez si un seul processus/conteneur est le principal consommateur d’E/S.
Deuxième point : cartographiez la douleur jusqu’à un périphérique et un mount
- Quel périphérique bloc est lent ?
- Le root Docker est-il sur ce périphérique ?
- Les fichiers de la base sont-ils sur overlay2 ou sur un volume dédié ?
Troisième point : choisissez une atténuation immédiate
- Brider le conteneur bruyant (IOPS ou bande passante) pour sauver le reste de l’hôte.
- Déplacer les données BD vers un volume/périphérique dédié.
- Arrêter l’hémorragie : désactiver les logs bavards, mettre en pause les jobs non critiques, réduire la concurrence.
Quatrième point : planifier la vraie correction
- Utiliser des volumes pour les données BD (pas les couches inscriptibles des conteneurs).
- Utiliser l’isolation IO (cgroups v2 io.max / weights) quand c’est possible.
- Choisir des options de stockage et de système de fichiers adaptées aux patterns de sync des BD.
Tâches pratiques : commandes, sorties, décisions
Ci‑dessous des tâches réelles à exécuter sur un hôte Linux Docker. Chaque tâche inclut : commande, sortie d’exemple, ce que ça signifie, et la décision à prendre. Exécutez-les en root ou avec sudo lorsque nécessaire.
Task 1: Check overall CPU, iowait, and load
cr0x@server:~$ top -b -n 1 | head -n 5
top - 12:41:02 up 21 days, 4:17, 2 users, load average: 9.12, 7.84, 6.30
Tasks: 312 total, 4 running, 308 sleeping, 0 stopped, 0 zombie
%Cpu(s): 8.1 us, 2.4 sy, 0.0 ni, 61.7 id, 25.9 wa, 0.0 hi, 1.9 si, 0.0 st
MiB Mem : 64035.7 total, 3211.3 free, 10822.9 used, 499... buff/cache
MiB Swap: 8192.0 total, 8192.0 free, 0.0 used. 5... avail Mem
Signification : 25,9 % d’iowait et une charge élevée suggèrent de nombreux threads bloqués en IO. Le CPU n’est pas « occupé », il attend.
Décision : Cessez d’accabler l’ordonnanceur et commencez à mesurer la latence disque et la mise en file.
Task 2: Identify which disks are suffering (latency, utilization, queue depth)
cr0x@server:~$ iostat -x 1 3
Linux 6.2.0 (server) 01/03/2026 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
7.52 0.00 2.61 23.98 0.00 65.89
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz %util
nvme0n1 95.0 820.0 3200.0 9870.0 0.0 15.0 0.00 1.80 3.2 42.7 18.90 98.50
Signification : %util proche de 100 % et w_await ~43ms signifie que le périphérique est saturé en écritures. aqu-sz (taille moyenne de la file) est énorme, confirmant l’arriéré.
Décision : Trouvez le processus écrivain. Ne touchez pas aux réglages BD à l’aveugle ; identifiez d’abord le processus/conteneur qui génère ces écritures.
Task 3: See if the block layer is backlogged (per-device stats)
cr0x@server:~$ cat /proc/diskstats | grep -E "nvme0n1 "
259 0 nvme0n1 128930 0 5128032 12043 942110 0 9230016 390122 0 220010 402210 0 0 0 0
Signification : Les champs exacts sont denses, mais un indice rapide est le temps élevé passé à faire des E/S comparé à la baseline. Combinez avec iostat -x pour vérifier.
Décision : Si vous voyez ce pic uniquement pendant la charge BD, vous êtes dans le cas classique « une charge sature le périphérique ».
Task 4: Find the top IO processes (host view)
cr0x@server:~$ sudo iotop -b -n 3 -o
Total DISK READ: 0.00 B/s | Total DISK WRITE: 74.32 M/s
Current DISK READ: 0.00 B/s | Current DISK WRITE: 71.91 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
21491 be/4 postgres 0.00 B/s 55.12 M/s 0.00 % 89.15 % postgres: checkpointer
21510 be/4 postgres 0.00 B/s 10.43 M/s 0.00 % 63.20 % postgres: walwriter
4321 be/4 root 0.00 B/s 3.01 M/s 0.00 % 12.10 % dockerd
Signification : Les processus Postgres dominent les écritures et passent un fort pourcentage de temps en attente IO. dockerd écrit un peu (logs, couches), mais ce n’est pas le principal coupable.
Décision : Confirmez quel conteneur possède ces processus, puis décidez de le brider ou de déplacer son stockage.
Task 5: Map a process to a container
cr0x@server:~$ ps -o pid,cgroup,cmd -p 21491 | sed -n '1,2p'
PID CGROUP CMD
21491 0::/docker/8b6c3b7e4a3a9b7d2a7b55c4a1a2f9b9b0f6c0f9d1a7b1e3c9e3a2c1e5b postgres: checkpointer
Signification : Le processus est dans un cgroup Docker identifié par l’ID du conteneur.
Décision : Inspectez ce conteneur. Confirmez ses mounts et s’il utilise overlay2 ou un volume.
Task 6: Inspect container mounts and storage driver
cr0x@server:~$ docker inspect -f 'Name={{.Name}} Driver={{.GraphDriver.Name}} DataRoot={{json .GraphDriver.Data}} Mounts={{json .Mounts}}' 8b6c3b7e4a3a
Name=/db Driver=overlay2 DataRoot={"LowerDir":"/var/lib/docker/overlay2/l/..","MergedDir":"/var/lib/docker/overlay2/1d3.../merged","UpperDir":"/var/lib/docker/overlay2/1d3.../diff","WorkDir":"/var/lib/docker/overlay2/1d3.../work"} Mounts=[{"Type":"volume","Name":"pgdata","Source":"/var/lib/docker/volumes/pgdata/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]
Signification : Le driver de stockage est overlay2 pour la couche inscriptible, mais le répertoire de données BD est monté en volume nommé. C’est bien. Si les données BD étaient dans UpperDir, ce serait pire.
Décision : Si les données BD ne sont pas sur un volume, corrigez ça d’abord. Si elles y sont, vérifiez où le volume réside physiquement (sur le même périphérique que tout le reste ?).
Task 7: Check what device backs Docker data root and volumes
cr0x@server:~$ docker info --format 'DockerRootDir={{.DockerRootDir}} Driver={{.Driver}}'
DockerRootDir=/var/lib/docker Driver=overlay2
cr0x@server:~$ df -hT /var/lib/docker /var/lib/docker/volumes/pgdata/_data
Filesystem Type Size Used Avail Use% Mounted on
/dev/nvme0n1p2 ext4 900G 612G 242G 72% /
/dev/nvme0n1p2 ext4 900G 612G 242G 72% /
Signification : Docker root et le volume de la base sont sur le même système de fichiers ext4 sur la même partition NVMe. La contention est donc garantie.
Décision : Si vous voulez que la BD et le reste de l’hôte coexistent, vous devez isoler les E/S (contrôles cgroup IO) ou déplacer les données BD sur un périphérique dédié.
Task 8: Check filesystem mount options (barriers, atime, etc.)
cr0x@server:~$ findmnt -no TARGET,SOURCE,FSTYPE,OPTIONS /
/ /dev/nvme0n1p2 ext4 rw,relatime,errors=remount-ro
Signification : relatime est acceptable. Rien d’évidemment étrange. Si vous voyez des options exotiques (ou un tunning mal appliqué), c’est un indice.
Décision : Ne changez pas les options de montage pendant un incident sauf si vous êtes certain ; utilisez le throttling/la migration comme premiers secours plus sûrs.
Task 9: Check if the host is stuck in writeback congestion
cr0x@server:~$ cat /proc/meminfo | egrep 'Dirty|Writeback|WritebackTmp'
Dirty: 82456 kB
Writeback: 195120 kB
WritebackTmp: 0 kB
Signification : Un Writeback élevé peut indiquer beaucoup de données en cours de flush vers le disque. Cela ne prouve pas la culpabilité de la BD, mais soutient l’hypothèse « pipeline de stockage surchargé ».
Décision : Si le writeback reste élevé et que la latence est haute, réduisez la pression d’écriture et isolez le gros écrivain.
Task 10: Look at per-process IO counters (sanity)
cr0x@server:~$ sudo cat /proc/21491/io | egrep 'write_bytes|cancelled_write_bytes'
write_bytes: 18446744073709551615
cancelled_write_bytes: 127385600
Signification : Certains noyaux exposent des compteurs de façon déroutante (et certains systèmes de fichiers ne rapportent pas proprement). Traitez-les comme indicateurs directionnels, pas absolus.
Décision : Si c’est inconclusif, fiez-vous à iotop, iostat, et aux métriques de latence au niveau du périphérique.
Task 11: Check Docker container resource limits (CPU/mem) and note the absence of IO limits
cr0x@server:~$ docker inspect -f 'CpuShares={{.HostConfig.CpuShares}} Memory={{.HostConfig.Memory}} BlkioWeight={{.HostConfig.BlkioWeight}}' 8b6c3b7e4a3a
CpuShares=0 Memory=0 BlkioWeight=0
Signification : Pas de limites explicites. Le CPU et la mémoire sont couramment contraints ; l’IO souvent non. C’est ainsi qu’un seul conteneur peut aplatir un hôte.
Décision : Ajoutez des contrôles IO (Docker blkio sur cgroups v1, ou systemd/cgroups v2), ou isolez la charge sur un stockage distinct.
Task 12: Apply a temporary IO throttle (bandwidth) to a container (incident mitigation)
cr0x@server:~$ docker update --device-write-bps /dev/nvme0n1:20mb 8b6c3b7e4a3a
8b6c3b7e4a3a
Signification : Docker a appliqué un bridage de bande passante d’écriture pour ce périphérique au conteneur. C’est un outil brutal. Il peut protéger l’hôte au prix de la latence et du débit de la BD.
Décision : Utilisez ceci pour arrêter les dommages collatéraux. Ensuite déplacez la BD sur un stockage dédié ou mettez en place un partage équitable avec de vrais contrôleurs IO.
Task 13: Apply an IOPS throttle (more relevant for small IO)
cr0x@server:~$ docker update --device-write-iops /dev/nvme0n1:2000 8b6c3b7e4a3a
8b6c3b7e4a3a
Signification : Limite les IOPS d’écriture. C’est typiquement plus pertinent pour la douleur BD que les bridages en MB/s.
Décision : Si le bridage améliore la réactivité de l’hôte, vous avez confirmé que la « nuisance voisine IO » est le principal mode de défaillance. Maintenant, concevez une solution durable.
Task 14: Check cgroup v2 IO controller status
cr0x@server:~$ stat -fc %T /sys/fs/cgroup
cgroup2fs
cr0x@server:~$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory pids
Signification : L’hôte utilise cgroups v2 et supporte le contrôleur io.
Décision : Préférez le contrôle IO cgroups v2 quand disponible ; c’est plus clair et c’est la direction prise par Linux.
Task 15: Inspect a container’s IO limits via its cgroup (v2)
cr0x@server:~$ CID=8b6c3b7e4a3a; cat /sys/fs/cgroup/docker/$CID/io.max
8:0 rbps=max wbps=max riops=max wiops=max
Signification : Pas de limites actuellement. Le major:minor 8:0 est un exemple ; votre NVMe peut être différent. « max » signifie illimité.
Décision : Si vous utilisez systemd ou un runtime intégré à cgroups v2, définissez io.max ou des weights pour la portée du service/conteneur.
Task 16: Check device major:minor for correct throttling target
cr0x@server:~$ lsblk -o NAME,MAJ:MIN,SIZE,TYPE,MOUNTPOINT | sed -n '1,6p'
NAME MAJ:MIN SIZE TYPE MOUNTPOINT
nvme0n1 259:0 953.9G disk
├─nvme0n1p1 259:1 512M part /boot/efi
└─nvme0n1p2 259:2 953.4G part /
Signification : Si vous configurez io.max vous devez spécifier le bon major:minor (par ex. 259:0 pour le disque, ou 259:2 pour une partition selon la configuration).
Décision : Ciblez le périphérique qui subit la contention. Brider le mauvais major:minor est la façon la plus sûre de « ne rien corriger avec la plus grande confiance ».
Causes profondes rencontrées en production
1) Les écritures BD vivent dans la couche inscriptible du conteneur
Si votre base de données stocke ses données à l’intérieur du système de fichiers du conteneur (overlay2 upperdir), vous empilez une charge d’écriture sur un mécanisme conçu pour des images en couches. Attendez‑vous à une latence pire et à plus de churn de métadonnées. Utilisez un volume Docker ou un bind mount vers un chemin dédié.
2) Un périphérique partagé, zéro contrôles d’équité IO
Même si les données BD sont sur un volume, si ce volume est juste un répertoire sur le même système de fichiers root, vous partagez toujours le périphérique. Sans poids ni throttles, l’écrivain le plus actif gagne. Les autres perdent.
3) Falaises de latence dues au stockage cloud bursty
Beaucoup de volumes cloud offrent des performances « baseline + burst ». Une BD occupée brûle la capacité de burst, puis le volume retombe à la baseline. Votre incident commence exactement quand les crédits sont épuisés. Rien dans Docker n’explique le timing, ce qui fait perdre des heures aux équipes à chercher au mauvais niveau.
4) Journalisation + fsync + forte concurrence = enfer des latences extrêmes
Les bases font souvent de nombreuses écritures concurrentes, plus des logs WAL/redo, plus des checkpoints. Ajoutez le comportement du système de fichiers journalisé et les flushes de cache périphérique, et vous pouvez obtenir un débit moyen correct avec une latence p99 catastrophique.
5) Logging sur le même périphérique que la BD
Quand le disque est saturé, les logs ne « s’écrivent pas juste plus lentement ». Ils peuvent bloquer des threads applicatifs, remplir des buffers et générer plus d’E/S au pire moment. Les logs JSON sont sympas jusqu’à ce qu’ils deviennent votre charge d’écriture principale pendant une panne.
Blague #2 : Si vous mettez une base de données et des logs debug bavards sur le même disque, vous avez inventé un nouveau système distribué : « latence ».
Erreurs courantes (symptôme → cause → fix)
C’est la partie où vous arrêtez de répéter la même panne avec des noms différents.
1) Symptom: CPU bas, mais moyenne de charge élevée
- Cause : threads bloqués en sommeil IO ininterruptible ; la charge les compte.
- Fix : vérifiez
iostat -xpourawaitet%util; identifiez les plus gros processus IO viaiotop; isolez ou bridez.
2) Symptom: tous les conteneurs deviennent lents, y compris des services « non liés »
- Cause : périphérique bloc partagé saturé ; writeback noyau et opérations de métadonnées impactent tout le monde.
- Fix : déplacez la BD sur un périphérique/volume dédié ; appliquez des contrôles IO cgroup ; séparez Docker root, les logs et les données BD sur des périphériques différents lorsque possible.
3) Symptom: latence BD en pic pendant checkpoints ou compaction
- Cause : phases d’écriture par rafales causant accumulation dans les files et tempêtes de flush.
- Fix : tunez les paramètres de checkpoint/compaction de la BD avec précaution ; limitez l’IO des writers en arrière-plan ; assurez-vous que le stockage a assez d’IOPS soutenus.
4) Symptom: « Le débit disque semble correct » dans le monitoring
- Cause : vous surveillez MB/s, pas la latence ou les IOPS ; les petites E/S aléatoires saturent les IOPS en premier.
- Fix : surveillez
await,aqu-sz, les percentiles de latence du périphérique si dispo, et les limites IOPS par volume dans le cloud.
5) Symptom: le conteneur BD est rapide seul, lent sur l’hôte de production
- Cause : l’hôte de test avait un stockage isolé ou moins de voisins bruyants ; le prod partage le disque root avec tout le reste.
- Fix : testez la performance sur un stockage représentatif ; imposez l’isolation via des périphériques séparés ou des contrôles IO.
6) Symptom: après des « optimisations », c’est pire
- Cause : désactiver la durabilité, changer les options de montage ou augmenter la concurrence a poussé le système dans une pire latence tail ou a augmenté le risque de perte de données.
- Fix : priorisez une latence et une durabilité prévisibles ; réglez une variable à la fois ; validez avec des mesures de latence, pas des impressions.
Trois mini-récits d’entreprise depuis le front du stockage
Mini-récit 1 : L’incident causé par une mauvaise hypothèse
L’équipe migrait un monolithe vers « quelques conteneurs » sur une grosse VM Linux. Ils ont commencé par la base de données parce que c’était le morceau le plus effrayant, et ça « marchait en staging ». En production, chaque déploiement après l’arrivée du conteneur BD apportait une vague de timeouts de services non liés : l’API, le job runner, même le sidecar de métriques.
L’hypothèse initiale était classique : « les conteneurs isolent les ressources ». Ils ont limité CPU et mémoire du conteneur BD, se sont félicités et ont continué. Quand la charge hôte est montée en flèche avec un CPU majoritairement idle, le blâme a tourné entre réseau, DNS, le réseau overlay de Docker, et un bref mais passionné débat sur les versions du noyau.
Il a fallu qu’une personne lance iostat -x pour clore le débat. Le disque root était à ~100 % d’utilisation avec une latence d’écriture en forme de chaîne de montagnes. Le répertoire de données BD était un bind mount dans /var/lib/docker sur le même filesystem root que tout le reste, y compris les logs et les couches d’images.
Une fois qu’ils ont admis que « conteneur » ne signifie pas « disque séparé », la correction fut simple : attacher un volume dédié pour la BD, le monter au répertoire de données, et déplacer les logs hors du disque root. L’hôte est passé de « panne systémique mystérieuse » à « ordinateur ennuyeux », ce qui est le compliment suprême en exploitation.
Mini-récit 2 : L’optimisation qui a mal tourné
Une autre entreprise avait un problème de performance : leur conteneur Postgres était goulot d’écritures en pic. Quelqu’un proposa d’alléger la pression de fsync en relaxant la durabilité, arguant « nous avons la réplication » et « le cloud storage est fiable ». Le changement améliora immédiatement le throughput dans un benchmark synthétique, et fut déployé.
Deux semaines plus tard, un nœud planta pendant un événement de stockage bruyant. Pas une catastrophe, mais le timing était parfait : forte charge d’écriture, retard des réplicas et basculement. Ils ne perdirent pas toute la base, mais suffisamment de transactions récentes pour déclencher une semaine de conversations inconfortables. Pendant ce temps, le symptôme initial (lag au niveau hôte) revint lors des rafales, parce que le vrai goulot était la saturation des files du périphérique et la contention IO partagée, pas seulement le surcoût de fsync.
Ils ont rollbacké le compromis de durabilité et fait la correction adulte : isoler la BD sur un périphérique bloc dédié avec des IOPS prévisibles, ajouter des poids IO pour garder le reste de l’hôte utilisable, et limiter les tâches de maintenance les plus abusives. L’« optimisation » n’était pas malveillante ; elle était mal appliquée. Elle a optimisé le mauvais niveau et acheté de la vitesse en faisant porter le risque sur vos données.
La leçon retenue : si vous allez échanger durabilité contre performance, assumez-le, documentez-le et obtenez l’accord de ceux qui seront paginés si ça tourne mal.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Celui-ci est moins dramatique, ce qui explique pourquoi ça a marché. Une équipe faisait tourner plusieurs conteneurs stateful sur une petite flotte : une BD, une queue et un moteur de recherche. Ils avaient une politique : chaque service stateful doit utiliser un point de montage dédié soutenu par une classe de volume dédiée, et chaque service doit avoir un budget IO explicite dans les notes de déploiement.
Ce n’était pas sophistiqué. Ils n’avaient pas de patchs noyau maison ni d’ordonnanceurs artisanaux. Ils refusaient simplement de mettre des données persistantes sur le filesystem root Docker, et gardaient les logs applicatifs sur un chemin séparé avec rotation et verbosité raisonnable.
Un jour, un job batch s’est emballé et a commencé à marteler la persistance de la queue. La latence a monté pour ce service, mais le reste de l’hôte est resté réactif. Leurs alertes ont pointé directement le périphérique utilisé par le volume de la queue, pas « le serveur est lent ». Ils ont bridé l’IO du conteneur du job, stabilisé, et fait un postmortem sans que personne ait à expliquer pourquoi SSH était inutilisable.
Parfois les pratiques « ennuyeuses » sont juste de l’incident response prépayée.
Correctifs et garde-fous qui fonctionnent vraiment
1) Placez les données BD sur un vrai volume, pas sur la couche inscriptible du conteneur
Si vous retenez une seule chose, que ce soit celle-ci : les bases de données doivent écrire sur un point de montage dédié (volume nommé ou bind mount), idéalement soutenu par un périphérique dédié. Cela signifie :
- Le répertoire de données BD est un point de montage que vous voyez dans
findmnt. - Ce mount correspond à un périphérique que vous pouvez mesurer indépendamment.
- Le Docker root (
/var/lib/docker) ne porte pas la charge de durabilité de votre base de données.
2) Séparez les préoccupations : images/logs vs données durables
Le Docker data root est occupé : extraction d’images, téléchargements de couches, métadonnées overlay, écritures de logs conteneurs, et plus. Combinez ça avec une BD qui fait des WAL et des checkpoints et vous avez une machine à contention.
Une séparation pratique :
- Disque A : OS + Docker root + logs conteneurs (assez rapide, mais pas sacré).
- Disque B : volume BD (IOPS prévisibles, faible latence, monitoré).
3) Utilisez des contrôles IO pour faire respecter l’équité
Si vous devez partager un périphérique, imposez l’équité. Pour cgroups v2, le contrôleur io fournit poids et throttling. L’UX Docker pour ça varie selon la version et le runtime, mais le principe est stable : ne laissez pas un conteneur devenir un aspirateur de disque.
Le throttling n’est pas que punitif. Il peut faire la différence entre « la BD est lente » et « tout est HS ». En incident, vous voulez souvent garder le reste de l’hôte réactif pendant que vous décidez du sort de la BD.
4) Mesurez la latence, pas seulement l’utilisation
%util est utile mais pas suffisant. Un périphérique peut montrer moins de 100 % d’utilisation et avoir pourtant une latence horrible, surtout sur du stockage virtualisé ou réseau où le « périphérique » est une abstraction.
Ce que vous devez connaître :
- La latence moyenne et en queue pour lectures/écritures.
- Les tendances de profondeur de file sous charge.
- Les limites IOPS et si vous vous en approchez.
5) Tondez le comportement BD seulement après avoir corrigé la géométrie du stockage
Le tuning BD a sa place. Mais si la BD partage simplement le mauvais disque avec tout le reste, le tuning est une perte de temps et un cadeau pour de futurs incidents.
D’abord : isolez le chemin du périphérique. Ensuite : évaluez les checkpoints, les réglages WAL et le travail en arrière-plan. Sinon vous finirez avec une configuration fragile qui marche jusqu’au prochain pic.
6) Empêchez que les logs conteneurs deviennent une charge IO
Si vous loggez beaucoup en JSON et laissez Docker écrire sur le même filesystem que la BD, vous entrez en compétition pour la même file. Utilisez la rotation des logs, réduisez la verbosité, et envisagez de déplacer les logs volumineux hors hôte ou sur un périphérique séparé.
7) Ne négligez pas le modèle de performance du volume cloud
Si votre volume a des performances baseline/burst, modèlez votre charge en conséquence. Une BD « ok pendant 20 minutes puis affreuse » n’est souvent pas un mystère ; c’est un bucket de crédits qui se vide. Prévoyez des performances soutenues ou concevez autour de cette contrainte.
Une citation à retenir
L’espoir n’est pas une stratégie.
— General Gordon R. Sullivan
Listes de contrôle
Étape par étape : stabiliser un incident (30 minutes)
- Confirmer l’attente IO/latence : lancez
topetiostat -x. - Identifier l’écrivain : lancez
iotop -oet mappez les PIDs aux conteneurs via les cgroups. - Vérifier le placement du stockage : contrôlez
docker inspectmounts etdf -hTpour Docker root et les volumes. - Atténuer : bridez les IOPS ou la bande passante de l’offenseur, ou réduisez temporairement sa concurrence (limites de connexions BD, jobs en arrière-plan).
- Réduire l’IO collatéral : coupez la verbosité des logs ; assurez-vous que la rotation des logs fonctionne ; mettez en pause les jobs batch non essentiels.
- Communiquer : indiquez clairement « la latence disque de l’hôte est saturée » et la mitigation appliquée. Évitez les « Docker est lent » vagues.
Étape par étape : correctif permanent (une itération)
- Déplacer les données BD sur un stockage dédié : périphérique séparé ou une classe de volume avec IOPS garanties.
- Séparer Docker root des volumes stateful : gardez
/var/lib/dockerhors du périphérique BD. - Mettre en place des contrôles IO : utilisez cgroup v2
io.max/io.weight(ou options Docker blkio lorsque supportées). - Configurer un monitoring qui détecte tôt : latence périphérique, profondeur de file, et état des crédits/burst des volumes si applicable.
- Test de charge sur la stack réelle : pas « la BD sur mon laptop », mais le backend de stockage réel et le runtime conteneur.
- Rédiger un runbook : incluez les commandes exactes de cet article et les points de décision.
Checklist pré-déploiement pour tout conteneur BD
- Le répertoire de données BD est un mount dédié (volume/bind mount), pas la couche inscriptible overlay2.
- Ce mount est sur un périphérique avec des caractéristiques d’IOPS et de latence connues.
- Le conteneur a une politique IO définie (poids ou throttling), pas « illimité ».
- Les logs sont rate-limités et rotatés ; les logs volumineux ne vont pas sur le disque BD.
- Des alertes existent pour la latence disque et la profondeur de file, pas seulement la saturation disque.
FAQ
1) Est-ce un bug Docker ?
Généralement non. C’est une contention de ressources partagées. Docker rend facile la colocation des charges, ce qui rend facile la colocation de leur douleur de stockage.
2) Pourquoi un seul conteneur BD cause-t-il un lag sur tout l’hôte ?
Parce que le périphérique bloc est partagé. Quand ce conteneur sature les IOPS ou déclenche une forte latence, les files noyau se remplissent et tout le monde attend derrière lui.
3) Déplacer la BD sur un volume nommé résoudra-t-il le problème ?
Seulement si le volume est soutenu par un stockage différent ou une classe de performance différente. Un volume nommé stocké sous /var/lib/docker/volumes sur le même filesystem est organisationnel, pas isolant.
4) Quelle est la différence entre IOPS et débit dans ce contexte ?
IOPS sont les opérations par seconde (souvent petites lectures/écritures 4K). Le débit est MB/s. Les bases de données se retrouvent souvent limitées par les IOPS et la latence, pas par la bande passante.
5) Dois-je changer l’ordonnanceur IO ?
Parfois, mais rarement comme premier correctif. Les tweaks d’ordonnanceur ne vous sauveront pas d’un périphérique partagé sans isolation et d’une BD faisant des écritures sync intensives.
6) overlay2 rend-il toujours les bases lentes ?
Non. Mais mettre des données BD chaudes sur la couche inscriptible est une erreur courante, et le comportement d’overlay peut aggraver des motifs d’écriture lourds en métadonnées. Utilisez des volumes pour les données BD.
7) Comment limiter l’IO d’un conteneur de façon fiable ?
Utilisez les contrôles IO des cgroups. Docker supporte --device-read-bps, --device-write-bps et les variantes IOPS pour le throttling. Sur cgroups v2, vous pouvez aussi gérer io.max via l’intégration du runtime/gestionnaire de services.
8) Pourquoi la « utilisation disque 100 % » arrive sur NVMe ? Ne sont-ils pas rapides ?
NVMe est rapide, pas infini. Les petites écritures sync et les flushs peuvent toujours saturer les files, et un périphérique unique a toujours une courbe de latence finie sous charge.
9) Pourquoi mes applications échouent les checks de santé pendant la contention IO ?
Les checks de santé font souvent des opérations disque ou réseau qui dépendent d’un noyau réactif et d’un logging en temps. Sous forte attente IO, tout est retardé, y compris ces vérifications « simples ».
10) Quelle est l’atténuation immédiate la plus sûre si la production fond ?
Bridez l’IO du conteneur bruyant pour restaurer la réactivité de l’hôte, puis planifiez une migration des données vers un stockage dédié. Arrêter la BD peut être nécessaire, mais le bridage vous achète du temps.
Conclusion : prochaines étapes que vous pouvez faire cette semaine
Si un conteneur BD fait ramer tout, croyez les signaux du stockage. Mesurez la latence et la profondeur de file, identifiez le conteneur, et arrêtez de prétendre que les disques partagés s’auto-organiseront en équité.
Prochaines étapes concrètes :
- Ajoutez la latence périphérique et la profondeur de file à vos tableaux de bord (pas seulement le % plein ou MB/s).
- Auditez les conteneurs stateful : vérifiez que les répertoires de données sont des volumes/bind mounts et cartographiez-les aux périphériques réels.
- Séparez le stockage : déplacez les données BD hors du filesystem root Docker sur un stockage dédié avec des performances soutenues connues.
- Mettez en place des contrôles IO : définissez des throttles ou weights raisonnables pour qu’un conteneur ne puisse pas prendre l’hôte en otage.
- Rédigez le runbook : copiez le « playbook de diagnostic rapide » et les tâches dans votre wiki on-call, puis organisez un game day.