CPU de Docker al 100%: identifica el contenedor ruidoso y limita correctamente

¿Te fue útil?

El host está atascado. El promedio de carga sube como si tuviera prisa. SSH se siente lento. Tus paneles muestran “CPU 100%”, pero no dicen de quién es la culpa. Docker está involucrado, lo que significa que el problema está o bien bien contenido… o maravillosamente distribuido.

Este es el manual que uso cuando una máquina Linux está quemando ciclos y los contenedores son los principales sospechosos. Es práctico, un poco opinado, y te ayudará a identificar el contenedor ruidoso, demostrar que es el verdadero cuello de botella y limitarlo de una manera que no boomerangue en latencia, throttling o comportamiento raro del planificador.

Guion de diagnóstico rápido

Cuando la CPU está al tope, no necesitas un retiro de meditación. Necesitas un bucle de triaje rápido que distinga:

  • Un contenedor quemando CPU vs. muchos contenedores cada uno “un poco” caliente
  • Saturación de CPU vs. throttling de CPU vs. contención en la cola de ejecución
  • Trabajo real vs. bucles de espera activa vs. sobrecarga del kernel

Primero: confirma que es CPU y no E/S con forma de CPU

  • Ejecuta uptime y top para ver el promedio de carga frente al idle de CPU.
  • Si la carga es alta pero el idle de CPU también es alto, probablemente estés bloqueado por E/S o bloqueos.

Segundo: identifica el/los contenedores responsables

  • Usa docker stats --no-stream para un ranking rápido.
  • Luego mapea PIDs del host de vuelta a contenedores (porque “docker stats” puede mentir por omisión cuando las cosas se ponen raras).

Tercero: decide si limitar, escalar o arreglar el código

  • Limitar cuando una carga está intimidando a sus vecinos y puedes tolerar más latencia para esa carga.
  • Escalar cuando la carga es legítima y el rendimiento importa.
  • Arreglar cuando ves bucles de espera activa, reintentos, bloqueos calientes o GC patológico.

Cuarto: aplica una limitación que coincida con la realidad de tu planificador

  • En Linux, el control de CPU de Docker son cgroups. Tus límites son tan sensatos como la versión de cgroup y el comportamiento del kernel.
  • Elige quota de CPU para “este contenedor puede usar hasta X tiempo de CPU”. Elige cpuset para “este contenedor solo puede ejecutarse en estos núcleos”.

Si estás de guardia y necesitas una línea rápida: identifica el PID principal del host, mápalo a un contenedor, y luego verifica si ya está siendo throttled. Limitar un contenedor que ya está throttled es como decirle a alguien “cálmate” mientras mantienes su cabeza bajo el agua.

Qué significa realmente “CPU 100%” en Docker

“CPU 100%” es una de esas métricas que suena precisa y se comporta como chisme.

  • En un host de 4 núcleos, un solo núcleo totalmente ocupado es 25% de la CPU total si mides capacidad total.
  • En herramientas de Docker, el porcentaje de CPU del contenedor puede reportarse relativo a un solo núcleo o a todos los núcleos dependiendo del cálculo y la versión.
  • En cgroups, el uso de CPU se mide como tiempo (nanosegundos). Los límites se aplican como cuotas por período, no como “porcentaje” en sentido humano.

La idea operativa clave: puedes tener un host mostrando “100% CPU” mientras el servicio importante para usuarios está lento porque está siendo throttled, privado por contención en la cola de ejecución, o perdiendo tiempo por steal en un hipervisor.

Aquí está el modelo mental que no te fallará:

  • Uso de CPU te dice cuánto tiempo se pasó ejecutando.
  • Cola de ejecución te dice cuántos hilos quieren ejecutar pero no pueden.
  • Throttling te dice que el kernel impidió activamente que un cgroup se ejecute porque alcanzó la cuota.
  • Steal time te dice que la VM quería CPU pero el hipervisor dijo “ahora no”.

Hechos y contexto: por qué esto es más complicado de lo que parece

Algunos puntos de contexto que importan en producción porque explican lo raro que verás en las salidas:

  1. Los límites de CPU de Docker son límites de cgroup. Docker no inventó el aislamiento de CPU; es un envoltorio sobre cgroups y namespaces de Linux.
  2. cgroups v1 vs v2 cambia la tubería. Muchas sesiones de “¿por qué no existe este archivo?” son simplemente “ahora estás en v2”.
  3. El control de ancho de banda CFS (quota/periodo de CPU) se integró en el kernel de Linux mucho antes de que los contenedores fueran comunes; los contenedores lo popularizaron, pero no lo originaron.
  4. Las CPU shares no son un tope estricto. Shares son un peso que se usa solo bajo contención; no impedirán que un contenedor use CPU ociosa.
  5. “cpuset” es más antiguo y contundente. Fijar a núcleos es determinista pero puede desperdiciar CPU si fijas mal o ignoras la topología NUMA.
  6. El throttling puede parecer “bajo uso de CPU”. Un contenedor puede estar lento mientras reporta uso moderado de CPU porque pasa tiempo bloqueado por la aplicación de la cuota.
  7. El promedio de carga incluye más que CPU. En Linux, el promedio de carga cuenta tareas en sleep ininterrumpible también, así que problemas de almacenamiento pueden hacerse pasar por problemas de CPU.
  8. La virtualización añade steal time. En hosts sobreaprovisionados, el “CPU 100%” de la VM puede ser mayormente “hubiera corrido si pudiera”.
  9. El monitoreo tiene una larga cola de mentiras. Las métricas de CPU son fáciles de recolectar y fáciles de malinterpretar; diferencias en ventanas de muestreo y normalización crean picos fantasma.

