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!!

Some comments may only be visible to logged-in visitors. Sign in to view all comments.