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.
Top comments (1)
Otimo post! No aguardo do proximo!!