DEV Community

Sobrevivendo ao Caos: Injetando Falhas com o ChaosEngineeringMasterDeck

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:

  1. O que é Chaos Engineering (e por que quebrar coisas de propósito)
  2. O que é o Master Deck e como ele estrutura um experimento
  3. Um estudo de caso: Carta #07 – Latência Venenosa em um microsserviço Spring Boot na AWS
  4. Observabilidade, resultados e padrões de resiliência em ação
  5. 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:

  1. Padronização: todo experimento segue a mesma estrutura, o que facilita revisão e comparação.
  2. Descoberta: a equipe "puxa uma carta" nos Game Days, o que torna o exercício leve e inclusivo, e não um ritual temido.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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>"
Enter fullscreen mode Exit fullscreen mode

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     │
 └──────────────┘    └──────────────┘    └──────────────┘
Enter fullscreen mode Exit fullscreen mode

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       │
              └────────────────┘   └────────────────┘
Enter fullscreen mode Exit fullscreen mode

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)"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

💡 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
Enter fullscreen mode Exit fullscreen mode
@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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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 traceId correlacionado, 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]))
Enter fullscreen mode Exit fullscreen mode

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-estoque lento consumisse todas as threads do api-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

  1. Falhas lentas são piores que falhas rápidas. Timeouts, circuit breakers e bulkheads precisam ser tratados como requisitos, não como "otimização".
  2. 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.
  3. Hipóteses refutadas são vitórias. Cada surpresa descoberta em staging é um incidente a menos em produção.
  4. Sem observabilidade, não há experimento. Se você não consegue medir o estado estável, não consegue saber se quebrou.
  5. 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:

  1. Escolha um fluxo crítico e defina o estado estável dele.
  2. Garanta observabilidade mínima (métricas + traces).
  3. Escreva a sua primeira carta com hipótese, blast radius e condição de aborto.
  4. Rode em staging, com a equipe reunida (um Game Day).
  5. Corrija, documente e jogue a carta de novo.
  6. 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)