bigywater.tech
gRPC · 33

超时的叠加:客户端、网关、服务端各设一个会怎样

客户端、网关、服务各设一个超时会怎样?链路一长,deadline 是怎么一路传下去的?重试和超时叠在一起时谁说了算?这篇用 客户端 → A → B 的三层调用逐项实测,线上的 grpc-timeout 头也一并抓出来。

304.2 ms
客户端 1000 ms、A 调 B 超时 300 ms 时,客户端实际得到 DeadlineExceeded 的时间
999 → 797 → 396 ms
A 在调用 B 前先耗 0/200/600 ms 时,B 看到的剩余 deadline
做完 1 次
客户端 200 ms 放弃后,A 用 Background 调 B,B 仍然把 1500 ms 的活做完

取最小

实测 · 本机执行输出
  客户端超时 1000 ms A 调 B 的超时 无       → 客户端 成功,用时 503.5 ms;B 看到的 grpc-timeout 头 999514u;B 收到 1 次、做完 1、被取消 0
  客户端超时 1000 ms A 调 B 的超时 300 ms  → 客户端 code=DeadlineExceeded,用时 304.2 ms;B 看到的 grpc-timeout 头 299990u;B 收到 1 次、做完 0、被取消 1
  客户端超时 200 ms  A 调 B 的超时 1000 ms → 客户端 code=DeadlineExceeded,用时 202.1 ms;B 看到的 grpc-timeout 头 197163u;B 收到 1 次、做完 0、被取消 1
  客户端超时 200 ms  A 调 B 的超时 无       → 客户端 code=DeadlineExceeded,用时 200.7 ms;B 看到的 grpc-timeout 头 199184u;B 收到 1 次、做完 0、被取消 1
  客户端超时 无       A 调 B 的超时 无       → 客户端 成功,用时 503.6 ms;B 看到的 grpc-timeout 头 (无);B 收到 1 次、做完 1、被取消 0

调用链上每一层的超时叠加起来,谁先到期谁说了算。内层的超时比外层剩余时间还长就没有作用。无论哪一层先到期,客户端看到的都是 DeadlineExceeded,分不清是自己的还是下游的,要靠日志、追踪或错误详情区分。

传的是剩余时间

实测 · 本机执行输出
  A 先耗   0 ms:客户端→A 的 grpc-timeout 999419u;A→B 的 grpc-timeout 999400u;B 看到的剩余 deadline 999 ms;成功,用时 13.1 ms
  A 先耗 200 ms:客户端→A 的 grpc-timeout 998313u;A→B 的 grpc-timeout 797916u;B 看到的剩余 deadline 797 ms;成功,用时 214.9 ms
  A 先耗 600 ms:客户端→A 的 grpc-timeout 999244u;A→B 的 grpc-timeout 397083u;B 看到的剩余 deadline 396 ms;成功,用时 614.4 ms
  客户端不设超时:客户端→A 的 grpc-timeout:(无);A→B 的 grpc-timeout:(无);B 看到的剩余 deadline -1(-1 表示没有 deadline)

gRPC 在 grpc-timeout 头里传剩余时间,每一跳按当前时刻重新计算,前面耗掉的时间自然被扣掉。没设 deadline 就一路都没有。

重试要放进预算

实测 · 本机执行输出
  客户端超时 1000 ms → A 实际尝试 3 次(B 收到 3 次、被取消 3);客户端 code=DeadlineExceeded,用时 1002.1 ms
  客户端超时 2000 ms → A 实际尝试 3 次(B 收到 3 次、被取消 3);客户端 code=DeadlineExceeded,用时 1209.1 ms
  客户端超时 无       → A 实际尝试 3 次(B 收到 3 次、被取消 3);客户端 code=DeadlineExceeded,用时 1206.2 ms

外层的 deadline 是总预算。重试的次数和每次的超时加起来超出时,最后一次会被截断,而每次失败的尝试下游都做了(部分)工作。

转发要传 ctx

实测 · 本机执行输出
  A 传 ctx          客户端 code=DeadlineExceeded,用时 202.1 ms;放弃后 100 ms:B 被取消 1 次;1.8 秒后:B 做完 0 次、被取消 1 次
  A 用 Background   客户端 code=DeadlineExceeded,用时 202.1 ms;放弃后 100 ms:B 被取消 0 次;1.8 秒后:B 做完 1 次、被取消 0 次

转发下游调用必须用收到的 ctx(或它的派生),否则上游放弃之后,下游仍把活做完。

分步演示

超时叠加
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。链路是本机的三个进程内服务(客户端 → A → B),A 和 B 都是 grpc 服务器,线上 grpc-timeout 头来自两个抓包代理(分别在客户端→A、A→B 上);grpc-timeout 头的单位是“u”表示微秒;所有时间是本机测量,B 的“活”是带 ctx 监听的 sleep
    • 没有验证:网关(HTTP→gRPC)层的超时与 Grpc-Timeout 头(第 29 篇测过一部分);服务网格或代理(Envoy)的超时叠加;客户端的 WithTimeout 与 per-call 超时的区别;deadline 在跨语言调用链上的传递;gRPC 重试策略(service config)与应用层重试的叠加(第 18 篇);服务端自己没有 ctx 监听时的行为(第 31 篇)
    京ICP备2026067502号-1京公网安备11010502063487号