Una cita que vale la pena tener en tu escritorio:

Werner Vogels (idea parafraseada): “Todo falla; diseña para que la falla sea esperada y manejada, no tratada como una excepción.”

Tareas prácticas: comandos, salidas, decisiones

Estas son tareas reales que espero que un ingeniero de guardia ejecute. Cada una incluye: comando, qué significa la salida y qué decisión tomar a partir de ello. No las ejecutes todas a ciegas; úsalas como una investigación ramificada.

Tarea 1: Ver si el host está realmente saturado de CPU

cr0x@server:~$ uptime
 14:22:19 up 37 days,  6:11,  2 users,  load average: 18.42, 17.96, 16.10

Qué significa: Promedio de carga ~18 en una máquina de 8 núcleos es problema; en una de 32 núcleos puede estar bien. La carga por sí sola no prueba saturación de CPU.

Decisión: A continuación, revisa el idle de CPU y la cola de ejecución con top o mpstat. Si el idle de CPU es alto, pivota a E/S o bloqueos.

Tarea 2: Revisa idle de CPU, steal y los principales ofensores

cr0x@server:~$ top -b -n1 | head -25
top - 14:22:27 up 37 days,  6:11,  2 users,  load average: 18.42, 17.96, 16.10
Tasks: 512 total,   9 running, 503 sleeping,   0 stopped,   0 zombie
%Cpu(s): 94.7 us,  2.1 sy,  0.0 ni,  0.6 id,  0.0 wa,  0.0 hi,  0.3 si,  2.3 st
MiB Mem :  32114.2 total,   1221.4 free,  14880.3 used,  16012.5 buff/cache
MiB Swap:   2048.0 total,   2048.0 free,      0.0 used.  14880.9 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
21483 root      20   0 1614920  82364  19624 R 380.0   0.3  76:12.39 python
12914 root      20   0 2344812 109004  40220 R 160.0   0.3  21:44.07 node
 9881 root      20   0  986604  61172  17640 R  95.0   0.2  12:11.88 java

Qué significa: Idle de CPU ~0.6%: estás saturado de CPU. El steal es 2.3%: no es enorme, pero indica que el hipervisor está tomando algunos ciclos.

Decisión: Identifica esos PIDs: ¿están en contenedores? Si sí, mapea a IDs de contenedor. Si no, tienes un problema de proceso del host (o un contenedor ejecutándose con el namespace de PID del host—sí, la gente hace eso).

Tarea 3: Ranking rápido de contenedores vía docker stats

cr0x@server:~$ docker stats --no-stream
CONTAINER ID   NAME                    CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O         PIDS
a12b3c4d5e6f   api-prod-1              265.42%   612.4MiB / 2GiB        29.90%    1.2GB / 980MB    11.2MB / 0B      78
b98c7d6e5f4a   worker-prod-queue       410.11%   1.1GiB / 1GiB          110.02%   120MB / 98MB     2.4GB / 1.9GB    213
c11d22e33f44   redis-prod              18.33%    3.2GiB / 4GiB          80.00%    420MB / 390MB    0B / 0B          6

Qué significa: worker-prod-queue está caliente en CPU y excede el límite de memoria (110% implica riesgo de swap/oom según la configuración). Los números de CPU sobre 100% indican uso de múltiples núcleos.

Decisión: No limites CPU aún. Primero confirma si ese worker hace trabajo legítimo, está thrasheando por presión de memoria o está en un bucle. La presión de memoria puede causar consumo de CPU vía GC, churn de asignaciones o tormentas de reintentos.

Tarea 4: Mapear un PID del host a su contenedor

cr0x@server:~$ ps -o pid,comm,cgroup -p 21483 | sed -n '1,3p'
  PID COMMAND         CGROUP
21483 python          0::/docker/b98c7d6e5f4a9c5b6a3e1c2d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a

Qué significa: Este PID del host pertenece a un cgroup de Docker (ruta unificada cgroups v2 mostrada como 0::). El hash largo coincide con el prefijo de un ID de contenedor.

Decisión: Confirma con docker ps y docker inspect, luego investiga dentro de ese contenedor.

Tarea 5: Confirma la identidad del contenedor y su entrypoint

cr0x@server:~$ docker ps --no-trunc | grep b98c7d6e5f4a
b98c7d6e5f4a9c5b6a3e1c2d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a   acme/worker:7f3c1d   "python /app/worker.py"   3 days ago   Up 3 days   worker-prod-queue

