Introdução: o dia em que o frete derrubou o checkout

Imagine uma loja virtual na Black Friday. O checkout está no ar, o banco está saudável, a CPU está tranquila. De repente, os pedidos começam a falhar. A causa? Um serviço de cotação de frete que não caiu, só ficou lento.

Essa é a pegadinha dos sistemas distribuídos: uma dependência lenta costuma ser mais perigosa que uma dependência morta. Se ela cai, você recebe um erro rápido. Se ela só demora, as threads ficam presas esperando, o pool de conexões enche e a lentidão se espalha pela cadeia até derrubar quem nem dependia dela.

É aqui que entra o Chaos Engineering: a prática de injetar falhas de forma controlada para descobrir como o sistema realmente se comporta, antes que um incidente real faça essa descoberta por você.

Por que quebrar coisas de propósito?

Porque a alternativa é quebrar sem querer, no pior horário possível. Um experimento de caos bem feito tem três características:

  • Hipótese clara: sabemos o que esperamos que aconteça.
  • Limites de segurança: sabemos até onde o impacto pode ir e quando parar.
  • Aprendizado registrado: cada execução vira melhoria concreta.

Quanto a staging versus produção: comece sempre em staging. Produção só entra depois de o time dominar o processo, com tráfego mínimo, aprovação explícita e um botão de interrupção já testado.

A mudança de mentalidade

Mentalidade tradicional Mentalidade de resiliência
"Como impedimos que isso falhe?" "Quando isso falhar, o que acontece com o usuário?"
Falha é exceção Falha é condição normal de operação
Descobrimos fragilidades em incidentes Descobrimos fragilidades em experimentos
Culpa-se a pessoa Investiga-se o sistema

Prevenir continua importante. Mas, além de prevenir, precisamos detectar, conter, degradar com elegância e recuperar.

Três conceitos que sustentam tudo

  • Estado estável: os indicadores que definem o "normal" (taxa de sucesso, latência p95, taxa de erros). Sem essa linha de base, você não sabe se o que viu foi efeito do experimento ou só oscilação.
  • Blast radius (raio de explosão): o alcance máximo permitido do impacto. Quanto menor no começo, mais seguro.
  • Observabilidade: métricas, logs e traces que permitem enxergar o que está acontecendo. Chaos sem observabilidade é só bagunça.

Dissecando o ChaosEngineeringMasterDeck

Pense num baralho em que cada carta é um experimento. A ideia é simples: em vez de planos soltos em um documento qualquer, cada falha que queremos testar vira uma carta com campos obrigatórios. Se um campo não está preenchido, a carta não pode ser jogada.

Uma carta do Master Deck tem estes campos:

  1. Hipótese: o que esperamos que aconteça.
  2. Alvo e variável: o que será perturbado e como (tipo de falha, intensidade, duração).
  3. Estado estável: as métricas de controle e seus limites.
  4. Blast radius: quem e o que pode ser afetado.
  5. Condições de aborto: quando o experimento é interrompido.
  6. Observabilidade: onde olhar durante a execução.
  7. Aprendizado: o que descobrimos e quais ações geram.

O deck não substitui ferramentas de monitoramento nem a revisão humana. Ele padroniza o raciocínio, para que qualquer pessoa do time consiga planejar e entender um experimento.

Estudo de caso: "Frete Lento"

A arquitetura

 ┌──────────────────┐ Cliente ──────► │ API de Pedidos │ ──────► Banco de dados │ (Spring Boot) │ └────────┬─────────┘ │ chamada HTTP ▼ ┌──────────────────┐ │ Serviço de Frete │ ◄── latência injetada aqui └──────────────────┘ 

A API de pedidos, em Spring Boot, consulta o serviço de frete para calcular o valor da entrega antes de confirmar o pedido. A falha escolhida é latência elevada no serviço de frete, sem derrubá-lo e sem alterar nenhum dado.

A carta jogada

carta: frete-lento hipotese: > Se o serviço de frete responder com latência alta, a API de pedidos vai interromper a espera por chamada, abrir o circuit breaker e usar uma tabela de frete padrão. O cliente consegue finalizar o pedido e o impacto não passa do grupo de teste. alvo: servico-de-frete variavel: tipo: latencia-adicional intensidade: acima do timeout configurado duracao_maxima: 5 minutos ambiente: staging blast_radius: usuarios: tráfego sintético de teste dados: nenhuma escrita fora do ambiente de staging estado_estavel: - taxa_de_sucesso_dos_pedidos - latencia_p95_do_checkout - taxa_de_erros_5xx - threads_ocupadas abortar_se: - taxa_de_sucesso_abaixo_do_limite_aprovado - erros_5xx_acima_do_limite - alerta_critico_de_saturacao 

Os limites numéricos devem sair dos SLOs do time. Não copie esta carta como configuração de produção.

Blast radius e salvaguardas

  • Ambiente: somente staging, sem clientes reais.
  • Alcance: apenas o serviço de frete; nenhum outro ambiente é tocado.
  • Duração: no máximo 5 minutos.
  • Responsável por abortar: definido antes de começar, com o mecanismo de interrupção testado.
  • Comunicação: o time avisado antes da execução.

