Sobrevivendo ao Caos: Injetando Falhas com o ChaosEngineeringMasterDeck

# software# architecture# tools# softwaredevelopment
Sobrevivendo ao Caos: Injetando Falhas com o ChaosEngineeringMasterDeckDherick

"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.


Sumário

  1. O que é Chaos Engineering (e por que quebrar coisas de propósito)
  2. Dissecando o ChaosEngineeringMasterDeck
  3. Estudo de caso: a carta "Parceiro Lento"
  4. Observabilidade e resultados
  5. 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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Game Days: sessões em que o time "compra" uma carta, executa o experimento e discute o resultado.
  2. 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)
Enter fullscreen mode Exit fullscreen mode

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

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

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)

  1. 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-size de 20 para 10) fez o breaker reagir mais rápido, e anotamos essa melhoria para a próxima rodada.
  2. 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.
  3. 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

  1. Resiliência é uma hipótese até ser testada. Ter Circuit Breaker configurado não prova que ele funciona como você imagina.
  2. Comece pequeno. Um blast radius reduzido permite errar barato e aprender rápido.
  3. Observabilidade vem primeiro. Se você não consegue ver o efeito da falha, não consegue aprender com ela.
  4. Falhas de negócio importam tanto quanto as técnicas. O fallback "pagamento pendente" só existe porque o time de produto participou da conversa.
  5. 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:

  1. Escolha um serviço crítico e defina seu estado estável.
  2. Escreva uma carta com hipótese, blast radius e condição de aborto.
  3. Execute em staging com um grupo pequeno, com o painel de métricas aberto.
  4. Documente o que aprendeu e corrija o que falhou.
  5. 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

Gostou? Deixe nos comentários qual carta você colocaria no seu baralho. 🃏