qinghuan
← 回到知识探索

后端现场 / 动手实验

每个接口都等一秒,整次请求为什么等了更久?

用一条总时间线分配剩余预算,把排队、重试和收尾也放进同一本账。

展开本文目录
  1. 先动手看看
  2. 给整次请求一条共同的截止线
  3. 排队与退避也会把余额用掉
  4. 下游收到的是剩余预算,不是重置后的全额
  5. 配置名字像“超时”,计的范围可能不同
  6. 预算用完时,剩下的工作还需要收尾
  7. 记住这三点
  8. 别把例子用过头
  9. 自己试一小步
  10. 查原始资料
  11. 找一份资料继续练
开始前,会这些就够了
  • 认识请求与响应;能运行一个 Python 文件
遇到不熟悉的词,先看这 3 个解释
超时
调用方为一次等待设置上限,超过时间还没拿到所需结果,就结束这次等待并报告超时;这样能限制资源占用,但并没有自动确认对方最终做到了哪一步。
重试
遇到可能只是暂时性的失败后,按预定规则再次尝试同一项操作;规则通常限制次数、等待间隔和总耗时,也要考虑重复执行会不会多做一次业务。
截止时间与时间预算
总预算回答整件事最多花多久,截止时间给出最晚应结束的时间点。发起下一步前,要扣掉已经花掉的时间和收尾预留,不能每一步都重新拿到完整预算。
打开完整术语小抄 ↗

假设用户只愿意等一秒,入口先排队,再查库存,失败后等一会儿重试,最后还要组织响应。如果每个步骤都重新领到一秒,总等待自然可能比用户想的长。

这里不猜一个适合所有系统的超时值。我们用虚拟时间练习记账:整次请求还有多少时间,新一步最多能拿到多少,什么时候应当停止继续投入工作。

已经等了一会儿,还能再给下游多久?

单次调用上限为 500 毫秒,另留 100 毫秒整理并返回结果。拖动的是假想的已用时间,页面没有发送网络请求。

离总时限还剩
300 ms
预留返回后可用
200 ms
本次最多等待
200 ms

max(0, min(500, 1000 − 700 − 100)) = 200

这次最多给 200 毫秒,不能不看剩余时间就重新发一张 500 毫秒的票。这只限制还愿意等待多久,并不保证操作能按时完成。

使用同一条虚拟单调时间线,排队、前面调用和退避都应计入已用时间。100 毫秒只是教学预留,不是通用设置;停止等待或发出取消,也不会自动撤销已经提交的操作。

给整次请求一条共同的截止线

本例从零开始,一千毫秒时截止;其中一百毫秒预留给组装响应与返回,每次下游调用自身最多五百毫秒。到了虚拟时刻 now,新调用能用的时间是三者算出的限制:五百、截止前剩余减去预留,最后不能小于零。

这是一项显式的教学分配规则,不表示一百毫秒一定足够回包。预算是计划,调度、网络和清理仍可能延迟实际完成。线上值要由业务目标与观测决定,不能把本例数字当作生产推荐。

排队与退避也会把余额用掉

排队花到第一百二十毫秒,首次调用仍可分到五百毫秒。假设它实际运行四百毫秒后失败,此时已到五百二十;再退避一百八十,就来到七百。第二次调用只能用两百,才能留下原定的一百收尾空间。

到九百毫秒时,这套规则不再启动新调用,不能因为“单次上限是五百”又额外领五百。程序中的加法使用预先指定的时长,没有实际请求或性能测量;重点是每一段都扣在同一条时间线上。

小实验 · python
deadline, reserve, per_attempt_cap = 1000, 100, 500

def grant(now):
    return max(0, min(per_attempt_cap, deadline - now - reserve))

now = 120  # Time already spent queueing, measured on a virtual monotonic clock.
print("first attempt starts", now, "budget", grant(now))
now += 400  # Assumed observed duration, not a performance measurement.
print("first attempt failed at", now)
now += 180  # Backoff consumes the same overall budget.
print("second attempt starts", now, "budget", grant(now))
now += 200
print("at", now, "new attempt allowed", grant(now) > 0)

怎样运行:Python 3.10+ 标准库;保存为 request_budget.py,运行 python request_budget.py。

运行后对照结果
first attempt starts 120 budget 500
first attempt failed at 520
second attempt starts 700 budget 200
at 900 new attempt allowed False

下游收到的是剩余预算,不是重置后的全额

服务甲调用服务乙时,应保留整件事已经花掉的时间。gRPC 的截止线传播就会考虑这一点,并通过传递扣除已耗时的超时时长来处理机器时钟差异;自动传播是否启用还取决于语言与配置。

本地计算经过了多久,宜使用适合测量时长的单调时钟。不要把甲机器的单调时钟读数原样当成乙机器可比较的时间戳。本例只有一台虚拟钟,没有实现跨机器协议、时间同步或网络延迟补偿。

配置名字像“超时”,计的范围可能不同

阅读代理配置时,要找到计时从哪里开始、什么时候停止。例如 Envoy v1.35.0 的路由超时等待上游完整响应,但要在下游请求完整接收后才开始;它不等于从用户点击按钮开始计算全部时间。空闲超时则关注多久没有活动,持续有少量数据到来可能一直不触发。

把入口排队、连接等待、请求上传、每次尝试、退避与响应传回画在同一条线上,再把每个计时器覆盖的区间标出来。重叠区间不能重复相加,遗漏区间也不会因为各层都填了数字就自动得到限制。

预算用完时,剩下的工作还需要收尾

停止启动新尝试后,还要通知正在进行的工作、释放资源,并保留可查询的状态。某一步已经完成了业务提交,超时或取消不会自动把它撤回;下一步该查询结果、重试还是补偿,必须结合实际完成点判断。

并行调用也不该机械平分时间:如果两个调用同时开始,它们可以各自在同一截止线内运行,但会争用资源,合并结果也需要时间。总预算约束最晚完成的范围,并不能替代并发限制或容量设计。

记住这三点

  • 新步骤使用剩余预算,排队与重试等待不会凭空免费。
  • 每个超时配置都要说明计时范围,空闲超时不等于总时长上限。
  • 预算耗尽后的停止和清理需要实现,已完成副作用另行处理。

别把例子用过头

  • 虚拟毫秒只是算例,没有证明实际网络能在期限内返回。
  • 各机器单调时钟读数不能直接互相比较。
  • 并行步骤的时长不能简单相加当成整个请求实际耗时。

自己试一小步

如果排队时间从 120 改成 650,仍允许一次 180 毫秒退避,第二次尝试之前应该怎样检查?

  1. 先画出截止线、收尾预留和所有已发生的步骤。
  2. 为退避前与退避后各增加一次预算检查,并说明不足时返回什么状态。
  3. 再让两个下游并行开始,写出合并结果前还需要保留的时间。

做到这些,就算完成

  • 不会在每一层重新发放完整一千毫秒。
  • 区分配置允许的最长时间与本次已经发生的时间。
  • 没有用“超时”推断此前业务必然回滚。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • gRPC:Deadlines (新标签页)↗gRPC · 英文

    Deadline Propagation 与服务端处理:扣除已耗时,传播支持依语言不同,应用负责停止所启动活动。

    已阅读截止线与超时定义、客户端/服务端及传播段;本例只实现虚拟预算运算。

  • Envoy v1.35.0:配置中的超时 (新标签页)↗Envoy · 英文

    Stream timeouts、Route timeouts:空闲计时、路由计时起点与每次尝试超时的不同范围。

    已读官方仓库 v1.35.0 标签的原始文档;只用来比较计时范围,不沿用其默认值。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。