Optimiza el rendimiento de los contenedores Docker con límites de CPU y memoria

Aprende a optimizar el rendimiento de los contenedores Docker estableciendo límites de CPU y memoria. Esta guía cubre opciones de configuración esenciales como acciones de CPU, cuotas, límites de memoria y swap. Descubre cómo monitorear el uso de recursos del contenedor con `docker stats` e implementa mejores prácticas para evitar la inanición de recursos, mejorar la estabilidad de la aplicación y aumentar la eficiencia general del sistema.

Optimiza el rendimiento de los contenedores Docker con límites de CPU y memoria

Los contenedores Docker no se comportan automáticamente como pequeñas máquinas virtuales con recursos fijos. A menos que le indiques lo contrario a Docker, un contenedor puede competir por la CPU y la memoria del host como cualquier otro proceso. Esto es conveniente en una laptop y riesgoso en un servidor compartido.

Los límites de CPU y memoria no son potenciadores mágicos del rendimiento. Un límite demasiado bajo hace que una aplicación sea más lenta o inestable. Un límite demasiado alto no protege al host. El objetivo es darle a cada contenedor suficiente espacio para el trabajo normal y pico, mientras se evita que un proceso defectuoso derribe todo lo demás en la máquina.

Por qué importan los límites

El modo de falla común es familiar: un contenedor inicia un trabajo descontrolado, la memoria sube, el host comienza a hacer swap y los servicios no relacionados se ralentizan. O una tarea por lotes que consume mucha CPU usa todos los núcleos y el contenedor API a su lado comienza a fallar en los objetivos de latencia.

Los límites ayudan con tres problemas prácticos:

  • Protegen al host de que un solo contenedor use toda la memoria.
  • Hacen que las pruebas de rendimiento sean más honestas porque el contenedor se ejecuta con restricciones similares a las de producción.
  • Te obligan a notar cuándo una aplicación necesita escalado o ajuste en lugar de tomar prestados recursos silenciosamente de los vecinos.

No comiences con números aleatorios. Ejecuta el servicio bajo carga, observa el uso real y establece límites con margen de maniobra. Un pequeño trabajador en segundo plano y un servicio JVM necesitan un tratamiento muy diferente.

Controles de CPU

Docker expone algunos controles de CPU. El más útil en el día a día es --cpus.

Limita un contenedor a aproximadamente una CPU y media:

docker run -d --name api --cpus="1.5" nginx

Esto es más fácil de leer que configurar manualmente la cuota y el período CFS. Internamente, Docker utiliza los controles de programación de CPU de Linux.

--cpu-shares es diferente. Es un peso relativo durante la contención de CPU, no un límite duro:

docker run -d --name important-api --cpu-shares 2048 nginx
docker run -d --name batch-worker --cpu-shares 512 worker-image

Cuando el host tiene CPU inactiva, ambos contenedores pueden usar más. Cuando compiten, el primer contenedor tiene más peso de programación. Esto es útil para la priorización, pero no evita que un contenedor use CPU libre.

Si necesitas configuraciones CFS exactas, --cpu-period y --cpu-quota aún están disponibles:

docker run -d --name limited-api \
  --cpu-period 100000 \
  --cpu-quota 50000 \
  nginx

Eso le da al contenedor 50,000 microsegundos de tiempo de CPU en cada período de 100,000 microsegundos, aproximadamente la mitad de una CPU. Para la mayoría de los equipos, --cpus="0.5" comunica la misma intención de manera más clara.

Controles de memoria

Los límites de memoria son más peligrosos que los límites de CPU porque excederlos puede matar el proceso. Establécelos deliberadamente y prueba el comportamiento pico.

La opción básica es --memory:

docker run -d --name web --memory 512m nginx

Si el contenedor excede el límite, el kernel puede matar un proceso dentro del contenedor. En la salida de Docker a menudo verás esto como una muerte OOM:

docker inspect web --format '{{.State.OOMKilled}}'

