Circuit breaker e retry patterns: resiliência em chamadas entre serviços
1. Fundamentos da Resiliência em Microservices
1.1. O problema das falhas em cascata (cascading failures) em arquiteturas distribuídas
Em sistemas distribuídos, uma falha em um único serviço pode se propagar rapidamente para outros serviços dependentes. Esse fenômeno é conhecido como falha em cascata. Imagine um serviço de catálogo que depende de um serviço de inventário. Se o inventário fica lento, o catálogo acumula conexões abertas, consome mais threads e eventualmente para de responder. Outros serviços que dependem do catálogo também são afetados, criando um efeito dominó.
1.2. Diferença entre resiliência e tolerância a falhas
- Tolerância a falhas: o sistema continua operando perfeitamente mesmo quando componentes falham. Exige redundância completa.
- Resiliência: o sistema reconhece que falhas ocorrerão e se adapta para continuar funcionando de forma degradada, mas aceitável.
Na prática, resiliência é mais realista e econômica para arquiteturas de microservices.
1.3. Visão geral dos padrões de resiliência
Os principais padrões incluem:
- Retry: repetir chamadas que falharam por razões transitórias
- Circuit Breaker: interromper chamadas para evitar sobrecarga
- Timeout: limitar o tempo de espera por uma resposta
- Bulkhead: isolar recursos para que falhas em um componente não afetem outros
- Fallback: fornecer respostas alternativas quando a chamada principal falha
2. Retry Pattern: Tentativas Inteligentes de Repetição
2.1. Retry simples vs. retry exponencial com jitter
O retry simples repete a chamada imediatamente após a falha. Isso pode sobrecarregar o serviço downstream. A abordagem recomendada é o exponential backoff com jitter:
Função calcularEspera(tentativa, baseMs, maxMs):
espera = min(baseMs * 2^tentativa, maxMs)
jitter = random(0, espera * 0.5)
retorne espera + jitter
2.2. Quando usar retry
- Falhas transitórias (timeouts de rede, conexões temporárias, throttling): retry é seguro
- Falhas permanentes (erro 400 Bad Request, 500 Internal Server Error com causa lógica): retry é inútil e pode agravar o problema
2.3. Implementação prática
Configuração:
maxTentativas: 3
baseDelayMs: 200
maxDelayMs: 2000
Fluxo:
1. tentativa = 0
2. Enquanto tentativa < maxTentativas:
resposta = chamarServico()
Se resposta.sucesso:
retorne resposta
Se resposta.erro for permanente:
lance exceção
tentativa++
delay = calcularEspera(tentativa, baseDelayMs, maxDelayMs)
aguardar(delay)
3. Lance exceção de falha permanente
3. Circuit Breaker Pattern: Protegendo o Sistema de Sobrecarga
3.1. Estados do circuit breaker
- Closed: circuito fechado, chamadas fluem normalmente
- Open: circuito aberto, chamadas são rejeitadas imediatamente
- Half-Open: após um tempo de espera, permite algumas chamadas para testar recuperação
3.2. Métricas de ativação
Thresholds típicos:
- Taxa de falhas: 50% em uma janela de 10 segundos
- Mínimo de chamadas para avaliação: 5
- Tempo de espera no estado Open: 30 segundos
- Número de chamadas de teste no Half-Open: 3
3.3. Comportamento em cada estado
Estado Closed:
- Chamadas permitidas
- Monitora taxa de falhas
- Se taxa > threshold: transita para Open
Estado Open:
- Chamadas rejeitadas imediatamente (lança exceção ou fallback)
- Após timeout: transita para Half-Open
Estado Half-Open:
- Permite número limitado de chamadas de teste
- Se sucesso: transita para Closed
- Se falha: retorna para Open
4. Integração entre Retry e Circuit Breaker: Estratégias Combinadas
4.1. Retry dentro do Circuit Breaker
A ordem correta é: primeiro aplicar retry, depois circuit breaker. O retry tenta lidar com falhas transitórias antes que o circuit breaker seja acionado.
4.2. Exemplo de fluxo combinado
1. Iniciar chamada
2. Aplicar retry (exponencial backoff, max 3 tentativas)
3. Se todas as tentativas falharem:
- Circuit breaker registra falha
- Se taxa de falhas > threshold: circuito abre
4. Próximas chamadas:
- Se circuito aberto: fallback imediato
- Se circuito fechado: retorna ao passo 1
4.3. Cuidados importantes
- Não fazer retry quando o circuito está aberto: isso anularia a proteção
- Configurar timeouts adequados: retry com timeout longo atrasa a abertura do circuito
- Idempotência: garantir que múltiplas tentativas não causem efeitos colaterais
5. Fallback Patterns: Respostas Alternativas quando Tudo Falha
5.1. Tipos de fallback
- Fallback estático: resposta fixa (ex: "Serviço temporariamente indisponível")
- Fallback dinâmico: dados de cache, versão degradada do serviço
5.2. Stale data como fallback
Exemplo: serviço de preços
1. Tentar obter preço atual do serviço principal
2. Se falhar: usar último preço conhecido (cache)
3. Se cache vazio: usar preço padrão do catálogo
5.3. Hierarquia de fallbacks
Ordem de tentativa:
1. Serviço primário (com retry + circuit breaker)
2. Serviço secundário (réplica)
3. Cache local (dados recentes)
4. Cache global (dados mais antigos)
5. Resposta genérica (mensagem de erro amigável)
6. Implementação Prática com Bibliotecas e Frameworks
6.1. Conceitos comuns em bibliotecas
Bibliotecas como Resilience4j (Java), Polly (.NET) e Opossum (Node.js) implementam esses padrões de forma consistente:
Configuração típica:
- CircuitBreaker:
failureRateThreshold: 50
waitDurationInOpenState: 30s
slidingWindowSize: 10
minimumNumberOfCalls: 5
- Retry:
maxAttempts: 3
waitDuration: 200ms
exponentialBackoffMultiplier: 2
6.2. Monitoramento
Eventos a monitorar:
- CircuitBreaker.onOpen: circuito abriu
- CircuitBreaker.onClose: circuito fechou
- Retry.onRetry: tentativa de retry
- Fallback.onFallback: fallback acionado
7. Armadilhas e Boas Práticas em Produção
7.1. Não usar retry em chamadas não-idempotentes
Evitar retry para:
- Criação de registros (POST sem idempotência)
- Transferências financeiras
- Envio de emails
Preferir retry para:
- Consultas (GET)
- Atualizações idempotentes (PUT com versão)
- Deleções
7.2. Cuidado com timeouts
Problema: timeout de 30s + retry de 3 tentativas = 90s de bloqueio
Solução: timeout de 2s + retry exponencial (máx 6s total)
7.3. Testes de resiliência
Técnicas de chaos engineering:
- Simular latência em serviços downstream
- Derrubar serviços aleatoriamente
- Injetar erros HTTP específicos
- Testar cenários de rede instável
8. Conclusão e Próximos Passos
8.1. Resumo dos padrões
- Retry: para falhas transitórias, com backoff exponencial e jitter
- Circuit Breaker: proteção contra sobrecarga, com estados Closed/Open/Half-Open
- Fallback: continuidade de serviço com respostas alternativas
8.2. Relação com outros temas
- Event Sourcing: fallback com stale data se alinha com replay de eventos
- Twelve-Factor App: configuração externalizada para thresholds de resiliência
- Chaos Engineering: validação prática dos patterns em produção
8.3. Checklist de resiliência
☐ Retry configurado com exponential backoff e jitter
☐ Circuit breaker com thresholds adequados ao SLA
☐ Fallback implementado para todos os serviços críticos
☐ Timeouts definidos e menores que o tempo de abertura do circuito
☐ Chamadas idempotentes validadas
☐ Monitoramento de eventos de resiliência ativo
☐ Testes de chaos engineering em ambiente de staging
Referências
- Resilience4j Documentation — Documentação oficial da biblioteca Resilience4j para Java, com exemplos de Circuit Breaker, Retry, TimeLimiter e Bulkhead
- Microsoft Polly Documentation — Documentação oficial da biblioteca Polly para .NET, incluindo políticas de retry, circuit breaker e fallback
- Martin Fowler - CircuitBreaker — Artigo clássico de Martin Fowler explicando o padrão Circuit Breaker com exemplos práticos
- AWS - Retry Patterns — Artigo da AWS sobre exponential backoff e jitter para melhorar a eficiência de retry em sistemas distribuídos
- Google SRE - Handling Cascading Failures — Capítulo do livro Google SRE sobre como lidar com falhas em cascata, incluindo circuit breakers e load shedding
- Netflix TechBlog - Resilience Engineering — Artigo do Netflix Engineering sobre práticas de resiliência em sistemas de alta escala
- Microsoft - Retry Pattern — Documentação do Azure Architecture Center sobre o padrão Retry com orientações para implementação em nuvem