Guía Completa de Cgroups de Systemd para Limitación y Aislamiento de Recursos

Usa cgroups, slices y propiedades de unidades de systemd para limitar CPU, memoria y E/S sin editar archivos cgroup sin procesar.

Guía Completa de Cgroups de Systemd para Limitación y Aislamiento de Recursos

Systemd ya coloca los servicios en grupos de control de Linux. No tienes que crear directorios cgroup sin procesar manualmente para evitar que un trabajador por lotes consuma toda la máquina. En muchos casos, puedes agregar algunas propiedades a un servicio o slice, recargar systemd y obtener controles de CPU, memoria, tareas y E/S que sobreviven al reinicio y aparecen en las herramientas normales de systemctl.

El truco está en elegir el tipo de límite adecuado. Un límite de memoria estricto puede proteger el host pero matar el servicio si lo configuras demasiado bajo. Los pesos de CPU son suaves hasta que el sistema está ocupado. Las cuotas de CPU son estrictas pero pueden agregar latencia. Los límites de E/S dependen de la pila de almacenamiento y la versión de cgroup. El control de recursos no es una casilla de verificación; es una compensación operativa.

Entendiendo los Grupos de Control (cgroups)

Antes de profundizar en la implementación de systemd, es esencial comprender los conceptos fundamentales de cgroups. Los cgroups son un mecanismo jerárquico en el kernel de Linux que permite agrupar procesos y luego asignar políticas de gestión de recursos a estos grupos. Estas políticas pueden incluir:

  • CPU: Limitar el tiempo de CPU, priorizar el acceso a la CPU.
  • Memoria: Establecer límites de uso de memoria, prevenir condiciones de falta de memoria (OOM).
  • E/S: Limitar las operaciones de lectura/escritura en disco.
  • Red: El control de red es posible a través del control de tráfico de Linux y herramientas relacionadas, pero las propiedades de unidad integradas de systemd se centran principalmente en CPU, memoria, recuento de procesos, acceso a dispositivos y E/S de bloque.
  • Acceso a Dispositivos: Controlar el acceso a dispositivos específicos.

El kernel expone las configuraciones de cgroup a través de un sistema de archivos virtual, típicamente montado en /sys/fs/cgroup. Cada controlador (por ejemplo, cpu, memory) tiene su propio directorio, y dentro de estos, jerarquías de directorios representan grupos y sus límites de recursos asociados.

Arquitectura de Gestión de Cgroups de Systemd

Systemd abstrae la complejidad de la manipulación directa de cgroup al proporcionar un sistema de gestión de unidades estructurado. Organiza los procesos en una jerarquía de unidades, que luego se asignan a jerarquías de cgroup. Los tipos de unidad principales relevantes para la gestión de recursos son:

  • Slices: Son contenedores abstractos para unidades de servicio. Los slices forman una jerarquía, permitiendo la delegación de recursos. Por ejemplo, un slice para sesiones de usuario podría contener slices para aplicaciones individuales. Systemd crea automáticamente slices para servicios del sistema, sesiones de usuario y máquinas virtuales/contenedores.
  • Scopes: Se utilizan típicamente para grupos de procesos temporales o creados dinámicamente, a menudo asociados con sesiones de usuario o servicios del sistema que no se gestionan como unidades de servicio completas. Son transitorios y existen mientras los procesos dentro de ellos están en ejecución.
  • Servicios: Son las unidades fundamentales para gestionar demonios y aplicaciones. Cuando se inicia una unidad de servicio, systemd coloca sus procesos en una jerarquía de cgroup, generalmente dentro de un slice. Los límites de recursos se pueden aplicar directamente a las unidades de servicio.

La jerarquía predeterminada de systemd a menudo se ve así:

