backoff.retry 是最稳妥的起点,它封装了延迟计算、上下文取消和随机抖动(jitter);需用 errors.is 判断错误类型,http 非 2xx 响应要读取响应体并关闭 body;应封装 shouldretry 统一判断重试逻辑,post 默认不重试;并发请求必须隔离退避状态,每个请求新建 backoff.exponentialbackoff 实例或调用 b.reset();使用独立 *rand.rand;避免 time.afterfunc 或 time.ticker 导致 goroutine 泄漏;延迟逻辑需可 mock 以支持快速测试;注意处理 backoff.stop 以终止重试。

用 backoff.Retry 做带 jitter 的延迟重试
直接调 backoff.Retry 是最稳妥的起点,它把延迟计算、上下文取消、随机抖动(jitter)都封装好了,不用自己算 time.Sleep(100 * time.Millisecond * (1 这种容易溢出又没扰动的逻辑。
常见错误现象:手写 for 循环 + time.Sleep,结果 context 超时后还在 sleep;多个请求复用同一个 backoff.BackOff 实例,导致第 3 次失败却拿到第 1 次的间隔;重试全堆在 200ms、400ms、800ms 这几个刻度上,下游刚恢复就被脉冲打挂。
- 必须用
backoff.WithContext(b, ctx)包一层,不能只把ctx传进业务函数里 —— 否则退避等待本身不响应 cancel - 每次复用
backoff.BackOff实例前,务必调b.Reset(),否则NextBackOff()返回的间隔会串号 - 别用
backoff.NewExponentialBackOff()默认构造,它的MaxInterval = 1 * time.Second太小,第 4 次重试就卡死在 1s,建议显式设MaxInterval: 30 * time.Second - 初始间隔推荐
100 * time.Millisecond,比 1s 更友好:首错不拖太久,也给下游留出恢复窗口
只对临时性错误重试,别把 400 当网络问题
重试不是兜底,是针对「可能恢复的故障」的补救。盲目 retry 会让错误放大,比如把 json.SyntaxError 或 http.StatusUnauthorized 当成连接失败反复发请求。
使用场景:HTTP 调用中,只重试 net.OpError、context.DeadlineExceeded(注意:这是上一次请求的超时,新请求要重置 context)、http.StatusCode >= 500;gRPC 中只重试 codes.Unavailable、codes.ResourceExhausted。
- 用
errors.Is(err, xxx)判断错误类型,别用strings.Contains(err.Error(), "timeout")—— 错误信息可能本地化或变更 - HTTP 场景下,
err == nil但resp.StatusCode != 200也要检查,非 2xx 响应体记得读完并resp.Body.Close() - 封装统一判断函数
shouldRetry(err error) bool,收口逻辑,别散落在各处 - POST 请求默认不重试(非幂等),若服务端明确保证幂等,才在
CheckRetry或包装函数中放行
并发请求必须隔离退避状态,别共享 backoff 实例
每个请求的退避计数器必须独立。如果把 backoff.BackOff 声明为全局变量或结构体字段长期持有,A 请求第 3 次失败会拿到 B 请求第 1 次的间隔,整个重试节奏就乱了。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
性能影响:共享实例会导致重试时间错位,轻则延迟不准,重则大量请求在错误时刻集中发起,形成“重试风暴”。
- 每个请求新建
backoff.ExponentialBackOff实例,或至少每次调用前b.Reset() - 每个请求用独立的
*rand.Rand,别用全局rand.Intn—— 并发修改rand.Source会 panic - 如果用
retryablehttp.Client,它内部已做隔离,但要注意Client.RetryMax是 per-request 的,不是全局计数器 - 别把
time.AfterFunc或time.Ticker塞进重试循环 —— 它们不感知上下文取消,timer 可能漏 stop 导致 goroutine 泄漏
测试时禁用真实 sleep,用可注入的延迟逻辑
单元测试里跑 time.Sleep(2 * time.Second) 不现实。真实延迟必须可 mock,否则测试要么慢得无法接受,要么跳过关键路径。
实操建议:把延迟逻辑抽成函数参数,例如 func() time.Duration,测试时返回 0 或固定值,主流程仍走完整退避判断。
- 第三方库如
github.com/cenkalti/backoff/v4支持Clock接口注入,可传入 mock clock - 自己封装时,避免在重试函数内部硬写
time.Sleep,改用传入的 delayFn 计算 - 测试中验证:是否在第 3 次失败后正确返回错误;是否在 context cancel 后立刻退出,不等待下一次 delay
- 别忽略
backoff.Stop的处理 —— 当MaxElapsedTime到达时,NextBackOff()返回该值,此时应终止重试
真正难的不是写对第一次重试,而是让第 5 次失败时的 jitter 值、context 取消时机、错误分类边界都刚好落在合理区间——这些细节不会报错,但会在高并发压测或下游抖动时突然暴露。










