Métricas de tiempo de ejecución
Docker stats
Puedes usar el comando docker stats para transmitir en vivo las métricas de tiempo de ejecución de un contenedor. El comando admite métricas de CPU, uso de memoria, límite de memoria y E/S de red.
A continuación se muestra un ejemplo de salida del comando 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 página de referencia de
docker stats tiene más detalles sobre el comando docker stats.
Grupos de control (Control groups)
Los contenedores de Linux dependen de los grupos de control (cgroups) que no solo rastrean grupos de procesos, sino que también exponen métricas sobre el uso de CPU, memoria y E/S de bloques. Puedes acceder a esas métricas y obtener también métricas de uso de red. Esto aplica tanto a contenedores LXC "puros" como a contenedores Docker.
Los grupos de control se exponen a través de un pseudo-sistema de archivos. En las distribuciones modernas, deberías encontrar este sistema de archivos bajo /sys/fs/cgroup. En ese directorio verás múltiples subdirectorios, llamados devices, freezer, blkio, etc. Cada subdirectorio corresponde a una jerarquía de cgroup diferente.
En sistemas más antiguos, los grupos de control podrían estar montados en /cgroup, sin jerarquías distintas. En ese caso, en lugar de ver subdirectorios, verás un conjunto de archivos en ese directorio, y posiblemente algunos directorios correspondientes a los contenedores existentes.
Para averiguar dónde están montados tus grupos de control, puedes ejecutar:
$ grep cgroup /proc/mounts
Enumerar cgroups
El diseño de archivos de cgroups es significativamente diferente entre v1 y v2.
Si /sys/fs/cgroup/cgroup.controllers está presente en tu sistema, estás usando v2; de lo contrario, estás usando v1.
Consulta la subsección que corresponda a tu versión de cgroup.
cgroup v2 se usa de forma predeterminada en las siguientes distribuciones:
- Fedora (desde 31)
- Debian GNU/Linux (desde 11)
- Ubuntu (desde 21.10)
cgroup v1
Puedes mirar en /proc/cgroups para ver los diferentes subsistemas de grupos de control conocidos por el sistema, la jerarquía a la que pertenecen y cuántos grupos contienen.
También puedes mirar /proc/<pid>/cgroup para ver a qué grupos de control pertenece un proceso. El grupo de control se muestra como una ruta relativa a la raíz del punto de montaje de la jerarquía. / significa que el proceso no ha sido asignado a un grupo, mientras que /lxc/pumpkin indica que el proceso es miembro de un contenedor llamado pumpkin.
cgroup v2
En hosts con cgroup v2, el contenido de /proc/cgroups carece de sentido.
Consulta /sys/fs/cgroup/cgroup.controllers para ver los controladores disponibles.
Cambiar la versión de cgroup
Cambiar la versión de cgroup requiere reiniciar todo el sistema.
En sistemas basados en systemd, cgroup v2 se puede habilitar agregando systemd.unified_cgroup_hierarchy=1 a la línea de comandos del kernel.
Para revertir la versión de cgroup a v1, debes establecer systemd.unified_cgroup_hierarchy=0 en su lugar.
Si el comando grubby está disponible en tu sistema (por ejemplo, en Fedora), la línea de comandos se puede modificar de la siguiente manera:
$ sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"
Si el comando grubby no está disponible, edita la línea GRUB_CMDLINE_LINUX en /etc/default/grub y ejecuta sudo update-grub.
Ejecutar Docker en cgroup v2
Docker admite cgroup v2 desde Docker 20.10. Ejecutar Docker en cgroup v2 también requiere que se cumplan las siguientes condiciones:
- containerd: v1.4 o posterior
- runc: v1.0.0-rc91 o posterior
- Kernel: v4.15 o posterior (se recomienda v5.2 o posterior)
Ten en cuenta que el modo cgroup v2 se comporta de forma ligeramente diferente al modo cgroup v1:
- El controlador de cgroup predeterminado (
dockerd --exec-opt native.cgroupdriver) essystemden v2, ycgroupfsen v1. - El modo de espacio de nombres de cgroup predeterminado (
docker run --cgroupns) esprivateen v2, yhosten v1. - La opción de
docker run--oom-kill-disablese descarta en v2.
Encontrar el cgroup para un contenedor dado
Para cada contenedor, se crea un cgroup en cada jerarquía. En sistemas antiguos con versiones anteriores de las herramientas de usuario de LXC, el nombre del cgroup es el nombre del contenedor. Con versiones más recientes de las herramientas de LXC, el cgroup es lxc/<nombre_contenedor>.
Para contenedores Docker que usan cgroups, el nombre del cgroup es el ID completo o ID largo del contenedor. Si un contenedor aparece como ae836c95b4c3 en docker ps, su ID largo podría ser algo como ae836c95b4c3c9e9179e0e91015512da89fdec91612f63cebae57df9a5444c79. Puedes buscarlo con docker inspect o docker ps --no-trunc.
Uniendo todo para ver las métricas de memoria de un contenedor Docker, echa un vistazo a las siguientes rutas:
/sys/fs/cgroup/memory/docker/<longid>/en cgroup v1, controladorcgroupfs/sys/fs/cgroup/memory/system.slice/docker-<longid>.scope/en cgroup v1, controladorsystemd/sys/fs/cgroup/docker/<longid>/en cgroup v2, controladorcgroupfs/sys/fs/cgroup/system.slice/docker-<longid>.scope/en cgroup v2, controladorsystemd
Métricas de cgroups: memoria, CPU, E/S de bloque
NoteEsta sección aún no se ha actualizado para cgroup v2. Para obtener más información sobre cgroup v2, consulta la documentación del kernel.
Para cada subsistema (memoria, CPU y E/S de bloque), existen uno o más pseudoarchivos que contienen estadísticas.
Métricas de memoria: memory.stat
Las métricas de memoria se encuentran en el cgroup memory. El grupo de control de memoria añade un poco de sobrecarga, porque realiza un seguimiento muy detallado del uso de la memoria en tu host. Por lo tanto, muchas distribuciones optaron por no habilitarlo de forma predeterminada. Generalmente, para habilitarlo, solo tienes que añadir algunos parámetros a la línea de comandos del kernel: cgroup_enable=memory swapaccount=1.
Las métricas están en el pseudoarchivo memory.stat.
Así es como se ve:
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 primera mitad (sin el prefijo total_) contiene estadísticas relevantes para los procesos dentro del cgroup, excluyendo los sub-cgroups. La segunda mitad (mitad con el prefijo total_) incluye también los sub-cgroups.
Algunas métricas son "indicadores" (gauges), o valores que pueden aumentar o disminuir. Por ejemplo, swap es la cantidad de espacio de intercambio utilizado por los miembros del cgroup. Otras son "contadores", o valores que solo pueden aumentar, porque representan ocurrencias de un evento específico. Por ejemplo, pgfault indica el número de fallos de página desde la creación del cgroup.
cache- La cantidad de memoria utilizada por los procesos de este grupo de control que se puede asociar precisamente con un bloque en un dispositivo de bloques. Cuando lees y escribes en archivos en el disco, esta cantidad aumenta. Este es el caso tanto si usas E/S "convencional" (llamadas al sistema
open,read,write) como archivos mapeados (conmmap). También contabiliza la memoria utilizada por los montajes detmpfs, aunque los motivos no están claros. rss- La cantidad de memoria que no corresponde a nada en el disco: pilas, montículos (heaps) y mapas de memoria anónimos.
mapped_file- Indica la cantidad de memoria mapeada por los procesos en el grupo de control. No te da información sobre cuánta memoria se usa, sino que te dice cómo se usa.
pgfault,pgmajfault- Indican el número de veces que un proceso del cgroup provocó un "fallo de página" y un "fallo mayor", respectivamente. Un fallo de página ocurre cuando un proceso accede a una página de memoria virtual que no está mapeada actualmente a un marco de memoria física. Esta es una parte normal de la gestión de memoria. Por ejemplo, un fallo de página ocurre cuando el proceso lee de una zona de memoria que ha sido paginada al espacio de intercambio (swap), o que corresponde a un archivo mapeado en memoria: en ese caso, el kernel carga la página desde el disco y deja que la CPU complete el acceso a la memoria. También ocurre cuando el proceso escribe en una zona de memoria de copia en escritura (copy-on-write): el kernel duplica la página de memoria y reanuda la operación de escritura en la copia propia de la página del proceso. Los fallos "mayores" ocurren cuando el kernel necesita leer datos del disco. Cuando duplica una página existente, o asigna una página vacía, es un fallo normal (o "menor").
swap- La cantidad de espacio de intercambio (swap) utilizada actualmente por los procesos en este cgroup.
active_anon,inactive_anon- La cantidad de memoria anónima que el kernel ha identificado como activa e inactiva respectivamente. La memoria "anónima" es aquella que no está vinculada a páginas de disco. En otras palabras, es el equivalente del contador rss descrito anteriormente. De hecho, la definición misma del contador rss es
active_anon+inactive_anon-tmpfs(donde tmpfs es la cantidad de memoria utilizada por los sistemas de archivostmpfsmontados por este grupo de control). Ahora, ¿cuál es la diferencia entre "activa" e "inactiva"? Las páginas son inicialmente "activas"; a intervalos regulares, el kernel recorre la memoria y etiqueta algunas páginas como "inactivas". Cada vez que se vuelve a acceder a ellas, se vuelven a etiquetar inmediatamente como "activas". Cuando el kernel se está quedando casi sin memoria y llega el momento de paginar al disco, el kernel pagina las páginas "inactivas". active_file,inactive_file- Memoria caché, con activa e inactiva similar a la memoria anon descrita anteriormente. La fórmula exacta es
cache=active_file+inactive_file+tmpfs. Las reglas exactas utilizadas por el kernel para mover páginas de memoria entre los conjuntos activo e inactivo son diferentes de las utilizadas para la memoria anónima, pero el principio general es el mismo. Cuando el kernel necesita recuperar memoria, es más económico recuperar una página limpia (= no modificada) de este grupo, ya que se puede recuperar de inmediato (mientras que las páginas anónimas y las páginas sucias/modificadas deben escribirse primero en el disco). unevictable- La cantidad de memoria que no se puede recuperar; generalmente, representa la memoria que ha sido "bloqueada" con
mlock. A menudo lo utilizan marcos criptográficos para asegurarse de que las claves secretas y otro material sensible nunca se paginen al disco. memory_limit,memsw_limit- No son realmente métricas, sino un recordatorio de los límites aplicados a este cgroup. El primero indica la cantidad máxima de memoria física que pueden usar los procesos de este grupo de control; el segundo indica la cantidad máxima de RAM+swap.
Contabilizar la memoria en la caché de páginas es muy complejo. Si dos procesos en diferentes grupos de control leen el mismo archivo (dependiendo en última instancia de los mismos bloques en el disco), el cargo de memoria correspondiente se divide entre los grupos de control. Esto es conveniente, pero también significa que cuando se termina un cgroup, podría aumentar el uso de memoria de otro cgroup, porque ya no comparten el costo de esas páginas de memoria.
Métricas de CPU: cpuacct.stat
Ahora que hemos cubierto las métricas de memoria, todo lo demás es sencillo en comparación. Las métricas de CPU están en el controlador cpuacct.
Para cada contenedor, un pseudoarchivo cpuacct.stat contiene el uso de CPU acumulado por los procesos del contenedor, desglosado en tiempo de usuario (user) y de sistema (system). La distinción es:
- El tiempo de
usuarioes la cantidad de tiempo que un proceso tiene el control directo de la CPU, ejecutando código del proceso. - El tiempo de
sistemaes el tiempo que el kernel está ejecutando llamadas al sistema en nombre del proceso.
Estos tiempos se expresan en ticks de 1/100 de segundo, también llamados "user jiffies". Hay USER_HZ "jiffies" por segundo, y en sistemas x86, USER_HZ es 100. Históricamente, esto se correspondía exactamente con el número de "ticks" del planificador por segundo, pero la planificación de mayor frecuencia y los kernels sin ticks han hecho que el número de ticks sea irrelevante.
Métricas de E/S de bloque
La E/S de bloque se contabiliza en el controlador blkio.
Diferentes métricas están distribuidas en distintos archivos. Aunque puedes encontrar detalles detallados en el archivo blkio-controller en la documentación del kernel, aquí tienes una lista corta de los más relevantes:
blkio.sectors- Contiene el número de sectores de 512 bytes leídos y escritos por los procesos miembros del cgroup, dispositivo por dispositivo. Las lecturas y escrituras se fusionan en un solo contador.
blkio.io_service_bytes- Indica el número de bytes leídos y escritos por el cgroup. Tiene 4 contadores por dispositivo, porque para cada dispositivo, diferencia entre E/S síncrona vs. asíncrona, y lecturas vs. escrituras.
blkio.io_serviced- El número de operaciones de E/S realizadas, independientemente de su tamaño. También tiene 4 contadores por dispositivo.
blkio.io_queued- Indica el número de operaciones de E/S actualmente en cola para este cgroup. En otras palabras, si el cgroup no está realizando ninguna E/S, esto es cero. Lo contrario no es cierto. Es decir, si no hay E/S en cola, no significa que el cgroup esté inactivo (en cuanto a E/S). Podría estar realizando lecturas puramente síncronas en un dispositivo que de otro modo estaría inactivo, y que por lo tanto puede manejarlas de inmediato, sin encolar. Además, aunque es útil para descubrir qué cgroup está ejerciendo presión sobre el subsistema de E/S, ten en cuenta que es una cantidad relativa. Incluso si un grupo de procesos no realiza más E/S, el tamaño de su cola puede aumentar solo porque la carga del dispositivo aumenta debido a otros dispositivos.
Métricas de red
Las métricas de red no se exponen directamente mediante los grupos de control. Hay una buena explicación para eso: las interfaces de red existen dentro del contexto de los espacios de nombres de red. El kernel probablemente podría acumular métricas sobre paquetes y bytes enviados y recibidos por un grupo de procesos, pero esas métricas no serían muy útiles. Quieres métricas por interfaz (porque el tráfico que ocurre en la interfaz local lo realmente no cuenta). Pero dado que los procesos en un solo cgroup pueden pertenecer a múltiples espacios de nombres de red, esas métricas serían más difíciles de interpretar: múltiples espacios de nombres de red significan múltiples interfaces lo, potencialmente múltiples interfaces eth0, etc.; por lo tanto, no hay una forma sencilla de recopilar métricas de red con grupos de control.
En su lugar, puedes recopilar métricas de red de otras fuentes.
iptables
iptables (o más bien, el framework netfilter del cual iptables es solo una interfaz) puede realizar una contabilidad seria.
Por ejemplo, puedes configurar una regla para contabilizar el tráfico HTTP saliente en un servidor web:
$ iptables -I OUTPUT -p tcp --sport 80
No hay ninguna opción -j o -g, por lo que la regla simplemente cuenta los paquetes que coinciden y pasa a la siguiente regla.
Más tarde, puedes comprobar los valores de los contadores con:
$ iptables -nxvL OUTPUT
Técnicamente, -n no es obligatorio, pero evita que iptables realice búsquedas inversas de DNS, lo cual probablemente sea inútil en este escenario.
Los contadores incluyen paquetes y bytes. Si quieres configurar métricas para el tráfico de contenedores como este, puedes ejecutar un bucle for para agregar dos reglas de iptables por dirección IP del contenedor (una en cada dirección), en la cadena FORWARD. Esto solo mide el tráfico que pasa a través de la capa NAT; también debes añadir el tráfico que pasa a través del proxy de usuario (userland proxy).
Luego, debes revisar esos contadores de forma regular. Si usas collectd, hay un complemento excelente para automatizar la recopilación de contadores de iptables.
Contadores a nivel de interfaz
Dado que cada contenedor tiene una interfaz Ethernet virtual, es posible que quieras comprobar directamente los contadores TX y RX de esta interfaz. Cada contenedor está asociado a una interfaz Ethernet virtual en tu host, con un nombre como vethKk8Zqi. Descubrir qué interfaz corresponde a qué contenedor es, desafortunadamente, difícil.
Pero por ahora, la mejor manera es comprobar las métricas desde dentro de los contenedores. Para lograr esto, puedes ejecutar un ejecutable desde el entorno del host dentro del espacio de nombres de red de un contenedor usando la magia de ip-netns.
El comando ip netns exec te permite ejecutar cualquier programa (presente en el sistema host) dentro de cualquier espacio de nombres de red visible para el proceso actual. Esto significa que tu host puede entrar en el espacio de nombres de red de tus contenedores, pero tus contenedores no pueden acceder al host ni a otros contenedores pares. Sin embargo, los contenedores pueden interactuar con sus subcontenedores.
El formato exacto del comando es:
$ ip netns exec <nsname> <comando...>
Por ejemplo:
$ ip netns exec mycontainer netstat -i
ip netns encuentra el contenedor mycontainer utilizando los pseudoarchivos de espacios de nombres. Cada proceso pertenece a un espacio de nombres de red, un espacio de nombres PID, un espacio de nombres mnt, etc., y esos espacios de nombres se materializan bajo /proc/<pid>/ns/. Por ejemplo, el espacio de nombres de red del PID 42 se materializa mediante el pseudoarchivo /proc/42/ns/net.
Cuando ejecutas ip netns exec mycontainer ..., espera que /var/run/netns/mycontainer sea uno de esos pseudoarchivos (se aceptan enlaces simbólicos).
En otras palabras, para ejecutar un comando dentro del espacio de nombres de red de un contenedor, necesitamos:
- Averiguar el PID de cualquier proceso dentro del contenedor que queramos investigar;
- Crear un enlace simbólico desde
/var/run/netns/<algunnombre>hacia/proc/<elpid>/ns/net - Ejecutar
ip netns exec <algunnombre> ....
Revisa la sección Enumerar cgroups para saber cómo encontrar el cgroup de un proceso dentro del contenedor cuyo uso de red deseas medir. Desde allí, puedes examinar el pseudoarchivo llamado tasks, que contiene todos los PIDs del cgroup (y por lo tanto, en el contenedor). Elige cualquiera de los PIDs.
Uniendo todo, si el "ID corto" de un contenedor se encuentra en la variable de entorno $CID, puedes hacer lo siguiente:
$ 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
Consejos para la recolección de métricas de alto rendimiento
Ejecutar un nuevo proceso cada vez que deseas actualizar las métricas es (relativamente) costoso. Si quieres recopilar métricas a alta resolución y/o en una gran cantidad de contenedores (piensa en 1000 contenedores en un solo host), no querrás iniciar un nuevo proceso cada vez.
Aquí tienes cómo recopilar métricas desde un solo proceso. Necesitas escribir tu recolector de métricas en C (o en cualquier lenguaje que te permita realizar llamadas al sistema de bajo nivel). Debes usar una llamada al sistema especial, setns(), que permite al proceso actual entrar en cualquier espacio de nombres arbitrario. Requiere, sin embargo, un descriptor de archivo abierto al pseudoarchivo del espacio de nombres (recuerda: ese es el pseudoarchivo en /proc/<pid>/ns/net).
Sin embargo, hay un inconveniente: no debes mantener abierto este descriptor de archivo. Si lo haces, cuando el último proceso del grupo de control finalice, el espacio de nombres no se destruirá y sus recursos de red (como la interfaz virtual del contenedor) permanecerán allí para siempre (o hasta que cierres ese descriptor de archivo).
El enfoque correcto sería realizar un seguimiento del primer PID de cada contenedor y volver a abrir el pseudoarchivo de espacio de nombres cada vez.
Recopilar métricas cuando un contenedor finaliza
A veces, no te importa la recopilación de métricas en tiempo real, sino que cuando un contenedor finaliza, quieres saber cuánta CPU, memoria, etc. ha utilizado.
Docker hace esto difícil porque depende de lxc-start, que realiza una limpieza cuidadosa tras de sí. Por lo general, es más fácil recopilar métricas a intervalos regulares, y así es como funciona el complemento LXC de collectd.
Pero, si aún deseas recopilar las estadísticas cuando un contenedor se detiene, aquí tienes cómo:
Para cada contenedor, inicia un proceso de recopilación y muévelo a los grupos de control que deseas monitorear escribiendo su PID en el archivo tasks del cgroup. El proceso de recopilación debe volver a leer periódicamente el archivo tasks para comprobar si es el último proceso del grupo de control. (Si también deseas recopilar estadísticas de red como se explica en la sección anterior, también debes mover el proceso al espacio de nombres de red correspondiente).
Cuando el contenedor finaliza, lxc-start intenta eliminar los grupos de control. Falla, ya que el grupo de control todavía está en uso; pero no pasa nada. Tu proceso debería detectar ahora que es el único que queda en el grupo. ¡Ahora es el momento adecuado para recopilar todas las métricas que necesitas!
Finalmente, tu proceso debe moverse de regreso al grupo de control raíz y eliminar el grupo de control del contenedor. Para eliminar un grupo de control, simplemente aplica rmdir a su directorio. Es contraintuitivo aplicar rmdir a un directorio que todavía contiene archivos; pero recuerda que este es un pseudo-sistema de archivos, por lo que las reglas habituales no se aplican. Una vez realizada la limpieza, el proceso de recopilación puede finalizar de forma segura.