← Voltar para o blog

GOMAXPROCS Inteligente para Containers no Go 1.25

Go 1.25 introduz configuração automática de GOMAXPROCS baseada em limites de CPU de containers, evitando throttling e melhorando a performance em produção.

Go 1.25 inclui novos padrões de GOMAXPROCS que reconhecem limites de containers, proporcionando comportamento mais sensato para muitas cargas de trabalho em containers, evitando throttling que pode impactar a latência de cauda, e melhorando a prontidão do Go para produção.

Neste post, vamos explorar como Go agenda goroutines, como esse agendamento interage com controles de CPU de containers, e como Go pode performar melhor com consciência dos controles de CPU.

GOMAXPROCS

Uma das forças do Go é sua concorrência built-in e fácil de usar via goroutines. Do ponto de vista semântico, goroutines parecem muito similares a threads do sistema operacional, permitindo escrever código simples e bloqueante. Por outro lado, goroutines são mais leves que threads do SO, tornando muito mais barato criá-las e destruí-las dinamicamente.

Enquanto uma implementação de Go poderia mapear cada goroutine para uma thread dedicada, Go mantém goroutines leves com um scheduler runtime que torna threads fungíveis. Qualquer thread gerenciada por Go pode executar qualquer goroutine, então criar uma nova goroutine não requer criar uma nova thread.

Dito isso, junto com um scheduler vêm questões de agendamento. Por exemplo, exatamente quantas threads devemos usar para executar goroutines? Se 1.000 goroutines estão prontas para executar, devemos agendá-las em 1.000 threads diferentes?

É aqui que GOMAXPROCS entra. Semanticamente, GOMAXPROCS diz ao runtime Go o “paralelismo disponível” que Go deve usar. Em termos mais concretos, GOMAXPROCS é o número máximo de threads a usar para executar goroutines simultaneamente.

Então, se GOMAXPROCS=8 e existem 1.000 goroutines prontas, Go usará 8 threads para executar 8 goroutines por vez. Frequentemente, goroutines executam por um tempo muito curto e depois bloqueiam, momento em que Go trocará para executar outra goroutine na mesma thread. Go também preempta goroutines que não bloqueiam por conta própria, garantindo que todas tenham chance de executar.

O Padrão Antigo

Do Go 1.5 até Go 1.24, GOMAXPROCS tinha como padrão o número total de cores de CPU na máquina. (Neste post, “core” significa mais precisamente “CPU lógica” — uma máquina com 4 CPUs físicas com hyperthreading tem 8 CPUs lógicas.)

Isso tipicamente era um bom padrão para “paralelismo disponível” porque naturalmente corresponde ao paralelismo disponível do hardware. Se há 8 cores e Go executa mais de 8 threads por vez, o sistema operacional terá que multiplexar essas threads nos 8 cores, assim como Go multiplexa goroutines em threads. Essa camada extra de agendamento é overhead desnecessário.

Orquestração de Containers

Outra força central do Go é a conveniência de fazer deploy de aplicações via container, e gerenciar o número de cores que Go usa é especialmente importante ao fazer deploy dentro de uma plataforma de orquestração de containers.

Plataformas de orquestração como Kubernetes pegam um conjunto de recursos de máquina e agendam containers dentro dos recursos disponíveis baseado nos recursos requisitados. Empacotar o máximo de containers possível dentro dos recursos de um cluster requer que a plataforma seja capaz de prever o uso de recursos de cada container agendado.

O Problema com Limites de CPU

Kubernetes tem o conceito de limites de recursos de CPU, que sinalizam ao SO quantos recursos de core um container específico receberá. Configurar um limite de CPU se traduz na criação de um limite de bandwidth de CPU de control group no Linux.

Antes do Go 1.25, Go não tinha conhecimento dos limites de CPU configurados por plataformas de orquestração. Ao invés disso, configurava GOMAXPROCS para o número de cores na máquina onde foi deployed. Se havia um limite de CPU em vigor, a aplicação poderia tentar usar muito mais CPU do que permitido pelo limite.

Para prevenir que uma aplicação exceda seu limite, o kernel Linux faz throttling (estrangulamento) da aplicação.

O Problema do Throttling

Throttling é um mecanismo bruto para restringir containers que de outra forma excederiam seu limite de CPU: ele pausa completamente a execução da aplicação pelo restante do período de throttling.

O período de throttling é tipicamente 100ms, então throttling pode causar impacto substancial na latência de cauda comparado aos efeitos mais suaves de multiplexação de agendamento com um GOMAXPROCS mais baixo.

Mesmo se a aplicação nunca tem muito paralelismo, tarefas executadas pelo runtime Go — como garbage collection — ainda podem causar picos de CPU que disparam throttling.

O Novo Padrão no Go 1.25

Queremos que Go forneça padrões eficientes e confiáveis quando possível, então no Go 1.25, fizemos GOMAXPROCS levar em conta seu ambiente de container por padrão.

Se um processo Go está executando dentro de um container com limite de CPU, GOMAXPROCS terá como padrão o limite de CPU se for menor que a contagem de cores.

