Como implementar circuit breaker em microservices

1. Fundamentos do Circuit Breaker em Microservices

O padrão Circuit Breaker é um mecanismo de tolerância a falhas projetado para proteger sistemas distribuídos contra falhas em cascata. Inspirado nos disjuntores elétricos, ele monitora chamadas remotas e interrompe automaticamente as requisições quando a taxa de falhas ultrapassa um limite predefinido. Seu propósito principal é evitar que um serviço sobrecarregado ou indisponível consuma recursos desnecessários, permitindo que o sistema se recupere gradualmente.

Os estados clássicos do Circuit Breaker são:

  • Fechado (Closed): Estado normal, onde as requisições fluem livremente. Falhas são contadas até atingir um limite.
  • Aberto (Open): Quando o limite de falhas é atingido, o circuito abre e todas as requisições são rejeitadas imediatamente, sem tentar a chamada real.
  • Semi-aberto (Half-Open): Após um tempo de espera, o circuito permite um número limitado de requisições de teste para verificar se o serviço se recuperou.

É crucial diferenciar Circuit Breaker de outros padrões: Retry tenta repetir uma operação que falhou, enquanto Timeout define um limite de tempo para uma chamada. O Circuit Breaker atua em um nível mais alto, prevenindo chamadas desnecessárias quando um serviço está claramente degradado.

2. Identificando Cenários Críticos para Aplicação

Os cenários mais comuns que demandam Circuit Breaker incluem:

  • Dependências externas instáveis: APIs de terceiros, gateways de pagamento, provedores de autenticação.
  • Serviços com latência variável: Bancos de dados sob carga, filas de mensagens congestionadas.
  • Cadeias de chamadas encadeadas: Quando o Serviço A chama o B, que chama o C. Uma falha em C pode travar toda a cadeia.

O efeito cascata é particularmente perigoso: se o Serviço A fica esperando uma resposta do B (que está lento), os threads de A se esgotam, causando falhas em cascata para outros serviços que dependem de A.

3. Projetando a Lógica de Transição de Estados

A transição entre estados depende de parâmetros bem definidos:

Thresholds de falha:
- Contagem absoluta: Exemplo: 5 falhas consecutivas.
- Janela de tempo: Exemplo: 10 falhas em 60 segundos.
- Taxa de erro: Exemplo: 50% de falhas nos últimos 100 requests.

Tempo de espera no estado Aberto: Geralmente entre 5 e 30 segundos, dependendo do tempo esperado de recuperação do serviço.

Estratégia para estado Semi-aberto:
- Número de requisições de sonda: Exemplo: 3 requisições de teste.
- Contagem de sucessos para fechar: Se todas as 3 sondas forem bem-sucedidas, o circuito fecha.
- Se alguma sonda falhar, o circuito volta ao estado Aberto e reinicia o timer.

4. Implementando o Circuit Breaker com Código Genérico

Abaixo, um exemplo de implementação simples em Python usando contadores e timers:

import time
import threading

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=10, half_open_max_requests=3):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.half_open_max_requests = half_open_max_requests
        self.state = 'CLOSED'
        self.failure_count = 0
        self.last_failure_time = None
        self.half_open_requests = 0
        self.lock = threading.Lock()

    def call(self, func, *args, **kwargs):
        with self.lock:
            if self.state == 'OPEN':
                if time.time() - self.last_failure_time > self.recovery_timeout:
                    self.state = 'HALF_OPEN'
                    self.half_open_requests = 0
                else:
                    raise Exception('Circuit breaker is OPEN')

            if self.state == 'HALF_OPEN':
                if self.half_open_requests >= self.half_open_max_requests:
                    raise Exception('Circuit breaker is HALF_OPEN, max test requests reached')
                self.half_open_requests += 1

        try:
            result = func(*args, **kwargs)
            with self.lock:
                if self.state == 'HALF_OPEN':
                    self.state = 'CLOSED'
                    self.failure_count = 0
                elif self.state == 'CLOSED':
                    self.failure_count = 0
            return result
        except Exception as e:
            with self.lock:
                self.failure_count += 1
                self.last_failure_time = time.time()
                if self.failure_count >= self.failure_threshold:
                    self.state = 'OPEN'
            raise e

# Exemplo de uso
cb = CircuitBreaker(failure_threshold=3, recovery_timeout=5)

def chamada_remota():
    # Simula uma chamada que falha
    raise ConnectionError('Serviço indisponível')

