3. Autoscaling
O lab 3 escalou o catalog à mão: três réplicas atrás do Nginx, qualquer que fosse a carga. O Horizontal Pod Autoscaler (HPA) fecha o laço — mede, compara com um alvo e muda o número de réplicas —: é o laço de controle de autoscaling da aula de computação, rodando a cada 15 segundos.
A regra
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: catalog
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: catalog
minReplicas: 2
maxReplicas: 8
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # % of the CPU *request* (250m): scale when pods average > 150m
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # react at once to load
selectPolicy: Max
policies:
- type: Percent
value: 100 # at most double...
periodSeconds: 15
- type: Pods
value: 2 # ...or +2 pods, whichever is more, every 15 s
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 120 # wait 2 min of lower load before removing pods
policies:
- type: Pods
value: 1
periodSeconds: 30
A fórmula central do HPA é simples:
Com 2 pods a 150% do seu request de CPU e um alvo de 60%: ⌈2 × 150 / 60⌉ = 5 pods. A utilização é uma porcentagem do request (250m), não do nó: um pod usando 150m está a 60%.
Campo do behavior | Por que existe |
|---|---|
scaleUp.stabilizationWindowSeconds: 0 | escalar para cima assim que a carga aparece — scale-up lento é requisição perdida |
scaleUp.policies (100% ou +2 pods a cada 15 s) | crescimento limitado: no máximo dobra por período, então uma leitura ruidosa não explode o deployment |
scaleDown.stabilizationWindowSeconds: 120 | usa a recomendação mais alta dos últimos 2 min — sem oscilação quando a carga oscila |
scaleDown.policies (1 pod a cada 30 s) | libera capacidade devagar; escalar para baixo rápido demais dispara o scale-up de novo |
Por que CPU, e quando ela é o sinal errado?
O catalog é limitado por CPU quando a taxa de acerto do cache é alta: mais requisições → mais CPU, quase linearmente, então a CPU é um bom indicador da carga. Ela é o sinal errado quando o gargalo está em outro lugar: um serviço esperando um banco de dados (CPU baixa, latência alta), um consumidor Kafka (escale pelo lag), um servidor WebSocket (escale por conexões). Para esses, o HPA aceita métricas customizadas e externas (requisições por segundo, tamanho de fila) por meio de um adaptador como o KEDA ou o Prometheus adapter.
Coloque carga
O script do k6 sobe a taxa de chegada em rampa (modelo aberto) pelo Ingress, segura e desce de volta. O observador registra o que o HPA vê e decide a cada 5 segundos.
import http from 'k6/http';
import { check } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://localhost'; // the Ingress
const PEAK = Number(__ENV.PEAK || 300);
export const options = {
scenarios: {
ramp: {
executor: 'ramping-arrival-rate', // open model: arrivals keep coming even if pods are slow
startRate: 20,
timeUnit: '1s',
preAllocatedVUs: 100,
maxVUs: 1000,
stages: [
{ target: PEAK, duration: '3m' }, // ramp up: watch the HPA react
{ target: PEAK, duration: '4m' }, // hold: does the latency come back down?
{ target: 20, duration: '1m' }, // ramp down
{ target: 20, duration: '3m' }, // quiet: watch the scale-down stabilization
],
},
},
summaryTrendStats: ['avg', 'med', 'p(95)', 'p(99)', 'max'],
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
},
};
export default function () {
const id = 1 + Math.floor(Math.random() * 200000);
const res = http.get(`${BASE_URL}/catalog/events/${id}`, { tags: { name: 'GET /catalog/events/{id}' } });
check(res, { 'status 200': (r) => r.status === 200 });
}
#!/bin/sh
# Logs time, desired/current replicas, CPU and ready pods every 5 s. Usage: sh scripts/watch-hpa.sh > results/lab10-hpa.csv
echo "time,cpu,desired,current,ready"
while true; do
HPA=$(kubectl -n boxoffice get hpa catalog -o jsonpath='{.status.currentMetrics[0].resource.current.averageUtilization},{.status.desiredReplicas},{.status.currentReplicas}')
READY=$(kubectl -n boxoffice get deploy catalog -o jsonpath='{.status.readyReplicas}')
echo "$(date +%T),$HPA,$READY"
sleep 5
done
Comece com pods aquecidos. Uma JVM que acabou de iniciar queima CPU compilando código, e o HPA escala com essa CPU também: numa execução de teste iniciada logo depois de um rollout, PEAK=250 levou o catalog a 8 réplicas, as liveness probes estouraram o timeout com as CPUs ocupadas, pods reiniciaram e o k6 viu 26% de erros. A mesma execução com pods aquecidos nunca saiu de 2 réplicas. Aqueça-os e espere o HPA voltar a 2 réplicas:
k6 run -q -e RATE=200 -e DURATION=1m load/catalog-otel.js > /dev/null # lab 8's script
kubectl -n boxoffice get hpa catalog -w # wait for REPLICAS 2 and a low cpu, then Ctrl+C
Três terminais:
mkdir -p results && sh scripts/watch-hpa.sh | tee results/lab10-hpa.csv # 1: the control loop
kubectl -n boxoffice get pods -l app=catalog -w # 2: pods appearing
k6 run -e PEAK=1000 load/catalog-hpa.js # 3: the load (≈ 11 min)
Ajuste o PEAK à sua máquina. O HPA reage à CPU, não à latência: o PEAK deve levar dois pods aquecidos claramente acima de 60% do request de CPU (150m cada, kubectl -n boxoffice top pods), mas ficar abaixo do que o seu laptop consegue gerar. Num laptop de teste (8 CPUs para o Docker), dois pods aquecidos ficaram em cerca de 40% com 250 req/s e passaram de 60% por volta de 600 req/s; daí o PEAK=1000.
Forma esperada (ilustrativa)
A CPU sobe durante a rampa; o HPA adiciona pods em degraus (2 → 4 → 6 …) tão rápido quanto a política de scaleUp permite; cada pod novo precisa de dezenas de segundos antes de ficar pronto (inicialização da JVM + probes), então o p95 sobe durante a rampa e só volta a cair quando os pods novos ficam prontos — num laptop, talvez só depois do fim do patamar. Depois que a carga cai, as réplicas ficam de pé durante a janela de estabilização de 2 minutos e então saem uma a uma.
O atraso entre "a carga chega" e "a capacidade está pronta" é o preço do autoscaling reativo. Para uma venda relâmpago com horário conhecido, você faria pré-escalonamento (aumentar o minReplicas antes das 10:00) em vez de torcer para o HPA ser rápido o bastante — o escalonamento reativo cuida do inesperado, o agendamento cuida do esperado.
Armadilhas
- Sem requests, sem HPA. A utilização é relativa a
resources.requests.cpu; sem requests o HPA reporta<unknown>e não faz nada. - O aquecimento da JVM parece carga. Uma JVM iniciando queima CPU (carregamento de classes, JIT): pods novos podem empurrar a média para cima e disparar mais scale-up. A startup probe mantém os pods não prontos fora do tráfego, mas a CPU deles ainda conta; a janela de estabilização e a política de scale-up limitam o estrago.
- O banco de dados não escala sozinho. Oito pods do
catalog× 10 conexões = 80 conexões a um único PostgreSQL. Escalar a camada stateless leva o gargalo para a stateful — confira omax_connections(ou use o PgBouncer do lab 6). - Capacidade dos nós. Se os pods pedidos não cabem nos nós, eles ficam em
Pending. Na nuvem um cluster autoscaler (ou o Karpenter) adiciona nós, o que leva minutos, não segundos.