Qué significa: Es un worker en Python, probablemente consumidores de colas o jobs por lotes.

Decisión: Si es un worker por lotes, las caps de CPU suelen ser seguras. Si es sensible a la latencia (API), los límites pueden convertirse en fallos autoinflictivos.

Tarea 6: Comprueba si el contenedor ya está limitado

cr0x@server:~$ docker inspect -f 'NanoCpus={{.HostConfig.NanoCpus}} CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} CpusetCpus={{.HostConfig.CpusetCpus}} CpuShares={{.HostConfig.CpuShares}}' worker-prod-queue
NanoCpus=0 CpuQuota=0 CpuPeriod=0 CpusetCpus= CpuShares=0

Qué significa: No hay caps de CPU o pesos explícitos configurados. Puede consumir toda la CPU que consiga.

Decisión: Tienes la opción de limitar. Pero primero, verifica qué está haciendo y revisa métricas de throttling después de aplicar un límite.

Tarea 7: Revisa estadísticas de CPU de cgroup v2 para throttling

cr0x@server:~$ CID=b98c7d6e5f4a9c5b6a3e1c2d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a
cr0x@server:~$ CGP=$(docker inspect -f '{{.HostConfig.CgroupParent}}' $CID); echo "${CGP:-/sys/fs/cgroup}"
/sys/fs/cgroup
cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpu.stat
usage_usec 129884420
user_usec 127110003
system_usec 2774417
nr_periods 0
nr_throttled 0
throttled_usec 0

Qué significa: En este host, el cgroup del contenedor existe bajo /sys/fs/cgroup/docker/<id>. No hay throttling aún porque no hay cuota configurada.

Decisión: Si luego estableces una cuota y nr_throttled sube rápidamente con alto throttled_usec, habrás creado un techo de rendimiento/latencia. Puede ser correcto, pero debe ser intencional.

Tarea 8: Valida si el “problema de CPU” es realmente tiempo de kernel o de usuario

cr0x@server:~$ pidstat -p 21483 1 3
Linux 6.5.0-18-generic (server) 	01/02/2026 	_x86_64_	(16 CPU)

14:23:31      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
14:23:32        0     21483   92.00    6.00    0.00    0.00   98.00     7  python
14:23:33        0     21483   93.00    5.00    0.00    0.00   98.00     7  python
14:23:34        0     21483   90.00    7.00    0.00    0.00   97.00     7  python

Qué significa: Mayormente tiempo de usuario. Eso apunta a cómputo a nivel de aplicación o un bucle cerrado, no a sobrecarga del kernel.

Decisión: Perfila dentro del contenedor (o en el host para ese PID) y busca funciones calientes, bucles de espera activa o una acumulación de cola que impulse trabajo legítimo.

Tarea 9: Entra al contenedor y verifica si hay fiesta de hilos

cr0x@server:~$ docker exec -it worker-prod-queue bash -lc 'ps -eLo pid,tid,pcpu,comm --sort=-pcpu | head'
  PID   TID %CPU COMMAND
    1     1 96.4 python
    1    42 92.1 python
    1    43 91.8 python
    1    44 90.9 python
    1    45 90.2 python
   88    88  1.1 bash

Qué significa: Múltiples hilos están calientes. Para Python esto puede significar múltiples procesos/hilos, extensiones en C haciendo trabajo, o algo como gevent/eventlet que aún consume CPU.

Decisión: Si es una pool de workers, limita o reduce la concurrencia. Si no se supone que sea multi-hilo, revisa paralelismo accidental (por ejemplo, una librería que crea hilos, o un cambio de configuración).

Tarea 10: Usa perf para encontrar hotspots (lado host, no requiere herramientas en el contenedor)

cr0x@server:~$ sudo perf top -p 21483 -n 5
Samples: 1K of event 'cycles', 4000 Hz, Event count (approx.): 250000000
Overhead  Shared Object          Symbol
  22.11%  python                 [.] _PyEval_EvalFrameDefault
  15.37%  python                 [.] PyObject_RichCompare
  10.02%  libc.so.6              [.] __memcmp_avx2_movbe
   7.44%  python                 [.] list_contains
   6.98%  python                 [.] PyUnicode_CompareWithASCIIString

Qué significa: La CPU está yendo a evaluación del intérprete y comparaciones. Eso es cómputo real, no un bug del kernel. También sugiere que la carga puede ser intensiva en comparaciones de datos (filtros, deduplicación, escaneos).

Decisión: Para contención inmediata: limita la CPU o regula la concurrencia del trabajo. A largo plazo: perfila a nivel de aplicación; tal vez estás haciendo comparaciones O(n²) en un lote de cola.

Chiste #1: Si tu worker está haciendo O(n²) dentro de un bucle, felicitaciones—ha reinventado el calefactor espacial.

Tarea 11: Revisa la presión de la cola de ejecución por CPU

