wlls / devops / circuit-breaker-em-microsservios-quando-usar
devops

Circuit breaker em microsserviços: quando usar

wander·29 de set.·7 min de leitura

Premissas e limitações desta análise

Antes de entrar no conteúdo, uma declaração honesta: este post foi produzido sem uma pesquisa de fontes externas verificáveis. Não há benchmarks, incidentes reais ou números de nenhum ambiente específico sustentando as afirmações abaixo. O que você vai ler é uma explicação do padrão circuit breaker com base em conceitos amplamente documentados na literatura de engenharia de software (o padrão foi popularizado por Michael Nygard no livro Release It! e descrito por Martin Fowler em seu material sobre CircuitBreaker), e em como bibliotecas conhecidas do ecossistema — Resilience4j (Java), Polly (.NET) e pybreaker (Python) — implementam a ideia na prática.

Os exemplos de código aqui são ilustrativos e não foram executados neste ambiente. Trate-os como ponto de partida conceitual, não como configuração pronta para produção. Antes de aplicar qualquer coisa, valide contra a documentação oficial da biblioteca que você escolher e contra o comportamento real do seu sistema sob carga.

O problema que o circuit breaker resolve

Em uma arquitetura de microsserviços, cada chamada de rede é um ponto de falha. Se o serviço A depende do serviço B, e B começa a responder devagar ou a falhar, o problema não fica contido em B. Ele se propaga.

O sintoma clássico: threads ou conexões do serviço A ficam presas esperando resposta de B que nunca chega (ou demora demais). Sem limite, essas conexões se acumulam até esgotar o pool de recursos de A. Nesse ponto, A também fica indisponível — mesmo que o problema original estivesse isolado em B. Isso é falha em cascata, e é exatamente o cenário que motivou o padrão circuit breaker.

A causa raiz não é "B caiu". É "A não tem um mecanismo para parar de tentar falar com B quando fica claro que B não vai responder". Timeout sozinho ajuda, mas não resolve: se A continua tentando a cada requisição e cada tentativa espera o timeout inteiro antes de falhar, o custo agregado continua alto e os recursos continuam sendo consumidos.

Como funciona: os três estados

O circuit breaker é uma máquina de estados que fica entre o cliente (serviço A) e a chamada remota (serviço B):

  • Fechado (closed): comportamento normal. As chamadas passam direto para B. O breaker conta falhas e sucessos numa janela de tempo ou de requisições.
  • Aberto (open): quando a taxa de falha ultrapassa um limiar configurado, o breaker "abre". Novas chamadas para B falham imediatamente, sem sequer tentar a rede — retornando erro rápido ou acionando um fallback. Isso protege tanto A (não gasta recursos esperando) quanto B (não recebe tráfego enquanto está degradado, dando espaço para se recuperar).
  • Semiaberto (half-open): depois de um tempo configurado, o breaker deixa passar um número limitado de chamadas de teste. Se elas tiverem sucesso, ele volta para fechado. Se falharem, volta para aberto e reinicia o tempo de espera.

Esse ciclo é o que diferencia circuit breaker de um simples retry com timeout: ele tem memória do estado recente e reage a padrões, não a chamadas isoladas.

Quando faz sentido usar

Circuit breaker resolve um problema específico: dependências remotas instáveis que podem degradar em cascata. Ele faz sentido quando:

  • O serviço chama outro serviço (ou banco, fila, API externa) pela rede, e essa dependência tem histórico ou risco real de lentidão/indisponibilidade parcial.
  • Existe um fallback razoável — resposta em cache, valor default, degradação de funcionalidade — para quando o breaker está aberto. Sem fallback, o breaker só transforma "espera longa" em "erro rápido", o que já é uma melhoria, mas é bem menos valioso.
  • O volume de chamadas é alto o suficiente para que o acúmulo de recursos presos seja um risco real. Em chamadas raras e de baixo volume, o retorno sobre a complexidade adicionada é menor.