for i in range(5):
    try:
        cb.call(chamada_remota)
    except Exception as e:
        print(f"Tentativa {i+1}: {e}")

Para integração com bibliotecas reais, Resilience4j (Java) e Polly (.NET) são amplamente utilizadas. Exemplo com Resilience4j:

// Configuração em Java com Resilience4j
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .waitDurationInOpenState(Duration.ofSeconds(10))
    .slidingWindowSize(100)
    .build();

CircuitBreaker circuitBreaker = CircuitBreaker.of("meu-servico", config);

// Uso com decorador
Supplier<String> supplier = () -> chamadaRemota();
Supplier<String> decorated = CircuitBreaker.decorateSupplier(circuitBreaker, supplier);
String resultado = Try.ofSupplier(decorated).get();

5. Configuração e Políticas de Timeout e Retry

Combinar Circuit Breaker com Timeout é essencial: um timeout impede que uma chamada fique pendente indefinidamente, enquanto o Circuit Breaker evita que chamadas sejam tentadas quando o serviço está instável.

Políticas de retry inteligentes:
- Backoff exponencial: espera 1s, 2s, 4s, 8s entre tentativas.
- Jitter: adiciona aleatoriedade para evitar tempestade de retries.

Ordem de execução: Geralmente, o Retry deve ser executado antes do Circuit Breaker. Isso permite que falhas temporárias sejam resolvidas com retries, sem abrir o circuito. Se o retry falhar, aí sim o Circuit Breaker conta a falha.

6. Monitoramento e Métricas Essenciais

Métricas fundamentais para coletar:

  • Taxa de falha: Percentual de chamadas que falham.
  • Tempo de abertura: Duração que o circuito permanece aberto.
  • Requisições rejeitadas: Número de chamadas bloqueadas pelo circuito.
  • Estado atual: Fechado, Aberto ou Semi-aberto.

Para expor em dashboards (Prometheus + Grafana), utilize exporters específicos. Exemplo de métrica exposta:

# HELP circuit_breaker_state Estado atual do circuit breaker (0=CLOSED, 1=OPEN, 2=HALF_OPEN)
# TYPE circuit_breaker_state gauge
circuit_breaker_state{service="payment-api"} 0
circuit_breaker_failure_rate{service="payment-api"} 0.23
circuit_breaker_rejected_requests_total{service="payment-api"} 150

Alertas devem ser configurados para:
- Mudança de estado para OPEN.
- Taxa de falha acima de 50% por mais de 5 minutos.
- Número excessivo de requisições rejeitadas.

7. Tratamento de Falhas e Fallbacks

Estratégias de fallback comuns:

  • Resposta padrão: Retornar um valor default (ex: lista vazia, preço padrão).
  • Cache: Usar dados em cache de respostas anteriores bem-sucedidas.
  • Degradação funcional: Desabilitar funcionalidades não essenciais.

Exemplo de fallback com Resilience4j:

Supplier<String> fallback = () -> "Resposta de fallback";
Supplier<String> decorated = CircuitBreaker.decorateSupplier(circuitBreaker, supplier)
    .withFallback(fallback);

Logs estruturados são vitais para debugging:

{
  "event": "circuit_breaker_opened",
  "service": "payment-gateway",
  "failure_count": 5,
  "timestamp": "2025-01-15T10:30:00Z"
}

8. Boas Práticas e Armadilhas Comuns

Boas práticas:
- Isolamento por endpoint ou operação: Cada dependência externa deve ter seu próprio Circuit Breaker.
- Testes de caos: Simule falhas em produção para validar o comportamento.
- Configuração baseada em SLA: Ajuste thresholds conforme o tempo de resposta esperado.

Armadilhas comuns:
- Circuit breakers aninhados: Se o Serviço A tem um CB para B, e B tem um CB para C, uma falha em C pode abrir ambos, criando loops de falha.
- Thresholds muito baixos: Podem abrir o circuito com poucas falhas, causando falsos positivos.
- Esquecer de resetar contadores: Após recuperação, os contadores devem ser zerados.

Implementar Circuit Breaker corretamente é uma das práticas mais eficazes para aumentar a resiliência de sistemas baseados em microservices. Quando combinado com monitoramento adequado e estratégias de fallback, ele transforma falhas inevitáveis em eventos controlados, mantendo a experiência do usuário estável.

Referências