retry.do 默认不感知 context.context,必须显式传入 retry.context(ctx);重试次数、延迟等参数需按场景调整,错误判断应区分客户端错误与服务端临时故障,grpc 错误需用 status.fromerror 解包并结合 code 与 message 综合判断。

Go 里用 retry.Do 做基础重试容易漏掉上下文取消
直接调用第三方 RPC 接口时,网络抖动或服务端临时不可用很常见。用 retry.Do(来自 github.com/avast/retry-go)确实省事,但默认不感知 context.Context —— 如果上游已超时或主动取消,重试还在傻跑,浪费资源还拖慢整体响应。
- 必须显式传入
retry.Context(ctx),否则重试逻辑完全无视父 context 生命周期 -
retry.Attempts(3)和retry.Delay(100 * time.Millisecond)是安全起点,但别硬编码;高 QPS 场景下建议把重试次数压到 2,避免雪崩 - 错误判断要具体:只对
rpc.Error或net.OpError重试,status.Code == codes.InvalidArgument这类客户端错重试没意义
指数退避(exponential backoff)不是加个 math.Pow 就完事
简单用 time.Sleep(time.Duration(math.Pow(2, float64(attempt))) * time.Second) 会出问题:整数溢出、无 jitter 导致重试洪峰、没 cap 住最大等待时间。
- 用
retry.BackOff(同上库)或手写时,务必设上限,比如maxDelay := 5 * time.Second - 加 jitter:每次 delay 乘上
0.5 ~ 1.5的随机因子,防止大量请求在重试窗口末尾扎堆 - 别在重试循环里重新生成
http.Client或grpc.ClientConn—— 连接池复用失效,反而加重服务端压力
gRPC 调用失败后,status.Code 和 err.Error() 别混着判
gRPC 错误是包装过的,直接 strings.Contains(err.Error(), "deadline") 不可靠;而只看 status.Code 又可能漏掉底层连接断开这类非 status 错误。
- 先用
status.FromError(err)解包,拿到code和message - 常见可重试 code:
codes.Unavailable、codes.DeadlineExceeded、codes.Internal(但排除codes.Internal中明确含 "panic" 的 message) - 如果
status.FromError返回 false,说明是底层 transport 错误(如connection refused),也该重试 - 注意:
codes.Unknown要结合 message 判断,不能一概跳过
重试逻辑写在 client 层还是 service 层?
写在 client 层(比如封装一个 DoWithRetry() 方法)更可控,但容易重复;写在 service 层统一处理看似整洁,实际会让业务逻辑和容错耦合过紧,调试时难定位是哪次调用触发的重试。
立即学习“go语言免费学习笔记(深入)”;
- 推荐:每个 RPC client 实例自带重试配置(如
rpcclient.New(..., rpcclient.WithRetryPolicy(...))),由调用方按需开关 - 禁止在中间件(如 gin handler)里全局加重试 —— 同一个请求里多个 RPC 调用会被反复重试,放大延迟
- 日志必须打清楚:第几次重试、当前 delay 时间、原始 error、是否最终成功 —— 否则线上出问题根本没法区分是网络抖动还是服务真挂了
重试不是兜底,而是给不稳定留出喘息时间;真正难的是判断“这次值不值得再试一次”,这得靠具体错误语义,而不是套模板。










