Internos del runtime: cgroups, runc y dónde encaja containerd
Qué aísla y limita realmente a un contenedor, desde la función del kernel hasta el daemon con el que habla Kubernetes.
“Contenedor” describe un conjunto de funciones del kernel de Linux conectadas entre sí, no un objeto del kernel por sí mismo. Esta guía recorre la pila desde abajo: la función del kernel que limita los recursos, la herramienta que ensambla un contenedor a partir de ella y el daemon que gestiona esa herramienta a escala.
cgroups: control de recursos en el kernel
Los grupos de control (cgroups) son una función del kernel de Linux, que data de 2008, para asignar y limitar recursos del sistema, CPU, memoria, E/S, entre un conjunto de procesos. Un tipo de recurso dentro de este sistema se llama controlador (o subsistema), y los procesos que gobierna se organizan en una jerarquía que refleja un sistema de archivos: un cgroup es un directorio, y un proceso se une a él escribiendo su PID en ese directorio.
Dos reglas determinan cómo se componen los cgroups:
- Una sola jerarquía puede tener varios controladores adjuntos, pero un controlador dado (CPU, por ejemplo) solo puede estar adjunto a una jerarquía a la vez.
- Un proceso pertenece exactamente a un cgroup dentro de una jerarquía dada. Moverlo a otro cgroup de esa misma jerarquía lo quita del anterior; nunca está en dos lugares de la misma jerarquía a la vez.
Los cgroups existen en dos versiones, v1 y v2, que difieren principalmente en cómo se relacionan las jerarquías y los controladores entre sí; v2 unifica los controladores bajo una única jerarquía en lugar de permitir la disposición más flexible (y más compleja) que permitía v1. Las distribuciones modernas y Kubernetes usan v2 por defecto.
Por sí solos, los cgroups solo gestionan límites de recursos. No dicen nada sobre sistemas de archivos, formatos de imagen ni sobre aislar la vista de un proceso sobre la red o el árbol de procesos, todo lo cual es lo que hace que algo parezca un contenedor aislado y no solo un proceso con recursos limitados.
runc: ensamblar un contenedor a partir de primitivas del kernel
runc es la implementación de referencia de la runtime spec de OCI: una herramienta de bajo nivel cuyo trabajo completo es tomar un sistema de archivos y un archivo de configuración y convertirlos en un proceso aislado en ejecución. Necesita dos cosas para ejecutar un contenedor:
- Un sistema de archivos raíz (
rootfs): el contenido del sistema de archivos del contenedor, extraído de una imagen. - Un config.json: generado con
runc spec, que describe todo sobre cómo debe ejecutarse el proceso: capabilities de Linux, puntos de montaje, qué namespaces crear o compartir, límites de recursos a aplicar mediante cgroups.
mkdir my-container && cd my-container
mkdir rootfs
# rellena rootfs a partir de las capas del sistema de archivos exportadas de una imagen
runc spec
runc run my-container-id
Si has escrito la especificación de un Pod de Kubernetes, la forma de config.json te resultará familiar: las capabilities, los montajes y la configuración de namespaces son los mismos conceptos subyacentes que Kubernetes expone a través de su propia API, solo que una capa más cerca del kernel. runc es intencionadamente limitado: inicia y detiene el proceso de un contenedor según esa configuración. No tiene concepto de descargar imágenes, de un daemon de larga ejecución ni de hablar con Kubernetes en absoluto.
containerd: gestionar runc a escala
containerd es el daemon que hace todo lo que rodea a runc: descargar y descomprimir imágenes, gestionar los snapshots de almacenamiento en los que se extraen, rastrear los contenedores en ejecución y exponer una API gRPC que pueden usar tanto los clientes directos como Kubernetes (mediante CRI). Es el componente con el que realmente habla el kubelet.
containerd no ejecuta directamente el proceso de tu contenedor, sino que lo delega en un shim, un pequeño proceso de larga vida que se sitúa entre containerd y el proceso real del contenedor. El trabajo del shim es mantener el contenedor en ejecución independientemente del propio containerd, de modo que reiniciar o actualizar el daemon de containerd no mate todos los contenedores en ejecución del nodo. containerd 1.x se distribuía con tres implementaciones de shim; containerd 2.0 eliminó las dos más antiguas, dejando el shim v2 basado en runc como el único, que ya llevaba años siendo el predeterminado.
kubelet ── CRI (gRPC) ──▶ containerd ── shim ──▶ runc ──▶ proceso del contenedor
│
└── gestiona imágenes, snapshots, almacenamiento
Como es una arquitectura en capas, containerd no está atado específicamente a runc. Un shim puede dar acceso a un motor de ejecución completamente distinto, gVisor para un sandboxing más fuerte, Kata Containers para aislamiento a nivel de VM, etc., mientras que la gestión de imágenes de containerd, su implementación de CRI y todo lo que mira a Kubernetes siguen igual.
Juntando todo el recorrido
El contenedor de un Pod, de principio a fin: el kubelet envía una petición CRI a containerd. containerd descarga la imagen si aún no está presente, la descomprime en un snapshot en el sistema de archivos del nodo y cede el control a un shim. El shim invoca a runc con un config.json generado que apunta a ese snapshot como rootfs. runc crea los namespaces de Linux, aplica los límites de cgroup a partir de los requests y limits de recursos del Pod e inicia el proceso. A partir de ahí, el shim supervisa el proceso en ejecución y reporta su estado hacia arriba, a través de containerd, hasta el kubelet, que es lo que en última instancia lee kubectl get pods.
Cada capa resuelve un problema: los cgroups limitan los recursos, runc ensambla un contenedor compatible con OCI a partir de un rootfs y una configuración, los shims mantienen ese contenedor supervisado independientemente del daemon, y containerd orquesta todo ello y expone la API que Kubernetes necesita. Ninguna de estas piezas hace el trabajo de las otras, que es exactamente lo que hace posible cambiar runc por un runtime de bajo nivel distinto sin tocar nada por encima de la capa del shim.
Guías populares
-
De Docker a containerd: una breve historia de los runtimes de Kubernetes Por qué Kubernetes dejó de hablar directamente con Docker y qué es realmente containerd en relación con él. -
Las herramientas CLI de containerd: ctr, nerdctl y crictl Tres herramientas de línea de comandos, tres trabajos distintos: cuál usar y cuándo.