"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.
Sumário
- O que é Chaos Engineering (e por que quebrar coisas de propósito)
- Dissecando o ChaosEngineeringMasterDeck
- Estudo de caso: a carta "Parceiro Lento"
- Observabilidade e resultados
- Conclusão: o que o caos nos ensinou
1. O que é Chaos Engineering (e por que quebrar coisas de propósito)
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.
A mudança de mentalidade
| 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 |
Por que não bastam os testes tradicionais?
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.
Os conceitos-chave
Antes de seguir, vale fixar o vocabulário que vai aparecer no restante do artigo:
- Estado estável (steady state): o comportamento normal e mensurável do sistema, definido por métricas de negócio e técnicas. Exemplo: "99,5% dos checkouts são concluídos com sucesso, com p95 abaixo de 800 ms".
- Hipótese: a aposta de que o estado estável se mantém mesmo durante a falha injetada.
- Variável (falha injetada): o evento que alteramos no ambiente (latência, queda de instância, perda de pacotes, erro de DNS).
- Blast radius (raio de explosão): o limite do impacto possível do experimento. Começamos pequenos de propósito.
- Observabilidade: a capacidade de entender o que acontece dentro do sistema a partir de métricas, logs e traces. Sem ela, o experimento é só uma queda.
- Abort conditions (condições de aborto): o "botão vermelho". Se uma métrica ultrapassar o limite, o experimento é interrompido automaticamente.
Produção ou staging?
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.
2. Dissecando o ChaosEngineeringMasterDeck
O que é o Master Deck?
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:
- Game Days: sessões em que o time "compra" uma carta, executa o experimento e discute o resultado.
- Regressão de resiliência: as cartas mais maduras são automatizadas e rodam periodicamente em staging.
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.
Anatomia de uma carta
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? |
Os naipes do baralho
Organizamos as cartas por tipo de falha:
- ♠️ Infraestrutura: queda de instância, de zona de disponibilidade (AZ), esgotamento de CPU/memória.
- ♥️ Rede: latência, perda de pacotes, partição de rede, falha de DNS.
- ♦️ Dependências: banco indisponível, API de terceiros lenta, fila cheia, cache fora do ar.
- ♣️ Aplicação: memory leak, deploy defeituoso, feature flag quebrada, dados corrompidos.
Exemplo de carta em YAML
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)
3. Estudo de caso: a carta "Parceiro Lento"
A arquitetura sob teste
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) │
└──────────────┘ └─────────────────┘
- order-service: backend em Spring Boot que recebe o checkout, grava o pedido e cobra o pagamento.
- payment-gateway: API de um parceiro. É a dependência que não controlamos.
- RDS PostgreSQL Multi-AZ e Auto Scaling Group distribuído em duas zonas de disponibilidade.
A carta sorteada
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%."
Definindo o blast radius
Esta é a etapa que separa um experimento sério de uma irresponsabilidade. Nosso raio de explosão foi deliberadamente pequeno:
- Ambiente: staging, com tráfego sintético gerado por k6.
-
Escopo: apenas a chamada
order-service → payment-gateway. O banco, o ALB e as demais rotas não foram tocados. - Duração: 10 minutos, com a condição de aborto sempre vigiada.
- Rollback: remover a "toxic" do proxy, uma operação de segundos.
Injetando a falha
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
O que o código precisava ter (e a hipótese testava)
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.
4. Observabilidade e resultados
Como monitoramos
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:
- Negócio: taxa de sucesso do checkout e pedidos em modo "pagamento pendente".
- Aplicação: latência p50/p95/p99, taxa de erros 5xx.
-
Resiliência: estado do Circuit Breaker (
closed,open,half_open) via métricas do Resilience4j no Actuator. - Recursos: uso do pool de threads, CPU e conexões abertas.
Linha do tempo do experimento
| 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 |
Resultado: a hipótese se confirmou?
| 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.
As surpresas (onde o caos paga a conta)
-
O pico inicial de latência foi pior que o esperado. Nos primeiros 12 segundos, antes de o breaker abrir, o p95 chegou a 1,9 s. Reduzir a janela de deslizamento (
sliding-window-sizede 20 para 10) fez o breaker reagir mais rápido, e anotamos essa melhoria para a próxima rodada. - O reprocessamento de pagamentos pendentes não tinha limite de taxa. Quando o gateway voltou, a fila disparou todas as cobranças de uma vez, quase criando uma segunda falha. Adicionamos rate limiting ao consumidor.
- Um alerta que deveria ter disparado, não disparou. A métrica de "breaker aberto" existia no Grafana, mas ninguém era notificado. Criamos o alerta antes de encerrar o Game Day.
Essas três descobertas custaram 30 minutos de experimento e evitaram, com boa probabilidade, um incidente real.
Padrões de resiliência que entraram em ação
- ⏱️ Timeout: impediu que threads ficassem presas indefinidamente.
- 🔌 Circuit Breaker: parou de bater em um serviço claramente doente e deu a ele espaço para se recuperar.
- 🛟 Fallback: transformou uma falha técnica em degradação controlada do negócio.
- 🧱 Bulkhead: garantiu que a lentidão do gateway não consumisse todas as threads e derrubasse rotas que nada têm a ver com pagamento.
5. Conclusão: o que o caos nos ensinou
Lições aprendidas
- Resiliência é uma hipótese até ser testada. Ter Circuit Breaker configurado não prova que ele funciona como você imagina.
- Comece pequeno. Um blast radius reduzido permite errar barato e aprender rápido.
- Observabilidade vem primeiro. Se você não consegue ver o efeito da falha, não consegue aprender com ela.
- Falhas de negócio importam tanto quanto as técnicas. O fallback "pagamento pendente" só existe porque o time de produto participou da conversa.
- O valor está nas surpresas. Um experimento em que tudo funciona como previsto é bom. Um que revela uma fragilidade escondida é ótimo.
O impacto na cultura DevOps
O maior efeito do Master Deck não foi técnico, foi cultural:
- Menos culpa, mais curiosidade. Falhar em um experimento é aprendizado, não erro de alguém.
- Desenvolvimento e operações na mesma mesa. O Game Day junta quem escreve o código e quem o opera, quebrando o clássico "problema do outro time".
- Resiliência como requisito, não como favor. Novas funcionalidades passaram a vir acompanhadas de uma pergunta: "qual carta do baralho testaria isso?"
- Confiança para evoluir. Times que sabem como o sistema se comporta sob falha fazem deploys com menos medo.
Próximos passos
- Automatizar as cartas maduras no pipeline de CI/CD (regressão de resiliência).
- Evoluir para experimentos em produção com tráfego canário.
- Criar cartas de maior risco, como queda de uma AZ inteira, usando o AWS Fault Injection Service.
Quer começar hoje?
Você não precisa de uma plataforma sofisticada. Faça assim:
- Escolha um serviço crítico e defina seu estado estável.
- Escreva uma carta com hipótese, blast radius e condição de aborto.
- Execute em staging com um grupo pequeno, com o painel de métricas aberto.
- Documente o que aprendeu e corrija o que falhou.
- Repita, sempre com uma carta nova.
Quebre as coisas com cuidado, para que elas não quebrem sozinhas na hora errada.
Referências e ferramentas
- Principles of Chaos Engineering
- Resilience4j — Circuit Breaker, Bulkhead, TimeLimiter para Java/Spring
- Toxiproxy — simulação de condições de rede
- Chaos Monkey for Spring Boot
- AWS Fault Injection Service
- k6 — geração de carga para os experimentos
Gostou? Deixe nos comentários qual carta você colocaria no seu baralho. 🃏
Top comments (0)