Quando não faz sentido (ou é prematuro)

  • Chamadas locais ou intra-processo. Circuit breaker é para dependências remotas com risco de latência e falha parcial. Aplicar o padrão em chamadas de função dentro do mesmo processo é complexidade sem benefício.
  • Sistemas com pouquíssimas dependências externas e baixo volume. Se você tem dois serviços conversando ocasionalmente, um timeout bem configurado e um retry com backoff podem ser suficientes. Adicionar um breaker antes de sentir o problema de cascata é otimização prematura.
  • Quando o verdadeiro problema é falta de timeout. Um erro comum é pular direto para circuit breaker sem primeiro garantir que toda chamada de rede tem timeout definido. Sem timeout, o breaker nem consegue contar falhas de forma confiável, porque as chamadas ficam penduradas indefinidamente.
  • Como substituto de retry ou de rate limiting. São padrões complementares, não intercambiáveis. Retry lida com falhas transitórias isoladas. Rate limiting protege contra excesso de volume. Circuit breaker lida com degradação sustentada de uma dependência.

Implementação prática (exemplo conceitual)

A maioria das linguagens tem biblioteca madura para isso — evite reimplementar a máquina de estados do zero. Alguns exemplos de como a configuração costuma se parecer.

Em Python, com pybreaker:

import pybreaker
import requests

# fail_max: quantas falhas consecutivas abrem o circuito
# reset_timeout: segundos em "aberto" antes de tentar half-open
breaker = pybreaker.CircuitBreaker(fail_max=5, reset_timeout=30)

@breaker
def chamar_servico_b():
    resposta = requests.get("http://servico-b/health", timeout=2)
    resposta.raise_for_status()
    return resposta.json()

def obter_dados():
    try:
        return chamar_servico_b()
    except pybreaker.CircuitBreakerError:
        # circuito aberto: retorna fallback em vez de tentar a rede
        return {"status": "degradado", "origem": "cache"}
    except requests.RequestException:
        # falha real de rede, conta para o breaker
        return {"status": "erro", "origem": "servico-b"}

Em Java, uma configuração equivalente com Resilience4j costuma ser feita via arquivo de configuração:

resilience4j:
  circuitbreaker:
    instances:
      servico-b:
        failureRateThreshold: 50
        waitDurationInOpenState: 30s
        slidingWindowSize: 20
        permittedNumberOfCallsInHalfOpenState: 5

Pontos que importam mais que a sintaxe da biblioteca:

  1. Defina o timeout da chamada antes do breaker. O breaker reage a falhas; sem timeout, uma chamada lenta nunca "falha", só fica pendurada.
  2. Escolha o limiar de falha com base em dado real, não em achismo. Se você não tem observabilidade sobre a taxa de erro atual da dependência, qualquer número aqui é chute.
  3. Trate CircuitBreakerError (ou equivalente) separado de erro de rede. São sinais diferentes: um diz "nem tentei", o outro diz "tentei e falhou".
  4. Tenha fallback definido antes de configurar o breaker, não depois. Se a resposta para "o que fazer quando abrir" é "não sei", o breaker só empurra a decisão para depois — ele não resolve isso sozinho.

Métricas e observabilidade

Um circuit breaker sem observabilidade é uma caixa preta que decide sozinha quando seu sistema degrada. No mínimo, exponha:

  • Estado atual do breaker por dependência (fechado/aberto/semiaberto), como métrica ou gauge.
  • Taxa de falha na janela corrente.
  • Contagem de transições de estado ao longo do tempo — abrir e fechar com muita frequência ("flapping") geralmente indica limiar mal calibrado, não instabilidade real da dependência.

Sem esses dados, é impossível diferenciar "o breaker está protegendo o sistema como esperado" de "o breaker está causando indisponibilidade artificial por estar mal configurado".

Erros comuns

  • Configurar limiar de falha igual para todas as dependências, ignorando que cada uma tem um perfil de latência e criticidade diferente.
  • Não testar o comportamento do fallback sob carga — descobrir em produção que o "modo degradado" também derruba o sistema.
  • Esquecer que o breaker é por instância/processo, a menos que a biblioteca ofereça estado compartilhado: em um cluster com várias réplicas, cada uma pode ter uma visão diferente do estado da dependência.
  • Tratar o breaker como solução de resiliência completa, sem combinar com timeout, retry com backoff e rate limiting onde fizer sentido.

Próximo passo

Antes de adicionar circuit breaker a um serviço, confirme duas coisas: que toda chamada remota já tem timeout definido, e que existe um fallback concreto para quando a dependência estiver fora do ar. Se qualquer uma dessas faltar, resolva isso primeiro — o breaker some em cima dessa base, não substitui ela.

#circuit-breaker#resilience4j#pybreaker#fallback#falha-em-cascata#retry-com-backoff