Retry policies com backoff exponencial e jitter
1. Fundamentos das Retry Policies em Sistemas Distribuídos
1.1. Por que retentativas são necessárias: falhas transitórias vs. permanentes
Em sistemas distribuídos, falhas são inevitáveis. Uma requisição pode falhar por timeout de rede, concorrência em banco de dados, reinicialização temporária de um serviço ou pico de carga. Essas são falhas transitórias — tendem a desaparecer após curto intervalo. Já falhas permanentes (como recurso inexistente ou permissão negada) não são resolvidas com repetição.
Retry policies devem diferenciar esses tipos. Repetir uma falha permanente apenas desperdiça recursos e amplifica o problema.
1.2. O problema do "thundering herd" em retentativas simultâneas
Quando múltiplos clientes detectam falha simultaneamente e repetem a requisição no mesmo instante, o servidor recém-recuperado recebe uma avalanche de tráfego — o fenômeno thundering herd. Isso pode derrubar o serviço novamente, criando um ciclo de falhas.
1.3. Trade-offs: latência vs. resiliência vs. carga no sistema
Cada retentativa adiciona latência à resposta final. Aumentar o número de tentativas melhora a resiliência, mas eleva a carga no sistema e o tempo de espera do cliente. O equilíbrio exige políticas bem calibradas.
2. Backoff Exponencial: Estratégia Clássica de Espera
2.1. Funcionamento matemático
O backoff exponencial calcula o tempo de espera como:
delay = min(cap, base * (2^attempt))
Onde base é o delay inicial (ex.: 100ms) e attempt começa em 0. A cada tentativa, o delay dobra até atingir o limite máximo (cap).
2.2. Implementação prática com limites mínimo e máximo
É fundamental definir:
- base delay: tempo inicial (ex.: 100ms)
- max delay: teto para evitar esperas excessivas (ex.: 10s)
- max retries: número máximo de tentativas (ex.: 5)
2.3. Exemplo de código: algoritmo básico de backoff exponencial
function exponentialBackoff(attempt, baseDelay, maxDelay):
delay = baseDelay * (2 ^ attempt)
return min(delay, maxDelay)
Exemplo de uso:
maxRetries = 5
baseDelay = 100 # ms
maxDelay = 10000 # ms
for attempt = 0 to maxRetries:
success = executeRequest()
if success:
break
if attempt < maxRetries:
delay = exponentialBackoff(attempt, baseDelay, maxDelay)
sleep(delay)
3. Jitter: Introduzindo Aleatoriedade para Evitar Sincronia
3.1. O problema da sincronização de retentativas em larga escala
Sem jitter, todos os clientes com a mesma base de tempo executam retentativas nos mesmos momentos, causando picos de carga sincronizados. O jitter adiciona variação aleatória para dispersar as tentativas.
3.2. Tipos de jitter
- Full jitter: delay = random(0, exponentialBackoff(attempt))
- Equal jitter: delay = exponentialBackoff(attempt) / 2 + random(0, exponentialBackoff(attempt) / 2)
- Decorrelated jitter: delay = min(cap, random(base, delay * 3))
O full jitter é o mais comum, pois espalha as tentativas uniformemente.
3.3. Exemplo de código: implementação de jitter em backoff exponencial
function fullJitterBackoff(attempt, baseDelay, maxDelay):
exponentialDelay = min(baseDelay * (2 ^ attempt), maxDelay)
return random(0, exponentialDelay)
Uso com jitter:
for attempt = 0 to maxRetries:
success = executeRequest()
if success:
break
if attempt < maxRetries:
delay = fullJitterBackoff(attempt, 100, 10000)
sleep(delay)
4. Padrões de Retry e Políticas de Decisão
4.1. Retentativas síncronas vs. assíncronas
Retentativas síncronas bloqueiam o thread do cliente até obter sucesso ou esgotar tentativas. São simples, mas consomem recursos durante a espera. Retentativas assíncronas (com filas ou schedulers) liberam o thread, melhorando a escalabilidade, mas exigem infraestrutura adicional.
4.2. Limite de tentativas e critérios de desistência
Defina número máximo de tentativas (ex.: 3 a 5). Critérios para desistir incluem:
- Exceder maxRetries
- Exceder tempo total máximo (ex.: 30s)
- Detectar falha permanente (ex.: HTTP 4xx)
4.3. Retry em cadeia: propagação entre serviços
Em arquiteturas de microsserviços, retentativas em cascata podem amplificar carga. Use retry budget (orçamento de retentativas) e deadlines para limitar o tempo total de processamento.
5. Integração com Circuit Breaker e Bulkhead
5.1. Coordenação entre retry e circuit breaker
O circuit breaker interrompe requisições quando a taxa de falhas ultrapassa um limiar. Retry e circuit breaker devem operar em conjunto: retry em falhas transitórias isoladas, circuit breaker para proteger o sistema quando falhas são generalizadas.
5.2. Isolamento de retentativas com bulkhead
O padrão bulkhead separa pools de threads para diferentes tipos de requisição ou cliente. Isso evita que retentativas de um cliente consumam todos os recursos.
5.3. Exemplo de código: combinando retry policy com circuit breaker
circuitBreaker = new CircuitBreaker(failureThreshold=5, recoveryTimeout=30000)
function callWithRetryAndCircuitBreaker(request):
if not circuitBreaker.isAllowed():
return "Circuit open - request blocked"
for attempt = 0 to maxRetries:
try:
response = executeRequest(request)
circuitBreaker.recordSuccess()
return response
except TransientException:
if attempt < maxRetries:
delay = fullJitterBackoff(attempt, 100, 10000)
sleep(delay)
else:
circuitBreaker.recordFailure()
throw
except PermanentException:
circuitBreaker.recordFailure()
throw
6. Tratamento de Falhas Persistentes: Dead Letter Queues
6.1. Roteamento para DLQ após esgotar retentativas
Quando todas as tentativas falham, a mensagem deve ser enviada a uma Dead Letter Queue (DLQ). Isso evita perda de dados e permite análise posterior.
6.2. Estratégias de reenvio manual e reprocessamento diferido
Operadores podem reenviar mensagens da DLQ após correção do problema. Algumas implementações permitem reprocessamento automático com backoff mais longo (ex.: após 1 hora).
6.3. Monitoramento de taxas de retry e falhas no DLQ
Métricas importantes: número de mensagens na DLQ, taxa de retry por serviço, tempo médio até sucesso ou falha definitiva.
7. Idempotência e Segurança em Retentativas
7.1. Garantindo idempotência
Operações idempotentes produzem o mesmo resultado independentemente de quantas vezes são executadas. Para garantir idempotência em retentativas:
- Inclua identificador único (idempotency key) em cada requisição
- O servidor deve verificar se a operação já foi processada
7.2. Efeitos colaterais em operações não-idempotentes
Retentativas em operações não-idempotentes (ex.: criação de recurso sem verificação) podem gerar duplicatas. Use verificação de estado antes de executar a ação.
7.3. Exemplo de código: consumer idempotente com retry seguro
idempotencyStore = new RedisCache()
function processMessage(message):
key = "processed:" + message.id
if idempotencyStore.exists(key):
return # já processado
for attempt = 0 to maxRetries:
try:
executeBusinessLogic(message)
idempotencyStore.set(key, "processed", ttl=3600)
return
except TransientException:
if attempt < maxRetries:
delay = fullJitterBackoff(attempt, 100, 10000)
sleep(delay)
else:
sendToDLQ(message)
throw
8. Boas Práticas e Monitoramento de Retry Policies
8.1. Métricas essenciais
- Taxa de retry: percentual de requisições que necessitam de retentativa
- Latência acumulada: tempo total incluindo retentativas
- Sucesso após retry: quantas requisições bem-sucedidas exigiram retentativa
8.2. Logging estruturado e tracing distribuído
Inclua em cada log: attemptNumber, retryDelay, errorType, idempotencyKey. Use OpenTelemetry ou Jaeger para correlacionar retentativas entre serviços.
8.3. Testes de caos com simulação de falhas
Ferramentas como Chaos Monkey ou Gremlin podem injetar falhas para validar se as políticas de retry se comportam conforme esperado. Teste cenários como:
- Falha intermitente de 50% das requisições
- Pico de latência de 2 segundos
- Falha total do serviço por 10 segundos
Referências
- AWS: Retry Mode and Exponential Backoff — Documentação oficial da AWS sobre retentativas com backoff exponencial e jitter
- Microsoft: Retry Pattern — Guia da Microsoft sobre o padrão Retry em arquiteturas de nuvem
- Google Cloud: Handling Failures with Retry Logic — Estratégias de retentativa recomendadas pelo Google Cloud
- Marc Brooker: Exponential Backoff and Jitter — Artigo técnico da AWS sobre implementação de jitter em backoff exponencial
- Martin Fowler: Circuit Breaker — Explicação do padrão Circuit Breaker, frequentemente combinado com retry policies
- Amazon Builders' Library: Timeouts, Retries, and Backoff with Jitter — Artigo aprofundado sobre implementação de retry com jitter em sistemas distribuídos
- Resilience4j: Retry Module Documentation — Documentação da biblioteca Resilience4j para implementação de retry em Java