Pular para o conteúdo
17 min de leitura

Autoscaling no Kubernetes na prática: HPA, VPA e Cluster Autoscaler sem mistério

Compartilhar

Por Equipe Nebular ·

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 requests sem risco — você lê a recomendação com kubectl describe vpa e aplica manualmente quando quiser.
  • Initial: ele define os requests apenas 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 requests de 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. Sem requests, 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 stabilizationWindowSeconds de 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 minReplicas mais 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.
  • maxReplicas sem 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.
  • PodDisruptionBudget ausente 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.

Compartilhar

Compartilhar

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly