03 - Circuit Breaker (na nuvem)
video: https://youtu.be/... no frontmatter da nota.Circuit Breaker (na nuvem)
O Circuit Breaker é o padrão que impede que uma dependência falha transforme a falha dela em falha de todo o sistema, ao interromper temporariamente as chamadas para o destino degradado e responder com um caminho alternativo. O nome vem do disjuntor elétrico, que corta a corrente quando detecta sobrecarga para proteger a instalação; no software, o corte protege o chamador de ficar preso em timeouts e protege o destino de receber ainda mais tráfego em momento de fragilidade. Em contexto de nuvem, o padrão ganha duas dimensões: pode viver no código do cliente, por meio de bibliotecas de resiliência, e pode viver na infraestrutura, em service mesh e API gateway que aplicam a mesma lógica sem que o código saiba. A diferença entre as duas camadas define latência de reação, granularidade de controle e custo de operação. Em sistemas distribuídos com dezenas de dependências, o disjuntor é o que separa uma falha pontual de uma cascata de indisponibilidade.
Conceito e estados
O disjuntor opera em três estados bem definidos. No estado fechado, as chamadas fluem normalmente e o componente acumula estatísticas de erro e latência em uma janela deslizante. Quando a taxa de falha cruza o limiar configurado, o estado abre, e as chamadas deixam de ser enviadas, retornando imediatamente erro ou resposta de fallback. Após um intervalo de espera, o estado passa a meio aberto, em que um volume pequeno de requisições de prova atravessa para avaliar se o destino se recuperou; em caso de sucesso, fecha-se novamente, e em caso de falha, volta a abrir.
Essa máquina de estados resolve um problema de física do tráfego: sem corte, cada requisição falha após o timeout completo, ocupando threads, conexões e memória do chamador. Com o corte, a falha é reconhecida em milissegundos, e o chamador libera recursos para atender o resto do tráfego. O efeito colateral positivo é aliviar a dependência doente, dando a ela chance real de se recuperar em vez de afogar sob uma fila crescente.
- Fechado (closed): tráfego normal, janela de estatísticas ativa, contagem de falhas em curso.
- Aberto (open): chamadas rejeitadas na hora, fallback executado, alarme emitido para o time de plantão.
- Meio aberto (half-open): sondagem limitada, transição automática conforme o resultado das provas.
- Fallback: resposta alternativa definida por negócio, de cache a valor padrão, executada quando o circuito está aberto.
O problema que resolve
O cenário clássico de morte por dependência começa pequeno: o serviço de recomendação passa a responder em 8 segundos em vez de 80 milissegundos. Sem disjuntor, cada chamada do checkout espera o timeout completo, o pool de conexões enche, as threads bloqueiam, e o próprio checkout, que estava saudável, para de responder. O incidente então se espalha para quem chama o checkout, e a cascata atinge todo o front. Esse padrão de propagação é uma das causas mais comuns de indisponibilidade total a partir de um componente secundário.
O disjuntor confina esse dano em duas direções. Para o chamador, converte falha lenta em falha rápida e previsível, permitindo responder com degraded mode em vez de travar. Para o destino, reduz a pressão exatamente quando ele menos aguenta, porque o circuito aberto corta o fluxo de requisições que jamais seriam processadas a tempo. Em nuvem, onde dependências cruzam zonas, contas e empresas, essa proteção local é essencial, pois ninguém controla o tempo de recuperação do vizinho.
Há também o custo silencioso da falha lenta: enquanto o destino responde devagar, o chamador paga por recursos bloqueados, e em ambientes com autoscaling o sistema cresce para absorver capacidade gasta em espera, não em trabalho útil. O disjuntor devolve essa capacidade ao sistema em segundos, e o efeito aparece na conta no fim do mês, não apenas nos gráficos de latência.
Funcionamento interno
A implementação precisa decidir quando abrir com base em sinais confiáveis. Os sinais mais usados são taxa de falha percentual na janela, número mínimo de requisições para calcular a taxa, e latência lenta tratada como falha, porque um sucesso em 10 segundos também derruba o chamador. Limiares mal escolhidos geram flapping, o circuito abre e fecha em ciclos rápidos, amplificando instabilidade; limiares bem calibrados combinam volume mínimo, taxa alvo e tempo de espera coerentes com o SLA da dependência.
# Configuração de circuit breaker por dependência (estilo resilience4j)
circuitbreakers:
instances:
pagamentos:
slidingWindowType: COUNT_BASED # janela por número de chamadas
slidingWindowSize: 100 # amostra mínima para decidir
minimumNumberOfCalls: 20 # não abre com estatística fraca
failureRateThreshold: 50 # abre com 50% de falha na janela
slowCallDurationThreshold: 2s # chamada acima disso é lenta
slowCallRateThreshold: 80 # 80% lentas também abrem o circuito
waitDurationInOpenState: 30s # espera antes de sondar de novo
permittedNumberOfCallsInHalfOpenState: 5 # provas do meio aberto
estoque:
slidingWindowSize: 50
failureRateThreshold: 60 # dependência mais tolerante
waitDurationInOpenState: 15s
O comportamento do meio aberto merece atenção, porque sondar com tráfego real de usuário pode penalizar clientes reais. Implementações maduras limitam as chamadas de prova a um percentual pequeno e distribuem a sondagem no tempo, enquanto monitoram a transição como evento de primeira classe nos alertas.
Circuit breaker por dependência
A granularidade correta é uma instância de disjuntor por dependência, e não um global. O checkout depende de pagamento, de estoque, de frete e de recomendação, e cada uma tem contrato, criticidade e perfil de falha próprios. O disjuntor de pagamento deve ser conservador, porque vender sem cobrar gera prejuízo direto, e o de recomendação deve ser agressivo, porque perder uma sugestão não quebra a venda. Ignorar essa assimetria e usar um limiar único faz o sistema escolher errado dos dois lados.
A decisão por dependência também orienta o fallback, que precisa ser especificado por negócio e não por engenharia. Para estoque, um fallback plausível é seguir com pedido e validar assíncrona após o circuito fechar. Para pagamento, não existe fallback de valor, e a resposta honesta é recusar a etapa com mensagem clara e retentativa controlada. O disjuntor mecânico só decide quando cortar; o que acontece depois do corte é decisão de produto.
Disjuntor na infraestrutura
O mesmo padrão existe fora do código. No Envoy e nos service mesh, o circuit breaking aparece como limite de conexões e requisições pendentes por cluster, e o outlier detection ejeta endpoints que devolvem erro sequencial ou respondem lento, recolocando-os depois de um intervalo de espera. Na borda, API gateways como Amazon API Gateway e Apigee aplicam throttling e, em combinação com retries, comportamento equivalente a um disjuntor grosseiro na entrada do sistema.
# Envoy: outlier detection no cluster, o disjuntor da camada de rede
outlier_detection:
consecutive_5xx: 5 # ejeta o host após 5 erros 5xx seguidos
interval: 10s # frequência de análise dos hosts
base_ejection_time: 30s # host ejetado volta após no mínimo 30s
max_ejection_percent: 50 # nunca ejeta mais da metade do pool
Aplicação prática em nuvem
Os serviços gerenciados oferecem blocos de construção para o caminho do fallback, e o desenho mais comum combina fila com armazenamento de leitura rápida. Quando o circuito do processador de pagamento abre, o pedido pode ser gravado em uma fila como SQS e processado quando o circuito fechar, preservando o trabalho do cliente sem prometer resposta síncrona. Para leituras, um cache como ElastiCache ou DynamoDB com TTL serve a última resposta conhecida marcada como stale, o que mantém a jornada navegável durante a degradação.
# Pseudocódigo: chamada protegida com disjuntor e fallback explícito
def criar_pedido(pedido):
if circuito_pagamento.aberto:
# fallback: registra intenção e responde aceitação diferida
fila_pedidos.enviar(pedido) # SQS garante durabilidade
return Resposta(202, "pedido recebido, pagamento será confirmado")
try:
cobranca = gateway_pagamento.cobrar(pedido, timeout=2s)
circuito_pagamento.sucesso()
return Resposta(201, cobranca.id)
except (TimeoutError, ErroGateway) as falha:
circuito_pagamento.falha() # alimenta a janela de decisão
if circuito_pagamento.deve_abrir:
alertar_plantao("circuito de pagamento aberto")
return Resposta(503, "pagamento indisponível, tente novamente")
A nuvem adiciona ainda o disjuntor econômico, menos conhecido e igualmente útil: proteger a conta. Chamadas a serviços cobrados por requisição, como APIs de IA ou passos de step functions, podem ser cercadas por um disjuntor que corta o consumo quando o erro do provedor indica limite de cota esgotada, evitando pagar por tentativas fadadas a falhar e protegendo o orçamento do dia.
Cliente versus infraestrutura
As duas camadas se complementam, mas não são intercambiáveis, e saber onde cada uma atua evita a falsa sensação de proteção duplicada.
| Critério | Disjuntor no cliente | Disjuntor na infraestrutura |
|---|---|---|
| Granularidade | Por chamada e por operação | Por host e por cluster |
| Conhecimento de negócio | Alto, permite fallback específico | Baixo, corta por sinal técnico |
| Latência de reação | Imediata na próxima chamada | Depende do intervalo de análise |
| Cobertura | Apenas onde o código existe | Todo tráfego que passa pelo proxy |
| Custo de implantação | Biblioteca em cada serviço | Plataforma, uma vez para todos |
| Fallback possível | Qualquer lógica de negócio | Resposta estática ou rota alternativa |
A prática recomendada em frotas grandes é usar a infraestrutura como rede de segurança universal, proteção contra hosts doentes e limites por cluster, e manter disjuntores no código apenas nas dependências críticas de negócio, onde o fallback tem valor real. Deixar a proteção só no código é frágil, pois serviços esquecidos ficam descobertos; deixar só na infraestrutura é insuficiente, porque nenhum proxy sabe o que fazer com um pedido de pagamento abortado.
Observabilidade do estado
Um disjuntor invisível é um incidente à espera de diagnóstico. O estado atual deve ser exportado como métrica por dependência, com transições registradas como eventos e alarmes no momento da abertura, e não horas depois, quando alguém nota o fallback rodando. Dashboards de serviço devem mostrar, lado a lado, taxa de erro do destino e estado do circuito, pois a correlação entre os dois é o que permite decidir entre aguardar a recuperação e escalonar para o time dono. Traces com a marcação de chamada rejeitada por circuito aberto evitam o erro de diagnóstico mais comum, que é tratar o erro rápido de circuito aberto como falha do chamador.
O conjunto mínimo de métricas por circuito tem cinco itens: estado atual codificado como valor, taxa de falha na janela deslizante, número de chamadas rejeitadas por segundo, duração média das chamadas de prova no meio aberto e tempo total que o circuito permaneceu aberto na última hora. Com esses cinco sinais, o plantão distingue um corte protetor saudável de um circuito preso em flapping, e consegue quantificar quanto da experiência do usuário está rodando em modo degradado.
Erros comuns
- Abrir com amostra pequena: três falhas em rede instável abrem o circuito e criam flapping; volume mínimo e janela de tempo resolvem.
- Fallback silencioso: degradação executada sem métrica nem log, e o negócio opera semanas com dado incompleto sem que ninguém saiba.
- Circuito global único: um disjuntor para todas as dependências faz a falha do estoque parar o pagamento.
- Retentativa dentro do circuito aberto: o cliente testa o destino a cada requisição, transformando o corte em denial de serviço involuntário.
- Timeout maior que o orçamento da requisição: o disjuntor abre corretamente, mas cada chamada já destruiu o SLA do usuário final antes disso.
- Estado sem exportação: o circuito funciona, mas ninguém descobre a degradação até o cliente reclamar de dado desatualizado no fallback.
Boas práticas
Defina orçamento de latência e de tentativas por dependência antes de configurar limiares, pois os números do disjuntor derivam do SLA da jornada, e não do contrário. Trate cada abertura de circuito como alarme com runbook, incluindo o que o fallback entrega e como o negócio percebe a degradação. Teste o comportamento com caos controlado, injetando falha na dependência em ambiente de produção com tráfego real, porque apenas esse teste prova que o fallback funciona e que o time sabe reagir. Por fim, revisite limiares a cada mudança relevante de contrato ou de tráfego, já que calibração de disjuntor é decisão viva e não configuração estática.