Namespaces y cgroups en Linux: la base real de los contenedores

Última actualización: 24 de febrero de 2026
  • Los contenedores en Linux se basan en namespaces para aislar lo que los procesos pueden ver y en cgroups para limitar cuántos recursos pueden usar.
  • Existen distintos tipos de namespaces (PID, NET, MNT, UTS, IPC, USER, cgroup, time) y controladores de cgroup (CPU, memory, I/O, pids) que se combinan según el caso.
  • Docker, Kubernetes y systemd se apoyan en estas primitivas del kernel para crear entornos aislados, aplicar políticas de recursos y gestionar servicios de forma uniforme.
  • Monitorear métricas de cgroups y comprender la topología de namespaces es esencial para diagnosticar OOMs, problemas de rendimiento y asegurar un aislamiento sólido en producción.

contenedores linux cgroups namespaces

Si trabajas con Docker, Kubernetes o cualquier plataforma de contenedores, tarde o temprano te toca bajar a las tripas del sistema. Detrás de esa “magia” de lanzar contenedores en segundos están dos piezas clave del kernel de Linux: namespaces y cgroups, el dúo responsable del aislamiento y la gestión fina de recursos. Entenderlos marca la diferencia entre ser solo usuario de herramientas y ser quien diseña la infraestructura.

A lo largo de este artículo vamos a desgranar, con calma pero sin enrollarnos de más, cómo funcionan los namespaces (lo que un proceso puede ver) y los cgroups (lo que un proceso puede usar); cómo los aprovechan Docker y Kubernetes; cómo puedes trastear con ellos a bajo nivel con comandos como unshare, ip netns o escribiendo directamente en /sys/fs/cgroup; y qué truquillos son útiles para rendimiento, seguridad y resolución de problemas en entornos de producción.

Qué hace realmente un contenedor en Linux

Cuando ejecutas algo como docker run, no se crea una máquina virtual mágica; lo que obtienes es un proceso de Linux (o un grupo de procesos) con aislamiento fuerte y recursos limitados. El “truco” se basa en cuatro mecanismos del kernel:

  • Namespaces: controlan qué ve el proceso (PIDs, red, sistema de ficheros, hostname, etc.).
  • Cgroups: controlan cuánto puede usar el proceso (CPU, RAM, I/O, PIDs…).
  • Chroot / rootfs aislado: le dan al proceso un sistema de ficheros propio.
  • Procesos normales de Linux: al final todo se reduce a PIDs del host gestionados por el kernel.

En otras palabras, un contenedor no es más que un proceso ejecutándose con un conjunto de namespaces nuevos, encajado en uno o varios cgroups y con un sistema de ficheros “enganchado” a medida. El resto lo pone la herramienta (Docker, containerd, runc, etc.) con automatización y APIs cómodas.

Namespaces de Linux: aislar lo que cada proceso puede ver

Los namespaces son una capa de abstracción del kernel que crea “mundos” separados para grupos de procesos. Un proceso solo ve los recursos que están dentro de su namespace; el resto del sistema sigue ahí, pero para él es invisible.

Linux cuenta actualmente con varios tipos de namespaces, cada uno enfocado a una parte concreta del sistema. Combinados, consiguen que un contenedor se perciba como un sistema operativo independiente aunque comparta kernel con el host.

Principales tipos de namespaces