cr0x@server:~$ mpstat -P ALL 1 2
Linux 6.5.0-18-generic (server) 	01/02/2026 	_x86_64_	(16 CPU)

14:24:31     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %idle
14:24:32     all    92.11    0.00    5.02    0.00    0.00    0.44    2.12    0.31
14:24:32       7    99.00    0.00    0.90    0.00    0.00    0.10    0.00    0.00
14:24:32       8    97.00    0.00    2.70    0.00    0.00    0.30    0.00    0.00

Qué significa: Varios CPUs están prácticamente al máximo. Si solo un par de núcleos estuvieran calientes, considerarías fijación por cpuset o revisar cuellos de botella mono-hilo.

Decisión: Si el host está globalmente saturado, limitar un contenedor es una medida de equidad. Si solo un núcleo está caliente, limitar por cuota no arreglará límites mono-hilo; arreglarías la concurrencia o el pinning.

Tarea 12: Inspeccionar restricciones de recursos del contenedor en cgroups v2 (quota y CPUs efectivas)

cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpu.max
max 100000
cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpuset.cpus.effective
0-15

Qué significa: cpu.max es “quota periodo”. max significa ilimitado. El período es 100000 microsegundos (100ms). CPUs efectivas muestran que el contenedor puede ejecutarse en las 16 CPUs.

Decisión: Si quieres “equivalente a 2 CPUs”, establecerás quota a 200000 para periodo 100000, o usarás el flag de conveniencia de Docker --cpus=2.

Tarea 13: Aplica una limitación de CPU en vivo (con cuidado) y verifica throttling

cr0x@server:~$ docker update --cpus 4 worker-prod-queue
worker-prod-queue
cr0x@server:~$ docker inspect -f 'CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} NanoCpus={{.HostConfig.NanoCpus}}' worker-prod-queue
CpuQuota=400000 CpuPeriod=100000 NanoCpus=4000000000
cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpu.max
400000 100000

Qué significa: El contenedor ahora puede consumir hasta el equivalente a 4 CPUs por período de 100ms. Docker tradujo --cpus en quota/period.

Decisión: Observa la CPU del host y la latencia del servicio. Si el contenedor ruidoso no es crítico, esto suele ser la solución inmediata correcta. Si es crítico, puede que necesites asignar más CPU o escalar horizontalmente.

Tarea 14: Confirma si la limitación está causando throttling (y si es aceptable)

cr0x@server:~$ sleep 2; cat /sys/fs/cgroup/docker/$CID/cpu.stat
usage_usec 131992884
user_usec 129050221
system_usec 2942663
nr_periods 2201
nr_throttled 814
throttled_usec 9811123

Qué significa: Está ocurriendo throttling (nr_throttled aumentó). El contenedor está alcanzando su quota de CPU. Eso es esperado cuando limitas una carga caliente.

Decisión: Decide si el throttling es el objetivo (proteger otros servicios) o si limitaste demasiado (colapso de throughput, crecimiento de backlog). Revisa la profundidad de la cola y la latencia. Si el backlog crece, sube la cuota o escala los workers.

Tarea 15: Identifica los hilos principales del contenedor desde el host (sin ejecutar exec)

cr0x@server:~$ ps -T -p 21483 -o pid,tid,pcpu,comm --sort=-pcpu | head
  PID   TID %CPU COMMAND
21483 21483 96.2 python
21483 21510 92.0 python
21483 21511 91.7 python
21483 21512 90.5 python
21483 21513 90.1 python

Qué significa: Los hilos calientes son visibles desde el host. Útil cuando los contenedores son imágenes mínimas sin herramientas de depuración.

Decisión: Si un hilo domina, estás en tierra mono-hilo. Si muchos hilos están calientes, las cuotas de CPU se comportarán de forma más predecible.

Tarea 16: Comprueba si estás luchando contra steal de CPU (oversubscription en hosts VM)

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
13  0      0 125132 210204 8023412    0    0     0     5 2841 7102 91  5  2  0  2
18  0      0 124980 210204 8023520    0    0     0     0 2911 7299 90  5  2  0  3
16  0      0 124900 210204 8023604    0    0     0     0 2780 7010 89  6  2  0  3
14  0      0 124820 210204 8023710    0    0     0    10 2894 7255 90  5  2  0  3
15  0      0 124700 210204 8023794    0    0     0     0 2810 7098 91  5  1  0  3

Qué significa: st (steal) es 2–3%. No es catastrófico. Si ves 10–30%, no estás “limitado por CPU”, estás “limitado por vecinos de hardware”.

Decisión: Si el steal es alto, las caps no arreglarán la experiencia. Mueve cargas, cambia el tamaño de instancias o modifica la colocación de hosts. De lo contrario estás optimizando la capa equivocada.

Cómo limitar la CPU correctamente (sin autosabotearse)

“Solo limita” es la forma de crear el siguiente incidente. Los límites de CPU son un contrato: le dices al kernel, “Esta carga puede ser ralentizada para proteger al resto.” Haz ese contrato explícito y verificable.

Elige el control adecuado: quota vs shares vs cpuset

1) Quota/periodo de CPU (el límite sensato por defecto)

Usar cuando: quieres que un contenedor obtenga como máximo N CPUs de tiempo, pero aún permitir que el planificador lo coloque entre núcleos.

  • Flag de Docker: --cpus 2 (conveniencia) o --cpu-quota y --cpu-period.
  • Mecanismo del kernel: control de ancho de banda CFS.

Qué puede salir mal: límites agresivos causan throttling fuerte, lo que puede crear latencia en ráfagas. Tu app se vuelve un metrónomo: corre, es throttled, corre otra vez.

2) CPU shares (un peso de equidad, no un tope)

Usar cuando: quieres prioridad relativa entre contenedores bajo contención, pero estás OK con que cualquier contenedor use CPU sobrante cuando el host está inactivo.

  • Flag de Docker: --cpu-shares.

Qué puede salir mal: la gente configura shares esperando un tope, y luego se pregunta por qué un contenedor desbocado sigue saturando el host por la noche.

3) Cpuset (fijar a núcleos)

Usar cuando: tienes restricciones de licencia, estás aislando vecinos ruidosos, o manejas conscientemente la localidad NUMA/caché. Esto es para adultos que gustan de gráficos.

  • Flag de Docker: --cpuset-cpus 0-3.

Qué puede salir mal: fijar a los núcleos “equivocados” puede colisionar con afinidad de IRQs, otros workloads fijados, o dejar la mitad de la máquina ociosa mientras un núcleo se derrite.

Mi enfoque preferido en producción

  1. Comienza con una cuota usando --cpus, colócala lo bastante alta para evitar throttling constante.
  2. Mide el throttling vía cpu.stat después del cambio. El throttling no es automáticamente malo; el throttling inesperado sí lo es.
  3. Si necesitas aislamiento más fuerte (ej., multi-tenant), añade cpuset, pero solo después de auditar la topología de CPU y la distribución de interrupciones.
  4. Usa shares para sesgar servicios críticos por encima de best-effort, pero no finjas que es un cinturón de seguridad.

¿Cuánta CPU deberías dar?

No adivines. Usa la demanda observada de la carga y la tolerancia del negocio.

  • Para servicios API, mantén suficiente CPU para proteger la latencia p99. Si la CPU está caliente, escalar suele ser más seguro que limitar.
  • Para workers/batch, limita por equidad y ajusta la concurrencia para empatar con la cuota. Si no, solo throttlearás una estampida.
  • Para bases de datos, ten cuidado: las caps de CPU pueden amplificar la latencia de cola y crear contención de bloqueos. Prefiere nodos dedicados o cpusets si debes aislar.

Validación consciente del throttling: qué vigilar después de limitar

Después de aplicar un límite, revisa estas señales:

  • Carga del host e idle de CPU: ¿Recuperaron otros servicios?
  • Throttling del contenedor: ¿Sube constantemente nr_throttled?
  • Profundidad de cola/backlog: Si el backlog crece, redujiste el throughput por debajo de la tasa de llegada.
  • Latencia/tasas de error: Para servicios sincrónicos, los límites suelen manifestarse como timeouts, no como “respuestas más lentas” limpias.

Chiste #2: Las cuotas de CPU son como presupuestos corporativos—a todos les caen mal, pero la alternativa es que un equipo compre seis máquinas de espresso y lo llame “infraestructura”.

Compose, Swarm y la trampa “¿por qué no funcionó mi límite?”

Hay dos clases de tickets “mi límite de CPU no funciona”:

  1. Nunca se aplicó. El orquestador lo ignoró o lo pusiste en la sección equivocada.
  2. Se aplicó, pero mediste mal. Esperabas “50%” y obtuviste “sigue caliente” porque el host tiene muchos núcleos, o porque la carga hace ráfagas y es throttled después.

Docker Compose: trampas de versión

Compose históricamente tuvo dos lugares para límites: el estilo antiguo cpu_shares/cpus y el estilo Swarm deploy.resources. La trampa: Compose no-Swarm ignora deploy en muchas configuraciones. La gente pega configs de posts y asume que el kernel obedece YAML.

Si quieres un límite confiable en local/Compose, valídalo con docker inspect después de docker compose up. No confíes en el archivo.

cr0x@server:~$ docker compose ps
NAME                 IMAGE             COMMAND                  SERVICE   CREATED         STATUS         PORTS
stack_worker_1        acme/worker:7f3c1d "python /app/worker.py" worker    2 minutes ago   Up 2 minutes

cr0x@server:~$ docker inspect -f 'CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} NanoCpus={{.HostConfig.NanoCpus}}' stack_worker_1
CpuQuota=200000 CpuPeriod=100000 NanoCpus=2000000000

Qué significa: Los límites están realmente aplicados. Si muestran ceros, tus ajustes de Compose no se aplicaron.

Decisión: Arregla la configuración para que los límites se apliquen donde tu runtime los respete, o aplícalos con docker update y luego codifícalo correctamente.

