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