← 中文资源

简体中文版本

指数退避加抖动的重试

实现封顶指数退避加全抖动的重试循环:错误按可重试性分类,永久错误立即放弃,等待期间响应 context 取消。

阅读英文原文

示例说明

固定间隔重试在下游过载时火上浇油;纯指数退避会让全部客户端在同一时刻齐射。抖动是解药——每次退避随机减去零到一半的时长,重试时间点在时间轴上散开。分类同样关键:参数错误重试一万次还是失败,只有瞬态错误值得等待重试。

技术要点

  • 位移运算实现指数增长并封顶
  • 全抖动打散多客户端重试相位
  • select 同时响应超时与取消

代码解读

backoff 用左移计算二的次幂倍数,溢出或超限都封顶到上限值;抖动区间是延迟的一半。withRetry 每轮先查 context,执行后分类——永久错误直接返回带上下文的包装错误。等待用 select 双路:到时或被取消 whichever first。主函数模拟前两次失败第三次成功。

使用建议

抖动模式有全抖动与等抖动两种,后者对下游更温和。重试要配配额防雪崩。错误分类与错误码体系对齐而非匹配文案。

代码示例

package main

import (
	"context"
	"errors"
	"fmt"
	"math/rand"
	"time"
)

var (
	errTransient = errors.New("transient upstream failure")
	errPermanent = errors.New("invalid argument")
)

func classify(err error) bool {
	if errors.Is(err, errPermanent) {
		return false // 永久错误不值得重试
	}
	return errors.Is(err, errTransient) || isNetworkErr(err)
}

func isNetworkErr(err error) bool {
	var netErr interface{ Timeout() bool }
	return errors.As(err, &netErr)
}

func backoff(attempt int, base, cap time.Duration) time.Duration {
	exp := base << attempt // base * 2^attempt
	if exp > cap || exp <= 0 {
		exp = cap
	}
	jitter := time.Duration(rand.Int63n(int64(exp) / 2))
	return exp - jitter // 全抖动:避免多客户端同步重试
}

func withRetry(ctx context.Context, fn func() error, maxAttempts int) error {
	var lastErr error

	for attempt := 0; attempt < maxAttempts; attempt++ {
		if err := ctx.Err(); err != nil {
			return fmt.Errorf("context done: %w", err)
		}
		if err := fn(); err == nil {
			return nil
		} else {
			lastErr = err
			if !classify(err) {
				return fmt.Errorf("permanent failure: %w", err)
			}
		}
		if attempt < maxAttempts-1 {
			wait := backoff(attempt, 100*time.Millisecond, 5*time.Second)
			select {
			case <-time.After(wait):
			case <-ctx.Done():
				return ctx.Err()
			}
		}
	}
	return fmt.Errorf("gave up after %d attempts: %w", maxAttempts, lastErr)
}

func main() {
	calls := 0
	err := withRetry(context.Background(), func() error {
		calls++
		if calls < 3 {
			return errTransient
		}
		return nil
	}, 5)
	fmt.Println(err, "calls:", calls)
}