Circuit breaker em comunicação entre serviços

1. Introdução ao Circuit Breaker

Em sistemas distribuídos, a comunicação entre serviços é inevitável e, com ela, surge o risco de falhas em cascata. Quando um serviço dependente apresenta lentidão ou indisponibilidade, as requisições acumuladas podem consumir recursos críticos (conexões de banco, threads, memória) e derrubar o serviço chamador. O circuit breaker é um padrão de resiliência que interrompe proativamente as chamadas a um serviço com falha, evitando que o problema se propague.

A analogia com circuitos elétricos é direta: assim como um disjuntor desarma quando a corrente excede um limite, o circuit breaker "abre" quando a taxa de falhas ultrapassa um limiar, protegendo o sistema contra sobrecarga. Diferente de estratégias como retry (que tenta novamente uma operação falha) ou timeout (que limita o tempo de espera), o circuit breaker atua como um bloqueio preventivo: em vez de continuar tentando e gastando recursos, ele rejeita requisições imediatamente por um período.

2. Estados e Ciclo de Vida do Circuit Breaker

O circuit breaker opera em três estados fundamentais:

  • Fechado (Closed): estado normal. As requisições fluem livremente, e o circuito monitora a taxa de falhas. Quando o número de falhas consecutivas ou a porcentagem de erros ultrapassa um limiar configurado (ex.: 5 falhas em 10 segundos), o circuito transita para o estado aberto.

  • Aberto (Open): o circuito rejeita todas as requisições imediatamente, sem tentar a chamada real. Isso libera recursos do serviço chamador. Após um período configurável (ex.: 30 segundos), o circuito transita para semiaberto. Durante esse tempo, a resposta padrão é um erro rápido ou um fallback.

  • Semiaberto (Half-Open): o circuito permite um número limitado de requisições de teste. Se essas requisições forem bem-sucedidas (dentro de um limiar, ex.: 3 sucessos consecutivos), o circuito volta ao estado fechado. Caso contrário, retorna ao estado aberto por mais um período.

As transições são governadas por parâmetros como:
- failureCount: número de falhas para abrir o circuito.
- timeout: tempo no estado aberto antes de tentar a recuperação.
- successThreshold: número de sucessos consecutivos no estado semiaberto para fechar o circuito.

3. Implementação Prática

Abaixo, um exemplo de circuit breaker simples em Python (usando uma classe genérica). O código é apresentado em bloco text conforme solicitado.

import time
import threading

class CircuitBreaker:
    def __init__(self, failure_count=5, recovery_timeout=30, success_threshold=3):
        self.failure_count = failure_count
        self.recovery_timeout = recovery_timeout
        self.success_threshold = success_threshold
        self.failures = 0
        self.state = "CLOSED"
        self.last_failure_time = 0
        self.successes = 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.successes = 0
                else:
                    raise Exception("Circuit is OPEN. Request rejected.")

        try:
            result = func(*args, **kwargs)
            with self.lock:
                if self.state == "HALF_OPEN":
                    self.successes += 1
                    if self.successes >= self.success_threshold:
                        self.state = "CLOSED"
                        self.failures = 0
                elif self.state == "CLOSED":
                    self.failures = 0  # reset on success
            return result
        except Exception as e:
            with self.lock:
                self.failures += 1
                self.last_failure_time = time.time()
                if self.failures >= self.failure_count:
                    self.state = "OPEN"
            raise e

# Uso hipotético com chamada HTTP
cb = CircuitBreaker(failure_count=3, recovery_timeout=10)
try:
    response = cb.call(requests.get, "http://servico-b:8080/api/dados")
except Exception as e:
    # fallback ou log
    print(f"Request failed: {e}")

Para integração com gRPC, o princípio é o mesmo: o interceptor de cliente gRPC pode envolver a chamada UnaryUnaryMultiCallable com a lógica do circuit breaker.

4. Estratégias de Resposta Durante o Estado Aberto

Quando o circuito está aberto, o serviço chamador não deve simplesmente lançar uma exceção — precisa oferecer uma resposta alternativa:

  • Fallback imediato: retornar um valor padrão (ex.: lista vazia, objeto nulo) ou dados de cache local. Exemplo: um serviço de catálogo que, ao não conseguir contatar o serviço de preços, retorna preços do último cache válido.

  • Degradação graciosa: desabilitar funcionalidades não essenciais. Por exemplo, um serviço de recomendação que, com o circuito aberto para o serviço de histórico, oferece recomendações genéricas em vez de personalizadas.

  • Notificação assíncrona: enviar um evento para uma fila (ex.: RabbitMQ, Kafka) para reativação manual ou automática quando o serviço alvo for restaurado.

5. Monitoramento e Métricas Essenciais

Sem monitoramento, o circuit breaker é uma caixa-preta. Métricas críticas incluem:

  • Taxa de falhas por minuto: antes e depois da abertura do circuito.
  • Tempo gasto em cada estado: ajuda a ajustar timeouts.
  • Throughput de requisições: comparar o volume normal com o volume durante o estado aberto.

Ferramentas como Prometheus + Grafana podem expor métricas do circuit breaker. Exemplo de métrica exportada:

# 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="servico-b"} 1.0

Logs devem registrar cada transição de estado para auditoria e debugging.

6. Armadilhas e Boas Práticas

  • Circuit breakers aninhados: se o serviço A chama B, e B chama C, e ambos têm circuit breakers, uma falha em C pode abrir o breaker de B, que por sua vez abre o de A. Isso pode mascarar a causa raiz. Solução: configurar timeouts e limiares de forma hierárquica, ou usar um breaker global no gateway.

  • Timeouts inconsistentes: o timeout do circuit breaker deve ser maior que o timeout da chamada HTTP/gRPC, mas menor que o SLA do serviço. Por exemplo, se o serviço B tem SLA de 2 segundos, o timeout da chamada deve ser 1,5s e o recovery_timeout do breaker, 10s.

  • Testes de caos: simular falhas parciais (ex.: latência alta, falhas intermitentes) para validar o comportamento do breaker. Ferramentas como Chaos Monkey ou Gremlin podem ser usadas.

7. Relação com Outros Padrões de Resiliência

O circuit breaker não opera isolado:

  • Retry policies: combinado com backoff exponencial, o retry pode ser usado antes de abrir o circuito. Exemplo: tentar 3 vezes com intervalo de 1s, depois abrir o breaker por 30s.

  • Bulkhead isolation: separa pools de threads ou conexões para diferentes serviços. Um bulkhead evita que uma falha em um serviço consuma todos os recursos do serviço chamador, complementando o circuit breaker.

  • Dead letter queues (DLQ): requisições rejeitadas durante o estado aberto podem ser enviadas para uma DLQ para processamento posterior ou análise manual.

Referências