02 - Circuit Breaker
video: https://youtu.be/... no frontmatter da nota.Circuit Breaker
Circuit breaker é o padrão de interromper automaticamente as chamadas a uma dependência quando a taxa de falha dessa dependência ultrapassa um limite tolerável, devolvendo o tráfego apenas após sinais de recuperação. O nome vem do disjuntor elétrico, que abre o circuito para proteger a instalação quando a corrente excede o limite e precisa ser rearmado depois. Em produção, o padrão protege o serviço chamador do efeito mais perigoso da integração distribuída: gastar seus próprios recursos, threads, conexões e filas, tentando conversar com um componente que não responde. A relevância aparece em todo incidente em cascata, em que a lentidão de uma dependência derruba uma cadeia inteira de serviços que ficaram esperando. Implementado com bom juízo, o padrão converte falha lenta e dolorosa em falha rápida e barata, com resposta alternativa enquanto o destino se recupera.
Conceito e analogia do disjuntor
O disjuntor elétrico não conserta a rede; ele apenas impede que a sobrecarga destrua a instalação, e permanece aberto até alguém ou algum mecanismo verificar que a condição passou. O circuit breaker de software replica exatamente essa lógica: mede o comportamento das chamadas recentes, abre o circuito quando o padrão indica dependência doente e fecha quando uma sondagem controlada mostra recuperação.
A analogia fornece o vocabulário de estados que virou convenção universal, herdada do artigo original de Michael Nygard: closed, open e half-open. Todo framework sério de resiliência, em qualquer linguagem, expõe esses três nomes, e a equipe que domina o vocabulário lê a configuração de qualquer biblioteca sem surpresas.
O problema que resolve: falha em cascata
Falha em produção raramente chega como exceção limpa; chega como lentidão. Um serviço de pagamento que passa de 100 ms para 30 segundos por chamada não derruba apenas a si mesmo: cada chamador mantém threads ocupadas, esgota pools de conexão e empilha requisições em fila até saturar memória. Em minutos, todos os serviços que dependem dele estão doentes, e depois os serviços que dependem dos chamadores, em uma propagação que ninguém desenhou, mas todos pagam.
O mecanismo de amplificação é o recurso compartilhado. Sem circuit breaker, a única proteção de um chamador é o timeout, e o timeout por si só é insuficiente: durante os segundos de espera, a thread fica bloqueada, e a taxa de chamadas simultâneas garante que todas as threads morram juntas. O circuit breaker adiciona o que o timeout não tem, que é memória: a história das falhas recentes muda o comportamento futuro, e o chamador deixa de pagar o preço de redescobrir, requisição após requisição, que o destino está fora.
O padrão também protege a dependência doente de seus salvadores. Retries sincronizados de milhares de clientes são uma das causas de destino que não volta a pé; cortar o tráfego de forma abrupta e temporária é exatamente o alívio que o componente precisa para se recuperar, e é por isso que abrir o circuito frequentemente acelera a volta do destino em vez de apenas esconder o problema.
Estados: closed, open e half-open
O funcionamento gira em torno de uma máquina de estados com três situações bem definidas, cada uma com comportamento e regras de transição próprios.
| Estado | Comportamento das chamadas | Transição de saída |
|---|---|---|
| Closed | Tráfego normal, todas as falhas e latências são registradas na janela | Taxa de falha acima do limiar na janela: transita para Open |
| Open | Nenhuma chamada alcança o destino; todas falham de imediato com erro próprio | Expirado o tempo de espera configurado: transita para Half-Open |
| Half-Open | Pequeno número de sondas controladas alcança o destino; o restante falha rápido | Sondas bem sucedidas: volta a Closed; alguma sonda falha: retorna a Open |
No estado closed o circuito é invisível para o negócio: as chamadas fluem e a única diferença é a contabilidade de falhas. O estado open é o momento de proteção máxima, em que o erro local, tipicamente uma exceção específica como CallNotPermittedException, substitui o erro remoto e permite resposta alternativa imediata. O half-open é o mais delicado, porque precisa sondar sem sobrecarregar: por isso libera um número pequeno de chamadas, e não o tráfego inteiro de uma vez.
Um detalhe de implementação separa amadores de profissionais: a transição automática de open para half-open. Sem ela, alguém precisa rearmar o circuito manualmente, e o sistema volta durante madrugadas e fins de semana a depender de vigilância humana. Bibliotecas maduras fazem a transição por relógio, e o papel do operador se resume a configurar o tempo de espera.
Limiares e janela de avaliação
A decisão de abrir exige duas escolhas de configuração, e ambas são estatísticas antes de serem engenharia. A janela de avaliação define o histórico considerado, em contagem de chamadas, por exemplo as últimas vinte, ou em tempo, por exemplo os últimos sessenta segundos. Janela pequena reage rápido e abre por ruído; janela grande reage tarde e deixa o incidente crescer. Em geral, vinte a cinquenta chamadas são suficientes para separar ruído de tendência sem tarde demais.
O limiar de falha define a porcentagem que dispara a abertura, e cinquenta por cento é o valor de partida comum. Números mais baixos, como vinte por cento, servem para dependências com SLO rígido e chamadas baratas; números mais altos evitam abrir por instabilidade tolerável. Além da taxa de falha, bibliotecas modernas permitem considerar chamadas lentas como falha, com limiar de duração e taxa de lentidão próprios, o que captura o cenário mais perigoso em produção: o destino responde, mas em trinta segundos.
Configuração única para todas as dependências é outro sinal de amadorismo. Uma consulta a cache local tolera pouco; uma API externa de pagamento tolera mais. Cada dependência merece janela, limiar e tempo de espera derivados do seu contrato de latência e do custo de ficar sem ela.
Funcionamento interno
A implementação é uma máquina de estados finita com contabilidade de falhas em janela deslizante; o esqueleto abaixo mostra a mecânica completa em poucas dezenas de linhas.
import time
class CircuitBreaker:
# Estados clássicos do padrão, na convenção universal
FECHADO, ABERTO, MEIO_ABERTO = "closed", "open", "half-open"
def __init__(self, limite_falhas=5, janela_s=60,
espera_aberto_s=30, sondas=3):
self.estado = self.FECHADO
self.falhas = [] # timestamps das falhas dentro da janela
self.limite_falhas = limite_falhas
self.janela_s = janela_s # janela deslizante de avaliação
self.espera_aberto_s = espera_aberto_s
self.aberto_em = None
self.sondas_restantes = sondas
def permitir(self) -> bool:
agora = time.monotonic()
if self.estado == self.ABERTO:
# expirou a espera: liberar sondas controladas no half-open
if agora - self.aberto_em >= self.espera_aberto_s:
self.estado = self.MEIO_ABERTO
self.sondas_restantes = 3
return True
return False # falha rápida, sem tocar o destino
if self.estado == self.MEIO_ABERTO:
# apenas as sondas autorizadas passam; o restante falha rápido
if self.sondas_restantes > 0:
self.sondas_restantes -= 1
return True
return False
# fechado: descartar falhas que saíram da janela deslizante
self.falhas = [t for t in self.falhas if agora - t <= self.janela_s]
return True
def registrar_sucesso(self):
self.falhas.clear()
if self.estado == self.MEIO_ABERTO:
self.estado = self.FECHADO # destino confirmou recuperação
def registrar_falha(self):
if self.estado == self.MEIO_ABERTO:
self.abrir(); return # sonda falhou; voltar a abrir
self.falhas.append(time.monotonic())
if len(self.falhas) >= self.limite_falhas:
self.abrir()
def abrir(self):
self.estado = self.ABERTO
self.aberto_em = time.monotonic()
Implementações reais adicionam thread safety, métricas de transição, listeners de evento e limiar em porcentagem em vez de contagem absoluta, mas a espinha dorsal é essa: perguntar se pode passar, executar e registrar o resultado. Em ambientes com muitas instâncias, cada réplica mantém seu próprio circuito, o que é aceitável na prática, pois a abertura local de qualquer réplica já reduz o tráfego global de forma proporcional.
Aplicação prática: fallback e degradação
Abrir o circuito sem plano B apenas troca uma falha remota por uma falha local, o que reduz o dano, mas não salva a operação. O valor completo aparece com o fallback, a resposta alternativa executada enquanto o circuito está aberto: servir dado em cache mesmo que obsoleto, enfileirar a operação para execução posterior, aplicar regra padrão de negócio ou responder capacidade reduzida.
O desenho do fallback é uma decisão de produto, não de infraestrutura. Numa loja, recomendações podem falhar silenciosamente e a página renderiza sem elas; pagamento não pode falhar silenciosamente, então o fallback correto é enfileirar e avisar o cliente do processamento pendente. Cada dependência precisa da resposta à pergunta: se esta chamada não acontece agora, qual é a menor versão aceitável desta operação?
Monitoramento fecha o ciclo. Toda transição de estado deve gerar métrica e alerta, porque circuito aberto por minutos significa funcionalidade degradada para o usuário, e circuito aberto repetidamente significa dependência cronicamente doente, um problema arquitetural que o padrão apenas contorna.
Falha rápida versus timeout longo
O instinto de configurar timeouts longos, para dar toda chance ao destino, é a causa comum da cascata: timeout de trinta segundos com quinhentas requisições por segundo ocupa dezoito mil threads simultâneas só de espera. A alternativa é o par timeout curto e circuit breaker: a requisição individual desiste cedo, e a contabilidade coletiva decide quando desistir para sempre até nova sondagem.
Falha rápida também é honesta com o usuário. Resposta imediata com mensagem de indisponibilidade e alternativa é superior a dez minutos de espera que terminam em erro, porque preserva o orçamento de paciência e permite ação. O circuit breaker institucionaliza a falha rápida: ele a torna política explícita e reversível, não improvisação de cada desenvolvedor.
A exceção legítima aos timeouts curtos são operações de batch e transferência de volume, em que espera longa é natureza da tarefa. Mesmo nesses casos o circuit breaker se aplica, apenas com limiar de lentidão ajustado à natureza da operação.
Bibliotecas comuns e configuração
| Biblioteca | Ecossistema | Observações de adoção |
|---|---|---|
| Resilience4j | JVM, integração Spring e Micronaut | Sucessor do Hystrix; janela por contagem ou tempo, métricas via Micrometer |
| Polly | .NET | Políticas compostas de timeout, retry e breaker em pipeline único |
| PyBreaker | Python | Estados clássicos e listeners de transição; simples de embutir |
| Opossum | Node.js | Padrão de fato no ecossistema; thresholds e half-open nativos |
| Hystrix | JVM, legado | Em manutenção apenas; migração recomendada para Resilience4j |
| Envoy e Istio | Service mesh | Circuit breaking por pool de conexões na malha, sem código na aplicação |
# Configuração exemplar de um breaker por dependência no Resilience4j
resilience4j:
circuitbreaker:
instances:
pagamentos:
sliding-window-type: COUNT_BASED
sliding-window-size: 20 # últimas 20 chamadas avaliadas
failure-rate-threshold: 50 # abre com 50% de falha na janela
slow-call-duration-threshold: 2s # chamada lenta também conta como falha
slow-call-rate-threshold: 50 # metade das chamadas lentas abre o circuito
wait-duration-in-open-state: 30s # tempo de espera no estado aberto
permitted-number-of-calls-in-half-open-state: 3
automatic-transition-from-open-to-half-open-enabled: true
Em malha de serviço, a decisão entre breaker na aplicação e breaker na malha depende da governança: a malha protege tudo de uma vez, mas não conhece o negócio e não executa fallbacks específicos; a aplicação conhece o contexto e responde com degradação real. Os dois níveis coexistem bem, com a malha cuidando do transporte e a aplicação cuidando da experiência.
Erros comuns
- Um breaker global para todas as dependências, que abre circuito de função acessória junto com pagamento e degrada tudo quando bastava isolar; a unidade correta é o par chamador e dependência.
- Limiar de falha tão baixo que abre por ruído, criando flapping, o vaivém entre open e closed que piora a latência média e enche o painel de alarmes falsos.
- Janela de avaliação tão pequena que três falhas seguidas abrem o circuito, ou tão grande que o incidente cresce por minutos antes da reação.
- Abrir o circuito sem fallback definido, deixando o usuário com erro seco em vez de resposta degradada.
- Fallback que executa a operação completa de forma síncrona, transformando degradação em nova fila de espera; a resposta alternativa precisa ser mais barata que a original.
- Tratar erro do breaker como falha do sistema e disparar retry, o que fere a intenção do padrão e reproduz a tempestade de tentativas que ele impediu.
- Sondagem no half-open liberando tráfego inteiro de uma vez, que derruba o destino que começava a se recuperar.
- Ausência de métricas e alertas por estado, deixando o time descobrir a degradação pelo canal de suporte.
Boas práticas
- Definir um breaker por dependência, com janela, limiar de falha, limiar de lentidão e tempo de espera derivados do contrato daquela integração.
- Considerar chamadas lentas como falha, pois lentidão crônica é o modo de falha mais comum e o mais destrutivo em produção.
- Projetar o fallback junto com a funcionalidade, com decisão de produto registrada sobre o que acontece quando cada dependência cai.
- Fazer transição automática de open para half-open e sondar com número pequeno de chamadas, nunca com o tráfego integral.
- Alertar sobre transições de estado e sobre frequência de abertura, usando a frequência como termômetro da saúde da dependência.
- Combinar com timeout curto por chamada e com retry limitado e com jitter, respeitando a ordem: breaker decide se tenta, retry decide quando tenta, timeout decide até quando espera.
- Testar o comportamento em ambiente, injetando falha e lentidão na dependência para validar abertura, fallback e recuperação antes do incidente real.
- Documentar o estado do breaker em página de operação, para que a planta de plantão saiba interpretar circuito aberto sem ler código.