Guide complet des Cgroups Systemd pour la limitation et l'isolation des ressources

Utilisez les cgroups, slices et propriétés d'unités systemd pour limiter le CPU, la mémoire et les E/S sans modifier les fichiers cgroups bruts.

Guide complet des Cgroups Systemd pour la limitation et l'isolation des ressources

Systemd place déjà les services dans des groupes de contrôle Linux. Vous n'avez pas besoin de créer manuellement des répertoires cgroups pour empêcher un travail par lots de monopoliser la machine. Dans de nombreux cas, vous pouvez ajouter quelques propriétés à un service ou une slice, recharger systemd, et obtenir des contrôles CPU, mémoire, tâches et E/S qui survivent au redémarrage et apparaissent dans les outils normaux systemctl.

L'astuce consiste à choisir le bon type de limite. Une limite mémoire dure peut protéger l'hôte mais tuer le service si elle est trop basse. Les poids CPU sont doux jusqu'à ce que le système soit occupé. Les quotas CPU sont stricts mais peuvent ajouter de la latence. Les limites E/S dépendent de la pile de stockage et de la version des cgroups. Le contrôle des ressources n'est pas une case à cocher ; c'est un compromis opérationnel.

Comprendre les groupes de contrôle (cgroups)

Avant de plonger dans l'implémentation de systemd, il est essentiel de comprendre les concepts fondamentaux des cgroups. Les cgroups sont un mécanisme hiérarchique du noyau Linux qui permet de regrouper des processus et d'assigner des politiques de gestion des ressources à ces groupes. Ces politiques peuvent inclure :

  • CPU : Limiter le temps CPU, prioriser l'accès au CPU.
  • Mémoire : Définir des limites d'utilisation mémoire, prévenir les conditions de mémoire insuffisante (OOM).
  • E/S : Limiter les opérations de lecture/écriture disque.
  • Réseau : Le contrôle réseau est possible via le contrôle de trafic Linux et les outils associés, mais les propriétés intégrées de systemd se concentrent principalement sur le CPU, la mémoire, le nombre de processus, l'accès aux périphériques et les E/S bloc.
  • Accès aux périphériques : Contrôler l'accès à des périphériques spécifiques.

Le noyau expose les configurations cgroups via un système de fichiers virtuel, généralement monté sur /sys/fs/cgroup. Chaque contrôleur (par exemple, cpu, memory) a son propre répertoire, et à l'intérieur de ceux-ci, des hiérarchies de répertoires représentent des groupes et leurs limites de ressources associées.

Architecture de gestion des cgroups de Systemd

Systemd abstrait la complexité de la manipulation directe des cgroups en fournissant un système structuré de gestion des unités. Il organise les processus en une hiérarchie d'unités, qui sont ensuite mappées aux hiérarchies cgroups. Les principaux types d'unités pertinents pour la gestion des ressources sont :

  • Slices : Ce sont des conteneurs abstraits pour les unités de service. Les slices forment une hiérarchie, permettant la délégation des ressources. Par exemple, une slice pour les sessions utilisateur peut contenir des slices pour des applications individuelles. Systemd crée automatiquement des slices pour les services système, les sessions utilisateur et les machines virtuelles/conteneurs.
  • Scopes : Ceux-ci sont généralement utilisés pour des groupes de processus temporaires ou créés dynamiquement, souvent associés à des sessions utilisateur ou des services système qui ne sont pas gérés comme des unités de service complètes. Ils sont transitoires et existent tant que les processus qu'ils contiennent sont en cours d'exécution.
  • Services : Ce sont les unités fondamentales pour gérer les démons et les applications. Lorsqu'une unité de service est démarrée, systemd place ses processus dans une hiérarchie cgroup, généralement dans une slice. Les limites de ressources peuvent être directement appliquées aux unités de service.

La hiérarchie par défaut de Systemd ressemble souvent à ceci :