-.slice (Slice raíz)
  |- system.slice
  |  |- <nombre_del_servicio>.service
  |  |- otro-servicio.service
  |  ... 
  |- user.slice
  |  |- user-1000.slice
  |  |  |- session-c1.scope
  |  |  |  |- <aplicación>.service (si es iniciado por el usuario)
  |  |  |  ...
  |  |  ...
  |  ... 
  |- machine.slice (para VMs/contenedores)
  ... 

Aplicando Límites de Recursos con Archivos de Unidad de Systemd

Systemd te permite especificar límites de recursos de cgroup directamente dentro de los archivos de unidad .service, .slice o .scope. Estas directivas se colocan bajo las secciones [Service], [Slice] o [Scope], respectivamente.

Límites de CPU

Las directivas principales para el control de recursos de CPU son:

  • CPUQuota=: Limita el tiempo total de CPU que la unidad puede usar. Se especifica como un porcentaje (por ejemplo, 50% para medio núcleo de CPU) o una fracción de un núcleo de CPU (por ejemplo, 0.5). También es posible especificar un valor en microsegundos por período. El período predeterminado es 100ms.
  • CPUWeight=: Establece una ponderación relativa para el tiempo de CPU en sistemas cgroup v2. Una unidad con un peso más alto obtiene una porción mayor cuando hay contención, pero no reserva CPU cuando la máquina está inactiva.
  • CPUShares=: Ponderación más antigua de la era cgroup v1. Prefiere CPUWeight= en distribuciones modernas a menos que sepas que necesitas compatibilidad con v1.
  • CPUQuotaPeriodSec=: Establece el período para CPUQuota. El valor predeterminado es 100ms.

Ejemplo: Limitando un servidor web al 75% de un núcleo de CPU:

Crea o edita un archivo de servicio, por ejemplo, /etc/systemd/system/miappweb.service:

[Unit]
Description=Mi Aplicación Web

[Service]
ExecStart=/usr/bin/miappweb
User=usuarioweb
Group=grupoweb

# Limitar al 75% de un núcleo de CPU
CPUQuota=75%

[Install]
WantedBy=multi-user.target

Después de crear o modificar el archivo de servicio, recarga el demonio systemd y reinicia el servicio:

sudo systemctl daemon-reload
sudo systemctl restart miappweb.service

Límites de Memoria

Los límites de memoria se controlan mediante directivas como:

  • MemoryMax=: Establece un límite estricto en la cantidad de memoria que los procesos de la unidad pueden consumir. Se puede especificar en bytes o con sufijos como K, M, G, T (por ejemplo, 512M).
  • MemoryLimit=: Ortografía más antigua conservada en algunos sistemas para compatibilidad. Prefiere MemoryMax= en versiones modernas de systemd.
  • MemoryHigh=: Establece un límite suave. Cuando se acerca a este límite, la recuperación de memoria (intercambio) se activa de manera más agresiva, pero el límite estricto aún no se aplica.
  • MemorySwapMax=: Limita la cantidad de espacio de intercambio que la unidad puede usar.

Ejemplo: Limitando una base de datos a 2GB de RAM:

Crea o edita un archivo de servicio, por ejemplo, /etc/systemd/system/mibd.service:

[Unit]
Description=Mi Servicio de Base de Datos

[Service]
ExecStart=/usr/bin/mibd
User=usuariobd
Group=grupobd

# Limitar la memoria a 2 Gigabytes
MemoryMax=2G

[Install]
WantedBy=multi-user.target

Recarga y reinicia:

sudo systemctl daemon-reload
sudo systemctl restart mibd.service

Límites de E/S

La limitación de E/S se puede controlar usando directivas como:

  • IOWeight=: Establece un peso relativo para las operaciones de E/S. Los valores más altos dan mayor prioridad de E/S. El rango es de 1 a 1000 (predeterminado 500).
  • IOReadBandwidthMax=: Limita el ancho de banda de lectura de E/S. Se especifica como [<dispositivo>] <bytes_por_segundo>. Por ejemplo, IOReadBandwidthMax=/dev/sda 100M limita las operaciones de lectura en /dev/sda a 100MB/s.
  • IOWriteBandwidthMax=: Limita el ancho de banda de escritura de E/S. Formato similar a IOReadBandwidthMax.

