Go, contenedores y el planificador de Linux
Los servicios Go en contenedores a menudo calculan mal cuánta CPU tienen realmente, lo que lleva al runtime de Go a crear demasiados hilos y a sufrir un throttling severo bajo el planificador CFS de Linux. Los comentaristas comparan quotas vs. shares de CPU, debaten si conviene confiar en límites de Kubernetes/Docker o en ajustes a nivel de aplicación, y señalan herramientas como `automaxprocs` y runtimes conscientes de cgroups (como en Java y .NET) como soluciones prácticas. En términos más amplios, el intercambio destaca las ventajas y desventajas entre límites estrictos de CPU, sobrecompromiso para una mejor utilización y latencia predecible en clústeres grandes multiusuario.
Go, CFS y los límites de CPU de los contenedores
- Problema principal: el runtime de Go dimensiona
GOMAXPROCSa partir de los núcleos visibles, ignorando las cuotas de CPU de cgroup, por lo que en contenedores a menudo crea muchos más hilos del sistema operativo de los que el tiempo de CPU realmente disponible permite. - Esto lleva a un fuerte throttling de CFS, degradación del rendimiento y la latencia, y un comportamiento de “núcleos desperdiciados” observado durante muchos años.
- Algunos sostienen que el fallo central está en el runtime de Go (usar la métrica incorrecta), no en el planificador de Linux.
Docker/Kubernetes: cuotas vs shares y banderas
--cpusen Docker se asigna a quota/period de CFS, no a cpuset, y puede provocar throttling duro incluso en hosts mayormente inactivos.cpu.shareses una planificación proporcional/relativa que solo se usa bajo contención; las cuotas (cpu.cfs_quota_us) son límites duros de tiempo.- En Kubernetes: request de CPU → shares; limit de CPU → quota. Algunos dicen “no pongas limits, solo requests”; otros ven la quota como esencial para un aislamiento fuerte.
- Hay desacuerdo sobre cuándo realmente throttling de CFS: algunos afirman “solo bajo contención”; otros señalan documentación y experimentos que muestran que es un límite duro sin importar eso.
Soluciones: GOMAXPROCS y sondeo de cgroup
- Patrón popular: configurar
GOMAXPROCSa partir de datos de cgroup (por ejemplo,cpu.cfs_quota_us / cpu.cfs_period_us) o usar bibliotecas comoautomaxprocs. - Hay varios informes de mejoras sustanciales de latencia y rendimiento tras hacer esto, frente tanto a “sin límite” como a “con quota pero sin ajuste”.
- Otros advierten que limitar
GOMAXPROCSa la quota puede perjudicar el manejo de picos y aumentar la latencia de cola; “el paralelismo máximo no debería equivaler al presupuesto de CPU a largo plazo”. - Las sugerencias incluyen: webhooks mutadores para inyectar
GOMAXPROCS, usarnproc/sched_getaffinity, olxcfspara falsificar/procy ofrecer vistas conscientes del contenedor.
Debate: ¿deberías usar límites de CPU en absoluto?
- Un bando: “deja de usar límites de CPU; usa solo reservas/requests”. Argumento: los límites desperdician CPU, causan throttling mientras el host está ocioso y hacen impredecible el comportamiento de capacidad.
- Bando contrario: los límites son necesarios para:
- Evitar que vecinos ruidosos afecten a servicios críticos.
- No depender de “CPU libre en ráfaga” que puede desaparecer a medida que se llenan los nodos.
- Simular condiciones de peor caso para una planificación de capacidad realista.
- Varios señalan que el sobrecompromiso y la mezcla de cargas sensibles a la latencia con cargas batch son intrínsecamente complejos; las abstracciones de K8s pueden inducir a error a equipos de operaciones con menos experiencia.
Otros runtimes y conciencia de contenedores
- Java, .NET y Rust han añadido lógica consciente de contenedores, usando límites de cgroup para dimensionar pools de hilos, hilos de GC, etc.
- El comportamiento de Java ha evolucionado desde usar shares hasta usar quotas; hay flags para controlar si la quota del contenedor influye en el recuento de CPU.
- Algunos sugieren que mecanismos similares o shims cargables podrían aplicarse a Go.
Contenedores vs VMs/unikernels para Go
- Pregunta recurrente: dado que Go produce binarios estáticos, ¿qué valor aportan los contenedores?
- Puntos a favor de los contenedores: empaquetado estandarizado, aislamiento de red/sistema de archivos/procesos, controles de recursos, compatibilidad con orquestación (Kubernetes) y entornos CI/CD y configuraciones multi-servicio más sencillos.
- Visión escéptica: para servicios Go individuales, los contenedores pueden ser “bloat” redundante; los unikernels junto con binarios Go podrían encajar mejor, aunque las herramientas y el despliegue siguen siendo ásperos.
- Consenso: el ecosistema y la estandarización del flujo de trabajo son razones importantes por las que las aplicaciones Go siguen acabando en contenedores.
Varios / puntos poco claros
- Algo de discusión sobre las políticas del CPU manager de Kubernetes (
staticvsnone) y cómo cambian las máscaras de CPU visibles; el comportamiento difiere según la configuración. - Mención de planificadores de Linux emergentes (EEVDF), pero no se informó de experiencia concreta en producción en el hilo.
- En general, los participantes coinciden en que la interacción entre runtimes de lenguaje, CFS, cgroups y orquestadores sigue siendo sutil y fácil de configurar mal.