3. Portão de Desempenho
Testes pegam respostas erradas; não pegam uma mudança que deixa a resposta certa dez vezes mais lenta. Um orçamento de desempenho transforma a latência que você prometeu (o SLO do lab 8, o planejamento de capacidade) numa verificação do build: o CI sobe o serviço, roda um teste k6 curto com thresholds e reprova o pull request quando um threshold é ultrapassado.
Um ambiente fixo
Comparações só fazem sentido se toda execução usa o mesmo ambiente. O CI sobe uma stack pequena e dedicada — PostgreSQL, Redis e catalog com o mesmo limite de CPU do docker-compose.yaml — a partir do seu próprio arquivo Compose:
# The performance-budget environment of CI (lab 11): the same small stack on every run, so that
# runs are comparable. Paths are relative to this file (.github/ci/).
name: perf
services:
db:
image: postgres:17
environment:
POSTGRES_DB: boxoffice
POSTGRES_USER: boxoffice
POSTGRES_PASSWORD: boxoffice
healthcheck:
test: ["CMD-SHELL", "pg_isready -U boxoffice -d boxoffice"]
interval: 5s
timeout: 3s
retries: 20
redis:
image: redis:8-alpine
command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru", "--save", ""]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20
catalog:
build: ../../services/catalog
environment:
DB_HOST: db
DB_PORT: 5432
DB_NAME: boxoffice
DB_USER: boxoffice
DB_PASSWORD: boxoffice
REDIS_HOST: redis
REDIS_PORT: 6379
ports:
- "8082:8082"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
deploy:
resources:
limits:
cpus: "1.0" # the same limit as in docker-compose.yaml: budgets are per replica
memory: 768M
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8082/actuator/health/readiness || exit 1"]
interval: 5s
timeout: 3s
start_period: 60s
retries: 30
docker compose … up --wait só retorna quando todos os healthchecks passam, então o k6 nunca mede uma JVM que ainda está subindo.
O orçamento
// Lab 11: performance budget of catalog, run by CI on every push and pull request.
// A crossed threshold makes k6 exit with code 99, which fails the "performance budget" job.
// k6 run load/budget.js (BASE_URL defaults to http://localhost:8082)
import http from 'k6/http';
import { check } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8082';
export const options = {
scenarios: {
warmup: { // JIT, connection pools and cache: not measured
executor: 'constant-arrival-rate',
rate: 20,
timeUnit: '1s',
duration: '20s',
preAllocatedVUs: 20,
},
budget: { // the measured part: an open-model load the service must sustain
executor: 'constant-arrival-rate',
startTime: '20s',
rate: Number(__ENV.RATE || 50),
timeUnit: '1s',
duration: '40s',
preAllocatedVUs: 50,
maxVUs: 200,
},
},
summaryTrendStats: ['avg', 'med', 'p(95)', 'p(99)', 'max'],
thresholds: {
'http_req_duration{scenario:budget}': ['p(95)<300', 'p(99)<800'],
'http_req_failed{scenario:budget}': ['rate<0.01'],
'checks{scenario:budget}': ['rate>0.99'],
},
};
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 });
}
| Escolha | Por quê |
|---|---|
um cenário warmup de 20 s, não medido | a compilação JIT, os pools de conexão e o cache esquentam; os thresholds só valem para scenario:budget |
modelo aberto (constant-arrival-rate) | a taxa de chegada não diminui quando o serviço fica lento, como com usuários reais |
| p95 < 300 ms e p99 < 800 ms | a requisição típica e a cauda; folgados para um runner, para que só regressões reais reprovem |
| falhas < 1%, checks > 99% | um 500 rápido não é um sucesso |
O job performance budget do ci.yaml (passo 1) instala o k6 com grafana/setup-k6-action, roda o script com grafana/run-k6-action e imprime os logs do serviço se falhar. Um threshold ultrapassado faz o k6 sair com código 99, e o job falha.
Rode localmente
docker compose -f .github/ci/compose.perf.yaml up -d --build --wait --wait-timeout 300
k6 run load/budget.js
docker compose -f .github/ci/compose.perf.yaml down -v
Você deve ver todos os thresholds com ✓. Uma execução de teste num notebook:
█ THRESHOLDS
checks{scenario:budget}
✓ 'rate>0.99' rate=100.00%
http_req_duration{scenario:budget}
✓ 'p(95)<300' p(95)=6.7ms
✓ 'p(99)<800' p(99)=9.13ms
http_req_failed{scenario:budget}
✓ 'rate<0.01' rate=0.00%
Quebre-o de propósito
Prove que o portão funciona. Numa nova branch, adicione um atraso artificial ao endpoint de evento de services/catalog/src/main/java/boxoffice/catalog/EventController.java — a primeira linha do método get:
Rode os comandos locais de novo: p(95)<300 agora mostra ✗ e o k6 termina com level=error msg="thresholds on metrics 'http_req_duration{scenario:budget}' have been crossed". Faça push da branch e abra um pull request: o check performance budget fica vermelho e o botão de merge o avisa. Feche o pull request sem fazer merge (e apague a branch) — era só um experimento.
Um portão ruidoso é pior que nenhum portão
Runners compartilhados de CI variam de uma execução para outra. Defina thresholds com uma margem de segurança sobre os seus números normais (aqui, o p95 é ~7 ms contra um orçamento de 300 ms), mantenha o teste curto mas longo o bastante para ter milhares de amostras, e olhe a tendência dos números ao longo de muitas execuções antes de apertá-los. Um portão que falha aleatoriamente ensina as pessoas a rodar de novo até ficar verde.