⚠️ Traduction non officielle - Cette documentation est une traduction communautaire non officielle de Docker.

Métriques d'exécution

Statistiques Docker

Vous pouvez utiliser la commande docker stats pour diffuser en direct les métriques d'exécution d'un conteneur. La commande prend en charge les métriques CPU, utilisation mémoire, limite mémoire, et E/S réseau.

Voici un exemple de sortie de la commande docker stats

$ docker stats redis1 redis2

CONTAINER           CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O
redis1              0.07%               796 KB / 64 MB        1.21%               788 B / 648 B       3.568 MB / 512 KB
redis2              0.07%               2.746 MB / 64 MB      4.29%               1.266 KB / 648 B    12.4 MB / 0 B

La page de référence docker stats a plus de détails sur la commande docker stats.

Groupes de contrôle

Les conteneurs Linux s'appuient sur les groupes de contrôle qui non seulement suivent les groupes de processus, mais exposent aussi des métriques sur l'utilisation du CPU, de la mémoire et des E/S de bloc. Vous pouvez accéder à ces métriques et obtenir aussi des métriques d'utilisation réseau. Ceci est pertinent pour les conteneurs LXC "purs", ainsi que pour les conteneurs Docker.

Les groupes de contrôle sont exposés via un pseudo-système de fichiers. Dans les distributions modernes, vous devriez trouver ce système de fichiers sous /sys/fs/cgroup. Sous ce répertoire, vous voyez plusieurs sous-répertoires, appelés devices, freezer, blkio, et ainsi de suite. Chaque sous-répertoire correspond en fait à une hiérarchie cgroup différente.

Sur les systèmes plus anciens, les groupes de contrôle pourraient être montés sur /cgroup, sans hiérarchies distinctes. Dans ce cas, au lieu de voir les sous-répertoires, vous voyez un tas de fichiers dans ce répertoire, et possiblement quelques répertoires correspondant aux conteneurs existants.

Pour découvrir où vos groupes de contrôle sont montés, vous pouvez exécuter :

$ grep cgroup /proc/mounts

Énumérer les cgroups

La disposition des fichiers des cgroups est significativement différente entre v1 et v2.

Si /sys/fs/cgroup/cgroup.controllers est présent sur votre système, vous utilisez v2, sinon vous utilisez v1. Référez-vous à la sous-section qui correspond à votre version de cgroup.

cgroup v2 est utilisé par défaut sur les distributions suivantes :

  • Fedora (depuis la 31)
  • Debian GNU/Linux (depuis la 11)
  • Ubuntu (depuis la 21.10)

cgroup v1

Vous pouvez regarder dans /proc/cgroups pour voir les différents sous-systèmes de groupes de contrôle connus du système, la hiérarchie à laquelle ils appartiennent, et combien de groupes ils contiennent.

Vous pouvez aussi regarder /proc/<pid>/cgroup pour voir à quels groupes de contrôle un processus appartient. Le groupe de contrôle est affiché comme un chemin relatif à la racine du point de montage de la hiérarchie. / signifie que le processus n'a pas été assigné à un groupe, tandis que /lxc/pumpkin indique que le processus est membre d'un conteneur nommé pumpkin.

cgroup v2

Sur les hôtes cgroup v2, le contenu de /proc/cgroups n'est pas significatif. Voir /sys/fs/cgroup/cgroup.controllers pour les contrôleurs disponibles.

Changer la version de cgroup

Changer la version de cgroup nécessite de redémarrer tout le système.

Sur les systèmes basés sur systemd, cgroup v2 peut être activé en ajoutant systemd.unified_cgroup_hierarchy=1 à la ligne de commande du noyau. Pour revenir à la version cgroup v1, vous devez définir systemd.unified_cgroup_hierarchy=0 à la place.

Si la commande grubby est disponible sur votre système (par exemple sur Fedora), la ligne de commande peut être modifiée comme suit :

$ sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"

Si la commande grubby n'est pas disponible, éditez la ligne GRUB_CMDLINE_LINUX dans /etc/default/grub et exécutez sudo update-grub.

Exécuter Docker sur cgroup v2

Docker prend en charge cgroup v2 depuis Docker 20.10. Exécuter Docker sur cgroup v2 nécessite aussi que les conditions suivantes soient satisfaites :

  • containerd : v1.4 ou ultérieur
  • runc : v1.0.0-rc91 ou ultérieur
  • Noyau : v4.15 ou ultérieur (v5.2 ou ultérieur est recommandé)

