Docker sur cgroups v2 : douleur, erreurs et chemin de correction

Cet article vous a aidé ?

Vous mettez à jour une distribution. Docker fonctionnait. Maintenant non. Ou pire : il tourne, mais les limites de ressources ne s’appliquent pas, les CPU se comportent bizarrement, et votre conteneur « limité à 2G » dévore l’hôte. Vous regardez docker run comme s’il vous avait trahi personnellement.

cgroups v2 est une meilleure ingénierie. C’est aussi une taxe de compatibilité. Voici le guide de terrain des erreurs exactes que vous verrez, ce qu’elles signifient réellement, et un chemin de correction qui n’implique pas d’invoquer /sys/fs/cgroup.

Ce qui a changé avec cgroups v2 (et pourquoi Docker le ressent)

Les control groups (« cgroups ») sont le cadre du noyau pour la comptabilité et le bridage : CPU, mémoire, IO, pids, et compagnons. Docker les utilise pour implémenter des options comme --memory, --cpus, --pids-limit, et pour garder les processus de conteneur dans un arbre ordonné.

cgroups v1 et v2 ne sont pas simplement des versions différentes. Ce sont des modèles différents.

cgroups v1 : plusieurs hiérarchies, nombreuses embûches

v1 permet à chaque contrôleur (cpu, memory, blkio, etc.) d’être monté séparément. Cette flexibilité a créé un passe-temps favori des distributions Linux : monter différents contrôleurs à différents endroits puis regarder les outils faire des hypothèses. Docker a grandi ici. Beaucoup de scripts à moitié fonctionnels aussi.

cgroups v2 : une hiérarchie unique, règles cohérentes, sémantique différente

v2 est une « hiérarchie unifiée » : un seul arbre, contrôleurs activables par sous-arbre, et règles sur la délégation. C’est plus cohérent. Cela signifie aussi que les logiciels doivent être explicites sur comment ils créent et gèrent les cgroups, et si systemd est en charge.

Où Docker trébuche

  • Incompatibilité de driver : Docker peut gérer les cgroups lui-même (driver cgroupfs) ou déléguer à systemd (driver systemd). Avec v2, systemd comme gestionnaire est le chemin privilégié sur la plupart des distributions systemd.
  • containerd/runc ancien : le support initial de v2 était partiel ; les versions anciennes échouent avec « unsupported » ou ignorent silencieusement les limites.
  • Contraintes rootless : la délégation en v2 est plus stricte. Docker rootless peut fonctionner, mais il dépend des services utilisateur systemd et d’une délégation correcte.
  • Modes hybrides : certains systèmes exécutent une configuration « mixte » v1/v2. C’est la recette pour « ça marche sur cet hôte mais pas sur l’autre ».

Idée paraphrasée de John Allspaw (reliability engineering) : « Blâmer ne résout que rarement les incidents ; comprendre les systèmes le fait. » C’est l’attitude nécessaire avec cgroups v2 : arrêtez de vous battre contre le symptôme, cartographiez le système.

Faits & brève histoire qui vous feront gagner du temps

Voici le genre de petites vérités concrètes qui vous empêchent de déboguer la mauvaise couche.

  1. Les cgroups ont été intégrés au noyau Linux en 2007 (dans le cadre de l’effort « process containers »). Le modèle original supposait beaucoup de hiérarchies indépendantes.
  2. cgroups v2 a commencé à apparaître vers 2016 pour corriger la fragmentation et les bogues sémantiques de v1, notamment autour de la mémoire et de la délégation.
  3. systemd a adopté profondément cgroups et est devenu le gestionnaire de cgroup par défaut sur de nombreuses distributions ; Docker a initialement résisté, puis a convergé vers le driver systemd comme choix sensé pour les configurations modernes.
  4. v2 change le comportement mémoire : la comptabilité et l’application des limites sont plus propres, mais il faut comprendre memory.max, memory.high et les signaux de pression. Les vieilles habitudes de « oom-kill comme contrôle de flux » sont exposées.
  5. Le contrôle IO a changé de nom et de sens : les blkio.* de v1 deviennent io.* en v2, et certains conseils d’optimisation anciens ne s’appliquent plus.
  6. v2 exige l’activation explicite des contrôleurs : on peut avoir un arbre cgroup où les contrôleurs existent mais ne sont pas activés pour un sous-arbre, produisant un comportement « fichier introuvable » qui ressemble à un problème de permissions.
  7. La délégation est volontairement stricte : v2 empêche les processus non privilégiés de créer des sous-arbres arbitraires sauf si le parent est configuré pour la délégation. C’est pourquoi les configurations rootless échouent de nouvelles façons.
  8. Certaines options Docker dépendent de la configuration du noyau : même avec v2, des fonctionnalités noyau manquantes produisent des erreurs confuses « not supported » qui ressemblent à des bogues Docker.

