Sobrevivendo ao Caos: Injetando Falhas com o ChaosEngineeringMasterDeck
"Seu sistema vai falhar. A única pergunta é se você vai descobrir isso numa terça às 15h, com a equipe preparada, ou num sábado às 3h da manhã, com o cliente ligando."
Quase toda equipe de tecnologia já viveu aquele incidente em que "o serviço A estava lento, o B ficou esperando, o C estourou o pool de threads e, de repente, o sistema inteiro caiu". Ninguém planejou aquilo. Ninguém ensaiou. E a resposta foi improvisada sob pressão.
Neste artigo eu conto como a nossa equipe passou a ensaiar o desastre de propósito, usando uma abordagem que chamamos de ChaosEngineeringMasterDeck: um baralho de cartas de experimentos de caos que transforma o "vamos testar a resiliência" em algo estruturado, repetível e até divertido.
Vamos passar por:
- O que é Chaos Engineering (e por que quebrar coisas de propósito)
- O que é o Master Deck e como ele estrutura um experimento
- Um estudo de caso: Carta #07 – Latência Venenosa em um microsserviço Spring Boot na AWS
- Observabilidade, resultados e padrões de resiliência em ação
- Lições aprendidas e impacto na cultura DevOps
1. Introdução ao Chaos Engineering
O que é, afinal?
Chaos Engineering é a disciplina de experimentar em um sistema distribuído para construir confiança na capacidade dele de suportar condições turbulentas em produção. A definição vem dos Principles of Chaos Engineering, consolidados a partir do trabalho da Netflix (o famoso Chaos Monkey, que derrubava instâncias aleatórias).
Repare que não se trata de "sair quebrando tudo". É ciência aplicada: formulamos uma hipótese, injetamos uma falha controlada e observamos se o comportamento do sistema confirma ou refuta o que esperávamos.
Por que quebrar coisas de propósito?
Porque sistemas distribuídos falham de formas que nenhum teste unitário ou de integração consegue prever:
- Redes perdem pacotes e introduzem latência imprevisível.
- Dependências ficam lentas (o que é bem pior do que ficar fora do ar).
- Zonas de disponibilidade inteiras podem ficar indisponíveis.
- Configurações de timeout, retry e pool de conexões interagem de maneiras inesperadas.
Testes tradicionais validam o "caminho feliz" e algumas falhas previstas. O caos valida o desconhecido: as falhas que você ainda não imaginou.
E em produção ou em staging?
A resposta honesta é: comece em staging, evolua para produção.
| Estágio de maturidade | Onde rodar | Característica |
|---|---|---|
| Iniciante | Ambiente local / dev | Aprender a ferramenta e validar o experimento |
| Intermediário | Staging com tráfego sintético | Validar hipóteses sem risco ao cliente |
| Avançado | Produção, com blast radius mínimo | Único lugar onde o tráfego, os dados e as dependências são reais |
O argumento para chegar à produção é simples: staging nunca é idêntico a produção. Mas só se chega lá com observabilidade sólida, botão de abortar e raio de explosão pequeno.
A mudança de mentalidade
A transformação mais importante não é técnica, é cultural:
| Mentalidade antiga | Mentalidade de resiliência |
|---|---|
| "Vamos evitar que falhe" | "Vamos assumir que vai falhar" |
| Disponibilidade = nunca cair | Disponibilidade = degradar com elegância |
| Falhou? Quem foi o culpado? | Falhou? O que o sistema nos ensinou? |
| Descobrir falhas em incidentes | Descobrir falhas em experimentos |
Em vez de investir tudo em MTBF (tempo médio entre falhas), passamos a investir também em MTTR (tempo médio de recuperação) e em reduzir o impacto de cada falha.
Vocabulário essencial
Antes de seguir, alguns conceitos que aparecerão ao longo do texto:
- Estado Estável (Steady State): o comportamento normal e mensurável do sistema, definido por métricas de negócio e de saúde (ex.: p99 de latência < 400 ms, taxa de checkout com sucesso > 99,5%).
- Hipótese: a afirmação testável de que o estado estável se manterá durante a falha.
- Blast Radius (raio de explosão): a extensão máxima do impacto potencial do experimento (usuários, serviços, regiões afetadas).
- Observabilidade: a capacidade de entender o que acontece dentro do sistema a partir de suas saídas: métricas, logs e traces.
- Abort Condition (condição de aborto): o gatilho que interrompe o experimento imediatamente.
2. Dissecando o ChaosEngineeringMasterDeck
O que é o Master Deck?
O ChaosEngineeringMasterDeck é o nosso baralho de experimentos de caos. Cada carta é um experimento documentado, versionado e reutilizável, que descreve uma falha, como injetá-la, o que esperamos que aconteça e como saber se deu certo.
Por que "cartas"? Porque o formato resolve três problemas práticos:
- Padronização: todo experimento segue a mesma estrutura, o que facilita revisão e comparação.
- Descoberta: a equipe "puxa uma carta" nos Game Days, o que torna o exercício leve e inclusivo, e não um ritual temido.
- Reuso: uma carta bem escrita vira regressão de resiliência; pode rodar de novo a cada mudança arquitetural.
Na prática, o Master Deck é um repositório Git com um arquivo YAML/Markdown por carta, organizado por naipe:
chaos-master-deck/
├── README.md
├── naipes/
│ ├── infraestrutura/ # ♠ queda de AZ, perda de nó, disco cheio
│ ├── rede/ # ♥ latência, perda de pacotes, partição
│ ├── dependencias/ # ♦ banco fora, API de terceiros lenta
│ └── recursos/ # ♣ CPU, memória, esgotamento de threads
├── templates/
│ └── carta.yaml
└── game-days/
└── 2026-10-relatorio.md
Anatomia de uma carta
Toda carta responde às mesmas perguntas, nesta ordem, que espelham o método científico:
| Campo | Pergunta que responde |
|---|---|
| Nome e naipe | Que tipo de falha é esta? |
| Estado estável | Como é "normal" para este sistema? Quais métricas provam isso? |
| Hipótese | O que acreditamos que vai acontecer quando a falha ocorrer? |
| Variáveis | O que vamos injetar, onde, com que intensidade e por quanto tempo? |
| Métricas de controle | O que observar para confirmar ou refutar a hipótese? |
| Blast Radius | Qual o impacto máximo permitido? |
| Condições de aborto | Quando paramos tudo, imediatamente? |
| Padrões esperados | Quais mecanismos de resiliência devem entrar em ação? |
| Resultado | Confirmada, refutada ou parcial? Que ações saíram disso? |
Um template simplificado:
# templates/carta.yaml
carta:
id: "00"
nome: "<nome da carta>"
naipe: "<infraestrutura | rede | dependencias | recursos>"
dificuldade: "<baixa | media | alta>"
estado_estavel:
descricao: "<comportamento normal em linguagem de negócio>"
metricas:
- { nome: "<metrica>", limite: "<valor>" }
hipotese: >
<Se X acontecer, então Y continuará dentro de Z.>
variaveis:
falha: "<o que injetar>"
alvo: "<onde>"
intensidade: "<quanto>"
duracao: "<por quanto tempo>"
metricas_de_controle: []
blast_radius: "<limite de impacto>"
condicoes_de_aborto: []
padroes_esperados: []
resultado: "<preencher após o experimento>"
O ciclo do experimento
Cada carta jogada segue o mesmo ciclo:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 1. Definir │───▶│ 2. Hipótese │───▶│ 3. Limitar │
│ estado │ │ "vai se │ │ blast radius │
│ estável │ │ manter" │ │ + abortar │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
┌──────────────┐ ┌──────────────┐ ┌──────▼───────┐
│ 6. Corrigir │◀───│ 5. Analisar │◀───│ 4. Injetar │
│ e documentar │ │ confirmou? │ │ falha e │
│ (voltar ao 1)│ │ refutou? │ │ observar │
└──────────────┘ └──────────────┘ └──────────────┘
3. Estudo de Caso: Carta #07 – Latência Venenosa
O cenário (a arquitetura)
Nosso ambiente de laboratório simula um e-commerce simplificado rodando na AWS, com microsserviços Spring Boot em um cluster EKS distribuído em 3 Zonas de Disponibilidade:
┌─────────────────────┐
Usuários ──────────▶ │ ALB (Multi-AZ) │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ api-checkout │ Spring Boot (3 réplicas, 1 por AZ)
└───┬──────────────┬──┘
│ │
┌─────────────▼───┐ ┌─────▼────────────┐
│ servico-estoque │ │ servico-pagamento│
│ (Spring Boot) │ │ (Spring Boot) │
└────────┬────────┘ └─────┬────────────┘
│ │
┌────────▼───────┐ ┌───────▼────────┐
│ Redis (cache) │ │ RDS PostgreSQL │
│ ElastiCache │ │ Multi-AZ │
└────────────────┘ └────────────────┘
O fluxo crítico é o checkout: api-checkout consulta o servico-estoque para validar disponibilidade e depois chama o servico-pagamento.
Por que "latência" e não "queda"?
Quando um serviço cai, o cliente recebe um erro rápido (connection refused) e os mecanismos de falha rápida funcionam bem. Mas quando ele fica lento, as threads do chamador ficam penduradas esperando, o pool de conexões se esgota e a falha se propaga em cascata. Por isso chamamos esta carta de "Latência Venenosa": a lentidão contamina quem depende de você.
A carta
carta:
id: "07"
nome: "Latência Venenosa"
naipe: "rede"
dificuldade: "media"
estado_estavel:
descricao: "Checkout rápido e confiável para o cliente."
metricas:
- { nome: "checkout_p99_latency", limite: "< 1200 ms" }
- { nome: "checkout_success_rate", limite: ">= 99.5 %" }
- { nome: "http_5xx_rate_api", limite: "< 0.5 %" }
hipotese: >
Se o servico-estoque passar a responder com 800 ms de atraso adicional
em 50% das requisições, o api-checkout continuará atendendo com taxa
de sucesso >= 99% e p99 < 2000 ms, graças a timeout, circuit breaker
e fallback para o cache de estoque.
variaveis:
falha: "Injeção de latência de rede (delay 800ms, jitter 200ms)"
alvo: "Pods do servico-estoque (namespace: loja-staging)"
intensidade: "50% do tráfego"
duracao: "10 minutos"
metricas_de_controle:
- "Taxa de sucesso e latência p50/p95/p99 do checkout"
- "Estado do circuit breaker (CLOSED/OPEN/HALF_OPEN)"
- "Threads ativas e fila do pool do api-checkout"
- "Taxa de acerto do fallback (cache) e requisições de retry"
blast_radius: >
Apenas o servico-estoque em staging, com tráfego sintético de
100 req/s. Nenhum dado é alterado (somente leitura).
condicoes_de_aborto:
- "checkout_success_rate < 90% por mais de 60s"
- "Saturação de threads do api-checkout > 95%"
- "Qualquer alerta crítico não previsto"
padroes_esperados:
- "Timeout curto"
- "Circuit Breaker (slow call rate)"
- "Fallback para cache"
- "Bulkhead (isolamento de threads)"
Definindo o Blast Radius
O raio de explosão é a decisão mais importante do experimento. Nós o limitamos em quatro dimensões:
| Dimensão | Limite adotado |
|---|---|
| Ambiente | Staging (primeira rodada) |
| Serviços | Somente servico-estoque
|
| Tráfego | 50% das requisições para esse serviço, com carga sintética controlada |
| Tempo | 10 minutos, com kill switch manual e automático |
E o botão de abortar foi tratado como requisito, e não como detalhe: o experimento só começa quando o time confirma que consegue desfazer a falha em segundos.
Como a falha foi injetada
Como o cluster é Kubernetes, usamos o Chaos Mesh para injetar latência de rede nos pods do serviço alvo:
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: carta-07-latencia-venenosa
namespace: loja-staging
spec:
action: delay
mode: fixed-percent
value: "50" # 50% dos pods selecionados
selector:
namespaces:
- loja-staging
labelSelectors:
app: servico-estoque
delay:
latency: "800ms"
jitter: "200ms"
correlation: "50"
duration: "10m"
💡 Alternativas: para falhas em nível de infraestrutura AWS (ex.: interromper instâncias de uma AZ, degradar o RDS), o AWS Fault Injection Service (FIS) é uma ótima opção. Para testes locais, o Toxiproxy resolve bem.
As defesas que preparamos (antes do caos)
No api-checkout, configuramos Resilience4j com Spring Boot:
# application.yml (api-checkout)
resilience4j:
circuitbreaker:
instances:
estoque:
sliding-window-type: COUNT_BASED
sliding-window-size: 20
minimum-number-of-calls: 10
failure-rate-threshold: 50
slow-call-duration-threshold: 500ms # chamada "lenta" a partir de 500ms
slow-call-rate-threshold: 60 # abre o circuito se 60% forem lentas
wait-duration-in-open-state: 15s
permitted-number-of-calls-in-half-open-state: 5
bulkhead:
instances:
estoque:
max-concurrent-calls: 25
max-wait-duration: 0
retry:
instances:
estoque:
max-attempts: 3
wait-duration: 200ms
@Service
public class EstoqueClient {
private final RestClient restClient;
private final EstoqueCache cache;
@CircuitBreaker(name = "estoque", fallbackMethod = "estoqueEmCache")
@Bulkhead(name = "estoque")
@Retry(name = "estoque")
public Disponibilidade consultar(String sku) {
return restClient.get()
.uri("/estoque/{sku}", sku)
.retrieve()
.body(Disponibilidade.class);
}
// Fallback: degrada com elegância usando o último valor conhecido
private Disponibilidade estoqueEmCache(String sku, Throwable t) {
return cache.ultimoValorConhecido(sku)
.orElse(Disponibilidade.assumirDisponivelComRessalva(sku));
}
}
O RestClient também possui timeouts de conexão e leitura explícitos (700 ms de leitura). Sem timeout, nenhum dos outros padrões funciona direito, porque a chamada nunca "falha": ela só espera.
4. Observabilidade e Resultados
Como monitoramos
Chaos sem observabilidade é só vandalismo. Antes de jogar a carta, montamos um painel único com os três pilares:
-
Métricas (Prometheus + Grafana): latência p50/p95/p99, taxa de erro, estado do circuit breaker (
resilience4j_circuitbreaker_state), threads ativas, taxa de fallback. - Traces (OpenTelemetry + Jaeger/X-Ray): para enxergar onde o tempo estava sendo gasto em cada requisição de checkout.
-
Logs estruturados (CloudWatch Logs / Loki): com
traceIdcorrelacionado, para investigar requisições específicas.
Um exemplo de consulta PromQL que ficou fixada no painel:
# Taxa de sucesso do checkout nos últimos 1m
sum(rate(http_server_requests_seconds_count{app="api-checkout",uri="/checkout",status=~"2.."}[1m]))
/
sum(rate(http_server_requests_seconds_count{app="api-checkout",uri="/checkout"}[1m]))
A linha do tempo do experimento
| Momento | O que aconteceu |
|---|---|
| T+0 min | Falha injetada. Latência do servico-estoque sobe para ~800–1000 ms em metade das chamadas. |
| T+0 a T+1 min | p99 do checkout dispara. O @Retry repete chamadas lentas, aumentando a carga sobre o serviço degradado. |
| T+1 min | O circuit breaker, por slow call rate, passa para OPEN. |
| T+1 a T+10 min | Com o circuito aberto, o api-checkout responde via fallback em cache. A latência volta ao patamar normal. |
| A cada 15s | Circuito vai a HALF_OPEN, testa 5 chamadas, e volta a OPEN enquanto o serviço continuar lento. |
| T+10 min | Falha removida. O circuito fecha após as chamadas de teste bem-sucedidas. |
A hipótese foi confirmada?
Parcialmente. E esse é o tipo de resultado mais valioso que um experimento de caos pode entregar.
✅ O que confirmou a hipótese:
- Depois que o circuito abriu, o checkout se manteve dentro do estado estável, com respostas servidas pelo cache.
- O bulkhead impediu que o
servico-estoquelento consumisse todas as threads doapi-checkout; o endpoint de pagamento seguiu saudável. - O sistema se recuperou sozinho após o fim da falha, sem intervenção manual.
❌ O que a refutou (e nos surpreendeu):
- Na primeira janela de ~1 minuto (antes do circuito abrir), o p99 ultrapassou nosso limite de 2000 ms. O motivo: três tentativas de retry sobre uma dependência lenta amplificaram a carga (o clássico retry storm).
- O fallback devolvia dados em cache sem indicar a idade do dado, o que, em um cenário real, poderia permitir a venda de um item sem estoque.
Ações corretivas geradas
| # | Achado | Ação |
|---|---|---|
| 1 | Retry amplificando a degradação | Reduzir para 2 tentativas, com backoff exponencial + jitter, e não aplicar retry em chamadas lentas |
| 2 | Circuito demorava a abrir | Diminuir minimum-number-of-calls e a janela deslizante |
| 3 | Fallback sem contexto | Marcar respostas do cache como "estoque sujeito a confirmação" e validar na etapa de pagamento |
| 4 | Falta de alerta específico | Criar alerta para circuito OPEN por mais de 2 minutos |
Todas as ações viraram tickets e, depois de implementadas, a mesma carta foi jogada de novo, agora como teste de regressão de resiliência.
5. Conclusão
O que aprendemos
- Falhas lentas são piores que falhas rápidas. Timeouts, circuit breakers e bulkheads precisam ser tratados como requisitos, não como "otimização".
- Padrões de resiliência também podem causar problemas. O retry, pensado para proteger, quase derrubou a nossa estabilidade. Só um experimento mostrou isso.
- Hipóteses refutadas são vitórias. Cada surpresa descoberta em staging é um incidente a menos em produção.
- Sem observabilidade, não há experimento. Se você não consegue medir o estado estável, não consegue saber se quebrou.
- Estrutura reduz o medo. O formato de cartas deu previsibilidade, segurança e uma linguagem comum para o time.
Impacto na cultura DevOps
O maior ganho do Master Deck não foi técnico. Foi cultural:
- Cultura sem culpa: falhas viram aprendizado coletivo, e não busca por responsáveis.
- Desenvolvedores e operações juntos: quem escreve o código participa de quem o "quebra", e isso reforça o you build it, you run it.
- Resiliência como parte do ciclo de entrega: as cartas podem rodar no pipeline de CI/CD como testes de regressão.
- Onboarding mais rápido: novas pessoas aprendem a arquitetura vendo-a falhar de forma segura.
Por onde começar
Se você quer experimentar na sua equipe, um caminho enxuto:
- Escolha um fluxo crítico e defina o estado estável dele.
- Garanta observabilidade mínima (métricas + traces).
- Escreva a sua primeira carta com hipótese, blast radius e condição de aborto.
- Rode em staging, com a equipe reunida (um Game Day).
- Corrija, documente e jogue a carta de novo.
- Só depois pense em produção, com o menor raio de explosão possível.
O caos não é o inimigo. A surpresa é. E a melhor forma de eliminar surpresas é provocá-las, com cuidado, antes que o mundo real o faça por você.
Referências e leituras recomendadas
- Principles of Chaos Engineering: principlesofchaos.org
- Chaos Engineering: System Resiliency in Practice (Casey Rosenthal & Nora Jones, O'Reilly)
- Documentação do Resilience4j: resilience4j.readme.io
- Documentação do Chaos Mesh: chaos-mesh.org
- AWS Fault Injection Service: aws.amazon.com/fis
- Release It! (Michael Nygard): origem de padrões como Circuit Breaker e Bulkhead
Gostou? Me conta nos comentários qual carta você jogaria primeiro na sua arquitetura. 🃏🔥
Tags: #chaosengineering #resilience #devops #springboot #aws #sre
Top comments (0)