bigywater.tech
gRPC · 18

重试:服务端配置(service config)里的重试策略与 hedging

gRPC 的重试是怎么工作的?配置里写 maxAttempts 就一定会试那么多次吗?什么情况下不会重试?hedging 呢?这篇用一个会按次数失败的服务端逐项验证。

3 次 / 171.8 ms
前 2 次 Unavailable、配置 retryPolicy 后:服务端收到的次数与总耗时
10 → 5
服务配置里 maxAttempts = 10 时,客户端实际只尝试 5 次
1 次
失败的响应带了响应头、或请求超过 256 KiB、或 hedging 配置下:服务端收到的次数

retryPolicy

实测 · 本机执行输出
  没有配置:服务端收到 1 次;结果 code=Unavailable message="第 1 次失败"
  有配置:服务端收到 3 次;结果 成功;总耗时 171.8 ms;每次到达的间隔 [57 112] ms
  线上每个请求的 grpc-previous-rpc-attempts 头:[(无) 1 2]
  退避的取值范围(源码:initialBackoff × multiplier^n,再乘以 0.8~1.2 的随机数):第 1 次重试前 40–60 ms,第 2 次重试前 80–120 ms
  再重复 8 次的间隔:[51 116] ms [48 87] ms [47 95] ms [46 118] ms [53 96] ms [58 87] ms [45 109] ms [45 121] ms

写在服务配置的 methodConfig 里:maxAttempts、initialBackoff、maxBackoff、backoffMultiplier、retryableStatusCodes。每次重试前等待 initialBackoff × multiplier^n,再乘以 0.8~1.2 的随机数。重试的请求会带 grpc-previous-rpc-attempts 头。

限制

实测 · 本机执行输出
  服务端返回 Internal,只允许 UNAVAILABLE 重试:服务端收到 1 次;code=Internal message="第 1 次失败"
  一直 Unavailable,配置 maxAttempts = 4:服务端收到 4 次;code=Unavailable message="max retries exhausted: failed after 4 attempts: rpc error: code = Unavailable de…"
  一直 Unavailable,配置 maxAttempts = 10:服务端收到 5 次;code=Unavailable message="max retries exhausted: failed after 5 attempts: rpc error: code = Unavailable de…"

错误码必须在 retryableStatusCodes 里;客户端把 maxAttempts 限制到 5(WithMaxCallAttempts 可调)。

什么时候不重试

实测 · 本机执行输出
  handler 里 SetHeader=false:服务端收到 3 次;成功
  handler 里 SetHeader=true :服务端收到 1 次;code=Unavailable message="第 1 次失败"

响应头已经发出(不是 Trailers-Only)就不重试;请求已经超过重试缓冲(默认 256 KiB)也不重试。所以失败的响应上不要先写响应头。

和 deadline 的关系

实测 · 本机执行输出
  服务端收到 2 次,每次到达的间隔 [105] ms;调用 252.2 ms 后返回:code=DeadlineExceeded message="context deadline exceeded"

重试的总时间受调用的 deadline 约束,退避等待期间 deadline 到期就直接以 DeadlineExceeded 结束。

服务端 pushback

实测 · 本机执行输出
  pushback = 300:服务端收到 2 次;到达间隔 [303] ms;总耗时 305.5 ms;成功
  pushback = -1 :服务端收到 1 次;到达间隔 [] ms;总耗时 1.3 ms;code=Unavailable message="第 1 次失败"

trailer grpc-retry-pushback-ms 为非负数时,客户端按它等待;为负数时不再重试。

请求缓冲

实测 · 本机执行输出
  请求 100 KiB:服务端收到 3 次;成功
  请求 300 KiB:服务端收到 1 次;code=Unavailable message="第 1 次失败"

重试要重发请求,所以客户端要把请求缓存起来;缓冲有上限。

hedging

实测 · 本机执行输出
  hedgingPolicy:服务端收到 1 次;总耗时 605.1 ms;成功

grpc-go 当前没有实现 hedging(源码里只有 TODO),配置里写了会被忽略。

分步演示

重试
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1 / grpc-go v1.84.0 / protobuf-go v1.36.12 / golang.org/x/net v0.57.0 / protoc 36.2(Homebrew);服务端和客户端都是本机起的 Go 进程(127.0.0.1,端口 9800 以上,不访问任何外部服务);需要看线上字节时用我自己写的 TCP 抓包代理加 golang.org/x/net/http2 的 Framer 解析 HTTP/2 帧;计时用 time.Now 或 perf_counter,数字每次运行会有小幅抖动。服务端按“前 N 次失败”的方式记录每次到达的时刻;grpc-previous-rpc-attempts 来自抓包代理;“最多 5 次”“Trailers-Only 才重试”“pushback”“256 KiB 缓冲”“hedging 没有实现”读自 grpc-go 源码(service_config.go、stream.go、dialoptions.go、rpc_util.go)并用实验核对
    • 没有验证:重试节流(retryThrottling,第 19 篇);流式 RPC 的重试(只测了 Unary);连接层面的透明重试(transparent retry,代码里有但本篇没有构造场景);WithMaxRetryRPCBufferSize 加大缓冲之后的行为;其它语言的重试实现;hedging 在其它实现(如 Java、C++)里的行为
    京ICP备2026067502号-1京公网安备11010502063487号