3. Game Day
Os padrões do passo 2 foram testados um de cada vez, por quem os escreveu, sabendo o que ia quebrar. Incidentes reais não são assim. Um game day é um incidente ensaiado: um time injeta falhas num sistema em funcionamento, sem contar aos operadores quais, e verifica duas hipóteses ao mesmo tempo — que o sistema degrada do jeito que o projeto diz, e que as pessoas percebem, diagnosticam e mitigam a tempo. É o lado prático da engenharia do caos.
Papéis (grupos de 3–4)
| Papel | Faz | Não pode |
|---|---|---|
| Mestre do jogo | escolhe e injeta as falhas com o Toxiproxy, controla o relógio | contar aos outros o que foi injetado |
| Operador(es) | acompanha o Grafana (lab 8) e os logs, diagnostica, mitiga | olhar o terminal do mestre do jogo |
| Escriba | escreve a linha do tempo: cada observação e decisão, com o horário | consertar qualquer coisa |
Troque os papéis entre as rodadas, para que todos operem uma vez.
Antes de começar
- Estado estável. Rode o
load/pay-under-chaos.jscomDURATION=30me anote os valores normais do dashboard RED: taxa, % de erros e p95 doordere dopayment. Essa é a linha de base com que todo experimento é comparado. - Hipóteses. Para cada experimento abaixo, o time escreve antes de injetar o que espera ver (por exemplo, "com o
payment+2 s, o p95 dopaynoorderfica abaixo de 1,5 s e oGET /orders/{id}não é afetado"). - Raio de impacto e aborto. Só o BoxOffice do seu laptop. Condição de aborto: qualquer coisa fora do experimento quebra (por exemplo, erros no
catalog). O botão de abortar é um comando:curl -s -X POST localhost:8474/reset(remove todo toxic e reativa todo proxy). - Detecção. O alerta de burn rate do lab 8 observa só o
catalog. Adicione as mesmas duas regras para oorder(o arquivo do lab 8 comcatalogtrocado pororder, mostrado abaixo) e recrie ootel-lgtmcomdocker compose up -d --force-recreate --wait otel-lgtm: Alerting → Alert rules agora lista quatro regras na pasta BoxOffice, todas Normal, para que os operadores possam ser acionados. Recriar (e não só reiniciar) também esvazia as métricas: senão os erros dos experimentos do passo 2, ainda dentro das janelas de 1 hora e de 6 horas, fazem as regras doorderdispararem na hora. O dashboard e as regras são arquivos, então voltam.
infra/grafana/alerting/slo-order.yaml — o arquivo completo
# Lab 9: slo-catalog.yaml of lab 8 with "catalog" replaced by "order", so the game day can page.
# Multiwindow, multi-burn-rate alerts for the order availability SLO (Google SRE workbook, ch. 5).
# SLI: share of order requests (health checks excluded) NOT answered with 5xx. SLO: 99.5% / 28 days.
apiVersion: 1
groups:
- orgId: 1
name: order-slo
folder: BoxOffice
interval: 30s
rules:
- uid: order-availability-fast-burn
title: "order availability SLO: fast burn"
condition: C
data:
- refId: A # burn rate over the long window (1 h)
relativeTimeRange: { from: 3600, to: 0 }
datasourceUid: prometheus
model:
refId: A
instant: true
expr: |
(
sum(rate(http_server_request_duration_seconds_count{service_name="order", http_route!~"/actuator.*", http_response_status_code=~"5.."}[1h]))
/ sum(rate(http_server_request_duration_seconds_count{service_name="order", http_route!~"/actuator.*"}[1h]))
) / 0.005
- refId: B # burn rate over the short window (5 min): stops paging soon after recovery
relativeTimeRange: { from: 300, to: 0 }
datasourceUid: prometheus
model:
refId: B
instant: true
expr: |
(
sum(rate(http_server_request_duration_seconds_count{service_name="order", http_route!~"/actuator.*", http_response_status_code=~"5.."}[5m]))
/ sum(rate(http_server_request_duration_seconds_count{service_name="order", http_route!~"/actuator.*"}[5m]))
) / 0.005
- refId: C
datasourceUid: __expr__
model:
refId: C
type: math
expression: "$A > 14.4 && $B > 14.4"
for: 1m
noDataState: OK
execErrState: Error
labels:
severity: page
slo: order-availability
annotations:
summary: "order is burning its 28-day error budget 14.4x too fast"
description: 'Burn rate {{ printf "%.1f" $values.A.Value }} (1 h) and {{ printf "%.1f" $values.B.Value }} (5 min): at this pace about 2% of the 28-day budget is gone in one hour.'
- uid: order-availability-slow-burn
title: "order availability SLO: slow burn"
condition: C
data:
- refId: A # 6 h window
relativeTimeRange: { from: 21600, to: 0 }
datasourceUid: prometheus
model:
refId: A
instant: true
expr: |
(
sum(rate(http_server_request_duration_seconds_count{service_name="order", http_route!~"/actuator.*", http_response_status_code=~"5.."}[6h]))
/ sum(rate(http_server_request_duration_seconds_count{service_name="order", http_route!~"/actuator.*"}[6h]))
) / 0.005
- refId: B # 30 min window
relativeTimeRange: { from: 1800, to: 0 }
datasourceUid: prometheus
model:
refId: B
instant: true
expr: |
(
sum(rate(http_server_request_duration_seconds_count{service_name="order", http_route!~"/actuator.*", http_response_status_code=~"5.."}[30m]))
/ sum(rate(http_server_request_duration_seconds_count{service_name="order", http_route!~"/actuator.*"}[30m]))
) / 0.005
- refId: C
datasourceUid: __expr__
model:
refId: C
type: math
expression: "$A > 6 && $B > 6"
for: 5m
noDataState: OK
execErrState: Error
labels:
severity: ticket
slo: order-availability
annotations:
summary: "order is burning its error budget 6x too fast"
description: 'Burn rate {{ printf "%.1f" $values.A.Value }} (6 h) and {{ printf "%.1f" $values.B.Value }} (30 min): about 5% of the 28-day budget in six hours.'
Os experimentos
O mestre do jogo escolhe três destes, em qualquer ordem, de 5 a 8 minutos cada, com tempo de recuperação entre eles (pelo menos 5 minutos, até a regra de queima rápida voltar a Normal: a sua janela de 5 minutos precisa esquecer o experimento anterior, ou o próximo é "detectado" por um alerta que nunca parou de disparar):
| # | Injeção (mestre do jogo) | O que um bom sistema faz | O que um bom operador faz |
|---|---|---|---|
| 1 | payment +2 s de latência | timeouts + breaker: 503s rápidos no pay, browse não é afetado | encontra o payment no trace, vê o breaker abrir |
| 2 | buraco negro no psp (toxic timeout, 0) | o timeout de leitura do payment (2 s) faz as chamadas falharem; o breaker do order abre | segue o trace um salto adiante, até o provedor |
| 3 | 40% de reset_peer no payment | os retries escondem a maioria dos erros; a taxa de erros sobe só um pouco | percebe os retries nas métricas, não entra em pânico |
| 4 | proxy do payment desativado | breaker aberto, 503 rápido, os pedidos ficam em PENDING_PAYMENT | comunica "pagamentos fora do ar, pedidos a salvo" |
| 5 | db pausado (docker compose pause db) | tudo fica lento; o alerta de SLO dispara | identifica a dependência compartilhada, não o sintoma |
| 6 | bandwidth de 1 KB/s no payment | pay cerca de 0,5 s mais lento (algumas centenas de bytes a 1 KB/s), abaixo do timeout de leitura: sem erros, só latência | distingue "lento" de "fora do ar" |
Comandos de injeção:
#!/bin/sh
# Game master helper. Usage: sh scripts/game-day.sh <experiment 1..6 | reset>
API=localhost:8474
toxic() { # proxy, json
curl -s -X POST "$API/proxies/$1/toxics" -d "$2" > /dev/null
}
echo "$(date +%T) experiment $1"
case "$1" in
1) toxic payment '{"name":"gd","type":"latency","stream":"downstream","attributes":{"latency":2000,"jitter":300}}' ;;
2) toxic psp '{"name":"gd","type":"timeout","stream":"downstream","attributes":{"timeout":0}}' ;;
3) toxic payment '{"name":"gd","type":"reset_peer","toxicity":0.4,"attributes":{"timeout":0}}' ;;
4) curl -s -X POST "$API/proxies/payment" -d '{"enabled":false}' > /dev/null ;;
5) docker compose pause db ;;
6) toxic payment '{"name":"gd","type":"bandwidth","stream":"downstream","attributes":{"rate":1}}' ;;
reset) curl -s -X POST "$API/reset" > /dev/null; docker compose unpause db 2> /dev/null || true ;;
*) echo "unknown experiment"; exit 1 ;;
esac
Durante a rodada
Os operadores trabalham como num incidente real (veja a aula de resposta a incidentes): declaram o incidente quando um alerta dispara ou um sintoma aparece, designam um incident commander, diagnosticam pelo caminho dashboard → trace → logs, mitigam primeiro (por exemplo, parar de mandar tráfego para um caminho quebrado, aumentar um timeout, escalar) e investigam depois. O escriba registra os horários neste formato:
| Horário | Quem | Observação / decisão |
|---|---|---|
| 10:02:10 | MJ | (escondido) experimento 1 injetado |
| 10:04:30 | Op | p95 do order subindo no dashboard; nenhum alerta ainda |
| 10:05:05 | Op | o trace mostra o span do payment com ~2 s; breaker OPEN no actuator |
| … |
Da linha do tempo saem os dois números que toda revisão de incidente pede: tempo para detectar (injeção → primeira observação correta) e tempo para mitigar (injeção → usuários não mais afetados).
Depois: uma revisão sem culpados
Escreva uma revisão por rodada em results/lab9-gameday.md. Sem culpados (blameless) significa que a revisão explica como o sistema e o processo permitiram o resultado — nunca quem é o culpado.
# Game day — round <n>
**Experiment:** <what was injected, when> · **Duration:** <mm:ss>
**Hypothesis:** <what we expected>
**Result:** <confirmed | refuted> — <one sentence>
## Impact
- Users affected: <which requests, how many, for how long> (from the RED dashboard)
- SLO budget spent: <% of the 28-day budget>
## Timeline
| Time | Event |
|---|---|
## Detection and mitigation
- Time to detect: <min> — how: <alert | dashboard | trace>
- Time to mitigate: <min> — how: <...>
## What went well / what was hard
## Action items
| Action | Type (prevent / detect / mitigate) | Owner |
|---|---|---|
Armadilhas
- Sem hipótese, não há experimento. Quebrar coisas para "ver o que acontece" ensina pouco; uma hipótese refutada é o resultado valioso.
- Comece pequeno. Latência antes de quedas, uma dependência antes de duas, um laptop antes da produção.
- O botão de abortar precisa funcionar antes da primeira injeção. Teste o
resetprimeiro. - Resolva os itens de ação. Um game day cujas descobertas ficam num documento será repetido, com as mesmas descobertas, por um incidente real.