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
) estsystemd
sur v2,cgroupfs
sur v1. - Le mode d'espace de noms cgroup par défaut (
docker run --cgroupns
) estprivate
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, pilotecgroupfs
/sys/fs/cgroup/memory/system.slice/docker-<longid>.scope/
sur cgroup v1, pilotesystemd
/sys/fs/cgroup/docker/<longid>/
sur cgroup v2, pilotecgroupfs
/sys/fs/cgroup/system.slice/docker-<longid>.scope/
sur cgroup v2, pilotesystemd
Métriques de cgroups : mémoire, CPU, E/S de bloc
NoteCette 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 (avecmmap
). Il prend également en compte la mémoire utilisée par les montagestmpfs
, 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èbreSegmentation 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 bytmpfs
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é.