Les erreurs que vous verrez en situation réelle

Les échecs liés à cgroups v2 se regroupent en trois catégories : démarrage du démon, démarrage de conteneur, et « ça tourne mais les limites mentent ». Voici les classiques, avec ce qu’ils signifient généralement.

Démon qui ne démarre pas

  • failed to mount cgroup ou cgroup mountpoint does not exist : Docker attend une disposition v1 mais l’hôte est en v2 unifié, ou les montages sont absents/verrouillés.
  • cgroup2: unknown option lors du montage : noyau trop ancien ou options de montage incompatibles avec ce noyau.
  • OCI runtime create failed: ... cgroup ... pendant l’initialisation du démon : incompatibilité runc/containerd avec le mode cgroup de l’hôte.

Le conteneur ne démarre pas (erreurs OCI)

  • OCI runtime create failed: unable to apply cgroup configuration: ... no such file or directory : fichiers de contrôleur non disponibles dans le cgroup cible, souvent parce que les contrôleurs ne sont pas activés dans ce sous-arbre.
  • permission denied en écrivant dans /sys/fs/cgroup/... : problème de délégation (rootless ou gestionnaire cgroup imbriqué) ou systemd possède le sous-arbre alors que Docker tente cgroupfs.
  • cannot set memory limit: ... invalid argument : utilisation de paramètres à l’ère v1 ou noyau qui ne supporte pas l’option ; cela arrive aussi quand les limites de swap sont mal configurées.

Les conteneurs tournent mais les limites ne s’appliquent pas

  • docker stats montre une mémoire illimitée : Docker ne lit pas la limite mémoire depuis le chemin cgroup v2 attendu ; incompatibilité de driver ou Docker ancien.
  • Quotas CPU ignorés : le contrôleur cpu n’est pas activé pour ce sous-arbre, ou vous utilisez une configuration de driver cgroup qui n’attache jamais les tâches là où vous le pensez.
  • Le bridage IO ne fonctionne pas : vous êtes sur v2 et tentez encore d’ajuster blkio ; ou le pilote du périphérique bloc ne supporte pas la politique demandée.

Blague #1 : cgroups v2, c’est comme l’âge adulte : plus de structure, moins de failles, et tout ce que vous faisiez « parce que ça marchait » est désormais illégal.

Playbook de diagnostic rapide (premier/deuxième/troisième)

Si vous n’avez que cinq minutes avant la fin de votre fenêtre de maintenance, faites ceci dans l’ordre.

Premier : confirmez quel mode cgroup l’hôte exécute réellement

Ne déduisez pas à partir de la version de la distro. Vérifiez le système de fichiers et les flags du noyau.

  • Est-ce que /sys/fs/cgroup est de type cgroup2 (unifié) ou plusieurs montages v1 ?
  • Le système est-il en mode hybride ?

Second : vérifiez le driver cgroup de Docker et les versions des runtimes

La plupart de la « douleur cgroups v2 » vient soit d’un mauvais driver soit d’un runtime ancien.

  • Docker utilise-t-il systemd ou cgroupfs ?
  • containerd et runc sont-ils assez récents pour votre noyau/distro ?

Troisième : vérifiez la disponibilité des contrôleurs et la délégation

Si une limite spécifique échoue (mémoire, cpu, pids), confirmez que le contrôleur est activé là où Docker place les conteneurs.

  • cgroup.controllers existe et liste le contrôleur.
  • cgroup.subtree_control l’inclut pour le cgroup parent.
  • Permissions et propriété ont du sens (surtout en rootless).

Tâches pratiques : commandes, sens des sorties et décisions

Ce sont les tâches « arrêtez de deviner ». Chacune contient : commande, ce que la sortie signifie, et la décision suivante.

Tâche 1 : Identifier le type du système de fichiers cgroup (v2 vs v1)

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

Sens : cgroup2fs signifie cgroups v2 unifié. Si vous voyez tmpfs ici et des montages séparés pour les contrôleurs, vous êtes probablement en v1/hybride.

Décision : Si c’est v2, planifiez Docker avec le driver systemd (ou au minimum vérifiez la compatibilité). Si c’est v1/hybride, décidez de migrer ou de forcer le mode legacy pour la cohérence.

