Hasta ahora hemos hablado mucho de contenedores, como crearlos y administrarlos, como usar orquestadores de contenedores como Kubernetes, etc. Pero ¿en que se basan soluciones como Docker para la creación de contenedores?
Basicamente aprovechan características del kernel de Linux que proporcionan capacidades para limitar los recursos disponibles para un proceso o conjunto de procesos. Esta característica del kernel se denomina cgroups o control groups. Por tanto, mediante la definición de cgroups, podemos particionar los recursos del sistema y asignarlos a procesos, asegurando así que ninguno consume más recursos de los necesarios.
El interfaz con esta característica del kernel es el pseudo sistema de archivos cgroupfs, el cual nos permite el control de un cgroup mediante la creación, borrado o renombrado de subdirectorios dentro del mismo.
Adicionalmente, podemos limitar la visibilidad que un grupo de procesos tiene del resto del sistema mediante la definición de namespaces. Un namespace es un conjunto de características del sistema, como los interfaces de red disponibles, los puntos de montaje o la lista de procesos, que aparecerán para los procesos ejecutándose en dicho namespace como los únicos disponibles. Estos recursos solo serán visibles para los procesos dentro del namespace y estarán aislados del resto de posibles namespaces existentes. Los namespaces existentes en el kernel de Linux son los siguientes:
- Mount (mnt). Este namespace controla los puntos de montaje, proporcionando aislamiento a la lista de puntos de montaje que están disponibles para los procesos de un namespace.
- Process ID (pid). Este namepsace controla y aisla el espacio de números de procesos, lo cual permite que diferentes procesos, en diferentes namespaces, tengan el mismo PID.
- Network (net). Mediante este namespace, un conjunto de procesos tendrá sus propios recursos de red, incluyendo dispositivos de red, tabla de rutas, protocolos IPv4 e IPv6, firewall, etc.
- Hostname y nombre de dominio NIS (UTS). Este namespace permite controlar el nombre de host así como el nombre de dominio NIS que verán los procesos ejecutándose dentro del namespace.
- User ID (user). Este namespace controla y aisla el espacio de identificadores de usuario y grupos, permitiendo realizar mapeos entre usuarios y grupos dentro y fuera de un namespace. Es importante tener en cuenta que este namespace incluye las capacidades que tendrán los procesos.
- Interprocess communications (ipc). Este namespace permite el aislamiento de objetos IPC, como colas de mensjaes, entre procesos que pertenezcan a diferentes namespaces.
- Control groups (cgroup). Este namespace permite aislar grupos de control de tal manera que un proceso tenga una jerarquía de grupo de procesos aislada del resto.
- Time. Este namespace proporciona vistas de los relojes del sistema CLOCK_MONOTONIC y CLOCK_BOOTTIME, lo cual permite que los procesos de un namespace tengan diferente fecha y hora que los de otros namespaces.
Pseudo sistema de archivos cgroupfs. |
Creación de un nuevo cgroup. |
Moviendo un proceso a un cgroup. |
Comando systemd-cgtop. |
Modificación del límite de memoria del cgroup mail. |
El cgroup mail. |
Podemos observar que aumenta el número de tareas dentro del cgroup, ya que la conexión que establecemos es manejada por un proceso hijo del que hemos movido al cgroup mail y que la memoria consumida es de 840K, cerca del límite que hemos fijado. Por tanto, si lanzamos varias conexiones simultaneamente conseguiremos lo siguiente:
Error de límite de memoria en cgroup mail. |
Como podemos ver en la salida anterior, el cgroup mail se ha quedado sin memoria, con lo que el kernel ha invocado el OOM killer para liberar memoria. En este caso se ha matado un proceso smtpd, uno de los hijos creado por el proceso master para gestionar una conexión, con lo que el proceso principal continua corriendo.
Revisando la salida del comando systemd-cgtop podemos ver como ahora el cgroup mail tiene un número de tareas y un uso de memoria que está por debajo del límite que hemos marcado:
Estado del cgroup. |
En general el número de tareas coincidirá con el contenido del fichero cgroup.procs dentro del cgroup que hayamos definido, que en este caso es /sys/fs/cgroup/memory/mail.
¿Que podemos hacer con todo esto? pues lo cierto es que, sin pensar en contenedores, podemos asegurarnos de limitar el consumo de recursos en nuestros sistemas en caso de ser necesario llegando al nivel de granularidad de aplicarlo a procesos individuales.
Este ejemplo es muy simple, pero sirve para empezar a hacernos una idea de en que se basan tecnologías como Docker. Como ejemplo de esto, podemos ver que, si arrancamos un contenedor, podemos encontrar su cgroup correspondiente en cada uno de los controladores del sistema.
Levantamos un contenedor, en este caso de una imagen de MySQL. |
Fijándonos en el container ID, podemos ver que tenemos definido un cgroup para dicho contenedor en los diferentes controladores:
Cgroup correspondiente al coontenedor. |
El proceso mysqld dentro del cgroup creado para el contenedor. |
Por último vemos en la salida anterior que el usuario propietario del proceso mysqld es polkitd, lo cual se debe a que en el contenedor el propietario del proceso es el usuario mysql con un UID igual al del usuario polkitd del sistema. Esto es posible gracias al namespace de user IDs que está usando Docker y que permite el aislamiento y reutilización de UIDs entre el cgroup del contenedor y el cgroup del sistema.
En caso de querer eliminar un cgroup, primero es necesario que no exista ningún proceso dentro del mismo y bastará con borrar la estructura de directorios creada o bien usar el comando cgdelete.
Para terminar es importante tener en cuenta que hay dos versiones diferentes de cgroups disponibles en el Kernel de Linux y en este post hemos comentado los aspectos más básicos de la v1 de dicha implementación.