客户端设了 1 秒超时,这个截止时间怎么穿过三层服务?线上是什么格式?中间一层忘了传 context 会怎样?服务端怎么知道客户端已经放弃?这篇用抓包和三层调用逐个验证。
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 传给下游。
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.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
因为线上传的是相对时长,不需要两台机器时钟同步,代价是传输延迟不会被扣除:服务端的截止时间比客户端的晚了传输那一段。所以每一层都应该给下游留出余量(超时预算,见弹性系列)。