Software · Soluções na Prática
Engenharia de Resiliência na Prática: Padrões Circuit Breaker, Retries com Jitter e Fallbacks
Como evitar falhas em cascata em sistemas distribuídos utilizando políticas de repetição com recuo exponencial, disjuntores de circuito e degradação suave.
Em arquiteturas monolíticas tradicionais, a maioria das chamadas entre componentes do sistema ocorre em memória com confiabilidade praticamente absoluta. No entanto, quando migramos para microsserviços, computação em nuvem e consumo de APIs externas de terceiros, a rede torna-se uma fronteira volátil: conexões oscilam, servidores sofrem sobrecargas momentâneas e roteadores descartam pacotes sem aviso prévio.
O erro mais frequente em equipes de desenvolvimento é tratar chamadas remotas de rede com ingenuidade, disparando requisições sem limites de tempo ou repetindo chamadas falhas de maneira cega e desordenada. Em momentos de lentidão de um parceiro externo (como uma adquirente de pagamentos), essa prática desencadeia uma "tempestade de retries" (Retry Storm), transformando uma instabilidade passageira em uma falha catastrófica em cascata que derruba toda a infraestrutura da empresa.
A Engenharia de Resiliência estrutura a aplicação para sobreviver a falhas parciais por meio de três padrões essenciais: Retries com Jitter, Circuit Breakers e Fallbacks.
Retries Inteligentes: Recuo Exponencial com Jitter
Tentar novamente uma operação que falhou é uma excelente estratégia quando a falha é puramente transitória (como uma perda momentânea de pacote de rede). Contudo, retries ingênuos e imediatos pioram drasticamente a saúde de um servidor que já está sobrecarregado.
Para evitar que milhares de instâncias repitam requisições exatamente nos mesmos intervalos (o efeito de manada ou Thundering Herd), a engenharia moderna aplica duas regras matemáticas:
| Tentativa de Retry | Espera Base ($2^n \times 100\text{ ms}$) | Jitter Aleatório | Tempo Total de Espera |
|---|---|---|---|
| Tentativa 1 ($n=1$) | $200\text{ ms}$ | $+42\text{ ms}$ | $242\text{ ms}$ |
| Tentativa 2 ($n=2$) | $400\text{ ms}$ | $-18\text{ ms}$ | $382\text{ ms}$ |
| Tentativa 3 ($n=3$) | $800\text{ ms}$ | $+85\text{ ms}$ | $885\text{ ms}$ |
| Tentativa 4 ($n=4$) | $1600\text{ ms}$ | $+120\text{ ms}$ | $1720\text{ ms}$ |
- Recuo Exponencial (Exponential Backoff): O intervalo de espera dobra a cada tentativa subsequente ($2^n \times \text{base}$), dando tempo para o serviço de destino recuperar seu fôlego de processamento.
- Jitter (Ruído Aleatório): Acrescenta-se uma variação aleatória de alguns milissegundos a cada cálculo de espera. Isso dessincroniza os múltiplos clientes na internet, espalhando as tentativas no tempo de forma uniforme em vez de bombardear o servidor em ondas concentradas.
O Padrão Circuit Breaker (Disjuntor de Circuito)
Descrito por Michael Nygard no clássico livro Release It!, o padrão Circuit Breaker funciona exatamente como o disjuntor elétrico de uma residência: se a corrente passar dos limites de segurança, o disjuntor desarma instantaneamente para proteger a fiação contra incêndios.
Em software, o Circuit Breaker monitora a taxa de falhas das chamadas para um serviço downstream, operando sob uma máquina de estados finita:
stateDiagram-v2
[*] --> Closed
Closed --> Open : Taxa de falhas > limiar (ex: 50%)
note right of Closed
Operação normal:
Requisições fluem livremente
end note
Open --> HalfOpen : Tempo de repouso expira (ex: 30s)
note right of Open
Circuito desarmado:
Fail-fast imediato sem ida à rede
end note
HalfOpen --> Closed : Sonda de teste é bem-sucedida
HalfOpen --> Open : Sonda de teste falha
note right of HalfOpen
Avaliação cautelosa:
Deixa passar 1 requisição teste
end note
Os Três Estados do Disjuntor
- Closed (Fechado): O sistema opera em condições normais. As requisições fluem livremente pela rede. Um contador em janela de tempo deslizante (por exemplo, os últimos 30 segundos) registra os sucessos e as falhas.
- Open (Aberto): Se a taxa de erros ultrapassar o teto tolerado (como 50% de falhas em um volume mínimo de 20 chamadas), o circuito desarma. Todas as novas requisições seguintes são imediatamente rejeitadas no cliente com falha rápida (fail-fast), sem consumir sockets de rede nem aguardar timeouts. Isso protege o servidor remoto doente e preserva as threads da aplicação local.
- Half-Open (Semiaberto): Após um período configurado de descanso (por exemplo, 30 segundos), o disjuntor deixa passar cautelosamente uma única requisição de teste (probe). Se a chamada for bem-sucedida, o circuito assume que o downstream se recuperou e retorna para Closed; caso contrário, desarma novamente para Open por mais um ciclo de repouso.
Implementação Prática com Polly e .NET
No ecossistema .NET, a biblioteca open-source Polly consolidou-se como o padrão oficial integrado nativamente ao Microsoft.Extensions.Resilience:
// Configuração da esteira de resiliência combinando Timeout, Retry e Circuit Breaker
var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddTimeout(TimeSpan.FromSeconds(3)) // 1. Desiste após 3 segundos
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true, // Evita tempestades de retries sincronizadas
Delay = TimeSpan.FromMilliseconds(200)
})
.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions<HttpResponseMessage>
{
FailureRatio = 0.5, // 50% de erros na janela
SamplingDuration = TimeSpan.FromSeconds(20),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30) // Repouso de 30s
})
.Build();
// Execução protegida da chamada HTTP externa
var resposta = await pipeline.ExecuteAsync(async cancellationToken =>
await httpClient.GetAsync("https://api.parceiro.com/v1/cotacao", cancellationToken));
Fallbacks e Degradação Suave
A peça final da engenharia de resiliência é o Fallback: o que a aplicação deve fazer quando o circuito estiver aberto ou as tentativas de retry se esgotarem?
A resposta nunca deve ser uma tela genérica de erro no navegador do cliente. Aplica-se a degradação graciosa (Graceful Degradation):
- Em um e-commerce, se o motor de recomendações personalizadas estiver fora do ar, o sistema devolve uma lista em cache com os produtos mais vendidos gerais.
- Se o serviço de cálculo de frete externo cair, o checkout aplica uma tabela estática de contingência baseada na faixa de CEP.
- Se a consulta de perfil falhar, devolvem-se os dados preservados localmente no navegador via IndexedDB.
Ao adotar essas práticas, sistemas corporativos preservam a continuidade operacional mesmo diante de interrupções severas de serviços externos, transformando dependências frágeis em plataformas tolerantes a falhas.