Cada tipo de namespace aísla una dimensión distinta. Lo habitual en un contenedor “serio” es usar varios de ellos a la vez. Estos son los más importantes:

  • PID namespace: aísla el espacio de identificadores de proceso. Dentro del contenedor, el proceso principal suele ser PID 1, aunque en el host tenga otro número. Los procesos de distintos namespaces PID pueden tener el mismo PID sin estorbarse, y no pueden enviarse señales entre sí salvo que el host lo permita explícitamente.
  • Network (NET) namespace: crea una pila de red independiente, con sus propias interfaces, rutas, reglas de firewall, sockets, etc. Cada contenedor tiene su loopback, sus IPs y su routing. Así, puedes reusar la misma dirección IP en distintos namespaces sin conflicto y aplicar reglas de iptables aisladas.
  • Mount (MNT) namespace: separa la vista de los puntos de montaje. Los procesos ven solo la jerarquía de ficheros de su namespace, aunque debajo haya bind mounts, volúmenes compartidos, overlays, etc. Es lo que permite que un contenedor parezca tener un “/” propio sin tocar el root real del host.
  • UTS namespace: controla el hostname y el dominio NIS. Dentro de un UTS namespace, puedes cambiar el nombre de host sin afectar al sistema anfitrión. Muy útil para distinguir contenedores o simular nodos en entornos de pruebas.
  • IPC namespace: aísla colas de mensajes System V, semáforos y memoria compartida. Evita que procesos de contenedores distintos se pisoteen la IPC por accidente o mal diseño.
  • User namespace: probablemente el más delicado. Permite mapear UIDs y GIDs internos del namespace a otros IDs del host. Esto hace posible que el “root” dentro del contenedor esté mapeado a un usuario sin privilegios en el host, reduciendo mucho el riesgo de escalada de privilegios y mejorando la seguridad en contenedores.
  • Cgroup namespace: da una vista virtualizada de /proc/self/cgroup y de la jerarquía de cgroups. Desde dentro del contenedor solo se ven sus propios grupos, lo que oculta la estructura real del host y refuerza el aislamiento.
  • Time namespace: permite ofrecer percepciones distintas del tiempo (relojes como CLOCK_MONOTONIC o CLOCK_BOOTTIME) para diferentes grupos de procesos. Es útil para escenarios de pruebas o aislamiento muy fino.
  Cómo activar el Arranque seguro tras actualizar la BIOS

Todos estos namespaces se exponen en /proc/<pid>/ns/, y se pueden crear o unir con herramientas como unshare y nsenter. Al morir el último proceso que pertenece a un namespace, ese namespace se destruye automáticamente.

Trastear con namespaces desde la línea de comandos

El comando unshare es uno de los básicos para experimentar: crea un proceso hijo con uno o varios namespaces nuevos. Por ejemplo, para probar un PID namespace aislado:

unshare --pid --fork --mount-proc bash
ps aux
echo $$

Dentro verás solo los procesos de ese namespace y tu shell aparecerá como PID 1 ahí dentro, aunque el host siga teniendo todos sus procesos de siempre. Es básicamente el esqueleto de lo que hará un runtime OCI cuando arranca un contenedor.

Namespaces de red en detalle

La parte de red es de las que más juego da. Al crear un NET namespace con ip netns add, obtienes una pila vacía salvo por la loopback. A partir de ahí puedes:

  • Conectar ese namespace al host con un par veth (dos interfaces virtuales unidas por un cable lógico).
  • Crear bridges tipo br0 y enganchar varios namespaces a ese puente para que se vean entre sí con IPs privadas.
  • Hacer NAT y forwarding para dar salida a Internet a esos “contenedores caseros” con iptables y /proc/sys/net/ipv4/ip_forward.

Así es, a un nivel muy bajo, lo que hace Docker con el bridge docker0 y sus redes por defecto: monta namespaces de red, pares veth, bridges y reglas NAT para que los contenedores se hablen entre ellos y salgan hacia fuera.

Mount y user namespaces para rootfs y seguridad

Con el mount namespace puedes jugar a crear un sistema de ficheros de contenedor sin usar Docker. El patrón típico es:

  • Montar o preparar un rootfs mínimo (por ejemplo, una Alpine minirootfs descargada por wget y extraída en un directorio).
  • Crear un mount namespace y hacer pivot_root o chroot a ese directorio.
  • Montar /proc, /sys y otros pseudo-sistemas dentro del nuevo root.

Con eso, tu shell verá solo ese sistema de ficheros, pero sigues siendo un proceso normal del host, con su PID real y controlable por el administrador. Si además usas un user namespace con --map-root-user, podrás tener un “root” dentro que fuera es un usuario sin privilegios.

Cgroups: cuánto recurso puede gastar cada proceso

Si los namespaces deciden lo que un proceso puede ver, los cgroups delimitan cuántos recursos del sistema puede consumir un conjunto de procesos. Se introdujeron en el kernel 2.6.24 y hoy son imprescindibles en cualquier plataforma de contenedores moderna.

