Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

02 - Circuit Breaker

#circuit-breaker#degradacao#fail-fast#fallback#resiliencia
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione 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.