Tâche 2 : Confirmer ce qui est monté sous /sys/fs/cgroup

cr0x@server:~$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

Sens : Un seul montage cgroup2 : mode unifié. Si vous voyez plusieurs lignes comme cgroup on /sys/fs/cgroup/memory, c’est v1.

Décision : Si unifié, arrêtez d’essayer de déboguer des chemins v1 comme /sys/fs/cgroup/memory. Ils n’existent pas.

Tâche 3 : Vérifier les paramètres de boot du noyau affectant les cgroups

cr0x@server:~$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-6.5.0 root=/dev/mapper/vg0-root ro quiet systemd.unified_cgroup_hierarchy=1

Sens : systemd.unified_cgroup_hierarchy=1 force v2 unifié. Certains systèmes utilisent l’inverse (ou les valeurs par défaut de la distro) pour forcer v1.

Décision : Si votre organisation veut un comportement prévisible, standardisez ce flag à travers votre flotte (soit v2 partout, soit v1 partout). Les flottes mixtes sont l’endroit où la joie des on-call meurt.

Tâche 4 : Demandez à systemd ce qu’il pense des cgroups

cr0x@server:~$ systemd-analyze --version
systemd 253 (253.5-1)
+PAM +AUDIT +SELINUX +APPARMOR +IMA +SMACK +SECCOMP +GCRYPT -GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT +QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK -XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified

Sens : default-hierarchy=unified signifie que systemd attend cgroups v2.

Décision : Si systemd est unifié, alignez Docker avec le driver cgroup systemd sauf si vous avez une raison impérieuse de ne pas le faire.

Tâche 5 : Inspecter le driver cgroup et la version cgroup de Docker

cr0x@server:~$ docker info --format '{{json .CgroupVersion}} {{json .CgroupDriver}}'
"2" "systemd"

Sens : Docker voit cgroups v2 et utilise le driver systemd. C’est la combinaison stable sur les distributions systemd modernes.

Décision : Si vous obtenez "2" "cgroupfs" sur un hôte systemd, envisagez de passer au driver systemd pour éviter les surprises de délégation et de subtree-control.

Tâche 6 : Vérifier les versions des composants runtime (containerd, runc)

cr0x@server:~$ docker info | egrep -i 'containerd|runc|cgroup'
 Cgroup Driver: systemd
 Cgroup Version: 2
 containerd version: 1.7.2
 runc version: 1.1.7

Sens : Les combinaisons containerd/runc anciennes sont les endroits où le support v2 devient « créatif ». Les versions modernes sont moins excitantes.

Décision : Si vous utilisez un package Docker Engine ancien verrouillé pour la « stabilité », désenclavez-le — car maintenant il est instable. Mettez à jour l’ensemble engine/runc/containerd comme une unité quand c’est possible.

Tâche 7 : Valider que les contrôleurs existent (à l’échelle de l’hôte)

cr0x@server:~$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc

Sens : Voici les contrôleurs supportés par le noyau et exposés dans la hiérarchie unifiée.

Décision : Si un contrôleur dont vous avez besoin (comme memory ou io) est absent, vous n’êtes pas face à un « problème de config Docker ». Vous êtes face à un problème de noyau/fonctionnalité.

Tâche 8 : Vérifier que les contrôleurs sont activés pour le sous-arbre que Docker utilisera

cr0x@server:~$ cat /sys/fs/cgroup/cgroup.subtree_control
+cpu +io +memory +pids

Sens : La liste préfixée par des plus indique quels contrôleurs sont activés pour les cgroups enfants à ce niveau. Si +memory n’est pas présent, les cgroups enfants n’auront pas memory.max et compagnons.

Décision : Si les contrôleurs ne sont pas activés, corrigez la configuration du cgroup parent (souvent via les settings d’unité systemd) ou déplacez le placement de Docker vers un sous-arbre correctement délégué (le driver systemd aide).

Tâche 9 : Vérifier où Docker place un conteneur dans l’arbre cgroup

cr0x@server:~$ docker run -d --name cgtest --memory 256m --cpus 0.5 busybox:latest sleep 100000
b7d32b3f6b1f3c3b2c0b9c9f8a7a6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9

cr0x@server:~$ docker inspect --format '{{.State.Pid}}' cgtest
22145

cr0x@server:~$ cat /proc/22145/cgroup
0::/system.slice/docker-b7d32b3f6b1f3c3b2c0b9c9f8a7a6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9.scope

Sens : Sur v2 il y a une seule entrée unifiée (0::). Le conteneur est dans une unité scope systemd sous system.slice, ce qui est souhaitable quand on utilise le driver cgroup systemd.