Ejemplo: Limitando un servicio de procesamiento en segundo plano a 50MB/s en un disco específico:

Crea o edita un archivo de servicio, por ejemplo, /etc/systemd/system/procesolote.service:

[Unit]
Description=Servicio de Procesamiento por Lotes

[Service]
ExecStart=/usr/bin/procesolote
User=usuariolote
Group=grupolote

# Limitar operaciones de escritura a 50MB/s en /dev/sdb
IOWriteBandwidthMax=/dev/sdb 50M

# Darle una prioridad de lectura moderada
IOWeight=200

[Install]
WantedBy=multi-user.target

Recarga y reinicia:

sudo systemctl daemon-reload
sudo systemctl restart procesolote.service

Gestionando y Monitoreando Cgroups

Systemd proporciona herramientas para inspeccionar y gestionar los cgroups asociados con tus unidades.

Inspeccionando el Estado del Cgroup

El comando systemctl status proporciona información sobre la membresía del cgroup de una unidad y el uso de recursos.

systemctl status miappweb.service

Busca líneas que indiquen la ruta del cgroup. Por ejemplo:

● miappweb.service - Mi Aplicación Web
     Loaded: loaded (/etc/systemd/system/miappweb.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-10-27 10:00:00 UTC; 1 day ago
       Docs: man:miappweb(8)
   Main PID: 12345 (miappweb)
      Tasks: 5 (limit: 4915)
     Memory: 15.5M
        CPU: 2h 30m 15s
      CGroup: /system.slice/miappweb.service
              └─12345 /usr/bin/miappweb

También puedes inspeccionar directamente el sistema de archivos cgroup:

systemd-cgls # Muestra la jerarquía de cgroup gestionada por systemd
systemd-cgtop # Similar a top, pero para cgroups

Para ver los límites específicos aplicados al cgroup de un servicio:

# Para límites de memoria en un host típico con cgroup v2
cat /sys/fs/cgroup/system.slice/miappweb.service/memory.max

# Para límites de CPU
cat /sys/fs/cgroup/system.slice/miappweb.service/cpu.max

Las rutas y nombres de archivo exactos varían según la versión de cgroup y la distribución. En sistemas cgroup v1, pueden existir rutas específicas del controlador como /sys/fs/cgroup/memory/.... En sistemas cgroup v2, la jerarquía unificada bajo /sys/fs/cgroup/... es la vista normal.

Modificando Límites de Cgroup sobre la Marcha

Si bien es una buena práctica establecer límites en los archivos de unidad, puedes ajustarlos temporalmente usando systemctl set-property:

sudo systemctl set-property miappweb.service CPUQuota=50%

Dependiendo de la versión de systemd y las banderas, set-property puede escribir un drop-in bajo /etc/systemd/system.control/ para propiedades persistentes. Usa systemctl cat miappweb.service y systemctl show miappweb.service -p CPUQuota -p MemoryMax para confirmar lo que sucedió. Para infraestructura como código y revisión por pares, un drop-in de unidad explícito suele ser más claro.

Slices para Delegación de Recursos

Los slices son poderosos para gestionar grupos de servicios o aplicaciones. Puedes definir límites de recursos en un slice, y todos los servicios o scopes dentro de ese slice heredarán o estarán restringidos por esos límites.

Ejemplo: Creando un slice dedicado para trabajos por lotes intensivos en recursos:

Crea un archivo de slice, por ejemplo, /etc/systemd/system/lote.slice:

[Unit]
Description=Slice de Procesamiento por Lotes

[Slice]
# Limitar la CPU total para todos los trabajos en este slice a 1 núcleo
CPUQuota=100%
# Limitar la memoria total a 4GB
MemoryMax=4G

Ahora, puedes configurar servicios para que se ejecuten dentro de este slice usando la directiva Slice= en sus archivos .service:

[Unit]
Description=Trabajo por Lotes Específico

[Service]
ExecStart=/usr/bin/mitrabajolote

# Colocar este servicio en el slice lote.slice
Slice=lote.slice

[Install]
WantedBy=multi-user.target

Recarga systemd, habilita/inicia el slice si es necesario (aunque a menudo se activa implícitamente) e inicia el servicio.

sudo systemctl daemon-reload
sudo systemctl start mitrabajolote.service

Este enfoque te permite agrupar procesos relacionados y gestionar su consumo colectivo de recursos.

Mejores Prácticas y Consideraciones

  • Comienza con Límites Incrementales: Al establecer límites, comienza con valores conservadores y auméntalos gradualmente según sea necesario. Los límites agresivos pueden desestabilizar las aplicaciones.
  • Monitorea: Monitorea regularmente el uso de recursos de tu sistema y el impacto de tus configuraciones de cgroup. Herramientas como systemd-cgtop, htop, top e iotop son invaluables.
  • Comprende Cgroup v1 vs. v2: Systemd soporta tanto cgroup v1 como v2. Si bien muchas directivas son similares, v2 ofrece una jerarquía unificada y algunas diferencias de comportamiento. Asegúrate de saber qué versión está usando tu sistema si encuentras problemas complejos.
  • Priorización vs. Límites Estrictos: Usa CPUWeight para priorización cuando los recursos son escasos, y CPUQuota para límites estrictos. De manera similar, MemoryHigh es para presión antes del límite estricto, y MemoryMax es el límite estricto.
  • Servicio vs. Slice: Usa unidades de servicio para aplicaciones individuales y slices para gestionar grupos de aplicaciones relacionadas o grupos de recursos.
  • Documentación: Documenta claramente los límites de recursos aplicados a servicios críticos, especialmente en entornos de producción.
  • OOM Killer: Ten en cuenta que si un proceso excede su límite MemoryMax, el asesino Out-Of-Memory (OOM) del kernel podría terminarlo, incluso si está dentro de un cgroup. Systemd puede gestionar cómo se comporta el asesino OOM para cgroups específicos usando directivas como OOMPolicy=.

Una Forma Más Segura de Implementar Límites

Comienza con la observación. Antes de agregar límites, observa cómo se comporta el servicio durante la carga normal y durante su peor carga esperada:

systemctl status miappweb.service
systemd-cgtop
systemctl show miappweb.service -p MemoryCurrent -p CPUUsageNSec -p TasksCurrent

Para la memoria, un buen primer paso suele ser MemoryHigh= en lugar de MemoryMax=:

[Service]
MemoryHigh=1G
MemoryMax=1536M

MemoryHigh= le dice al kernel que aplique presión antes de que el servicio alcance el límite estricto. MemoryMax= es el muro. Si el proceso lo cruza y la memoria no se puede recuperar, el kernel puede matar un proceso en el cgroup. Eso puede ser exactamente lo que quieres para un trabajador descontrolado, pero es una mala sorpresa para una base de datos a menos que lo hayas planeado.

Para la CPU, decide si quieres equidad o un límite estricto:

[Service]
CPUWeight=50

Esto reduce la prioridad bajo contención pero aún permite que el servicio use CPU inactiva. Para trabajos en segundo plano, eso suele ser mejor que una cuota.

[Service]
CPUQuota=200%

Esto limita el servicio a aproximadamente dos núcleos de CPU de tiempo. Eso es útil para un procesador por lotes ruidoso, pero puede perjudicar a las aplicaciones sensibles a la latencia si los hilos de trabajo se limitan durante los picos de tráfico.

Para explosiones de procesos, agrega un límite de tareas:

[Service]
TasksMax=200

Esto protege al host de tormentas de bifurcación accidentales. Ajústalo lo suficientemente alto para los recuentos normales de hilos. Las cargas de trabajo de Java, bases de datos y similares a navegadores pueden usar más tareas de las que esperas.

Drop-Ins en Lugar de Editar Unidades del Proveedor

Evita editar archivos de unidad proporcionados por paquetes bajo /usr/lib/systemd/system/ o /lib/systemd/system/. Usa un drop-in:

sudo systemctl edit miappweb.service

Luego agrega:

[Service]
MemoryHigh=1G
MemoryMax=1536M
CPUWeight=80

Después de guardar:

sudo systemctl daemon-reload
sudo systemctl restart miappweb.service
systemctl cat miappweb.service

systemctl cat muestra la unidad del proveedor y tu anulación juntas. Eso facilita mucho la depuración futura porque la configuración activa es visible en un solo comando.

Slices para Equipos, Inquilinos y Clases de Carga de Trabajo

Los slices se vuelven útiles cuando dejas de pensar en un servicio a la vez. Supongamos que un host ejecuta la API, un generador de informes y varios trabajadores de importación. Puede que no te importe qué trabajador de importación usa la CPU, pero sí te importa que todo el trabajo de importación en conjunto no pueda privar de recursos a la API.

Crea un slice:

# /etc/systemd/system/importacion.slice
[Unit]
Description=Cargas de trabajo de importación y relleno

[Slice]
CPUWeight=30
MemoryHigh=4G
MemoryMax=5G

Coloca los servicios de importación dentro de él:

[Service]
Slice=importacion.slice
ExecStart=/usr/local/bin/trabajador-importacion

Ahora el grupo tiene presión compartida. Esto es más limpio que poner límites estrictos separados en cada trabajador y esperar que las matemáticas sigan funcionando después de que alguien agregue uno nuevo.

Hay un detalle de nomenclatura que atrapa a la gente: los nombres de slice codifican la jerarquía. cliente-a.slice es un slice de nivel superior. cliente-a-lote.slice no es un hijo de cliente-a.slice; es solo otro nombre de nivel superior. Los slices jerárquicos usan guiones como separadores de una manera específica, así que lee systemd.slice(5) antes de diseñar un árbol de slices grande.

Lo que los Límites de Recursos No Pueden Arreglar

Los cgroups pueden evitar que una carga de trabajo abrume al host, pero no pueden hacer que una máquina subdimensionada sea rápida. Si una base de datos necesita más memoria para su conjunto de trabajo de la que permites, puede pasar más tiempo recuperando memoria o fallar bajo carga. Si una API necesita tiempos de respuesta cortos, una cuota de CPU estricta puede crear retrasos de limitación que parecen latencia aleatoria. Si un dispositivo de almacenamiento ya está saturado, los pesos de E/S pueden mejorar la equidad pero no crear rendimiento.

Trata los límites como barreras de protección. Combínalos con configuraciones a nivel de aplicación: tamaños de búfer de base de datos, recuentos de trabajadores, concurrencia de colas, límites de heap de JVM, GOMEMLIMIT de Go, banderas de memoria de Node, o lo que sea que proporcione tu tiempo de ejecución. La mejor configuración suele ser ambas: la aplicación conoce su propio modelo de memoria y concurrencia, y systemd protege el resto de la máquina si ese modelo se rompe.

El Modelo Mental a Mantener

Usa límites a nivel de servicio para un demonio. Usa límites a nivel de slice para un grupo de cargas de trabajo relacionadas. Usa pesos cuando quieras prioridad bajo contención. Usa cuotas y límites de memoria estrictos cuando necesites un límite firme y estés preparado para las consecuencias. Verifica las propiedades efectivas con systemctl show, observa el comportamiento con systemd-cgtop y mantén la configuración en drop-ins o archivos de unidad que tu equipo pueda revisar.