RPC调用失败重试需严格区分临时性故障且幂等,优先用status.FromError解包判断code,仅codes.Unavailable等可重试,配合指数退避+jitter+context感知,超时须注入ctx,熔断降级同步启用,重试≤3次。

大规模 RPC 调用失败时,不能靠“多试几次”硬扛——错误类型没分清、重试没退避、超时没协同,轻则放大抖动,重则触发雪崩。核心判断标准就一条:是否为临时性故障(如网络闪断、服务瞬时过载),且调用本身幂等。
gRPC 中 status.Code() 必须解包后判断
直接对 err.Error() 做字符串匹配(比如 "timeout" 或 "unavailable")在大规模场景下极不可靠:中间件会包装错误、gRPC 版本升级可能改写 message、日志脱敏还可能抹掉关键词。必须先用 status.FromError(err) 解包,再取 .Code()。
- 可重试的 code:仅限
codes.Unavailable、codes.DeadlineExceeded、codes.ResourceExhausted(限流)、codes.Internal(但需额外检查 message 是否含"panic"或"nil pointer") - 绝对不可重试的 code:
codes.InvalidArgument、codes.PermissionDenied、codes.NotFound、codes.AlreadyExists - 若
status.FromError(err)返回false,说明是底层 transport 错误(如net.OpError),这类也应纳入重试范围
重试策略必须带指数退避 + jitter + context 感知
简单 for 循环加 time.Sleep(100 * time.Millisecond) 在 QPS 过万时就是定时炸弹:所有请求在 100ms 后扎堆重试,瞬间压垮下游。退避必须动态、随机、有上限。
- 起始延迟建议
100 * time.Millisecond,每次翻倍,上限设为1s(防止长尾累积) - 每次 delay 乘上
0.5 ~ 1.5的随机因子(jitter),避免重试洪峰 - 必须用
backoff.Retry(来自github.com/cenkalti/backoff/v4)或retry.Do(配retry.Context(ctx)),不能手写裸循环 - 每次重试前要
select判断ctx.Done(),一旦上游取消,立即终止整个重试链
net/rpc 调用失败时必须手动注入超时上下文
net/rpc 的 Client.Call 和 Client.Go 不接受 context.Context,默认无限等待。大规模部署中,一个卡死的连接会拖垮整个 goroutine 池。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法:用
context.WithTimeout创建子 ctx,启动 goroutine 发起client.Go,同时select等待donechannel 或超时 - 超时后不要只关 channel,建议调用
client.Close()(如果连接已半开)并新建 client 实例,避免复用坏连接 - 别在重试循环里反复
dial—— 连接池失效;应在 client 初始化阶段设置net.Dialer.Timeout和KeepAlive - 错误判断优先用类型断言:
errors.As(err, &net.OpError{})或errors.Is(err, io.EOF),而非字符串匹配
重试不是万能解药,熔断和降级必须同步启用
当 10 秒窗口内失败率超 50% 且请求数 ≥ 20 时,重试已无意义——此时该熔断,而不是继续发请求。大规模场景下,单靠重试只会让问题扩散更快。
- 熔断器需独立配置:失败率阈值、最小请求数、熔断持续时间(建议 30s 起),且状态需跨 goroutine 共享(用
sync/atomic或专用库如sony/gobreaker) - 熔断开启后,所有调用应快速返回预设降级响应(如缓存数据、空对象、兜底文案),不走网络
- 重试次数必须 ≤ 3(含首次),否则总耗时易突破 SLA;高 QPS 服务建议直接设为 2
- 关键点常被忽略:重试期间的 traceID 必须透传(gRPC 需服务端开启
grpc.EnableTracing),否则监控里看不出是第几次重试


















