Docker : écritures lentes sur overlay2 — quand passer aux volumes et pourquoi

Cet article vous a aidé ?

Vous vous en apercevez pendant un incident, parce que c’est alors que vous êtes le plus honnête avec vous-même.
L’application est « OK », le CPU est tranquille, le réseau calme, mais les requêtes stagnent.
Quelque part, un conteneur écrit sur le disque comme s’il sculptait des octets dans du granit.

Dans neuf cas sur dix, le coupable n’est pas « Docker est lent ». C’est le système de fichiers par défaut des conteneurs (overlay2) qui fait exactement ce pour quoi il a été conçu :
rendre les images et les couches de conteneur pratiques. Pas rapide pour des charges d’écriture intensives et friandes de fsync.

Overlay2 en un modèle mental (et pourquoi les écritures posent problème)

overlay2 est le driver de stockage Docker qui utilise Linux OverlayFS : un système de fichiers en union qui superpose un « upperdir » inscriptible au-dessus d’un ou plusieurs « lowerdir » en lecture seule (vos couches d’image).
Quand un conteneur lit un fichier qui existe dans l’image, il lit depuis les couches inférieures. Quand il écrit, il écrit dans la couche supérieure.

La douleur commence à l’intersection du copy-on-write et du turnover de métadonnées.
Si un conteneur modifie un fichier présent dans la couche inférieure, OverlayFS doit souvent « copier vers le haut » le fichier dans l’upperdir d’abord.
Ce copy-up génère de l’I/O réelle. Il peut être volumineux. Il s’accompagne aussi d’opérations sur les métadonnées qui sont bon marché sur le papier et coûteuses à grande échelle.

Pour les charges d’écriture intensives, deux schémas sont particulièrement punitifs :

  • Petites écritures aléatoires avec fsync (bases de données, SQLite, files d’attente avec durabilité). La couche en union n’est pas le seul problème ; c’est l’indirection supplémentaire, le travail sur les métadonnées et la façon dont le système de fichiers hôte se comporte sous cette charge.
  • Beaucoup de création/suppression de fichiers (caches de build, extraction d’archives, gestionnaires de paquets, répertoires temporaires). overlay2 peut transformer « beaucoup de petites écritures » en « beaucoup de petites écritures plus beaucoup de petites opérations sur les métadonnées ».

Les volumes existent parce qu’on finit par admettre qu’on exécute des charges d’état. Un volume Docker est essentiellement un répertoire géré par Docker, monté dans le conteneur directement depuis le système de fichiers hôte (pas de superposition de couches sur ce chemin).
Pour le chemin monté, vous contournez le surcoût du copy-on-write et réduisez généralement l’amplification des écritures.

La décision n’est pas philosophique. Elle est mécanique : si les données sont mutables et sensibles aux performances, arrêtez de les stocker dans la couche du conteneur.
Gardez le système de fichiers du conteneur pour les binaires et la configuration ; gardez votre état quelque part de banal et direct.

Faits intéressants et contexte historique

Bribes courtes et concrètes qui expliquent pourquoi on en est arrivé là :

  1. Docker n’a pas commencé avec overlay2. Les premières versions utilisaient beaucoup AUFS ; c’était relativement rapide et riche en fonctionnalités, mais peu en accord avec l’upstream Linux. Le passage à OverlayFS visait en partie à être « dans le noyau » et maintenable.
  2. Les séquelles de l’ère device mapper sont réelles. Le driver devicemapper (surtout loop-lvm) provoquait des latences d’écriture spectaculaires et des moments « pourquoi mon disque est plein ? ». overlay2 est devenu le défaut pour de bonnes raisons.
  3. OverlayFS a mûri sur plusieurs versions du noyau. Des fonctionnalités comme plusieurs lower layers et des correctifs de performance sont arrivées par itérations. Votre version du noyau compte plus qu’on ne veut l’admettre.
  4. Le d_type de XFS compte. OverlayFS exige le support du type de fichier dans l’entrée de répertoire (d_type). Sur XFS, cela est contrôlé par ftype=1. Si c’est mal configuré, Docker avertira—ou pire, vous aurez des comportements/performances étranges.
  5. Le copy-up n’est pas hypothétique. Éditer un fichier qui était dans la couche image déclenche une copie de ce fichier dans l’upperdir du conteneur. Pour les gros fichiers, vous payez immédiatement.
  6. Les whiteouts sont la manière dont les suppressions fonctionnent dans les union filesystems. Quand vous supprimez un fichier présent dans une lower layer, OverlayFS enregistre un « whiteout » dans l’upperdir. Ce sont des métadonnées supplémentaires, qui s’accumulent.
  7. Les systèmes de fichiers journalisés troquent latence contre sécurité. ext4 et XFS ont des comportements différents par défaut ; ajoutez des applications friandes de fsync et vous pouvez amplifier la douleur. overlay2 n’efface pas cela ; il peut l’amplifier.
  8. Les logs de conteneur ne sont pas spéciaux. Si vous loggez vers un fichier à l’intérieur de la couche du conteneur, vous écrivez dans overlay2. Si vous loggez vers stdout, le driver de log de Docker écrit ailleurs—toujours sur disque, mais sur un chemin différent et avec des modes d’échec différents.
  9. « C’est rapide sur mon laptop » est souvent du cache page, pas du débit. overlay2 peut sembler excellent jusqu’à ce que vous atteigniez des écritures synchrones, une pression mémoire, ou un nœud avec des voisins bruyants.

