grpc go客户端重试需手动配置retrypolicy并显式启用,http调用重试须自研封装且注意幂等性、请求重建与上下文取消。

Go 微服务中直接用 net/rpc 或 gRPC 自带的重试是不行的——标准库不提供重试逻辑,gRPC Go 客户端默认也关闭重试(需显式配置且仅限于特定状态码和方法类型)。
gRPC Go 客户端启用重试必须手动配置 grpc.RetryPolicy
gRPC 的重试不是开箱即用的功能,需要在 Dial 时传入 grpc.WithDefaultServiceConfig,并指定 JSON 格式的重试策略。常见错误是只写了策略但没生效,原因通常是:
- 服务端未返回可重试的状态码(如
UNAVAILABLE、DEADLINE_EXCEEDED;FAILED_PRECONDITION等默认不可重试) - 方法未声明为「幂等」(
retryable: true必须在 proto 的service_config中显式标注) - 客户端配置了重试策略,但没通过
WithDefaultServiceConfig注入,而是误用了WithServiceConfig(后者只对后续 NewClient 生效)
示例配置片段:
{
"methodConfig": [{
"name": [{"service": "helloworld.Greeter", "method": "SayHello"}],
"retryPolicy": {
"MaxAttempts": 4,
"InitialBackoff": "0.1s",
"MaxBackoff": "1s",
"BackoffMultiplier": 2,
"RetryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]
}
}]
}注意:该 JSON 必须转成字符串后传给 grpc.WithDefaultServiceConfig,不能直接写 struct。
立即学习“go语言免费学习笔记(深入)”;
自研重试逻辑时优先用 backoff.Retry 而非手写 for+sleep
自己封装重试容易忽略退避(backoff)、上下文取消、错误分类等关键点。社区成熟的 backoff.Retry(来自 github.com/cenkalti/backoff/v4)能自动处理指数退避、jitter、context.Done() 中断。常见疏漏包括:
- 未将原始 error 包装进重试判断逻辑,导致网络超时和业务错误混在一起重试
- 重试次数硬编码在循环里,无法动态调整或按错误类型分级(比如连接失败重试 3 次,认证失败直接失败)
- 忘记传递
ctx到底层调用,导致重试阻塞无法被 cancel
典型用法:
err := backoff.Retry(func() error {
resp, err := client.SayHello(ctx, req)
if err != nil {
// 只对特定错误重试
if isNetworkError(err) || status.Code(err) == codes.Unavailable {
return err
}
return backoff.Permanent(err) // 终止重试
}
// 成功逻辑
*out = *resp
return nil
}, backoff.WithContext(backoff.NewExponentialBackOff(), ctx))HTTP-based RPC(如 Gin + JSON)重试要自己控制 transport 层
如果微服务间走的是 HTTP(比如用 http.Client 调用 REST 接口),gRPC 那套配置完全不适用。此时重试必须落在 http.Client 或请求封装层。关键点:
-
http.Client.Timeout不影响重试行为,它只控制单次请求总耗时;重试需靠外层逻辑驱动 - 不要复用同一个
*http.Request多次调用Do()—— body 可能已被读取,导致后续请求 body 为空 - 若接口有 side effect(如扣库存),必须确保重试前校验幂等性(例如依赖
Idempotency-Keyheader 或服务端 token)
建议把重试封装成独立函数,每次构造新 request:
func doWithRetry(ctx context.Context, reqFn func() (*http.Request, error), client *http.Client, maxRetries int) (*http.Response, error) {
var resp *http.Response
var err error
for i := 0; i <= maxRetries; i++ {
req, err := reqFn()
if err != nil {
return nil, err
}
resp, err = client.Do(req.WithContext(ctx))
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
if i == maxRetries {
break
}
time.Sleep(time.Second * time.Duration(1<<i)) // 简单退避
}
return resp, err
}重试边界必须明确区分 transient error 和 permanent error
重试不是万能解药。很多团队把所有 error 都丢进重试逻辑,结果放大问题:比如数据库唯一键冲突、参数校验失败、权限不足,这些错误重试多少次都是同样结果,还可能引发雪崩。判断依据应基于错误语义而非错误类型:
-
connection refused、i/o timeout、UNAVAILABLE→ transient,适合重试 -
INVALID_ARGUMENT、PERMISSION_DENIED、NOT_FOUND(针对写操作)→ permanent,应立即失败 - 即使同个错误码,也要结合场景判断:对「创建订单」返回
ALREADY_EXISTS是永久错误;对「查询订单」则是可接受的成功分支
最易被忽略的一点:重试逻辑本身不能掩盖上游超时。如果外层 context 已 deadline,重试循环必须响应 ctx.Err() 并退出,否则会卡住整个调用链。










