Dherick"Se o seu sistema só funciona quando nada dá errado, ele não está pronto para produção. Está só com...
"Se o seu sistema só funciona quando nada dá errado, ele não está pronto para produção. Está só com sorte."
Às 2h17 da manhã, o alerta de latência alta dispara. O serviço de pagamentos de um parceiro começou a responder em 8 segundos. Em poucos minutos, as threads do nosso backend estão todas presas esperando uma resposta que não vem, o checkout para de funcionar e o time de plantão acorda.
Eu já vivi essa história, e o pior não foi a falha em si. O pior foi descobrir durante o incidente que nossa arquitetura não estava preparada para ela.
Foi por isso que passamos a quebrar coisas de propósito, de forma controlada, e a organizar essa prática em um baralho de experimentos: o ChaosEngineeringMasterDeck. Neste artigo eu explico como ele funciona e mostro um experimento completo, da hipótese ao resultado.
Nota: o cenário e os números deste artigo são didáticos, construídos para ilustrar o método. Os conceitos, as ferramentas e os trechos de código, porém, são reais e aplicáveis.
Chaos Engineering é a disciplina de realizar experimentos controlados em um sistema para ganhar confiança de que ele resiste a condições turbulentas em produção. A prática foi popularizada pela Netflix com o Chaos Monkey, que desligava instâncias aleatoriamente para forçar os engenheiros a construir serviços tolerantes a falhas.
Em sistemas distribuídos, a pergunta mudou. Já não é "será que algo vai falhar?", e sim "quando algo vai falhar, e o que acontece com o usuário?". Com dezenas de microsserviços, redes, filas, bancos e dependências de terceiros, falhas parciais são o estado normal da operação.
| Mentalidade tradicional | Mentalidade de resiliência |
|---|---|
| Evitar toda falha | Assumir que falhas vão acontecer |
| Foco em MTBF (tempo médio entre falhas) | Foco em MTTR (tempo médio de recuperação) |
| Testar só o "caminho feliz" | Testar também os caminhos de falha |
| Descobrir fragilidades em incidentes reais | Descobrir fragilidades em experimentos planejados |
| Resiliência como "sorte" ou herói de plantão | Resiliência como propriedade verificada |
Testes unitários e de integração verificam se o código faz o que esperamos. O caos descobre o que não esperávamos: timeouts mal configurados, retries que amplificam o problema, filas que enchem, fallbacks que nunca foram exercitados.
Antes de seguir, vale fixar o vocabulário que vai aparecer no restante do artigo:
O ideal é a produção, porque só lá existem o tráfego, os dados e as interações reais. Mas ninguém começa por lá. O caminho seguro é:
Local/Dev → Staging → Produção (canário, baixo tráfego) → Produção (ampliado)
Cada degrau só é subido quando o experimento anterior já passou sem surpresas.
O ChaosEngineeringMasterDeck é o nosso catálogo gamificado de experimentos de caos. Em vez de uma planilha confusa de "testes de resiliência", cada experimento vira uma carta padronizada. O baralho inteiro vive em um repositório Git, com uma carta por arquivo, e é usado em dois momentos:
A gamificação tem um objetivo prático: tirar o medo. Quando quebrar o sistema vira uma dinâmica de equipe com regras claras, e não um ato isolado e arriscado, mais gente participa e mais fragilidades aparecem.
Toda carta segue a mesma estrutura, que reflete o método científico do Chaos Engineering:
| Campo | Pergunta que responde |
|---|---|
| Nome / Naipe | Que tipo de falha é esta? |
| Nível de risco | Quão perigosa é (🟢 baixo, 🟡 médio, 🔴 alto)? |
| Estado estável | O que é "normal" para este sistema? |
| Hipótese | O que acreditamos que vai acontecer? |
| Variável injetada | O que vamos quebrar, e como? |
| Blast radius | Até onde o impacto pode chegar? |
| Métricas de controle | O que vamos observar? |
| Condição de aborto | Quando interrompemos? |
| Padrões esperados | Quais mecanismos de resiliência devem agir? |
Organizamos as cartas por tipo de falha:
Guardar a carta como código permite versionar, revisar em pull request e até automatizar a execução:
# cards/dependencias/parceiro-lento.yaml
id: DEP-007
nome: "Parceiro Lento"
naipe: dependencias
risco: medio
estado_estavel:
descricao: "Checkout saudável sob carga normal"
metricas:
- "taxa de sucesso do checkout >= 99,5%"
- "latência p95 do POST /checkout <= 800ms"
- "taxa de erros 5xx <= 0,5%"
hipotese: >
Se o serviço de pagamentos do parceiro passar a responder com 3s de
latência adicional, o Circuit Breaker abrirá em até 30s e o checkout
continuará aceitando pedidos em modo degradado (pagamento pendente),
mantendo a taxa de sucesso acima de 95%.
variavel:
tipo: latencia
alvo: "payment-gateway (via proxy Toxiproxy)"
valor: "+3000ms, jitter 500ms"
duracao: "10 minutos"
blast_radius:
ambiente: staging
escopo: "somente chamadas do order-service ao payment-gateway"
trafego_afetado: "100% do tráfego sintético; 0% de usuários reais"
condicao_de_aborto:
- "taxa de erros 5xx > 10% por 2 minutos"
- "pool de threads do order-service > 90% por 1 minuto"
padroes_esperados:
- Timeout agressivo
- Circuit Breaker
- Fallback (pedido com pagamento pendente)
- Bulkhead (isolamento de threads)
Nosso sistema de e-commerce roda na AWS. Resumindo o que importa para este experimento:
┌──────────────────────┐
Usuário ───────► │ Application Load │
│ Balancer (ALB) │
└──────────┬───────────┘
│
┌────────────────┴────────────────┐
│ Auto Scaling Group (2 AZs) │
│ ┌───────────┐ ┌───────────┐ │
│ │ order-svc │ │ order-svc │ │ Spring Boot
│ │ (AZ-a) │ │ (AZ-b) │ │
│ └─────┬─────┘ └─────┬─────┘ │
└────────┼───────────────┼────────┘
│ │
┌───────────┴──┐ ┌──────┴──────────┐
│ RDS Postgres │ │ payment-gateway │
│ (Multi-AZ) │ │ (API externa) │
└──────────────┘ └─────────────────┘
No Game Day, o time comprou DEP-007 — Parceiro Lento (🟡 risco médio).
Estado estável: 99,5% de sucesso no checkout e p95 abaixo de 800 ms.
Hipótese: "Mesmo com +3s de latência no gateway, o Circuit Breaker abre em até 30s e o checkout segue funcionando em modo degradado, com taxa de sucesso acima de 95%."
Esta é a etapa que separa um experimento sério de uma irresponsabilidade. Nosso raio de explosão foi deliberadamente pequeno:
order-service → payment-gateway. O banco, o ALB e as demais rotas não foram tocados.Usamos o Toxiproxy como proxy entre o order-service e o gateway, o que permite injetar latência sem alterar o código da aplicação:
# Cria o proxy para o gateway (o order-service passa a apontar para ele)
toxiproxy-cli create payment_gateway \
--listen 0.0.0.0:8474 \
--upstream payment-gateway.staging.internal:443
# Injeta 3000ms de latência com 500ms de jitter (a "carta" em ação)
toxiproxy-cli toxic add payment_gateway \
--type latency \
--attribute latency=3000 \
--attribute jitter=500
# Rollback imediato (botão vermelho)
toxiproxy-cli toxic remove payment_gateway --toxicName latency_downstream
A hipótese dependia de quatro defesas configuradas com Resilience4j no Spring Boot:
# application.yml
resilience4j:
timelimiter:
instances:
paymentGateway:
timeout-duration: 2s # não esperamos mais que 2s
circuitbreaker:
instances:
paymentGateway:
sliding-window-type: COUNT_BASED
sliding-window-size: 20
failure-rate-threshold: 50 # abre com 50% de falhas
slow-call-duration-threshold: 2s
slow-call-rate-threshold: 50 # chamadas lentas também contam!
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 5
bulkhead:
instances:
paymentGateway:
max-concurrent-calls: 20 # isola o pool de threads
max-wait-duration: 100ms
@Service
public class PaymentService {
private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
private final PaymentGatewayClient gateway;
public PaymentService(PaymentGatewayClient gateway) {
this.gateway = gateway;
}
@CircuitBreaker(name = "paymentGateway", fallbackMethod = "pagamentoPendente")
@Bulkhead(name = "paymentGateway")
@TimeLimiter(name = "paymentGateway")
public CompletableFuture<PaymentResult> cobrar(Order order) {
return CompletableFuture.supplyAsync(() -> gateway.charge(order));
}
// Fallback: não derruba o pedido, apenas agenda a cobrança para depois
private CompletableFuture<PaymentResult> pagamentoPendente(Order order, Throwable ex) {
log.warn("Gateway indisponível/lento. Pedido {} em modo degradado: {}",
order.getId(), ex.getClass().getSimpleName());
return CompletableFuture.completedFuture(
PaymentResult.pending(order.getId()) // cobrança reprocessada por fila
);
}
}
A ideia do fallback é de negócio, não só técnica: é melhor aceitar o pedido com pagamento pendente e cobrar depois do que perder a venda.
Sem observabilidade, o experimento vira um "derrubei e não sei o que aconteceu". Nosso painel (Prometheus + Grafana, com traces no OpenTelemetry) acompanhava, lado a lado:
closed, open, half_open) via métricas do Resilience4j no Actuator.| Tempo | O que aconteceu |
|---|---|
| T+0s | Latência de +3s injetada no gateway |
| T+0–12s | Chamadas começam a estourar o TimeLimiter (2s); p95 do checkout sobe |
| T+12s | Taxa de chamadas lentas passa de 50%: Circuit Breaker abre |
| T+12s em diante | Fallback assume; pedidos entram como "pagamento pendente" e a latência volta ao normal |
| T+42s | Breaker vai para half_open e testa 5 chamadas; todas ainda lentas, então volta a open
|
| T+10min | Toxic removida. No próximo ciclo half_open, as chamadas passam e o breaker fecha
|
| T+11min | Sistema de volta ao estado estável; fila reprocessa os pagamentos pendentes |
| Métrica | Estado estável | Durante a falha | Hipótese |
|---|---|---|---|
| Sucesso do checkout | 99,7% | 98,9% | ✅ (meta: > 95%) |
| Latência p95 | 410 ms | 1,9 s (só nos primeiros 12s), depois 520 ms | ✅ |
| Erros 5xx | 0,2% | 1,1% (pico inicial) | ✅ |
| Tempo até o breaker abrir | n/a | 12 s | ✅ (meta: ≤ 30 s) |
| Pedidos em modo degradado | 0% | ~9% | ✅ esperado |
A arquitetura sobreviveu. Mas o experimento não termina aí: o maior valor está no que não saiu como o planejado.
sliding-window-size de 20 para 10) fez o breaker reagir mais rápido, e anotamos essa melhoria para a próxima rodada.Essas três descobertas custaram 30 minutos de experimento e evitaram, com boa probabilidade, um incidente real.
O maior efeito do Master Deck não foi técnico, foi cultural:
Você não precisa de uma plataforma sofisticada. Faça assim:
Quebre as coisas com cuidado, para que elas não quebrem sozinhas na hora errada.
Gostou? Deixe nos comentários qual carta você colocaria no seu baralho. 🃏