Notez que le mode cgroup v2 se comporte légèrement différemment du mode cgroup v1 :

  • Le pilote cgroup par défaut (dockerd --exec-opt native.cgroupdriver) est systemd sur v2, cgroupfs sur v1.
  • Le mode d'espace de noms cgroup par défaut (docker run --cgroupns) est private sur v2, host sur v1.
  • Les drapeaux docker run --oom-kill-disable et --kernel-memory sont ignorés sur v2.

Trouver le cgroup pour un conteneur donné

Pour chaque conteneur, un cgroup est créé dans chaque hiérarchie. Sur les systèmes plus anciens avec des versions plus anciennes des outils LXC utilisateur, le nom du cgroup est le nom du conteneur. Avec des versions plus récentes des outils LXC, le cgroup est lxc/<container_name>.

Pour les conteneurs Docker utilisant cgroups, le nom du cgroup est le ID ou le long ID du conteneur. Si un conteneur apparaît comme ae836c95b4c3 dans docker ps, son long ID pourrait être quelque chose comme ae836c95b4c3c9e9179e0e91015512da89fdec91612f63cebae57df9a5444c79. Vous pouvez le vérifier avec docker inspect ou docker ps --no-trunc.

Mettre tout ensemble pour regarder les métriques de mémoire pour un conteneur Docker, regardez les chemins suivants :

  • /sys/fs/cgroup/memory/docker/<longid>/ sur cgroup v1, pilote cgroupfs
  • /sys/fs/cgroup/memory/system.slice/docker-<longid>.scope/ sur cgroup v1, pilote systemd
  • /sys/fs/cgroup/docker/<longid>/ sur cgroup v2, pilote cgroupfs
  • /sys/fs/cgroup/system.slice/docker-<longid>.scope/ sur cgroup v2, pilote systemd

Métriques de cgroups : mémoire, CPU, E/S de bloc

Note

Cette section n'est pas encore mise à jour pour cgroup v2. Pour plus d'informations à propos de cgroup v2, reportez-vous à la documentation du noyau.

Pour chaque sous-système (mémoire, CPU et E/S de bloc), un ou plusieurs pseudo-fichiers existent et contiennent des statistiques.

Métriques de mémoire : memory.stat

Les métriques de mémoire se trouvent dans le cgroup memory. Le cgroup de contrôle de mémoire ajoute un peu d'overhead, car il fait une très fine comptabilité de l'utilisation de la mémoire sur votre hôte. Par conséquent, de nombreuses distributions ont choisi de ne pas l'activer par défaut. En général, pour l'activer, tout ce que vous avez à faire est d'ajouter quelques paramètres de ligne de commande du noyau : cgroup_enable=memory swapaccount=1.

Les métriques sont dans le pseudo-fichier memory.stat. Voici à quoi cela ressemble :

cache 11492564992
rss 1930993664
mapped_file 306728960
pgpgin 406632648
pgpgout 403355412
swap 0
pgfault 728281223
pgmajfault 1724
inactive_anon 46608384
active_anon 1884520448
inactive_file 7003344896
active_file 4489052160
unevictable 32768
hierarchical_memory_limit 9223372036854775807
hierarchical_memsw_limit 9223372036854775807
total_cache 11492564992
total_rss 1930993664
total_mapped_file 306728960
total_pgpgin 406632648
total_pgpgout 403355412
total_swap 0
total_pgfault 728281223
total_pgmajfault 1724
total_inactive_anon 46608384
total_active_anon 1884520448
total_inactive_file 7003344896
total_active_file 4489052160
total_unevictable 32768

La première moitié (sans le préfixe total_) contient des statistiques pertinentes pour les processus dans le cgroup, à l'exclusion des sous-cgroups. La seconde moitié (avec le préfixe total_) inclut également les sous-cgroups.

Certaines métriques sont des "gauges", ou des valeurs qui peuvent augmenter ou diminuer. Par exemple, swap est la quantité d'espace swap utilisée par les membres du cgroup. D'autres sont des "counters", ou des valeurs qui ne peuvent augmenter, car ils représentent des occurrences d'un événement spécifique. Par exemple, pgfault indique le nombre de défauts de page depuis la création du cgroup.

