Era uma sexta-feira, 18h47. O dashboard estava verde, o deploy da semana tinha passado sem sustos e metade do time já pensava no fim de semana. Foi exatamente nesse momento que um dos nossos microsserviços começou a responder em 4 segundos em vez de 80 milissegundos. Nenhum servidor caiu. Nenhum alerta de "serviço fora do ar" disparou. Mesmo assim, em menos de três minutos, o checkout inteiro da plataforma estava travado.
A boa notícia: isso não aconteceu em um incidente real. Aconteceu porque nós mesmos provocamos — de forma controlada, com hipótese escrita, métricas monitoradas e um botão de abortar ao alcance da mão. E foi uma das descobertas mais valiosas que fizemos naquele trimestre.
Neste artigo, vou mostrar como nossa equipe usa o ChaosEngineeringMasterDeck — um baralho de experimentos de caos que criamos para tornar a engenharia de resiliência sistemática, repetível e (por que não?) divertida.
1. Introdução ao Chaos Engineering
1.1 O que é, afinal?
Chaos Engineering é a disciplina de experimentar em um sistema distribuído para construir confiança na sua capacidade de suportar condições turbulentas em produção. O termo ganhou força com a Netflix por volta de 2010, quando a empresa migrou para a AWS e criou o famoso Chaos Monkey, uma ferramenta que desligava instâncias aleatoriamente durante o horário comercial.
A ideia parece absurda à primeira vista: por que alguém desligaria seus próprios servidores? A resposta é simples:
- Em sistemas distribuídos, a falha é uma certeza, não uma possibilidade. Redes têm latência, discos enchem, zonas de disponibilidade caem, dependências externas ficam lentas.
- Testes tradicionais validam o que você já sabe. Testes unitários e de integração verificam comportamentos esperados. O caos revela comportamentos emergentes — aqueles que surgem da interação entre dezenas de componentes.
- É melhor descobrir uma fraqueza às 14h de uma terça-feira, com o time inteiro preparado, do que às 3h da manhã de um domingo, com clientes reclamando.
1.2 Os princípios fundamentais
O manifesto Principles of Chaos Engineering define a base que seguimos à risca:
- Construa uma hipótese em torno do estado estável (steady state). Em vez de olhar para métricas internas (CPU, memória), defina o comportamento normal pelo que importa ao negócio: pedidos por minuto, taxa de sucesso, latência percebida pelo usuário.
- Varie eventos do mundo real. Simule o que de fato acontece: queda de instâncias, latência de rede, falha de DNS, picos de tráfego.
- Execute experimentos em produção (ou no ambiente mais próximo possível dela). Só a produção tem o tráfego, os dados e as configurações reais.
- Automatize os experimentos para rodar continuamente. Resiliência é um alvo móvel — cada deploy pode reintroduzir uma fraqueza.
- Minimize o raio de explosão (blast radius). Comece pequeno, expanda gradualmente.
1.3 A mudança de mentalidade: de evitar falhas para abraçá-las
A engenharia tradicional tenta prevenir falhas a todo custo — o que, em sistemas distribuídos, é uma batalha perdida. A engenharia de resiliência parte de outra premissa:
| Mentalidade tradicional | Mentalidade de resiliência |
|---|---|
| "Nosso sistema não pode cair." | "Nosso sistema vai falhar. Como ele se comporta quando isso acontecer?" |
| Foco em MTBF (tempo médio entre falhas) | Foco em MTTR (tempo médio de recuperação) |
| Incidente = culpa de alguém | Incidente = oportunidade de aprendizado (blameless) |
| Confiança baseada em esperança | Confiança baseada em evidência |
💡 Ponto-chave: Chaos Engineering não é sair quebrando coisas aleatoriamente. É o método científico aplicado à confiabilidade: hipótese, experimento controlado, observação e conclusão.
2. Dissecando o ChaosEngineeringMasterDeck
2.1 O que é o Master Deck?
Quando começamos a praticar Chaos Engineering, enfrentamos três problemas:
- Os experimentos eram ad hoc: cada engenheiro testava o que lembrava, do jeito que achava melhor.
- Os resultados se perdiam em threads de chat e nunca viravam conhecimento compartilhado.
- O time via o caos como algo intimidador, reservado a "especialistas em SRE".
O ChaosEngineeringMasterDeck nasceu para resolver os três. Ele funciona em três camadas ao mesmo tempo:
- Framework: um catálogo padronizado de experimentos, em que cada "carta" é um modelo completo de teste de falha.
- Ferramenta gamificada: nos nossos Game Days mensais, o time "compra" cartas do baralho e executa os cenários sorteados, acumulando pontos por fraquezas descobertas e corrigidas.
- Documentação viva de incidentes: cada execução gera um registro versionado no repositório, e incidentes reais de produção viram novas cartas.
2.2 Os naipes do baralho
As cartas são organizadas em naipes, cada um representando uma categoria de falha:
| Naipe | Categoria | Exemplos de cartas |
|---|---|---|
| ♠️ Espadas | Infraestrutura | Queda de instância EC2, perda de uma Availability Zone, nó do Kubernetes drenado |
| ♥️ Copas | Rede | Latência injetada, perda de pacotes, falha de DNS, partição de rede |
| ♦️ Ouros | Dados e estado | Banco de dados indisponível, failover de réplica, cache Redis zerado |
| ♣️ Paus | Aplicação e dependências | Exceções injetadas, API de terceiros fora do ar, exaustão de pool de threads |
O valor da carta indica sua severidade e, consequentemente, o nível de maturidade exigido do time:
- 2 a 5: experimentos de baixo impacto, ideais para staging e times iniciantes.
- 6 a 10: experimentos em produção com blast radius limitado.
- Valete, Dama, Rei: cenários compostos (múltiplas falhas simultâneas).
- Ás: desastres regionais — só liberados depois que o time completou as cartas anteriores do mesmo naipe.
2.3 Anatomia de uma carta
Cada carta é um arquivo YAML versionado no Git. Essa estrutura obriga o time a pensar antes de quebrar qualquer coisa:
id: COPAS-07
nome: "A Lesma no Estoque"
naipe: copas # rede
valor: 7 # produção com blast radius limitado
hipotese: >
Se o inventory-service sofrer 3s de latência adicional,
o checkout continuará concluindo pedidos com taxa de sucesso ≥ 99%
e p99 < 1,5s, pois o circuit breaker abrirá e o fallback
de reserva otimista assumirá.
estado_estavel:
- metrica: taxa_sucesso_checkout
baseline: ">= 99.5%"
- metrica: latencia_p99_checkout
baseline: "< 800ms"
- metrica: pedidos_por_minuto
baseline: "~1.200 (horário comercial)"
variaveis:
independente: "latência de 3000ms em 100% das respostas do inventory-service"
alvo: "pods com label canary=chaos (10% do tráfego)"
duracao: "10 minutos"
blast_radius:
ambiente: producao
trafego_afetado: "10% (pods canary)"
usuarios: "somente região sa-east-1"
dependencias_protegidas: ["payment-service", "user-service"]
condicoes_de_aborto:
- "taxa_sucesso_checkout < 97% por mais de 60s"
- "erros 5xx no API Gateway > 2%"
- "qualquer alerta P1 disparado"
rollback: "kubectl delete -f experiments/copas-07-latency.yaml"
responsaveis: ["@time-checkout", "@sre-plantao"]
Repare que a carta separa claramente os três pilares de um bom experimento:
- Hipótese: uma afirmação falsificável sobre o comportamento do sistema.
- Variáveis: o que estamos alterando (variável independente) e onde.
- Métricas de controle: o estado estável que precisa ser preservado e os limites que disparam o aborto.
3. Estudo de Caso: a carta "A Lesma no Estoque" (♥️ Copas 7)
3.1 A arquitetura sob teste
Nossa plataforma de e-commerce roda na AWS, em um cluster Amazon EKS distribuído em três Availability Zones da região sa-east-1 (São Paulo). O fluxo de checkout envolve quatro microsserviços em Spring Boot 3:
┌──────────────────┐
Usuário ──HTTPS──▶ │ API Gateway │
│ (AWS ALB + WAF) │
└────────┬─────────┘
│
┌────────▼─────────┐
│ checkout-service │ ◀── orquestra o pedido
│ (Spring Boot) │
└──┬──────┬─────┬──┘
│ │ │
┌─────────────┘ │ └──────────────┐
▼ ▼ ▼
┌─────────────────────┐ ┌───────────────┐ ┌──────────────────┐
│ inventory-service │ │payment-service│ │ user-service │
│ 💥 ALVO DO CAOS │ │ │ │ │
└─────────┬───────────┘ └───────┬───────┘ └────────┬─────────┘
│ │ │
┌───────▼───────┐ ┌───────▼───────┐ ┌───────▼───────┐
│ Amazon RDS │ │ Gateway de │ │ ElastiCache │
│ (PostgreSQL) │ │ pagamento │ │ (Redis) │
└───────────────┘ └───────────────┘ └───────────────┘
O checkout-service consulta o inventory-service para reservar o estoque antes de cobrar o cliente. Essa chamada é síncrona e fica no caminho crítico do pedido — o lugar perfeito para uma fraqueza se esconder.
3.2 Por que essa carta?
Uma queda total de serviço é relativamente fácil de lidar: a conexão é recusada, o erro é imediato e os mecanismos de fallback disparam. Latência é mais traiçoeira. Um serviço lento não está "fora do ar" — ele segura conexões, ocupa threads e pode propagar a lentidão para todos os serviços que dependem dele. É o que chamamos de falha em cascata.
3.3 Os padrões de resiliência que esperávamos ver
O checkout-service usa Resilience4j para proteger a chamada ao estoque:
# application.yml do checkout-service
resilience4j:
circuitbreaker:
instances:
inventory:
sliding-window-type: COUNT_BASED
sliding-window-size: 50
failure-rate-threshold: 50 # abre com 50% de falhas
slow-call-duration-threshold: 1s # chamada > 1s conta como lenta
slow-call-rate-threshold: 50 # abre com 50% de chamadas lentas
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 5
timelimiter:
instances:
inventory:
timeout-duration: 1500ms
retry:
instances:
inventory:
max-attempts: 3
wait-duration: 200ms
@CircuitBreaker(name = "inventory", fallbackMethod = "reservaOtimista")
@Retry(name = "inventory")
public ReservaResponse reservarEstoque(PedidoRequest pedido) {
return inventoryClient.reservar(pedido.getItens());
}
// Fallback: aceita o pedido e reconcilia o estoque de forma assíncrona
private ReservaResponse reservaOtimista(PedidoRequest pedido, Throwable ex) {
log.warn("Fallback de estoque acionado para pedido {}: {}",
pedido.getId(), ex.getMessage());
eventPublisher.publish(new ReservaPendenteEvent(pedido));
return ReservaResponse.pendente(pedido.getId());
}
No papel, tudo parecia sólido: o circuit breaker abriria, o fallback assumiria e o cliente nem perceberia.
3.4 Definindo o Blast Radius
O blast radius é a área máxima de impacto que aceitamos causar. Para essa carta, ele foi limitado em quatro dimensões:
-
Tráfego: apenas os pods do
inventory-servicemarcados comcanary=chaos, que recebem 10% das requisições via divisão de tráfego. -
Geografia: somente a região
sa-east-1. - Tempo: janela máxima de 10 minutos, às 14h de uma terça-feira (horário de pico moderado, time completo disponível).
-
Dependências:
payment-serviceeuser-serviceficaram explicitamente fora do escopo.
Na prática, o impacto máximo teórico seria uma degradação em ~10% dos checkouts por 10 minutos — e as condições de aborto garantiam que isso nunca chegaria perto de um incidente real.
3.5 Injetando a falha
Usamos o Chaos Mesh no cluster EKS para injetar a latência de rede apenas nos pods canary:
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: copas-07-lesma-no-estoque
namespace: inventory
spec:
action: delay
mode: all
selector:
namespaces: ["inventory"]
labelSelectors:
app: inventory-service
canary: chaos
delay:
latency: "3000ms"
jitter: "200ms"
correlation: "50"
direction: to
duration: "10m"
4. Observabilidade e Resultados
4.1 Como monitoramos o experimento
Sem observabilidade, Chaos Engineering é só sabotagem. Nossa stack para o experimento:
- Prometheus + Grafana: métricas do estado estável e um painel dedicado à carta, com as condições de aborto desenhadas como linhas de limite.
-
Micrometer + Resilience4j: exposição do estado do circuit breaker (
CLOSED,OPEN,HALF_OPEN) em tempo real. - OpenTelemetry + Jaeger: tracing distribuído para seguir uma requisição de ponta a ponta e ver onde o tempo estava sendo gasto.
- Amazon CloudWatch: métricas do ALB e do RDS.
Algumas das consultas que acompanhamos ao vivo:
# Taxa de sucesso do checkout (estado estável)
sum(rate(http_server_requests_seconds_count{service="checkout", status!~"5.."}[1m]))
/
sum(rate(http_server_requests_seconds_count{service="checkout"}[1m]))
# Latência p99 do checkout
histogram_quantile(0.99,
sum(rate(http_server_requests_seconds_bucket{service="checkout"}[1m])) by (le))
# Estado do circuit breaker (1 = OPEN)
resilience4j_circuitbreaker_state{name="inventory", state="open"}
4.2 A linha do tempo do experimento
| Tempo | Evento observado |
|---|---|
| T+0:00 | Latência injetada. Estado estável dentro do baseline. |
| T+0:40 | p99 do checkout sobe de 780ms para 2,1s nos pedidos roteados para o canary. |
| T+1:10 | Threads do Tomcat no checkout-service chegam a 92% de uso. 😬 |
| T+1:35 | Taxa de sucesso cai para 97,8% — perto da condição de aborto (97%). |
| T+1:50 | Circuit breaker finalmente abre. Fallback de reserva otimista assume. |
| T+2:30 | Taxa de sucesso volta a 99,6%. p99 estabiliza em 310ms. ✅ |
| T+10:00 | Experimento encerrado automaticamente. Circuit breaker volta a CLOSED após o HALF_OPEN. |
4.3 A arquitetura sobreviveu?
Sim — mas não pelo motivo que imaginávamos. A hipótese foi parcialmente refutada: o sistema se recuperou, mas levou quase dois minutos para isso, e chegou perigosamente perto da condição de aborto.
O tracing no Jaeger revelou o culpado: a combinação de Retry com TimeLimiter.
Uma única requisição de checkout, antes da correção:
checkout ──▶ inventory [tentativa 1] 1500ms ⏱ timeout
──▶ inventory [tentativa 2] 1500ms ⏱ timeout (+200ms de espera)
──▶ inventory [tentativa 3] 1500ms ⏱ timeout (+200ms de espera)
──▶ fallback
Total: ~4,9s segurando uma thread
Cada requisição ficava quase 5 segundos presa antes de chegar ao fallback. Além disso, como o @Retry estava posicionado fora do circuit breaker na ordem de decoradores, cada tentativa era contada como uma chamada separada — mas lentas o suficiente para que a janela deslizante de 50 chamadas demorasse a encher. Resultado: o mecanismo que deveria proteger o sistema estava amplificando a carga sobre um serviço já degradado — um clássico retry storm em miniatura.
4.4 Padrões de resiliência: o que funcionou e o que corrigimos
✅ Funcionou como esperado:
- Circuit Breaker: uma vez aberto, isolou completamente o serviço lento e permitiu a recuperação em segundos.
- Fallback com reserva otimista: nenhum pedido foi perdido; as reservas pendentes foram reconciliadas pelo consumidor de eventos assim que o estoque voltou.
- Bulkhead implícito via canary: os 90% de tráfego fora do canary nunca perceberam o experimento.
-
Horizontal Pod Autoscaler: escalou o
checkout-servicede 6 para 9 réplicas ao detectar a alta de CPU, aliviando a pressão sobre as threads.
🔧 O que mudamos depois do experimento:
- Redução do timeout de 1500ms para 800ms, alinhado ao p99.9 real do serviço de estoque.
- Retry com backoff exponencial e jitter, limitado a 2 tentativas, e apenas para erros transitórios (não para timeouts).
- Ajuste da ordem dos decoradores para que o circuit breaker avalie o resultado agregado.
- Bulkhead explícito (pool de threads dedicado) para a chamada ao estoque, impedindo que ela consuma todas as threads do Tomcat.
- Novo alerta para uso de threads acima de 80%.
4.5 Reexecutando a carta
Duas semanas depois, rodamos a mesma carta novamente. O resultado:
| Métrica | 1ª execução | 2ª execução |
|---|---|---|
| Tempo até o circuit breaker abrir | 1min50s | 14s |
| Menor taxa de sucesso observada | 97,8% | 99,4% |
| Pico de p99 do checkout | 4,9s | 920ms |
| Pico de uso de threads | 92% | 41% |
| Hipótese | Parcialmente refutada | Confirmada ✅ |
Essa é a beleza do Master Deck: a carta não é descartada depois de jogada. Ela volta para o baralho e é incorporada ao nosso pipeline de CI/CD, rodando automaticamente em staging a cada release do checkout-service. Se alguém reintroduzir o problema, descobrimos antes da produção.
5. Conclusão
Quebrar coisas de propósito pode parecer contraintuitivo, mas o experimento da carta ♥️ Copas 7 mostrou exatamente por que fazemos isso: nossa configuração de resiliência estava errada, e nenhum teste convencional teria revelado. Ela passava em todos os testes unitários e de integração. Só falhou sob as condições reais de um sistema distribuído em produção.
Lições aprendidas
- Padrões de resiliência mal configurados podem ser piores do que nenhum. Retry sem limites e timeouts generosos transformam uma falha pequena em uma falha em cascata.
- Latência é mais perigosa que indisponibilidade. Um serviço lento consome recursos de todos que dependem dele.
- O estado estável deve ser medido pelo negócio. Foi a taxa de sucesso do checkout — e não a CPU — que nos mostrou o problema real.
- Blast radius pequeno permite aprender com segurança. Com 10% do tráfego e condições de aborto claras, descobrimos uma fraqueza séria sem gerar um único incidente.
- Observabilidade é pré-requisito. Sem tracing distribuído, teríamos visto apenas "o checkout ficou lento" — e não o porquê.
O impacto na cultura DevOps
O maior ganho do ChaosEngineeringMasterDeck não foi técnico, foi cultural:
- Resiliência virou responsabilidade de todos, não só do time de SRE. Desenvolvedores de produto agora criam suas próprias cartas.
- Os Game Days viraram rituais esperados — a gamificação reduziu o medo de "mexer em produção" e aumentou o engajamento.
- Os postmortems ganharam continuidade: todo incidente real gera uma nova carta, garantindo que a mesma falha seja testada para sempre.
- A confiança mudou de natureza: antes, achávamos que o sistema aguentaria. Agora, temos evidência de que aguenta — e sabemos exatamente quais cartas ainda não jogamos.
Se você quer começar, não precisa de um baralho completo. Escolha uma dependência crítica, escreva uma hipótese, defina um blast radius pequeno e rode um experimento em staging. O caos vai acontecer de qualquer forma — a única pergunta é se você vai estar preparado quando ele chegar.
E você? Qual seria a primeira carta do seu baralho? Conta aqui nos comentários! 👇
📚 Referências e leituras recomendadas
- Principles of Chaos Engineering
- Rosenthal, C.; Jones, N. Chaos Engineering: System Resiliency in Practice. O'Reilly, 2020.
- Documentação do Resilience4j
- Documentação do Chaos Mesh
- AWS Fault Injection Service
- Nygard, M. Release It!: Design and Deploy Production-Ready Software. 2ª ed. Pragmatic Bookshelf, 2018.
Top comments (0)