TT Lab
开始
学习 学习路径 课程

集成与部署

对方变慢了,先倒下的却是我们

在 TT Lab 中继续学习

目标

把分给一个上层请求的时间当作预算,亲手制作一个把这份预算分配给连接、读取、重试和下层调用的客户端。预算不够时不发起调用、尽快失败,并通过请求头把剩余预算向下传递。

为什么重要

把超时设得宽松的习惯看似稳妥,其实正相反。对方变慢到 30 秒时,如果我们的上限是 60 秒,我们的工作进程就会被占住 30 秒,而从调用我们的一方来看,变慢的是我们。这种延迟还会再向上一层蔓延。 所以超时不是体谅对方、等一等,而是保护我们的断路器。而且它不是一个值,而是两个值——连接是到达的时间,花了几秒就不是慢而是不通;读取是对方干活的时间,因业务而异。 用预算的角度来想,规则是三行。为上层请求确定总预算,每次调用的读取上限取那一刻剩余的预算,剩余预算小于下限时不发起调用。重试及其等待时间也要从同一份预算中扣除。 评分器不会相信你写的句子。评分器会把变慢的合作方启动在评分器所选的端口上,实际运行你的调用器,同时测量耗时和输出中的区段名称。

步骤

  1. 创建 /root/budget/partner.py 并在端口 8014 上启动,测量三种失败形态,写入 /root/budget/probe.json。
  2. 创建 /root/budget/call.py,分别设定连接和读取的上限,并说明是在哪个区段断开的。
  3. 创建 /root/budget/deadline.py,用一个总预算进行多次调用,并让剩余预算成为下一次调用的读取上限。
  4. 给 deadline.py 加上下限(--min-ms),剩余预算小于下限时,不发起调用,以 no_budget 跳过。
  5. 创建 /root/budget/retry.py,让重试的等待按指数增长并加入随机数,同时把这段等待也从预算中扣除。
  6. 创建 /root/budget/fanout.py,把当时的剩余预算通过 X-Budget-Ms 请求头传给下层调用。
  7. 在 /root/budget/budget_plan.json 中用数字写出一个画面的预算表。
  8. 在 /root/budget/budget_report.md 中分四节进行汇报。

参考

测量失败的三种形态

创建 /root/budget/partner.py 并在端口 8014 上启动,亲自测量连接被拒绝、连接超时、读取超时这三种情况,并以 kind·target·elapsed_ms·error 写成三行,保存到 /root/budget/probe.json。

缓慢的响应由合作方用 sleep 来模拟。连接被拒绝用没有人监听的 127.0.0.1 端口,连接超时用不会路由到任何地方的地址来制造。三种情况的耗时彼此有何不同,是这一步的关键。

把连接和读取分开设定

创建 /root/budget/call.py,分别接收 --connect 和 --read,失败时在 phase 中写明是 connect 还是 read。如果收到了响应,即使状态码是 4xx、5xx,phase 也是 done。

requests 的 timeout 接收一个装着两个值的元组。异常也分了种类,所以可以区分连接一侧和读取一侧——不过连接被拒绝不是超时异常,所以要单独捕获,而且它是在到达对方之前就结束的事,所以属于 connect 区段。

剩余预算就是下一次调用的上限

创建 /root/budget/deadline.py,用一个 --budget-ms 依次调用多个 --url。每次调用的读取上限必须是那一刻剩余的预算,并且要把这个值写入 read_timeout_ms。

把上一步的 call.py 作为模块引入使用,就不必把同样的代码写两遍。记下一次起始时刻,在每次调用之前计算“预算减去到目前为止已用的时间”。这样一来,无论调用有多少个,整体都不可能超出预算。

没有希望的调用就不发起

给 deadline.py 加上 --min-ms,剩余预算小于该值时,不发起调用,将 phase 记录为 no_budget,并增加 skipped。被跳过的调用的 elapsed_ms 为 0。

只剩 200ms,却去发起平时要花 400ms 的调用,是把失败推迟了 200ms。如果当场就失败,还能用剩下的时间做出哪怕一个部分响应。下限要通过参数接收,这样才能针对不同情形给出不同的值。

重试也要从预算里出

创建 /root/budget/retry.py,让它重新发起失败的调用,但等待上限按 base-ms * 2^(시도-1) 增长(指数中的韩文词意为“尝试次数”),实际等待在 0 到该上限之间随机选取。如果等待之后预算里没有再发起一次调用的空间,就把 stopped_reason 设为 budget 并停止。

如果不加入随机数,在同一时刻失败的客户端会在同一时刻再次涌来。而且等待时间也要从预算中出,所以要在睡眠之前先问自己,醒来之后是否还有空间发起调用。把停止的原因分成三种来记录,之后只看日志就知道原因。

把剩余预算向下传递

创建 /root/budget/fanout.py,调用 --n 次 <BASE>/budget?ms=200,每次都在 X-Budget-Ms 请求头中带上当时剩余的预算。在输出的每个 call 中写入 sent_budget_ms·received·elapsed_ms。

如果我们只剩 1200ms,而下游服务按自己的标准等待 5 秒,多出的 4 秒就被用来生成没有人看的答案。请求头名称是我们约定的,接收的一方把它的值当作自己的上限。合作方的 /budget 会原样返回收到的请求头的值,所以可以确认是否真的传递了。

一个画面的预算表

在 /root/budget/budget_plan.json 中写出预算表。需要 total_ms·reserve_ms·worst_case_ms,以及至少 3 个 calls(名称·budget_ms·max_attempts)。reserve_ms 至少为 100,worst_case_ms 是各调用预算之和再加上预留量,并且不得超过 total_ms。

预留(reserve)是我们这边序列化、模板渲染这类不属于调用的时间。如果漏掉这部分,把预算全都分给调用,画面总是在上限边缘险险通过。带有重试的调用,意思是重试也必须在该调用的预算之内完成。

预算检查报告

在 /root/budget/budget_report.md 中分为 ## 지금 무엇이 시간을 먹는가 ## 구간을 어떻게 나눴나 ## 재시도가 먹는 몫 ## 예산을 넘겼을 때 무엇을 하나 四节来写(韩文,依次意为“现在什么在吃掉时间”“如何划分区段”“重试占用的份额”“超出预算时怎么办”)。probe.json 和 budget_plan.json 中的数字必须写进正文。

读者是那个说“请把超时调大一点”的人。请用数字向他展示,调大之后什么会变糟。重试的份额,是把尝试次数乘以等待上限,计算出最坏情况并写下来。