cache
La quantité de mémoire utilisée par les processus de ce cgroup qui peut être associée précisément à un bloc sur un dispositif de bloc. Lorsque vous lisez et écrivez des fichiers sur disque, cette quantité augmente. C'est le cas si vous utilisez "conventionnel" I/O (open, read, write syscalls) ainsi que des fichiers mappés (avec mmap). Il prend également en compte la mémoire utilisée par les montages tmpfs, bien que les raisons ne sont pas claires.
rss
La quantité de mémoire qui ne correspond à rien sur disque : piles, tas, et cartes mémoire anonymes.
mapped_file
Indique la quantité de mémoire mappée par les processus dans le cgroup. Il ne vous donne aucune information sur la quantité de mémoire utilisée ; il vous dit plutôt comment il est utilisé.
pgfault, pgmajfault
Indiquent le nombre de fois que le processus d'un cgroup a déclenché un "défaut de page" et un "défaut majeur", respectivement. Un défaut de page se produit lorsqu'un processus accède à une partie de son espace mémoire virtuel qui n'existe pas ou n'est protégé. Le premier peut se produire si le processus est défectueux et tente d'accéder à une adresse invalide (il est envoyé un signal SIGSEGV, typiquement le message célèbre Segmentation fault). Le second peut se produire lorsque le processus lit d'une zone mémoire qui a été échangée, ou qui correspond à un fichier mappé : dans ce cas, le noyau charge la page de disque, et laisse le CPU terminer l'accès mémoire. Il peut également se produire lorsque le processus écrit dans une zone mémoire de copie-sur-écriture : de même, le noyau préempt le processus, duplique la page mémoire, et reprend l'écriture sur le processus lui-même copie de la page. Les "défauts majeurs" se produisent lorsque le noyau doit réellement lire les données de disque. Lorsqu'il duplique simplement une page existante, ou alloue une page vide, c'est un défaut régulier (ou "mineur").
swap
La quantité de swap actuellement utilisée par les processus de ce cgroup.
active_anon, inactive_anon
La quantité de mémoire anonyme qui a été identifiée comme respectivement active et inactive par le noyau. "Anonymous" memory is the memory that is not linked to disk pages. In other words, that's the equivalent of the rss counter described above. In fact, the very definition of the rss counter is active_anon + inactive_anon - tmpfs (where tmpfs is the amount of memory used up by tmpfs filesystems mounted by this control group). Now, what's the difference between "active" and "inactive"? Pages are initially "active"; and at regular intervals, the kernel sweeps over the memory, and tags some pages as "inactive". Whenever they're accessed again, they're immediately re-tagged "active". When the kernel is almost out of memory, and time comes to swap out to disk, the kernel swaps "inactive" pages.
active_file, inactive_file
Cache memory, with active and inactive similar to the anon memory above. The exact formula is cache = active_file + inactive_file + tmpfs. The exact rules used by the kernel to move memory pages between active and inactive sets are different from the ones used for anonymous memory, but the general principle is the same. When the kernel needs to reclaim memory, it's cheaper to reclaim a clean (=non modified) page from this pool, since it can be reclaimed immediately (while anonymous pages and dirty/modified pages need to be written to disk first).
unevictable
The amount of memory that cannot be reclaimed; generally, it accounts for memory that has been "locked" with mlock. It's often used by crypto frameworks to make sure that secret keys and other sensitive material never gets swapped out to disk.
memory_limit, memsw_limit
These aren't really metrics, but a reminder of the limits applied to this cgroup. The first one indicates the maximum amount of physical memory that can be used by the processes of this control group; the second one indicates the maximum amount of RAM+swap.

Accounting for memory in the page cache is very complex. If two processes in different control groups both read the same file (ultimately relying on the same blocks on disk), the corresponding memory charge is split between the control groups. It's nice, but it also means that when a cgroup is terminated, it could increase the memory usage of another cgroup, because they're not splitting the cost anymore for those memory pages.

Métriques de CPU : cpuacct.stat

Maintenant que nous avons couvert les métriques de mémoire, tout le reste est simple en comparaison. Les métriques de CPU sont dans le cpuacct contrôleur.