A nivel conceptual, un cgroup es un directorio dentro de /sys/fs/cgroup con archivos que describen límites, prioridades y métricas. Cuando añades un PID al archivo adecuado (por ejemplo, cgroup.procs), ese proceso queda sometido a esas reglas.

Controladores de recursos en cgroups

Los cgroups se componen de controladores (subsystems) que actúan sobre recursos específicos. Entre los más usados están:

  • CPU: controla tiempo de CPU asignado. Con shares defines prioridad relativa; con quotas marcas límites duros (por ejemplo, 50 ms de CPU por cada 100 ms de periodo); con cpusets fijas procesos a núcleos concretos.
  • Memory: limita RAM y swap. Permite límites duros y suaves, configuración del OOM killer, estadísticas y presión de memoria. Muy útil para evitar que un servicio se coma el servidor entero.
  • Block I/O (blkio / io): restringe ancho de banda e IOPS por dispositivo. Puedes asignar peso proporcional o establecer límites exactos en bytes/s o ops/s.
  • Network: existen mecanismos para limitar ancho de banda y paquetes, aunque el soporte está menos maduro y suele combinarse con tc y QoS.
  • PIDs: marca el máximo número de procesos/hilos que puede crear un grupo, mitigando fork bombs o errores de código que disparan hilos.

La idea práctica es muy directa: colocas tu aplicación en un cgroup y le marcas CPU, memoria, I/O y PIDs máximos. A partir de ahí, el kernel se encarga de hacer cumplir las reglas.

cgroups v1 vs cgroups v2

En producción te vas a encontrar dos sabores de cgroups, y conviene tener claro en qué terreno pisas:

  • cgroups v1: cada controlador tiene su jerarquía independiente. Tienes rutas como /sys/fs/cgroup/cpu/..., /sys/fs/cgroup/memory/..., etc. Es flexible pero complicado de gestionar a gran escala. Muchas distros y plataformas clásicas todavía lo usan o lo soportan por compatibilidad.
  • cgroups v2: agrupa todo bajo una jerarquía unificada en /sys/fs/cgroup. Los límites se configuran vía archivos como cpu.max, memory.max, io.max y similares. Simplifica la gestión y mejora el control fino de recursos. Distribuciones modernas tienden a usar v2 por defecto, a veces con modo híbrido.
  Administración de sistemas Linux: guía completa para sysadmins

En /proc/cgroups puedes ver qué controladores están habilitados, cuántos cgroups activos hay y si estás en v1 o v2 (en v2 verás una jerarquía unificada con ID 0). El comando mount | grep cgroup también te chiva el tipo de sistema de ficheros (cgroup2fs implica v2, tmpfs suele apuntar a v1).

Creación manual de cgroups y límites típicos

Para entender bien qué hace Docker por debajo, merece la pena crear cgroups a mano. El patrón general es:

  • Crear un directorio de cgroup.
  • Configurar archivos de límite/prioridad (CPU, memoria, I/O, pids…).
  • Añadir PIDs al cgroup escribiendo en cgroup.procs o tasks (en v1).

Por ejemplo, en cgroups v2, podrías tener algo así para CPU y memoria:

mkdir /sys/fs/cgroup/test_app
echo "+cpu +memory +io +pids" > /sys/fs/cgroup/cgroup.subtree_control
echo "50000 100000" > /sys/fs/cgroup/test_app/cpu.max
echo "512M" > /sys/fs/cgroup/test_app/memory.max
echo $$ > /sys/fs/cgroup/test_app/cgroup.procs

Con esto, el proceso actual queda limitado al 50 % de un core y 512 MB de RAM. Si arrancas algo intensivo (un dd a /dev/null, por ejemplo), verás que el sistema no se desmadra. En cgroups v1, el esquema es parecido pero con archivos tipo cpu.shares, cpu.cfs_quota_us, memory.limit_in_bytes, etc., cada uno en su jerarquía.

Cómo se apilan Docker, Kubernetes y el kernel