-.slice (Slice racine)
  |- system.slice
  |  |- <nom_service>.service
  |  |- autre-service.service
  |  ... 
  |- user.slice
  |  |- user-1000.slice
  |  |  |- session-c1.scope
  |  |  |  |- <application>.service (si démarré par l'utilisateur)
  |  |  |  ...
  |  |  ...
  |  ... 
  |- machine.slice (pour les VM/conteneurs)
  ... 

Appliquer des limites de ressources avec les fichiers d'unités Systemd

Systemd vous permet de spécifier des limites de ressources cgroups directement dans les fichiers .service, .slice ou .scope. Ces directives sont placées respectivement sous les sections [Service], [Slice] ou [Scope].

Limites CPU

Les principales directives pour le contrôle des ressources CPU sont :

  • CPUQuota= : Limite le temps CPU total que l'unité peut utiliser. Ceci est spécifié en pourcentage (par exemple, 50% pour un demi-cœur CPU) ou une fraction d'un cœur CPU (par exemple, 0.5). Il est également possible de spécifier une valeur en microsecondes par période. La période par défaut est de 100 ms.
  • CPUWeight= : Définit un poids relatif pour le temps CPU sur les systèmes cgroup v2. Une unité avec un poids plus élevé obtient une part plus importante en cas de contention, mais elle ne réserve pas de CPU lorsque la machine est inactive.
  • CPUShares= : Ancien poids de l'ère cgroup v1. Préférez CPUWeight= sur les distributions modernes, sauf si vous savez que vous avez besoin de la compatibilité v1.
  • CPUQuotaPeriodSec= : Définit la période pour CPUQuota. Par défaut, 100ms.

Exemple : Limiter un serveur web à 75% d'un cœur CPU :

Créez ou modifiez un fichier de service, par exemple /etc/systemd/system/mywebapp.service :

[Unit]
Description=Mon Application Web

[Service]
ExecStart=/usr/bin/mywebapp
User=webappuser
Group=webappgroup

# Limiter à 75% d'un cœur CPU
CPUQuota=75%

[Install]
WantedBy=multi-user.target

Après avoir créé ou modifié le fichier de service, rechargez le démon systemd et redémarrez le service :

sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service

Limites Mémoire

Les limites mémoire sont contrôlées par des directives telles que :

  • MemoryMax= : Définit une limite dure sur la quantité de mémoire que les processus de l'unité peuvent consommer. Cela peut être spécifié en octets ou avec des suffixes comme K, M, G, T (par exemple, 512M).
  • MemoryLimit= : Ancienne orthographe conservée sur certains systèmes pour la compatibilité. Préférez MemoryMax= sur les versions modernes de systemd.
  • MemoryHigh= : Définit une limite souple. Lorsque cette limite est approchée, la récupération de mémoire (swap) est déclenchée de manière plus agressive, mais la limite dure n'est pas encore appliquée.
  • MemorySwapMax= : Limite la quantité d'espace swap que l'unité peut utiliser.

Exemple : Limiter une base de données à 2 Go de RAM :

Créez ou modifiez un fichier de service, par exemple /etc/systemd/system/mydb.service :

[Unit]
Description=Mon Service de Base de Données

[Service]
ExecStart=/usr/bin/mydb
User=dbuser
Group=dbgroup

# Limiter la mémoire à 2 Gigaoctets
MemoryMax=2G

[Install]
WantedBy=multi-user.target

Rechargez et redémarrez :

sudo systemctl daemon-reload
sudo systemctl restart mydb.service

Limites E/S

La limitation des E/S peut être contrôlée à l'aide de directives comme :

  • IOWeight= : Définit un poids relatif pour les opérations d'E/S. Des valeurs plus élevées donnent une priorité d'E/S plus élevée. La plage est de 1 à 1000 (par défaut 500).
  • IOReadBandwidthMax= : Limite la bande passante de lecture des E/S. Spécifié comme [<périphérique>] <octets_par_seconde>. Par exemple, IOReadBandwidthMax=/dev/sda 100M limite les opérations de lecture sur /dev/sda à 100 Mo/s.
  • IOWriteBandwidthMax= : Limite la bande passante d'écriture des E/S. Format similaire à IOReadBandwidthMax.

Exemple : Limiter un service de traitement en arrière-plan à 50 Mo/s sur un disque spécifique :

Créez ou modifiez un fichier de service, par exemple /etc/systemd/system/batchproc.service :

[Unit]
Description=Service de Traitement par Lots

[Service]
ExecStart=/usr/bin/batchproc
User=batchuser
Group=batchgroup

# Limiter les opérations d'écriture à 50 Mo/s sur /dev/sdb
IOWriteBandwidthMax=/dev/sdb 50M

# Donner une priorité de lecture modérée
IOWeight=200

[Install]
WantedBy=multi-user.target

Rechargez et redémarrez :

sudo systemctl daemon-reload
sudo systemctl restart batchproc.service

Gérer et surveiller les Cgroups

Systemd fournit des outils pour inspecter et gérer les cgroups associés à vos unités.

Inspecter l'état des Cgroups

La commande systemctl status fournit des informations sur l'appartenance d'une unité à un cgroup et l'utilisation des ressources.

systemctl status mywebapp.service

Recherchez les lignes indiquant le chemin du cgroup. Par exemple :

● mywebapp.service - Mon Application Web
     Loaded: loaded (/etc/systemd/system/mywebapp.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-10-27 10:00:00 UTC; 1 day ago
       Docs: man:mywebapp(8)
   Main PID: 12345 (mywebapp)
      Tasks: 5 (limit: 4915)
     Memory: 15.5M
        CPU: 2h 30m 15s
      CGroup: /system.slice/mywebapp.service
              └─12345 /usr/bin/mywebapp

Vous pouvez également inspecter directement le système de fichiers cgroup :

systemd-cgls # Affiche la hiérarchie cgroup gérée par systemd
systemd-cgtop # Similaire à top, mais pour les cgroups

Pour voir les limites spécifiques appliquées au cgroup d'un service :

# Pour les limites mémoire sur un hôte cgroup v2 typique
cat /sys/fs/cgroup/system.slice/mywebapp.service/memory.max

# Pour les limites CPU
cat /sys/fs/cgroup/system.slice/mywebapp.service/cpu.max

Les chemins exacts et les noms de fichiers varient selon la version de cgroup et la distribution. Sur les systèmes cgroup v1, des chemins spécifiques au contrôleur comme /sys/fs/cgroup/memory/... peuvent encore exister. Sur les systèmes cgroup v2, la hiérarchie unifiée sous /sys/fs/cgroup/... est la vue normale.

Modifier les limites des Cgroups à la volée

Bien qu'il soit recommandé de définir les limites dans les fichiers d'unités, vous pouvez les ajuster temporairement à l'aide de systemctl set-property :

sudo systemctl set-property mywebapp.service CPUQuota=50%

Selon la version de systemd et les indicateurs, set-property peut écrire un drop-in sous /etc/systemd/system.control/ pour les propriétés persistantes. Utilisez systemctl cat mywebapp.service et systemctl show mywebapp.service -p CPUQuota -p MemoryMax pour confirmer ce qui s'est passé. Pour l'infrastructure en tant que code et la revue par les pairs, un drop-in d'unité explicite est généralement plus clair.

Slices pour la délégation de ressources

Les slices sont puissantes pour gérer des groupes de services ou d'applications. Vous pouvez définir des limites de ressources sur une slice, et tous les services ou scopes de cette slice hériteront ou seront contraints par ces limites.

Exemple : Créer une slice dédiée pour les travaux par lots gourmands en ressources :

Créez un fichier de slice, par exemple /etc/systemd/system/batch.slice :

[Unit]
Description=Slice de Traitement par Lots

[Slice]
# Limiter le CPU total pour tous les travaux de cette slice à 1 cœur
CPUQuota=100%
# Limiter la mémoire totale à 4 Go
MemoryMax=4G

Maintenant, vous pouvez configurer les services pour qu'ils s'exécutent dans cette slice en utilisant la directive Slice= dans leurs fichiers .service :

[Unit]
Description=Travail par Lots Spécifique

[Service]
ExecStart=/usr/bin/mybatchjob

# Placer ce service dans la slice batch.slice
Slice=batch.slice

[Install]
WantedBy=multi-user.target

Rechargez systemd, activez/démarrez la slice si nécessaire (bien qu'elle soit souvent activée implicitement), et démarrez le service.

sudo systemctl daemon-reload
sudo systemctl start mybatchjob.service

Cette approche vous permet de regrouper des processus connexes et de gérer leur consommation collective de ressources.

Bonnes pratiques et considérations

  • Commencez par des limites progressives : Lorsque vous définissez des limites, commencez par des valeurs prudentes et augmentez-les progressivement si nécessaire. Des limites agressives peuvent déstabiliser les applications.
  • Surveillez : Surveillez régulièrement l'utilisation des ressources de votre système et l'impact de vos paramètres cgroup. Des outils comme systemd-cgtop, htop, top et iotop sont inestimables.
  • Comprenez cgroup v1 vs. v2 : Systemd prend en charge à la fois cgroup v1 et v2. Bien que de nombreuses directives soient similaires, v2 offre une hiérarchie unifiée et quelques différences de comportement. Assurez-vous de savoir quelle version votre système utilise si vous rencontrez des problèmes complexes.
  • Priorisation vs. Limites dures : Utilisez CPUWeight pour la priorisation lorsque les ressources sont rares, et CPUQuota pour des limites strictes. De même, MemoryHigh est pour la pression avant la limite dure, et MemoryMax est la limite dure.
  • Service vs. Slice : Utilisez les unités de service pour les applications individuelles et les slices pour gérer des groupes d'applications connexes ou des pools de ressources.
  • Documentation : Documentez clairement les limites de ressources appliquées aux services critiques, en particulier dans les environnements de production.
  • Tueur OOM : Soyez conscient que si un processus dépasse sa limite MemoryMax, le tueur Out-Of-Memory (OOM) du noyau peut le terminer, même s'il est dans un cgroup. Systemd peut gérer le comportement du tueur OOM pour des cgroups spécifiques à l'aide de directives comme OOMPolicy=.

Une façon plus sûre de déployer les limites

Commencez par l'observation. Avant d'ajouter des limites, regardez comment le service se comporte pendant la charge normale et pendant sa pire charge attendue :

systemctl status mywebapp.service
systemd-cgtop
systemctl show mywebapp.service -p MemoryCurrent -p CPUUsageNSec -p TasksCurrent

Pour la mémoire, un bon premier pas est souvent MemoryHigh= plutôt que MemoryMax= :

[Service]
MemoryHigh=1G
MemoryMax=1536M

MemoryHigh= indique au noyau d'appliquer une pression avant que le service n'atteigne le plafond dur. MemoryMax= est le mur. Si le processus le franchit et que la mémoire ne peut pas être récupérée, le noyau peut tuer un processus dans le cgroup. Cela peut être exactement ce que vous voulez pour un travail incontrôlé, mais c'est une mauvaise surprise pour une base de données à moins que vous ne l'ayez prévu.

Pour le CPU, décidez si vous voulez l'équité ou une limite dure :

[Service]
CPUWeight=50

Cela abaisse la priorité en cas de contention mais permet toujours au service d'utiliser le CPU inactif. Pour les travaux en arrière-plan, c'est souvent mieux qu'un quota.

[Service]
CPUQuota=200%

Cela limite le service à environ deux cœurs CPU de temps. C'est utile pour un processeur par lots bruyant, mais cela peut nuire aux applications sensibles à la latence si les threads de travail sont limités pendant les pics de trafic.

Pour les explosions de processus, ajoutez une limite de tâches :

[Service]
TasksMax=200

Cela protège l'hôte des tempêtes de fork accidentelles. Réglez-le suffisamment haut pour les nombres de threads normaux. Les charges de travail Java, base de données et de type navigateur peuvent utiliser plus de tâches que vous ne le pensez.

Drop-Ins au lieu de modifier les unités du fournisseur

Évitez de modifier les fichiers d'unités fournis par les paquets sous /usr/lib/systemd/system/ ou /lib/systemd/system/. Utilisez un drop-in :

sudo systemctl edit mywebapp.service

Puis ajoutez :

[Service]
MemoryHigh=1G
MemoryMax=1536M
CPUWeight=80

Après avoir enregistré :

sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service
systemctl cat mywebapp.service

systemctl cat affiche l'unité du fournisseur et votre remplacement ensemble. Cela rend le débogage futur beaucoup plus facile car la configuration active est visible en une seule commande.

Slices pour les équipes, les locataires et les classes de charge de travail

Les slices deviennent utiles lorsque vous arrêtez de penser un service à la fois. Supposons qu'un hôte exécute l'API, un générateur de rapports et plusieurs workers d'importation. Vous ne vous souciez peut-être pas de savoir quel worker d'importation utilise le CPU, mais vous vous souciez que tout le travail d'importation ensemble ne puisse pas affamer l'API.

Créez une slice :

# /etc/systemd/system/import.slice
[Unit]
Description=Charges de travail d'importation et de remblayage

[Slice]
CPUWeight=30
MemoryHigh=4G
MemoryMax=5G

Placez les services d'importation à l'intérieur :

[Service]
Slice=import.slice
ExecStart=/usr/local/bin/import-worker

Maintenant, le groupe a une pression partagée. C'est plus propre que de mettre des limites dures séparées sur chaque worker et d'espérer que les calculs fonctionnent toujours après que quelqu'un en ait ajouté un nouveau.

Il y a un détail de nommage qui surprend les gens : les noms de slice encodent la hiérarchie. customer-a.slice est une slice de premier niveau. customer-a-batch.slice n'est pas un enfant de customer-a.slice ; c'est juste un autre nom de premier niveau. Les slices hiérarchiques utilisent des tirets comme séparateurs d'une manière spécifique, alors lisez systemd.slice(5) avant de concevoir un grand arbre de slices.

Ce que les limites de ressources ne peuvent pas résoudre

Les cgroups peuvent empêcher une charge de travail de submerger l'hôte, mais ils ne peuvent pas rendre une machine sous-dimensionnée rapide. Si une base de données a besoin de plus de mémoire pour son ensemble de travail que vous ne lui en autorisez, elle peut passer plus de temps à récupérer de la mémoire ou échouer sous charge. Si une API a besoin de temps de réponse courts, un quota CPU strict peut créer des délais de limitation qui ressemblent à une latence aléatoire. Si un périphérique de stockage est déjà saturé, les poids E/S peuvent améliorer l'équité mais pas créer de débit.

Considérez les limites comme des garde-fous. Associez-les à des paramètres au niveau de l'application : tailles de tampon de base de données, nombres de workers, concurrence de file d'attente, limites de tas JVM, GOMEMLIMIT Go, indicateurs mémoire Node, ou tout ce que votre environnement d'exécution fournit. La meilleure configuration est généralement les deux : l'application connaît son propre modèle de mémoire et de concurrence, et systemd protège le reste de la machine si ce modèle se brise.

Le modèle mental à conserver

Utilisez les limites au niveau du service pour un démon. Utilisez les limites au niveau de la slice pour un groupe de charges de travail connexes. Utilisez les poids lorsque vous voulez une priorité en cas de contention. Utilisez les quotas et les limites mémoire dures lorsque vous avez besoin d'une frontière ferme et que vous êtes prêt à en assumer les conséquences. Vérifiez les propriétés effectives avec systemctl show, surveillez le comportement avec systemd-cgtop, et conservez la configuration dans des drop-ins ou des fichiers d'unités que votre équipe peut examiner.