Pour chaque conteneur, un pseudo-fichier cpuacct.stat contient la CPU accumulée par les processus du conteneur, décomposée en user et system time. La distinction est :

  • user time is the amount of time a process has direct control of the CPU, executing process code.
  • system time is the time the kernel is executing system calls on behalf of the process.

Those times are expressed in ticks of 1/100th of a second, also called "user jiffies". There are USER_HZ "jiffies" per second, and on x86 systems, USER_HZ is 100. Historically, this mapped exactly to the number of scheduler "ticks" per second, but higher frequency scheduling and tickless kernels have made the number of ticks irrelevant.

Block I/O metrics

Block I/O is accounted in the blkio controller. Different metrics are scattered across different files. While you can find in-depth details in the blkio-controller file in the kernel documentation, here is a short list of the most relevant ones:

blkio.sectors
Contains the number of 512-bytes sectors read and written by the processes member of the cgroup, device by device. Reads and writes are merged in a single counter.
blkio.io_service_bytes
Indicates the number of bytes read and written by the cgroup. It has 4 counters per device, because for each device, it differentiates between synchronous vs. asynchronous I/O, and reads vs. writes.
blkio.io_serviced
The number of I/O operations performed, regardless of their size. It also has 4 counters per device.
blkio.io_queued
Indicates the number of I/O operations currently queued for this cgroup. In other words, if the cgroup isn't doing any I/O, this is zero. The opposite is not true. In other words, if there is no I/O queued, it doesn't mean that the cgroup is idle (I/O-wise). It could be doing purely synchronous reads on an otherwise quiescent device, which can therefore handle them immediately, without queuing. Also, while it's helpful to figure out which cgroup is putting stress on the I/O subsystem, keep in mind that it's a relative quantity. Even if a process group doesn't perform more I/O, its queue size can increase just because the device load increases because of other devices.

Métriques de réseau

