ResiliênciaAvançado9 min de leitura7 seções2007, Data aproximada

Circuit Breaker

Quando uma dependência já está claramente fora do ar, continuar chamando não ajuda ninguém: gasta o seu prazo e atrapalha a recuperação dela. O disjuntor detecta o padrão de falha e passa a recusar na hora, até que valha a pena tentar de novo.

Em 10 segundos

Se a dependência já está claramente fora, pare de chamar. O disjuntor conta as falhas, abre o circuito ao passar do limiar e passa a recusar na hora — devolvendo o seu prazo e dando à dependência a folga que ela precisa para se levantar. De tempos em tempos, deixa uma sondagem passar para ver se já dá.

Antes de ter nome

O nome e a metáfora vêm do disjuntor elétrico: ele abre o circuito para proteger a instalação quando a corrente passa do seguro, e é rearmado depois.

  • #resiliencia
  • #disjuntor
  • #falha-rapida
  • #producao
  • #cascata
Analogia

O disjuntor do quadro de luz

Quando há curto-circuito, o disjuntor desarma. Ele não conserta o curto — ele impede que a corrente continue passando e queime a fiação inteira. E ninguém religa sem parar: você espera, religa uma vez para testar, e se desarmar de novo, sabe que o problema continua lá. O disjuntor de software faz exatamente isso, contando falhas em vez de amperes.

01Conceito

Insistir custa dos dois lados

Chamar uma dependência que está fora não é neutro. Cada tentativa gasta uma conexão sua, consome o orçamento de tempo do pedido, e termina no mesmo erro — mas trinta segundos depois, em vez de imediatamente.

E do outro lado é pior: um serviço que caiu por sobrecarga precisa de folga para se recuperar, e a insistência dos clientes é justamente o que impede isso.

O padrão resolve dois problemas que parecem um só. O primeiro é proteger quem chama: sem disjuntor, cada requisição paga o timeout inteiro para descobrir algo que já se sabia. Com trinta segundos de prazo e uma dependência morta, o seu pool esgota do mesmo jeito que esgotaria sem timeout nenhum — só um pouco mais devagar.

O segundo é proteger quem é chamado. Um serviço sobrecarregado que recebe menos tráfego consegue drenar a fila e voltar. Um serviço sobrecarregado que recebe o mesmo tráfego mais os retries nunca sai do buraco. O disjuntor é, do ponto de vista da dependência, um mecanismo de alívio de carga imposto pelos clientes.

Vale distinguir de Retry, que é o padrão irmão e resolve o problema oposto. Retry aposta que a falha é passageira e que insistir vale a pena. Circuit Breaker aposta que a falha é persistente e que insistir custa caro. Os dois convivem: repete-se algumas vezes; se o padrão de falha se mantém, o disjuntor abre e as repetições param. Sem o disjuntor, o retry sozinho vira o *thundering herd*.

Como se organiza
  • Quem chamanão sabe que existe disjuntor no caminho

    • Disjuntorconta falhas e decide se deixa passar

      • Fechadopassa tudo, contando

      • Abertorecusa na hora, sem chamar

      • Meio-abertodeixa poucas passarem, em teste

    • Caminho degradadocache antigo, valor padrão, seção escondida

  • Dependência externapode estar fora por minutos

O disjuntor se coloca entre quem chama e a dependência. O caminho degradado é opcional — e é ele que decide se abrir o circuito melhora a experiência ou só troca um erro por outro.
02Conceito

Três estados, e o do meio é o que importa

Fechado é a operação normal: tudo passa, e as falhas vão sendo contadas. Aberto é a recusa imediata, sem tocar na dependência. Meio-aberto é o estado de teste, e é onde mora a sutileza.

Ao sair do aberto, o disjuntor não pode simplesmente voltar a passar tudo — isso jogaria o tráfego acumulado de uma vez sobre um serviço que talvez mal tenha se levantado.

Meio-aberto deixa passar poucas requisições, em teste. Se elas tiverem sucesso, o circuito fecha; se uma falhar, ele volta a abrir imediatamente — não espera atingir limiar de novo, porque a evidência de que ainda está ruim já apareceu.

O erro clássico é dimensionar o limiar por contagem absoluta em vez de taxa. "Abre depois de 5 falhas" se comporta de maneira completamente diferente a 10 requisições por segundo e a 10 mil: no segundo caso, 5 falhas é ruído estatístico normal. O limiar sensato é uma fração — e com um mínimo de amostras, para não abrir com uma falha em uma requisição.

O disjuntor também precisa ser por dependência, não global. Um disjuntor único para todas as chamadas externas abre por causa do serviço de recomendação e derruba junto o de pagamento, que estava perfeito. E, em geral, por dependência *e por instância*: é assim que o *outlier detection* de um service mesh funciona.

03No mundo real

Como isso aparece na prática

O gateway de pagamento que ficou fora por 20 minutos

Cenário

O provedor de pagamento tem um incidente e passa a responder 503 em 100% das chamadas. Sem disjuntor, cada tentativa de compra espera o timeout de 10 segundos antes de falhar, e o pool de conexões do checkout esgota.

Como o padrão entra

O disjuntor abre após 50% de falha em 20 amostras e passa a recusar em microssegundos. A interface mostra 'pagamento temporariamente indisponível, tente em instantes' de imediato, e oferece o boleto como alternativa.

Trade-off: Enquanto aberto, mesmo uma recuperação parcial do provedor não é aproveitada até a próxima sondagem — perde-se alguns minutos de vendas possíveis em troca de o resto do site continuar de pé.