Décision : Si le conteneur atterrit quelque part d’attendu (ou dans un cgroup créé par Docker en dehors de l’arbre systemd), réconciliez le driver cgroup de Docker et la configuration systemd.

Tâche 10 : Confirmer que la limite mémoire est réellement appliquée (fichiers v2)

cr0x@server:~$ CGPATH=/sys/fs/cgroup/system.slice/docker-b7d32b3f6b1f3c3b2c0b9c9f8a7a6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9.scope
cr0x@server:~$ cat $CGPATH/memory.max
268435456

cr0x@server:~$ cat $CGPATH/memory.current
1904640

Sens : memory.max est en octets. 268435456 correspond à 256 MiB. memory.current affiche l’usage actuel.

Décision : Si memory.max vaut max (illimité) malgré les options Docker, vous avez un décalage de placement/driver cgroup ou un bug runtime. Ne touchez pas à l’application ; corrigez le câblage cgroup.

Tâche 11 : Confirmer que le quota CPU est appliqué (sémantique v2)

cr0x@server:~$ cat $CGPATH/cpu.max
50000 100000

Sens : En v2, cpu.max est « quota période ». Ici : quota 50ms par période 100ms = 0.5 CPU.

Décision : Si vous voyez max 100000, le quota n’est pas appliqué. Vérifiez si le contrôleur cpu est activé et si Docker a effectivement respecté --cpus dans votre version.

Tâche 12 : Vérifier la limite pids (un cas fréquent « marche jusqu’à un certain point »)

cr0x@server:~$ cat $CGPATH/pids.max
1024

cr0x@server:~$ cat $CGPATH/pids.current
3

Sens : Le contrôle pids fonctionne. Sans cela, les fork-bombs deviennent des « tests de charge inattendus ».

Décision : Si pids.max est absent, les contrôleurs ne sont pas activés pour le sous-arbre. Corrigez la délégation/activation, pas les options Docker.

Tâche 13 : Chercher la preuve irréfutable dans journald

cr0x@server:~$ journalctl -u docker -b --no-pager | tail -n 20
Jan 03 08:12:11 server dockerd[1190]: time="2026-01-03T08:12:11.332111223Z" level=error msg="failed to create shim task" error="OCI runtime create failed: unable to apply cgroup configuration: mkdir /sys/fs/cgroup/system.slice/docker.service/docker/xyz: permission denied: unknown"
Jan 03 08:12:11 server dockerd[1190]: time="2026-01-03T08:12:11.332188001Z" level=error msg="failed to start daemon" error="... permission denied ..."

Sens : Docker tente de créer des cgroups à un emplacement que systemd n’autorise pas (ou le processus n’a pas les droits de délégation). Le chemin est l’indice.

Décision : Passez au driver cgroup systemd, ou corrigez les règles de délégation si rootless/niché, au lieu de faire chmod sur des fichiers sysfs au hasard (ce n’est pas une correction, c’est une confession).

Tâche 14 : Confirmer que systemd est le gestionnaire cgroup pour Docker

cr0x@server:~$ systemctl show docker --property=Delegate,Slice,ControlGroup
Delegate=yes
Slice=system.slice
ControlGroup=/system.slice/docker.service

Sens : Delegate=yes est crucial : cela indique à systemd d’autoriser le service à créer/gérer des sous-cgroups. Sans cela, la délégation cgroups v2 casse de façons qui ressemblent à des erreurs de permission aléatoires.

Décision : Si Delegate=no, corrigez le drop-in d’unité (ou utilisez l’unité packagée qui le définit correctement). C’est souvent tout le problème.

Tâche 15 : Détecter le mode rootless (règles différentes, douleur différente)

cr0x@server:~$ docker info --format 'rootless={{.SecurityOptions}}'
rootless=[name=seccomp,profile=default name=rootless]

Sens : Rootless change les cgroups que vous pouvez toucher, et comment la délégation doit être configurée dans les sessions utilisateur.

Décision : Si rootless est activé et que les écritures cgroup échouent, arrêtez d’essayer de « réparer » ça dans /sys/fs/cgroup en root. Configurez correctement les services utilisateur systemd et la délégation, ou acceptez des limites fonctionnelles.

Le chemin de correction (choisissez votre route, ne devinez pas)

Il n’y a que quelques états finaux stables. Choisissez-en un délibérément. « Ça marche sur mon laptop » n’est pas une architecture.

Route A (recommandée sur les distributions systemd) : cgroups v2 + driver systemd pour Docker

