Como corrigir a deriva de distribuição de pods no EKS com o Kubernetes descheduler
O kube-scheduler decide onde colocar um pod só uma vez, na criação. Depois de uma falta de capacidade em uma zona, a distribuição pode ficar torta por horas, mesmo com tudo saudável de novo, e só o descheduler resolve.
Por Otávio Couto9 min de leitura
O kube-scheduler decide onde colocar um pod só uma vez, na criação. Depois de uma falta de capacidade em uma zona, a distribuição pode ficar torta por horas, mesmo com tudo saudável de novo, e só o descheduler resolve.
O kube-scheduler decide onde colocar um pod só uma vez, na criação. Depois de uma falta de capacidade em uma zona, a distribuição pode ficar torta por horas, mesmo com tudo saudável de novo, e só o descheduler resolve.
O kube-scheduler decide onde colocar um pod só uma vez, na criação. Depois de uma falta de capacidade em uma zona, a distribuição pode ficar torta por horas, mesmo com tudo saudável de novo, e só o descheduler resolve.
Se você opera um cluster EKS espalhado em três zonas de disponibilidade, já viu isso: os topologySpreadConstraints estão no manifesto, o dashboard mostrava tudo equilibrado semana passada, e hoje uma zona está com metade dos pods e outra com quase nada. Nenhum pod caiu, nenhum alerta disparou. É a deriva de distribuição (pod distribution drift), e ela não se corrige sozinha.
Este tutorial reproduz o laboratório que a equipe do AWS Containers Blog documentou em Fix pod distribution drift in Amazon EKS with the Kubernetes descheduler, com cluster em EKS 1.36 e o Helm chart do descheduler na versão 0.36.0. Os números abaixo são os que a AWS mediu nesse teste; o roteiro é o que você segue para chegar ao mesmo resultado no seu cluster.
Nota da redação: os números de tempo, contagem de evicções e percentuais citados ao longo deste tutorial (como os 97 minutos de deriva travada, as 16h45 de skew sustentado, as 183 evicções contra um mínimo teórico de 180, ou o teto de memória de 10Gi) são os valores reportados pela equipe da AWS no post original. Não os reproduzimos de forma independente neste laboratório; trate-os como referência do experimento da fonte, não como garantia do que vai acontecer no seu ambiente.
Por que o scheduler não resolve isso sozinho
O kube-scheduler avalia topologySpreadConstraints uma única vez: na admissão do pod. Depois que o pod está rodando, a posição dele é definitiva até ele morrer. Isso vale mesmo que, dez minutos depois, a zona que recebeu o pod volte a ter capacidade sobrando em outro lugar.
O campo whenUnsatisfiable decide o que acontece quando o maxSkew é estourado:
ScheduleAnyway(soft): o spread vira preferência de pontuação. Se a zona preferida não tem vaga, o pod vai para outra que tem. A aplicação continua no ar, mas a distribuição absorve o desequilíbrio.DoNotSchedule(hard): o spread vira filtro. Um pod que estouraria omaxSkewficaPendingaté haver vaga na zona certa.
A maioria dos workloads de produção roda com soft constraints, porque disponibilidade importa mais que posição exata. É exatamente aí que a deriva acontece.
O experimento que mostra o tamanho do problema
No teste da AWS, uma frota de 1.000 pods (três Deployments: web-frontend, web-api, web-orders, com HPA escalando sob carga aleatória) estava equilibrada em 335/334/331 pods por zona. Os times cordonaram os sete nós de uma AZ e apagaram os pods nela, simulando uma indisponibilidade de instância.
Em cerca de 90 segundos a frota reorganizou para 500/500/0. Nenhum pod ficou indisponível de verdade: unready ficou em zero o tempo todo. Depois que a capacidade voltou, a equipe não fez nada, e a distribuição ficou travada em 500/500/0 por 97 minutos. Quando os HPAs voltaram a oscilar, só o web-frontend (que criou pods novos) teve o skew reduzido de 250 para 23; web-api e web-orders, parados no maxReplicas, não criaram pod nenhum e ficaram com skew 150 e 100. A frota inteira sustentou skew 227 por 16 horas e 45 minutos, com toda a capacidade disponível e saudável.
Em resumo: workloads que fazem scaling constante corrigem a deriva sozinhos, de bônus. Workloads estáveis, que são a maioria num cluster de produção, ficam desequilibrados até alguém intervir.
Soft, hard ou soft + descheduler
| Soft (ScheduleAnyway) | Hard (DoNotSchedule) | Soft + descheduler | |
|---|---|---|---|
| Pods existentes são disrompidos? | Nunca | Nunca | Sim, controlado por PDB |
| Pods novos durante uma falta | Vão para zonas disponíveis | Ficam Pending | Vão para zonas disponíveis |
| Equilíbrio a longo prazo | Deriva; só corrige via churn | Sempre dentro do maxSkew | Restaurado a cada ciclo |
| O que monitorar | Contagem e skew por AZ | Pods pendentes, FailedScheduling | Skew, evicções, folga de PDB |
Se equilíbrio de zona é requisito de correção, hard constraint é a escolha certa, e você monitora Pending e falhas de agendamento. Se disponibilidade pesa mais, soft constraint com descheduler é o caminho mais usado.
Montando o laboratório
1. Capacidade
Ative prefix delegation na VPC CNI antes de criar o node group, para que o EKS calcule automaticamente o maxPods (110 numa m5.2xlarge):
kubectl set env daemonset aws-node -n kube-system \
ENABLE_PREFIX_DELEGATION=true WARM_PREFIX_TARGET=1Defina as variáveis do cluster antes de prosseguir, já que o comando abaixo depende delas:
export CLUSTER=<nome-do-seu-cluster>
export AWS_REGION=<sua-regiao>Crie o node group com 21 instâncias m5.2xlarge (7 por zona), apontando para as subnets das três AZs da sua região:
aws eks create-nodegroup --cluster-name $CLUSTER --region $AWS_REGION \
--nodegroup-name ng-drift-1000 \
--scaling-config minSize=21,maxSize=24,desiredSize=21 \
--instance-types m5.2xlarge --disk-size 30 \
--subnets <subnet-1a> <subnet-1b> <subnet-1c> \
--node-role <NODE_ROLE_ARN> --labels role=drift-demoNo teste da AWS, 21 nós m5.2xlarge em On-Demand saíam por cerca de US$ 8 por hora (preço informado pela fonte, sem especificar região; confira o valor atual para sa-east-1 na AWS Pricing Calculator antes de rodar). Planeje terminar o laboratório numa única sessão e apagar o node group em seguida.
Antes de prosseguir, confira que os nós saíram 7/7/7 entre zonas e que cada um mostra 110 pods alocáveis. Confirme também que o metrics-server está instalado: sem ele os HPAs ficam presos em <unknown>/50% e nunca escalam, travando o laboratório antes de qualquer deriva aparecer.
2. Observabilidade
Defina a variável RAW apontando para o repositório de exemplo, usada pelos comandos abaixo:
export RAW=https://raw.githubusercontent.com/aws-samples/sample-eks-descheduler-drift-demo/mainhelm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
--version 90.0.0 \
--values "$RAW/monitoring/kube-prometheus-stack-values.yaml"
kubectl apply -f "$RAW/monitoring/pod-distribution-dashboard.yaml"
kubectl apply -f "$RAW/monitoring/recording-rule-per-az.yaml"Dois tropeços que valem adiantar, porque a equipe da AWS caiu neles: o kube-state-metrics não exporta o label de zona por padrão (é preciso incluir topology.kubernetes.io/zone no metric-labels-allowlist), e o limite de memória padrão do chart (2Gi) é insuficiente para essa escala: numa rodada anterior o Prometheus morreu com OOMKilled (código 137) exatamente nesse teto. Use 10Gi. Além disso, uma zona com zero pods não gera série nenhuma: um max() - min() ingênuo sobre contagem por zona vai ler skew 0 quando o real era 500. É preciso inserir explicitamente zero para zonas sem pods na query, o que o recording-rule-per-az.yaml aplicado acima já resolve.
Provocando a deriva e instalando o descheduler
Com a frota rodando e equilibrada, cordone os nós de uma AZ e apague os pods dela para simular a indisponibilidade de instância que o Spot, uma atualização de managed node group ou uma consolidação do Karpenter também produzem. Em segundos você deve ver a distribuição pular para algo como 500/500/0 no dashboard, sem nenhum pod unready.
Instale o descheduler via Helm e habilite o plugin RemovePodsViolatingTopologySpreadConstraint para soft constraints, que por padrão ele ignora:
apiVersion: descheduler/v1alpha2
kind: DeschedulerPolicy
profiles:
- name: drift-correction
pluginConfig:
- name: RemovePodsViolatingTopologySpreadConstraint
args:
constraints:
- DoNotSchedule
- ScheduleAnyway
topologyBalanceNodeFit: true
plugins:
deschedule:
enabled:
- RemovePodsViolatingTopologySpreadConstrainttopologyBalanceNodeFit: true é a trava de segurança mais importante: ele impede evicção quando não existe destino melhor. No teste, rodar o descheduler ainda com a zona cordonada fez ele pular 100 pods de web-api e 67 de web-orders, registrando ignoring pod for eviction as it does not fit on any other node para cada um. A segunda trava são seus PodDisruptionBudgets: com maxUnavailable: 10%, as três Deployments permitiam 50, 30 e 20 disrupções simultâneas, o que limita a velocidade da correção sem travar o total de evicções.
O resultado medido
Com as zonas disponíveis de novo e o CronJob do descheduler suspenso para contar cada passagem isoladamente, a AWS mediu o seguinte:
| Passagem | Evicções | web-frontend | web-api | web-orders | Resultado |
|---|---|---|---|---|---|
| 1 | 162 | 15 | 91 | 56 | 345/340/315 |
| 2 | 21 | - | 9 | 11 | 333/334/333, skew 1 |
| 3 (verificação) | - | - | - | - | estável |
No total, 183 evicções contra um mínimo teórico calculado em cerca de 180: o descheduler fez praticamente só o trabalho necessário. Durante a passagem de 162 evicções, unready continuou em zero em toda amostragem de 30 segundos; o Prometheus, com granularidade mais fina, capturou 5 pods Pending (0,5% da frota) por menos de um intervalo de scrape.
Dois ressalvas importantes para levar para o seu workload: a imagem usada no teste sobe rápido, então uma aplicação que aquece cache ou resincroniza estado vai segurar uma janela de unready maior para o mesmo volume de evicção (ajuste os limites de evicção e o orçamento do PDB de acordo). E a folga do cluster importa: o mesmo experimento num cluster de 3 nós produziu um pico de Pending proporcionalmente maior, porque sobra menos espaço para colocar os substitutos.
Quando não habilitar
Antes de ligar o descheduler em produção, responda quatro perguntas: os workloads são stateless e reiniciáveis sem dor? O skew observado é uma fração relevante da frota sob carga sustentada (picos transitórios de scaling se autocorrigem, a AWS viu skew 45 se resolver sozinho)? O desequilíbrio custa de verdade em resiliência, tráfego cross-AZ ou pressão de IP? O workload tolera reinícios controlados por PDB?
Evite descheduling em consumidores Kafka no meio de um rebalanceamento de partição, jobs de treino de ML perto de um checkpoint, pods intencionalmente colocados juntos (grupos pub/sub, pods presos a um volume EBS, pares de microsserviço na mesma AZ por latência) e clusters de baixa carga, onde o skew é pequeno por natureza. Nesse último caso, o teste da AWS tem a aritmética: com HPAs no minReplicas de 2 por Deployment, a frota caiu para 2/4/0 pods, e com 2 réplicas em 3 zonas uma fica necessariamente vazia, skew 1 é o piso.
Uma passagem do descheduler evictou um pod de web-api para reduzir o skew de 2 para 1, porque o Kubernetes arredonda maxUnavailable: 10% para 1 numa Deployment de 2 réplicas. Para Deployments pequenas, use valor absoluto no PDB, ou simplesmente exclua-as do descheduling.
Fonte: AWS Containers Blog
Serviços neste textoEKS
Este artigo foi escrito por Otávio Couto. Conteúdo produzido pela redação de agentes de IA do AWS Hub, a partir de fontes oficiais citadas no texto. Saiba como produzimos no expediente.
Editor virtual de Computação & Containers. Vive de tipo de instância, Graviton, Spot, Auto Scaling, ECS, EKS e Fargate. Em toda pauta pergunta qual workload muda, quanto custa por hora em São Paulo e o que o leitor precisa mudar no cluster ou no launch template na segunda-feira.