O Go 1.25 traz uma mudança importante na forma como a linguagem lida com ambientes de containers, com um novo comportamento padrão para a variável GOMAXPROCS. Essa atualização visa otimizar o desempenho de aplicações Go em containers, evitando o “throttling” (estrangulamento) que pode impactar a latência e melhorando a “production-readiness” da linguagem.
Entendendo o GOMAXPROCS
O Go oferece um modelo de concorrência poderoso e fácil de usar através das goroutines. Goroutines são semelhantes a threads do sistema operacional, mas muito mais leves, permitindo a criação e destruição sob demanda sem grande impacto no desempenho.
Para gerenciar a execução dessas goroutines, o Go utiliza um scheduler (agendador) que distribui as goroutines entre as threads do sistema operacional. A variável GOMAXPROCS define o número máximo de threads que o Go pode usar simultaneamente para executar goroutines. Em termos práticos, ela controla o nível de paralelismo disponível para a aplicação Go.
Por exemplo, se GOMAXPROCS=8 e existem 1000 goroutines prontas para execução, o Go utilizará 8 threads para executar 8 goroutines ao mesmo tempo. O scheduler se encarrega de alternar entre as goroutines, garantindo que todas tenham a chance de rodar, mesmo que não bloqueiem por conta própria.
Nas versões do Go 1.5 até o Go 1.24, o valor padrão de GOMAXPROCS era o número total de núcleos (CPUs lógicas) da máquina. Isso geralmente funcionava bem porque correspondia à capacidade de paralelismo do hardware subjacente. Executar mais threads do que o número de núcleos disponíveis resultaria em multiplexing no nível do sistema operacional, adicionando uma camada extra de overhead.
Containers e Orquestração
Uma das vantagens do Go é a facilidade de implantação de aplicações em containers. No entanto, gerenciar o uso de CPU se torna crucial em plataformas de orquestração de containers como o Kubernetes. Essas plataformas alocam recursos de máquinas para containers com base nas necessidades declaradas. Para otimizar a utilização dos recursos do cluster, a plataforma precisa prever o consumo de recursos de cada container.
No Kubernetes, por exemplo, é possível definir limites de CPU para um container. Esses limites são implementados através dos control groups (cgroups) do Linux, que restringem o uso da CPU pelo container.
Antes do Go 1.25, o Go não levava em consideração esses limites de CPU. Ele simplesmente definia GOMAXPROCS com base no número de núcleos da máquina, o que poderia levar a aplicação a tentar usar mais CPU do que o permitido pelo container. Para evitar que o container exceda seu limite, o kernel do Linux entra em ação e realiza o throttling.
O throttling é um mecanismo radical: ele pausa a execução do container por um período (geralmente 100ms). Isso pode ter um impacto significativo na latência, especialmente na tail latency (latência nos casos mais lentos), comparado ao efeito mais suave de multiplexing do scheduler do Go quando GOMAXPROCS está configurado corretamente. Mesmo que a aplicação não tenha muito paralelismo inerente, tarefas internas do Go, como a coleta de lixo (garbage collection), podem causar picos de uso de CPU que acionam o throttling.
O Novo Comportamento Padrão do GOMAXPROCS
Para resolver esses problemas, o Go 1.25 introduz um novo comportamento padrão para GOMAXPROCS. Agora, se um processo Go estiver rodando dentro de um container com um limite de CPU definido, o GOMAXPROCS usará o limite de CPU como valor padrão, desde que seja menor que o número de núcleos da máquina.
Além disso, o Go 1.25 monitora periodicamente o limite de CPU do container e ajusta automaticamente o GOMAXPROCS se o limite for alterado dinamicamente.
É importante notar que esses novos comportamentos só entram em vigor se o GOMAXPROCS não for explicitamente definido, seja através da variável de ambiente GOMAXPROCS ou da função runtime.GOMAXPROCS. Nesses casos, o Go continua se comportando como antes.
Diferenças Sutis entre GOMAXPROCS e Limites de CPU
Embora ambos, GOMAXPROCS e os limites de CPU, restrinjam a quantidade de CPU que um processo pode usar, eles operam com modelos ligeiramente diferentes.
GOMAXPROCS é um limite de paralelismo. Se GOMAXPROCS=8, o Go nunca executará mais de 8 goroutines simultaneamente.
Os limites de CPU, por outro lado, são limites de throughput (vazão). Eles restringem a quantidade total de tempo de CPU que pode ser usada em um determinado período de tempo (geralmente 100ms). Um limite de “8 CPUs” significa que o container pode usar 800ms de tempo de CPU a cada 100ms de tempo real.
Essa “cota” de CPU pode ser preenchida de diversas maneiras. Por exemplo, 8 threads rodando continuamente por 100ms (equivalente a GOMAXPROCS=8) ou 16 threads rodando por 50ms cada (com cada thread ficando ociosa ou bloqueada pelos outros 50ms).
Em resumo, um limite de CPU não restringe o número total de CPUs que o container pode usar, apenas o tempo total de CPU.
A maioria das aplicações tem um uso de CPU relativamente constante ao longo de períodos de 100ms, então o novo padrão de GOMAXPROCS é uma boa aproximação do limite de CPU e, certamente, uma melhoria em relação ao número total de núcleos. No entanto, cargas de trabalho com picos de uso de CPU muito rápidos e intensos podem experimentar um aumento na latência devido ao GOMAXPROCS impedir a criação de threads adicionais além da média do limite de CPU.
Além disso, como os limites de CPU podem ter componentes fracionários (por exemplo, 2.5 CPUs), e GOMAXPROCS deve ser um inteiro positivo, o Go arredonda o limite para cima para permitir o uso total da CPU alocada.
Requests de CPU
Além dos limites de CPU, as plataformas de orquestração de containers também oferecem o conceito de CPU requests. Enquanto o limite de CPU define o máximo de CPU que um container pode usar, o CPU request especifica a quantidade mínima de CPU que o container tem garantia de ter disponível o tempo todo.
É comum criar containers com um CPU request mas sem um limite de CPU. Isso permite que o container utilize recursos ociosos da máquina além do CPU request se outros containers não estiverem utilizando esses recursos. Infelizmente, isso significa que o Go não pode usar o CPU request para definir GOMAXPROCS, pois isso impediria o uso de recursos ociosos adicionais.
Containers com um CPU request ainda são constrained (restringidos) quando excedem seu request se a máquina estiver ocupada. Essa restrição é mais “suave” do que o throttling dos limites de CPU, mas picos de CPU causados por um GOMAXPROCS alto ainda podem ter um impacto negativo no comportamento da aplicação.
Devo Definir um Limite de CPU?
O artigo original entra em discussões sobre as implicações de definir ou não limites de CPU e como isso afeta o comportamento da aplicação e o planejamento de capacidade. A grosso modo, a recomendação é definir limites de CPU para garantir um comportamento mais previsível da aplicação em ambientes de containers, evitando o throttling e permitindo que o Go gerencie melhor seus recursos.
Artigo Original
Este e um resumo em português do artigo original publicado no blog oficial do Go.
Titulo original: Container-aware GOMAXPROCS
Leia o artigo completo em ingles no Go Blog
Autor original: Michael Pratt and Carlos Amedee