Sistemas de orquestração de containers podem ajustar limites de CPU dinamicamente, então Go 1.25 também verificará periodicamente o limite de CPU e ajustará GOMAXPROCS automaticamente se ele mudar.

Ambos esses padrões só se aplicam se GOMAXPROCS não foi especificado de outra forma. Configurar a variável de ambiente GOMAXPROCS ou chamar runtime.GOMAXPROCS continua funcionando como antes.

Modelos Ligeiramente Diferentes

Tanto GOMAXPROCS quanto um limite de CPU de container colocam um limite na quantidade máxima de CPU que o processo pode usar, mas seus modelos são sutilmente diferentes.

GOMAXPROCS = Limite de Paralelismo

Se GOMAXPROCS=8, Go nunca executará mais de 8 goroutines por vez.

Limite de CPU = Limite de Throughput

Limites de CPU limitam o tempo total de CPU usado em algum período de tempo real. O período padrão é 100ms. Então um “limite de 8 CPUs” é na verdade um limite de 800ms de tempo de CPU a cada 100ms de tempo real.

Este limite poderia ser preenchido executando 8 threads continuamente por 100ms inteiros, o que é equivalente a GOMAXPROCS=8. Por outro lado, o limite também poderia ser preenchido executando 16 threads por 50ms cada, com cada thread ociosa ou bloqueada pelos outros 50ms.

Em outras palavras, um limite de CPU não limita o número total de CPUs em que o container pode executar. Ele só limita o tempo total de CPU.

Na Prática

A maioria das aplicações tem uso de CPU razoavelmente consistente ao longo de períodos de 100ms, então o novo padrão de GOMAXPROCS é uma boa correspondência ao limite de CPU, e certamente melhor que a contagem total de cores!

No entanto, vale notar que cargas de trabalho particularmente “espinhosas” podem ver um aumento de latência com essa mudança devido a GOMAXPROCS prevenir picos de threads adicionais de curta duração além da média do limite de CPU.

Além disso, como limites de CPU são um limite de throughput, eles podem ter um componente fracionário (ex: 2.5 CPU). Por outro lado, GOMAXPROCS deve ser um inteiro positivo. Assim, Go deve arredondar o limite para um valor válido de GOMAXPROCS. Go sempre arredonda para cima para permitir uso do limite de CPU completo.

CPU Requests

O novo padrão de GOMAXPROCS do Go é baseado no limite de CPU do container, mas sistemas de orquestração também fornecem um controle de request de CPU.

  • Limite de CPU: máximo de CPU que um container pode usar
  • Request de CPU: mínimo de CPU garantido para estar disponível ao container o tempo todo

É comum criar containers com um request de CPU mas sem limite de CPU, pois isso permite que containers utilizem recursos de CPU da máquina além do request que de outra forma estariam ociosos. Infelizmente, isso significa que Go não pode configurar GOMAXPROCS baseado no request de CPU, o que preveniria utilização de recursos ociosos adicionais.

Containers com um request de CPU ainda são restringidos quando excedem seu request se a máquina está ocupada. A restrição baseada em peso de exceder requests é “mais suave” que o throttling baseado em período rígido de limites de CPU, mas picos de CPU de GOMAXPROCS alto ainda podem ter impacto adverso no comportamento da aplicação.

Devo Configurar um Limite de CPU?

Configurar um limite de container de CPU permite que Go automaticamente configure um GOMAXPROCS apropriado, então uma pergunta óbvia é se todos os containers devem configurar um limite de CPU.

Enquanto isso pode ser um bom conselho para obter padrões razoáveis de GOMAXPROCS automaticamente, há muitos outros fatores a considerar ao decidir configurar um limite de CPU, como priorizar utilização de recursos ociosos evitando limites vs priorizar latência previsível configurando limites.

Os piores comportamentos de um descasamento entre GOMAXPROCS e limites de CPU efetivos ocorrem quando GOMAXPROCS é significativamente maior que o limite de CPU efetivo. Por exemplo, um container pequeno recebendo 2 CPUs executando em uma máquina de 128 cores.

Esses são os casos onde é mais valioso considerar configurar um limite de CPU explícito, ou, alternativamente, configurar GOMAXPROCS explicitamente.

Conclusão

Go 1.25 fornece comportamento padrão mais sensato para muitas cargas de trabalho em containers configurando GOMAXPROCS baseado em limites de CPU de containers.

Fazer isso evita throttling que pode impactar latência de cauda, melhora eficiência, e geralmente tenta garantir que Go esteja pronto para produção out-of-the-box.

Você pode obter os novos padrões simplesmente configurando a versão do Go para 1.25.0 ou maior no seu go.mod.

Obrigado a todos na comunidade que contribuíram para as longas discussões que tornaram isso realidade, e em particular ao feedback dos mantenedores do go.uber.org/automaxprocs da Uber, que há muito tempo fornece comportamento similar aos seus usuários.


Traduzido de Container-aware GOMAXPROCS — Go Blog, 20 de agosto de 2025