Resiliência em sistemas distribuídos: circuit breakers, retries e backoff
Em um sistema distribuído, a falha não é exceção, é rotina. Redes engasgam, serviços ficam lentos, dependências caem sem avisar. A pergunta não é se uma dependência vai falhar, e sim o que o seu sistema faz quando ela falha. Sem uma resposta pensada, a falha de um serviço secundário vira uma reação em cadeia que derruba o produto inteiro. Um serviço lento pode ser pior do que um serviço morto, porque prende recursos de todos que dependem dele até tudo travar junto.
Este guia cobre os padrões que transformam um sistema frágil em um resiliente, com foco em chamadas síncronas entre serviços. Enquanto filas e mensageria resolvem resiliência pelo desacoplamento assíncrono, aqui tratamos do outro lado: como fazer uma chamada direta entre serviços falhar com elegância, sem contaminar o resto.
A anatomia de uma falha em cascata
Imagine o serviço A que chama o serviço B a cada requisição. B fica lento. As requisições de A começam a se acumular, esperando B responder. As threads de A ficam presas nessa espera. Em segundos, A esgota seus recursos e para de responder, mesmo para requisições que nem dependiam de B. Agora A também caiu, e quem depende de A cai em seguida. Uma lentidão em B virou um apagão geral.
Esse é o efeito cascata, e todos os padrões a seguir existem para interrompê-lo em algum ponto. A ideia central é a mesma: contenha a falha perto da origem, antes que ela se espalhe.
Timeout: nunca espere para sempre
O primeiro e mais básico padrão é o timeout. Toda chamada de rede precisa de um limite de tempo. Sem timeout, uma dependência lenta prende o chamador indefinidamente, e é exatamente assim que a cascata começa.
O erro comum não é esquecer o timeout, e sim configurá-lo alto demais. Um timeout de trinta segundos numa chamada que normalmente responde em cinquenta milissegundos é quase o mesmo que não ter timeout: quando B trava, A espera trinta segundos por requisição e afoga. Ajuste o timeout com base na latência real esperada da dependência, com uma margem, não em um número redondo tirado do nada.
Retry com backoff e jitter
Falhas transitórias existem: um pacote perdido, um pico momentâneo, uma reinicialização rápida. Para essas, repetir a chamada faz sentido. Mas retry ingênuo é perigoso, e há três regras que separam o retry útil do desastroso.
- Só repita o que é seguro repetir. Retry exige idempotência: repetir a operação não pode causar efeito duplicado. Reenviar um "consultar saldo" é inofensivo; reenviar um "cobrar cartão" sem proteção é um bug financeiro.
- Espere mais a cada tentativa (backoff exponencial). Repetir imediatamente, em rajada, contra um serviço já sobrecarregado só piora a situação. O backoff exponencial aumenta o intervalo entre tentativas (por exemplo, 1s, 2s, 4s), dando tempo para a dependência se recuperar.
- Adicione aleatoriedade (jitter). Se mil clientes falham juntos e todos repetem no mesmo instante, eles criam ondas sincronizadas que reafogam o serviço, o chamado thundering herd. O jitter espalha as tentativas no tempo com um componente aleatório, quebrando a sincronia.
Junte os três: retry apenas de operações idempotentes, com backoff exponencial e jitter, e um teto de tentativas. Retry sem essas travas não aumenta a resiliência, ele amplifica a falha.
Circuit breaker: parar de bater na porta fechada
Se uma dependência está claramente fora do ar, insistir é desperdício: cada tentativa consome recursos, aumenta a latência e adia o inevitável. O circuit breaker (disjuntor) resolve isso imitando um disjuntor elétrico, com três estados:
- Fechado (closed): as chamadas passam normalmente. O breaker conta as falhas.
- Aberto (open): depois de um limiar de falhas, o breaker "abre" e passa a rejeitar as chamadas na hora, sem nem tentar a dependência. Isso protege o chamador e dá folga para a dependência se recuperar.
- Meio-aberto (half-open): após um tempo, o breaker deixa passar algumas chamadas de teste. Se voltam com sucesso, ele fecha; se falham, abre de novo.
O ganho é duplo: o chamador falha rápido em vez de esperar timeouts sucessivos, e a dependência em recuperação não recebe uma enxurrada de tráfego que a derrubaria de novo. O circuit breaker é o padrão que transforma "falha lenta e contagiosa" em "falha rápida e contida".
Bulkhead: compartimentar para não afundar junto
O nome vem dos navios: o casco é dividido em compartimentos estanques (bulkheads), para que um furo alague só uma seção, não a embarcação toda. Em software, o padrão bulkhead isola recursos por dependência. Em vez de um único pool de conexões ou threads compartilhado por todas as chamadas, cada dependência recebe seu próprio pool limitado.
Assim, se a dependência B travar e consumir todo o seu pool, ela afeta apenas as chamadas a B. As chamadas a C e D seguem com seus próprios recursos intactos. Sem bulkhead, uma única dependência lenta drena o pool comum e derruba funcionalidades que não tinham nada a ver com ela.
Fallback: uma resposta degradada é melhor que nenhuma
Quando tudo falha, a última linha de defesa é o fallback: em vez de propagar o erro, o sistema entrega uma resposta alternativa e degradada. Um serviço de recomendações fora do ar pode devolver uma lista genérica de populares em vez de recomendações personalizadas. Um cache pode servir o último valor conhecido quando a origem não responde. A experiência piora um pouco, mas o produto continua de pé, e o usuário raramente percebe.
Os padrões em conjunto
| Padrão | Problema que resolve | Efeito principal |
|---|---|---|
| Timeout | Espera infinita por dependência lenta | Libera o chamador rápido |
| Retry com backoff e jitter | Falhas transitórias | Recupera sem afogar a dependência |
| Circuit breaker | Dependência claramente fora do ar | Falha rápido e deixa a dependência respirar |
| Bulkhead | Uma dependência drenando recursos comuns | Isola a falha a uma seção |
| Fallback | Ausência total de resposta | Degrada em vez de quebrar |
| Idempotência | Efeitos duplicados de retry | Torna repetir seguro |
Esses padrões não competem, eles se somam. Um sistema resiliente costuma usar todos ao mesmo tempo: timeout em cada chamada, retry idempotente com backoff e jitter, protegido por um circuit breaker, isolado por bulkheads, com fallback para o pior caso.
Resiliência é decisão de projeto, não conserto
O maior erro é tratar resiliência como algo a adicionar depois do primeiro incidente. Falha em sistema distribuído é certeza estatística, não azar. Cada chamada de rede é um ponto onde algo vai dar errado um dia, e o comportamento nesse dia é definido pelo que você projetou (ou deixou de projetar) hoje. Escolher os limiares, os timeouts e os fallbacks certos exige conhecer o comportamento real de cada dependência, e é esse cuidado que separa um sistema que degrada com elegância de um que desaba por inteiro no primeiro soluço da rede.
Seu sistema cai em cascata quando uma dependência falha? Na ConstructorTI, projetamos resiliência desde a arquitetura, com timeouts, circuit breakers, bulkheads e fallbacks calibrados para o comportamento real das suas dependências. Se um serviço lento já derrubou o seu produto inteiro, fale com a nossa engenharia e receba um diagnóstico dos pontos de falha em cascata da sua arquitetura.