开始前,会这些就够了
- 认识请求与响应;能运行一个 Python 文件
遇到不熟悉的词,先看这 3 个解释
- 超时
- 调用方为一次等待设置上限,超过时间还没拿到所需结果,就结束这次等待并报告超时;这样能限制资源占用,但并没有自动确认对方最终做到了哪一步。
- 重试
- 遇到可能只是暂时性的失败后,按预定规则再次尝试同一项操作;规则通常限制次数、等待间隔和总耗时,也要考虑重复执行会不会多做一次业务。
- 截止时间与时间预算
- 总预算回答整件事最多花多久,截止时间给出最晚应结束的时间点。发起下一步前,要扣掉已经花掉的时间和收尾预留,不能每一步都重新拿到完整预算。
假设用户只愿意等一秒,入口先排队,再查库存,失败后等一会儿重试,最后还要组织响应。如果每个步骤都重新领到一秒,总等待自然可能比用户想的长。
这里不猜一个适合所有系统的超时值。我们用虚拟时间练习记账:整次请求还有多少时间,新一步最多能拿到多少,什么时候应当停止继续投入工作。
已经等了一会儿,还能再给下游多久?
单次调用上限为 500 毫秒,另留 100 毫秒整理并返回结果。拖动的是假想的已用时间,页面没有发送网络请求。
- 离总时限还剩
- 300 ms
- 预留返回后可用
- 200 ms
- 本次最多等待
- 200 ms
max(0, min(500, 1000 − 700 − 100)) = 200
这次最多给 200 毫秒,不能不看剩余时间就重新发一张 500 毫秒的票。这只限制还愿意等待多久,并不保证操作能按时完成。
使用同一条虚拟单调时间线,排队、前面调用和退避都应计入已用时间。100 毫秒只是教学预留,不是通用设置;停止等待或发出取消,也不会自动撤销已经提交的操作。
给整次请求一条共同的截止线
本例从零开始,一千毫秒时截止;其中一百毫秒预留给组装响应与返回,每次下游调用自身最多五百毫秒。到了虚拟时刻 now,新调用能用的时间是三者算出的限制:五百、截止前剩余减去预留,最后不能小于零。
这是一项显式的教学分配规则,不表示一百毫秒一定足够回包。预算是计划,调度、网络和清理仍可能延迟实际完成。线上值要由业务目标与观测决定,不能把本例数字当作生产推荐。
排队与退避也会把余额用掉
排队花到第一百二十毫秒,首次调用仍可分到五百毫秒。假设它实际运行四百毫秒后失败,此时已到五百二十;再退避一百八十,就来到七百。第二次调用只能用两百,才能留下原定的一百收尾空间。
到九百毫秒时,这套规则不再启动新调用,不能因为“单次上限是五百”又额外领五百。程序中的加法使用预先指定的时长,没有实际请求或性能测量;重点是每一段都扣在同一条时间线上。
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 毫秒退避,第二次尝试之前应该怎样检查?
- 先画出截止线、收尾预留和所有已发生的步骤。
- 为退避前与退避后各增加一次预算检查,并说明不足时返回什么状态。
- 再让两个下游并行开始,写出合并结果前还需要保留的时间。
做到这些,就算完成
- 不会在每一层重新发放完整一千毫秒。
- 区分配置允许的最长时间与本次已经发生的时间。
- 没有用“超时”推断此前业务必然回滚。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- gRPC:Deadlines (新标签页)↗
Deadline Propagation 与服务端处理:扣除已耗时,传播支持依语言不同,应用负责停止所启动活动。
- Envoy v1.35.0:配置中的超时 (新标签页)↗
Stream timeouts、Route timeouts:空闲计时、路由计时起点与每次尝试超时的不同范围。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
Dubbo 中文:一次调用能等多久、失败后重试几次 (新标签页)↗
只读 Deadline 机制,给 A→B→C 画一条共享截止时间线,每一步只分配当时剩下的时间。
查看学习准备与访问说明 →Go 官方短读:把取消信号一路传到数据库调用 (新标签页)↗
只看父子 Context 的截止时间与取消传递,检查子步骤能否继续等得比父请求更久。
查看学习准备与访问说明 →