Vous vous réveillez avec le système racine plein. Docker « n’utilise que 40 Go » selon docker system df,
pourtant df -h hurle. Les builds CI sont lents. Les prune ne servent à rien. Quelqu’un suggère « ajoutez du disque »,
et vous sentez votre futur vous en astreinte préparer une plainte.
Sous ZFS, la différence entre un hôte qui exécute des conteneurs tranquillement pendant des années et un hôte qui explose en
mille petites couches, snapshots et clones tient surtout à une chose : la disposition des datasets. Bien faite, l’utilisation
disque devient lisible. Mal faite, vous ferez de l’archéologie avec zdb à 2 h du matin.
À quoi ressemble « l’explosion de couches » sur ZFS
Les images Docker sont des piles de couches. Avec le driver ZFS, chaque couche peut correspondre à un dataset ZFS
(ou à un clone d’un snapshot) selon l’implémentation et la version de Docker. Ce n’est pas automatiquement mauvais.
Les clones ZFS sont bon marché à la création. La facture arrive plus tard, avec les intérêts :
- Tempêtes de clones : chaque démarrage de conteneur produit un clone de couche inscriptible, et votre pool
commence à ressembler à un arbre généalogique. - Prolifération de snapshots : les couches s’accompagnent de snapshots ; les snapshots maintiennent des blocs vivants ;
les données « supprimées » comptent encore car elles sont référencées. - Churn des métadonnées : beaucoup de petits datasets signifie beaucoup de métadonnées, d’opérations de montage
et de surprises d’héritage de propriétés. - Comptabilité d’espace trompeuse (pour les humains) :
dfvoit un point de montage, Docker voit des couches logiques,
ZFS voit des octets référencés, et votre cerveau ne voit rien clairement.
L’explosion de couches n’est pas seulement « trop d’images ». C’est trop d’objets de système de fichiers dont la durée de vie ne correspond pas
à votre cycle opérationnel. La solution n’est pas « prune davantage » ; la solution est d’aligner les frontières des datasets ZFS
sur ce que vous gérez réellement : l’état du moteur, le cache d’images, les couches inscriptibles et les données applicatives persistantes.
Faits intéressants et contexte historique
- Les clones ZFS ont été conçus pour le provisioning instantané (pensez environnements développeur et modèles de VM). Le modèle de couches de Docker correspond parfois trop bien à cette idée.
- Docker a initialement fortement poussé AUFS car les systèmes de fichiers en union étaient le modèle mental le plus simple pour les couches. ZFS est arrivé plus tard comme driver avec des sémantiques différentes et des bords plus tranchants.
- OverlayFS a remporté la place par défaut sur la plupart des distributions Linux en grande partie parce qu’il est « suffisant » et vit dans le noyau, sans l’histoire du module ZFS séparé.
- ZFS distingue « referenced » vs « logical ». C’est pourquoi « je l’ai supprimé » n’est pas la même chose que « le pool a récupéré l’espace » quand snapshots et clones sont impliqués.
- Les valeurs par défaut de recordsize (128K) datent de workloads orientés débit. Les bases de données, charges avec petits fichiers et couches de conteneurs veulent parfois d’autres valeurs ; « une taille unique pour tous » est un mythe.
- La compression LZ4 est devenue le choix évident dans de nombreux déploiements ZFS car elle est suffisamment rapide pour être ennuyeuse et réduit souvent les écritures—surtout pour les couches d’images pleines de texte et binaires.
- La déduplication est une histoire d’avertissement récurrente dans le monde ZFS : attrayante sur les slides, impitoyable en RAM et en complexité opérationnelle. Les images conteneur poussent souvent à l’essayer.
- « ZFS on Linux » a mûri pour devenir OpenZFS en projet multiplateforme. Cette maturité explique pourquoi beaucoup de personnes utilisent maintenant ZFS en production pour des hôtes conteneurs en toute confiance.
Objectifs de conception : ce qu’apporte une disposition sensée
Si vous exécutez Docker sur ZFS en production, vous voulez que l’hôte se comporte comme un appareil :
mises à jour prévisibles, rollbacks ennuyeux (et fiables), planification de capacité simple, et des erreurs bruyantes tôt plutôt que subtiles tard.
1) Séparer les cycles de vie
L’état du moteur Docker, le cache d’images, les couches inscriptibles et les volumes persistants n’ont pas le même cycle de vie.
Les traiter comme un seul arbre de répertoires sous un dataset unique est la voie la plus rapide vers « on ne peut pas nettoyer sans risquer la production ».
2) Rendre la comptabilité d’espace lisible
ZFS peut vous dire exactement où sont les octets—si vous lui donnez des frontières qui correspondent à votre modèle mental.
Les datasets vous donnent used, usedbydataset, usedbysnapshots, usedbychildren, des quotas et des réservations.
Un dataset monolithique vous donne un grand nombre et un mal de tête.
3) Empêcher que les déchets survivent
Le churn Docker crée des déchets qui persistent à cause des snapshots et des clones. La disposition doit permettre de
détruire des sous-arborescences entières en toute sécurité (et rapidement) quand vous décidez que le cache ou les couches sont jetables.
4) Garder la performance réglable
Les couches inscriptibles des conteneurs se comportent comme des écritures aléatoires petites. Les pulls d’images ressemblent à des écritures séquentielles.
Les bases de données dans des volumes peuvent avoir leurs propres besoins. Il vous faut des propriétés au niveau dataset pour pouvoir tuner sans transformer tout le pool en laboratoire.
Idée paraphrasée de Werner Vogels : « Tout échoue, tout le temps—concevez pour que les échecs soient contenus et récupérables. »
C’est exactement ce que font les frontières de datasets pour les défaillances de stockage : elles contiennent le rayon d’explosion.
Disposition recommandée des datasets (à faire)
Le schéma est simple : un pool, quelques datasets de premier niveau avec une propriété claire, et exactement un endroit où Docker est autorisé à faire ses clones/snapshots étranges.
Pool et datasets de premier niveau
tank/ROOT/<os>— vos datasets root OS, gérés par vos outils OStank/var—/vargénéral, pas Dockertank/var/lib/docker— le monde interne de Docker (images, couches, métadonnées)tank/var/lib/docker/volumes— optionnellement séparé, mais généralement je préfère les volumes en dehors de l’arbre Docker (voir ci‑dessous)tank/containers— données applicatives persistantes (bind mounts, volumes compose via chemins hôtes)tank/containers/<app>— datasets par application avec quotas/refquotastank/backup— cibles de réplication (receive-only), pas de charges actives
Règle d’opinion : garder les données persistantes hors du dataset du driver Docker
Le driver ZFS de Docker est optimisé pour le comportement des images et des couches, pas pour « votre base de données qui ne doit jamais être supprimée ».
Mettez les données persistantes dans des datasets dédiés, montés quelque part de façon stable comme /containers, et bind‑montez cela dans les conteneurs.
Cela vous donne des politiques de réplication et de rétention propres.
Propriétés qui gagnent généralement
Ce sont des valeurs par défaut, pas une religion. Mais ce sont des types de valeurs par défaut ennuyeuses qui survivent au contact du CI,
des tempêtes de logs et d’administrateurs somnolents.
- Dataset Docker :
compression=lz4,atime=off,xattr=sa,acltype=posixacl(si votre distro l’attend),recordsize=16Kou32K(souvent meilleur pour le churn de couches que 128K). - Bases de données dans datasets persistants : tuner par moteur ; point de départ courant :
recordsize=16Kpour PostgreSQL, parfois8K, et considérerlogbias=latencypour des workloads sync‑intensifs. - Dataset de logs : souvent
compression=lz4etrecordsize=128Kconvient ; la lutte plus importante est la rétention, pas la taille de bloc. - Backups :
readonly=onsur les cibles receive pour prévenir les modifications accidentelles.
Blague #1 : les snapshots ZFS sont comme les emails de bureau—supprimer une chose ne signifie pas qu’elle a disparu, ça signifie qu’elle est « archivée pour toujours par quelqu’un d’autre ».
Où l’explosion de couches s’arrête réellement
L’explosion de couches devient gérable lorsque :
- Le dataset du driver Docker est jetable et borné par des quotas (pour qu’un build incontrôlé ne puisse pas dévorer l’hôte).
- Les données persistantes vivent ailleurs, si bien que « nuke Docker state » est une option de récupération légitime.
- Les snapshots sur le dataset Docker sont évités ou strictement contrôlés (parce que Docker utilise déjà des snapshots/clones en interne).
Pourquoi ça marche : mécanismes ZFS importants
Les clones maintiennent les blocs vivants
Le driver ZFS s’appuie sur des snapshots et des clones : les couches d’images deviennent des snapshots, les couches inscriptibles deviennent des clones.
C’est efficace—jusqu’à ce que vous essayiez de récupérer de l’espace. Une couche supprimée peut encore référencer des blocs via une chaîne de clones.
Le pool voit des « octets référencés », et ces octets référencés ne disparaissent pas simplement parce que Docker les a oubliés.
Les frontières de datasets sont des frontières opérationnelles
Si votre état Docker est dans un seul dataset, vous pouvez définir des propriétés pour ce comportement global. Vous pouvez aussi
le détruire en bloc. Si vos volumes persistants vivent à l’intérieur de ce dataset, vous avez soudé vos bijoux de famille à votre tas d’ordures.
Quota et refquota ne sont pas le même outil
quota limite un dataset et ses enfants. refquota limite seulement le dataset lui‑même.
Pour « chaque appli obtient 200G mais peut créer des enfants dedans », quota est utile.
Pour « ce dataset ne doit pas croître, indépendamment des snapshots ailleurs », refquota vous donne un contrôle plus direct.
Le comportement de montage compte pour la fiabilité de Docker
Docker s’attend à ce que /var/lib/docker soit présent tôt et reste stable. Les datasets ZFS se montent via zfs mount
(souvent géré par des services systemd). Si vous enterrez Docker dans une hiérarchie auto‑montée avec des dépendances limites,
vous produirez tôt ou tard une course au démarrage et un daemon Docker très confus.
La pression ARC est réelle sur les hôtes conteneurs
ZFS adore la RAM. Les hôtes conteneurs aiment aussi la RAM. Si vous ne limitez pas l’ARC sur un nœud occupé, vous pouvez affamer
les workloads conteneurs de façons subtiles : reclaim élevé, pics de latence, et beaucoup de « c’est lent mais rien n’est saturé ».
Blague #2 : la dédup semble offrir du stockage gratuit jusqu’à ce que votre RAM comprenne ce que veut dire « heures sup obligatoires ».
Tâches pratiques (commandes, sorties et décisions)
Ce sont des tâches opérationnelles réelles que vous pouvez exécuter sur un hôte Docker utilisant ZFS. Chacune inclut : la commande, une sortie d’exemple,
ce que la sortie signifie, et la décision à prendre à partir de cela. Exécutez‑les dans cet ordre quand vous construisez la confiance, et en boucle serrée quand vous éteignez un feu.
Task 1: Confirm Docker is actually using the ZFS storage driver
cr0x@server:~$ docker info --format '{{.Driver}}'
zfs
Signification : le magasin d’images/couches de Docker est ZFS‑aware. Si la sortie est overlay2, cet article reste utile pour les volumes, mais pas pour la mécanique des couches.
Décision : Si ce n’est pas zfs, arrêtez‑vous et décidez si vous migrez de driver ou si vous organisez simplement les volumes.
Task 2: Identify the dataset backing /var/lib/docker
cr0x@server:~$ findmnt -no SOURCE,TARGET /var/lib/docker
tank/var/lib/docker /var/lib/docker
Signification : votre root Docker est un dataset, pas juste un répertoire. Bien—vous pouvez maintenant définir des propriétés et des quotas proprement.
Décision : Si ce n’est pas un dataset (par ex. /dev/sda2), planifiez une migration avant de toucher au tuning.
Task 3: List the Docker dataset and immediate children
cr0x@server:~$ zfs list -r -o name,used,refer,avail,mountpoint tank/var/lib/docker | head
NAME USED REFER AVAIL MOUNTPOINT
tank/var/lib/docker 78.4G 1.20G 420G /var/lib/docker
tank/var/lib/docker/zfs 77.1G 77.1G 420G /var/lib/docker/zfs
Signification : le driver de Docker crée souvent un dataset enfant (souvent nommé zfs) contenant les datasets de couches.
Décision : Si vous voyez des milliers d’enfants sous cet arbre, l’explosion de couches est déjà en cours ; gérez‑la avec des quotas et une cadence de nettoyage.
Task 4: Count how many datasets Docker has spawned
cr0x@server:~$ zfs list -r tank/var/lib/docker/zfs | wc -l
3427
Signification : c’est le nombre de datasets, pas le nombre d’images. Des milliers ne sont pas automatiquement fatals, mais cela corrèle avec des montages lents, des destructions lentes et des démarrages lents.
Décision : Si cela croît sans limite, vous avez besoin d’une rétention d’images plus stricte, d’un nettoyage CI, ou d’un nœud de build séparé que vous pouvez réinitialiser.
Task 5: See where the space is: dataset vs snapshots vs children
cr0x@server:~$ zfs list -o name,used,usedbydataset,usedbysnapshots,usedbychildren -r tank/var/lib/docker | head
NAME USED USEDDS USEDSNAP USEDCHILD
tank/var/lib/docker 78.4G 1.20G 9.30G 67.9G
tank/var/lib/docker/zfs 77.1G 2.80G 8.90G 65.4G
Signification : si usedbysnapshots est grand, des données « supprimées » sont maintenues par des snapshots. Si usedbychildren domine, les datasets de couches sont les principaux consommateurs d’espace.
Décision : Usage élevé de snapshots : réduisez la prise de snapshots sur les datasets Docker et nettoyez les vieux snapshots. Usage élevé des enfants : prunez images/containers et envisagez de réinitialiser le dataset Docker si c’est sûr.
Task 6: Find the oldest Docker-related snapshots (if any)
cr0x@server:~$ zfs list -t snapshot -o name,creation,used -s creation | grep '^tank/var/lib/docker' | head
tank/var/lib/docker@weekly-2024-11-01 Fri Nov 1 02:00 1.12G
tank/var/lib/docker@weekly-2024-11-08 Fri Nov 8 02:00 1.08G
Signification : les politiques de snapshots au niveau hôte incluent parfois par erreur les datasets Docker. C’est généralement contre‑productif avec le driver ZFS.
Décision : excluez les datasets Docker des plannings génériques de snapshots ; snapshottez plutôt les datasets applicatifs persistants.
Task 7: Check critical ZFS properties on Docker dataset
cr0x@server:~$ zfs get -o name,property,value -s local,inherited compression,atime,xattr,recordsize,acltype tank/var/lib/docker
NAME PROPERTY VALUE
tank/var/lib/docker compression lz4
tank/var/lib/docker atime off
tank/var/lib/docker xattr sa
tank/var/lib/docker recordsize 16K
tank/var/lib/docker acltype posixacl
Signification : ces propriétés influencent fortement la performance sur petits fichiers et la surcharge de métadonnées.
Décision : si atime=on, désactivez‑le pour les datasets Docker. Si la compression est désactivée, activez lz4 sauf raison spécifique.
Task 8: Apply a quota to bound Docker’s blast radius
cr0x@server:~$ sudo zfs set quota=250G tank/var/lib/docker
cr0x@server:~$ zfs get -o name,property,value quota tank/var/lib/docker
NAME PROPERTY VALUE
tank/var/lib/docker quota 250G
Signification : Docker ne peut plus consommer l’intégralité du pool et mettre l’hôte hors service.
Décision : choisissez un quota qui prend en charge votre churn d’images attendu plus de la marge. Si vous atteignez régulièrement le quota, corrigez la rétention ; n’augmentez pas immédiatement.
Task 9: Confirm pool health and see if you’re capacity-constrained
cr0x@server:~$ zpool status -x
all pools are healthy
cr0x@server:~$ zpool list
NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
tank 928G 721G 207G - - 41% 77% 1.00x ONLINE -
Signification : pool sain, 77 % de capacité, fragmentation modérée. À l’approche de 85–90 % d’utilisation, la performance ZFS et le comportement d’allocation se dégradent.
Décision : si la CAP est au‑dessus d’environ 85 %, priorisez la libération d’espace ou l’ajout de vdevs avant de chasser des micro‑optimisations.
Task 10: Identify write amplification and latency at a glance
cr0x@server:~$ iostat -x 1 3
Linux 6.8.0 (server) 12/25/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.1 0.0 6.2 9.8 0.0 71.9
Device r/s w/s rKB/s wKB/s avgrq-sz avgqu-sz await svctm %util
nvme0n1 210.0 980.0 9800.0 42000.0 72.1 8.90 7.40 0.52 62.0
Signification : %iowait et await élevés suggèrent que la latence stockage affecte le système. %util inférieur à 100 % signifie que vous pouvez être limité par l’encombrement de la file d’attente ou le comportement de sync, pas par le débit brut.
Décision : si await est élevé lors de tempêtes de builds, envisagez de séparer les nœuds de build, tuner les datasets sync‑intensifs, et vérifier l’efficacité du SLOG (si présent).
Task 11: Check ARC size and memory pressure signals
cr0x@server:~$ cat /proc/spl/kstat/zfs/arcstats | egrep '^(size|c|c_min|c_max) '
size 4 8589934592
c 4 10737418240
c_min 4 1073741824
c_max 4 17179869184
Signification : l’ARC est actuellement ~8G, peut croître jusqu’à ~16G. Sur un hôte conteneur, une ARC qui grandit sans limite peut affamer les workloads.
Décision : si votre nœud tue des conteneurs OOM pendant que l’ARC grandit, limitez l’ARC via les paramètres du module et laissez de la mémoire pour les applications.
Task 12: Find which datasets have the most snapshots (a proxy for churn)
cr0x@server:~$ zfs list -H -t snapshot -o name | awk -F@ '{print $1}' | sort | uniq -c | sort -nr | head
914 tank/var/lib/docker/zfs/graph/3f0c2b3d2a0e
842 tank/var/lib/docker/zfs/graph/9a1d11c7e6f4
Signification : si les datasets liés à Docker accumulent des snapshots en dehors de la gestion de Docker, quelque chose prend des snapshots trop agressivement.
Décision : auditez vos outils de snapshot ; excluez les arbres de couches Docker.
Task 13: Detect space held by deleted-but-referenced blocks (snapshots/clones)
cr0x@server:~$ zfs get -o name,property,value used,referenced,logicalused,logicalreferenced tank/var/lib/docker
NAME PROPERTY VALUE
tank/var/lib/docker used 78.4G
tank/var/lib/docker referenced 1.20G
tank/var/lib/docker logicalused 144G
tank/var/lib/docker logicalreferenced 3.10G
Signification : l’espace logique est plus élevé que l’espace physique utilisé : la compression fonctionne, et/ou des blocs sont partagés. L’essentiel : used inclut enfants et snapshots ; referenced est ce que ce dataset seul libérerait s’il était détruit.
Décision : si used est énorme mais referenced est petit, détruire le dataset pourrait libérer beaucoup (car cela emporte enfants et snapshots). C’est une stratégie de réinitialisation valide pour l’état Docker—si les données persistantes sont ailleurs.
Task 14: Create a persistent application dataset with a hard boundary
cr0x@server:~$ sudo zfs create -o mountpoint=/containers tank/containers
cr0x@server:~$ sudo zfs create -o mountpoint=/containers/payments -o compression=lz4 -o atime=off tank/containers/payments
cr0x@server:~$ sudo zfs set refquota=200G tank/containers/payments
cr0x@server:~$ zfs get -o name,property,value mountpoint,refquota tank/containers/payments
NAME PROPERTY VALUE
tank/containers/payments mountpoint /containers/payments
tank/containers/payments refquota 200G
Signification : les données persistantes ont leur propre point de montage et une limite stricte de taille.
Décision : bind‑montez /containers/payments dans les conteneurs. Si l’appli atteint le refquota, elle échoue de façon contenue au lieu de consommer l’hôte.
Task 15: Replicate persistent datasets safely (send/receive)
cr0x@server:~$ sudo zfs snapshot tank/containers/payments@replica-001
cr0x@server:~$ sudo zfs send -c tank/containers/payments@replica-001 | sudo zfs receive -u backup/containers/payments
cr0x@server:~$ zfs get -o name,property,value readonly backup/containers/payments
NAME PROPERTY VALUE
backup/containers/payments readonly off
Signification : vous avez transféré un snapshot cohérent. Le dataset reçu n’est pas automatiquement en lecture seule à moins de le définir.
Décision : définissez readonly=on sur les cibles de backup pour éviter les écritures accidentelles.
cr0x@server:~$ sudo zfs set readonly=on backup/containers/payments
cr0x@server:~$ zfs get -o name,property,value readonly backup/containers/payments
NAME PROPERTY VALUE
backup/containers/payments readonly on
Task 16: Verify Docker disk usage vs ZFS usage (spot the mismatch)
cr0x@server:~$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 44 12 38.7GB 22.3GB (57%)
Containers 61 9 4.1GB 3.5GB (85%)
Local Volumes 16 10 9.8GB 1.2GB (12%)
Build Cache 93 0 21.4GB 21.4GB
cr0x@server:~$ zfs list -o name,used,avail tank/var/lib/docker
NAME USED AVAIL
tank/var/lib/docker 78.4G 420G
Signification : Docker rapporte des tailles logiques qu’il pense contrôler. ZFS rapporte l’utilisé réel incluant snapshots et relations de clones. Si l’espace utilisé par ZFS est bien plus grand que la vue de Docker, vous avez des blocs référencés hors du comptage de Docker (souvent des snapshots).
Décision : chassez les snapshots et clones retenant l’espace ; envisagez d’exclure les datasets Docker des outils de snapshot et de réinitialiser l’état Docker si nécessaire.
Playbook de diagnostic rapide
Quand l’hôte est lent ou plein, ne commencez pas par des prune aléatoires. Commencez par une boucle serrée qui vous dit
quel sous‑système est coupable : capacité du pool, rétention de snapshots ZFS, churn du cache Docker, ou latence pure I/O.
Premier point : capacité et « espace pris en otage »
- Capacité du pool :
zpool list. Si la CAP > ~85 %, attendez‑vous à des problèmes. - Où est l’espace :
zfs list -o used,usedbysnapshots,usedbychildren -r tank/var/lib/docker. - Snapshots :
zfs list -t snapshot | grep docker. Si des snapshots existent sur les datasets Docker, c’est suspect.
Interprétation : si les snapshots dominent, prunez les snapshots. Si les enfants dominent, prunez images/containers ou envisagez de réinitialiser le dataset Docker.
Second point : type de goulot (latence vs CPU vs mémoire)
- Latence disque :
iostat -x 1 3et surveillezawait,%util,%iowait. - Croissance ARC : vérifiez
/proc/spl/kstat/zfs/arcstatset la mémoire système. - Steal CPU / contention : si virtualisé, vérifiez
%stealdans la sortie deiostat.
Interprétation : la pression ARC et la latence I/O se déguisent souvent en « Docker est lent ». Ce ne sont pas les mêmes correctifs.
Troisième point : source du churn Docker
- Explosion du cache de build :
docker system dfet stratégiedocker builder prune. - Rétention d’images : listez les anciennes images et tags ; appliquez un TTL dans le CI et les registries.
- Tendance du nombre de datasets : nombre de datasets dans
tank/var/lib/docker/zfssemaine après semaine.
Interprétation : si le nombre de datasets croît sans relâche, votre charge de build/pull est effectivement une usine de couches. Contenez‑la avec des quotas et de l’isolation.
Trois mini-histoires d’entreprise (comment les équipes se plantent)
Incident : la mauvaise hypothèse (« Docker prune libère de l’espace »)
Une entreprise de taille moyenne exécutait son CI sur un hôte Docker robuste et ZFS‑backed. Ils avaient un job nocturne : prune des images,
prune des containers, prune du build cache. Il passait vert et les logs semblaient responsables. Pendant ce temps, le pool est monté
de 60 % à 90 % en un mois puis s’est effondré durant une semaine de releases chargée.
L’astreint a fait le rituel : lancé les prune manuellement, redémarré Docker, même rebooté l’hôte. Rien n’a bougé.
docker system df prétendait qu’il y avait beaucoup à récupérer. ZFS n’était pas d’accord. zpool list indiquait 94 % plein,
et la latence I/O a explosé parce que ZFS allouait à partir des mauvais segments restants.
La mauvaise hypothèse était subtile : ils supposaient que la notion Docker de « unused » correspondait à la capacité de ZFS à libérer des blocs.
Mais l’hôte exécutait aussi une politique générique de snapshots sur tank/var, qui incluait /var/lib/docker.
Chaque nuit, ils prenaient des snapshots d’un dataset plein de clones et de churn. Autrement dit, les couches « supprimées » étaient toujours
référencées par des snapshots, donc l’espace était bloqué.
La correction n’a pas été héroïque. Ils ont exclu le dataset Docker de la politique de snapshots, détruit les anciens snapshots,
et déplacé les données persistantes hors du dataset Docker pour pouvoir nettoyer l’état Docker si besoin.
Après cela, les prune ont recommencé à fonctionner parce que ZFS a enfin pu réellement libérer les blocs.
Optimisation qui a mal tourné (« Activons dedup pour les images »)
Une autre équipe avait une bonne intuition : les images conteneur partagent beaucoup de fichiers identiques. Pourquoi ne pas activer la dédup ZFS
sur le dataset Docker et économiser de l’espace ? Ils l’ont piloté sur un nœud et ont célébré les chiffres initiaux.
L’espace utilisé a chuté. Des high‑fives ont été échangés dans une salle de réunion encore couverte des KPI du trimestre précédent.
Puis le nœud a commencé à trébucher sous charge. Les builds sont devenus erratiques. Des pics de latence sont apparus pendant le trafic de pointe,
pas seulement pendant le CI. L’équipe a ajouté du CPU. Ils ont ajouté des disques plus rapides. Ils effectuaient la danse rituelle du debug perf
pendant que le vrai problème restait silencieux.
La dédup augmente énormément les recherches de métadonnées et exige beaucoup de RAM pour la DDT (dedup table). Le nœud effectuait maintenant
du travail supplémentaire sur chaque chemin d’écriture et de lecture, surtout avec le churn des créations/suppressions de couches.
Pire, les défaillances étaient intermittentes, car dépendant des taux de hit du cache et de la working set de la DDT.
Le rollback a été douloureux car désactiver la dédup ne « dédupe » pas rétroactivement les blocs existants ; cela empêche seulement la dédup sur les nouvelles écritures.
Ils ont finalement migré l’état Docker vers un dataset neuf avec dedup désactivé, et ont gardé la compression.
Ils ont obtenu la plupart des économies d’espace nécessaires grâce à lz4 et une rétention sensée, sans la charge opérationnelle.
Pratique ennuyeuse mais correcte qui a sauvé la situation (datasets séparés + quotas)
Une équipe de plateforme paiements exécutait Docker sur ZFS avec une disposition presque trop propre : Docker vivait dans un dataset
avec un quota ferme. Chaque service stateful avait son propre dataset sous /containers avec refquota et un simple planning de snapshots.
La cible de backup était receive‑only. Rien de sophistiqué. Aucun script malin. Aucune « initiative d’optimisation de stockage ».
Un après‑midi, une mauvaise configuration CI a déclenché une boucle : un pipeline de build tirait des images de base à répétition et créait de nouveaux tags à chaque exécution.
Sur la plupart des systèmes, cela aurait simplement bouffé le disque jusqu’à ce que l’hôte tombe. Ici, le dataset Docker a atteint son quota et Docker a commencé
à échouer lors des pulls. Bruyamment. Le nœud est resté vivant. Les bases de données ont continué de fonctionner.
L’astreint a eu une alerte sur des builds échoués, pas un hôte de production mort. Ils ont corrigé la config CI, puis nettoyé le dataset Docker. Pas de restauration de données.
Pas d’achat d’urgence de capacité. Le quota n’a pas empêché l’erreur ; il a empêché l’erreur de devenir un incident.
C’est le genre de pratique qui n’a pas de post de célébration. Elle devrait en avoir. Des frontières de stockage ennuyeuses sont ce qui transforme
« oups » en « ticket », au lieu de « oups » en « incident ».
Erreurs courantes : symptôme → cause → correctif
1) « Docker prune a été exécuté, mais l’espace ZFS n’est pas revenu »
Symptôme : Docker rapporte de l’espace récupérable ; used ZFS reste élevé.
Cause : snapshots sur les datasets Docker maintenant des blocs référencés ; ou chaînes de clones gardant des blocs vivants.
Correctif : arrêtez de snapshotter les datasets gérés par le driver Docker ; détruisez les snapshots ; envisagez de réinitialiser tank/var/lib/docker après avoir déplacé les données persistantes.
2) « L’hôte a des milliers de montages et le boot est lent »
Symptôme : temps de boot longs, unités de montage systemd prennent des ages, Docker démarre tard ou échoue.
Cause : le driver ZFS de Docker a produit un grand nombre de datasets ; la gestion des montages devient coûteuse.
Correctif : bornez le dataset avec un quota ; réduisez le churn d’images ; reconstruisez périodiquement l’état Docker sur les nœuds CI ; séparez les nœuds de build des nœuds prod longue durée.
3) « Les conteneurs ralentissent aléatoirement ; le CPU n’est pas saturé »
Symptôme : pics de latence, timeouts, performance de build incohérente.
Cause : pool presque plein, fragmentation et ralentissements d’allocation ; ou ARC affamant les applications.
Correctif : gardez le pool sous ~80–85 % ; capping ARC ; ajoutez des vdevs (pas des disques plus gros en place si vous voulez une amélioration réelle de performance).
4) « ZFS usedbysnapshots est énorme sous /var/lib/docker »
Symptôme : l’espace des snapshots domine les chiffres d’utilisation.
Cause : politique générique de snapshots appliquée au dataset Docker ; Docker utilise déjà son propre modèle snapshot/clone en interne.
Correctif : excluez le dataset Docker des plannings de snapshots génériques ; snapshottez /containers/<app> à la place.
5) « Nous avons tune recordsize pour Docker et la base s’est dégradée »
Symptôme : latence DB augmentée après le « tuning du stockage conteneur ».
Cause : les données DB sont stockées à l’intérieur du dataset Docker ou dans le chemin géré par le driver ; recordsize choisi pour le churn des couches, pas pour les patterns DB.
Correctif : mettez la DB sur son propre dataset ; tunez recordsize et logbias là‑bas ; gardez le dataset Docker optimisé pour Docker.
6) « La réplication est bordélique et les restaurations font peur »
Symptôme : les backups incluent couches Docker, caches et état, rendant send/receive énormes et lents.
Cause : données persistantes mélangées avec l’état Docker sous un même arbre de dataset.
Correctif : séparez les datasets persistants sous /containers ; répliquez ceux‑ci. Traitez le dataset Docker comme cache/état, pas comme matière à sauvegarder.
7) « Nous avons activé dedup et maintenant tout est imprévisible »
Symptôme : variance de performance, pression mémoire, pics de latence étranges.
Cause : working set de la DDT trop gros ; surcharge métadonnées pour des couches à fort churn.
Correctif : n’utilisez pas dedup pour les stores de couches Docker. Utilisez la compression et des politiques de rétention ; si dedup est déjà activé, migrez vers un nouveau dataset.
Listes de contrôle / plan pas à pas
Plan A : Nouvel hôte (installation propre)
- Créez le pool avec un ashift et un design de vdevs sensés pour votre matériel (miroir/RAIDZ selon votre modèle de panne).
- Créez des datasets :
tank/var/lib/dockermonté sur/var/lib/dockertank/containersmonté sur/containers- Datasets par appli optionnels :
tank/containers/<app>
- Définissez les propriétés du dataset Docker :
compression=lz4,atime=off,xattr=sa, et envisagezrecordsize=16Kou32K. - Définissez un quota sur
tank/var/lib/dockerdimensionné pour votre churn attendu. - Pour chaque service stateful, créez un dataset sous
/containerset définissez unrefquota. - Configurez Docker pour utiliser le driver ZFS et le bon zpool/dataset (via la config du daemon), puis démarrez Docker.
- Excluez les datasets Docker de toute automatisation générique de snapshots ; snapshottez seulement les datasets persistants.
- Définissez la réplication du sous‑arbre
tank/containersvers un pool de backup receive‑only.
Plan B : Hôte existant (migration sans drame)
- Inventoriez ce qui est persistant :
- Listez les stacks compose et leurs volumes.
- Identifiez quels volumes sont réellement des bases de données ou services stateful.
- Créez des datasets
/containerspar appli et déplacez les données (rsync ou migration au niveau applicatif). - Mettez à jour les manifests compose/k8s pour bind‑monter les chemins hôtes depuis
/containers/<app>. - Ce n’est qu’après avoir extrait les données persistantes : appliquez un quota au dataset Docker.
- Audit des snapshots : si vous avez des snapshots des datasets Docker, supprimez‑les soigneusement après vérification qu’ils ne servent pas à des rollbacks requis.
- Définissez les propriétés sur le dataset Docker et redémarrez Docker dans une fenêtre contrôlée.
- Si l’arbre de datasets est déjà pathologique (dizaines de milliers), envisagez de reconstruire l’état Docker :
- Stopper Docker
- Détruire et recréer
tank/var/lib/docker - Démarrer Docker et re‑puller les images
Plan C : Nœuds CI (traitez‑les comme du bétail)
- Mettez l’état Docker CI sur son propre dataset avec un quota strict.
- Ne snapshottez pas les datasets Docker CI.
- Planifiez un nettoyage agressif du build cache.
- Reconstruisez périodiquement les nœuds CI plutôt que d’essayer de « les garder propres pour toujours ».
- Conservez les artéfacts dans un store externe ; gardez Docker comme cache.
FAQ
1) Dois‑je utiliser le driver ZFS de Docker ou overlay2 sur ZFS ?
Si vous avez déjà ZFS et que vous voulez des snapshots/clones ZFS natifs pour les couches, utilisez le driver ZFS. Si vous voulez la voie mainstream
et des opérations day‑2 plus simples, overlay2 sur ZFS peut être acceptable—mais vous perdez certaines sémantiques ZFS‑natifs et pouvez rencontrer des interactions étranges.
Dans les deux cas, gardez les données persistantes dans leurs propres datasets.
2) Puis‑je snapshotter /var/lib/docker pour les backups ?
Vous pouvez. Vous ne devriez pas. L’état Docker est reconstruisible ; vos bases de données ne le sont pas. Snapshottez et répliquez /containers/<app>.
Traitez les images et couches Docker comme cache et matériel de reconstruction.
3) Pourquoi le nombre de datasets compte‑t‑il autant ?
Chaque dataset a des métadonnées et peut impliquer la gestion de montages. Des milliers peuvent être corrects ; des dizaines de milliers deviennent une friction opérationnelle :
listing lent, destroy lent, montage/démontage lent, et un rayon d’explosion plus grand pour les erreurs.
4) Quelles propriétés sont les plus importantes pour les datasets Docker ?
compression=lz4, atime=off, xattr=sa sont les gains habituels. recordsize dépend de la charge ; 16K–32K se comporte souvent mieux pour le churn que 128K.
5) Dois‑je mettre les volumes Docker sous le dataset Docker ?
Pour les volumes éphémères, c’est acceptable. Pour les workloads stateful, non. Utilisez des bind mounts host‑path soutenus par des datasets sous /containers.
C’est ainsi que les backups et les quotas deviennent gérables.
6) L’ajout d’un SLOG est‑il utile pour Docker ?
Seulement si vous avez des workloads sync‑intensifs sur des datasets avec sync=standard et que vos applications effectuent vraiment des écritures sync.
Beaucoup de workloads conteneur ne sont pas sync‑bound. Testez avec des métriques ; n’achetez pas un SLOG par superstition.
7) Pourquoi je vois un grand usedbysnapshots même sans snapshots manuels ?
Les outils de snapshots hôtes ciblent souvent des arbres entiers (comme tank/var). Ou un produit de backup prend des snapshots récursifs.
Docker utilise aussi des snapshots ZFS en interne, mais ceux‑ci sont généralement gérés sous l’arbre du driver Docker.
La correction est de cibler précisément votre automatisation de snapshots.
8) Puis‑je « défragmenter » un pool ZFS pour améliorer la performance ?
Pas au sens classique d’un système de fichiers. Le correctif pratique est la discipline de capacité (ne pas le faire chauffer), un bon design de vdev,
et parfois réécrire les données en migrant les datasets (send/receive) vers un pool neuf.
9) Quelle est la façon la plus sûre de réinitialiser l’état Docker sur un hôte ZFS ?
Arrêtez Docker, assurez‑vous qu’aucune donnée persistante n’est dans /var/lib/docker, puis détruisez et recréez le dataset Docker.
C’est pour cela que nous séparons /containers—ainsi ce geste est sûr quand nécessaire.
10) Comment empêcher une appli runaway de remplir le pool ?
Mettez chaque appli stateful dans son propre dataset et définissez un refquota. Mettez l’état Docker sous quota.
Puis alertez sur l’utilisation des quotas avant d’atteindre le mur.
Conclusion : prochaines étapes réalisables aujourd’hui
Si vous retenez une chose : l’état Docker n’est pas précieux, et votre disposition ZFS doit le refléter. Donnez à Docker son propre dataset,
limitez‑le par un quota, et arrêtez de le snapshotter comme un album photo familial. Placez les données persistantes dans leurs propres datasets sous
/containers, avec refquotas et un plan de réplication que vous pouvez expliquer à un collègue fatigué à 3 h du matin.
Prochaines étapes pratiques :
- Exécutez
findmntet confirmez que/var/lib/dockerest un dataset dédié. - Exécutez
zfs list -o usedbydataset,usedbysnapshots,usedbychildrenet apprenez ce qui retient réellement l’espace. - Excluez les datasets Docker de l’automatisation de snapshots.
- Créez des datasets
/containerspour les services stateful et déplacez les données là‑bas. - Définissez des quotas/refquotas pour que les erreurs échouent petites et bruyantes.
L’explosion de couches ne s’arrête pas parce que vous l’avez demandé gentiment. Elle s’arrête parce que vous avez tracé une frontière et que vous l’avez appliquée.