Retry policies e backoff exponencial
1. Fundamentos de Retry Policies
Em sistemas distribuídos, falhas são inevitáveis. Redes instáveis, picos de tráfego e serviços temporariamente indisponíveis fazem parte do dia a dia de qualquer aplicação que se comunica com outros serviços. Retry policies são estratégias que definem como e quando devemos repetir uma operação que falhou.
Os erros tratáveis por retry são chamados de erros transitórios — aqueles que têm alta probabilidade de serem resolvidos após um breve período. Exemplos incluem timeouts de rede, conexões recusadas, e respostas HTTP 503 (Service Unavailable). Já os erros permanentes (como 400 Bad Request ou 404 Not Found) não devem ser repetidos, pois a operação sempre falhará.
A estrutura mais básica de retry em Go utiliza um loop for combinado com time.Sleep:
func operacaoComRetry() error {
maxTentativas := 3
var err error
for i := 0; i < maxTentativas; i++ {
err = chamadaExterna()
if err == nil {
return nil
}
time.Sleep(1 * time.Second)
}
return err
}
Embora funcional, essa abordagem ingênua tem limitações sérias: espera fixa entre tentativas e nenhum controle sobre o tempo total de espera.
2. Backoff Exponencial: Conceito e Implementação
Backoff exponencial resolve o problema da espera fixa aumentando progressivamente o intervalo entre tentativas. A fórmula matemática é:
delay = base * 2^n
Onde base é o intervalo inicial (ex: 100ms) e n é o número da tentativa (começando em 0).
Implementação manual em Go:
func backoffExponencial(base time.Duration, tentativa int) time.Duration {
segundos := base.Seconds() * math.Pow(2, float64(tentativa))
return time.Duration(segundos) * time.Second
}
func operacaoComBackoff() error {
maxTentativas := 5
base := 100 * time.Millisecond
for i := 0; i < maxTentativas; i++ {
err := chamadaExterna()
if err == nil {
return nil
}
espera := backoffExponencial(base, i)
time.Sleep(espera)
}
return fmt.Errorf("falha apos %d tentativas", maxTentativas)
}
O problema do backoff puro é que, com muitas tentativas, o tempo de espera cresce rapidamente. Para 5 tentativas com base de 100ms, a última espera seria de 3.2 segundos. Além disso, sem aleatoriedade, todos os clientes que falham simultaneamente tentarão novamente no mesmo momento, causando o efeito manada (thundering herd).
3. Jitter: Adicionando Aleatoriedade ao Backoff
Jitter introduz variação aleatória nos intervalos de espera para distribuir as tentativas de retry no tempo. Existem três tipos principais:
Full jitter: aleatoriza entre 0 e o valor máximo calculado
func fullJitter(base time.Duration, tentativa int) time.Duration {
maxEspera := float64(base) * math.Pow(2, float64(tentativa))
return time.Duration(rand.Float64() * maxEspera)
}
Equal jitter: divide o intervalo em duas partes, uma fixa e uma aleatória
func equalJitter(base time.Duration, tentativa int) time.Duration {
espera := float64(base) * math.Pow(2, float64(tentativa))
metade := espera / 2
return time.Duration(metade + rand.Float64()*metade)
}
Decorrelated jitter: usa o valor anterior para calcular o próximo
func decorrelatedJitter(base, max time.Duration, anterior time.Duration) time.Duration {
min := float64(base)
maxJitter := float64(anterior) * 3
if maxJitter > float64(max) {
maxJitter = float64(max)
}
return time.Duration(min + rand.Float64()*(maxJitter-min))
}
4. Bibliotecas Prontas no Ecossistema Go
cenkalti/backoff
A biblioteca mais popular, oferece implementações robustas de backoff:
import "github.com/cenkalti/backoff/v4"
func main() {
b := backoff.NewExponentialBackOff()
b.InitialInterval = 500 * time.Millisecond
b.MaxInterval = 30 * time.Second
b.MaxElapsedTime = 2 * time.Minute
err := backoff.Retry(func() error {
return chamadaExterna()
}, b)
}
RetryNotify adiciona callbacks para falhas parciais:
err := backoff.RetryNotify(
operacao,
b,
func(err error, d time.Duration) {
log.Printf("Falha na tentativa, esperando %v: %v", d, err)
},
)
avast/retry-go
Oferece API declarativa com opções:
import "github.com/avast/retry-go/v4"
func main() {
err := retry.Do(
func() error { return chamadaExterna() },
retry.Attempts(5),
retry.Delay(100 * time.Millisecond),
retry.MaxDelay(10 * time.Second),
retry.DelayType(retry.BackOffDelay),
)
}
hashicorp/go-retryablehttp
Especializado para clientes HTTP:
import "github.com/hashicorp/go-retryablehttp"
func main() {
client := retryablehttp.NewClient()
client.RetryMax = 3
client.RetryWaitMin = 1 * time.Second
client.RetryWaitMax = 10 * time.Second
resp, err := client.Get("https://api.exemplo.com/recurso")
}
5. Integração com Context e Cancelamento
Context.Context permite propagar deadlines e cancelamentos através do pipeline:
func operacaoComContext(ctx context.Context) error {
b := backoff.NewExponentialBackOff()
b.MaxElapsedTime = 30 * time.Second
return backoff.Retry(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
return chamadaExterna()
}, backoff.WithContext(b, ctx))
}
Verificação manual dentro do loop:
func operacaoManual(ctx context.Context) error {
b := backoff.NewExponentialBackOff()
for {
err := chamadaExterna()
if err == nil {
return nil
}
espera := b.NextBackOff()
if espera == backoff.Stop {
return err
}
select {
case <-time.After(espera):
case <-ctx.Done():
return ctx.Err()
}
}
}
6. Estratégias Avançadas de Retry
Circuit Breaker
Interrompe tentativas após um limite de falhas consecutivas:
type CircuitBreaker struct {
maxFalhas int
falhasAtuais int
mu sync.Mutex
}
func (cb *CircuitBreaker) Execute(fn func() error) error {
cb.mu.Lock()
if cb.falhasAtuais >= cb.maxFalhas {
cb.mu.Unlock()
return fmt.Errorf("circuit breaker aberto")
}
cb.mu.Unlock()
err := fn()
if err != nil {
cb.mu.Lock()
cb.falhasAtuais++
cb.mu.Unlock()
} else {
cb.mu.Lock()
cb.falhasAtuais = 0
cb.mu.Unlock()
}
return err
}
Retry baseado em status HTTP
Nem todo erro merece retry:
func deveRetentar(statusCode int) bool {
switch statusCode {
case http.StatusTooManyRequests, http.StatusServiceUnavailable,
http.StatusGatewayTimeout, http.StatusBadGateway:
return true
default:
return false
}
}
7. Monitoramento e Logging de Tentativas
Logging estruturado com slog:
import "log/slog"
func operacaoMonitorada() error {
logger := slog.Default()
b := backoff.NewExponentialBackOff()
return backoff.RetryNotify(
func() error { return chamadaExterna() },
b,
func(err error, d time.Duration) {
logger.Warn("retry",
"erro", err,
"proxima_espera", d,
)
},
)
}
Métricas com Prometheus:
var (
retryCount = promauto.NewCounter(prometheus.CounterOpts{
Name: "operacoes_retry_total",
Help: "Total de operações com retry",
})
retryLatency = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "operacoes_retry_latency_seconds",
Help: "Latência das operações com retry",
Buckets: prometheus.DefBuckets,
})
)
8. Boas Práticas e Armadilhas Comuns
Limite máximo de tentativas e tempo total: Sempre defina limites para evitar loops infinitos:
b := backoff.NewExponentialBackOff()
b.MaxElapsedTime = 1 * time.Minute // tempo total máximo
b.MaxInterval = 10 * time.Second // intervalo máximo entre tentativas
Evite goroutines presas: Use context para cancelamento e sempre verifique ctx.Err():
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := operacaoComContext(ctx)
}()
Testes unitários: Mock de temporizadores com pacotes como clock:
import "github.com/benbjohnson/clock"
func TestBackoff(t *testing.T) {
mockClock := clock.NewMock()
b := backoff.NewExponentialBackOff()
b.Clock = mockClock
// avança o tempo manualmente nos testes
}
Armadilha comum: esquecer de resetar o backoff entre operações bem-sucedidas:
func handler() {
b := backoff.NewExponentialBackOff()
for {
err := operacao()
if err != nil {
time.Sleep(b.NextBackOff())
} else {
b.Reset() // essencial!
}
}
}
Referências
- cenkalti/backoff v4 - Documentação oficial — Pacote Go com implementações de backoff exponencial, jitter e integração com context
- avast/retry-go - GitHub — Biblioteca declarativa de retry com suporte a backoff exponencial e jitter
- hashicorp/go-retryablehttp - Documentação — Cliente HTTP com retry automático e backoff exponencial
- AWS Architecture Blog - Exponential Backoff and Jitter — Artigo clássico sobre estratégias de backoff e tipos de jitter
- Google Cloud - Retry Design Pattern — Guia oficial do Google sobre implementação de retry com backoff exponencial
- Microsoft - Retry pattern — Documentação do padrão de retry com exemplos e considerações de design
- Go slog package documentation — Documentação oficial do pacote de logging estruturado do Go
- Prometheus Go client library — Biblioteca oficial para métricas Prometheus em Go