À quoi ressemble une « écriture lente » en production

La lenteur d’overlay2 n’est rarement une seule cause évidente. C’est un ensemble de symptômes qui se ressemblent :

  • La latence P99 augmente alors que le CPU reste sage. Les threads de l’app se bloquent sur l’I/O.
  • Les checkpoints ou compactages de la base prennent plus de temps à l’intérieur des conteneurs que sur l’hôte.
  • « Le disque n’est pas plein » mais les écritures stagnent : vous êtes peut-être à court d’inodes, coincé derrière une contention de journal, ou limité par la file du périphérique bloc sous-jacent.
  • La iowait au niveau du nœud monte en flèche sans histoire de « gros débit ». C’est un signe classique d’écritures synchrones petites et nombreuses.
  • Les redémarrages de conteneurs deviennent plus lents avec le temps si la couche inscriptible accumule beaucoup de fichiers et de whiteouts. Les parcours de métadonnées ne vieillissent pas gracieusement.

La partie délicate : overlay2 n’est pas toujours le goulot d’étranglement. Le goulot peut être votre stockage bloc sous-jacent, vos options de montage du système de fichiers, votre noyau,
ou le fait d’avoir mis un WAL de base de données sur un union filesystem puis d’avoir demandé à ce WAL de fsyncer comme si sa mission en dépendait (et c’est le cas).

Guide de diagnostic rapide

Quand vous êtes de garde, vous ne voulez pas une enquête en 40 étapes. Vous voulez une échelle courte qui vous amène à « déplacer les données sur un volume » ou « c’est le disque sous-jacent » rapidement.

Première étape : confirmer qui écrit et où

  • Identifiez les plus gros écrivains au niveau hôte (processus, périphérique).
  • Mappez le PID du conteneur vers un nom de conteneur.
  • Déterminez si le chemin chaud est dans /var/lib/docker/overlay2 ou sur un volume/montage bind.

Deuxième étape : décider s’il s’agit de latence d’écriture synchrone ou de débit

  • Si await est élevé et %util est élevé : le périphérique est saturé ou fait de l’attente en file.
  • Si await est élevé mais %util est modéré : vous payez peut-être la latence par opération (fsync, verrous de journal, tempêtes de métadonnées).
  • Si l’application appelle fsync constamment : vous êtes dans le « jeu de la latence », pas dans le « jeu des MB/s ».

Troisième étape : vérifier les amplificateurs spécifiques à overlay2

  • Déclencheurs de copy-up (écriture sur des fichiers provenant de la couche image).
  • Grand nombre de fichiers dans la couche inscriptible (pression sur les inodes, coût du parcours de répertoire).
  • Incompatibilité du système de fichiers sous-jacent (XFS ftype, options de montage étranges).

Quatrième étape : choisir le changement le plus petit et sûr

  • Si ce sont des données mutables : déplacez-les vers un volume ou un bind mount. Préférez les volumes pour l’hygiène opérationnelle.
  • Si ce sont des données éphémères de build/cache : envisagez tmpfs si ça tient, ou acceptez des écritures plus lentes mais cessez de les persister.
  • Si c’est le disque sous-jacent : corrigez l’histoire du disque (IOPS, latence, ordonnanceur, classe de stockage). overlay2 n’est que le messager.

Tâches pratiques : commandes, sorties, décisions (12+)

Voici les tâches que vous exécutez réellement à 2 h du matin. Chacune contient : une commande, ce que la sortie typique signifie, et la décision suivante.
Les commandes supposent un hôte Linux exécutant Docker Engine avec overlay2.

Task 1: Confirm Docker is using overlay2 (and on what backing filesystem)

cr0x@server:~$ docker info --format '{{.Driver}} {{.DockerRootDir}}'
overlay2 /var/lib/docker

Ce que ça signifie : Le driver de stockage est overlay2 ; la racine des données Docker est /var/lib/docker.

Décision : Toutes les couches inscriptibles des conteneurs vivent sous cette racine sauf si vous l’avez déplacée. C’est là que vous cherchez la chaleur.

Task 2: Check backing filesystem type and mount options for Docker root

cr0x@server:~$ findmnt -no SOURCE,FSTYPE,OPTIONS /var/lib/docker
/dev/nvme0n1p2 ext4 rw,relatime,errors=remount-ro

Ce que ça signifie : La racine Docker est sur ext4, avec des options typiques.

