Un processus part en boucle et sature un cœur. Un service fuit et mange toute la RAM. Les cgroups (control groups) sont le mécanisme du noyau Linux qui empêche cela : ils regroupent des processus et plafonnent ce que ce groupe a le droit de consommer en CPU, en mémoire, en entrées/sorties disque et en nombre de processus. Cette page vous montre comment poser ces limites, comment prouver qu'elles s'appliquent, et surtout pourquoi, sur un serveur moderne, la bonne interface n'est pas /sys/fs/cgroup mais systemd. Les exemples sont exécutés sur Ubuntu 24.04 (noyau 6.8), mais le comportement est identique sur Debian 12 et RHEL 10, qui utilisent toutes cgroups v2 par défaut.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Vérifier en une commande si votre machine est en cgroups v1 ou v2.
- Créer un cgroup, y placer un processus et le limiter à 20 % d'un cœur, preuve à l'appui.
- Plafonner la mémoire et observer l'OOM kill déclenché par le cgroup.
- Brider les I/O disque et le nombre de processus (
pids.max). - Limiter un service avec
systemctl set-property, la seule méthode qui survit à un redémarrage. - Comprendre ce que Docker et Kubernetes font réellement quand vous écrivez
--memory=128m.
Sur quelle version de cgroups êtes-vous
Section intitulée « Sur quelle version de cgroups êtes-vous »Deux générations de cgroups coexistent dans le noyau et elles n'ont ni les mêmes fichiers ni les mêmes chemins. Mélanger les deux est la première source de commandes qui ne marchent pas : un tutoriel qui vous fait écrire dans /sys/fs/cgroup/cpu/cpu.cfs_quota_us parle de la v1, morte sur les distributions actuelles. La commande qui tranche interroge le type du système de fichiers monté.
stat -fc %T /sys/fs/cgroupcgroup2fsLa réponse cgroup2fs signifie cgroups v2 unifié : une seule hiérarchie, tous les contrôleurs au même endroit. Une réponse tmpfs signifierait v1 ou mode hybride. Toute cette page suppose la v2, qui est le défaut d'Ubuntu 22.04+, Debian 11+ et RHEL 9+.
Un contrôleur (cpu, memory, io, pids…) est le module du noyau qui applique une catégorie de limite. La racine vous dit lesquels sont compilés et disponibles.
cat /sys/fs/cgroup/cgroup.controllerscpuset cpu io memory hugetlb pids rdma miscLimiter un processus à 20 % de CPU
Section intitulée « Limiter un processus à 20 % de CPU »Le meilleur moyen de comprendre les cgroups est d'en fabriquer un et de le voir mordre. Un cgroup est un simple répertoire sous /sys/fs/cgroup : le créer suffit à le faire exister, et le noyau y dépose immédiatement les fichiers de contrôle. Toutes ces opérations exigent les droits root, et comme la redirection > est exécutée par votre shell (pas par sudo), il faut passer par sudo tee pour écrire dans ces fichiers.
sudo mkdir /sys/fs/cgroup/democat /sys/fs/cgroup/demo/cgroup.controllerscpuset cpu io memory hugetlb pids rdma miscCes contrôleurs sont utilisables ici parce que le parent (la racine) les a délégués à ses enfants. Les fichiers de limite sont donc déjà présents, en position ouverte.
cat /sys/fs/cgroup/demo/cpu.maxcat /sys/fs/cgroup/demo/memory.maxcat /sys/fs/cgroup/demo/pids.maxmax 100000maxmaxLançons maintenant une boucle infinie qui sature un cœur, puis rattachons-la au cgroup. Le fichier cgroup.procs contient la liste des PID (identifiants de processus) du groupe : y écrire un PID déplace le processus, qui hérite aussitôt des limites en vigueur.
while :; do :; done &# PID=718500echo 718500 | sudo tee /sys/fs/cgroup/demo/cgroup.procscat /proc/718500/cgroup0::/demoLa ligne 0::/demo confirme le rattachement. Un processus n'appartient qu'à un seul cgroup à la fois : l'y placer le retire automatiquement du précédent. Sans limite, il consomme 100 % d'un cœur, ce que confirme le compteur usage_usec de cpu.stat mesuré sur 5 secondes.
top -b -n 1 -p 718500 | tail -1 718500 bob 20 0 7348 1808 1512 R 100,0 0,0 0:05.23 bashLe fichier cpu.max attend deux nombres : un quota et une période, en microsecondes. Écrire 20000 100000 autorise 20 ms de temps CPU par tranche de 100 ms, soit 20 % d'un cœur. La limite s'applique immédiatement aux processus déjà rattachés, sans redémarrage ni signal.
echo "20000 100000" | sudo tee /sys/fs/cgroup/demo/cpu.maxtop -b -n 1 -p 718500 | tail -1 718500 bob 20 0 7348 1808 1512 R 18,2 0,0 0:06.69 bashLe processus est tombé de 100 % à 18 %, sans avoir été modifié ni redémarré. Le fichier cpu.stat raconte l'histoire côté noyau : il compte les périodes pendant lesquelles le groupe a été étranglé (throttled), c'est-à-dire mis en pause parce qu'il avait épuisé son quota.
cat /sys/fs/cgroup/demo/cpu.statusage_usec 6677619user_usec 6676621system_usec 997nr_periods 72nr_throttled 72throttled_usec 5719555nr_throttled 72 sur nr_periods 72 : toutes les périodes ont été étranglées. C'est la signature d'un processus qui tape en permanence dans son plafond. Sur un service en production, un nr_throttled qui grimpe est le symptôme d'un quota trop serré, pas d'une machine surchargée : c'est la métrique à surveiller avant d'accuser le matériel.
La règle qui fait échouer la plupart des tutoriels
Section intitulée « La règle qui fait échouer la plupart des tutoriels »Le fichier cgroup.subtree_control est le piège numéro un de cgroups v2, et il mérite qu'on s'y arrête avant d'aller plus loin. Beaucoup de guides vous font écrire +cpu +memory dedans juste avant d'y placer un processus. Cette séquence échoue systématiquement, et voici pourquoi.
Pour revenir en arrière, retirez les contrôleurs avec le préfixe -, et le cgroup redevient capable d'accueillir des processus.
echo "-cpu -memory" | sudo tee /sys/fs/cgroup/demo/cgroup.subtree_controlPlafonner la mémoire et déclencher l'OOM kill
Section intitulée « Plafonner la mémoire et déclencher l'OOM kill »La limite mémoire est la plus brutale des trois, parce qu'on ne peut pas « ralentir » une allocation : quand un processus demande une page et qu'il n'y a plus de quota, le noyau tue quelqu'un dans le cgroup. Le fichier memory.max fixe ce plafond et il accepte les suffixes K, M, G, ce qui évite de compter les octets à la main. La relecture, elle, renvoie toujours des octets.
echo "512M" | sudo tee /sys/fs/cgroup/demo/memory.maxcat /sys/fs/cgroup/demo/memory.max536870912Pour observer le déclenchement, descendons le plafond à 64 Mo et coupons le swap du groupe (sinon le noyau se contente de gagner du temps en swappant au lieu de tuer). Un petit programme Python alloue ensuite la mémoire mégaoctet par mégaoctet.
echo "64M" | sudo tee /sys/fs/cgroup/demo/memory.maxecho "0" | sudo tee /sys/fs/cgroup/demo/memory.swap.maxLe processus est rattaché au cgroup avant de commencer à allouer, puis il compte à voix haute.
58 Mo alloues59 Mo alloues60 Mo allouesbash: 719360 Killed ./oom.shcode de sortie : 137Le processus meurt à 60 Mo, tué par le noyau, avec le code de sortie 137 (128 + signal 9). Ce chiffre est exactement celui que vous verrez sur un conteneur OOMKilled. Le fichier memory.events garde la trace de l'incident, et c'est lui qu'il faut lire pour un diagnostic après coup.
cat /sys/fs/cgroup/demo/memory.eventslow 0high 0max 35oom 1oom_kill 1oom_group_kill 0Trois compteurs comptent vraiment. max 35 signifie que le groupe a buté 35 fois sur son plafond, forçant le noyau à récupérer de la mémoire : un max qui grimpe sans oom_kill révèle un cgroup trop juste, qui passe son temps à recycler des pages au lieu de travailler. oom 1 indique qu'une allocation n'a pas pu être satisfaite, et oom_kill 1 qu'un processus a été abattu pour cela. Le journal du noyau nomme le coupable et le contexte.
sudo dmesg | tail -2oom-kill:constraint=CONSTRAINT_MEMCG,oom_memcg=/demo,task_memcg=/demo,task=python3,pid=719360Memory cgroup out of memory: Killed process 719360 (python3) total-vm:80560kB, anon-rss:65152kBCONSTRAINT_MEMCG est le mot-clé qui distingue un OOM de cgroup (une seule application dépasse son quota, le reste de la machine va bien) d'un OOM système (la machine entière est à court de RAM). Confondre les deux fait chercher le problème au mauvais endroit pendant des heures.
Brider les I/O disque et le nombre de processus
Section intitulée « Brider les I/O disque et le nombre de processus »Le contrôleur io plafonne le débit disque via le fichier io.max, qui attend le numéro majeur:mineur du périphérique bloc suivi des limites voulues : rbps et wbps pour les octets par seconde en lecture et en écriture, riops et wiops pour le nombre d'opérations. Ces numéros s'obtiennent avec lsblk.
lsblk -o NAME,MAJ:MIN,TYPENAME MAJ:MIN TYPEnvme0n1 259:0 disk├─nvme0n1p3 259:3 part └─ubuntu--vg-ubuntu--lv 252:0 lvmLe piège est ici : il faut viser le disque physique (259:0), pas le volume logique LVM (252:0) sur lequel le système de fichiers est monté. Une limite posée sur le mauvais numéro s'écrit sans erreur et ne freine strictement rien, ce qui fait perdre un temps considérable. Limitons la lecture à 1 Mo/s, puis relisons un fichier en I/O direct (iflag=direct) pour contourner le cache page, sans quoi la lecture serait servie par la RAM et ne toucherait jamais le disque.
echo "259:0 rbps=1048576" | sudo tee /sys/fs/cgroup/demo/io.maxdd if=fichier-test of=/dev/null bs=1M count=20 iflag=direct20971520 bytes (21 MB, 20 MiB) copied, 19,9714 s, 1,1 MB/sLa même lecture montait à 1,2 Go/s sans limite : le plafond est respecté au pourcent près. Le contrôleur pids joue dans un autre registre : il ne rationne pas une ressource matérielle mais le nombre de processus, ce qui en fait le rempart le plus simple contre une fork bomb ou une application qui fuit ses threads. Le fichier pids.max prend un entier.
echo 5 | sudo tee /sys/fs/cgroup/demo/pids.maxToute création de processus au-delà de la cinquième est alors refusée par le noyau, et l'échec remonte jusqu'au shell.
./pids.sh: fork: retry: Resource temporarily unavailable./pids.sh: fork: Resource temporarily unavailableLe compteur pids.events enregistre les refus (max 5 ici), ce qui permet de repérer une application qui frôle sa limite avant qu'elle ne casse.
Ce que les cgroups ne savent pas faire
Section intitulée « Ce que les cgroups ne savent pas faire »Une croyance très répandue veut que les cgroups limitent aussi la bande passante réseau. C'est faux, et cette confusion vient de la v1. Les anciens contrôleurs net_cls et net_prio se contentaient de marquer ou de prioriser les paquets ; ils ne plafonnaient aucun débit, et ils n'ont aucun équivalent en cgroups v2. Le fichier que vous cherchez n'existe pas.
Pour brider un débit réseau, il faut sortir des cgroups et employer tc (traffic control), le sous-système de mise en forme du trafic du noyau, ou un programme eBPF attaché au cgroup, ce que fait Cilium sur Kubernetes. Retenez la frontière : les cgroups gèrent CPU, mémoire, I/O bloc, PID et périphériques, et s'arrêtent là.
systemd est le vrai propriétaire des cgroups
Section intitulée « systemd est le vrai propriétaire des cgroups »Voilà le point que la plupart des tutoriels passent sous silence, et c'est le plus important de cette page. Sur toute distribution moderne, systemd est le gestionnaire de cgroups de la machine. Il en revendique la propriété exclusive, place chaque service dans son propre cgroup et réapplique ses valeurs dès qu'il en a l'occasion. Tout ce que vous avez écrit à la main dans /sys/fs/cgroup pour un service géré par systemd est donc temporaire.
La démonstration est sans appel. Prenons un service dont systemd a fixé le quota CPU à 20 %, écrasons cpu.max directement dans /sys, puis lançons un simple daemon-reload.
cat /sys/fs/cgroup/system.slice/demo-burn.service/cpu.maxecho "80000 100000" | sudo tee /sys/fs/cgroup/system.slice/demo-burn.service/cpu.maxsudo systemctl daemon-reloadcat /sys/fs/cgroup/system.slice/demo-burn.service/cpu.max20000 10000080000 10000020000 100000Votre modification a vécu quelques secondes. Un daemon-reload, un set-property sur n'importe quelle autre propriété, un redémarrage du service ou de la machine, et systemd restaure sa vision du monde. Ajoutez que /sys/fs/cgroup est un système de fichiers virtuel du noyau, recréé vide à chaque démarrage : votre mkdir n'a jamais rien persisté.
Limiter un service correctement
Section intitulée « Limiter un service correctement »La commande systemctl set-property applique la limite immédiatement et l'écrit dans un fichier de configuration qui survit au redémarrage. Elle accepte les mêmes directives que les fichiers d'unité.
sudo systemctl set-property demo-burn.service CPUQuota=20% MemoryMax=256MSystemd traduit ces directives en écritures dans le cgroup du service, exactement celles que vous auriez faites à la main, mais dont il garde désormais la mémoire.
cat /sys/fs/cgroup/system.slice/demo-burn.service/cpu.maxcat /sys/fs/cgroup/system.slice/demo-burn.service/memory.max20000 100000268435456La persistance est assurée par un drop-in, un fragment de configuration qui complète l'unité sans la modifier. Systemd le dépose dans un répertoire dédié aux propriétés posées en ligne de commande.
cat /etc/systemd/system.control/atd.service.d/50-CPUQuota.conf# This is a drop-in unit file extension, created via "systemctl set-property"# or an equivalent operation. Do not edit.[Service]CPUQuota=10%Pour un réglage que vous voulez versionner avec le reste de votre configuration, préférez un drop-in écrit à la main via sudo systemctl edit nom.service, qui contient les mêmes directives. CPUQuota=20% correspond à cpu.max, MemoryMax=256M à memory.max, TasksMax=100 à pids.max, et IOReadBandwidthMax=/dev/nvme0n1 5M à io.max. Pour annuler proprement une propriété posée par set-property, sudo systemctl revert nom.service supprime le drop-in et remet le service dans son état d'origine.
Vérifier ce qui est réellement appliqué
Section intitulée « Vérifier ce qui est réellement appliqué »Attention au nom des propriétés : systemctl show nom.service -p CPUQuota ne renvoie rien. CPUQuota est une directive d'unité, pas une propriété exposée par systemd. La propriété à interroger porte un autre nom et s'exprime en temps CPU par seconde.
systemctl show demo-burn.service -p CPUQuotaPerSecUSec -p MemoryMaxCPUQuotaPerSecUSec=200msMemoryMax=268435456200ms par seconde, ce sont bien les 20 % demandés. La commande systemctl status donne la même information sous une forme plus lisible, en affichant le drop-in actif, la consommation courante et le cgroup du service.
● demo-burn.service - Demo cgroups Drop-In: /run/systemd/transient/demo-burn.service.d └─50-CPUQuota.conf, 50-MemoryMax.conf Memory: 344.0K (max: 256.0M available: 255.6M peak: 848.0K) CGroup: /system.slice/demo-burn.service └─722975 /bin/bash -c "while :; do :; done"Explorer l'arbre des cgroups
Section intitulée « Explorer l'arbre des cgroups »Systemd fournit deux commandes qui remplacent avantageusement les explorations à coups de cat dans /sys. systemd-cgls affiche l'arborescence des cgroups avec les processus qu'ils contiennent, ce qui répond immédiatement à la question « quel service ce processus appartient-il ».
systemd-cgls --no-pager -u system.sliceUnit system.slice (/system.slice):├─containerd.service …│ ├─ 1287 /usr/bin/containerd│ ├─ 2335 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 259166de4274f28…├─systemd-networkd.service│ └─603 /usr/lib/systemd/systemd-networkdsystemd-cgtop est le pendant dynamique : un top par cgroup, qui trie les services par consommation. C'est l'outil qui répond en quelques secondes à la question « quel service mange le CPU de ce serveur », là où top ne montre que des processus isolés sans dire à qui ils appartiennent.
systemd-cgtop -b -n 1 --depth=2CGroup Tasks %CPU Memory Input/s Output/s/ 2469 - 11.5G - -system.slice 1146 - 10.2G - -system.slice/atd.service 1 - 504.0K - -init.scope 1 - 9.9M - -Ce que font vraiment Docker et Kubernetes
Section intitulée « Ce que font vraiment Docker et Kubernetes »Quand vous écrivez docker run --cpus=0.5 --memory=128m, Docker ne fait rien d'autre que ce que vous venez de faire à la main. Il crée un cgroup pour le conteneur et y écrit vos valeurs. La preuve tient en trois commandes. Le moteur annonce d'abord qu'il délègue à systemd et travaille en v2.
docker info | grep -i cgroup Cgroup Driver: systemd Cgroup Version: 2Lançons un conteneur limité, puis allons lire son cgroup sur l'hôte. Le nom du cgroup contient l'ID complet du conteneur, dans une scope rangée sous system.slice.
docker run -d --rm --name demo-cg --cpus=0.5 --memory=128m alpine:3.22 sleep 120cat /sys/fs/cgroup/system.slice/docker-8920e22899cd....scope/cpu.maxcat /sys/fs/cgroup/system.slice/docker-8920e22899cd....scope/memory.max50000 100000134217728--cpus=0.5 est devenu 50000 100000, et --memory=128m est devenu 134217728 octets. Rien de magique : un conteneur, c'est un ensemble de processus dans un cgroup (pour les ressources) et dans des namespaces (pour l'isolation de la vue système). Kubernetes applique la même mécanique un cran plus haut : les requests d'un Pod deviennent des poids (cpu.weight), les limits deviennent des plafonds (cpu.max, memory.max). C'est pour cela qu'un conteneur qui dépasse sa limits.memory finit en OOMKilled avec le code 137 : c'est le même OOM kill de cgroup que celui observé plus haut, avec le même CONSTRAINT_MEMCG dans dmesg.
Dépannage
Section intitulée « Dépannage »Les cgroups échouent presque toujours en silence ou avec des messages trompeurs. Voici les erreurs réellement rencontrées en construisant cette page, avec leur cause et leur correctif.
| Symptôme | Cause | Solution |
|---|---|---|
mkdir: cannot create directory '/sys/fs/cgroup/demo': Permission denied | /sys/fs/cgroup appartient à root | Préfixer par sudo |
bash: /sys/fs/cgroup/demo/cpu.max: Permission denied avec sudo echo ... > | La redirection > est faite par votre shell, pas par sudo | Utiliser echo "..." | sudo tee /sys/fs/cgroup/demo/cpu.max |
tee: /sys/fs/cgroup/demo/cgroup.procs: Device or resource busy | Des contrôleurs sont activés dans le cgroup.subtree_control du même cgroup (règle « no internal processes ») | Les retirer (echo "-cpu -memory" | sudo tee .../cgroup.subtree_control) ou placer les processus dans un sous-groupe |
rmdir: failed to remove '/sys/fs/cgroup/demo': Device or resource busy | Le cgroup contient encore des processus | Vider cgroup.procs (arrêter ou déplacer les processus) avant le rmdir |
io.max écrit, mais aucun ralentissement | Mauvais major:minor (volume LVM au lieu du disque physique), ou lecture servie par le cache page | Viser le disque physique (lsblk -o NAME,MAJ:MIN) et tester avec dd iflag=direct |
fork: Resource temporarily unavailable | pids.max atteint | Relever pids.max ou corriger la fuite de processus |
La valeur écrite dans /sys revient à son ancienne valeur | systemd réapplique les propriétés de l'unité au daemon-reload, au restart et au boot | Passer par systemctl set-property ou un drop-in |
| Le cgroup créé à la main a disparu après un redémarrage | /sys/fs/cgroup est un système de fichiers noyau, recréé vide au boot | Déclarer la limite dans une unité systemd |
systemctl show svc -p CPUQuota ne renvoie rien | CPUQuota est une directive d'unité, pas une propriété exposée | Lire CPUQuotaPerSecUSec |
D'où viennent les cgroups
Section intitulée « D'où viennent les cgroups »Un peu de contexte aide à comprendre pourquoi deux versions coexistent. Les cgroups sont entrés dans le noyau Linux en 2007, poussés par des ingénieurs de Google qui devaient faire cohabiter des milliers d'applications sur les mêmes serveurs. Cette v1 donnait à chaque contrôleur sa propre hiérarchie indépendante, ce qui menait à des arbres incohérents et à des règles impossibles à raisonner globalement.
La v2, stabilisée en 2016, a tout ramené sous une hiérarchie unique montée sur /sys/fs/cgroup, avec des noms de fichiers cohérents (cpu.max, memory.max, io.max). C'est aujourd'hui le seul mode utilisé par défaut. La conséquence pratique est simple : toute documentation antérieure à 2019 parle probablement de la v1 et ses chemins n'existent plus chez vous. Vérifiez avec stat -fc %T /sys/fs/cgroup avant de suivre un tutoriel.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »stat -fc %T /sys/fs/cgroupdoit renvoyercgroup2fs; sinon vous êtes en v1 et les chemins de cette page ne s'appliquent pas.- Un cgroup est un répertoire ; on y place un processus en écrivant son PID dans
cgroup.procs, et les limites s'appliquent aussitôt. cgroup.subtree_controlconfigure les enfants, jamais le cgroup lui-même. Un cgroup qui délègue des contrôleurs ne peut plus contenir de processus (Device or resource busy).cpu.maxétrangle (nr_throttleddanscpu.stat),memory.maxtue (oom_killdansmemory.events, code de sortie 137),io.maxralentit,pids.maxrefuse lesfork.- cgroups v2 ne limite aucun débit réseau :
net_clsetnet_prione faisaient que marquer le trafic. Utiliseztcou eBPF. - systemd est propriétaire des cgroups : tout
echodans/sys/fs/cgrouppour un service est écrasé au premierdaemon-reloadet perdu au reboot. - La bonne méthode est
systemctl set-property nom.service CPUQuota=20%, vérifiée avecCPUQuotaPerSecUSec(et nonCPUQuota, qui ne renvoie rien). systemd-cglsetsystemd-cgtopsont les outils d'exploration ; Docker et Kubernetes ne font qu'écrire dans ces mêmes fichiers.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Docker : voir comment un moteur de conteneurs assemble cgroups et namespaces au quotidien.
- Commandes Docker essentielles : les options
--cpus,--memoryet leur traduction en limites de cgroup. - Débugger une application Kubernetes : diagnostiquer un Pod
OOMKilled, c'est-à-dire un OOM kill de cgroup. - Kubernetes : où les
requestsetlimitsdeviennent des poids et des plafonds de cgroup.