C’est l’option la plus propre sur les systèmes modernes Ubuntu/Debian/Fedora/RHEL-ish où systemd est PID 1 et où la hiérarchie unifiée est la valeur par défaut.

1) Assurer que Docker est configuré pour le driver cgroup systemd

Vérifiez d’abord l’état actuel (Tâche 5). S’il n’est pas systemd, définissez-le.

cr0x@server:~$ sudo mkdir -p /etc/docker
cr0x@server:~$ sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "journald"
}
EOF

Ce que cela signifie : Cela indique à dockerd de créer les conteneurs sous des cgroups gérés par systemd plutôt que de gérer sa propre hiérarchie cgroupfs.

Décision : Si vous exécutez Kubernetes, assurez-vous que kubelet utilise le même driver. Un décalage est un classique « le nœud semble OK jusqu’à ce que non ».

2) Redémarrer Docker et vérifier

cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ docker info | egrep -i 'Cgroup Driver|Cgroup Version'
 Cgroup Driver: systemd
 Cgroup Version: 2

Ce que cela signifie : Vous êtes aligné : systemd + v2 + driver Docker.

Décision : Si Docker ne démarre pas, vérifiez immédiatement journald (Tâche 13). Ne « rebootez » pas sans diagnostic. Les reboots transforment un problème reproductible en folklore.

3) Confirmer que la délégation est correcte sur l’unité docker.service

La plupart des unités packagées sont correctes. Les unités personnalisées souvent ne le sont pas.

cr0x@server:~$ systemctl show docker --property=Delegate
Delegate=yes

Décision : Si Delegate=no, ajoutez un drop-in :

cr0x@server:~$ sudo systemctl edit docker <<'EOF'
[Service]
Delegate=yes
EOF

Route B : forcer cgroups v1 (mode legacy) pour gagner du temps

Ceci est une retraite tactique. Elle peut être appropriée quand vous avez des agents tiers, des noyaux anciens, ou des appliances vendor qui ne gèrent pas encore v2. Mais traitez cela comme de la dette avec intérêts.

1) Basculer systemd vers la hiérarchie legacy via la ligne de commande du noyau

Le mécanisme exact dépend de la distro/bootloader. Le principe : définir un paramètre noyau pour désactiver la hiérarchie unifiée.

cr0x@server:~$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-6.5.0 root=/dev/mapper/vg0-root ro quiet systemd.unified_cgroup_hierarchy=0

Sens : Vous forcez le comportement v1/hybride.

Décision : Si vous faites cela, documentez-le et faites-en une norme de flotte. Une flotte à moitié v2 est la source de « seul cet AZ est cassé ».

Route C : Docker rootless sur cgroups v2 (fonctionne, mais ne le romanticisez pas)

Rootless est excellent pour les postes développeur et certains scénarios multi-tenant. Pour la production, c’est acceptable si vous acceptez les contraintes et les testez. cgroups v2 rend en réalité rootless plus cohérent, mais seulement avec une délégation utilisateur systemd correcte.

Règles typiques :

  • Utiliser les services utilisateur systemd quand c’est possible.
  • Attendre que certains contrôles de ressources se comportent différemment qu’en mode rootful.
  • Accepter que posture de sécurité et opérabilité fassent des compromis.

Blague #2 : Docker rootless avec cgroups v2 est tout à fait réalisable, comme faire un expresso en pédalant — ne l’essayez pas pour la première fois pendant un incident.

Route D : containerd directement (quand l’ergonomie Docker n’en vaut pas la peine)

Certaines organisations migrent vers containerd + nerdctl (ou orchestration) pour réduire les couches. Si vous êtes déjà profondément dans Kubernetes, la valeur de Docker est surtout UX pour les développeurs. En production, moins de couches peut signifier moins de surprises cgroup.

Mais le chemin de correction est le même : faire de systemd le gestionnaire cgroup, garder containerd/runc à jour, et valider l’activation des contrôleurs.

Trois mini-histoires d’entreprise (horriblement plausibles)

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

L’entreprise a migré un gros lot de workloads d’un LTS ancien vers un plus récent. Le ticket de changement indiquait « mise à jour Docker + correctifs sécurité ». L’hypothèse était simple : « mêmes conteneurs, mêmes limites ».

Après la mise à jour, Docker a démarré. Les jobs ont démarré. Tout semblait vert. Puis les hôtes ont commencé à swapper, la latence a grimpé, et des services non liés ont commencé à timeout-er. L’on-call a fait ce que fait l’on-call : redémarré des conteneurs, drainé des nœuds, et accusé les « noisy neighbors ». Ça s’est empiré.

