MÓDULO 07 · CONCEITO 08 DE 12

Auto-scaling

HPA (Kubernetes), AWS Auto Scaling Groups, KEDA. Métricas de scaling (CPU, RPS, queue depth). Reactive vs predictive. Por que scale-down é mais difícil que scale-up.

Tempo de leitura ~22 min Pré-requisito Conceito 02 (stateless services) Próximo Backpressure e elastic systems

Em 2009, a AWS lançou o Auto Scaling Group (ASG): um mecanismo para automaticamente subir e descer instâncias EC2 conforme demanda. A ideia era simples e revolucionária: em vez de provisionar capacidade para o pico (caro, ocioso na maior parte do tempo), o sistema ajusta capacidade dinamicamente. Pico de tráfego → mais instâncias; tráfego cai → menos instâncias. Pagar pelo uso real, não pelo planejado. A promessa de "elasticidade" da cloud virou concreta.

Quinze anos depois, auto-scaling é primitiva. Kubernetes tem HPA (Horizontal Pod Autoscaler) desde 2016; KEDA (Kubernetes Event-Driven Autoscaling) generaliza em 2019; AWS, GCP, Azure todos têm equivalentes nativos. A maioria das aplicações modernas em cloud opera com auto-scaling em produção. Mas configurá-lo bem — para que ajuste acompanhe carga sem oscilar, sem causar incidentes, e respondendo às métricas certas — continua sendo arte de operação.

Este conceito articula auto-scaling como disciplina. Métricas que disparam scaling (CPU, RPS, queue depth, custom), modos (reactive vs predictive), o problema clássico de scale-down (mais difícil que scale-up), oscilação (flapping), e os anti-padrões comuns. O foco é prático — configurações que sobrevivem a produção real, não apenas conceito teórico.

A premissa do auto-scaling é que o serviço é stateless (conceito 02 do módulo). Sem stateless, adicionar/remover instâncias gera bug ou perda de dado. Com stateless, é mecânica que pode ser configurada confiavelmente.

Métricas — qual usar para disparar scaling

Auto-scaling opera por métrica. Você define um alvo (target) para alguma métrica; o sistema ajusta o número de instâncias para manter a métrica próxima do alvo. A escolha da métrica define qualidade do scaling.

CPU utilization

Métrica mais clássica. Alvo típico: 60-70%. Quando média de CPU sobe, escala up; quando cai, escala down. Implementação trivial em qualquer cloud provider; dado disponível por default.

Ganhos: simples, sempre disponível, indicador correlacionado com carga. Limitações: para workloads I/O-bound, CPU baixo não significa folga (latência alta com CPU baixo). Workloads burst (picos curtos) podem terminar antes do scaling reagir.

Request rate (RPS)

Mais direto: escalar com base em quantas requests estão chegando. Alvo: capacidade conhecida por instância (ex.: 100 RPS por instância) × N instâncias = capacidade total. Quando RPS crescer, adicionar instâncias.

Ganhos: correlação direta com carga; antecipa CPU saturando. Limitações: capacidade por instância varia com tipo de request; precisa medir e calibrar; nem todos providers expõem trivialmente.

Queue depth

Para workers consumindo de fila (Kafka, SQS, RabbitMQ). Métrica: quantas mensagens não processadas estão na fila. Alvo: lag baixo (mensagens consumidas em janela de tempo aceitável).

Ganhos: ideal para workers — a métrica é exatamente o que precisa ser drenado. KEDA tornou trivial em Kubernetes. Limitações: não aplicável a APIs síncronas. Pode oscilar se mensagens chegam em bursts.

Latência (P99)

Escalar quando P99 ultrapassa alvo (ex.: P99 > 200ms por 5 minutos). Métrica de saúde direta — escalar quando experiência degrada.

Ganhos: correlação com SLO; só escala quando precisa. Limitações: latência pode ter múltiplas causas (banco lento, dependência externa) — escalar instâncias não resolve. Pode ser reativo demais.

Custom metrics

Métricas próprias do domínio: número de usuários online, tamanho do job atual, threads ativos. Kubernetes HPA via Prometheus Adapter; ASG via CloudWatch custom metrics.

Ganhos: precisamente alinhado com o que importa para o sistema. Limitações: complexidade de configurar pipeline de métrica; depende de observabilidade madura.

Times maduros tipicamente combinam: CPU como métrica primária + queue depth ou latência como secundária. KEDA tem ScaledObjects que aceitam múltiplos triggers.

Reactive vs predictive

