No eliges “contenedores” o “VMs” en abstracto. Los eliges mientras alguien te está llamando porque la latencia p99 se duplicó,
el trabajo por lotes tardó 3× más, o una actualización “menor” de nodos se convirtió en una semana de arqueología de culpas.
La CPU es donde esta decisión se vuelve política. Todo el mundo cree entender la CPU. Luego se encuentran con el throttling, el steal time, NUMA,
y la verdad incómoda: el scheduler no se preocupa por tus compromisos de sprint.
Perfiles de CPU que realmente importan
“Limitado por CPU” no es una descripción suficiente. La elección entre contenedores y VMs depende de la forma de la demanda de CPU y de
las penalizaciones que estés dispuesto a aceptar: jitter, latencia en la cola, sobrecarga por cambios de contexto, churn de caché, comportamiento ante preempciones y cuánto confías en las cargas vecinas.
Perfil 1: Sensible a latencia en cola (p99/p999 es el producto)
Piensa en: trading, pujas de anuncios, OLTP, servicios request/response con SLO estrictos, o cualquier cosa donde un solo núcleo lento arruina tu día.
Estas cargas odian:
- Throttling de CPU (cgroup quotas) y programación con ráfagas.
- Preempciones impredecibles por otras cargas (vecinos ruidosos).
- Acceso a memoria cross-NUMA y fallos de caché.
- Tormentas de interrupciones que caen sobre “tus” núcleos.
Usualmente quieren tiempo de CPU garantizado, aislamiento de cores y ubicación estable. Los contenedores pueden ofrecer esto, pero solo si
realmente lo configuras. Las VMs también pueden lograrlo, pero solo si dejas de fingir que el overcommit es comida gratis.
Perfil 2: Batch limitado por throughput (terminar para la mañana, a nadie le importa el jitter)
Piensa en: ETL, codificación de vídeo, indexado, generación de features ML. Estas cargas se preocupan por los segundos-CPU totales y por escalar.
Toleran el jitter. No toleran que les cobres por núcleos inactivos.
Los contenedores suelen ganar aquí porque puedes empacarlos apretado, escalarlos rápido y dejar que el scheduler haga su trabajo. Las VMs pueden competir
cuando necesitas límites de inquilino estrictos o kernels distintos, pero pagarás en densidad y fricción operativa.
Perfil 3: Interactivo con ráfagas (CPU espigada, humano en el loop)
Piensa en: runners de CI, entornos de previsualización para desarrolladores, dashboards internos que se disparan en despliegues. Estos quieren arranque rápido y elasticidad.
Operacionalmente, los contenedores tienden a ganar. En CPU, ganan siempre que no “optimices” poniendo límites de CPU agresivos que convierten las ráfagas en miseria por throttling.
Perfil 4: Modo mixto “está bien hasta que no lo está” (hosts compartidos, muchos servicios)
Este es el defecto corporativo: muchos servicios pequeños, algunos medianos y una carga misteriosa que nadie posee.
La contención de CPU aquí es un problema de gobernanza disfrazado de técnico.
Los contenedores te dan control más fino y mejor densidad, pero también más formas de crear colisiones por error. Las VMs te dan una pared más gruesa, lo cual es agradable hasta que te das cuenta de que aún compartes la CPU física y la pared no es insonorizada.
Perfil 5: Comportamiento CPU especial (real-time, DPDK, tipo HPC)
Si necesitas planificación en tiempo real, enrutamiento predecible de interrupciones, o red en espacio de usuario que quiere núcleos dedicados, estás en la categoría de “deja de adivinar”.
Ambos, contenedores y VMs, pueden funcionar, pero probablemente acabarás haciendo pinning de CPU, isolcpus, hugepages y colocación NUMA explícita. En ese punto, tu problema no es “contenedores vs VMs”. Es si tu equipo de plataforma puede soportar ajustes deterministas sin romperlo todo.
Un modelo mental de CPU: qué hacen realmente el kernel y el hipervisor
Los contenedores no son pequeñas VMs. Son procesos con opiniones: namespaces para mentir sobre lo que pueden ver, y cgroups para hacer cumplir lo que pueden usar. El scheduler de CPU sigue siendo el scheduler del kernel del host.
Las VMs ejecutan un kernel invitado en CPUs virtuales (vCPUs). Esas vCPUs se programan en CPUs físicas por el hipervisor/kernel del host.
Así que tienes dos schedulers: el del invitado programando hilos en vCPUs, y luego el host programando vCPUs en pCPUs.
Esa doble capa es a la vez una ventaja (aislamiento) y una fuente de rarezas (steal time, interferencia de planificación).
Tiempo de CPU: las tres métricas grandes que deciden tu destino
- Uso: cuánto CPU consume realmente tu carga.
- Espera: tiempo ejecutable pero sin correr (colas, contención).
- Jitter: varianza de latencia por scheduling, throttling, interrupciones y efectos de caché.
Los contenedores tienden a minimizar la sobrecarga y maximizar la densidad, pero aumentan la probabilidad de “espera” y “jitter” a menos que tengas disciplina en la gobernanza de recursos.
Las VMs añaden una capa de sobrecarga y complejidad en la planificación, pero pueden reducir el acoplamiento entre inquilinos cuando se configuran correctamente. La frase clave es “cuando se configuran correctamente.” La mayoría no lo está.
Cuotas vs shares: cómo los contenedores obtienen “menos CPU” sin que lo notes
En cgroup v2, el control de CPU suele ser una mezcla de:
- weights (prioridad relativa bajo contención), y
- max (tope duro que puede causar throttling incluso cuando hay CPU libre en otro lugar, según la configuración y el comportamiento de ráfagas).
El modo de falla es clásico: pones límites de CPU “por equidad”, luego tu servicio sensible a latencia alcanza el límite durante un pico,
se le aplica throttling y la p99 se desploma mientras la CPU media parece “razonable”.
Steal time: cómo las VMs te dicen “alguien se comió mi almuerzo”
En entornos virtualizados, steal es tiempo de CPU que el invitado quería pero no obtuvo porque el host corría otra cosa.
Un steal alto es el equivalente a hacer la fila del café mientras tu reunión empieza sin ti.
NUMA: el impuesto que pagas cuando la memoria está “allá”
En sistemas multi-socket, CPU y memoria están organizadas en nodos NUMA. Acceder a memoria local es más rápido que a memoria remota.
Cuando programas hilos en un socket y su memoria vive en otro, obtienes rendimiento que se siente como pérdida de paquetes: esporádico,
difícil de reproducir y siempre se culpa “a la red” primero.
Comportamiento de caché y cambios de contexto: la factura oculta
Si empaquetas muchos contenedores en un host, aumentas los cambios de contexto y la presión de caché. Eso no es automáticamente malo.
Es malo cuando esperas latencias en cola de un par de milisegundos y tratas la CPU como un bien infinitamente divisible.
Idea parafraseada de Werner Vogels (CTO de Amazon): Lo construyes, lo operas; la propiedad mejora la fiabilidad.
Se aplica aquí también: el equipo que establece la política de CPU debería recibir el pager.
Quién gana cuándo: contenedores vs VMs según perfil de CPU
Servicios sensibles a latencia: normalmente VMs para aislamiento fuerte, contenedores para plataformas disciplinadas
Si ejecutas una plataforma multi-inquilino donde los equipos no se coordinan, las VMs son el valor predeterminado más seguro para servicios sensibles a latencia.
No porque las VMs sean mágicas, sino porque los límites son más difíciles de eludir por error. Un contenedor sin límites de CPU y sin aislamiento puede arruinar todo un nodo; una VM también puede, pero requiere otro tipo de mala configuración.
Si ejecutas una plataforma Kubernetes disciplinada con:
- Pods con QoS Guaranteed (requests == limits para CPU),
- política static CPU manager para cores fijados donde haga falta,
- planificación consciente de NUMA,
- afinidad de IRQs ajustada en caminos críticos,
…entonces los contenedores pueden ofrecer excelente latencia en cola con mayor densidad que las VMs. Pero eso no es “Kubernetes por defecto”. Eso es
“Kubernetes después de leer las notas al pie y pagar el impuesto”.
Computación stateless de alto throughput: contenedores ganan en densidad y velocidad operativa
Para cómputo stateless, escalable horizontalmente y donde el jitter es aceptable, los contenedores son la mejor herramienta. La sobrecarga es menor,
la programación es más simple y obtienes mejor bin packing. Además: las actualizaciones continuas, el autoscaling y los rollback rápidos son más fáciles de ejecutar
sin tratar la flota de hipervisores como un artefacto sagrado.
Apps legacy y kernels distintos: las VMs ganan, y ni siquiera es un debate
Si necesitas un kernel diferente, módulos de kernel, o ejecutas software que asume que posee el sistema (hola, sistemas de licencias antiguos), usa una VM.
Los contenedores comparten el kernel del host; si el kernel es la capa de compatibilidad que necesitas controlar, los contenedores son la abstracción equivocada.
Riesgo de vecino ruidoso: las VMs reducen el radio de explosión; los contenedores necesitan políticas y aplicación
Tanto contenedores como VMs comparten la misma CPU física. La diferencia es cuántas capas tienes que fallar antes de que una carga dañe a otra.
- En contenedores, el scheduler del kernel se comparte directamente. Los errores en cgroups se ven de inmediato.
- En VMs, contar mal vCPUs, el overcommit y la contención del host aparecen como steal time y jitter.
Cuando “casi metal” importa: los contenedores tienen menos sobrecarga, pero las VMs se pueden ajustar
Los contenedores suelen tener menos sobrecarga de CPU porque no hay un segundo kernel ni dispositivos emulados (asumiendo que no haces algo creativo con la red). Las VMs pueden acercarse mucho al nativo con virtualización por hardware y drivers paravirtualizados, pero aún pagas algún coste en exits, interrupciones y la indirección de scheduling.
Consejo práctico: si buscas el último 5–10% en cargas intensivas de CPU, mide. No discutas.
Hechos e historia que explican las rarezas actuales
- 1970s: la virtualización no es nueva. Las mainframes de IBM ejecutaban máquinas virtuales décadas antes de que la nube la hiciera popular; la idea era aislamiento y utilización.
- 2006: AWS popularizó “alquila una VM”. EC2 convirtió las operaciones centradas en VMs en el modelo mental predeterminado para una generación de ingenieros.
- 2007–2008: llegaron los cgroups de Linux. Control groups dio al kernel una forma de contabilizar y limitar CPU/memoria por grupo de procesos—los contenedores luego cabalgaron esa ola.
- 2008: LXC hizo que los “contenedores” se sintieran reales. Antes de Docker, LXC ya usaba namespaces+cgroups; simplemente no tenía la misma UX ni la historia de distribución.
- 2013: Docker hizo contagioso el empaquetado. La característica asesina no fue el aislamiento; fue distribuir una app con sus dependencias de forma reproducible.
- 2014: Kubernetes convirtió la programación en un producto. Normalizó la idea de que la plataforma asigna CPU, no el equipo de la app pidiendo VMs.
- 2015+: la era Spectre/Meltdown aumentó la conversación sobre “sobrecarga”. Algunas mitigaciones afectaron rutas intensivas en syscall y virtualización; las discusiones de rendimiento se volvieron más matizadas.
- cgroup v2 unificó las semánticas de control. Redujo algunas rarezas heredadas pero introdujo nuevos mandos que la gente interpreta mal, especialmente alrededor de CPU.max y comportamiento de ráfagas.
- Las CPUs modernas no son “solo cores”. Turbo, escalado de frecuencia, SMT/Hyper-Threading y caches compartidas hacen que el aislamiento de CPU sea un ejercicio probabilístico a menos que hagas pinning y aislamiento.
Guion de diagnóstico rápido
Cuando el rendimiento de CPU se tuerce, tu primer trabajo es identificar si tratas con no hay suficiente CPU,
no te están programando, o haces demasiado por petición. Todo lo demás es decoración.
Primero: confirma el síntoma y el alcance
- ¿La latencia subió, el throughput bajó o ambos?
- ¿Es un pod/VM, un nodo, una AZ o en todas partes?
- ¿Comenzó después de un deploy, evento de escalado, reciclado de nodo, actualización de kernel o cambio de tipo de host?
Segundo: decide si es throttling/steal o saturación genuina
- Contenedores: revisa contadores de throttling de cgroup y la configuración CPU.max/CPU quota.
- VMs: revisa steal time y la contención a nivel de host.
- Ambos: revisa la longitud de la cola de ejecución (run queue) y la tasa de cambios de contexto.
Tercero: revisa patologías de colocación (NUMA, pinning, interrupciones)
- Desequilibrio NUMA o acceso a memoria cross-node.
- Cargas con CPU fijadas compartiendo hyperthreads hermanos.
- IRQs que caen en los mismos cores que tus hilos sensibles a latencia.
Cuarto: valida frecuencia y gestión de energía
- Frecuencia de CPU inesperadamente baja por governor de energía o throttling térmico.
- Comportamiento de Turbo cambiando tras actualizaciones de BIOS/firmware.
Quinto: solo entonces mira la “ineficiencia de la aplicación”
Si empiezas con flamegraphs cuando el kernel literalmente está aplicando throttling a tu proceso, estás haciendo arqueología a oscuras.
Tareas prácticas: comandos, salidas y decisiones (12+)
Estos son los cheques que puedes ejecutar hoy. Cada uno incluye: el comando, qué significa una salida plausible y qué decisión tomar después.
Ejecútalos en el nodo/host, luego dentro del contenedor o VM según corresponda. No te fíes solo de los dashboards; los dashboards son donde la matización va a morir.
Task 1: See if the host is CPU saturated (run queue and load vs CPU count)
cr0x@server:~$ nproc
32
cr0x@server:~$ uptime
15:42:10 up 12 days, 3:17, 2 users, load average: 48.12, 44.90, 39.77
Significado: Un load average ~48 en un host de 32 cores sugiere cola de ejecución intensa (o muchas sleeps ininterrumpibles; revisa las siguientes tareas).
Si la p99 está mal, esto es una señal roja.
Decisión: Si el load > cores durante periodos sostenidos, deja de discutir micro-optimizaciones. Reduce la contención: escala, reduce la colocalización, o aumenta la asignación de CPU.
Task 2: Check CPU breakdown and steal time (VM clue)
cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (node-7) 01/12/2026 _x86_64_ (32 CPU)
15:42:21 CPU %usr %nice %sys %iowait %irq %soft %steal %idle
15:42:22 all 62.10 0.00 10.43 0.12 0.00 0.55 18.34 8.46
15:42:23 all 63.02 0.00 10.21 0.09 0.00 0.61 17.80 8.27
15:42:24 all 61.77 0.00 10.66 0.14 0.00 0.59 18.02 8.82
Significado: 18% de steal es enorme. En una VM, esto significa que el hipervisor no está programando tus vCPUs.
Decisión: No optimices la app aún. Mueve la VM, reduce el overcommit del host, o negocia capacidad reservada. Si no puedes, acepta peor latencia en cola—es física con facturas.
Task 3: Check per-process CPU and context switching pressure
cr0x@server:~$ pidstat -w -u 1 3
Linux 6.5.0 (node-7) 01/12/2026 _x86_64_ (32 CPU)
15:43:10 UID PID %usr %system %CPU CPU Command
15:43:11 1001 24811 310.0 42.0 352.0 7 java
15:43:11 1001 24811 12000.0 800.0 - cswch/s nvcswch/s
15:43:11 1001 24811 12000.0 650.0 - java
Significado: Tasas de cambios de contexto extremadamente altas suelen indicar contención de hilos, churn de locks o oversubscription.
Decisión: Si esto es un contenedor en un nodo compartido, considera pinning y reducir la cotenencia. Si es una VM, verifica el conteo de vCPU vs hilos y revisa la planificación del host.
Task 4: For containers, verify cgroup v2 CPU limits (CPU.max)
cr0x@server:~$ cat /sys/fs/cgroup/cpu.max
200000 100000
Significado: Esto significa una cuota de 200ms de CPU por periodo de 100ms (efectivamente 2 cores). Si la carga supera eso en ráfagas, será throttled.
Decisión: Para servicios sensibles a latencia, evita topes duros de CPU salvo que estés aplicando un comportamiento predecible deliberado. Prefiere requests/QoS Guaranteed y cores dedicados.
Task 5: For containers, check throttling counters
cr0x@server:~$ cat /sys/fs/cgroup/cpu.stat
usage_usec 12888904512
user_usec 12110000000
system_usec 778904512
nr_periods 934112
nr_throttled 212334
throttled_usec 9901123456
Significado: Si nr_throttled es alto y throttled_usec es no trivial, tu contenedor está siendo pausado forzosamente por el kernel debido a límites de cuota.
Decisión: Si te importa la p99, o bien sube/elimina el límite o rediseña el patrón de ráfagas (por ejemplo, control de concurrencia). El throttling es una máquina de latencia.
Task 6: In Kubernetes, confirm QoS class (Guaranteed vs Burstable)
cr0x@server:~$ kubectl -n prod get pod api-7d9c6b8c9f-4kq2m -o jsonpath='{.status.qosClass}{"\n"}'
Burstable
Significado: Los pods Burstable pueden ser depriorizados bajo contención y son más propensos a sufrir variabilidad en tiempo de CPU.
Decisión: Para servicios críticos de latencia, apunta a Guaranteed (requests igual a limits) o usa nodos dedicados sin límites de CPU pero con control de admisión estricto.
Task 7: Check Kubernetes CPU requests/limits and spot “limit set, request tiny”
cr0x@server:~$ kubectl -n prod get pod api-7d9c6b8c9f-4kq2m -o jsonpath='{range .spec.containers[*]}{.name}{" req="}{.resources.requests.cpu}{" lim="}{.resources.limits.cpu}{"\n"}{end}'
api req=100m lim=2000m
Significado: El scheduler piensa que necesitas 0.1 core, pero puedes ráfaguear hasta 2 cores. Bajo contención, perderás prioridad de planificación y tendrás jitter.
Decisión: Para latencia estable, establece requests realistas. Para eficiencia de costes, no le mientas al scheduler y luego te quejes de los resultados.
Task 8: Check CPU manager policy on a Kubernetes node (pinning capability)
cr0x@server:~$ ps -ef | grep -E 'kubelet.*cpu-manager-policy' | head -n 1
root 1123 1 1 Jan10 ? 00:12:44 /usr/bin/kubelet --cpu-manager-policy=static --kube-reserved=cpu=500m --system-reserved=cpu=500m
Significado: La política static permite asignación exclusiva de CPU para pods Guaranteed (con requests de CPU enteros).
Decisión: Si necesitas latencia de cola consistente, considera nodos con static CPU manager más topology manager. Sin eso, estás apostando.
Task 9: Identify NUMA topology and whether your workload spans nodes
cr0x@server:~$ lscpu | egrep 'NUMA node|Socket|CPU\(s\)|Thread|Core'
CPU(s): 32
Thread(s) per core: 2
Core(s) per socket: 8
Socket(s): 2
NUMA node(s): 2
NUMA node0 CPU(s): 0-15
NUMA node1 CPU(s): 16-31
Significado: Dos nodos NUMA. Si tu proceso salta entre CPUs 0–31 y asigna memoria libremente, puedes pagar penalizaciones por memoria remota.
Decisión: Para servicios sensibles a latencia de CPU, mantiene CPU y memoria locales: pinnea CPUs, usa planificación consciente de NUMA, o ejecuta instancias más pequeñas que quepan en un nodo.
Task 10: Check which CPUs a process is allowed to run on (cpuset / affinity)
cr0x@server:~$ taskset -pc 24811
pid 24811's current affinity list: 0-31
Significado: El proceso puede correr en cualquier lugar. Eso maximiza flexibilidad pero puede aumentar churn de caché y efectos NUMA.
Decisión: Si ves jitter y problemas cross-NUMA, considera estrechar la afinidad (o usa funciones del orquestador para hacerlo de forma segura). Si el objetivo es throughput, déjalo flexible.
Task 11: Check interrupt distribution (IRQ affinity hot spots)
cr0x@server:~$ cat /proc/interrupts | head -n 8
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
24: 98122321 0 0 0 1022334 0 0 0 PCI-MSI 524288-edge eth0-TxRx-0
25: 0 93455321 0 0 0 993442 0 0 PCI-MSI 524289-edge eth0-TxRx-1
26: 0 0 90211233 0 0 0 889120 0 PCI-MSI 524290-edge eth0-TxRx-2
27: 0 0 0 88777210 0 0 0 901223 PCI-MSI 524291-edge eth0-TxRx-3
NMI: 1223311 1219988 1213444 1209987 1198877 1190021 1189911 1180221 Non-maskable interrupts
Significado: Las interrupciones de red están concentradas en CPUs específicas. Si tu carga sensible a latencia comparte esos CPUs, verás jitter.
Decisión: Ajusta la afinidad de IRQ o aísla CPUs para la carga. No “soluciones” la latencia en cola añadiendo reintentos; así es como los incidentes ganan personalidad.
Task 12: Check frequency governor and current MHz (silent performance killer)
cr0x@server:~$ cpupower frequency-info | egrep 'governor|current CPU frequency' | head -n 6
The governor "powersave" may decide which speed to use
current CPU frequency: 1200 MHz (asserted by call to hardware)
Significado: Governor powersave a 1.2GHz en un servidor que se espera trabaje caliente no es… ideal.
Decisión: Cambia a performance governor para nodos sensibles a latencia, o al menos valida la configuración de firmware/energía. Mide los ahorros antes de vender la idea.
Task 13: In a VM, correlate steal with host contention (guest view)
cr0x@server:~$ sar -u 1 3
Linux 6.5.0 (vm-12) 01/12/2026 _x86_64_ (8 CPU)
15:45:01 CPU %user %nice %system %iowait %steal %idle
15:45:02 all 52.11 0.00 9.88 0.10 22.34 15.57
15:45:03 all 50.90 0.00 10.12 0.08 21.77 17.13
15:45:04 all 51.33 0.00 9.94 0.09 22.01 16.63
Significado: Steal >20% significa que tu VM rutinariamente no se está programando. El invitado ve “idle” también, pero no es idle real.
Decisión: No añadas vCPUs a la reflexiva; eso a menudo empeora la planificación. En su lugar, reduce overcommit, mueve hosts, o usa instancias reservadas/pinning de CPU en el hipervisor.
Task 14: Spot hyperthread sibling contention (SMT/HT awareness)
cr0x@server:~$ for c in 0 1 2 3; do echo -n "cpu$c siblings: "; cat /sys/devices/system/cpu/cpu$c/topology/thread_siblings_list; done
cpu0 siblings: 0,16
cpu1 siblings: 1,17
cpu2 siblings: 2,18
cpu3 siblings: 3,19
Significado: CPU0 comparte un core físico con CPU16, etc. Si fijas dos cargas ruidosas en hilos hermanos, espera sorpresas en el rendimiento.
Decisión: Para trabajo sensible a latencia, prefiere un hilo por core (evita compartir siblings) o desactiva SMT en nodos dedicados si puedes permitir la pérdida de capacidad.
Broma #1: Los límites de CPU son como presupuestos de oficina—todo el mundo se siente “disciplinado” hasta el primer incidente, luego de repente los límites se convierten en “solo una sugerencia”.
Tres micro-historias corporativas desde las trincheras de la CPU
Micro-historia 1: El incidente causado por una suposición incorrecta (contenedores “no tienen sobrecarga”)
Una empresa SaaS mediana movió una API sensible a latencia de VMs a Kubernetes. El argumento fue limpio: “los contenedores son más ligeros, así que tendremos mejor rendimiento y mayor densidad.” La migración fue rápida, lo que debió haber sido la primera pista.
La API se desplegó con límites de CPU para “prevenir vecinos ruidosos.” Las requests eran pequeñas—100m—porque el servicio no usaba mucha CPU en promedio. Los límites eran 2 cores, lo que sonaba generoso. Bajo tráfico normal, todo parecía bien. Entonces hicieron una campaña de marketing y el tráfico se disparó. El servicio escaló, pero cada pod alcanzó su cuota de CPU repetidamente durante las ráfagas de peticiones.
Los gráficos mostraban uso de CPU por debajo de la capacidad del nodo, así que la respuesta inicial fue afinar el código de la aplicación y culpar a la base de datos. Mientras tanto, el kernel silenciosamente aplicó throttling a los pods más calientes. La p99 se duplicó, luego se triplicó. Llegaron los timeouts. El autoscaler entró en pánico y añadió más pods, lo que aumentó la contención y creó un bucle de retroalimentación de miseria.
La solución real fue aburrida: quitar límites de CPU para este servicio en nodos dedicados, establecer requests realistas de CPU y usar el static CPU manager para cores pinneados. También pusieron guardrails en el control de admisión para que “100m requests” no se colaran en producción para servicios críticos.
La suposición equivocada no fue “los contenedores son más rápidos.” Los contenedores pueden ser rápidos. La suposición equivocada fue que el CPU promedio es la métrica relevante para un servicio con ráfagas y SLOs estrictos en la cola.
Micro-historia 2: La optimización que salió mal (sobreprovisión de vCPU)
Un equipo empresarial corría una plataforma basada en VMs para servicios internos. Tenían presión para reducir el conteo de VMs. Alguien propuso “redimensionar” aumentando vCPUs por VM para que cada VM pudiera alojar más hilos de trabajo, reduciendo el número de instancias.
En papel, parecía eficiente: menos VMs, menos sobrecarga de gestión, mejor utilización. En la práctica, el clúster de hipervisores ya estaba moderadamente overcommitado. Aumentar vCPUs hizo que cada VM fuera más difícil de programar en hosts ocupados. El steal time subió, pero solo durante las horas pico.
Lo peor: el equipo añadió vCPUs otra vez, pensando que la aplicación estaba hambrienta. Eso aumentó el conjunto ejecutable e hizo la planificación aún más caótica. La latencia en cola degradó múltiples servicios, no solo el “optimizado”.
Se recuperaron revirtiendo los cambios de vCPU, repartiendo la carga en más VMs y aplicando una política de overcommit más estricta para ese clúster. La lección fue poco glamorosa: VMs más grandes no son siempre VMs más rápidas. La planificación es una restricción real, no un detalle de implementación.
Micro-historia 3: La práctica aburrida pero correcta que salvó el día (aislamiento de CPU + disciplina de cambios)
Un sistema adyacente a pagos tenía un pequeño conjunto de servicios con SLOs de latencia estrictos y criptografía intensiva. El equipo de plataforma los mantuvo en un pool dedicado de nodos, con static CPU manager, cores pinneados y ajuste explícito de IRQ. No fue popular. Los pools dedicados parecen “capacidad desperdiciada” para finanzas y “diversión desperdiciada” para ingenieros que quieren una plataforma única para todo.
También tenían una política de cambios: actualizaciones de kernel y BIOS se hacían por etapas, con un nodo canario ejecutando pruebas de carga sintéticas que medían p99 y jitter, no solo throughput. Cada vez que alguien pedía saltarse el paso del canario, la respuesta fue consistentemente: no.
Un día, una actualización rutinaria de firmware de flota llegó al pool general. Cambió el comportamiento de potencia de la CPU. Varios servicios vieron cambios de latencia, y algunos equipos entraron en modo incidente. El pool de pagos no se inmutó. Sus nodos estaban fijados a un perfil de governor validado y solo se actualizaron después de que los resultados del canario fueran estables.
La práctica no fue ingeniosa. Fue simplemente consistente. En producción, “aburrido” es una característica.
Broma #2: Lo único más virtual que una VM es la certeza en un postmortem escrito antes de que lleguen las métricas.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: p99 sube cada pocos minutos; la CPU media se ve bien
Causa raíz: Throttling de cuota de CPU en contenedores (cgroup CPU.max / CFS quota) durante ráfagas.
Solución: Quita o sube límites de CPU para el servicio; establece requests realistas; usa QoS Guaranteed y considera pinning de CPU para SLOs estrictos.
2) Síntoma: uso de CPU en VM alto pero throughput bajo; los gráficos muestran “idle” también
Causa raíz: Steal time alto por overcommit del host o contención de CPU.
Solución: Reduce el overcommit; mueve la VM a un host menos cargado; usa reservas o políticas de host dedicadas; no añadas vCPUs como primera respuesta.
3) Síntoma: el rendimiento empeoró al escalar a un tipo de instancia más grande
Causa raíz: Efectos NUMA o planificación cross-socket; la memoria es remota para parte de la carga.
Solución: Asegura colocación consciente de NUMA; pinnea CPU y memoria; prefiere instancias que quepan en un nodo NUMA para servicios sensibles a latencia.
4) Síntoma: picos de latencia “aleatorios” durante tráfico intenso de red
Causa raíz: Interrupciones (IRQs) que caen en cores de la aplicación; sobrecarga de softirq.
Solución: Ajusta afinidad de IRQ; separa interrupciones de red de las CPUs de la aplicación; valida con /proc/interrupts y estadísticas de softirq.
5) Síntoma: servicio Java/Go en contenedor se vuelve inestable bajo carga, muchos cambios de contexto
Causa raíz: Sobre- suscripción de hilos en relación con la asignación de CPU; thrash del scheduler amplificado por límites de CPU estrictos.
Solución: Reduce conteos de hilos; aumenta requests de CPU; evita requests bajos con limits altos; considera pinning de CPU para comportamiento estable.
6) Síntoma: todo se vuelve más lento después de desplegar “ahorro de energía”
Causa raíz: Governor de frecuencia de CPU en powersave; limitación térmica/energética.
Solución: Usa performance governor para nodos críticos; valida ajustes de BIOS; monitorea frecuencia y throttling bajo carga sostenida.
7) Síntoma: el nodo Kubernetes parece infrautilizado pero los pods son lentos
Causa raíz: Límites de CPU que throttlean por pod; el nodo puede estar idle mientras pods individuales están capados.
Solución: Revisa límites; usa requests para colocación, no limits como herramienta contundente; separa cargas ruidosas en nodos distintos.
8) Síntoma: servicio basado en VM empeora tras activar “más seguridad”
Causa raíz: Cambios en microcode/mitigaciones de kernel que afectan rutas intensivas en syscall/virtualización (dependiente de la carga).
Solución: Haz benchmarks antes/después; aísla el cambio; si es necesario, ajusta el dimensionamiento de la instancia o mueve la carga a un perfil que tolere la sobrecarga.
Listas de verificación / plan paso a paso
Paso a paso: elegir contenedores vs VMs para un perfil de CPU
- Escribe la métrica de éxito de CPU. ¿p99 de latencia? ¿requests/segundo? ¿tiempo de finalización de trabajos? Si no puedes nombrarla, vas a optimizar sensaciones.
- Clasifica la carga. Sensible a latencia, throughput batch, interactivo con ráfagas, modo mixto, comportamiento CPU especial.
- Decide las necesidades de aislamiento. ¿Multi-inquilino con gobernanza débil? Prefiere VMs o nodos dedicados. ¿Plataforma disciplinada? Los contenedores pueden funcionar bien.
-
Decide cómo evitar vecinos ruidosos.
- Contenedores: requests, QoS, pinning de CPU para pods críticos, pools de nodos, control de admisión.
- VMs: limitar overcommit, reservas, pinning de CPU donde haga falta, monitoreo del host para contención.
- Establece predeterminados que reflejen la realidad. Nada de “100m request” para servicios que hacen picos. Nada de “8 vCPUs” para un servicio que pasa la mitad del tiempo esperando.
- Valida en hardware representativo. Topología NUMA, SMT, comportamiento de frecuencia. “Mismo conteo de vCPU” no significa “mismo rendimiento.”
- Prueba carga por jitter, no solo promedios. Mide p95/p99/p999 y la varianza bajo contención y durante colocalización.
- Operacionaliza el diagnóstico. Mete métricas de throttling/steal/run-queue en alertas. Si no se mide, se convertirá en debate.
Checklist: gobernanza de CPU para plataformas de contenedores
- Define rangos permitidos para requests/limits de CPU por nivel de servicio.
- Aplica mediante políticas de admisión: no requests mínimos para cargas críticas.
- Usa QoS Guaranteed para servicios con SLO estrictos; considera static CPU manager.
- Separa pools de nodos: “throughput pack” vs “latency clean room”.
- Monitorea contadores de throttling de cgroup y alerta sobre throttling sostenido.
- Documenta cuándo se requieren límites de CPU (usualmente para equidad en pools batch compartidos).
Checklist: gobernanza de CPU para plataformas de VMs
- Establece y publica una política de overcommit (y cúmplela).
- Monitorea steal time y run queue del host; alerta cuando la contención persista.
- Evita escalar vCPU por reflejo; valida el impacto en la planificación.
- Para cargas críticas: considera pinning de CPU y capacidad reservada.
- Rastrea colocación NUMA y alineación de topología del host para VMs grandes.
Preguntas frecuentes
1) ¿Son los contenedores siempre más rápidos que las VMs para CPU?
A menudo, los contenedores tienen menor sobrecarga porque no hay kernel invitado y menos salidas de virtualización. Pero “más rápido” se derrumba si
throtleas contenedores con límites de CPU o los empaquetas en contención. Las VMs pueden acercarse mucho al nativo cuando están afinadas y no están overcommitadas.
2) ¿Cuál es el equivalente de “vecino ruidoso” para CPU en Kubernetes?
Un pod con alta demanda de CPU más bien sin límites (acaparando bajo contención) o con límites mal configurados (provocando cascadas de throttling en otros
por presión de planificación). La solución es pools tierados de nodos, requests realistas y aplicación.
3) ¿Por qué los límites de CPU dañan la latencia aunque los nodos estén idle?
Porque los límites pueden actuar como topes duros por cgroup. Tu pod puede ser throttled aunque haya CPU disponible en otro lugar del nodo, dependiendo
de cómo se alinee la demanda con el periodo de cuota. El síntoma es contadores de throttling subiendo mientras la CPU del host no está al 100%.
4) ¿Debo poner límites de CPU en cada contenedor?
No. Pon límites cuando intentes restringir ráfagas por equidad (pools batch) o prevenir procesos fuera de control.
Para servicios sensibles a latencia, los límites frecuentemente son una herida auto infligida a menos que hayas validado que no inducen throttling.
5) En VMs, ¿más vCPU siempre es mejor?
No bajo contención. Más vCPUs pueden hacer que la VM sea más difícil de programar y aumentar steal time. Alinea vCPUs con el paralelismo que realmente puedes usar y valida bajo condiciones pico.
6) ¿Cómo sé si NUMA me está perjudicando?
Verás rendimiento inconsistente, peor latencia en cola en instancias grandes y a veces más tráfico cross-socket. Confírmalo con topología NUMA, afinidad de proceso y, cuando esté disponible, estadísticas de localidad de memoria NUMA. La solución es colocación consciente de NUMA y pinning.
7) ¿SMT/Hyper-Threading ayuda o perjudica?
Ayuda al throughput para muchas cargas, especialmente las que tienen stalls. Puede perjudicar la predictibilidad para trabajo sensible a latencia cuando los hilos hermanos compiten por recursos compartidos de ejecución. Para SLOs estrictos, evita compartir siblings o desactiva SMT en nodos dedicados si la capacidad lo permite.
8) Si estoy en Kubernetes, ¿debería usar nodos dedicados para servicios críticos?
Sí, cuando la p99 del servicio importa y el resto del clúster es un batiburrillo de cargas y equipos. Los nodos dedicados simplifican la historia de rendimiento y reducen la entropía de incidentes. No es “desperdicio”, es pagar para evitar sorpresas a las 3 AM.
9) ¿Qué métricas deberían hacer sonar el pager por problemas de CPU?
Contenedores: tiempo throttled (cpu.stat), run queue, cambios de contexto y saturación de CPU del nodo. VMs: steal time más run queue del invitado y contención del host. Para ambos: p99 correlacionado con indicadores de scheduling.
10) ¿Puedo obtener aislamiento tipo VM con contenedores?
Puedes acercarte para CPU usando pools de nodos dedicados, QoS Guaranteed, static CPU manager pinning y aplicación estricta de políticas.
Aún compartes un kernel, así que el aislamiento no es idéntico. Si importa depende de tu modelo de amenazas y madurez operativa.
Conclusión: próximos pasos que puedes tomar
Elige según el perfil de CPU, no por moda. Si necesitas latencia en cola determinista y no tienes gobernanza de plataforma fuerte, empieza
con VMs o nodos de contenedores dedicados. Si quieres throughput y densidad, los contenedores suelen ser la apuesta correcta—solo no los sabotees
con límites de CPU ingenuos.
Próximos pasos:
- Elige un servicio crítico y ejecuta las comprobaciones de diagnóstico rápido durante la carga pico: throttling (contenedores) o steal (VMs), run queue, hotspots de IRQ, frecuencia.
- Arregla un problema de política, no diez: o bien quita límites dañinos de CPU para servicios de latencia o reduce el overcommit de VMs donde el steal es alto.
- Crea dos clases de nodos/hosts: “latency clean room” (pinneado/aislado) y “throughput pack” (alta densidad, reparto justo).
- Haz que requests/limits (o dimensionamiento de vCPU) formen parte de la revisión de código con un breve rubro. El pager te lo agradecerá luego.