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

构建 EAI 中间层

超时不是失败,而是未知

在 TT Lab 中继续学习

一句话总结

同步中继,就是接收报文、调用目标系统,并把它的结果转换成标准响应码返回。最重要的区分是“失败了”与“不知道”。如果连连接都没建立成功,那么可以确定没有发送(E902),但如果发送之后没有收到答复,就没有人知道目标是否已经处理(E901)。一旦把这两者混为同一个代码,重复转账就开始了。

为什么需要它

如果渠道(网上银行)直接调用账户系统,渠道就必须了解账户系统的地址、HTTP 约定和错误格式。无论账户系统以 422 {"result":"INSUFFICIENT_FUNDS"} 还是 200 {"code":"E-17"} 给出错误,每个渠道都要各自写一份解释代码,目标系统一变,就要修改十个渠道。中继层在一个地方承担这种解释,只向渠道返回一个标准响应码——余额不足,无论来自哪个目标,都是 B201。

然而一旦插入中继,失败点就多了一个。渠道 → 枢纽 → 账户系统的环节中,任何一处都可能停住,而停在哪里,含义完全不同。本模块的账户系统夹具,做得与现实一模一样。再慢也会处理到底。即使枢纽在 2 秒时放弃了,账户系统也会在第 4 秒扣掉余额。这时如果枢纽向渠道答复“失败”,渠道就会重发,钱就被扣两次。

工作原理

一个请求的路径。① 通过 TCP 准确读取 4 字节长度,再按该长度读取(第 1 模块) ② 解析报文头,格式错误则为 E102 ③ 如果交易码不是中继对象,则为 E101(路由,第 2 模块) ④ 按布局把正文转成 JSON(第 3 模块的规则,现在是公共库 lhconv) ⑤ 调用账户系统 POST /v1/transfers——把 GUID 放在正文和 X-GUID 请求头中 ⑥ 把结果转换成标准代码,生成响应报文(仅在正常时响应正文为 45 字节)。

把结果分为四类。在 HTTP 语义(RFC 9110)中,2xx 表示请求被成功处理,4xx 表示请求一侧的问题,5xx 表示服务器无法处理。账户系统以 422(RFC 9110 15.5.21,理解了请求格式,但无法处理其内容)给出业务拒绝,并通过正文中的 result 区分原因。所以转换表要同时看状态码和业务码。

情形 能知道什么 标准代码
200 已处理 0000
422 + INSUFFICIENT_FUNDS 等 被拒绝(钱没有动) B201–B203
500 目标说它没能处理 E500
等待响应时超时 发送了。是否处理不知道 E901
连接被拒绝、连接超时 没能发送(确定未发送) E902

超时有两种。建立连接期间的超时,与发出请求之后等待响应期间的超时,含义截然相反。前者没有一个请求字节发出去,所以重发也是安全的。后者的请求已经到达了目标。Python 3.12 的 urllib.request.urlopen 会以不同的异常抛出这两者(在这个过程中实测)——连接阶段的问题会用 URLError 包裹(reason 是 ConnectionRefusedError 或 TimeoutError),而等待响应时产生的超时,则以未被包裹的 TimeoutError 抛上来。如果把一个异常整个捕获并转换成同一个代码,这种区分就消失了。

超时预算越往内层越短。渠道等待 10 秒,而枢纽等待账户系统 15 秒,那么渠道已经放弃并重发了,枢纽却还抓着第一个请求。枢纽的结果没有任何人接收。所以要按渠道 > 枢纽 > 目标的顺序缩减预算,枢纽在自己的预算之内,必须答复点什么——如果不知道,就说不知道(E901)。

每个连接单独处理。在处理一个请求期间不接受下一个连接的服务器,会因为一笔缓慢的转账,让后面所有转账排队。Python 标准库的 socketserver.ThreadingTCPServer 会为每个连接启动一个线程。线程可能会无限增长,所以在第 11 模块中要加上按目标划分的并发处理上限(舱壁)。

在现场相遇的样子

代价最高的事故,是把 E901 当作失败处理的渠道。仅凭一句画面文案“响应超时——请重试”,客户就会再按一次按钮,账户系统里就留下两笔转账。所以对结果未确定的响应,要禁止重发与结果查询成对出现(第 8 模块)。第二种是把目标系统的错误消息原样转给渠道的枢纽。账户系统内部的错误文案(堆栈跟踪、表名)会显示在客户画面上,而渠道会堆积越来越多解释各目标不同文案的代码。第三种是本地一直正常的中继,在生产环境里会把报文丢掉一半——这是没有按长度读完,而是只用一次 recv 就结束的代码。

下一项实验要做什么

启动账户系统夹具,亲自调用一下,并根据定义书编写响应码转换表。然后逐步扩展中继服务器 relay.py——读完被拆碎的报文的骨架、正常转账的中继、业务错误的转换、读取超时(E901)、连接失败(E902)、并发处理。评分器会直接启动你的 relay.py,并通过账户系统夹具的调用统计,连调用了几次也会检查。