Décision : Si c’est un stockage réseau ou un HDD lent, arrêtez de blâmer overlay2 et commencez à blâmer la physique. Si c’est XFS, vérifiez ftype=1 (Task 3).

Task 3: If using XFS, verify d_type support (ftype=1)

cr0x@server:~$ xfs_info /dev/nvme0n1p2 | grep ftype
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1

Ce que ça signifie : OverlayFS peut fonctionner correctement. ftype=0 est un signal d’alerte.

Décision : Si ftype=0, planifiez une migration vers un système de fichiers correctement formaté. Ne « bricolez » pas autour d’un mismatch structurel.

Task 4: Find top block devices and whether they’re saturated

cr0x@server:~$ iostat -xz 1 5
Linux 6.2.0 (server) 	01/03/2026 	_x86_64_	(16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.10    0.00    3.40   22.80    0.00   61.70

Device            r/s     w/s   rMB/s   wMB/s  rrqm/s  wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz  rareq-s  wareq-s  svctm  %util
nvme0n1         10.0   980.0    0.5    12.0     0.0    20.0    0.0    2.0   1.20   18.50  20.10    52.0    12.5   0.90  89.0

Ce que ça signifie : Les écritures dominent ; w_await est élevé et %util approche de la saturation. Le périphérique se met en file d’attente.

Décision : Ce n’est pas purement overlay2. Il vous faut soit plus d’IOPS/moins de latence, soit moins d’écritures synchrones, soit isoler les charges.

Task 5: Identify which process is doing the I/O

cr0x@server:~$ sudo iotop -oPa
Total DISK READ:         0.00 B/s | Total DISK WRITE:      25.30 M/s
  PID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
23144 be/4  999        0.00 B/s   18.20 M/s  0.00 %  35.00 % postgres: wal writer process
19872 be/4  root       0.00 B/s    5.10 M/s  0.00 %  10.00 % dockerd --host=fd://

Ce que ça signifie : Un processus de base de données (probablement à l’intérieur d’un conteneur) pousse des écritures WAL ; dockerd est aussi actif.

Décision : Mappez le PID au conteneur et vérifiez où vit son répertoire de données. Les bases sur overlay2 sont un piège bien connu.

Task 6: Map a PID to a container

cr0x@server:~$ ps -o pid,cmd -p 23144
  PID CMD
23144 postgres: wal writer process

cr0x@server:~$ sudo cat /proc/23144/cgroup | grep -E 'docker|kubepods' | head -n 1
0::/docker/5c3b1f2d0b0a8b3d5a2d6c5d8c1f0e9a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3

Ce que ça signifie : Le processus appartient à un conteneur Docker avec cet ID en préfixe.

Décision : Inspectez les mounts de ce conteneur (Task 7). Si PGDATA n’est pas un volume/bind mount, corrigez cela.

Task 7: Check container mounts and confirm whether data is on overlay2

cr0x@server:~$ docker inspect 5c3b1f2d0b0a --format '{{range .Mounts}}{{println .Destination .Type .Source}}{{end}}'
/var/lib/postgresql/data volume /var/lib/docker/volumes/pgdata/_data

Ce que ça signifie : Le répertoire de données est un volume Docker (bon signe). Si vous n’aviez pas de mount pour PGDATA, il serait dans overlay2 (mauvais).

Décision : S’il s’agit déjà d’un volume et que c’est lent, le goulot est probablement le stockage sous-jacent ou les patterns de fsync, pas la superposition.

Task 8: Prove whether writes hit overlay2 paths

cr0x@server:~$ sudo lsof -p 23144 | grep overlay2 | head
postgres 23144 999  cwd    DIR  8,2     4096  131081 /var/lib/docker/overlay2/9d2f.../merged/var/lib/postgresql/data

Ce que ça signifie : Si vous voyez des fichiers ouverts sous /var/lib/docker/overlay2/.../merged, ce processus opère sur le montage en union.

Décision : Déplacez ce chemin vers un volume. Si c’est une base de données, n’en débattez pas — faites-le.

Task 9: Check inode exhaustion (sneaky “disk full”)

cr0x@server:~$ df -hi /var/lib/docker
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/nvme0n1p2    20M    19M     1M   95% /var/lib/docker

Ce que ça signifie : Vous êtes proche d’épuiser les inodes. Les écritures peuvent échouer ou se dégrader à mesure que le FS lutte.

Décision : Nettoyez images/couches, déplacez les répertoires à fort turnover vers des volumes, et envisagez un filesystem avec plus d’inodes ou une disposition différente pour la racine Docker.

Task 10: Find big writable layers and churners

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.ID}}'
NAMES                 ID
api-1                  2f1c9b8c0d3a
worker-1               8a7d6c5b4e3f
postgres-1             5c3b1f2d0b0a

cr0x@server:~$ docker container inspect api-1 --format '{{.GraphDriver.Data.UpperDir}}'
/var/lib/docker/overlay2/1a2b3c4d5e6f7g8h9i0j/upper