Há duas filosofias de auto-scaling, com perfis complementares.

Reactive: ajusta com base em métrica corrente. CPU passou de 70%? Escala up. Voltou abaixo? Escala down. Simples, robusto, adapta a qualquer carga. Limitação: tem atraso. Métrica precisa cruzar threshold (alguns minutos), decisão é tomada, instância provisionada (1-3 minutos), entra no pool, começa a receber tráfego (alguns minutos). Total: 3-8 minutos para reagir. Em pico abrupto, sistema degrada antes de scaling reagir.

Predictive: antecipa carga com base em histórico ou eventos conhecidos. Padrão diário (carga sobe às 9h da manhã) detectado e escala up às 8:50. Black Friday agendada — escala preventivamente. AWS Predictive Scaling usa machine learning para prever; Kubernetes KEDA tem cron-based scaling.

Ganhos: zero latência reativa — capacidade já está pronta. Limitações: previsão pode estar errada (over-provisiona ou sub-provisiona); requer histórico ou conhecimento prévio.

O padrão maduro combina: predictive para padrões conhecidos (diário, semanal, eventos previstos) + reactive para o resto. Scale-up é predictivo quando há padrão; reativo quando há surpresa. Scale-down é tipicamente sempre reactive (com cooldown adequado).

Scale-up vs scale-down — assimetria

Scaling tem assimetria fundamental: scale-up é seguro, scale-down é arriscado. A consequência é que configurações maduras tratam os dois diferentemente.

Scale-up agressivo: quando carga cresce, melhor escalar rápido demais que tarde. Custo de instância extra por alguns minutos é barato; custo de degradação por sub-provisionamento é caro. Configurações típicas: ajuste por janela curta (1-3 min), múltiplas instâncias por step.

Scale-down conservador: quando carga cai, melhor descer devagar. Razões:

Configurações típicas: cooldown de 5-15 min antes de tentar scale-down após scale-up; ajuste em janela longa (5-15 min); poucas instâncias por step. Algumas equipes preferem scale-down manual em horários conhecidos (final do dia) em vez de automático.

Flapping — o problema da oscilação

Auto-scaling mal configurado pode oscilar: escalar up, escalar down, repetir. Flapping é o nome desse comportamento, e gera vários problemas:

Custo aumentado. Provisionar instância tem custo de startup (em cloud, ~1 minuto). Se sistema escala +10 e -10 em ciclos de 5 minutos, esse custo é desperdiçado.

Cache cold em ciclo. Cada nova instância começa com cache vazio. Se está sempre adicionando/removendo, cache nunca esquenta de verdade.

Conexões cortadas. Em scale-down, conexões ativas precisam terminar (graceful) ou são cortadas (não-graceful). Flapping força isso repetidamente.

A defesa contra flapping é configurar thresholds com hysteresis: scale-up em uma threshold (CPU > 70%), scale-down em threshold mais baixa (CPU < 40%). Janela entre 40-70% é zona de "não mexer". Combine com cooldown adequado.

Kubernetes HPA tem stabilizationWindowSeconds explicitamente para isso. AWS ASG tem cooldown period. Configurar bem evita oscilação.

Cold start — o custo de instância nova

Quando auto-scaling adiciona instância, ela precisa: ser provisionada (cloud), inicializar runtime (JVM/CLR/Python), carregar dependências, conectar a banco/cache, possivelmente warm-up de cache. Tempo total: cold start.

Tipos de cold start:

Provisionamento de máquina (1-3 min). Em ASG/Kubernetes node pools, criar nova VM. Pode ser eliminado com pools pré-aquecidos ou serverless.

Container start (10-60 s). Pull de imagem, montagem, init. Pode ser reduzido com imagens otimizadas (distroless, multi-stage builds).

Application startup (5-30 s). JVM JIT warmup, .NET tiered compilation, Python imports. Pode ser melhorado com AOT compilation, GraalVM native-image, .NET Native AOT.

Cache warmup (variável). Se a app tem cache local, atingir hit rate alto leva minutos. Em pior caso, instância nova é mais lenta por horas.

Soma típica: 1-5 minutos de cold start. Para auto-scaling reagir a pico abrupto, esse atraso é o que limita responsividade. Sistemas com SLO agressivo precisam ou (a) capacidade reserva sempre (mais cara), ou (b) predictive scaling, ou (c) cold start otimizado.

HPA do Kubernetes — caso canônico