L’hypothèse erronée était que --memory était appliqué. Sur le nouvel OS, l’hôte était sur cgroups v2. Docker utilisait encore le driver cgroupfs parce qu’une image dorée ancienne avait un daemon.json persistant depuis des années. Les conteneurs étaient placés dans un sous-arbre sans le contrôleur memory activé. Le noyau n’ignorait pas les limites par malice ; il n’avait littéralement aucun endroit où les appliquer.

La correction n’était pas d’ajuster les jobs. C’était d’aligner Docker sur le driver systemd, de vérifier Delegate=yes, puis de prouver l’application en lisant memory.max dans le cgroup scope du conteneur. L’action post-mortem n’était pas « surveiller la mémoire davantage ». C’était « standardiser le mode cgroup et le driver au niveau de la flotte ».

Mini-histoire 2 : L’optimisation qui a dérapé

Une équipe plateforme a tenté de réduire le bruit de throttling CPU en « lissant les quotas ». Ils ont déplacé beaucoup de services de quotas stricts vers des shares, pensant diminuer la contention et augmenter le débit global. Ils ont aussi activé un nouvel ensemble de paramètres noyau dans le cadre du passage à la hiérarchie unifiée cgroups v2.

Au début, ça semblait bien : moins d’alertes de throttling, meilleure latence médiane. Puis un service batch lourd a déployé une nouvelle version et a commencé à faire des rafales périodiques. Les rafales étaient légales sous l’ordonnancement par shares, mais elles ont volé suffisamment de CPU pour pousser une API sensible à la latence dans une spirale de latence tail.

Le modèle mental de l’équipe était de l’ère v1 : « les shares sont doux, les quotas sont durs ». Sous v2, avec une comptabilité unifiée et des comportements de contrôleurs différents, l’absence de cap dur signifiait que le job batch pouvait dominer lors des rafales, surtout si le contrôleur cpu n’était pas activé là où ils pensaient. L’« optimisation » a déplacé le goulot d’étranglement du throttling visible vers la mise en file invisible.

La correction fut ennuyeuse : restaurer des limites explicites cpu.max pour le workload rafaleur, activer le contrôleur cpu de manière cohérente dans le bon sous-arbre, et garder les shares pour les services réellement coopératifs. Ils ont aussi ajouté une étape de validation en CI qui inspecte les fichiers cgroup en fonctionnement après déploiement. « Faire confiance, mais vérifier » est un cliché parce qu’il reste vrai.

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

Un service adjacent aux finances tournait sur un petit cluster qui semblait toujours sur-provisionné. Quelqu’un a demandé pourquoi ils payaient pour de l’inactivité. La réponse SRE fut prévisible et agaçante : « Parce qu’on aime dormir. »

Ils avaient une pratique : à chaque modification d’image nœud, ils exécutaient un script de conformance court. Pas une grosse suite de tests — juste une douzaine de vérifications : mode cgroup, driver Docker, disponibilité des contrôleurs, et un conteneur unique qui fixe des limites mémoire et CPU et les prouve en lisant memory.max et cpu.max. Ça prenait moins de deux minutes.

Un jour, une nouvelle image de base s’est glissée avec les cgroups unifiés activés mais un runtime conteneur ancien à cause d’un dépôt verrouillé. Docker tournait, les conteneurs tournaient, mais les limites mémoire étaient aléatoires et échouaient parfois avec invalid argument. Leur script de conformance l’a détecté avant le déploiement en production.

La correction fut aussi terne qu’efficace : déverrouiller les packages runtime, mettre à jour en bundle, relancer les vérifications de conformance, et avancer. Pas d’incident. Pas de thread d’email exécutif. Juste la satisfaction tranquille d’avoir eu raison à l’avance.

Erreurs courantes : symptôme → cause racine → correction

Cette section est volontairement directe. Voici les motifs que je vois constamment sur le terrain.

1) « Docker démarre, mais les limites mémoire ne fonctionnent pas »

Symptôme : docker run --memory réussit ; le conteneur utilise plus que la limite ; docker stats montre illimité.

Cause racine : Docker utilise le driver cgroupfs sur un hôte systemd+v2, plaçant les conteneurs dans un sous-arbre sans +memory dans cgroup.subtree_control, ou runtime ancien qui rapporte mal les limites v2.

Correction : Passer au driver cgroup systemd, confirmer Delegate=yes, vérifier memory.max dans le chemin scope du conteneur.

2) « OCI runtime create failed: permission denied » sous /sys/fs/cgroup

Symptôme : Les conteneurs échouent à la création avec des erreurs de permission sysfs.

Cause racine : Délégation manquante au service Docker, utilisateur rootless sans contrôleurs délégués, ou conflits SELinux/AppArmor qui se manifestent comme des échecs d’écriture.

Correction : Assurer que l’unité systemd a Delegate=yes, utiliser le driver systemd, valider les prérequis rootless, vérifier les logs audit si MAC est activé.

3) « No such file or directory » pour des fichiers cgroup qui devraient exister

Symptôme : L’erreur référence des fichiers comme cpu.max ou memory.max manquants.

Cause racine : Contrôleur non activé pour ce sous-arbre. En v2, la présence d’un contrôleur dans cgroup.controllers n’est pas la même chose que son activation pour les enfants.

Correction : Activer les contrôleurs au bon niveau parent (configuration de slice systemd ou placement de sous-arbre approprié). Re-vérifier cgroup.subtree_control.

4) « Quota CPU ignoré »

Symptôme : --cpus défini, mais le conteneur utilise des cœurs complets.

Cause racine : contrôleur cpu non activé, ou le conteneur se retrouve en dehors du sous-arbre géré à cause d’un décalage de driver.

Correction : Valider cpu.max dans le cgroup du conteneur. Si c’est max, corriger l’activation du contrôleur et l’alignement des drivers.

5) « Ça casse seulement sur certains nœuds »

Symptôme : Un déploiement identique se comporte différemment selon les nœuds.

Cause racine : Flotte mixte v1/v2, ou drivers cgroup Docker mixtes, ou versions runtime mixtes.

Correction : Standardiser. Choisir un mode cgroup et l’appliquer via la construction d’image + flags de boot + gestion de configuration. Puis vérifier via un contrôle de conformance.

6) « Nous avons forcé cgroups v1 pour corriger et oublié »

Symptôme : Nouveaux outils supposent v2 ; agent de sécurité suppose v2 ; vous avez maintenant des attentes contradictoires.

Cause racine : Un rollback tactique devenu architecture permanente.

Correction : Traiter cela comme une dette avec un propriétaire et une date. Migrer délibérément, pool de nœuds par pool de nœuds.

Listes de contrôle / plan étape par étape

Checklist 1 : Acceptation d’une nouvelle image de nœud (10 minutes, épargne des heures)

  1. Confirmer le mode cgroup : stat -fc %T /sys/fs/cgroup doit correspondre à votre standard de flotte.
  2. Confirmer la hiérarchie systemd : systemd-analyze --version affiche default-hierarchy=unified (si standard v2).
  3. Confirmer que Docker voit v2 : docker info montre Cgroup Version: 2.
  4. Confirmer le driver Docker : Cgroup Driver: systemd (recommandé pour v2+systemd).
  5. Confirmer la délégation docker.service : systemctl show docker --property=Delegate vaut yes.
  6. Lancer un conteneur test avec limites CPU et mémoire et relire memory.max et cpu.max depuis son cgroup scope.
  7. Vérifier journald pour les avertissements : journalctl -u docker -b.

Checklist 2 : Chemin de correction quand Docker casse juste après une mise à jour de distro

  1. Confirmer le mode cgroup réel (Tâches 1–3). Ne comptez pas sur les notes de version.
  2. Vérifier driver/version Docker (Tâches 5–6). Mettre à jour si ancien.
  3. Vérifier délégation et activation des contrôleurs (Tâches 7–8, 14).
  4. Reproduire avec un conteneur minimal (Tâche 9). Ne déboguez pas encore votre JVM 4 Go.
  5. Confirmer que la limite est écrite (Tâches 10–12). Si elle n’est pas dans le fichier, elle n’existe pas.
  6. Ce n’est qu’ensuite que vous touchez au tuning applicatif.

Checklist 3 : Plan de standardisation pour une flotte cgroup mixte

  1. Choisir une cible : v2 unifié + driver Docker systemd (le plus courant), ou v1 legacy (temporaire).
  2. Définir un contrôle de conformité (la checklist d’acceptation ci-dessus) et l’exécuter par pool de nœuds.
  3. Mettre à jour Docker/containerd/runc comme un bundle dans chaque pool.
  4. Basculer le mode cgroup via flags de boot uniquement aux frontières de pool (éviter le mix intra-pool).
  5. Déployer avec canaries, vérifier en lisant /sys/fs/cgroup et les fichiers scope des conteneurs.
  6. Documenter la décision et l’intégrer dans les pipelines d’image pour éviter la dérive.