A busca que virou opcional

Cenário

O cluster de busca fica degradado sob carga de pico e responde em 15 segundos. A home usa a busca para montar um bloco de sugestões, e a página inteira passa a demorar 15 segundos.

Como o padrão entra

Disjuntor com limiar baixo e caminho degradado: com o circuito aberto, o bloco de sugestões cai para uma lista estática pré-computada, e a home volta a carregar em 200ms.

Trade-off: As sugestões ficam genéricas durante o incidente, o que provavelmente reduz conversão naquele bloco. É uma perda mensurável e aceita — bem menor que a de uma home que não carrega.

04Cuidado

Onde isto dá errado

  1. Limiar por contagem absoluta em vez de taxa

    "Abre depois de 5 falhas" significa coisas opostas a 10 e a 10 mil requisições por segundo. No volume alto, 5 falhas é ruído normal e o circuito abre sem motivo. O limiar precisa ser uma fração das amostras, com um mínimo antes de julgar.

  2. Um disjuntor global para todas as dependências

    Se o mesmo disjuntor cobre recomendação, busca e pagamento, o serviço menos importante derruba o mais crítico ao abrir o circuito de todos. O escopo correto é por dependência — e, quando possível, por instância.

  3. Janela de espera curta demais

    Sondar de cinco em cinco segundos um serviço que leva minutos para voltar mantém uma pressão constante sobre quem está tentando se levantar, e nunca dá tempo de a recuperação acontecer. A espera precisa ser da ordem do tempo real de recuperação.

  4. Voltar de meio-aberto para fechado de uma vez

    Ao sair do estado aberto, liberar todo o tráfego acumulado de uma vez derruba de novo o serviço que mal tinha se levantado. Meio-aberto existe para deixar passar poucas requisições em teste, e uma falha nelas basta para reabrir.

  5. Abrir o circuito sem ter para onde ir

    Se a dependência é essencial e não há caminho degradado, abrir o circuito só troca um erro lento por um erro rápido. Continua sendo melhor — o prazo é devolvido —, mas o ganho é bem menor do que se imagina, e não justifica sozinho a complexidade.

  6. Contar timeout do cliente como falha da dependência

    Se o seu timeout está apertado demais, você registra falha em chamadas que teriam funcionado. O disjuntor abre por causa da sua configuração, não da saúde do outro lado — e o diagnóstico aponta para o lugar errado.

05Estrutura

A máquina de estados

Diagrama de classes
06Implementação

Um disjuntor com o relógio injetado

type Estado = "fechado" | "aberto" | "meio-aberto";

interface Config {
  /** fracao de falhas que abre o disjuntor (ex.: 0.5 = 50%). */
  limiar: number;
  /** minimo de amostras antes de julgar — evita abrir com 1 de 1. */
  minimoAmostras: number;
  /** quanto tempo fica aberto antes de sondar. */
  esperaMs: number;
}

class Disjuntor {
  private estado: Estado = "fechado";
  private falhas = 0;
  private total = 0;
  private abertoDesde = 0;

  // 'agora' entra por parametro: nada de Date.now() aqui dentro.
  permitir(agora: number): boolean {
    if (this.estado === "aberto") {
      if (agora - this.abertoDesde < this.cfg.esperaMs) return false;
      this.estado = "meio-aberto"; // hora de sondar
    }
    return true;
  }

  registrar(ok: boolean, agora: number): void {
    if (this.estado === "meio-aberto") {
      // Em teste: um erro basta para voltar a abrir.
      if (ok) this.fechar();
      else this.abrir(agora);
      return;
    }
    this.total++;
    if (!ok) this.falhas++;
    if (this.total >= this.cfg.minimoAmostras &&
        this.falhas / this.total > this.cfg.limiar) {
      this.abrir(agora);
    }
  }

  constructor(private cfg: Config) {}
  private abrir(agora: number) { this.estado = "aberto"; this.abertoDesde = agora; }
  private fechar() { this.estado = "fechado"; this.falhas = 0; this.total = 0; }
}
07Decisão

Quando usar × quando evitar

Use quando

3
  • Dependências externas que podem ficar fora por minutos.
  • Quando existe caminho degradado aceitável.
  • Junto com timeout e retry — os três se completam.

Evite quando

3
  • Em volume baixo, onde a taxa de falha é ruído estatístico.
  • Como substituto de timeout: sem prazo não há falha para contar.
  • Em dependência essencial e sem alternativa, onde o ganho é pequeno.

Testar o que ficou

6 armadilhas de Circuit Breaker viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • Outlier detection do EnvoyO service mesh tira do balanceamento a instância que passa a falhar, e a devolve depois de um tempo.
  • Health check de load balancerParar de mandar tráfego para quem falhou nas últimas sondagens é o mesmo padrão, no nível do balanceador.
  • O disjuntor do seu quadro de luzO nome é emprestado dele: corta o circuito antes que a sobrecarga queime a fiação inteira.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Ja falhou demais? Nem tenta.
if (!disjuntor.permitir(agora)) throw new Aberto();

O que ele cobra

1 salto a mais entre a chamada e o efeito.

  • Três parâmetros para calibrar (limiar, amostras mínimas, janela de espera)
  • Um estado a mais para observar e entender durante o incidente

Não vale se: não há caminho degradado. Sem alternativa, abrir o circuito só troca um erro lento por um rápido.

Onde isto já custou caro

Incidentes reais que este conceito explica.

Onde isto aparece

Continue por aqui