bigywater.tech
gRPC · 5

deadline:截止时间怎么跨服务传播

客户端设了 1 秒超时,这个截止时间怎么穿过三层服务?线上是什么格式?中间一层忘了传 context 会怎样?服务端怎么知道客户端已经放弃?这篇用抓包和三层调用逐个验证。

999978u
1 秒的 context 超时在线上 grpc-timeout 头里的值(发出那一刻剩下的微秒数)
999 → 898 → 797 ms
客户端设 1 秒,A、B、C 三层各自看到的剩余时间
0 对 1
客户端 200 ms 超时后,传递 ctx 与不传 ctx 时下游 B 最终完成的次数

线上格式

实测 · 本机执行输出
  context 超时 50ms       → grpc-timeout: 49994167n
  context 超时 1s         → grpc-timeout: 999978u
  context 超时 5s         → grpc-timeout: 4999984u
  context 超时 1m30s      → grpc-timeout: 89999969u
  context 超时 2h0m0s     → grpc-timeout: 7200000m
  context 超时 100h0m0s   → grpc-timeout: 360000S
  不设截止时间(context.Background())→ 请求头里有 grpc-timeout:false

grpc-timeout 是“最多 8 位数字 + 单位字母”的相对时长。写入的是发出调用那一刻 ctx 里剩余的时间。没有设截止时间的调用不带这个头。

跨服务传播

实测 · 本机执行输出
  A 看到剩余 999ms | B 看到剩余 898ms | C 看到剩余 797ms  (err=<nil>)

每一跳把当前剩余时间写进新的 grpc-timeout,调用链共享同一个总时限。前提是每层把收到的 ctx 传给下游。

不传 ctx 的后果

实测 · 本机执行输出
  A→B 传递 ctx=true :客户端 DeadlineExceeded 用时 201.1 ms;1.2 秒后 B 完成了 0 次、C 完成了 0 次
  A→B 传递 ctx=false:客户端 DeadlineExceeded 用时 201.1 ms;1.2 秒后 B 完成了 1 次、C 完成了 1 次

A 调 B 时用 context.Background(),下游没有截止时间,上游超时取消也传不到下游;客户端已经放弃,B 和 C 还各完成了 1 次。

过期的调用

实测 · 本机执行输出
  已过期 1 秒的 deadline → code=DeadlineExceeded message="context deadline exceeded" 用时 0.0 ms;线上的 HEADERS/DATA 帧 0 个

截止时间已过的调用在客户端本地就返回 DeadlineExceeded,0 帧上线。

服务端要自己看 ctx

实测 · 本机执行输出
  监听 ctx.Done():客户端 DeadlineExceeded,151.1 ms 返回;服务端在 151.1 ms 时发现 ctx 已取消
  不监听 ctx:客户端 DeadlineExceeded,150.6 ms 返回;1.1 秒后服务端仍把 1 秒的活干完了(完成次数 1)

客户端超时或取消会通过 RST_STREAM 取消服务端的 ctx,但 handler 不监听 ctx.Done() 就不会停,会把 1 秒的活干完。长耗时的 handler 要在循环或阻塞处检查 ctx。

相对时长

实测 · 本机执行输出
  线上额外延迟         服务端 handler 看到的剩余      此刻客户端真正还剩
  0s             999 ms                 999 ms
  100ms          999 ms                 798 ms
  300ms          999 ms                 598 ms

因为线上传的是相对时长,不需要两台机器时钟同步,代价是传输延迟不会被扣除:服务端的截止时间比客户端的晚了传输那一段。所以每一层都应该给下游留出余量(超时预算,见弹性系列)。

分步演示

deadline
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。第六节用抓包代理给客户端到服务端方向加了固定延迟(每个数据块转发前等待,所以 HEADERS 和 DATA 各被延迟一次,100 ms 的延迟让请求晚到约 200 ms);其余各节为真实调用;耗时为单次测量,几 ms 的波动属正常
    • 没有验证:其它语言实现对 grpc-timeout 的取整;网关或代理在转发时是否改写 grpc-timeout;服务端用 grpc.MaxConnectionAge 等配置对在途调用的影响;WaitForReady 与 deadline 的交互(第 3 篇已经看过)
    京ICP备2026067502号-1京公网安备11010502063487号