FAQ

1) Comment savoir si je suis sur cgroups v2 sans lire un billet de blog ?

Exécutez stat -fc %T /sys/fs/cgroup. S’il affiche cgroup2fs, vous êtes sur v2 unifié. Si vous voyez plusieurs montages de contrôleurs v1, vous êtes en v1/hybride.

2) Dois-je utiliser le driver cgroupfs de Docker sur un hôte systemd ?

Évitez-le sauf contrainte spécifique. Sur systemd + cgroups v2, le driver systemd est le choix stable car il aligne délégation, slices/scopes et activation des contrôleurs.

3) Pourquoi docker stats affiche des limites incorrectes sur v2 ?

Généralement incompatibilité runtime (Docker/containerd/runc ancien), ou Docker lit des données cgroup depuis un chemin qui ne correspond pas à l’endroit où les conteneurs résident réellement. Vérifiez en lisant memory.max directement dans le cgroup scope du conteneur.

4) Mon erreur dit « no such file or directory » pour memory.max. Mais v2 est activé. Pourquoi ?

Parce que v2 exige que les contrôleurs soient activés pour le sous-arbre. Le fichier n’existera pas si le contrôleur memory n’est pas activé au parent via cgroup.subtree_control.

5) Forcer cgroups v1 est-ce un correctif acceptable ?

Comme échappatoire à court terme, oui. Comme stratégie long terme, c’est un passif. Vous rencontrerez de plus en plus d’outils et de distributions qui supposent v2. Si vous forcez v1, standardisez-le et planifiez la migration.

6) Dois-je changer mes flags de ressources conteneur pour v2 ?

Généralement non ; les flags Docker restent les mêmes. Ce qui change, c’est si ces flags sont traduits vers les bons fichiers v2 et si le noyau les autorise dans ce sous-arbre.

7) Quel est le fichier le plus utile à regarder quand on débogue des problèmes de ressources v2 ?

Le chemin cgroup du conteneur depuis /proc/<pid>/cgroup, puis lisez les fichiers pertinents : memory.max, memory.current, cpu.max, pids.max. Si la valeur n’est pas là, la limite n’existe pas.

8) Docker rootless : puis-je l’exécuter en production avec cgroups v2 ?

Ça peut convenir pour des cas spécifiques, mais n’en faites pas un repas gratuit. Vous avez besoin d’une délégation utilisateur systemd appropriée, et certains contrôles diffèrent du comportement rootful. Testez précisément les limites dont vous dépendez.

9) Angle Kubernetes : que se passe-t-il si kubelet et Docker utilisent des drivers cgroup différents ?

Cette incompatibilité est un bug de fiabilité. Les processus se retrouvent dans des sous-arbres inattendus, la comptabilité devient étrange, et les limites peuvent ne pas s’appliquer. Alignez-les (systemd/systemd sur v2 est l’appariement courant).

10) Et l’IO — pourquoi mon tuning blkio a-t-il cessé de fonctionner ?

Parce que blkio est une terminologie v1. En v2, regardez les contrôles io.* et confirmez que le noyau et le périphérique supportent la politique. Vérifiez aussi que le contrôleur io est activé pour le sous-arbre.

Conclusion : prochaines étapes pratiques

cgroups v2 n’est pas « Docker cassé ». C’est le noyau exigeant un contrat plus propre. La douleur provient d’attentes décalées : drivers, délégation, et activation des contrôleurs.

Faites ceci ensuite :

  1. Standardisez votre flotte : choisissez cgroups v2 unifié (préféré) ou v1 legacy (temporaire) et imposez-le au boot.
  2. Sur hôtes v2 systemd, configurez Docker pour le driver cgroup systemd et vérifiez Delegate=yes.
  3. Créez un petit script de conformance qui lance un conteneur test et prouve les limites en lisant directement les fichiers cgroup.
  4. Mettez à jour Docker/containerd/runc en bundle. Les runtimes anciens sont l’endroit où naît le « ça marche parfois ».

Si vous ne retenez rien d’autre : quand les conteneurs « ignorent » les limites, n’argumentez pas avec les flags docker run. Lisez les fichiers cgroup. Le noyau est la source de vérité, et il n’accepte pas les excuses.

← Précédent
Profondeur de file d’attente ZFS : pourquoi certains SSD excellent sur ext4 et suffoquent sur ZFS
Suivant →
ZFS RAIDZ3 : quand la triple parité vaut les disques

Laisser un commentaire