Circuit breaker e resiliência

1. Introdução ao Circuit Breaker

Em sistemas distribuídos, falhas são inevitáveis. Um serviço pode ficar lento, indisponível ou retornar erros inesperados. O padrão Circuit Breaker — ou disjuntor — atua como um mecanismo de proteção que interrompe temporariamente as chamadas a um serviço com falhas, evitando que o sistema inteiro seja arrastado para baixo.

A analogia é direta com um disjuntor elétrico: quando a corrente ultrapassa um limite seguro, o disjuntor "abre" o circuito, interrompendo o fluxo. Em software, quando a taxa de falhas de uma chamada remota excede um threshold configurado, o circuito abre e as requisições subsequentes são recusadas imediatamente, sem tentar a chamada real.

Por que isso é crítico? Em arquiteturas de microsserviços, uma única falha pode se propagar em cascata. Se o Serviço A depende do Serviço B, e B começa a responder lentamente, A pode acumular threads e conexões, até exaurir seus próprios recursos. Esse efeito dominó é conhecido como falha em cascata e pode derrubar todo o ecossistema.

O Circuit Breaker não atua sozinho. Ele se combina com outros padrões de resiliência:

  • Retry: tentar novamente operações que falharam, mas com backoff exponencial
  • Timeout: limitar o tempo máximo de espera por uma resposta
  • Bulkhead: isolar recursos para que falhas em um componente não afetem outros

Juntos, esses padrões formam a base de um sistema resiliente.

2. Estados e Ciclo de Vida do Circuit Breaker

O Circuit Breaker clássico possui três estados:

Fechado (Closed)

  • Estado normal de operação
  • Todas as requisições são encaminhadas ao serviço alvo
  • Um contador de falhas é incrementado a cada erro
  • Quando o contador atinge o threshold de falhas (ex.: 5 falhas consecutivas), o circuito transita para Aberto

Aberto (Open)

  • Requisições são recusadas imediatamente, sem chamar o serviço
  • Uma exceção de circuito aberto é lançada ou um fallback é acionado
  • Permanece aberto por um período configurável (timeout duration)
  • Após esse período, transita para Meio-Aberto

Meio-Aberto (Half-Open)

  • Estado de teste: um número limitado de requisições é permitido
  • Se essas requisições forem bem-sucedidas, o circuito retorna a Fechado
  • Se falharem, o circuito volta a Aberto e o timeout é reiniciado

Parâmetros configuráveis típicos:

failureCountThreshold: 5
timeoutDuration: 10 segundos
halfOpenMaxRequests: 3
halfOpenRetryInterval: 5 segundos

A escolha desses parâmetros depende da criticidade do serviço e do comportamento esperado das falhas. Um serviço crítico pode ter thresholds mais baixos para reagir rapidamente; um serviço tolerante pode permitir mais falhas antes de abrir o circuito.

3. Implementação Prática do Circuit Breaker

Estrutura básica em Python (sem biblioteca)

import time
from enum import Enum

class CircuitState(Enum):
    CLOSED = 1
    OPEN = 2
    HALF_OPEN = 3

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=10, half_open_max=3):
        self.state = CircuitState.CLOSED
        self.failure_count = 0
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.half_open_max = half_open_max
        self.half_open_attempts = 0
        self.last_failure_time = None

    def call(self, func, *args, **kwargs):
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time >= self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
                self.half_open_attempts = 0
            else:
                raise Exception("Circuit is open")

        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise e

    def _on_success(self):
        if self.state == CircuitState.HALF_OPEN:
            self.half_open_attempts += 1
            if self.half_open_attempts >= self.half_open_max:
                self.state = CircuitState.CLOSED
                self.failure_count = 0
        else:
            self.failure_count = 0

    def _on_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()
        if self.failure_count >= self.failure_threshold:
            self.state = CircuitState.OPEN

Integração com chamadas HTTP e fallback

import requests

def call_external_api():
    response = requests.get("https://api.exemplo.com/dados", timeout=5)
    response.raise_for_status()
    return response.json()

def fallback_response():
    return {"status": "unavailable", "data": None}

breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=15)

try:
    result = breaker.call(call_external_api)
except Exception:
    result = fallback_response()

Uso com Resilience4j (Java)

// Configuração via código
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .waitDurationInOpenState(Duration.ofSeconds(10))
    .slidingWindowSize(5)
    .build();

CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
CircuitBreaker breaker = registry.circuitBreaker("servicoA");

// Decorando uma chamada
Supplier<String> decorated = CircuitBreaker.decorateSupplier(breaker, () -> {
    return restTemplate.getForObject("http://servico-a/api", String.class);
});

// Com fallback
String result = Try.ofSupplier(decorated)
    .recover(throwable -> "Fallback: serviço indisponível")
    .get();

4. Monitoramento e Métricas do Circuit Breaker

