Autoscaling no Kubernetes na prática: HPA, VPA e Cluster Autoscaler sem mistério
Guia técnico dos três escaladores do Kubernetes: como HPA, VPA e Cluster Autoscaler funcionam, quando usar cada um, métricas, thresholds e armadilhas.
Neste artigo
Quando você sobe uma aplicação no Kubernetes e ela começa a receber tráfego real, uma pergunta aparece rápido: quantas réplicas eu deixo rodando? Se você dimensiona para o pico, paga por capacidade ociosa a maior parte do dia. Se dimensiona para a média, o serviço engasga na primeira campanha de marketing ou no horário de pico. Autoscaling existe justamente para tirar esse número da sua cabeça e colocá-lo sob controle de um loop automático que observa a carga e reage a ela.
O problema é que "autoscaling no Kubernetes" não é uma coisa só. São três mecanismos diferentes, que atuam em camadas diferentes, e que muita gente confunde ou tenta usar de forma sobreposta sem entender o conflito que isso gera. O HPA (Horizontal Pod Autoscaler) muda o número de pods. O VPA (Vertical Pod Autoscaler) muda o tamanho de cada pod. O Cluster Autoscaler muda o número de nós na infraestrutura. Cada um resolve um problema específico, e usá-los juntos exige entender como eles interagem — porque dois deles brigam entre si se você não tomar cuidado.
Neste guia você vai entender como cada escalador funciona por dentro, qual métrica ele observa, como configurar thresholds e janelas de estabilização para evitar oscilação, e as armadilhas operacionais que derrubam autoscaling em produção — a começar pela mais comum de todas, que é rodar pods sem requests declarados. Vamos do alicerce (requests e limits) até o topo (escalonamento orientado a eventos com KEDA).
Requests e limits: o alicerce de tudo#
Antes de falar de qualquer escalador, você precisa entender que autoscaling no Kubernetes é construído sobre requests e limits. Sem eles, nada funciona direito, e boa parte dos problemas de escalonamento que as pessoas relatam nasce aqui.
O requests é a quantidade de CPU e memória que o pod declara precisar para funcionar. O scheduler usa esse número para decidir em qual nó colocar o pod — ele só encaixa o pod num nó que tenha requests livre suficiente. O limits é o teto: a CPU é estrangulada (throttled) quando o pod passa do limite, e a memória causa um OOMKill se o pod ultrapassar o limite de RAM.
``yaml resources: requests: cpu: "250m" memory: "256Mi" limits: cpu: "500m" memory: "512Mi" ``
Por que isso importa tanto para o autoscaling? Porque o HPA calcula utilização em cima do requests, não do consumo absoluto. Quando você diz "escala quando a CPU passar de 70%", esse 70% é 70% do requests somado de todos os pods. Um pod que consome 180m de CPU com requests de 250m está a 72% de utilização. O mesmo pod, se você não tivesse declarado requests, deixaria o HPA sem denominador — e o autoscaler simplesmente não consegue calcular a porcentagem, marcando a métrica como <unknown> e não escalando nada.
Essa é a primeira armadilha, e a mais frequente: pod sem requests é invisível para o HPA baseado em utilização. Antes de configurar qualquer escalador, garanta que todo contêiner tem requests de CPU e memória declarados de forma realista. "Realista" aqui significa medido, não chutado — observe o consumo em produção ou em teste de carga por alguns dias antes de cravar o número.
Horizontal Pod Autoscaler: mais réplicas#
O HPA é o escalador que a maioria das pessoas conhece. Ele ajusta o número de réplicas de um Deployment, ReplicaSet ou StatefulSet com base em métricas observadas. A ideia é simples: se a carga por pod está alta, adicione pods; se está baixa, remova pods.
O loop de controle#
O HPA roda como um loop dentro do controller manager, por padrão a cada 15 segundos. A cada ciclo ele consulta as métricas atuais dos pods, aplica uma fórmula e decide o número desejado de réplicas. A fórmula central é direta:
`` réplicas desejadas = ceil( réplicas atuais × ( métrica atual / métrica alvo ) ) ``
Se você tem 4 réplicas rodando a 90% de CPU e o alvo é 70%, a conta dá ceil(4 × (90/70)) = ceil(5.14) = 6 réplicas. O HPA sobe para 6. No ciclo seguinte, com 6 réplicas dividindo a mesma carga, a utilização média cai, e o loop se estabiliza. É um controlador proporcional simples, e entender essa fórmula ajuda a prever como o sistema vai reagir a um pico.
De onde vêm as métricas? Para CPU e memória, o HPA lê o metrics-server, um componente leve que agrega o consumo de recursos reportado pelo kubelet de cada nó. O metrics-server não é instalado por padrão em muitos clusters — se o seu kubectl top pods não retorna nada, é sinal de que ele não está lá, e sem ele o HPA baseado em recursos não funciona.
Um manifesto de HPA na prática#
Aqui está um HPA moderno (API autoscaling/v2) que escala por CPU e memória ao mesmo tempo:
```yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: api-web spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: api-web minReplicas: 3 maxReplicas: 20 metrics:
- type: Resource
resource: name: cpu target: type: Utilization averageUtilization: 70
- type: Resource
resource: name: memory target: type: Utilization averageUtilization: 80 ```
Quando você define mais de uma métrica, o HPA calcula o número desejado para cada uma e usa o maior — a métrica mais "faminta" ganha. Isso é importante: se a CPU pede 6 réplicas e a memória pede 4, o resultado é 6. O HPA nunca deixa uma métrica sozinha derrubar réplicas que outra métrica ainda considera necessárias.
Repare também nos minReplicas e maxReplicas. O mínimo protege contra o serviço ficar sem capacidade em vale de tráfego (e evita cold start no primeiro request depois de um período calmo). O máximo é uma trava de segurança: sem ela, um pico anômalo ou um loop de retry mal comportado poderia escalar para centenas de pods e estourar tanto o cluster quanto sua fatura.
Custom e external metrics#
CPU e memória resolvem muitos casos, mas nem todos. Um serviço que processa uma fila pode estar com CPU baixa e mesmo assim acumulando trabalho — o gargalo é o backlog, não o processador. Para esses casos o HPA aceita métricas custom (originadas de dentro do cluster, tipicamente requisições por segundo ou profundidade de fila expostas pela sua aplicação) e external (originadas de fora, como o tamanho de uma fila SQS ou o lag de um consumer group do Kafka).
Essas métricas não vêm do metrics-server. Elas passam por um adapter que implementa a Custom Metrics API ou a External Metrics API do Kubernetes. O mais comum é o Prometheus Adapter, que traduz consultas PromQL em métricas que o HPA consegue ler. Com ele você escala por "requisições por segundo por pod", que costuma ser um sinal muito mais fiel de carga real do que a CPU.
```yaml metrics:
- type: Pods
pods: metric: name: http_requests_per_second target: type: AverageValue averageValue: "100" ```
Aqui o alvo é um valor absoluto: 100 requisições por segundo por pod. Se a média subir para 150, o HPA adiciona réplicas até a média por pod voltar para perto de 100. Note a diferença de type: Utilization é porcentagem do requests, AverageValue é um número bruto por pod, e Value é um número total agregado.
Estabilização e behavior: matando o thrashing#
O maior inimigo prático do HPA é o thrashing: o serviço escala para cima, a carga cai, ele escala para baixo, a carga sobe de novo, e você fica com réplicas nascendo e morrendo o tempo todo. Isso desperdiça recursos, aumenta cold starts e polui seus dashboards.
O Kubernetes resolve isso com a stabilization window e as behavior policies. A janela de estabilização faz o HPA olhar para trás antes de reduzir réplicas: em vez de reagir ao valor instantâneo, ele considera o pico dos últimos N segundos. Por padrão são 300 segundos (5 minutos) para scale-down, o que significa que o HPA só remove réplicas depois que a carga ficou baixa de forma sustentada por cinco minutos. Para scale-up a janela padrão é zero — subir é urgente, descer pode esperar.
```yaml behavior: scaleDown: stabilizationWindowSeconds: 300 policies:
- type: Percent
value: 50 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 0 policies:
- type: Percent
value: 100 periodSeconds: 30
- type: Pods
value: 4 periodSeconds: 30 selectPolicy: Max ```
As policies controlam a velocidade da mudança. No exemplo, o scale-up pode dobrar o número de pods a cada 30 segundos OU adicionar 4 pods a cada 30 segundos, o que for maior (selectPolicy: Max). O scale-down é mais conservador: remove no máximo 50% das réplicas por minuto, para não derrubar capacidade de forma abrupta e causar um novo pico. Ajustar essas políticas é o que separa um HPA nervoso de um HPA que respira com o tráfego. Suba rápido, desça devagar — essa é a regra prática que resolve a maioria dos casos de oscilação.
Vertical Pod Autoscaler: pods do tamanho certo#
Enquanto o HPA muda quantos pods você tem, o VPA muda o tamanho de cada pod. Ele observa o consumo histórico de CPU e memória e recomenda (ou aplica) novos valores de requests para que o pod não fique nem superdimensionado nem sufocado.
O VPA resolve um problema diferente do HPA. Pense num serviço que não escala bem horizontalmente — um job de processamento com estado, um banco, um serviço legado single-threaded. Adicionar réplicas não ajuda; o que ele precisa é de mais memória ou mais CPU por instância. É aí que o VPA brilha. Ele também é ótimo para acertar os requests iniciais que você chutou lá no começo: em vez de você ficar adivinhando, o VPA mede e corrige.
Os três modos#
O VPA opera em modos que definem o quão agressivo ele é:
- Off: ele só calcula e expõe recomendações, sem tocar em nada. Excelente para descobrir os valores certos de
requestssem risco — você lê a recomendação comkubectl describe vpae aplica manualmente quando quiser. - Initial: ele define os
requestsapenas na criação do pod, e não mexe mais depois disso. - Auto / Recreate: ele aplica as recomendações em pods já rodando. Como o Kubernetes tradicionalmente não permite mudar
requestsde um pod vivo, o VPA nesse modo despeja e recria o pod com os novos valores. Isso significa reinício, e é a fonte da principal ressalva de usar VPA em Auto em serviços sensíveis a disponibilidade.
```yaml apiVersion: autoscaling.k8s.io/v1 kind: VerticalPodAutoscaler metadata: name: worker-vpa spec: targetRef: apiVersion: apps/v1 kind: Deployment name: worker updatePolicy: updateMode: "Off" resourcePolicy: containerPolicies:
- containerName: "*"
minAllowed: cpu: "100m" memory: "128Mi" maxAllowed: cpu: "2" memory: "2Gi" ```
Começar em updateMode: "Off" é a recomendação para quase todo mundo. Você deixa o VPA observando por alguns dias, lê as recomendações, e ganha uma base de dados real sobre o que suas cargas realmente consomem — o que melhora até o dimensionamento manual e a configuração do próprio HPA.
O conflito HPA + VPA#
Aqui está o ponto que causa mais estrago quando ignorado: você não pode usar HPA e VPA sobre a mesma métrica no mesmo Deployment. O motivo é um loop de realimentação destrutivo. O HPA olha a utilização de CPU e adiciona réplicas quando ela sobe. O VPA olha o consumo e aumenta o requests de CPU quando ele sobe. Como a utilização é consumo / requests, os dois ficam brigando pelo mesmo número: o VPA sobe o denominador, o que derruba a utilização, o que faz o HPA remover réplicas, o que concentra carga, o que faz o VPA subir mais o requests... e o sistema oscila de forma imprevisível.
A regra prática é clara:
- Use HPA para CPU/memória e VPA em modo Off (só recomendação) no mesmo workload — o VPA te ajuda a acertar os
requests, o HPA cuida das réplicas. - Ou use VPA em Auto para CPU/memória e o HPA em cima de uma métrica custom que não seja CPU nem memória (por exemplo, requisições por segundo). Assim cada escalador olha um sinal diferente e eles não colidem.
Nunca deixe HPA e VPA disputando o mesmo recurso. Se você só tem essa regra na cabeça ao terminar de ler, já evitou um dos incidentes mais chatos de diagnosticar.
Cluster Autoscaler: mais nós#
HPA e VPA operam dentro da capacidade que o cluster já tem. Mas o que acontece quando o HPA quer subir 10 pods novos e não há espaço em nenhum nó para agendá-los? Sem uma terceira camada, esses pods ficam Pending para sempre, e seu autoscaling horizontal simplesmente não se materializa.
O Cluster Autoscaler fecha esse ciclo. Ele observa pods que não conseguem ser agendados por falta de recursos e, quando encontra pods Pending por esse motivo, adiciona nós ao cluster — pedindo uma nova máquina ao provedor de nuvem através dos node groups (Auto Scaling Groups na AWS, node pools no GKE e AKS, e equivalentes). No sentido inverso, quando um nó fica subutilizado por tempo suficiente e seus pods poderiam ser realocados em outros nós, ele drena e remove o nó, devolvendo a máquina e cortando custo.
Como ele decide#
O gatilho de scale-up do Cluster Autoscaler é justamente o pod Pending por falta de recursos — e é por isso que requests bem definidos são novamente essenciais. O Cluster Autoscaler raciocina inteiramente em cima de requests: ele calcula se o pod Pending caberia num nó novo do node group olhando o requests do pod contra a capacidade do tipo de máquina. Se seus pods não declaram requests, o Cluster Autoscaler pode achar que cabe tudo em qualquer lugar e nunca provisionar máquina nenhuma, mesmo com os nós sufocados de trabalho real.
O scale-down é mais cauteloso. Um nó só é candidato à remoção se estiver abaixo de um limiar de utilização (por padrão em torno de 50%) por um período contínuo (por padrão 10 minutos), e se todos os seus pods puderem ser reagendados em outros nós respeitando PodDisruptionBudgets, afinidades e taints. Pods de sistema, pods com armazenamento local e pods sem controlador que os recrie podem bloquear a remoção de um nó — o que às vezes explica por que um cluster teimosamente não encolhe.
A interação com o HPA é uma cadeia elegante quando bem configurada: o HPA decide que precisa de mais réplicas → cria pods → se não há espaço, os pods ficam Pending → o Cluster Autoscaler vê os Pending e adiciona um nó → o scheduler agenda os pods no nó novo. Cada camada faz uma coisa e a passagem entre elas é o que dá elasticidade real à infraestrutura. Vale registrar que existe também o Karpenter, uma alternativa mais moderna ao Cluster Autoscaler na AWS que provisiona nós de forma mais direta e flexível, sem depender de node groups pré-configurados — se você opera na AWS, vale conhecer.
KEDA: escalonamento orientado a eventos#
O HPA baseado em CPU tem um limite conceitual: ele reage ao sintoma (CPU alta) e não à causa (trabalho acumulado). Para cargas orientadas a eventos — consumidores de fila, processadores de stream, jobs disparados por mensagens — você quer escalar pelo tamanho da fila, e quer poder ir até zero quando não há trabalho, algo que o HPA sozinho não faz (ele respeita minReplicas, que precisa ser pelo menos 1 na forma tradicional).
O KEDA (Kubernetes Event-Driven Autoscaling) é a evolução natural aqui. Ele não substitui o HPA — na verdade ele gera um HPA por baixo dos panos — mas adiciona duas coisas cruciais: dezenas de scalers prontos (Kafka, RabbitMQ, SQS, Azure Service Bus, Redis, Prometheus, cron e muitos outros) e a capacidade de escalar de zero a um e de volta a zero. Quando uma fila está vazia, o KEDA desliga o serviço por completo; quando a primeira mensagem chega, ele ativa uma réplica e entrega o resto do escalonamento ao HPA que ele mesmo mantém.
```yaml apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: name: processador-fila spec: scaleTargetRef: name: processador minReplicaCount: 0 maxReplicaCount: 30 triggers:
- type: rabbitmq
metadata: queueName: pedidos mode: QueueLength value: "20" ```
Nesse exemplo, o KEDA mantém aproximadamente 20 mensagens por réplica: se a fila tem 200 mensagens pendentes, ele mira 10 réplicas. Fila vazia, zero réplicas, custo zero. Para arquiteturas de processamento assíncrono, isso é transformador — você escala pelo trabalho real que existe, não por um proxy indireto dele. Se o seu HPA custom com Prometheus Adapter está ficando complicado de manter, o KEDA quase sempre é o caminho mais limpo.
Armadilhas comuns em produção#
Já cobrimos algumas ao longo do texto, mas vale consolidar as que mais derrubam autoscaling na prática, porque quase todo incidente cai numa destas categorias:
- Pods sem
requests. Repetimos porque é o número um. Semrequests, o HPA por utilização não calcula nada e o Cluster Autoscaler não sabe dimensionar nós. Declare sempre, e declare com base em medição. - Thrashing por falta de estabilização. Se seu serviço fica subindo e descendo réplicas o tempo todo, ajuste a
stabilizationWindowSecondsde scale-down e limite a velocidade com behavior policies. Suba rápido, desça devagar. - Cold start ignorado. Se cada pod novo leva 40 segundos para ficar pronto (JVM aquecendo, cache carregando, conexões abrindo), o HPA que reage tarde vai deixar o serviço sofrer durante o pico inteiro. Combata isso com
minReplicasmais generoso, com escalonamento antecipado por métrica custom (RPS sobe antes da CPU), e com readiness probes honestas que não deixem tráfego entrar antes do pod estar realmente pronto. - HPA e VPA no mesmo recurso. O conflito de denominador que já detalhamos. Separe os sinais ou mantenha o VPA em Off.
maxReplicassem teto sensato ou baixo demais. Muito alto e um retry storm te leva à falência; muito baixo e você bate no teto durante o pico legítimo e o serviço degrada mesmo com o HPA "querendo" escalar. Dimensione o máximo pensando no pior caso plausível de tráfego.- Métricas ausentes ou atrasadas. Se o metrics-server não está instalado, ou se o Prometheus Adapter está com scrape lento, o HPA opera com dados velhos ou nenhum dado. Monitore o próprio pipeline de métricas — um HPA cego é pior que nenhum HPA, porque te dá falsa sensação de segurança.
PodDisruptionBudgetausente durante scale-down. Quando o Cluster Autoscaler ou o VPA em Auto vai remover/recriar pods, um PDB mal configurado (ou inexistente) pode derrubar réplicas demais de uma vez e causar indisponibilidade. Defina PDBs para os serviços críticos.
Como começar#
Se você está introduzindo autoscaling num serviço hoje, siga esta ordem e resista à tentação de pular etapas. Primeiro, acerte os requests e limits. Instale o metrics-server, rode o VPA em modo Off por alguns dias e use as recomendações dele — junto com o consumo observado em teste de carga — para cravar valores realistas. Sem esse alicerce, tudo que vem depois é castelo na areia.
Segundo, coloque um HPA simples por CPU, com minReplicas que sustente o vale de tráfego sem cold start e maxReplicas que cubra o pico com folga. Configure a stabilization window de scale-down para uns 300 segundos e observe o comportamento por uma ou duas semanas antes de refinar. Não comece com métrica custom; comece com CPU, entenda como seu serviço responde, e só então evolua.
Terceiro, se seu serviço tem uma métrica de carga mais fiel que a CPU — requisições por segundo, latência, profundidade de fila —, evolua o HPA para essa métrica custom via Prometheus Adapter. É quase sempre uma melhoria grande de qualidade de escalonamento, porque você passa a reagir à causa e não ao sintoma.
Quarto, se você opera em nuvem com capacidade elástica, ative o Cluster Autoscaler (ou Karpenter, na AWS) para que os pods do HPA tenham onde ser agendados. Sem essa camada, seu HPA promete uma elasticidade que a infraestrutura não entrega, e você descobre isso da pior forma, no meio de um pico, com pods presos em Pending.
E quinto, se sua carga é orientada a eventos, considere KEDA desde cedo — o modelo de escalar pela fila e ir até zero é tão superior para esse tipo de workload que raramente compensa insistir no HPA de CPU. Autoscaling bem feito não é sobre ligar todos os mecanismos de uma vez; é sobre entender qual problema você tem — muitos pods, pods pequenos demais, ou poucos nós — e aplicar a camada certa, medindo antes e depois. Comece pequeno, meça, e deixe cada escalador provar seu valor antes de adicionar o próximo.