bigywater.tech
微服务 · 1

超时预算与截止时间:调用链上每一跳该分多少,排队超过截止时间就别处理

一条调用链上,每一跳的超时该设多少?固定超时、传播剩余预算、重试和排队分别会发生什么?这篇用本机起的几个小 HTTP 服务,把“预算”在多跳之间怎么被吃掉、被浪费量出来。

123.6 s → 73.9 s
C 花在干活上的总时间:每一跳固定 1000 ms 超时 对 传播剩余预算(成功数都是 126 / 200)
120 → 80
重试 3 次、每次 500 ms 对 每次不超过剩余预算时,C 收到的调用数(前者 40 次是 client 放弃之后才发来的)
34 → 162
到达率 80/s、容量 50/s 时 400 ms 内成功的请求数:来者不拒 对 出队时丢掉已过期的

固定超时 vs 传播截止时间

实测 · 本机执行输出
  策略               | client 1 s 内拿到结果 | client 超时 | 拿到 504 | C 花在干活上的总时间 | C 把活干完的次数 | 干完时 client 早已放弃的次数
  -----------------+------------------+-----------+--------+-------------+-----------+-------------------
  每一跳固定 1000 ms 超时 | 126              | 70        | 4      | 123.6 s     | 200       | 74                
  传播剩余预算           | 126              | 0         | 74     | 73.9 s      | 126       | 0                 
  这 200 个请求里按随机数事先数出来有 74 个会让 C 慢(1500 ms);1500 ms 的活在 1000 ms 的预算里本来就做不完,所以不论什么策略,这 74 个都拿不到结果;区别是 C 要不要把它们干完

两种策略的成功数一样,差别在下游:固定超时的下游不知道 client 已经走了,会把活干完;传播剩余预算后下游可以提前停下。

重试怎么吃掉预算

实测 · 本机执行输出
  B 的策略               | client 在 1 s 内得到 B 的回复(504) | client 超时(什么也没等到) | client 看到的延迟中位数 | C 一共收到的调用 | 其中 client 放弃之后才发来的
  --------------------+-----------------------------+-------------------+-----------------+-----------+-------------------
  固定:每次 500 ms,最多 3 次 | 0                           | 40                | 1002 ms         | 120       | 40                
  每次 ≤ 剩余预算           | 40                          | 0                 | 985 ms          | 80        | 0                 

总耗时 = 次数 × 每次超时。不看剩余预算,最后一次尝试会在 client 放弃之后才发出。

预算怎么分

实测 · 本机执行输出
  策略               | 1 s 内成功   | 成功率   | 成功请求延迟中位数 | 成功请求延迟 p99
  -----------------+-----------+-------+-----------+-----------
  预算平分:每跳 333 ms   | 350 / 400 | 87.5% | 316 ms    | 337 ms    
  共用剩余预算:每跳最多用掉剩下的 | 393 / 400 | 98.2% | 315 ms    | 823 ms    
  按随机数事先数出来:至少一跳慢的请求 50 个(平分必败),至少两跳慢的请求 7 个(共用预算也会超过 1000 ms:600 + 600 > 1000)

平分预算让任何一跳变慢都必败;共用剩余预算把前面省下的时间留给后面,代价是成功请求的 p99 更高。

排队的请求要看截止时间

实测 · 本机执行输出
  策略        | 400 ms 内成功 | client 超时(或晚于 400 ms) | 收到 504 | worker 实际处理的请求 | 处理完时已超过截止时刻的 | 直接丢掉的
  ----------+------------+-----------------------+--------+----------------+--------------+------
  来者不拒      | 34         | 366                   | 0      | 313            | 279          | 0    
  出队时丢掉已过期的 | 162        | 62                    | 176    | 224            | 54           | 176  
  来者不拒时,队列越排越长,排到的请求已经过了截止时间,worker 白干;出队时检查截止时间,把时间花在还有人在等的请求上

队列越长,排到的请求越是没人在等。出队时检查剩余时间,把容量留给还有人等的请求。

分步演示

超时预算与截止时间
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇的服务是 Python 3.14 http.server(线程化)在本进程里起的,“耗时”用 sleep 模拟,慢请求由请求编号的 crc32 决定,所以两种策略面对完全相同的慢请求;第四节为了让“处理完时是否已超过截止时刻”算得准,把绝对截止时刻放进 X-Deadline 头,client 和服务在同一台机器上所以时钟一致,真实系统里传的是剩余时间(如 gRPC 的 grpc-timeout 头)
    • 没有验证:真实网络的延迟与抖动(都在环回上);跨机器时钟偏差对绝对截止时刻的影响;下游干活时能否被取消取决于语言和库(这里是 C 每 10 ms 自己查一次剩余预算);Python 的 GIL 与线程调度对毫秒级计时的影响(同一台机器上起了几百个线程);出队检查通过后仍有 54 个在 20 ms 的处理里过期,没有做“按预计处理时间提前丢”的进一步调参
    京ICP备2026067502号-1京公网安备11010502063487号