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
- Microsoft - Circuit Breaker Pattern (Azure Architecture Center) — Documentação oficial da Microsoft sobre o padrão, com diagramas de estados e exemplos em .NET.
- Martin Fowler - CircuitBreaker — Artigo seminal de Martin Fowler explicando o padrão com exemplos em Ruby.
- Resilience4j - Circuit Breaker Module (Java) — Documentação da biblioteca Resilience4j, com configuração detalhada de parâmetros e integração com Spring Boot.
- Netflix Hystrix - How it Works — Explicação do circuit breaker na biblioteca Hystrix da Netflix, com exemplos de métricas e fallbacks.
- AWS - Circuit Breaker Pattern (Well-Architected Framework) — Guia da AWS sobre implementação do padrão em arquiteturas cloud-native, com foco em resiliência.