O HPA é o padrão de auto-scaling em Kubernetes (cluster-wide, múltiplos providers). Vale ver configuração real.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: catalog-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: catalog-api
  minReplicas: 3            # nunca menor que 3 (HA)
  maxReplicas: 50           # custos: cap superior

  metrics:
  # CPU como métrica primária
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

  # custom metric (RPS) via Prometheus adapter
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "100"

  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30   # reage em 30s
      policies:
      - type: Percent
        value: 100                    # pode dobrar
        periodSeconds: 60
      - type: Pods
        value: 5                      # ou +5 instâncias
        periodSeconds: 60
      selectPolicy: Max               # qual mais agressiva

    scaleDown:
      stabilizationWindowSeconds: 300  # espera 5 min antes de scale-down
      policies:
      - type: Percent
        value: 10                     # remove no máximo 10% por vez
        periodSeconds: 60

Note três aspectos importantes do exemplo. Primeiro: múltiplas métricas (HPA escolhe a que pede mais instâncias). Segundo: comportamento assimétrico (scale-up agressivo, scale-down conservador). Terceiro: limites mín/máx — sem máximo, custo pode explodir; sem mínimo, ficamos vulneráveis a queda.

KEDA — auto-scaling event-driven

KEDA (Kubernetes Event-Driven Autoscaling, lançado 2019, CNCF graduated 2023) estende HPA para escalar baseado em qualquer evento externo. Suporta 60+ "scalers": Kafka, RabbitMQ, SQS, Redis, Prometheus, Postgres, custom.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: notification-worker
spec:
  scaleTargetRef:
    name: notification-worker
  minReplicaCount: 0                # KEDA pode escalar para 0!
  maxReplicaCount: 100
  pollingInterval: 30               # checa a cada 30s
  cooldownPeriod: 300               # espera 5 min antes de zerar

  triggers:
  - type: kafka
    metadata:
      bootstrapServers: kafka:9092
      topic: notifications
      consumerGroup: notification-workers
      lagThreshold: "1000"          # escala se lag > 1000
      offsetResetPolicy: latest

  - type: cpu
    metadata:
      type: Utilization
      value: "70"

KEDA aceita múltiplos triggers; usa o mais restritivo. minReplicaCount: 0 permite zerar workers quando não há tráfego — economia significativa para workloads bursty (job batch noturno, eventos esporádicos). Quando primeira mensagem chega, KEDA acorda 1 worker; daí escala conforme lag cresce.

Auto-scaling em três stacks

Cada ecossistema tem padrão idiomático para auto-scaling.

C# — ASP.NET Core em K8s com HPA
// Program.cs — preparar app para auto-scaling
var builder = WebApplication.CreateBuilder(args);

// readyz/livez probes
builder.Services.AddHealthChecks()
    .AddNpgSql(connStr, name: "postgres", tags: new[] { "ready" })
    .AddRedis(redisConn, name: "redis", tags: new[] { "ready" });

var app = builder.Build();

app.MapHealthChecks("/livez");
app.MapHealthChecks("/readyz", new HealthCheckOptions {
    Predicate = h => h.Tags.Contains("ready"),
});

// graceful shutdown — 30 s para drenar
app.Lifetime.ApplicationStopping.Register(() =>
{
    // ready falha primeiro, balancer remove
    // espera in-flight requests terminarem
    Thread.Sleep(2000);
});

// custom metric exportada (RPS via Prometheus)
builder.Services.AddSingleton<Counter>(_ =>
    Metrics.CreateCounter("http_requests_total", "Total HTTP requests"));

# K8s deployment com resources adequadas para HPA decidir
# resources:
#   requests: { cpu: 250m, memory: 256Mi }   # base para HPA target
#   limits:   { cpu: 1000m, memory: 1Gi }
# terminationGracePeriodSeconds: 30           # para drenar
# preStop hook: sleep 5 (espera balancer remover)

ASP.NET Core integra com K8s via probes, e HPA escala baseado em CPU + RPS via Prometheus adapter. Pontos críticos: probes corretos, resources requests calibradas, graceful shutdown configurado.

Python — workers escalonados via KEDA por queue
# Celery worker que processa fila Redis
from celery import Celery

app = Celery("worker", broker="redis://redis:6379/0")

@app.task
def processar_notificacao(payload):
    # processa notificação
    return enviar(payload)

# K8s deployment para o worker
# replicas: 1 (KEDA controla)

# ScaledObject KEDA para escalar conforme fila
# scaleTargetRef: deployment do worker
# triggers:
# - type: redis
#   metadata:
#     address: redis:6379
#     listName: celery
#     listLength: "100"   # escala se > 100 mensagens
# minReplicaCount: 1        # mínimo 1 (cold start fica longo se 0)
# maxReplicaCount: 50