Um Circuit Breaker sem monitoramento é uma caixa-preta. Métricas essenciais incluem:

  • Taxa de falhas: percentual de chamadas que falharam na janela atual
  • Tempo médio de resposta: latência das chamadas bem-sucedidas
  • Estado atual: fechado, aberto ou meio-aberto
  • Número de requisições recusadas: quando o circuito está aberto
  • Tempo desde a última transição: ajuda a entender padrões de falha

Logging e alertas

# Exemplo de log estruturado
{
  "event": "circuit_state_change",
  "service": "servico-pagamentos",
  "from_state": "CLOSED",
  "to_state": "OPEN",
  "failure_count": 5,
  "timestamp": "2025-03-20T14:30:00Z"
}

Alertas devem ser configurados para:
- Circuito abre: notificar equipe de plantão
- Circuito permanece aberto por mais de X minutos: escalar para engenharia
- Circuito alterna entre estados com frequência: indicar instabilidade

Integração com Prometheus e Grafana

Bibliotecas como Resilience4j expõem métricas automaticamente via Micrometer. Um dashboard típico pode mostrar:

# Métricas expostas (Prometheus)
resilience4j_circuitbreaker_state{name="servicoA", state="closed"} 1
resilience4j_circuitbreaker_calls_total{name="servicoA", kind="success"} 120
resilience4j_circuitbreaker_calls_total{name="servicoA", kind="failure"} 8

5. Estratégias de Fallback e Degradação Graciosa

Quando o circuito abre, o sistema não deve simplesmente falhar. É aqui que entram as estratégias de fallback.

Fallback estático vs. dinâmico

Fallback estático: retorna uma resposta pré-definida, como um JSON com status de erro.

def fallback_estatico():
    return {"status": "error", "message": "Serviço temporariamente indisponível"}

Fallback dinâmico: utiliza dados em cache ou respostas parciais de fontes alternativas.

cache_local = {
    "ultimo_preco": 150.75,
    "timestamp": "2025-03-20T12:00:00Z"
}

def fallback_dinamico():
    if cache_local:
        return {"status": "degraded", "data": cache_local["ultimo_preco"]}
    return {"status": "unavailable"}

Degradação graciosa

Em vez de falhar completamente, o sistema reduz funcionalidades. Por exemplo:

  • Serviço de recomendações offline: exibe apenas produtos populares em vez de recomendações personalizadas
  • Serviço de pagamentos indisponível: permite apenas pagamentos com cartão (modo offline), bloqueando PIX e boleto
def obter_recomendacoes(usuario_id):
    try:
        return breaker.call(recomendacoes_api, usuario_id)
    except Exception:
        # Degradação: retorna recomendações genéricas
        return obter_produtos_mais_vendidos()

6. Armadilhas e Boas Práticas

Armadilhas comuns

  • Timeouts mal configurados: um timeout muito longo (ex.: 60s) pode fazer o circuito demorar para abrir, mantendo threads bloqueadas
  • Circuitos muito sensíveis: abrir após 2 falhas em uma janela de 1 minuto pode causar abertura desnecessária durante picos normais de erro
  • Circuitos muito insensíveis: permitir 100 falhas antes de abrir pode causar danos significativos ao sistema
  • Granularidade inadequada: um único circuito para todo um serviço pode mascarar falhas em endpoints específicos

Boas práticas

  • Granularidade correta: um Circuit Breaker por endpoint ou operação crítica, não por serviço inteiro
  • Testes de resiliência: usar chaos engineering para simular falhas (ex.: Netflix Chaos Monkey)
  • Configuração baseada em SLA: thresholds devem refletir o nível de serviço acordado
  • Monitoramento contínuo: ajustar parâmetros com base em dados reais de produção

7. Integração com Microsserviços e Ecossistema

Em arquiteturas de microsserviços, o Circuit Breaker é uma peça central da resiliência. Ele se integra com:

Service Discovery e Load Balancing

Quando um serviço está com o circuito aberto, o load balancer pode ser notificado para evitar rotear requisições para instâncias problemáticas. Combinado com health checks, o sistema pode remover automaticamente instâncias degradadas do pool.

# Exemplo com Kubernetes e service mesh (Istio)
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: servico-pagamentos
spec:
  host: servico-pagamentos
  trafficPolicy:
    connectionPool:
      http:
        http1MaxPendingRequests: 10
        maxRequestsPerConnection: 5
    outlierDetection:
      consecutive5xxErrors: 3
      interval: 30s
      baseEjectionTime: 30s

Relação com o padrão Strangler Fig

Durante a migração de um monolito para microsserviços, o Circuit Breaker ajuda a isolar falhas. Se o novo microsserviço falhar, o circuito abre e o fallback pode redirecionar para a funcionalidade legada, garantindo continuidade operacional.

def obter_dados_cliente(cliente_id):
    try:
        return breaker.call(novo_microsservico, cliente_id)
    except Exception:
        # Fallback para o monolito legado
        return monolito_legado.obter_dados(cliente_id)

Referências