cr0x@server:~$ sudo du -sh /var/lib/docker/overlay2/1a2b3c4d5e6f7g8h9i0j/upper
18G	/var/lib/docker/overlay2/1a2b3c4d5e6f7g8h9i0j/upper

Ce que ça signifie : La couche inscriptible du conteneur est énorme. Cela signifie généralement que quelqu’un écrit des données qui devraient être sur un volume, ou qu’on cache agressivement dans le système de fichiers du conteneur.

Décision : Identifiez quels répertoires grossissent et déplacez-les vers des volumes/bind mounts (ou tmpfs). Traitez un upperdir gigantesque comme un mauvais signe opérationnel.

Task 11: Confirm whether your workload is fsync-bound

cr0x@server:~$ sudo strace -p 23144 -e trace=fdatasync,fsync -tt -T -f
18:20:11.102938 fdatasync(7)            = 0 <0.024981>
18:20:11.128201 fdatasync(7)            = 0 <0.031442>

Ce que ça signifie : Chaque appel de sync coûte ~25–30ms. C’est brutal pour le débit d’une base. overlay2 peut ajouter un surcoût, mais l’ennemi réel est la latence de sync.

Décision : Mettez le WAL/les données sur le stockage le plus rapide que vous pouvez justifier, et évitez les chemins union FS. Si vous ne pouvez pas changer le stockage, réduisez la fréquence des fsync seulement si votre modèle de durabilité le permet.

Task 12: Benchmark overlay2 path vs a volume path (A/B test, not vibes)

cr0x@server:~$ docker run --rm -it alpine sh -lc 'apk add --no-cache fio >/dev/null && fio --name=randwrite --directory=/tmp --size=512m --bs=4k --rw=randwrite --iodepth=1 --direct=1 --numjobs=1 --runtime=20 --time_based --fsync=1'
randwrite: (groupid=0, jobs=1): err= 0: pid=23: Fri Jan  3 18:20:45 2026
  write: IOPS=120, BW=480KiB/s (492kB/s)(9600KiB/20001msec)
    clat (usec): min=2000, max=65000, avg=8000.00, stdev=5000.00

cr0x@server:~$ docker volume create fiotest
fiotest

cr0x@server:~$ docker run --rm -it -v fiotest:/data alpine sh -lc 'apk add --no-cache fio >/dev/null && fio --name=randwrite --directory=/data --size=512m --bs=4k --rw=randwrite --iodepth=1 --direct=1 --numjobs=1 --runtime=20 --time_based --fsync=1'
randwrite: (groupid=0, jobs=1): err= 0: pid=23: Fri Jan  3 18:21:15 2026
  write: IOPS=320, BW=1280KiB/s (1311kB/s)(25600KiB/20002msec)
    clat (usec): min=1500, max=32000, avg=3500.00, stdev=2000.00

Ce que ça signifie : Le chemin sur volume fournit des IOPS sensiblement meilleurs et une latence plus faible pour des écritures 4k sync-intensives.

Décision : Si votre charge ressemble à ce schéma (bases de données, queues), déplacez-la hors d’overlay2.

Task 13: Check Docker’s logging driver and log path impact

cr0x@server:~$ docker info --format '{{.LoggingDriver}}'
json-file

cr0x@server:~$ docker inspect api-1 --format '{{.LogPath}}'
/var/lib/docker/containers/2f1c9b8c0d3a.../2f1c9b8c0d3a...-json.log