# graceful shutdown — Celery captura SIGTERM e termina jobs em curso
# soft_time_limit / time_limit limitam jobs longos

Celery + KEDA é stack canônica para workers Python escalonáveis. KEDA monitora fila Redis, escala workers conforme lag. minReplicaCount=1 evita cold start; pode ser 0 para workloads esporádicos.

Go — workers Kafka com KEDA scaling
// Go worker que consome Kafka
package main

import (
    "context"
    "github.com/segmentio/kafka-go"
)

func main() {
    reader := kafka.NewReader(kafka.ReaderConfig{
        Brokers: []string{"kafka:9092"},
        Topic:   "notifications",
        GroupID: "notification-workers",
    })
    defer reader.Close()

    ctx, cancel := signal.NotifyContext(context.Background(),
        os.Interrupt, syscall.SIGTERM)
    defer cancel()

    for {
        msg, err := reader.FetchMessage(ctx)
        if err != nil { return }
        if err := processar(ctx, msg); err != nil { continue }
        reader.CommitMessages(ctx, msg)
    }
}

# ScaledObject KEDA para Kafka lag
# triggers:
# - type: kafka
#   metadata:
#     bootstrapServers: kafka:9092
#     topic: notifications
#     consumerGroup: notification-workers
#     lagThreshold: "1000"
# minReplicaCount: 1
# maxReplicaCount: 50
# advanced:
#   horizontalPodAutoscalerConfig:
#     behavior:
#       scaleDown:
#         stabilizationWindowSeconds: 300

# Go é especialmente bom para workers escalonáveis:
# - cold start rápido (~200ms para AOT)
# - baixo footprint de memória
# - paralelismo nativo (goroutines)

Go shines em workers escalonáveis: cold start rápido, baixa memória, paralelismo nativo. Combinado com KEDA, escala bem para milhares de workers em pico e zera para 1 em ociosidade.

Anti-padrões frequentes

Auto-scaling sem stateless adequado. Já visto. Conceito 02. Sistema com cache local, sticky sessions, ou conexões persistentes mal coordenadas vai sofrer com auto-scaling. Defesa: stateless rigoroso.

Métrica errada. CPU como métrica primária para workload I/O-bound; ou RPS sem considerar request heterogeneity. Defesa: medir antes; entender qual métrica melhor reflete capacidade saturada.

Thresholds simétricos. Scale-up e scale-down em mesma threshold (ex.: 70%). Causa flapping. Defesa: hysteresis (scale-up 70%, scale-down 40%).

Cooldown insuficiente. 1 minuto entre eventos de scaling. Não dá tempo para capacidade nova entregar valor; oscila. Defesa: cooldown de 3-5 min para scale-up, 10-15 min para scale-down.

maxReplicas sem teto significativo. Em incidente (DDoS, bug que faz queries lentas), auto-scaling escala para 1000 instâncias antes de time perceber. Custo explode; banco satura. Defesa: maxReplicas razoável (50-100), com alerta quando atinge.

minReplicas baixo demais. minReplicas: 1 em produção crítica. Quando essa única instância falha, downtime total. Defesa: minReplicas: 3 ou mais para HA — três instâncias permitem perder uma sem degradar.

Não testar comportamento sob auto-scaling. Configura, deixa rodando, espera funcionar. Em produção, descobre que cold start é mais lento que esperado, ou que cache local faz instâncias novas lentas. Defesa: testes de spike em staging simulando o pior cenário.

armadilha em produção

Cascade failure por auto-scaling. Banco fica ligeiramente lento; latência das instâncias da aplicação sobe; HPA escala mais instâncias para compensar; novas instâncias abrem mais conexões ao banco; banco fica mais lento; HPA escala ainda mais; banco fica praticamente parado; sistema entra em colapso. Defesa: connection pool bem dimensionado (não cresce com instâncias); circuit breaker em torno do banco; alerta em pool utilization alta; rate limit antes de auto-scaling agressivo. Auto-scaling pressupõe que o gargalo é a aplicação. Quando o gargalo é dependência downstream, auto-scaling agrava o problema.

heurística do sênior