Por encima de namespaces y cgroups hay varias capas de software que se van pasando la pelota hasta llegar al kernel. Aunque cambie el nombre de la herramienta, el final del viaje es siempre el mismo: llamadas al kernel para crear namespaces, configurar cgroups y montar sistemas de ficheros.

El “stack” típico de contenedores hoy en día se parece mucho a esto:

  • Capa de orquestación: Kubernetes, Docker Swarm, Nomad… Se encargan del ciclo de vida de pods/servicios, scheduling, escalado y soportan arquitecturas de microservicios.
  • Container runtime de alto nivel: Docker Engine, containerd, CRI-O. Reciben las órdenes del orquestador y las convierten en acciones concretas (descargar imágenes, crear contenedores, gestionar snapshots, etc.).
  • Runtime OCI de bajo nivel: runc, crun y similares. Implementan la especificación OCI y son quienes directamente llaman a clone(), unshare(), mount() y compañía para materializar el contenedor.
  • Kernel de Linux: proporciona namespaces, cgroups, OverlayFS y todo el resto de primitivos. Es donde realmente ocurre la “magia”.

Cuando lanzas un docker run, el recorrido es: cliente CLI → dockerd → containerd → runc → llamadas al kernel. En ese camino se crean los namespaces, se asignan cgroups, se monta el rootfs con OverlayFS más volúmenes y se enganchan las interfaces de red y el bridge. Lo que tú ves como “un contenedor” es, para el kernel, un puñado de procesos con ciertos flags y rutas especiales.

Requisitos y comprobaciones previas en el sistema

Antes de ponerte a jugar duro con namespaces y cgroups en un servidor, viene bien asegurarse de que el hardware y el kernel cumplen unos mínimos razonables, sobre todo si piensas montar algo parecido a producción.

Como referencia, suelen recomendarse al menos 2 CPUs lógicas, 4 GB de RAM (mejor 8 GB o más), unos 20 GB libres en disco y un kernel 3.10 o superior (idealmente 4.x o 5.x para tener todas las florituras de cgroups v2 y features modernos).

Para validar que el kernel soporta namespaces y cgroups basta revisar el archivo de configuración de la versión en uso:

grep -E "CONFIG_.*_NS" /boot/config-$(uname -r)
grep -E "CONFIG_CGROUP" /boot/config-$(uname -r)

Ahí deberían aparecer opciones como CONFIG_NAMESPACES, CONFIG_PID_NS, CONFIG_NET_NS, CONFIG_CGROUPS, CONFIG_MEMCG, CONFIG_CGROUP_SCHED marcadas como =y. Sin eso, poco juego vas a tener.

Otro detalle práctico es si el sistema permite crear user namespaces sin ser root, algo muy útil para pruebas y algunas herramientas. Eso se controla con el sysctl kernel.unprivileged_userns_clone; si está a 0, tendrás que activarlo (o tirar de sudo en todas partes).

Explorando cgroups y namespaces en máquinas “reales”

Una forma bastante directa de interiorizar estos conceptos es mirar cómo los usa el propio sistema y tus contenedores. Algunas pistas útiles:

  • ls /proc/self/ns/ te enseña los namespaces a los que pertenece tu shell actual (cgroup, ipc, mnt, net, pid, user, uts…).
  • systemd-cgtop muestra el uso de CPU, memoria y I/O por cgroup, muy similar a top pero agrupando por servicios y contenedores.
  • En /sys/fs/cgroup puedes localizar los cgroups que Docker o Podman crean para cada contenedor, normalmente con el ID del container en la ruta. Dentro, cgroup.procs lista los PIDs asociados.
  • /proc/<pid>/cgroup te indica a qué cgroups está adscrito un proceso concreto, perfecto para seguirle la pista a una app problemática.
  Sistemas Digitales: Cómo transforman el futuro de la tecnología

Verás que los contenedores no son más que otra rama de la jerarquía de cgroups, con sus límites de CPU, memoria, blkio y compañía, tal y como tú podrías crearlos a mano. La gracia de Docker es que lo hace automáticamente y con una API agradable.

Rendimiento, NUMA y estrategias de afinidad