Diferencias entre Swarm y Kubernetes (operativas, no filosóficas)

  • En Swarm, deploy.resources.limits.cpus es real y se aplica porque Swarm programa tareas con esas restricciones.
  • En Kubernetes, límites y requests de CPU interactúan con clases QoS. Los límites pueden throttlear; los requests influyen en el scheduling. “Puse un límite” no es lo mismo que “garanticé CPU”.

Si depuras en hosts Docker pero tu modelo mental viene de Kubernetes, ten cuidado: puedes estar perdiendo la matiz “request vs limit” que afecta el comportamiento de vecinos ruidosos.

Tres mini-historias corporativas desde las trincheras de la CPU

Mini-historia 1: El incidente causado por una suposición equivocada

La empresa tenía un host Docker ejecutando “solo unos pocos servicios”. Esa frase es una mentira que la gente se dice para sentirse en control. Uno de los servicios era una API, otro un worker de colas, y un tercero era un sidecar de métricas que nadie quería tocar porque “funcionaba”.

Durante un periodo de alta carga, el host empezó a alcanzar 100% de CPU. El ingeniero de guardia abrió el panel, vio la línea de CPU de la API dispararse, y tomó la suposición razonable pero equivocada: “La API es el problema.” Limitó la API a 1 CPU con una actualización en vivo. La CPU del host bajó. Todos se relajaron durante unos ocho minutos.

Entonces subió la tasa de errores. Aparecieron timeouts. La API no estaba “arreglada”; estaba estrangulada. El verdadero culpable era el contenedor worker que inundaba Redis con reintentos porque había alcanzado un límite de memoria y comenzó a hacer swap dentro del entorno del cgroup del contenedor. El worker creó una tormenta de reintentos. La API solo sufrió al intentar mantenerse al día.

Lo que hizo esta incidencia educativa fue el detalle del postmortem: los picos de CPU de la API eran síntoma de contención aguas abajo y reintentos, no la causa raíz. Cuando se limitó la API, ya no pudo responder lo suficientemente rápido para descargar carga con gracia. El worker seguía ruidoso; la API solo quedó débil.

La solución no fue dramática. Quitaron el límite de la API, añadieron una cuota al worker y—esta parte siempre es incómoda—redujeron la concurrencia del worker para que coincidiera con el nuevo contrato de CPU. La tormenta de reintentos paró. Redis se calmó. La CPU se normalizó. La lección quedó: limita al matón, no a la víctima.

Mini-historia 2: La optimización que salió mal

Un equipo distinto tenía una tubería de ingestión por lotes en contenedores. Querían más throughput y notaron que la CPU estaba infrautilizada en horas valle. Alguien propuso “más paralelismo” aumentando threads de 4 a 32. Sonaba moderno. Se vio bien en una prueba rápida. En producción, se convirtió en una desintegración en cámara lenta.

La tubería parseaba datos comprimidos y hacía validación de esquemas. Con 32 threads por contenedor y varios contenedores por host, crearon una estampida en CPU y memoria. El cambio de contexto subió. La localidad de cache empeoró. El planificador del host hizo su mejor imitación de un malabarista en tormenta.

Intentaron limitar CPU para “estabilizarlo”. Se estabilizó, sí—de la misma manera que un coche “se estabiliza” cuando choca contra una pared. El throttling se disparó y el throughput colapsó. Las colas de trabajo se acumularon, y el equipo empezó a escalar contenedores. Eso empeoró la contención, porque el cuello de botella era la CPU y el ancho de banda de memoria del host, no la cantidad de contenedores.

Lo que finalmente lo arregló fue aburrido: redujeron los threads del worker, luego aumentaron ligeramente el número de contenedores pero fijaron la carga por lotes a un rango de cpuset alejado de servicios sensibles a latencia. También aprendieron a medir nr_throttled y no solo el porcentaje de CPU. La “optimización” falló porque asumió que la CPU es lineal. En sistemas reales, el paralelismo compite consigo mismo.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Hay organizaciones que no hacen heroicidades porque no las necesitan. Un equipo de plataforma tenía una regla estricta: cada contenedor en producción debe declarar límites de CPU y memoria, y deben validarse por automatización tras el despliegue.

Los ingenieros se quejaban. Decían que ralentizaba el envío. Decían que era “pensamiento Kubernetes” aunque esto fuera Docker simple. El equipo de plataforma los ignoró con educación y siguió haciendo cumplir la norma. También exigieron que cada servicio defina un “modo de degradación”: cuando la CPU esté limitada, ¿descartas trabajo, lo encolas o fallas rápido?

Una tarde, una actualización de una librería de terceros introdujo una regresión: un bucle activo se disparaba bajo un patrón de entrada raro. Un subconjunto de peticiones causó picos de CPU. Normalmente esto habría tumbado el host y creado un incidente que afectara múltiples servicios.

