必须在指数退避中加入随机抖动并设超时上限,否则客户端会同步重试压垮下游;推荐用 backoff/v4.withjitter,禁用全局 rand,grpc 与 http 重试策略不可混用。

Go RPC重试导致连接数暴涨怎么办
直接上结论:不加抖动的指数退避,在服务端短暂抖动时会触发大量客户端几乎同时重试,瞬间压垮下游——这不是重试机制太激进,而是所有客户端的重试时间被同步了。
典型现象是 context.DeadlineExceeded 错误陡增,同时服务端观察到连接数、CPU、goroutine 数在几秒内翻倍。根本原因在于:默认用 time.Sleep(2^retry * time.Second) 后重试,1000 个客户端在第 3 次重试时全卡在 time.Sleep(8 * time.Second) 后同一毫秒发起请求。
- 必须在退避时间里加入随机因子,让重试“错峰”
- 退避上限要设硬限制(比如不超过 30 秒),否则单次失败可能拖太久
- 重试次数别超过 3–5 次,RPC 调用本身不是幂等操作保险柜,盲目重试可能放大语义错误
用 backoff.Retry 做带抖动的 RPC 重试
别手写 for + time.Sleep,用 github.com/cenkalti/backoff/v4 是最省心的选择。它内置了 ConstantBackOff、ExponentialBackOff 和抖动支持,且对 Go 的 context 友好。
关键点是初始化时调用 backoff.WithJitter,它会让每次退避时间在 [min, max] 区间内均匀随机——不是简单乘个 0.5~1.5 系数,而是确保分布真正打散。
立即学习“go语言免费学习笔记(深入)”;
-
backoff.NewExponentialBackOff()默认最大间隔是 128 秒,远超多数 RPC 场景,务必手动改MaxInterval - 把
context.WithTimeout套在最外层,而不是每次重试都新建,否则总超时会被重试次数稀释 - 如果 RPC 方法本身不幂等(比如含
CreateOrder),重试前得先检查是否已提交成功,靠idempotency key或服务端状态查询兜底
bo := backoff.NewExponentialBackOff()
bo.MaxInterval = 10 * time.Second
bo.MaxElapsedTime = 30 * time.Second
bo = backoff.WithJitter(bo)
<p>err := backoff.Retry(func() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := client.Call(ctx, "Service.Method", req, &resp)
return err
}, bo)
自定义抖动逻辑时,别用 rand.Float64() 全局实例
自己实现抖动时最容易踩的坑:在包级变量里用 var r = rand.New(rand.NewSource(time.Now().UnixNano())),然后在多个 goroutine 里并发调 r.Float64() ——这会引发 panic,因为 *rand.Rand 不是并发安全的。
更隐蔽的问题是:如果在函数内用 rand.NewSource(time.Now().UnixNano()) 初始化,但没传入真随机种子(比如漏了 time.Now()),所有 goroutine 会拿到相同 seed,抖动完全失效,又回到“同步重试”原点。
- 正确做法是用
rand.New(rand.NewSource(time.Now().UnixNano()))每次创建新实例,或复用全局sync.Pool管理 - 抖动范围建议控制在 ±30% 以内(如
base * (0.7 + 0.6*rand.Float64())),太大会让退避失去节奏感,太小起不到错峰作用 - 避免用
math/rand的全局函数如rand.Float64(),它们共享同一个锁,高并发下成性能瓶颈
HTTP 客户端和 gRPC 客户端的重试策略不能混用
gRPC 的 RetryPolicy(通过 grpc.DialOption 配置)只对 unary call 生效,且底层依赖服务端返回 UNAVAILABLE 或 RESOURCE_EXHAUSTED;而 HTTP 客户端(如 http.Client)需要自己 wrap RoundTrip 或用中间件拦截 5xx/timeout。两者触发条件、重试粒度、上下文传播方式完全不同。
常见误操作是给 gRPC 客户端配了 WithBlock() + 重试,结果阻塞在 DNS 解析或连接建立阶段,退避还没开始就卡死;或者对 HTTP 接口照搬 gRPC 的指数退避参数,导致 429 响应被反复重试,反而加重限流压力。
- gRPC 重试必须配合
grpc.WaitForReady(false),否则会阻塞等待连接就绪,绕过所有退避逻辑 - HTTP 重试建议只针对
net.OpError、context.DeadlineExceeded和明确的 503/504,跳过 4xx(尤其是 400/401) - 如果调用链路含负载均衡(如 envoy),需确认重试是否由 LB 统一处理,客户端再重试会造成嵌套重试,雪崩风险翻倍
重试的边界很窄:既要扛住瞬时故障,又不能把压力反射回去。抖动不是锦上添花,是防止退避算法从“容错”变成“共谋”的必要扰动。真正难的不是算出下一个 sleep 时间,而是判断此刻该不该 retry。










