DEV Community

Dhellano Castro
Dhellano Castro

Posted on

Gestão de nodes e pods com Karpenter - Perspectiva do Desenvolvedor

O problema

Recentemente "tive a oportunidade" de passar por um problema envolvendo um deploy de um serviço em estratégia canário (se você não sabe o que é um deploy em canário, pode ler mais sobre isso aqui: Entendendo como funciona o Canary Deployment ) e o autoscaler de nodes Karpenter. Enquanto o canário estava em execução, o Karpenter estava drenando vários pods da aplicação e impactando a latência do serviço.

O que é o Karpenter e o que ele faz?

Imaginem que o Karpenter é o profissional que faz a “faxina inteligente” do cluster (K8s).

Ele fica monitorando constantemente os nós (máquinas EC2) do cluster — e com qual fim?

Basicamente, verificando se as máquinas estão cheias, vazias ou mal utilizadas. Quando identifica que há recursos sobrando, ele pode encerrar máquinas subutilizadas; quando percebe falta de capacidade, ele também pode provisionar novas máquinas.

Ou seja: o Karpenter não só “mata nós”, mas também cria nós novos, conforme a demanda.

Ele é, por definição, um autoscaler de nós — um paralelo ao Keda, que faz autoscaling de pods.

Enquanto o Keda ajusta o número de pods, o Karpenter ajusta o número de máquinas.

E como o cenário de deploy em canário se relaciona com isso?

No canário, acontecem os seguintes passos:

  • a versão antiga da app ainda fica rodando
  • a nova versão sobe alguns pods
  • aos poucos, a nova vai substituindo a antiga — e as duas coexistem por um tempo

Essa coexistência (em vários nós, inclusive novos que foram criados para suportar a nova versão) pode desbalancear o cluster. Por exemplo, alguns pods antigos morrem, outros novos sobem, e alguns nós acabam ficando quase vazios. Quando o Karpenter “vê” esses nós com pouca carga, ele entende que estão subutilizados e começa a agir: drena os pods daquele nó e os realoca em outros nós mais bem aproveitados, para então encerrar a instância EC2.

Mas atenção: o Karpenter não costuma drenar dois nós simultaneamente.
Ele age de forma progressiva, reconhecendo e ajustando o uso à medida que as condições mudam. Um cenário de drenagem em massa só ocorreria se dezenas de pods saíssem ao mesmo tempo, o que é uma situação incomum. Além disso, antes de encerrar o nó, o Karpenter primeiro realoca os pods, o que pode gerar reinícios, mas o serviço tende a continuar ativo durante o processo. Se o comportamento parecer agressivo (muitos pods sendo terminados de uma vez), é sinal de que a configuração está muito sensível e precisa de ajuste.

O papel do PDB (Pod Disruption Budget)

Aqui entra o PDB, que define um “orçamento” de quantos pods podem ser interrompidos ao mesmo tempo. Com ele, você instrui o cluster e o Karpenter a não drenar todos os pods de uma vez.

Ex.: imagine que você tem 10 pods no ar
Sem PDB:

  • O Karpenter decide apagar 2 nós
  • Cada nó tem 5 pods
  • Ele drena ambos ao mesmo tempo
  • A aplicação pode ficar totalmente fora do ar por alguns segundos

Com PDB definido (ex.: 80% dos pods devem permanecer ativos):

  • O Karpenter e o K8s drenam no máximo 2 dos 10 pods de cada vez
  • A app continua disponível e a transição ocorre de forma suave

Assim, o PDB protege a operação e evita alta latência ou indisponibilidade total.

Interação entre Keda e Karpenter

  • O Keda escala pods (com base em métricas como fila, CPU, etc.)
  • O Karpenter escala nós (subindo ou encerrando máquinas EC2).

Cenário comum:

  • O Keda detecta fila cheia e escala de 2 → 10 pods
  • A fila processa rápido, e o Keda reduz a quantidade de pods novamente
  • O Karpenter percebe nós quase vazios e começa a drenar e encerrar máquinas

Tudo certo — até aqui.

O problema aparece quando os tempos de reação dos dois não estão bem sincronizados.