En cambio, el contenedor afectado alcanzó su cuota de CPU y fue throttled. Se volvió más lento, sí, pero no dejó sin recursos al resto del nodo. Los demás servicios siguieron atendiendo. El monitoreo mostró una señal clara: aumentó el throttling de ese servicio. El de guardia hizo un rollback rápido. No hubo fallo en cascada, ni “todos los servicios degradados”, ni sala de guerra a medianoche.

La práctica aburrida—siempre establecer límites, siempre validarlos, siempre definir comportamiento bajo restricción—no solo previno contención de recursos. Hizo el modo de fallo legible.

Errores comunes: síntoma → causa raíz → solución

Esta sección te salva de repetir los grandes éxitos.

1) Síntoma: El host tiene CPU al 100%, pero docker stats no muestra nada extremo

Causa raíz: Procesos del host están calientes (journald, node exporter, hilos del kernel), o contenedores corren con namespace de PID del host, o el muestreo de docker stats se pierde picos cortos.

Solución: Usa herramientas de PID del host primero: top, luego mapea PIDs calientes a contenedores vía ps -o cgroup. Si no está en cgroup de Docker, no es un problema de contenedor.

2) Síntoma: Tras poner --cpus, el servicio se vuelve más lento y aumentan los timeouts

Causa raíz: Limitaste un servicio sensible a latencia por debajo de sus necesidades p99, causando ráfagas de throttling y encolamiento de peticiones.

Solución: Quita o sube la cuota; escala horizontalmente; añade backpressure y límites de concurrencia. Mide cpu.stat y latencia de peticiones en conjunto.

3) Síntoma: Pusiste CPU shares pero el contenedor sigue saturando el host

Causa raíz: Shares son pesos, no un tope. Con CPU ociosa disponible, el contenedor puede tomarla.

Solución: Usa --cpus o --cpu-quota para un tope duro. Mantén shares para priorización bajo contención.

4) Síntoma: El promedio de carga es enorme, pero el idle de CPU también es alto

Causa raíz: Tareas bloqueadas (espera E/S, sleep ininterrumpible), contención de locks, o stalls del filesystem. El promedio de carga cuenta más que tareas ejecutables.

Solución: Revisa top por wa, usa iostat si está disponible, inspecciona tareas bloqueadas y busca cuellos de botella en almacenamiento/red. No “limites CPU” para un problema de E/S.

5) Síntoma: La CPU está caliente solo en un núcleo y el rendimiento es terrible

Causa raíz: Cuello de botella mono-hilo, lock global, o una partición/shard caliente.

Solución: No añadas límites de CPU; arregla concurrencia o particionado. Si debes aislar, pinning por cpuset puede evitar que un hilo caliente interfiera con todo, pero no lo hará más rápido.

6) Síntoma: El uso de CPU parece correcto, pero el servicio está lento

Causa raíz: Throttling: el contenedor está limitado y pasa tiempo sin poder ejecutarse. El porcentaje de CPU puede verse moderado porque el tiempo throttled no es “uso de CPU”.

Solución: Lee cpu.stat (nr_throttled, throttled_usec). Sube la cuota, reduce concurrencia o escala.

7) Síntoma: Los picos de CPU ocurren después de “optimizar” logs o métricas

Causa raíz: Métricas de alta cardinalidad, formateo costoso de logs, logging síncrono o contención en pipelines de telemetría.

Solución: Reduce cardinalidad, muestrea, agrupa por lotes o mueve formateo pesado fuera de rutas calientes. Limita también los sidecars de telemetría; no son inocentes.

8) Síntoma: Tras fijar cpuset, el throughput cae y algunas CPUs quedan ociosas

Causa raíz: Selección de núcleos mala, interferencia con IRQs, desajuste NUMA, o fijar muy pocos núcleos para patrones de ráfaga.

Solución: Prefiere cuota primero. Si usas cpuset, audita la topología de CPU, considera nodos NUMA y deja margen para trabajo del kernel/interrupciones.

Listas de verificación / plan paso a paso

Paso a paso: localizar el contenedor ruidoso

  1. Mide el host: uptime, top. Confirma que el idle de CPU es bajo y la división user/system.
  2. Clasifica contenedores rápidamente: docker stats --no-stream.
  3. Clasifica PIDs del host: en top ordena por CPU, copia los PIDs principales.
  4. Mapea PID → cgroup: ps -o cgroup -p <pid>. Si está bajo /docker/<id>, tienes tu contenedor.
  5. Confirma nombre/imagen del contenedor: docker ps --no-trunc | grep <id> y docker inspect.
  6. Valida qué está haciendo: pidstat, perf top, o ps dentro del contenedor.

Paso a paso: limitarlo de forma segura

  1. Decide el objetivo: proteger otras cargas vs preservar throughput de ésta.
  2. Elige el control: cuota (--cpus) para la mayoría; shares para pesos relativos; cpuset para aislamiento duro.
  3. Aplica el límite en vivo (si es necesario): docker update --cpus N <container>.
  4. Verifica que se aplicó: docker inspect y cat cpu.max (v2) o los archivos equivalentes v1.
  5. Mide el throttling: revisa cpu.stat después de unos segundos.
  6. Observa los SLOs del servicio: latencia, errores, profundidad de cola, reintentos. Si empeoran, sube la cuota o escala.
  7. Hazlo permanente: actualiza Compose/Swarm o tu pipeline de despliegue; no dejes docker update en vivo como magia tribal.