Para levar o mesmo experimento à produção, o caminho seria: tráfego canário, percentual mínimo de usuários, janela combinada e aprovação explícita.

Os padrões de resiliência sob teste

Na API de pedidos usei Resilience4j com três proteções: circuit breaker, bulkhead e fallback. O timeout de 2 segundos fica no cliente HTTP.

resilience4j: circuitbreaker: instances: frete: slidingWindowType: COUNT_BASED slidingWindowSize: 20 slowCallDurationThreshold: 2s slowCallRateThreshold: 50 failureRateThreshold: 50 waitDurationInOpenState: 30s permittedNumberOfCallsInHalfOpenState: 5 bulkhead: instances: frete: maxConcurrentCalls: 10 maxWaitDuration: 0 
@Service public class FreteClient { private final RestClient restClient; // readTimeout configurado em 2s public FreteClient(RestClient restClient) { this.restClient = restClient; } @CircuitBreaker(name = "frete", fallbackMethod = "fretePadrao") @Bulkhead(name = "frete") public CotacaoFrete cotar(String cep, BigDecimal pesoKg) { return restClient.get() .uri("/frete?cep={cep}&peso={peso}", cep, pesoKg) .retrieve() .body(CotacaoFrete.class); } // Chamado quando o circuito está aberto, o bulkhead está cheio ou a chamada falha private CotacaoFrete fretePadrao(String cep, BigDecimal pesoKg, Throwable causa) { return CotacaoFrete.tabelaPadrao(cep, pesoKg); // valor estimado, marcado como tal } } 

Por que cada peça importa:

  • Timeout: impede que uma chamada espere para sempre.
  • Circuit breaker: depois de várias chamadas lentas, para de chamar o frete por um tempo e evita acumular espera.
  • Bulkhead: limita quantas chamadas ao frete podem ocorrer ao mesmo tempo, protegendo o restante das threads da aplicação.
  • Fallback: usa uma tabela de frete padrão. Atenção: só é válido porque o valor é uma estimativa claramente identificada. Um fallback que entrega dado errado como se fosse certo é pior que um erro.

Duas ressalvas importantes: retentativas sem limite podem transformar lentidão em avalanche, e Auto Scaling não resolve uma dependência lenta, podendo até aumentar a pressão sobre ela.

Observabilidade e resultados

Como a falha foi monitorada

  • Métricas (Micrometer + Prometheus + Grafana): taxa de sucesso, latência p95, erros 5xx, estado do circuit breaker e threads ocupadas.
  • Traces distribuídos: mostram em qual salto da cadeia o tempo está sendo gasto.
  • Logs: registram cada abertura do circuito e cada uso do fallback.
  • Alertas: confirmam se o time seria avisado sem precisar olhar o painel.

Resultado ilustrativo

Momento O que esperamos observar
Antes da falha Estado estável: sucesso alto, p95 baixo, circuito fechado
Primeiros segundos p95 do checkout sobe; chamadas ao frete batem no timeout
Após ~10 chamadas lentas Circuit breaker abre; fallback assume
Durante a falha Pedidos continuam sendo finalizados com frete estimado; p95 volta perto do normal
Fim da falha Circuito vai para meio-aberto, testa algumas chamadas e fecha

A arquitetura sobreviveu? Na hipótese, sim, mas com degradação: o cliente continua comprando, só que com frete estimado em vez do valor exato. A arquitetura degradou em vez de quebrar, que é exatamente o comportamento que queríamos validar.

Isso não prova resiliência em todos os cenários. Numa execução real, você registra as métricas observadas, compara com os SLOs e documenta qualquer desvio.

E se o resultado for ruim?

Ótimo, foi para isso que o experimento serviu. Algumas descobertas comuns:

  • timeout maior do que o previsto, acumulando threads;
  • alerta que dispara tarde demais, ou que nem dispara;
  • fallback que não cobre todos os fluxos;
  • painel que mostra sintomas, mas não aponta o frete como causa.

Cada achado vira uma tarefa priorizada no backlog, não só uma linha no relatório.

Conclusão: resiliência é prática, não promessa

O que levei dessa experiência:

  1. Falhas lentas são traiçoeiras. Teste latência, não só queda total.
  2. Hipótese antes de ação. Uma carta preenchida obriga a pensar no que se espera e no que fazer se der errado.
  3. Blast radius pequeno primeiro. Confiança se constrói em degraus.
  4. Observabilidade é pré-requisito. Sem ela, o experimento não ensina nada.
  5. O resultado é o aprendizado, não o "sistema não caiu".

No plano cultural, a prática muda a conversa entre desenvolvimento e operações. Em vez de procurar culpados depois do incidente, o time investiga o sistema junto, com evidências, e melhora as defesas antes do problema aparecer.

Se você quer começar hoje: escolha uma dependência do seu sistema, escreva uma hipótese, defina o estado estável e prepare a forma de interromper antes de injetar a falha. O caos só é útil quando é controlado.

Já rodou algum experimento de caos no seu time? Conta nos comentários qual foi a maior surpresa.