Go, Contêineres e o Agendador do Linux

Serviços Go em contêineres muitas vezes julgam mal quanta CPU realmente têm, levando o runtime do Go a criar threads demais e sofrer forte estrangulamento sob o agendador CFS do Linux. Os comentaristas comparam quotas de CPU e shares, discutem se vale a pena depender de limites do Kubernetes/Docker ou de ajuste no aplicativo, e apontam ferramentas como `automaxprocs` e runtimes conscientes de cgroup (como em Java e .NET) como correções práticas. De forma mais ampla, a discussão destaca as trocas entre limites rígidos de CPU, overcommit para melhor utilização e latência previsível em grandes clusters multi-tenant.

Go, CFS e limites de CPU em contêineres

  • Problema principal: o runtime do Go dimensiona o GOMAXPROCS a partir dos núcleos visíveis, ignorando as cotas de CPU do cgroup, então em contêineres ele frequentemente cria muito mais threads do sistema operacional do que o tempo de CPU realmente disponível.
  • Isso leva a forte estrangulamento pelo CFS, piora de throughput e latência, e comportamento de “núcleos desperdiçados” observado לאורך de muitos anos.
  • Alguns argumentam que o bug central está no runtime do Go (usando a métrica errada), e não no agendador do Linux.

Docker/Kubernetes: cotas vs. shares e flags

  • --cpus no Docker se mapeia para quota/período do CFS, não para cpuset, e pode causar estrangulamento rígido mesmo em hosts majoritariamente ociosos.
  • cpu.shares é um agendamento proporcional/relativo usado apenas sob contenção; quotas (cpu.cfs_quota_us) são limites rígidos de tempo.
  • No Kubernetes: pedido de CPU (request) → shares; limite de CPU (limit) → quota. Alguns dizem “não defina limits, apenas requests”; outros veem a quota como essencial para isolamento forte.
  • Há desacordo sobre quando a quota do CFS realmente estrangula: alguns afirmam “apenas sob contenção”; outros citam documentação e experimentos mostrando que ela é um teto rígido independentemente disso.

Contornos: GOMAXPROCS e sondagem de cgroup

  • Padrão popular: definir GOMAXPROCS a partir de dados do cgroup (por exemplo, cpu.cfs_quota_us / cpu.cfs_period_us) ou usar bibliotecas como automaxprocs.
  • Há vários relatos de ganhos substanciais de latência e throughput depois disso, em comparação tanto com “sem limite” quanto com “com quota, mas sem ajuste”.
  • Outros alertam que limitar GOMAXPROCS à quota pode prejudicar o tratamento de picos e aumentar a latência de cauda; “o paralelismo máximo não deve ser igual ao orçamento de CPU de longo prazo.”
  • As sugestões incluem: webhooks mutantes para injetar GOMAXPROCS, usar nproc/sched_getaffinity, ou lxcfs para falsificar /proc e oferecer visões conscientes de contêiner.

Debate: deve-se usar limites de CPU?

  • Um lado: “Pare de usar limites de CPU; use apenas reservas/requests.” Argumento: limites desperdiçam CPU, causam estrangulamento enquanto o host está ocioso e tornam o comportamento de capacidade imprevisível.
  • Lado contrário: limites são necessários para:
    • Impedir que vizinhos barulhentos afetem serviços críticos.
    • Evitar depender de “CPU de burst grátis” que pode desaparecer conforme os nós enchem.
    • Simular condições de pior caso para um planejamento de capacidade realista.
  • Vários observam que overcommit e a mistura de workloads sensíveis à latência com workloads em lote são inerentemente complexos; abstrações do K8s podem enganar equipes de operações menos experientes.

Outros runtimes e consciência de contêiner

  • Java, .NET e Rust adicionaram lógica consciente de contêiner, usando limites de cgroup para dimensionar pools de threads, threads de GC etc.
  • O comportamento do Java evoluiu de usar shares para quotas; há flags para controlar se a quota do contêiner influencia a contagem de CPUs.
  • Alguns sugerem que mecanismos semelhantes ou shims carregáveis antecipadamente poderiam ser aplicados ao Go.

Contêineres vs VMs/unikernels para Go

  • Pergunta recorrente: como o Go produz binários estáticos, que valor os contêineres adicionam?
  • Pontos pró-contêiner: empacotamento padronizado, isolamento de rede/sistema de arquivos/processos, controles de recursos, compatibilidade com orquestração (Kubernetes) e setups mais fáceis de CI/CD e multi-serviços.
  • Visão cética: para serviços Go isolados, contêineres podem ser “bloat” redundante; unikernels junto com binários Go podem ser um encaixe melhor, embora as ferramentas e o deployment ainda sejam fracos.
  • Consenso: ecossistema e padronização de fluxo de trabalho são razões importantes pelas quais apps Go ainda acabam em contêineres.

Diversos / pontos pouco claros

  • Há alguma discussão sobre políticas do gerenciador de CPU do Kubernetes (static vs none) e como elas alteram as máscaras de CPU visíveis; o comportamento difere entre configurações.
  • Menção a novos agendadores do Linux (EEVDF), mas sem experiência concreta em produção relatada no tópico.
  • No geral, os participantes concordam que a interação entre runtimes de linguagem, CFS, cgroups e orquestradores continua sutil e fácil de configurar incorretamente.