Paso a paso: prevenir recurrencias

  1. Define defaults: cada servicio declara límites de CPU y memoria.
  2. Valida automáticamente: verificaciones post-despliegue comparan límites deseados vs docker inspect.
  3. Instrumenta throttling: alerta sobre incrementos sostenidos de nr_throttled en servicios críticos.
  4. Alinea concurrencia: tamaños de pool de workers deben escalar con las cuotas de CPU; evita “32 threads porque hay cores en algún lugar”.
  5. Interruptores de emergencia: feature flags o rate limits para reducir creación de trabajo durante picos.

Preguntas frecuentes

1) ¿Por qué docker stats muestra 400% de CPU para un contenedor?

Porque está usando aproximadamente 4 núcleos de CPU durante la ventana de muestreo. El %CPU a menudo se normaliza por núcleo, así que el uso multinúcleo excede 100%.

2) ¿Es --cpus lo mismo que --cpuset-cpus?

No. --cpus usa quota/periodo CFS (tope basado en tiempo). --cpuset-cpus restringe en qué CPUs puede ejecutarse el contenedor (aislamiento basado en colocación). Resuelven problemas diferentes.

3) ¿Las CPU shares evitarán que un contenedor desbocado pegue el host?

Sólo cuando hay contención. Si el host tiene CPU ociosa, las shares no detendrán que un contenedor la consuma. Usa quota para un tope duro.

4) ¿Cómo sé si el throttling me está perjudicando?

Lee cpu.stat. Si nr_throttled y throttled_usec suben continuamente mientras la latencia/backlog empeoran, limitaste demasiado para la carga actual.

5) ¿Por qué el promedio de carga del host es alto pero el idle de CPU no es cero?

El promedio de carga incluye tareas esperando E/S (sleep ininterrumpible), no solo tareas ejecutables. Carga alta con idle significativo puede significar stalls de almacenamiento, bloqueos u otros bloqueos.

6) ¿Puedo limitar la CPU en un contenedor en ejecución sin reiniciarlo?

Sí: docker update --cpus N <container> (y flags relacionados) se aplica en vivo. Trátalo como una palanca de emergencia; codifica los cambios en tu configuración de despliegue tras el incidente.

7) Puse límites en docker-compose.yml bajo deploy: pero nada cambió. ¿Por qué?

Porque los límites bajo deploy son principalmente para Swarm. Muchos Compose sin Swarm los ignoran. Siempre confirma con docker inspect que los límites se aplicaron.

8) ¿Debería limitar el contenedor de base de datos cuando la CPU está caliente?

Usualmente no, no como primer movimiento. Las bases de datos bajo presión de CPU suelen necesitar más CPU, optimización de queries o aislamiento. Limitar puede convertir una contención breve en latencias largas y acumulación de locks.

9) ¿“CPU 100%” dentro de un contenedor significa lo mismo que en el host?

Depende de la herramienta y normalización. Dentro del contenedor puedes ver una vista que ignora la capacidad del host. Confía en el accounting a nivel host y en estadísticas de cgroup para límites y throttling.

10) ¿Vale la pena el pinning por cpuset para problemas de vecinos ruidosos?

A veces. Es poderoso y determinista, pero fácil de hacer mal. Empieza con cuotas y shares; pasa a cpuset cuando necesites aislamiento duro y entiendas la topología de CPU.

Conclusión: pasos prácticos siguientes

Cuando hosts Docker llegan al 100% de CPU, la jugada ganadora no es “reinicia cosas hasta que el gráfico mejore.” La jugada ganadora es: identifica el contenedor exacto (y el PID), valida si está haciendo trabajo real o tonterías, luego limita con intención y confirma el comportamiento de throttling.

La próxima vez que el host esté al rojo, haz esto:

  • Usa top para obtener los PIDs más calientes, mapea a contenedores vía cgroups.
  • Usa docker stats como pista, no como veredicto.
  • Antes de limitar, decide si la carga es sensible a latencia o orientada a throughput.
  • Aplica docker update --cpus para contención rápida, luego confirma vía cpu.stat si el throttling es aceptable.
  • Tras el incidente, haz permanentes los límites, valídalos automáticamente y alinea la concurrencia de workers al contrato de CPU.

Las emergencias de CPU rara vez son misteriosas. Suelen ser simplemente poco instrumentadas, sobreasumidas y sin límites. Arregla esas tres cosas, y tus rotaciones de guardia serán más cortas—y un poco menos poéticas.

← Anterior
Rutas de dispositivo faltantes en ZFS: usar by-id y WWN como un adulto
Siguiente →
ZFS dnodes y metadatos: por qué los metadatos pueden ser tu verdadero cuello de botella

Deja un comentario