cgroups¶
Les cgroups (control groups) sont un mécanisme du noyau Linux permettant d'organiser les processus en groupes hiérarchiques et d'appliquer des limites de ressources (CPU, mémoire, I/O, processus) à chacun de ces groupes. systemd repose intégralement sur les cgroups : chaque unité de service, slice et scope correspond directement à un cgroup.
v1 vs v2
Deux versions coexistent. cgroups v1 utilisait des hiérarchies multiples et indépendantes par contrôleur. cgroups v2 (Linux 4.5+) unifie tout dans une hiérarchie unique, simplifie la délégation et apporte PSI (Pressure Stall Information). Les distributions modernes — Debian 11+, Ubuntu 21.10+, RHEL 9+, Arch — utilisent le mode unified (v2 exclusif) par défaut. systemd fonctionne dans les deux modes mais ses fonctionnalités les plus avancées nécessitent v2.
Hiérarchie et montage¶
En mode unifié (v2), cgroupfs est monté en un seul point :
mount | grep cgroup
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
Chaque répertoire sous /sys/fs/cgroup/ est un cgroup. Les fichiers à l'intérieur exposent l'état et les paramètres du groupe.
# Voir l'arbre complet des cgroups (avec systemd)
systemd-cgls
# Voir l'utilisation en temps réel
systemd-cgtop
# Naviguer directement dans le VFS
ls /sys/fs/cgroup/system.slice/sshd.service/
Fichiers pseudo-FS communs à tous les cgroups¶
| Fichier | Contenu |
|---|---|
cgroup.procs | PIDs des processus membres (lecture/écriture) |
cgroup.controllers | Contrôleurs disponibles sur ce cgroup |
cgroup.subtree_control | Contrôleurs activés pour les enfants |
cgroup.type | domain, domain threaded, threaded |
cgroup.events | Événements : populated, frozen |
cgroup.kill | Écrire 1 tue tous les processus du groupe |
Contrôleurs disponibles (v2)¶
| Contrôleur | Ressource gérée | Fichiers clés |
|---|---|---|
cpu | Part CPU et quota dur | cpu.weight, cpu.max, cpu.stat |
memory | Mémoire anonyme et cache page | memory.max, memory.high, memory.current, memory.events |
io | Bande passante et IOPS disque | io.weight, io.max, io.stat |
pids | Nombre de processus/threads | pids.max, pids.current |
cpuset | Affinité CPU et nœuds NUMA | cpuset.cpus, cpuset.mems |
rdma | Ressources RDMA/InfiniBand | rdma.max, rdma.current |
misc | Ressources hétérogènes (DMA-BUF…) | misc.max, misc.current |
Vérifier les contrôleurs actifs sur le système¶
cat /sys/fs/cgroup/cgroup.controllers
# cpu io memory pids
# Contrôleurs délégués aux enfants au niveau racine
cat /sys/fs/cgroup/cgroup.subtree_control
Contrôleur cpu¶
Poids relatif (time-sharing)¶
cpu.weight exprime une part relative entre frères. La valeur par défaut est 100. Un service avec cpu.weight=200 obtiendra le double de CPU qu'un service à 100 lorsque la contention est forte.
Quota dur (hard throttling)¶
cpu.max est au format quota période en microsecondes. La valeur max signifie aucune limite.
# Lire la limite actuelle d'un service
cat /sys/fs/cgroup/system.slice/nginx.service/cpu.max
# max 100000
# Lire les stats d'utilisation et de throttling
cat /sys/fs/cgroup/system.slice/nginx.service/cpu.stat
# usage_usec 4521389
# user_usec 3211004
# system_usec 1310385
# nr_periods 4521
# nr_throttled 12
# throttled_usec 8204
Contrôleur memory¶
| Fichier | Rôle |
|---|---|
memory.current | Usage actuel (octets) |
memory.max | Limite dure — déclenche OOM-kill si dépassée |
memory.high | Limite souple — déclenche throttling et réclamation de pages |
memory.min | Réservation : ces pages ne sont jamais réclamées |
memory.low | Priorité de protection : réclamées en dernier |
memory.swap.max | Limite d'utilisation du swap |
memory.events | Compteurs : oom, oom_kill, high, max |
memory.pressure | PSI — saturation mémoire |
# Usage mémoire d'un service
cat /sys/fs/cgroup/system.slice/postgresql.service/memory.current
# Nombre d'OOM kills depuis le démarrage
grep oom_kill /sys/fs/cgroup/system.slice/postgresql.service/memory.events
Contrôleur io¶
io.weight fonctionne comme cpu.weight : part relative entre groupes frères lorsque le disque est saturé.
io.max limite les débits et/ou le nombre d'opérations par seconde, au format MAJ:MIN rbps=N wbps=N riops=N wiops=N :
# Lire les stats I/O d'un service
cat /sys/fs/cgroup/system.slice/postgresql.service/io.stat
# 8:0 rbytes=204800 wbytes=1048576 rios=50 wios=256 dbytes=0 dios=0
Contrôleur pids¶
Limite le nombre total de processus et threads dans le cgroup, agissant comme protection contre les fork bombs :
cat /sys/fs/cgroup/system.slice/apache2.service/pids.max
# 512
cat /sys/fs/cgroup/system.slice/apache2.service/pids.current
# 48
PSI — Pressure Stall Information¶
Linux 4.20+ expose pour chaque contrôleur un fichier *.pressure indiquant le pourcentage de temps pendant lequel des tâches ont été bloquées en attente de la ressource.
# Pression mémoire sur un service
cat /sys/fs/cgroup/system.slice/mon-service.service/memory.pressure
# some avg10=0.12 avg60=0.03 avg300=0.01 total=1234567
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
some: au moins une tâche était bloquéefull: toutes les tâches étaient bloquées (saturation totale)avg10/avg60/avg300: moyennes glissantes en pourcentage sur 10 s, 1 min, 5 min
Intégration systemd¶
Directives de ressources dans les unités¶
| Directive systemd | Contrôleur cgroup | Fichier cgroupfs |
|---|---|---|
CPUWeight= | cpu | cpu.weight |
CPUQuota= | cpu | cpu.max |
MemoryMax= | memory | memory.max |
MemoryHigh= | memory | memory.high |
MemoryMin= | memory | memory.min |
MemoryLow= | memory | memory.low |
MemorySwapMax= | memory | memory.swap.max |
IOWeight= | io | io.weight |
IOReadBandwidthMax= | io | io.max |
IOWriteBandwidthMax= | io | io.max |
IOReadIOPSMax= | io | io.max |
IOWriteIOPSMax= | io | io.max |
TasksMax= | pids | pids.max |
AllowedCPUs= | cpuset | cpuset.cpus |
AllowedMemoryNodes= | cpuset | cpuset.mems |
Exemple d'unité avec limites de ressources¶
[Unit]
Description=Serveur applicatif Java
[Service]
User=appuser
ExecStart=/opt/app/bin/start.sh
# CPU : 50 % d'un cœur au maximum, poids relatif réduit
CPUQuota=50%
CPUWeight=50
# Mémoire : throttling à 512 Mo, OOM-kill à 768 Mo
MemoryHigh=512M
MemoryMax=768M
MemorySwapMax=0
# I/O : limiter l'écriture sur /dev/sda (identifier le device avec lsblk)
IOWriteBandwidthMax=/dev/sda 20M
# Nombre de processus/threads maximum
TasksMax=128
[Install]
WantedBy=multi-user.target
Modifier les limites à chaud avec systemctl set-property¶
Les limites de ressources peuvent être ajustées sans redémarrer le service :
# Augmenter la limite mémoire à chaud
sudo systemctl set-property mon-service.service MemoryMax=1G
# Limiter le quota CPU à 25 %
sudo systemctl set-property mon-service.service CPUQuota=25%
# Appliquer de manière non persistante (annulé au redémarrage)
sudo systemctl set-property --runtime mon-service.service MemoryHigh=256M
# Voir les propriétés actuelles
systemctl show mon-service.service -p MemoryMax -p CPUQuota -p TasksMax
Persistance
Sans --runtime, set-property écrit un fichier drop-in dans /etc/systemd/system/mon-service.service.d/50-CPUQuota.conf. Ces fichiers sont visibles avec systemctl cat mon-service.service.
Slices¶
Les slices forment l'ossature de l'arbre cgroup de systemd. Elles regroupent des services partageant une même politique de ressources.
systemd (root)
├── system.slice ← services système
│ ├── nginx.service
│ └── postgresql.service
├── user.slice ← sessions utilisateurs
│ └── user-1000.slice
│ └── session-3.scope
└── machine.slice ← VMs et containers nspawn/podman
└── mon-container.scope
# Rattacher un service à une slice dédiée
# /etc/systemd/system/mon-service.service
# [Service]
# Slice=app.slice
# Créer une slice avec des limites qui s'appliquent à tous ses enfants
# /etc/systemd/system/app.slice
# [Slice]
# MemoryMax=4G
# CPUWeight=200
Délégation¶
La délégation (Delegate=yes) transfère le contrôle d'un sous-arbre cgroup à un processus non-root — typiquement un gestionnaire de containers rootless.
[Service]
User=podman-user
Delegate=yes
DelegateControllers=cpu memory pids io
ExecStart=/usr/bin/podman start --log-driver=journald mon-container
Sécurité de la délégation
Un cgroup délégué peut positionner ses propres limites, mais ne peut pas dépasser celles définies par le parent. La délégation ne donne pas accès aux cgroups frères. Vérifier que le service ne dispose pas de capabilities permettant de modifier des cgroups hors de son sous-arbre.
# Vérifier qu'une délégation est en place
systemctl show podman.service -p Delegate -p DelegateControllers
Monitoring¶
systemd-cgtop¶
# Vue temps réel de l'utilisation par cgroup
systemd-cgtop
# Trier par mémoire
systemd-cgtop --order=memory
# Rafraîchissement toutes les 2 secondes, 5 itérations
systemd-cgtop --delay=2 --iterations=5
systemctl status¶
systemctl status inclut les statistiques cgroup du processus principal :
systemctl status postgresql.service
# ● postgresql.service - PostgreSQL RDBMS
# ...
# Memory: 156.3M (max: 512.0M available: 355.6M)
# CPU: 1.234s
# CGroup: /system.slice/postgresql.service
# └─1234 /usr/lib/postgresql/15/bin/postgres -D /var/lib/postgresql/15/main
Lecture directe pour le scripting¶
# Usage mémoire en octets d'un service
cat /sys/fs/cgroup/system.slice/nginx.service/memory.current
# Part CPU consommée depuis le démarrage (en microsecondes)
grep usage_usec /sys/fs/cgroup/system.slice/nginx.service/cpu.stat
# Nombre de processus dans un service
cat /sys/fs/cgroup/system.slice/nginx.service/pids.current
# Lister tous les PIDs d'un service
cat /sys/fs/cgroup/system.slice/nginx.service/cgroup.procs
Diagnostics courants¶
Service tué par l'OOM killer¶
# Voir le statut et les derniers messages du service
systemctl status mon-service.service
# Compter les OOM kills depuis le démarrage
grep oom_kill /sys/fs/cgroup/system.slice/mon-service.service/memory.events
# Chercher dans le journal
journalctl -u mon-service.service -g 'oom|OOM|killed'
# Voir l'event noyau
journalctl -k -g 'oom_kill'
Service throttlé sur le CPU¶
# Vérifier si le service subit du throttling
cat /sys/fs/cgroup/system.slice/mon-service.service/cpu.stat
# Si nr_throttled > 0, le service est limité par CPUQuota=
# Voir la valeur actuelle du quota
systemctl show mon-service.service -p CPUQuota
TasksMax atteint — fork impossible¶
# Voir l'événement dans le journal
journalctl -u mon-service.service -g 'fork|Tasks'
# Voir la limite et l'usage actuels
cat /sys/fs/cgroup/system.slice/mon-service.service/pids.max
cat /sys/fs/cgroup/system.slice/mon-service.service/pids.current
# Augmenter la limite
sudo systemctl set-property mon-service.service TasksMax=256
Identifier le cgroup d'un processus¶
# Depuis le PID
cat /proc/1234/cgroup
# 0::/system.slice/nginx.service
# Voir les limites qui s'appliquent au processus
PID=1234
CGROUP=$(cat /proc/${PID}/cgroup | cut -d: -f3)
ls "/sys/fs/cgroup${CGROUP}/"
Migration d'un système v1/v2 hybride¶
# Vérifier le mode cgroup actif
mount | grep cgroup
# Forcer le mode unifié v2 (paramètre noyau à ajouter dans le bootloader)
# systemd.unified_cgroup_hierarchy=1
Bonnes pratiques¶
- Toujours définir
MemoryMax=sur les services produisant de la charge variable — un service sans limite peut épuiser la mémoire du système entier. - Utiliser
MemoryHigh=comme garde-fou avantMemoryMax=: le throttling progressif évite les OOM kills brutaux. - Surveiller PSI plutôt que d'attendre les OOM events pour détecter une saturation naissante.
- Préférer les slices pour appliquer des politiques groupées plutôt que de dupliquer les directives dans chaque service.
- Ne pas déléguer sans restriction explicite : préciser
DelegateControllers=pour limiter ce que le processus délégué peut modifier. - Tester les limites en environnement de staging :
MemoryMax=trop bas peut tuer un service en production lors d'un pic de charge légitime.
Voir aussi¶
- namespaces — isolation de vue des ressources, complémentaire aux cgroups
- capabilities — permissions nécessaires pour modifier des cgroups hors délégation
- systemd-nspawn — containers légers avec cgroups et namespaces
man 7 cgroups— référence noyau complèteman 5 systemd.resource-control— toutes les directives de contrôle de ressourcesman 1 systemd-cglsetman 1 systemd-cgtop- Documentation kernel cgroups v2