Network metrics aren't exposed directly by control groups. There is a good explanation for that: network interfaces exist within the context of network namespaces. The kernel could probably accumulate metrics about packets and bytes sent and received by a group of processes, but those metrics wouldn't be very useful. You want per-interface metrics (because traffic happening on the local lo interface doesn't really count). But since processes in a single cgroup can belong to multiple network namespaces, those metrics would be harder to interpret: multiple network namespaces means multiple lo interfaces, potentially multiple eth0 interfaces, etc.; so this is why there is no easy way to gather network metrics with control groups.

Instead you can gather network metrics from other sources.

iptables

iptables (or rather, the netfilter framework for which iptables is just an interface) can do some serious accounting.

For instance, you can setup a rule to account for the outbound HTTP traffic on a web server:

$ iptables -I OUTPUT -p tcp --sport 80

There is no -j or -g flag, so the rule just counts matched packets and goes to the following rule.

Later, you can check the values of the counters, with:

$ iptables -nxvL OUTPUT

Technically, -n isn't required, but it prevents iptables from doing DNS reverse lookups, which are probably useless in this scenario.

Counters include packets and bytes. If you want to setup metrics for container traffic like this, you could execute a for loop to add two iptables rules per container IP address (one in each direction), in the FORWARD chain. This only meters traffic going through the NAT layer; you also need to add traffic going through the userland proxy.

Then, you need to check those counters on a regular basis. If you happen to use collectd, there is a nice plugin to automate iptables counters collection.

Interface-level counters

Since each container has a virtual Ethernet interface, you might want to check directly the TX and RX counters of this interface. Each container is associated to a virtual Ethernet interface in your host, with a name like vethKk8Zqi. Figuring out which interface corresponds to which container is, unfortunately, difficult.

But for now, the best way is to check the metrics from within the containers. To accomplish this, you can run an executable from the host environment within the network namespace of a container using ip-netns magic.

The ip-netns exec command allows you to execute any program (present in the host system) within any network namespace visible to the current process. This means that your host can enter the network namespace of your containers, but your containers can't access the host or other peer containers. Containers can interact with their sub-containers, though.

The exact format of the command is:

$ ip netns exec <nsname> <command...>

For example:

$ ip netns exec mycontainer netstat -i

ip netns finds the mycontainer container by using namespaces pseudo-files. Each process belongs to one network namespace, one PID namespace, one mnt namespace, etc., and those namespaces are materialized under /proc/<pid>/ns/. For example, the network namespace of PID 42 is materialized by the pseudo-file /proc/42/ns/net.

When you run ip netns exec mycontainer ..., it expects /var/run/netns/mycontainer to be one of those pseudo-files. (Symlinks are accepted.)

In other words, to execute a command within the network namespace of a container, we need to:

  • Find out the PID of any process within the container that we want to investigate;
  • Create a symlink from /var/run/netns/<somename> to /proc/<thepid>/ns/net
  • Execute ip netns exec <somename> ....

Review Énumérer les cgroups for how to find the cgroup of an in-container process whose network usage you want to measure. From there, you can examine the pseudo-file named tasks, which contains all the PIDs in the cgroup (and thus, in the container). Pick any one of the PIDs.

Putting everything together, if the "short ID" of a container is held in the environment variable $CID, then you can do this:

$ TASKS=/sys/fs/cgroup/devices/docker/$CID*/tasks
$ PID=$(head -n 1 $TASKS)
$ mkdir -p /var/run/netns
$ ln -sf /proc/$PID/ns/net /var/run/netns/$CID
$ ip netns exec $CID netstat -i

Conseils pour la collecte de métriques à haute performance

Exécuter un nouveau processus chaque fois que vous voulez mettre à jour les métriques est (relativement) coûteux. Si vous voulez collecter des métriques à haute résolution, et/ou sur un grand nombre de conteneurs (pensez 1000 conteneurs sur un seul hôte), vous ne voulez pas forker un nouveau processus à chaque fois.

Voici comment collecter des métriques d'un seul processus. Vous devez écrire votre collecteur de métriques en C (ou tout langage qui vous permet de faire des appels système de bas niveau). Vous devez utiliser un appel système spécial, setns(), qui permet au processus actuel d'entrer dans n'importe quel espace de noms arbitraire. Il nécessite, cependant, un descripteur de fichier ouvert vers le pseudo-fichier d'espace de noms (rappelez-vous : c'est le pseudo-fichier dans /proc/<pid>/ns/net).

Cependant, il y a un piège : vous ne devez pas garder ce descripteur de fichier ouvert. Si vous le faites, lorsque le dernier processus du groupe de contrôle se ferme, l' espace de noms n'est pas détruit, et ses ressources réseau (comme l' interface virtuelle du conteneur) restent pour toujours (ou jusqu'à ce que vous fermiez ce descripteur de fichier).

La bonne approche serait de garder une trace du premier PID de chaque conteneur, et de rouvrir le pseudo-fichier d'espace de noms à chaque fois.

Collecter les métriques quand un conteneur se ferme

Parfois, vous ne vous souciez pas de la collecte de métriques en temps réel, mais quand un conteneur se ferme, vous voulez savoir combien de CPU, mémoire, etc. il a utilisé.

Docker rend cela difficile car il s'appuie sur lxc-start, qui nettoie soigneusement après lui-même. Il est généralement plus facile de collecter des métriques à intervalles réguliers, et c'est ainsi que le plugin LXC collectd fonctionne.

Mais, si vous voulez encore rassembler les statistiques quand un conteneur s'arrête, voici comment :

Pour chaque conteneur, démarrez un processus de collecte, et déplacez-le vers les groupes de contrôle que vous voulez surveiller en écrivant son PID dans le fichier tasks du cgroup. Le processus de collecte devrait périodiquement relire le fichier tasks pour vérifier s'il est le dernier processus du groupe de contrôle. (Si vous voulez aussi collecter des statistiques réseau comme expliqué dans la section précédente, vous devriez aussi déplacer le processus vers l'espace de noms réseau approprié.)

Quand le conteneur se ferme, lxc-start tente de supprimer les groupes de contrôle. Il échoue, puisque le groupe de contrôle est encore en utilisation ; mais c'est bien. Votre processus devrait maintenant détecter qu'il est le seul restant dans le groupe. C'est maintenant le bon moment pour collecter toutes les métriques dont vous avez besoin !

Finalement, votre processus devrait se déplacer lui-même vers le groupe de contrôle racine, et supprimer le groupe de contrôle du conteneur. Pour supprimer un groupe de contrôle, juste rmdir son répertoire. C'est contre-intuitif de rmdir un répertoire car il contient encore des fichiers ; mais rappelez-vous que c'est un pseudo-système de fichiers, donc les règles habituelles ne s'appliquent pas. Après que le nettoyage soit fait, le processus de collecte peut se fermer en sécurité.