domingo, 27 de septiembre de 2020

Linux namespaces y cgroups

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.
Pues muy bien todo esto, porque visto así no queda excesivamente claro ¿verdad? Veamos como podemos controlar un proceso de sistema operativo, creando un nuevo cgroup y asignándolo al mismo.

Como hemos dicho, los cgroups se controlan mediante subdirectorios dentro del pseudo sistema de ficheros cgroupfs. Este se encuentra accesible en la ruta /sys/fs/cgroup:

Pseudo sistema de archivos cgroupfs.

Cada uno de los puntos de montaje que vemos se corresponde con lo que se denomina un controlador. De momento no vamos a complicarnos con todos los controladores y vamos a crear un nuevo cgroup, dentro del controlador memory, para controlar la cantidad de memoria de los procesos que pasemos a dicho cgroup.
 
Para crear el cgroup solo tenemos que crear un nuevo directorio y especificar el nombre que queramos:
 
Creación de un nuevo cgroup.

Observamos que, al crear el nuevo cgroup, el contenido del directorio hereda el contenido del directorio padre, con lo que en principio este nuevo cgroup es hijo del cgroup principal que se crea cuando arranca el sistema. Por tanto, de momento, podemos decir que este cgroup tiene los mismos límites de memoria que tiene el cgroup principal. Como podemos ver, uno de los ficheros existentes se llama cgroup.procs el cual ahora mismo está vacio ya que, todos los procesos del sistema operativo, están en el cgroup por defecto. ¿Como muevo un proceso a este nuevo cgroup? pues haciendo algo tan sencillo como escribir el PID del proceso que quiero mover en el fichero cgroup.procs. Por ejemplo, para mover el proceso postfix a mi nuevo cgroup:

Moviendo un proceso a un cgroup.
 
Lo cierto es que esto está muy bien, hemos movido un proceso a un cgroup nuevo pero, ¿como puedo comprobar esto? Afortunadamente tenemos una serie de herramientas en el sistema operativo que nos permiten controlar los cgroups activos en el sistema. Una de estas herramientas es systemd-cgtop que nos mostrará el uso de recursos de cada cgroup existente en el sistema, de una manera similar a como lo hace el comando top:

Comando systemd-cgtop.

Como vemos en la salida del comando, el cgroup mail tiene una tarea y de momento no tiene ningún tipo de consumo.

Para tener más información sobre los diferentes cgroups, y no hacerlo todo tan artesanalmente, podemos instalar el paquete libcgroup-tools el cual incluye una serie de herramientas de línea de comandos que nos permitirán manejar los cgroups de una manera más cómoda.

Por ejemplo, si quiero fijar un límite de memoria en el cgroup mail que he creado, puedo hacer lo siguiente:

Modificación del límite de memoria del cgroup mail.

Los mensajes que recibimos relacionados con el resto de controladores se debe a que, cuando hemos creado el cgroup, este solo lo hemos creado bajo el controlador memory ya que lo que queremos es limitar su uso de memoria.

Ahora probemos a realizar una conexión con el servidor SMTP con un simple telnet y veamos que sucede:

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.

Y confirmamos que, efectivamente, se corresponde a nuestro contenedor no solo por el ID del mismo, sino porque dentro del fichero cgroup.procs contiene el PID del proceso mysqld ejecutado por el contenedor:

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.