Hay una frase que se repite en los postmortems de equipos que acaban de adoptar contenedores: «creíamos que estaba aislado». El proceso se pasó de memoria, murió el proceso equivocado, o un contenedor estaba viendo más del sistema de lo que debería. Nada roto por un fallo del software. Todo roto por no entender qué hay debajo.

Lo que hay debajo son dos mecanismos del kernel de Linux: cgroups y namespaces. No son tecnología reciente. Los namespaces llevan en el kernel desde 2002. Cgroups llegaron en 2008, desarrollados originalmente por ingenieros de Google —Paul Menage y Rohit Seth— antes de integrarse en el kernel principal con la versión 2.6.24. Docker los popularizó en 2013. La diferencia entre las fechas y la popularización dice mucho sobre cuánto tiempo estuvo la infraestructura esperando a que alguien la usara bien.

Entender cómo funcionan estos mecanismos no es un ejercicio académico. Es la diferencia entre diagnosticar un problema de límites de memoria en producción en cinco minutos o pasarse horas mirando métricas que no cuadran. Es saber por qué un proceso dentro de un contenedor ve un árbol de procesos distinto al del anfitrión, o por qué dos contenedores pueden tener la misma dirección IP sin colisionar. Y es comprender qué significa realmente que un contenedor sea «ligero» y cuándo esa ligereza se convierte en un problema de seguridad.

Este artículo no va de cómo crear contenedores. Va de qué hay debajo cuando se crean, y qué decisiones de diseño del kernel de Linux hicieron posible que la contenedorización se convirtiera en el estándar que es hoy.

Namespaces: el aislamiento que no aísla del todo

Un namespace es, en esencia, una forma de mentirle a un proceso sobre lo que puede ver del sistema. Cuando un proceso arranca dentro de un namespace de red, ve interfaces de red que no son las del sistema anfitrión. Cuando está en un namespace de PID, ve un árbol de procesos que empieza en 1, aunque en el sistema real ese proceso tenga otro identificador completamente distinto. El kernel mantiene varias vistas del mismo sistema, y cada proceso vive en una de esas vistas sin saber que hay otras.

A día de hoy, Linux ofrece ocho tipos de namespaces, cada uno responsable de aislar un aspecto distinto del sistema: PID para procesos, NET para red, MNT para sistemas de archivos montados, UTS para nombres de host, IPC para comunicación entre procesos, USER para identificadores de usuario, cgroup para la jerarquía de cgroups visible desde dentro del contenedor, y time para el reloj del sistema. No todos llegaron a la vez. El primero fue el namespace de montaje en 2002. El de usuario —el más delicado desde el punto de vista de seguridad, y el que permitió los contenedores sin privilegios de root— no se consideró completo hasta el kernel 3.8, en 2013. Cada uno funciona de forma independiente, aunque en la práctica los contenedores los combinan todos para construir un entorno que parece autónomo.

La clave está en que los namespaces no son barreras infranqueables. Son capas de abstracción. Un proceso en el anfitrión con privilegios suficientes puede entrar en el namespace de un contenedor y ver exactamente lo mismo que ve el proceso contenido. Esto es útil para depuración, pero también significa que la seguridad de un contenedor no depende solo de los namespaces. Depende de permisos, de capacidades del kernel, de políticas de seguridad adicionales.

Quien administra sistemas en entornos con contenedores se encuentra tarde o temprano con situaciones donde el aislamiento no es suficiente. Un contenedor que necesita acceder a un dispositivo de hardware. Otro que debe compartir un socket Unix con el anfitrión. O simplemente un proceso que requiere una capacidad del kernel que los namespaces no pueden delegar sin romper el modelo de seguridad. En esos casos, se recurre a montajes bind, a volúmenes compartidos o a privilegios elevados, y cada decisión de ese tipo erosiona un poco el aislamiento original.

Cgroups: el control que nunca es perfecto

Si los namespaces controlan qué puede ver un proceso, los cgroups controlan qué puede usar. Memoria, CPU, ancho de banda de disco, número de procesos: todo lo que consume recursos en un sistema puede limitarse mediante cgroups. La idea es sencilla: agrupar procesos y asignarles cuotas. La práctica es más complicada.

Un cgroup no es una jaula hermética. Es un mecanismo de contabilidad y límites. El kernel lleva la cuenta de cuánta memoria ha usado un grupo de procesos, y cuando se alcanza el límite configurado, actúa en consecuencia. En el caso de la memoria, eso puede significar matar procesos. En el caso de la CPU, significa reducir el tiempo de procesador asignado. Pero los cgroups no impiden que un proceso intente usar más recursos de los permitidos: simplemente gestionan las consecuencias.

Esto genera situaciones que desconciertan a quien no conoce el funcionamiento interno. Un contenedor configurado con un límite de memoria de 512 MB puede seguir solicitando más memoria al kernel, y el kernel se la concederá hasta que el límite se alcance. En ese momento, el proceso que más memoria esté usando dentro del cgroup será terminado por el OOM killer (asesino por falta de memoria). No necesariamente el proceso que causó el problema, sino el que más pesa en ese instante. Esto es especialmente frustrante en aplicaciones que gestionan múltiples procesos internos, donde el que muere puede no ser el que se pasó de la raya.

Los cgroups también tienen versiones. La primera implementación, conocida como cgroups v1, permitía configurar cada tipo de recurso de forma independiente, con jerarquías distintas. Esto daba flexibilidad, pero también generaba inconsistencias: cada controlador —memoria, CPU, I/O— tenía su propia jerarquía, y no había forma de garantizar coherencia entre ellas. La versión 2, cuya documentación oficial apareció en el kernel 4.5 en 2016, unificó las jerarquías en una sola y simplificó el modelo. Es la que usa systemd en la mayoría de distribuciones modernas, y la que Kubernetes recomienda desde hace tiempo. Hoy es habitual encontrar sistemas con ambas versiones activas simultáneamente, con toda la complejidad que eso implica para la monitorización y el diagnóstico.

Verlo en acción: tres comandos útiles

Antes de continuar con la teoría, merece la pena tocar el suelo. Estos comandos no requieren instalación adicional y funcionan en cualquier distribución moderna:

# Ver los cgroups del sistema (v2)
systemd-cgls

# Ver los namespaces de un proceso en ejecución
# (sustituir PID por el identificador del proceso)
ls -la /proc/PID/ns/

# Ver el límite de memoria de un contenedor Docker en ejecución
# (sustituir ID por el identificador del contenedor)
cat /sys/fs/cgroup/system.slice/docker-ID.scope/memory.max

El primero muestra la jerarquía completa de cgroups del sistema, con qué servicios y procesos están agrupados en cada nodo. El segundo revela qué namespaces tiene asociados un proceso: cada enlace simbólico apunta a un namespace distinto. El tercero deja ver, en bytes, cuánta memoria tiene asignada un contenedor según el kernel. Si el valor es max, el contenedor no tiene límite configurado.

Ninguno de los tres requiere ser root. Suficiente para empezar a ver la infraestructura que normalmente permanece oculta.

Dónde se rompe el modelo

Hay un malentendido común sobre los contenedores: que son seguros por defecto. No lo son. Un contenedor mal configurado puede escapar de sus límites, acceder a recursos del anfitrión o interferir con otros contenedores. Los namespaces y cgroups proporcionan aislamiento, pero no son una solución de seguridad completa. Para eso hacen falta capas adicionales: SELinux, AppArmor, seccomp, políticas de red estrictas.

Otro punto de fricción aparece cuando se intenta monitorizar contenedores desde el anfitrión. Las métricas que se ven dentro del contenedor no siempre coinciden con las del sistema real. Un proceso dentro de un contenedor puede creer que tiene acceso a toda la CPU del sistema, cuando en realidad está limitado a una fracción mediante cgroups. Las herramientas de monitorización que no entienden esta distinción pueden reportar datos engañosos, lo que lleva a decisiones equivocadas sobre escalado o asignación de recursos.

También está el problema del rendimiento. Los namespaces y cgroups añaden una capa de indirección que, aunque ligera, no es gratuita. En la mayoría de los casos, el coste es despreciable. Pero en entornos con miles de contenedores arrancando y parándose constantemente, esa sobrecarga se acumula. El kernel tiene que gestionar estructuras de datos adicionales, mantener tablas de traducción, aplicar límites en tiempo de ejecución. Nada de esto es gratis.

Qué aprender de todo esto

Comprender cgroups y namespaces cambia la forma de trabajar con contenedores. En lugar de tratarlos como cajas negras, se entienden como procesos del sistema con restricciones específicas. Eso permite diagnosticar problemas más rápido, configurar límites de forma más precisa y evitar errores comunes como asumir que un contenedor está completamente aislado del anfitrión.

También ayuda a tomar mejores decisiones sobre cuándo usar contenedores y cuándo no. No todo problema se resuelve con uno. A veces, una unidad de systemd bien configurada con sus propios límites de cgroup basta y no añade la complejidad operativa de un runtime de contenedores. Otras veces, el aislamiento que ofrecen los contenedores no es suficiente, y hace falta virtualización completa. Conocer las limitaciones de cada enfoque es lo que separa una arquitectura sólida de una que se sostiene por inercia.

Los contenedores no son magia. Son una aplicación inteligente de mecanismos del kernel que existían antes de que Docker popularizara el concepto. Cgroups y namespaces son las raíces de ese árbol, y entenderlas es entender qué hace posible que millones de aplicaciones corran hoy en entornos aislados sin necesidad de virtualizar hardware. Esa comprensión no solo hace mejor a quien administra sistemas. Hace que los sistemas que administra sean más predecibles, más seguros y más fáciles de mantener cuando algo falla.

Porque algo siempre falla.

Para seguir aprendiendo

  • Documentación oficial del kernel sobre cgroups v2 — la referencia canónica, mantenida por los desarrolladores del núcleo. Cubre el diseño, la interfaz y los controladores disponibles en la versión unificada.
  • Documentación oficial del kernel sobre namespaces — índice de todos los tipos de namespaces con sus particularidades e interacciones.
  • Container Security de Liz Rice, O’Reilly (2ª edición, 2025) — analiza los mecanismos de aislamiento y seguridad desde una perspectiva práctica, con énfasis en cómo fallan y cómo protegerse. La segunda edición incorpora el estado actual de Kubernetes y las amenazas modernas.
  • Proyecto runc en GitHub — implementación de referencia del runtime de contenedores OCI. Leer su código es la forma más directa de ver cómo se usan cgroups y namespaces en producción.
  • Linux Observability with BPF de David Calavera y Lorenzo Fontana, O’Reilly — aunque centrado en BPF, dedica capítulos a cómo observar el comportamiento de cgroups y namespaces en sistemas en producción.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.

ACEPTAR
Aviso de cookies