cr0x@server:~$ sudo ls -lh /var/lib/docker/containers/2f1c9b8c0d3a.../*json.log
-rw-r----- 1 root root 9.2G Jan  3 18:21 /var/lib/docker/containers/2f1c9b8c0d3a.../2f1c9b8c0d3a...-json.log

Ce que ça signifie : Les logs grossissent sur le système racine Docker. Cela peut concurrencer les écritures overlay2 et remplir les disques rapidement.

Décision : Faites la rotation des logs, limitez-les, ou changez de driver de logs adapté à votre environnement. Ne laissez pas le « printf debugging » devenir un DoS de stockage.

Task 14: Look for mount propagation mistakes (volume not actually mounted)

cr0x@server:~$ docker exec -it api-1 sh -lc 'mount | grep -E "/data|overlay" | head -n 3'
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...,workdir=...)
/dev/nvme0n1p2 on /data type ext4 (rw,relatime)

Ce que ça signifie : /data est un montage réel (bon). Si vous ne voyez que le montage overlay et aucun montage séparé pour votre chemin de données prévu, votre « volume » n’est pas monté là où vous le pensez.

Décision : Corrigez la spec du conteneur (chemin de destination erroné, faute de frappe, -v manquant), puis retestez les performances. Les hypothèses sont l’endroit où naissent les incidents.

Quand passer aux volumes (et quand ne pas le faire)

Passer aux volumes quand les données sont mutables et que l’une des conditions suivantes s’applique

  • C’est une base de données (Postgres, MySQL, MongoDB, Redis avec AOF, etc.). Les bases ne sont pas « juste des fichiers ». Ce sont des machines à fsync bien chorégraphiées.
  • C’est un journal d’écriture ou un journal (segments type Kafka, logs de durabilité de file, translogs d’index de recherche). Ces charges punissent la latence.
  • C’est un état applicatif à fort turnover (uploads, répertoires de cache importants, contenu généré par les utilisateurs).
  • C’est un output de build dont vous dépendez entre redémarrages (caches CI, caches de paquets) et vous voulez de la prévisibilité.
  • Vous avez besoin de sémantiques de sauvegarde/restauration qui ne se résument pas à « commit du conteneur et prière ». Les volumes donnent une frontière claire pour les snapshots et la migration.

Conserver overlay2 pour ce pour quoi il est bon

  • Bits applicatifs immuables : binaires, bibliothèques, modèles de configuration. C’est le rôle des couches d’image.
  • Fichiers temporaires éphémères qui n’ont pas besoin de persister et qui n’appellent pas un million de fsync. Si vous avez besoin de vitesse, utilisez tmpfs. Si vous avez besoin de persistance, utilisez un volume.

Volumes vs bind mounts : choisissez délibérément

Un bind mount est « montez ce chemin de l’hôte dans le conteneur ». Un volume Docker est « Docker gère un chemin hôte et le monte pour vous ».
Les performances peuvent être similaires car les deux contournent la couche union sur ce chemin. La différence est opérationnelle :

  • Les volumes sont plus faciles à inventorier, migrer et raisonner via les outils Docker. Ils évitent aussi un couplage accidentel à la hiérarchie de répertoires de l’hôte.
  • Les bind mounts sont excellents quand vous avez besoin d’un contrôle précis (arbres de répertoires existants, système de fichiers spécifique, intégration avec des outils hôtes). Ils facilitent aussi le montage involontaire de quelque chose que vous ne vouliez pas monter.

Mon biais : en production, préférez les volumes à moins d’avoir une raison claire de ne pas le faire.
Votre futur vous remerciera d’avoir moins de conversations « pourquoi ce chemin est vide sur le nouveau nœud ? ».

Blague #1 : Faire tourner une base de données sur overlay2, c’est comme aller sur un chantier en tongs — possible, mais le rapport des blessures s’écrit tout seul.

Comment basculer en toute sécurité : modèles qui ne vous réveillent pas la nuit

Modèle 1 : Répertoires de données explicites, jamais « ce que l’image utilise par défaut »

Les images définissent souvent des chemins de données par défaut dans le système de fichiers du conteneur. Si vous ne les remplacez pas par un montage de volume, vous utilisez implicitement overlay2.
Pour les bases, soyez explicite et bruyant.

cr0x@server:~$ docker volume create pgdata
pgdata

cr0x@server:~$ docker run -d --name pg \
  -e POSTGRES_PASSWORD=example \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16
c1d2e3f4a5b6c7d8e9f0

Modèle 2 : Déplacez uniquement le chemin chaud, pas tout le système de fichiers

Vous n’avez pas besoin de monter /. Vous n’avez pas besoin de tout replatformer. Identifiez les répertoires à forte écriture :
données de base, WAL, uploads, caches, logs (parfois).
Montez ceux-là. Laissez le reste tranquille.

Modèle 3 : Séparez WAL/log des données quand les besoins latence diffèrent

Ce n’est pas nécessaire pour tous les déploiements, mais quand c’est utile, c’est salvateur :
placez le WAL (ou équivalent) sur la classe de stockage la plus rapide, gardez les données volumineuses sur du stockage capacité.
C’est plus simple avec un orchestrateur, mais vous pouvez le faire aussi avec Docker si vous êtes discipliné.

Modèle 4 : Évitez d’écrire les logs dans les couches des conteneurs

Logger sur stdout n’est pas automatiquement « gratuit », mais cela évite le système de fichiers en union pour les logs applicatifs.
Si vous loggez dans des fichiers, montez un volume ou un bind mount pour le répertoire de logs et faites la rotation.

Modèle 5 : Décidez de la durabilité dont vous avez réellement besoin

Le débat sur le driver de stockage cache souvent une décision métier : la perte de données est-elle acceptable ?
Si vous désactivez fsync (ou utilisez des modes async), vous obtiendrez de meilleurs chiffres — jusqu’au moment où ce n’est plus vrai.
« On peut perdre les dernières 5 secondes de données » est une politique valide. « On n’y a pas pensé » ne l’est pas.

Trois mini-récits d’entreprise depuis le terrain

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

Une équipe a déployé un nouveau service API qui ingérait des événements et les écrivait dans une file locale avant de les pousser vers le pipeline principal.
En staging tout semblait correct. En production, ça a commencé à timeout sous charge. Le graphe on-call montrait la latence monter et une hausse soudaine de iowait.
Le CPU était bas, ce qui rendait tout le monde soupçonneux du « réseau » parce que c’est ce qu’on fait quand le CPU n’est pas coupable.

Ils supposaient que comme la file était « juste un fichier », elle se comporterait comme un fichier hôte. Mais le fichier vivait à l’intérieur du système de fichiers du conteneur.
Ça signifiait overlay2. Ça signifiait la sémantique du montage en union. Et sous charge, la file a fait ce que font les files : beaucoup de petits ajouts, beaucoup de syncs.

Le symptôme était étrange : le débit tenait quelques minutes après le déploiement, puis se dégradait.
La couche inscriptible grossissait, les métadonnées devenaient plus chaudes, et la file du périphérique se formait.
Quelqu’un a essayé d’ajouter du CPU parce que les tableaux semblaient vides. Cela n’a rien fait. Bien sûr que non.

La correction fut ennuyeuse : monter un volume sur le répertoire de la file et redéployer.
La latence s’est stabilisée. L’amplification d’écriture a chuté. L’incident s’est terminé discrètement, ce qui est le seul type de fin à viser.

La vraie leçon : si un composant est une frontière de durabilité (file, journal, base), traitez son chemin de stockage comme de l’infrastructure, pas comme un détail d’implémentation.
La couche du conteneur n’est pas de l’infrastructure ; c’est de l’emballage.

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

Une équipe plateforme a observé des temps de build lents en CI. Les conteneurs dépaquetaient des dépendances à répétition.
Ils ont décidé de « l’accélérer » en cachant les répertoires de paquets à l’intérieur du système de fichiers du conteneur, pensant éviter l’I/O externe.
Ils appréciaient aussi la simplicité : moins de volumes, moins de montages, moins d’« état ».

Ça a marché—brièvement. Puis l’utilisation du disque a explosé sous /var/lib/docker/overlay2.
L’hôte n’était pas à court d’octets, mais il saignait en inodes. Les jobs de nettoyage ont commencé à durer plus longtemps.
Les nouveaux builds sont devenus plus lents, pas plus rapides, parce que chaque conteneur commençait avec une couche inscriptible grosse et beaucoup de churn de répertoire.

L’équipe a alors « optimisé » le nettoyage en élaguant les images agressivement entre jobs.
Cela a réduit l’usage disque mais ajouté du trafic de pulls vers le registre et des cache misses, et accru la pression d’écriture pendant l’extraction car rien n’était chaud.
L’effet net fut une plus grande variance et plus de timeouts sporadiques.

La correction fut de déplacer les caches vers un volume dédié (ou un répertoire cache par runner via bind mount) et de le gérer intentionnellement :
limites, expiration et propriété claire. Les sorties de build sont redevenues prévisibles.

La morale : le caching, c’est du stockage. Si vous le traitez comme un effet secondaire, il traitera votre disque comme une suggestion.

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

Une autre organisation exécutait plusieurs services stateful en conteneurs—pas par effet de mode, mais parce que ça réduisait la dérive et rendait les upgrades moins terrifiants.
Ils avaient une règle qui sonnait peu glamour en revue d’architecture : tout état persistant doit être sur des volumes nommés, et chaque service doit documenter ses mounts de volumes.

Quand un nœud a commencé à montrer une latence d’écriture élevée, ils ont pu immédiatement séparer les « écritures overlay2 » des « écritures volume ».
Leurs tableaux suivaient la latence disque par périphérique, et leurs specs de déploiement montraient clairement ce qui vivait où.
Le triage était rapide parce que la disposition était cohérente entre les services.

Pendant l’incident, ils ont migré en direct une charge vers un nœud avec un meilleur stockage et rattaché les volumes.
Aucune donnée de couche de conteneur n’était critique, donc ils n’ont pas eu à copier des répertoires aléatoires hors de /var/lib/docker/overlay2.
Le temps de récupération s’est mesuré en minutes, pas en archéologie.

Les propriétaires de service détestaient la règle en prototypage parce qu’elle les obligeait à penser aux chemins tôt.
L’équipe on-call l’a adorée pour toujours.

Blague #2 : Rien ne tue le récit du « microservice sans état » comme une couche inscriptible de 20 Go nommée « upper ».

Erreurs courantes : symptôme → cause racine → correctif

Cette section se lit après que vous ayez déjà essayé de redémarrer. Ne vous inquiétez pas, on est tous passés par là.

1) Les écritures sont lentes seulement à l’intérieur du conteneur

Symptôme : La même opération sur l’hôte est rapide ; à l’intérieur du conteneur elle est lente.

Cause racine : Le chemin de données est dans overlay2, payant le copy-on-write et le surcoût des métadonnées ; ou le conteneur utilise des patterns fsync différents à cause d’une config.

Correctif : Déplacez le chemin inscriptible vers un volume/bind mount. Vérifiez avec mount dans le conteneur et faites un A/B benchmark.

2) Latence de base de données en pic pendant checkpoints/flushes

Symptôme : Stalls périodiques ; le writer WAL ou le processus de checkpoint montre un IO wait élevé.

Cause racine : Latence de fsync et contention du journal sur le stockage sous-jacent ; overlay2 peut l’amplifier si les fichiers DB sont dans la couche inscriptible.

Correctif : Assurez-vous que les données/WAL DB sont sur des volumes sur un stockage rapide. Si c’est toujours lent, adressez la latence/IOPS sous-jacente (classe de stockage, device, tuning filesystem).

3) Erreurs « disque plein » mais df -h montre de l’espace

Symptôme : Les écritures échouent ; les pulls Docker échouent ; les conteneurs plantent ; mais les octets ne sont pas épuisés.

Cause racine : Épuisement des inodes sur la racine Docker, souvent à cause d’un fort churn de fichiers dans les upperdirs overlay2 ou des caches de build.

Correctif : Vérifiez df -hi. Prunez, réduisez le churn, déplacez les chemins à fort churn vers des volumes, et envisagez un filesystem/disposition avec suffisamment d’inodes.

4) La performance se dégrade avec le temps sans montée en charge

Symptôme : Même RPS, pire latence après jours/semaines.

Cause racine : Les couches inscriptibles accumulent beaucoup de fichiers/whiteouts ; les opérations sur métadonnées ralentissent ; des backups/antivirus touchent la racine Docker ; les fichiers de log grossissent.

Correctif : Gardez l’état hors d’overlay2, faites la rotation des logs, élagage des images/conteneurs inutilisés, et ne partagez pas la racine Docker avec des processus hôtes bruyants.

5) « On est passés aux volumes et c’est toujours lent »

Symptôme : Les données sont sur un volume, mais la latence d’écriture reste élevée.

Cause racine : Le stockage sous-jacent est le goulot (disque réseau IOPS-limit, crédits burst, throttling), ou la charge est liée à la latence de sync.

Correctif : Mesurez la latence du périphérique avec iostat, regardez le timing des fsync, et passez à un stockage isolé/plus performant. Un volume ne rendra pas un disque lent rapide.

6) Erreurs mystérieuses sur une racine Docker sur XFS

Symptôme : Comportement de fichiers étrange ou avertissements ; parfois mauvaise performance.

Cause racine : XFS formaté avec ftype=0 (pas de d_type) ou combinaison noyau/filesystem non supportée.

Correctif : Migrez la racine Docker vers XFS avec ftype=1 (ou utilisez ext4). C’est un travail de rebuild/migration, pas un simple toggle.

7) Croissance massive de la racine Docker et comportement lent du nœud

Symptôme : /var/lib/docker grossit rapidement ; le nœud devient lent ; les opérations conteneur ralenties.

Cause racine : Logs conteneurs sans limites, couches inscriptibles volumineuses, ou artefacts de build qui s’emballent à l’intérieur des conteneurs.

Correctif : Mettez en place des limites/rotation des logs, déplacez les artefacts vers des volumes, et appliquez des politiques autour des builds et des écritures runtime.

Listes de contrôle / plan pas à pas

Checklist A: Décider si vous devez passer aux volumes (gating rapide)

  • Les données sont-elles mutables ? Si oui, penchez volume.
  • La charge appelle souvent fsync/fdatasync ? Si oui, évitez overlay2 pour ce chemin.
  • Le chemin est-il supposé dépasser quelques centaines de Mo ? Si oui, ne le laissez pas dans upperdir.
  • Avez-vous besoin de sauvegardes ou de migration ? Si oui, les volumes structurent la chose.
  • Le chemin est-il un cache que vous pouvez perdre ? Si oui, envisagez tmpfs ou acceptez overlay2 mais limitez/nettoyez.

Checklist B: Plan de migration pour un service stateful (édition sans drame)

  1. Identifiez les vrais répertoires de données. Lisez la config de l’app. Ne devinez pas. Pour les bases, trouvez le répertoire de données et les WAL/redo logs.
  2. Créez des volumes aux noms signifiants. Évitez « data1 » pour tout ; vous le regretterez.
  3. Arrêtez le service proprement. Laissez-le flush. « Kill -9 » n’est pas un outil de migration.
  4. Copiez les données de l’ancien chemin vers le volume. Préservez propriétaires et permissions.
  5. Montez le volume au même chemin exact que l’app attend. La cohérence gagne.
  6. Démarrez et validez par des vérifications applicatives. Pas seulement « le conteneur tourne ».
  7. Benchmarkez le chemin d’écriture (fio ou métriques applicatives) et comparez au baseline.
  8. Mettez en place une politique : rotation des logs, limites de taille, calendriers de pruning.

Checklist C: Si les volumes n’ont pas réparé (vérif stockage)

  • Mesurez la latence et la saturation du périphérique avec iostat.
  • Vérifiez le throttling des stockages burstables (les volumes cloud peuvent le faire silencieusement).
  • Confirmez la santé du filesystem et les options de montage.
  • Cherchez des voisins bruyants : autres conteneurs écrivant des logs ou faisant des compactages.
  • Envisagez de séparer racine Docker, volumes et logs sur des périphériques différents.

FAQ

1) Overlay2 est-il « lent » par conception ?

overlay2 est optimisé pour la superposition d’images et un usage raisonnable des systèmes de fichiers de conteneurs. Il n’est pas pensé comme un filesystem haute performance pour bases.
Pour des écritures lourdes et sensibles à la latence de sync, il est souvent plus lent qu’un montage direct.

2) Les volumes Docker surpassent-ils toujours overlay2 ?

Pas toujours. Si le stockage sous-jacent est lent, un volume ne changera pas la physique.
Mais les volumes suppriment le surcoût du union filesystem sur le chemin monté, ce qui aide souvent pour les charges riches en métadonnées et fsync.

3) Et les bind mounts — sont-ils « plus rapides » que les volumes ?

Les performances sont généralement similaires. La différence tient à la gestion et à la sécurité.
Les volumes sont plus simples à inventorier et migrer avec les outils Docker ; les bind mounts sont des chemins hôtes explicites et utiles quand vous avez besoin de ce contrôle.

4) Pourquoi les petites écritures souffrent-elles plus que les gros writes séquentiels ?

Les petites écritures synchrones sont dominées par la latence par opération : mises à jour de métadonnées, commits de journal, flushs, et parfois barrières.
overlay2 ajoute des couches de travail supplémentaires. Les gros writes séquentiels peuvent être bufferisés et streamés plus efficacement.

5) Puis-je « tuner » overlay2 pour qu’il soit aussi rapide que les volumes ?

Vous pouvez réduire la douleur (mises à jour noyau, choix du filesystem, éviter les patterns copy-up), mais vous ne pouvez pas éliminer la sémantique fondamentale du union filesystem.
Si vous tenez à la performance d’écriture sur des données mutables, montez-les depuis l’hôte.

6) Dois-je mettre /var/lib/docker sur un disque séparé ?

Souvent oui en production. Séparer la racine Docker du disque OS réduit la contention et facilite la planification de capacité.
Si les logs sont aussi sur le même disque, vous invitez un service bruyant à partager une chambre avec un dormeur léger.

7) tmpfs est-il une bonne alternative aux volumes pour la performance ?

Pour des données vraiment éphémères, oui. tmpfs est rapide et évite la latence disque.
Mais il utilise de la RAM (et peut swapper si vous n’y prenez pas garde). Ne mettez pas de données importantes sur tmpfs à moins d’aimer expliquer une perte de données.

8) Kubernetes change-t-il le conseil ?

Le principe reste : ne stockez pas d’état mutable et sensible aux performances dans la couche du conteneur.
Dans Kubernetes vous utiliserez PersistentVolumes, hostPath (avec précaution), ou des volumes éphémères comme emptyDir (backés par disque ou mémoire) selon les besoins de durabilité.

9) Si ma base est sur un volume, puis-je ignorer overlay2 complètement ?

Pas complètement. Le démarrage/arrêt des conteneurs, les pulls d’images, et toute écriture hors de vos répertoires montés frappent encore overlay2 et la racine Docker.
Gardez la racine Docker saine : espace, inodes, rotation des logs et hygiène des images.

10) Quelle est la règle la plus simple qui évite la plupart des incidents d’écriture overlay2 ?

Si ça persiste après un redémarrage et que vous seriez triste de sa perte, mettez-le sur un volume. Si c’est une base de données, mettez-la sur un volume même si vous pensez que ça ira.

Étapes pratiques suivantes

overlay2 est un excellent défaut pour empaqueter et lancer des logiciels. Ce n’est pas un bon endroit pour entasser des données mutables, à fort churn ou sensibles à la durabilité.
Quand les écritures sont lentes, la bonne question est : « Quel chemin est chaud, et vit-il dans la couche du conteneur ? »

Étapes concrètes que vous pouvez faire aujourd’hui :

  1. Exécutez le guide de diagnostic rapide : identifiez l’écrivain, mappez-le à un conteneur, confirmez le chemin de données.
  2. Benchmarkez overlay2 vs un chemin de volume avec un A/B fio rapide qui reflète votre charge (sync vs async compte).
  3. Déplacez les bases, files et logs importants sur des volumes. Laissez le reste dans l’image.
  4. Mettez des garde-fous : limites de logs, surveillance d’inodes, et une politique simple que l’état persistant ne vit jamais dans upperdir.

Une idée paraphrasée de Werner Vogels (CTO d’Amazon) : on construit la fiabilité en s’attendant à la défaillance et en concevant des systèmes qui continuent de fonctionner.
Traitez overlay2 comme une couche d’empaquetage, pas comme une stratégie de stockage, et vos systèmes deviendront ennuyeux dans le meilleur sens du terme.

← Précédent
Debian/Ubuntu « Fonctionne en LAN, échoue en WAN » : vérifications de routage et NAT qui révèlent la cause (cas n°25)
Suivant →
Contenu mixte WordPress : pourquoi HTTPS affiche encore des avertissements et comment réparer correctement

Laisser un commentaire