Introduction
En lien avec mon prédécent article 1 et de certaines expérimentations, il m’arrive de vouloir superviser l’activité d’applications conteneurisées. Il se trouve que les métriques des conteneurs sont à prendre avec précautions. Dans cette optique, on va mettre en place un pod pourvu d’un shell, et comprendre pourquoi les utilitaires tels que top ne rendent pas correctement compte de la situation réelle.
Préparation de l’environnement
On se base sur Minikube, en suivant une partie du tutoriel présenté ici 2. Le pod qui exécute un process a une limite de consommation RAM et CPU. Un seul conteneur le composera, basé sur l’image progrium/stress
3.
Voici le manifest K8s qui définit ce petit déploiement.
apiVersion: v1
kind: Pod
metadata:
name: stress-demo
spec:
containers:
- name: stress1
image: progrium/stress
resources:
limits:
cpu: "1.5"
memory: "600M"
requests:
cpu: "1"
memory: "500M"
command: ["stress"]
args: ["--vm", "2", "--vm-bytes", "256M", "--verbose"]
$ kubectl apply -f pod.yml
On veillera à gérer la demande en RAM au regard des limits pour ne pas que le kernel stoppe le processus du conteneur qui en demande trop.
➡ On applique ce manifest pour mettre en place l’environnement, on a un pod qui s’exécute dans le cluster Minikube.
Observations
On déroule les outils classiques de diagnostic, et on note plusieurs différences importantes:
- En faisant appel aux métriques Kubernetes
$ kubectl top pods
NAME CPU(cores) MEMORY(bytes)
stress-demo 1501m 152Mi
- En se plaçant dans le pod pour exécuter des commandes shell
$ kubectl exec -ti stress-demo -- free -h
total used free shared buffers cached
Mem: 7.5G 6.9G 584M 750M 79M 2.8G
$ kubectl exec -ti stress-demo -- top
Tasks: 4 total, 3 running, 1 sleeping, 0 stopped, 0 zombie
%Cpu(s): 7.1 us, 35.6 sy, 0.0 ni, 57.2 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
KiB Mem: 7861644 total, 7327232 used, 534412 free, 82008 buffers
KiB Swap: 2097148 total, 311296 used, 1785852 free. 2895988 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
8 root 20 0 269468 30276 896 R 75.3 0.4 1:31.23 stress
9 root 20 0 269468 206988 896 R 75.0 2.6 1:31.04 stress
1 root 20 0 7320 1536 1536 S 0.0 0.0 0:00.01 stress
17 root 20 0 19864 2176 1920 R 0.0 0.0 0:00.00 top
Les informations rendues par kubectl
et top
attestent de la limite CPU (1.5 CPU utilisé au maximum qui est la somme de 2 fois 75%, sur 2 demandés par le programme de stress). La liste des processus de top
se limite à ceux du conteneur.
Cependant on note une différence majeure entre kubectl
et free
concernant la mémoire utilisée.
➡ Autant l’utilisation CPU semble correspondre entre un diagnostic fait sur le pod et avec les API Kubernetes, autant l’utilisation mémoire ne semble pas alignée.
Solution
Les proc entries font partie de la hiérarchie de dossiers procfs qui sont partiellement altérés dans les environnements de conteneurs. Par exemple, l’utilitaire top
n’a qu’une visibilité partielle sur les processus, tandis que l’utilitaire free
lit le fichier /proc/meminfo
, qui n’est pas transformé et contient les informations de la machine hôte.
# dans le container stress-demo
$ strace free 2>&1 | grep open
[...]
openat(AT_FDCWD, "/proc/meminfo", O_RDONLY) = 3
[...]
Il récupère l’utilisation mémoire non pas pour le conteneur dans lequel on lance la commande, mais pour la machine hôte. Cette commande ne rend donc pas compte de la réalité du conteneur.
La liste de toutes les entrées transformées dans le conteneur dépend de la version de la plateforme de conteneur, des politiques d’accès,… Il n’y a donc pas de liste exhaustive ! La vigilance est donc de vigueur.
➡ Il faudra donc rester critique sur les utilitaires en ligne de commande lancés dans un conteneur, car toutes les sources d’informations ne sont pas forcément intégrées au fonctionnement conteneurisé. On accordera notre confiance aux outils de gestion de conteneurs, tels que kubectl top pods
, ou docker stats
,…
Pour aller plus loin
D’après la documentation de Kubernetes, le concept de cgroups (pour control groups) est utilisé4 pour gérer la distribution des ressources des pods. Les cgroups sont décrits comme un mécanisme d’organisation et de hiérarchie des processus, notamment pour leur assignation des ressources système. Les configurations sont stockées dans des dossiers et des fichiers “d’interfaces”5.
Plongeons dans les cgroups
Succintement, on peut mentionner que :
- Il y a à ce jour deux versions majeures, la
v1
et lav2
6, - L’implémentation des cgroups
v2
est native dans le kernel Linux>= 5.8
, - Pour fonctionner avec K8s7, doit être installée la bibliothèque de gestion de conteneur (container runtime)
containerd >= v1.4
oucri-o >= v1.20
.
Et que pour la version v2
,
- Chaque processus appartient à un seul cgroup,
- Tous les threads d’un même processus appartiennent au même cgroup,
- Lors de la création, les processus sont placés dans le même cgroup que leur processus parent. Ils peuvent être migrés ensuite.
Premièrement, déterminons quelle version notre système utilise :
$ stat -fc %T /sys/fs/cgroup/
cgroup2fs
C’est actuellement la version v2
.
Maintenant créons un pod dans un namespace dédié stressed-ns
.
$ kubectl create namespace stressed-ns
$ kubectl --namespace stressed-ns apply -f pod.yml
Récupérons l’identifiant du processus et inspectons la configuration du système. On filtre la liste des processus en ne gardant que ceux qui contiennent “stress”.
$ ps aux | grep stress
root 37140 0.0 0.0 7320 1664 ? Ss 10:17 0:00 stress --vm 2 --vm-bytes 256M --verbose
root 37153 74.7 2.0 269468 162136 ? R 10:17 4:18 stress --vm 2 --vm-bytes 256M --verbose
root 37154 75.1 3.0 269468 241684 ? R 10:17 4:20 stress --vm 2 --vm-bytes 256M --verbose
antoine 42883 0.0 0.0 11780 2560 pts/0 S+ 10:22 0:00 grep --color=auto stress
Vérifions quand même que ces processus fassent bien partie du pod que l’on configure. Sélectionnons le pid
du processus dont le status est sleeping
.
$ nsenter -t 37140 -u hostname
stress-demo
Parfait, on travaille bien sur un processus conteneurisé.
Lien entre namespace K8s et cgroup
Un des moyens est de récupérer le nom du cgroup associé au pid
:
$ cat /proc/37140/cgroup
0::/system.slice/docker-0fef63ebdbef160d586f3b6e4a271aeb3329030xxxx.scope/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podxxxx.slice/docker-xxxx.scope
On note un chemin, qui est à parcourir dans la hiérarchie /sys/fs/cgroup/
. Allons voir ce qu’il s’y trouve !
Informations d’un cgroup
$ cd /sys/fs/cgroup/system.slice/docker-0fef63ebdbef160d586f3b6e4a271aeb3329030xxxx.scope/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podxxxx.slice/docker-xxxx.scope/
$ ls
cgroup.controllers cgroup.type cpu.uclamp.max hugetlb.2MB.current io.weight memory.pressure misc.events
cgroup.events cpu.idle cpu.uclamp.min hugetlb.2MB.events memory.current memory.reclaim misc.max
cgroup.freeze cpu.max cpu.weight hugetlb.2MB.events.local memory.events memory.stat pids.current
cgroup.kill cpu.max.burst cpu.weight.nice hugetlb.2MB.max memory.events.local memory.swap.current pids.events
cgroup.max.depth cpu.pressure hugetlb.1GB.current hugetlb.2MB.numa_stat memory.high memory.swap.events pids.max
cgroup.max.descendants cpuset.cpus hugetlb.1GB.events hugetlb.2MB.rsvd.current memory.low memory.swap.high pids.peak
cgroup.pressure cpuset.cpus.effective hugetlb.1GB.events.local hugetlb.2MB.rsvd.max memory.max memory.swap.max rdma.current
cgroup.procs cpuset.cpus.partition hugetlb.1GB.max io.max memory.min memory.swap.peak rdma.max
cgroup.stat cpuset.mems hugetlb.1GB.numa_stat io.pressure memory.numa_stat memory.zswap.current
cgroup.subtree_control cpuset.mems.effective hugetlb.1GB.rsvd.current io.prio.class memory.oom.group memory.zswap.max
cgroup.threads cpu.stat hugetlb.1GB.rsvd.max io.stat memory.peak misc.current
➡ On retrouve les fichiers d’interface décrits dans la documentation des cgroups.
On pourra par exemple afficher les informations relatives au CPU et à la mémoire.
L’interprétation des fichiers sera grandement aidée par la consultation de la documentation. C’est ainsi que l’on pourra retrouver la limite CPU:
Inspectons par exemple le fichier cpu.max
, qui d’après la documentation contient la limite d’allocation CPU.
cpu.max: A read-write two value file which exists on non-root cgroups. The default is “max 100000”. The maximum bandwidth limit. It’s in the following format: $MAX $PERIOD which indicates that the group may consume up to $MAX in each $PERIOD duration. “max” for $MAX indicates no limit. If only one number is written, $MAX is updated.
$ cat cpu.max
150000 100000
Pour une période unitaire d’une seconde, ce cgroup peut consommer 1,5 CPU. Mettons à jour la définition du pod pour autoriser 1,8 CPU. On devra supprimer, recréer un pod et réitérer ces dernières étapes pour voir la nouvelle valeur.
$ ps aux | grep stress
$ nsenter ...
$ cd /sys/fs/cgroup/system.slice/...
$ cat cpu.max
180000 100000
➡ On constate que la configuration utilisateur dans K8s se répercute sur la configuration du cgroup.
cgroups et namespaces linux
La distribution des ressources système via les cgroups n’est pas le seul procédé mis en place. Les cgroups sont un type, un sous-ensemble de restrictions, d’isolations concrètes réalisées par les namespaces linux. Et d’autres types sont effectivement appliqués.
On note le double sens du mot namespace, qui définit ici deux concepts différents : l’isolation des ressources dans le contexte de Kubernetes et l’isolation des processus dans le contexte du système Linux.
Vu depuis l’extérieur, on peut lister les namespaces linux actuels, notamment celui contenant le container stress-demo
. On invoque la commande suivante sur le node choisi.
$ lsns | grep stress
4026533745 mnt 3 79723 root stress --vm 2 --vm-bytes 256M --verbose
4026533746 uts 3 79723 root stress --vm 2 --vm-bytes 256M --verbose
4026533747 pid 3 79723 root stress --vm 2 --vm-bytes 256M --verbose
4026533748 cgroup 3 79723 root stress --vm 2 --vm-bytes 256M --verbose
Les processus enfants du parent stress --vm 2 --vm-bytes 256M --verbose
vivront dans le même namespace, tel que mentionné pour la version cgroup v2
. À cette échelle, il n’y a plus de notion de namespace Kubernetes, on ne parle que de cgroup et namespace linux.
On note les différents types de namespace linux: mnt
, uts
, pid
, cgroup
. On peut résumer chaque fonctionnalité de type via la table suivante8.
Namespace | Isolation |
---|---|
cgroup | Informations relatives à chaque cgroup |
ipc | Messages et ressources IPC |
net | Ressources réseau |
mnt | Points de montage de fichiers |
pid | Identification des processus |
time | Fonctionnalité de démarrage et accès aux horloges système |
user | Permissions utilisateurs |
uts | Hostname et nom de domaine NIS |
C’est pour cela que lancer la commande top
dans le conteneur stress-demo
ne montre que les processus du pod.
L’appel aux APIs de namespaces telles que unshare
9 remplit ce rôle en créant ce namespace linux dont la visibilité est limitée. Plus d’info avec man unshare
.
Conclusion
Nous ne descendrons pas jusqu’au kernel, même si une description de l’implémentation est disponible 10, et que cette vidéo 11 reste à ce jour encore très informative.
➡ Le concept de namespace Kubernetes est très puissant pour isoler les pods sur un même système ; cependant leur supervision peut s’avérer délicate si l’on n’a pas conscience des différentes couches de bas en haut : les namespaces linux dont les cgroups sont un sous-ensemble, puis les namespaces Kubernetes.
https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#core-interface-files ↩︎
https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html ↩︎
https://kubernetes.io/docs/concepts/architecture/cgroups/#requirements ↩︎
https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/cgroups.html#how-are-cgroups-implemented ↩︎