El comportamiento del swap es fácil de malinterpretar. Cuando se establece --memory, --memory-swap es la asignación total de memoria más swap, no la asignación de swap por sí sola.

Esto permite 256 MB de RAM y hasta 256 MB de swap, para un total de 512 MB:

docker run -d --name worker --memory 256m --memory-swap 512m alpine

Establecer --memory-swap igual a --memory desactiva el swap adicional para ese contenedor en sistemas donde la contabilidad de swap está disponible:

docker run -d --name no-extra-swap --memory 256m --memory-swap 256m alpine

Establecer --memory-swap -1 permite swap ilimitado hasta el swap disponible del host. Eso puede mantener vivo un proceso, pero puede hacer que la latencia sea terrible.

Monitorea antes y después

Usa docker stats mientras el servicio está bajo carga realista:

docker stats
docker stats web worker

Observa CPU %, MEM USAGE / LIMIT, BLOCK I/O y PIDS. Un servicio que está al 100 por ciento de su límite de memoria no es "eficiente"; está a un pico de tráfico de ser eliminado. Un servicio con uso intensivo de CPU que está constantemente limitado puede mostrar una CPU promedio aceptable mientras los usuarios ven solicitudes lentas.

Para una vista rápida de una sola vez:

docker stats --no-stream

Para pistas sobre OOM y reinicios:

docker ps -a
docker inspect <container> --format 'OOM={{.State.OOMKilled}} Exit={{.State.ExitCode}}'
docker logs --tail 100 <container>

Para monitoreo continuo, exporta las métricas del contenedor a Prometheus, Grafana, un servicio de monitoreo en la nube o la plataforma que ya usas. docker stats es triaje, no un sistema de alerta a largo plazo.

Un flujo de trabajo de ajuste sensato

Comienza sin límites estrictos en un entorno de prueba y captura el uso inactivo, normal y pico. Luego establece la memoria por encima del pico observado con suficiente margen para la recolección de basura, el crecimiento de caché, los apretones de manos TLS y los picos cortos. Para la CPU, decide si necesitas un límite duro o solo prioridad relativa.

Ejemplo para una API pequeña:

docker run -d --name api \
  --cpus="2" \
  --memory 1g \
  --memory-swap 1g \
  -p 8080:80 \
  my-api:latest

Eso dice que la API obtiene hasta dos CPUs, 1 GB de RAM y sin asignación adicional de swap. No es universalmente correcto. Es un punto de partida claro para un servicio que ha sido probado cerca de ese rango.

Para Docker Compose, el motor Docker local admite opciones de recursos como:

services:
  api:
    image: my-api:latest
    mem_limit: 1g
    cpus: 2.0

Las configuraciones de recursos de Compose y Swarm/Kubernetes no son idénticas, así que verifica el destino de implementación antes de asumir que un campo tiene el mismo comportamiento en todas partes.

Errores comunes

Establecer la memoria demasiado baja es el error más común. La aplicación se inicia, pasa una prueba de humo y luego muere bajo un patrón de solicitudes que asigna más memoria de la que cubrió la prueba.

Usar límites de CPU para ocultar código ineficiente es otro. Si un servicio es lento porque está haciendo trabajo costoso por solicitud, un límite de CPU más bajo hace que los síntomas sean más obvios. No soluciona la ruta del código.

Ignorar los tiempos de ejecución del lenguaje también perjudica. Las aplicaciones JVM, Node.js, Go, Python y Ruby responden de manera diferente a la presión de la memoria. Algunos tiempos de ejecución necesitan configuraciones de heap explícitas para que sus suposiciones internas de memoria coincidan con el límite del contenedor.

Finalmente, no ajustes solo un contenedor. El host todavía necesita CPU y memoria para el kernel, Docker, registro, agentes de monitoreo y cualquier proceso sidecar. Deja espacio.

Los buenos límites hacen que los fallos sean más pequeños y el rendimiento más predecible. Deben provenir de la medición, no de conjeturas.