4. Meça
Um script conduz todos os experimentos do passo 2 e o game day. Ele roda dois cenários ao mesmo tempo, porque uma falha em cascata só fica visível quando você também mede o que não deveria ser afetado:
| Cenário | Taxa | Requisição | Toca o payment? |
|---|---|---|---|
pay | PAY_RATE (padrão 20/s) | faz um pedido, depois POST /orders/{id}/pay | sim |
browse | 20/s | GET /orders/{id} dos pedidos criados no setup() | não |
import http from 'k6/http';
import { check } from 'k6';
import exec from 'k6/execution';
import { Rate } from 'k6/metrics';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8084'; // order directly: isolate the service
const RUN = __ENV.RUN || `${Date.now()}`;
const DURATION = __ENV.DURATION || '2m';
const JSON_HEADERS = { 'Content-Type': 'application/json' };
const fastFail = new Rate('pay_fast_fail'); // share of payments answered 503 by the fallback
export const options = {
scenarios: {
pay: {
executor: 'constant-arrival-rate',
exec: 'pay',
rate: Number(__ENV.PAY_RATE || 20),
timeUnit: '1s',
duration: DURATION,
preAllocatedVUs: 50,
maxVUs: 600,
},
browse: {
executor: 'constant-arrival-rate',
exec: 'browse',
rate: 20,
timeUnit: '1s',
duration: DURATION,
preAllocatedVUs: 20,
maxVUs: 300,
},
},
summaryTrendStats: ['avg', 'med', 'p(95)', 'p(99)', 'max'],
thresholds: {
'http_req_duration{name:pay}': ['p(95)<1500'],
'http_req_duration{name:browse}': ['p(95)<200'], // the collateral damage
'http_req_failed{name:browse}': ['rate<0.01'],
},
};
export function setup() {
const ids = [];
for (let i = 0; i < 20; i++) {
const res = http.post(`${BASE_URL}/orders`,
JSON.stringify({ showId: 'show-9', holdId: `browse-${RUN}-${i}`, customerEmail: `b${i}@example.com`, amount: 100.0 }),
{ headers: JSON_HEADERS });
ids.push(res.json('id'));
}
return { ids };
}
export function pay() {
const n = exec.scenario.iterationInTest;
const placed = http.post(`${BASE_URL}/orders`,
JSON.stringify({ showId: 'show-9', holdId: `pay-${RUN}-${n}`, customerEmail: `fan${n}@example.com`, amount: 350.0 }),
{ headers: JSON_HEADERS, tags: { name: 'place' } });
if (placed.status !== 201) {
return;
}
const res = http.post(`${BASE_URL}/orders/${placed.json('id')}/pay`, null, { tags: { name: 'pay' } });
fastFail.add(res.status === 503);
check(res, { 'paid and confirmed': (r) => r.status === 200 && r.json('status') === 'CONFIRMED' });
}
export function browse(data) {
const id = data.ids[exec.scenario.iterationInTest % data.ids.length];
const res = http.get(`${BASE_URL}/orders/${id}`, { tags: { name: 'browse' } });
check(res, { 'order read': (r) => r.status === 200 });
}
Leia quatro linhas do resumo depois de cada execução: http_req_duration{name:pay} e {name:browse} (p95, p99), o check paid and confirmed (fração de pagamentos confirmados; o total de checks também conta o check order read do browse), pay_fast_fail e http_req_failed{name:browse}. Acompanhe o circuit breaker durante a execução:
Forma esperada (ilustrativa)
Sem proteção, com +3 s: o p95 do pay cresce para muitos segundos e o p95 do browse cresce junto — a cascata. Com um timeout: o pay falha em cerca de um segundo, o browse volta aos milissegundos. Com o circuit breaker durante uma queda: o pay responde 503 em milissegundos na maior parte do tempo (pay_fast_fail perto de 1) e as chamadas ao payment caem para um fio de sondas half-open. Com o bulkhead sob chamadas lentas, mas bem-sucedidas: a maioria dos pagamentos recebe 503 (só 10 cabem por vez), mas o browse continua rápido.
Uma execução de teste num laptop ocupado (a stack inteira do lab 9, 45 s por execução, payment + 3 s de latência):
| Configuração | pay p50 / p95 | browse p50 / p95 | pay_fast_fail |
|---|---|---|---|
sem proteção (PAYMENT_RESILIENCE=false, PAYMENT_READ_TIMEOUT=0) | 13,8 s / 17,1 s | 9,04 s / 13,17 s | 0% |
| todos os padrões (padrões de configuração) | 5 ms / 1,38 s | 6,5 ms / 805 ms | 100% |
Sem proteção, o GET /orders/{id} — que nunca chama o payment — esperou 9 s na mediana: a cascata. Com os padrões, o breaker abriu em segundos e todo pagamento falhou rápido com 503, então o browse voltou aos milissegundos (o p95 dele ainda mostra os primeiros segundos, antes de o breaker abrir). Os seus números absolutos vão ser outros; o contraste entre as duas colunas do browse é o que você precisa reproduzir.
Resultados
Copie para results/lab9.md. "Antes" é a configuração sem proteção ou desativada de cada linha; "Depois" é a com o padrão.
| Experimento (toxic) | Métrica | Antes | Depois |
|---|---|---|---|
| Linha de base (sem toxic) | p95 do pay (ms) / p95 do browse (ms) | — | |
| Timeout (latência 3 s) | p95 do pay (ms) | ||
| Timeout (latência 3 s) | p95 do browse (ms) / erros do browse (%) | ||
| Retry (reset_peer 30%) | pagamentos confirmados (%) | ||
| Retry (reset_peer 30%) | p95 do pay (ms) | ||
| Circuit breaker (proxy desativado) | p95 do pay (ms) / pay_fast_fail (%) | ||
| Circuit breaker (proxy desativado) | tempo da reativação até CLOSED (s) | — | |
| Bulkhead (latência 700 ms, 70 pay/s) | p95 do browse (ms) | ||
| Bulkhead (latência 700 ms, 70 pay/s) | pagamentos rejeitados (%) | ||
| Game day | tempo para detectar / tempo para mitigar (min) | — |
Abaixo da tabela, um parágrafo por padrão: o mecanismo (com a Lei de Little onde se aplica), o número que o comprova e o preço que você pagou por ele.