Antes de configurar auto-scaling, articule cinco coisas. "Qual a métrica que melhor reflete saturação?" Se a resposta é "CPU porque é default", está cedo demais. "Qual o cold start típico?" Se é >5 min, predictive scaling pode ser obrigatório. "Qual o min/max razoável?" Min para HA; max para evitar custo descontrolado. "Qual o behavior assimétrico de scale up/down?" Articulado antes de produção. "Como o sistema se comporta se o gargalo é downstream?" Auto-scaling agrava — circuit breaker e rate limit são defesas necessárias. Quem articula esses 5 pontos antes de configurar evita os incidentes mais comuns.

Por que importa para a sua carreira

Auto-scaling é configuração cotidiana em sistemas cloud-native. Em entrevistas, "como você escalonaria essa aplicação automaticamente?" é convite para articular métrica, modo, behavior. A resposta forte cita assimetria scale-up/down, hysteresis, cold start, e considera o cenário de cascade failure. Em revisão de configuração K8s, perceber HPA mal-configurado (thresholds simétricos, maxReplicas alto demais) é serviço ao time. Em pos-mortem de "auto-scaling escalou para 200 instâncias e quebrou banco", articular que o problema é missing rate limit + circuit breaker é trabalho de senior. Em discussão de capacity planning, articular auto-scaling como ferramenta complementar (não substituta) de planning é vocabulário maduro.

Como praticar

  1. HPA local em minikube/kind. Configure aplicação simples em K8s local com HPA baseado em CPU. Use ferramenta de carga (k6) para gerar tráfego variável; observe HPA escalando. Modifique thresholds, behavior; observe efeitos. Esse exercício, feito uma vez, fixa muitos conceitos.
  2. KEDA com queue. Suba RabbitMQ ou Kafka local; aplicação que consome; KEDA configurado para escalar baseado em lag. Envie burst de mensagens; observe KEDA escalando workers. Compare com HPA por CPU. Esse exercício é valioso especialmente para sistemas com workers.
  3. Spike test com auto-scaling. Em ambiente staging com auto-scaling configurado, execute teste com k6 que aumenta carga 10× em 30 segundos. Meça: tempo para HPA detectar, tempo para nova instância entregar tráfego, tempo até latência estabilizar. Documente a "janela de degradação" — o tempo entre pico e capacidade restaurada. Esse é métrica que sêniores operam em sistemas críticos.

Referências para aprofundar

  1. docs Kubernetes HPA. kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale — Documentação canônica. Cobre algoritmo, behavior, métricas custom.
  2. docs KEDA Documentation. keda.sh/docs — Documentação completa. Lista de scalers (60+) com exemplos.
  3. docs AWS Auto Scaling. docs.aws.amazon.com/autoscaling — Documentação AWS. Inclui Predictive Scaling moderno.
  4. docs GCP Managed Instance Groups Autoscaling. cloud.google.com/compute/docs/autoscaler — Equivalente GCP. Conceitos similares com diferenças sutis.
  5. livro Kubernetes Patterns — Bilgin Ibryam, Roland Huß (O'Reilly, 2019). Cap. sobre Elastic Scale, Predictable Demands, Health Probe cobrem auto-scaling em profundidade prática.
  6. livro Site Reliability Engineering — Beyer et al., Google (O'Reilly, 2016). Cap. 19 (Capacity Planning) e cap. 22 (Cascading Failures) cobrem como Google pensa scaling em produção.
  7. livro The Art of Capacity Planning (2ª ed.) — Allspaw & Kejariwal (O'Reilly, 2017). Capítulos sobre auto-scaling articulam como Twitter, Etsy, e outros fazem capacity planning combinado com auto-scaling.
  8. artigo Predictive Auto Scaling at AWS — Various AWS Architecture Blog posts (2019+). aws.amazon.com/blogs/architecture — Casos reais de Predictive Scaling em produção. Útil para ver quando vale.
  9. artigo Lessons from Six Years of Production Use of Kubernetes — Spotify Engineering (várias publicações). engineering.atspotify.com — Spotify articula em vários posts as lições de operar K8s em escala, incluindo auto-scaling.
  10. artigo How Discord Migrated to Kubernetes — Discord Engineering (blog). discord.com/blog — Conta de migração para K8s incluindo decisões de auto-scaling em sistema crítico.
  11. artigo Adopting KEDA: How Microsoft uses event-driven autoscaling — Microsoft Engineering (várias publicações). techcommunity.microsoft.com — KEDA foi co-iniciado por Microsoft. Posts de adoção em produção.
  12. vídeo Cluster Autoscaling and Pod Autoscaling — KubeCon talks (vários anos). YouTube. Talks da CNCF cobrem auto-scaling em K8s com profundidade. Vale procurar pelas mais recentes.