En escenarios de alta carga (bases de datos gordas, procesamiento intensivo, streaming, etc.), no basta con “levantar contenedores y ya”. La forma en que asignas CPU y memoria a cada servicio marca muchísimo el rendimiento.

Con el controlador cpuset puedes fijar procesos o servicios a conjuntos concretos de CPUs y nodos NUMA. Algo tan simple como:

mkdir /sys/fs/cgroup/cpuset/dedicated_cpus
echo "4-7" > /sys/fs/cgroup/cpuset/dedicated_cpus/cpuset.cpus
echo "0" > /sys/fs/cgroup/cpuset/dedicated_cpus/cpuset.mems
echo 1 > /sys/fs/cgroup/cpuset/dedicated_cpus/cpuset.cpu_exclusive

permite reservar un grupo de cores para un servicio concreto y evitar que el resto del sistema se meta por medio. Esto reduce el ruido, las migraciones de hilos y los problemas de latencia que suelen aparecer cuando todo el mundo pelea por las mismas CPUs.

A nivel de memoria, combinar numactl con cgroups (o bien usar directamente los knobs NUMA del controlador de memoria cuando estén disponibles) ayuda a que los datos se queden en el nodo físico más cercano a los cores que los usan. En apps muy sensibles a latencia, se nota.

Monitoreo y observabilidad sobre cgroups y namespaces

Meter límites de recursos está bien, pero si no mides lo que pasa te puedes pegar tiros en el pie sin enterarte. El propio cgroup v2 ofrece bastante métrica útil en los archivos *.stat, *.events y *.pressure.

Algunos ejemplos prácticos:

  • cpu.stat te dice cuánto tiempo de CPU ha usado el grupo, cuántas veces ha sido throttled, etc.
  • memory.current y memory.max muestran consumo actual y límite configurado.
  • memory.events registra OOMs, soft limits alcanzados y demás sustos.
  • io.stat acumula lecturas/escrituras por dispositivo.

Para una vista rápida, systemd-cgtop es muy práctico, y si quieres algo más industrial, herramientas como cAdvisor exportan métricas de cgroups de Docker/containerd a Prometheus, con lo que puedes montar dashboards completos en Grafana sin demasiado esfuerzo, ideal para un monitor de sistema avanzado.

A la hora de depurar problemas de namespaces, scripts sencillos que recorran /proc/<pid>/ns/ y comparen los enlaces simbólicos ayudan a saber qué procesos comparten exactamente el mismo namespace, o a localizar un namespace de red “perdido” que se ha quedado colgado.

Uso de systemd como gestor de cgroups

En la mayoría de distros modernas, systemd se apoya de forma intensiva en cgroups para gestionar servicios, slices y scopes. Cuando defines una unidad con límites de recursos, en realidad lo que hace systemd es crear un cgroup para ese servicio y configurar sus controladores.

Un servicio declarado con directivas como:

CPUQuota=50%
CPUWeight=500
MemoryMax=512M
IOReadBandwidthMax=/dev/sda 10M

acaba traducido en límites de cgroup sobre CPU, memoria e I/O para el proceso de esa unidad. Esto permite alinear la gestión “clásica” de servicios con el modelo de contenedores, usando el mismo subsistema del kernel por debajo. Para más detalles sobre systemd y sus requisitos, consulta cambios de systemd.

Para los equipos que mezclan servicios tradicionales con contenedores (muy habitual), apoyarse en systemd para marcar límites a demonios críticos es una forma elegante de evitar que se maten a recursos con los pods del entorno containerizado.

En entornos donde conviven varias cargas, desde bases de datos hasta agentes de monitorización y aplicaciones a medida, dominar namespaces y cgroups permite definir políticas de aislamiento fuertes, ajustar la calidad de servicio y garantizar que cada pieza tiene su “parcela” de CPU y memoria bien controlada. Entender cómo Docker, Kubernetes y systemd explotan estas capacidades del kernel te arma para diseñar infraestructuras multi-inquilino más seguras, depurar problemas difíciles (desde OOMs hasta cuellos de botella de I/O) y sacar el máximo partido al hardware sin perder estabilidad.

systemd 259 soporte para musl
Artículo relacionado:
systemd 259: soporte para musl, seguridad y cambios clave