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