gRPC 微服务不能用 retryablehttp 做重试,因其仅支持 HTTP/1.1,而 gRPC 基于 HTTP/2,存在状态码映射、流控、元数据透传等根本性不兼容,硬套会导致重试失效或 panic。

gRPC 微服务不能用 retryablehttp 做重试——它只支持 HTTP/1.1,而 gRPC 走的是 HTTP/2,底层状态码映射、流控、元数据透传全不兼容,硬套会导致重试失效甚至 panic。
为什么 retryablehttp 在 gRPC 场景下必然失败
这是最常被踩的坑:开发者看到 “HTTP 可重试” 就想复用现有库,但没意识到协议栈已切换。gRPC 的错误不是 503 Service Unavailable 这类字符串,而是 codes.Unavailable 枚举值;它的超时由 context.DeadlineExceeded 触发,不是 net.Error.Timeout();它的请求体是二进制 Protocol Buffer,无法像 HTTP Body 那样简单重放。
-
retryablehttp依赖net/http.Client,完全无法解析status.FromError(err)返回的 gRPC 状态 - 它对
codes.DeadlineExceeded也会重试,但该错误本身已是超时结果,再重试只会加剧雪崩 - 它不处理 gRPC 的
metadata.MD透传,重试后 trace ID、鉴权 token 全丢失 - 一旦遇到流式 RPC(
stream.SendMsg),直接 panic:不支持流式重试
正确做法:用 gRPC 拦截器 + backoff 实现带 jitter 的指数退避
必须用 grpc.WithUnaryInterceptor 或 grpc.WithStreamInterceptor 注入自定义逻辑,核心是三件事:判断是否可重试、控制退避时间、隔离上下文 deadline。
- 只重试临时性错误:
codes.Unavailable、codes.ResourceExhausted、codes.Aborted - 绝对跳过语义错误:
codes.InvalidArgument、codes.NotFound、codes.AlreadyExists - 用
github.com/cenkalti/backoff/v4配置退避策略,关键参数不是最大次数,而是MaxElapsedTime(总耗时上限) - 每次重试前必须调用
bo.NextBackOff()获取新延迟,不能复用初始值 - 重试时要
ctx, cancel := context.WithTimeout(parentCtx, timeout),否则原 ctx 的 deadline 会传染过去
bo := backoff.NewExponentialBackOff() bo.InitialInterval = 100 * time.Millisecond bo.MaxInterval = 2 * time.Second bo.MaxElapsedTime = 10 * time.Second bo.Multiplier = 2.0 bo.RandomizationFactor = 0.5 // 加抖动,防同步重试
重试不是万能的:必须配合幂等校验与熔断器
重试接口如 CreateOrder 或 PayOrder,若没服务端幂等控制,两次重试 = 两笔订单或两次扣款。这不是客户端能解决的问题。
立即学习“go语言免费学习笔记(深入)”;
- 业务层必须显式标记接口是否可重试,例如通过
idempotency-keyheader 或 request 字段传递 - 服务端收到后需先查 DB 或 Redis 是否已存在该 key,存在则直接返回上次结果
- 客户端重试前,应检查熔断器状态:
if cb.State() == gobreaker.StateClosed || cb.State() == gobreaker.StateHalfOpen - 熔断器阈值建议设为连续 20 次调用中失败 ≥10 次(50% 错误率),Open 状态持续 30 秒
- 限流要前置在重试之前:先用
go.uber.org/ratelimit控制 QPS,再走熔断,最后才进重试循环
HTTP 客户端重试可以更宽松,但仍有硬约束
如果只是普通 HTTP 调用(非 gRPC),retryablehttp 是可用的,但要注意三个边界条件:
- 只对
GET、HEAD方法重试;POST、PUT必须确保 Body 可重放(*bytes.Reader安全,os.File不安全) - 响应码仅重试
408(Request Timeout)、429(Too Many Requests)、5xx;400、401、403等一律跳过 - 必须设置
Client.RetryMax(建议 ≤3),且外层用context.WithTimeout控制总耗时,避免退避叠加导致整体超时
真正难的不是写重试代码,而是厘清“这个请求到底能不能重试”——这取决于接口语义、服务端实现、上下游 SLA 协议,而不是客户端的一厢情愿。










