Pular para conteúdo

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:

.github/ci/compose.perf.yaml
# 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

load/budget.js
// 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:

try { Thread.sleep(400); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }

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.

Próximo: GitOps com Argo CD