Isso pode causar a famosa flutuação de pods:

  • Keda escala os pods por conta da fila cheia → Karpenter cria novos nós
  • A fila processa rapidamente e o Keda reduz a quantidade de pods → Karpenter ainda pode está criando máquinas novas
  • Karpenter detecta baixo uso → começar a drenar e desligar as máquinas
  • Fila volta a encher → Keda tenta escalar → não há nós disponíveis (Karpenter ainda matando as máquinas)
  • Pods ficam Pending → Karpenter começa a criar novos nós

E o ciclo se repete até estabilizar.

Por isso, é importante parametrizar um autoscaling menos agressivo, definindo intervalos maiores entre upscale e downscale, e limitando quantos pods podem escalar por vez.

Impactos em aplicações Java

Durante essa flutuação, apps Java sofrem bastante, porque:

  • Cada novo pod leva um certo tempo para “aquecer” (JIT, cache, pools, conexões...)
  • Pods sobem e morrem antes de ficarem prontos

Resultado:

  • Aumento no tempo de resposta da app
  • Perda de vazão
  • Mais tempo de GC inicial
  • É comum muitos logs de started e shutdown

Quando pods novos degradam pods antigos

Mesmo com anti-affinity configurado (o que evita que vários pods do mesmo serviço caiam no mesmo nó), é possível que novos pods subam em nós já ocupados. O K8s agenda pods com base nos requests declarados
— que funcionam como reserva de capacidade. O Karpenter e o K8s garantem essa reserva, ou seja, não realocam mais pods do que cabe nos requests. Se sobrar capacidade, ela pode ser usada sob demanda até o limit configurado.

Ex.: imagine um nó com 4 vCPUs

  • 2 pods limitados (limit, não request) a 2 vCPUs cada
  • O cluster tenta subir mais um pod no mesmo nó
  • Agora temos 3 JVMs disputando 4 CPUs

Durante o start do terceiro pod, ele consome muita CPU (JIT, cache, inicialização...), o que afeta o desempenho dos pods antigos. Isso pode aumentar a latência e até induzir o Keda a escalar mais pods (se estiver usando CPU como trigger), piorando ainda mais a situação.

O velho problema do readiness

Se um readiness probe estiver mal configurado, ele pode marcar um pod como “OK” antes de estar realmente pronto.

O resultado: o balanceador envia tráfego para ele, as respostas demoram, surgem picos de latência e, em alguns casos, o próprio K8s começa a matar e recriar pods — um ciclo de degradação.

Outro ponto crítico: conexões de banco

Quando novos pods sobem, eles normalmente criam novas conexões no banco, criam conexões com outros serviços (kafka, rabbitMQ...) e tentam se registrar em um service discovery, entre outras coisas. Se não houver limite de conexões no banco, o mesmo pode ficar sobrecarregado e/ou começar a recusas conexões, afetando assim os pods antigos que já estavam no ar no mesmo nó em que os pods novos subiram.

Para além da configuração de recursos e probes: Graceful shutdown

Um das causas mais comuns para restart de pods por “qualquer motivo", é eles continuarem recebendo requests mesmo quando já estão "morrendo", ou como já falado, quando ainda não estão prontos. Uma solução para o primeiro caso é a configuração do recurso de Graceful Shutdown, que evita que o pod seja terminado enquanto ainda recebe requisições. Isso evita ruídos no processo de check de probes (liveness e readiness) e consequente flutuação de pods subindo e descendo, causando aumento de latência e outros efeitos colaterais já mencionados.

Em resumo

  • O Karpenter é importante para a gestão funcional e financeira do nodes no cluster.
  • O Keda é importante para a gestão funcional e financeira dos recursos do cluster.
  • E a engenharia entender como as ferramentas funcionam é importante para configurar os serviços de maneira sustentável e eficaz, fazendo sentido funcionalmente para os clientes (baseado na necessidade) e financeiramente para nós enquanto plataforma.

Documentação de apoio

Top comments (1)

Collapse
 
mensonones profile image
Emerson Vieira

Otimo post! No aguardo do proximo!!