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:
- Hipótese: o que esperamos que aconteça.
- Alvo e variável: o que será perturbado e como (tipo de falha, intensidade, duração).
- Estado estável: as métricas de controle e seus limites.
- Blast radius: quem e o que pode ser afetado.
- Condições de aborto: quando o experimento é interrompido.
- Observabilidade: onde olhar durante a execução.
- 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:
- Falhas lentas são traiçoeiras. Teste latência, não só queda total.
- Hipótese antes de ação. Uma carta preenchida obriga a pensar no que se espera e no que fazer se der errado.
- Blast radius pequeno primeiro. Confiança se constrói em degraus.
- Observabilidade é pré-requisito. Sem ela, o experimento não ensina nada.
- 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.
