Solucionar throttling de CPU en Kubernetes: CFS, JVM, Go y pinning de núcleos
El consumo medio del 20% en los dashboards oculta bloqueos de milisegundos. Un manual de SRE para detectar el throttling de CFS, ajustar runtimes Java y Go y usar el cpuManager en bare metal.

Es el clásico infarto del SRE: la alerta de latencia P99 suena, pero Grafana muestra un consumo de CPU del 20%. No es que la métrica mienta, es que promedia datos a lo largo de uno o cinco minutos, ocultando lo que ocurre a nivel de kernel. El culpable es el Completely Fair Scheduler (CFS) de Linux. Cuando Kubernetes aplica límites de CPU, CFS gestiona el acceso en ventanas de 100 milisegundos. Si tienes un límite de 100m, tu contenedor recibe 10ms de ejecución por cada ventana de 100ms. Si una aplicación con picos de carga gasta esa cuota en los primeros 10ms, el kernel congela el proceso los 90 restantes. El dashboard sigue bajo, pero la aplicación ha estado parada el 90% del segundo. El resultado son picos de latencia artificiales que arruinan el rendimiento percibido.
Detectar el ruido real
Para ver lo que pasa, hay que dejar de mirar container_cpu_usage_seconds_total y empezar a rastrear cuántas de esas ventanas de 100ms el kernel ha detenido tu contenedor. La consulta en PromQL divide las periodos de throttling entre el total de periodos CFS. Si la比率 supera el 15-25%, la aplicación está sufriendo latencia por culpa de los cgroups, no por código ineficiente. Cualquier valor por encima de ese umbral indica que el límite de CPU es demasiado ajustado para el patrón de ejecución de la carga de trabajo.
Runtimes y la ampliación de hilos
El problema se agrava con runtimes que inspeccionan la máquina host en lugar de leer los límites del contenedor. En un nodo bare metal con 64 núcleos físicos, una aplicación Java o Go puede decidir crear 64 hilos de recolección de basura o workers. Si el contenedor tiene un límite de 2 vCPUs, esos 64 hilos se despiertan simultáneamente y agotan la cuota de 100ms en milisegundos, provocando el bloqueo inmediato.
La solución pasa por_FORzar al runtime a respetar los límites de cgroup. En Java (JDK 11+), se activa con JAVA_OPTS="-XX:+UseContainerSupport". En Go, se usa la librería automaxprocsde Uber para ajustar automáticamenteGOMAXPROCS` a los límites reales del contenedor.
Ajuste y evasión en bare metal
Eliminar los límites de CPU no es una opción viable por el riesgo de agotamiento de recursos y ataques de denegación de servicio. La guía propone un método de ajuste basado en telemetría: usar el percentil P50 (mediana) de los últimos siete días para el parámetro requests.cpu, y el percentil P99 multiplicado por dos para limits.cpu.
Para cargas críticas en infraestructura bare metal, donde el problema se complica con el "tiempo robado" del hipervisor en la nube pública, cabe usar la política cpuManagerPolicy: static en el Kubelet. Esto permite pinning de nucleos físicos a los contenedores si se usa la clase de calidad de servicio Guaranteed (donde requests es igual a limits y los valores de CPU son enteros). Al hacerlo, Kubernetes evade el sistema de cuotas de 100ms de CFS y asigna núcleos exclusivos al proceso, eliminando el throttling por completo.
Este enfoque distingue claramente entre un problema de configuración de recursos y uno de arquitectura de software. Ajustar los runtimes suele ser suficiente para la gran mayoría de los casos, mientras que el pinning de núcleos requiere infraestructura dedicada y una gestión de nodos más rígida. Vale la pena revisar estas métricas antes de asumir que el código es lento.