grpc客户端需在service config中配置retrypolicy(如通过grpc.withserviceconfig),指定retryablestatuscodes字符串数组、指数退避参数(带单位),且客户端版本≥1.27.0;否则重试不生效或静默失败。

gRPC客户端怎么配基于错误码的重试
gRPC默认不重试,必须显式开启并配置策略。关键不是“能不能”,而是“在哪配、配什么、配错会怎样”。RetryPolicy要写在服务端返回的service config里(通过grpc.WithServiceConfig传给客户端),或者用grpc.WithDefaultServiceConfig全局设——别指望在CallOption里塞重试逻辑,那行不通。
-
retryableStatusCodes必须用字符串形式列全,比如["UNAVAILABLE", "DEADLINE_EXCEEDED"],写503或14都无效 - 指数退避的
initialBackoff、maxBackoff、backoffMultiplier全得是字符串,带单位,如"100ms"、"2s",写100或2000直接被忽略 - 客户端版本必须≥1.27.0,旧版只支持简单重试,不认
retryPolicy字段
为什么重试后还是立刻失败
常见原因是服务端没返回可重试状态码,或者客户端压根没加载到配置。gRPC重试只响应UNAVAILABLE、DEADLINE_EXCEEDED、RESOURCE_EXHAUSTED等明确标记为可重试的错误;NOT_FOUND、INVALID_ARGUMENT这类业务错误永远不重试,哪怕你写进retryableStatusCodes也没用。
- 用
grpcurl -plaintext -v localhost:8080 your.Service/Method抓原始响应,确认grpc-status头确实是14(UNAVAILABLE) - 检查
WithServiceConfig传入的JSON是否合法,尤其注意引号和逗号——少个引号,整个配置静默失效 - Go客户端需调用
grpc.Dial时显式传入配置,不能只靠环境变量或文件自动加载
指数退避参数调不对会卡死请求
退避时间不是越长越好,也不是越短越稳。initialBackoff太小(如"1ms"),连续失败会迅速打满重试次数;太大(如"5s"),单次失败就拖慢整个链路。真实场景下,建议从"100ms"起步,maxBackoff不超过"2s",backoffMultiplier设1.6比2.0更平滑。
- 重试次数上限(
maxAttempts)默认是5,含首次调用,即最多再试4次——别以为写5就是总共发5次 - 所有退避时间会被截断到毫秒级,
"1.234s"等价于"1234ms",但"1.2345s"会四舍五入成"1234ms",不是截断 - 如果服务端响应带
grpc-encoding或grpc-encoding: identity以外的头,某些老版本gRPC会拒绝重试,哪怕状态码对得上
Go里手动实现重试反而更可控
当服务端无法统一配service config,或需要按响应内容动态判断(比如某字段为"retryable": true),硬套内置重试策略容易失控。这时候用for循环+time.Sleep自己控节奏,反而更透明。
for i := 0; i < maxRetries; i++ {
resp, err := client.DoSomething(ctx, req)
if err == nil {
return resp, nil
}
if status.Code(err) != codes.Unavailable && status.Code(err) != codes.DeadlineExceeded {
return nil, err
}
if i == maxRetries-1 {
return nil, err
}
d := time.Duration(math.Pow(1.6, float64(i)) * float64(baseDelayMS)) * time.Millisecond
time.Sleep(d)
}- 手动重试能精确控制哪些错误进退避、哪些直接抛,不受
service config限制 - 注意
ctx可能已超时,每次重试前最好用ctx, cancel := context.WithTimeout(parentCtx, timeout)重包一层 - 别在重试循环里复用
req指针——如果req含时间戳或随机ID,必须深拷贝,否则多次请求发的是一模一样的数据
最麻烦的从来不是写对退避公式,而是确认服务端真的返回了你认为它该返回的状态码。抓包看grpc-status,比翻文档快十倍。










