Qu’est-ce que les cgroups ?
A qui s’adresse cet article ?
Cet article s’adresse aux gens qui ont une connaissance relativement avancée de Docker (ou autre technique de conteneurisation) en tant qu’utilisateur, mais ne connaissent pas les mécanismes internes mis en oeuvre pour leur fonctionnement. Plus en précisemment, pour ceux qui ne savent pas ce que sont (voire même n’ont jamais entendu parler) des cgroups.
Il faut avoir une vague idée de comment le kernel linux fonctionne pour pouvoir comprendre facilement cet article.
Introduction
Le but de cet article est de fournir une petite introduction sommaire aux cgroups. N’étant moi-même pas un expert de Docker ou du kernel Linux, mais un simple amateur enthousiaste, j’ai récemment eu l’envie de découvrir un peu plus le fonctionnement interne de Docker et des conteneurs, après plusieurs années à utiliser Docker sans réellement comprendre ce qu’il se passe sous le capot. Si les noms d’overlayfs
, cgroups
, capabilities
et namespace
m’étaient familier, je n’avais cependant qu’une vague connaissance de ces mécanismes. J’ai donc cherché à en savoir plus sur les cgroups, pour répondre à quelques questions:
- Qu’est-ce que c’est ?
- A quoi ça sert ?
- A quoi ça sert pour Docker et les conteneurs ?
- Comment les manipuler ?
Présentation brève (style wikipédia) des cgroups
Le terme cgroups
signifie control groups
. La documentation de Linux précise qu’il existe la forme singulière, et que cgroups
ne doit jamais être capitalisé. En réalité, cgroup
est une espèce de raccourci pour cgroup controller
.
Cette fonctionnalité a été ajouté à Linux vers la fin 2007, pour la version 2.6.24.
Depuis la version 4.5 de linux, les cgroups existent également en version 2, mais cet article n’en parlera pas.
Le but des cgroups est donc d’établir un groupe de process sur lequel on va pouvoir excercer un contrôle via certaines règles et paramètres. Une des fonctionnalité offerte par les cgroups est la possibilité d’organiser ces groupes de process selon une hiérarchie.
Ainsi, un cgroup controller permet d’effectuer un contrôle sur un type de ressource, par exemple le cgroup controller memory permet de limiter l’utilisation totale de RAM ou de swap pour les process dans ce cgroup.
Un process peut être dans plusieurs cgroups, pour être sous le joug de plusieurs limites de différents types.
Le système hiérarchique des cgroups permet de limiter les ressources de plusieurs cgroups d’un coup, un cgroup situé en aval d’un autre ne peut dépasser les limites imposées par ce dernier.
Un peu de concret
man mount(8)
Les cgroups sont en réalité exposé via un filesystem particulier interne (de type … cgroup
). Pour les manipuler (en créer, les modifier, rajouter des process, etc), il faut ainsi passer par le filesystem.
Avec les OS récents disposants de systemd, ce dernier défini automatiquement des points de montages et monte les pseudo-filesystems requis pour utiliser tous les cgroups (de la version 1).
Ainsi, sur ma machine :
On note que cgroup
ici est au singulier et non au pluriel. La documentation (man 7 cgroups
) explique que le terme cgroup
au singulier désigne le control group controller
, là où le pluriel désigne la fonctionnalité dans son ensemble.
On remarque que tous les cgroups montés sont chacun monté dans un dossier qui porte le nom de l’option de montage du fs. Ainsi on a un cgroup
dans le dossier blkio
avec l’option de montage blkio
. Comme on peut le deviner assez facilement, c’est cette option de montage qui permet d’activer un cgroup controller.
Il est possible de préciser plusieurs options de montage avec la commande mount en les séparants par des virgules. Si l’on précise plsuieurs cgroup controllers lors du montage, alors on obtiendra un cgroup disposant de chacun de ces controllers. On remarque que systemd l’utilise dans le dossier cpu,cpuacct
par exemple.
Créons notre cgroup
Préparons le terrain
Maintenant qu’on a vu que pour intéragir avec un croup controller, il suffit de monter dans un dossier un fs de type cgroup
et d’activer le cgroup controller
qui nous intéresse via l’option de montage, essayons de créer et manipuler un cgroup.
Tentons d’utiliser le controller memory
pour limiter la RAM d’un process :
Ces commandes sont faites en root
L’interface que nous propose le kernel pour intéragir avec un cgroup est donc remplie de plusieurs fichiers. Si on les catégorise, on a:
- Ceux qui commencent par
memory
. Ce sont les interfaces propre au controller que l’on a activé. - Ceux qui commencent par
cgroup
. Ce sont les interfaces communes à chacun des cgroups et qui permettent d’intéragir avec le groupe. - 3 fichiers un peu à part,
notify_on_release
,release_agent
ettasks
. Les deux premiers sont des mécanismes de notification, et le troisième sera évoqué un peu plus bas dans cet article. - Différents dossiers. Ce sont en réalité d’autre cgroups créé automatiquement par systemd et docker dans cet exemple.
Constatation immédiate
On peut déjà remarquer que même si nous venons de créer notre dossier dans /tmp
, et qu’on vient à peine de mount le cgroup memory, il y a déjà des cgroups fils (docker
, system.slice
, user.slice
, etc) qui sont présents. La raison est assez simple : les cgroups sont uniques au système. Mount un cgroup quelque part permet juste d’intéragir avec ce système, qui est unique. Ainsi, Docker et Systemd ont pu déjà créer leurs cgroups au démarrage de ma machine, et utilisant la même interface mais mount ailleurs (/sys/fs/cgroup/memory
en l’occurence, comme vu plus haut).
La création
La création en elle-même est triviale :
Grâce à un simple mkdir
, nous venons de créer un groupe appelé myapp
de type memory
.
Configuration de notre cgroup et ajout de process
Pour utiliser le cgroups, on va premièrement le configurer brièvement :
En écrivant la valeur 104857600 dans le fichier memory.limit_in_bytes
, on configure ce cgroup pour qu’il empêche la somme de la mémoire utilisée par les process rattachés à lui-même de dépasser 100Kib.
Ajoutons notre shell courant à ce cgroup :
En écrivant 0 dans cgroups.procs
on demande au kernel de déplacer le process actuel (donc mon shell) dans ce cgroup. L’opération revient à echo $$ > cgroups.procs
.
On vérifie ensuite quels sont les process qui sont dans ce cgroups avec un cat, et on tombe bien sur deux valeurs, le PID de mon shell, et le PID de cat
que je viens d’invoquer. En effet, comme cat
est mon process fils, il est par défaut dans le même cgroup que moi-même. Refaire un autre cat
nous donneras également “173423” mais le deuxième PID sera différent.
Test du cgroup
Maintenant que nous sommes dans un cgroup, et que celui ci nous impose une limite de RAM de 100Kib, essayons de voir ce qu’il se passe quand on la brise.
Dans un autre shell (important !), je prépare un petit code en C que je vais compiler :
Ce script a pour but d’allouer plein de mémoire jusqu’à ce qu’il n’y en ai plus de disponible, puis de print un message d’erreur et d’attendre de se faire tuer.
Lancons le programme avec notre shell soumis au cgroup myapp
:
Notre process semble se faire tuer avant de pouvoir print ou de rentrer dans sa boucle infini.
Quelqu’un d’un peu familier avec le fonctionnement de linux va immédiatement suspecter l’OOM-killer d’être passé par là :
En effet, notre programme s’est fait tuer avant de pouvoir print quoi que ce soit.
En regardant rapidement dans notre cgroup, on découvre le fichier memory.oom_control
. Écrivons “1” dedans pour désactiver l’OOM killer pour ce cgroup, et retentons notre expérience :
Le programme tourne à l’infini, mais n’affiche rien. De plus, mon CPU n’est pas en train de cramer. Explorons un peu dans un autre shell :
Le D+
sur la ligne du process indique que celui-ci est en état uninterruptable sleep (D), et au premier plan d’un process group (+). En regardant dans la documentation du kernel, on peut effectivement lire :
You can disable the OOM-killer by writing "1" to memory.oom_control file, as:
#echo 1 > memory.oom_control
If OOM-killer is disabled, tasks under cgroup will hang/sleep
in memory cgroup's OOM-waitqueue when they request accountable memory.
For running them, you have to relax the memory cgroup's OOM status by
* enlarge limit or reduce usage.
To reduce usage,
* kill some tasks.
* move some tasks to other group with account migration.
* remove some files (on tmpfs?)
Then, stopped tasks will work again.
Le comportement actuel est donc celui attendu, notre process a épuisé la mémoire disponible et est donc bloqué.
Conclusion
Les cgroups sont en réalité assez facile à comprendre et manipuler grâce à leur interface assez simple. Cependant, derrière cette apparente simplicité se cache beaucoup de complexité dans le kernel, et quelques incohérences (certains articles sur internet expliquerons mieux que moi pourquoi).
Ces mécanismes sont néanmoins à la base du fonctionnement d’outils de conteneurisation que l’on connait et utilise régulièrement aujourd’hui.
Il existe une volonté de faire bouger les cgroups vers une nouvelle interface, avec un fonctionnement assez différent, les cgroups v2, qui feront surement l’objet d’un nouvel article de blog.
Leave a Comment
Your email address will not be published. Required fields are marked *