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

集成与部署

请求返回了 200,却有三笔不见了

在 TT Lab 中继续学习

一句话总结

批量发送的 API 常常会在对一个请求返回 200 的同时,把逐笔结果放在正文里,而调用的一方如果只看状态码,失败的几笔就会在哪里都没有留下记录地消失。所以需要记录逐笔结果,区分出要重发的和要修正的,最后再核对两边的数量,也就是对账。

为什么需要它

“昨天发了 200 笔结算,对方说他们那边只有 187 笔。”接到这个投诉,最先确认的是我们自己的日志。然而我们的日志里写着200 笔全部成功。并不是说谎。只是我们看到的仅仅是请求的状态码而已。

批量发送 API 的响应通常是这样的。

{"batch_id": "B-07", "accepted": 27, "rejected": 3,
 "results": [{"item_id": "IT-0031", "status": "rejected", "reason": "unknown_account"},
             {"item_id": "IT-0032", "status": "accepted", "reason": null}]}

请求成功了。服务器收到请求并处理了,结果放在正文里。从 HTTP 的角度,200 是正确的——RFC 9110 定义的 2xx 表示“收到、理解并接受了请求”,而不是“请求中的所有条目都成功了”。

所以这种事故是在没有错误、也没有告警的情况下发生的。而且要积累好几天之后,在与对方的数字核对时才会被发现。

工作原理

修复的顺序有四步。

第一,把逐笔结果留在我们这边。对发送的每一笔,记录 status·reason·시도 횟수(最后一项的韩文意为“尝试次数”)。没有这份记录,就无法进行后面的三步。常见的错误是“只记录失败的”,这样“从未发送过的”和“发送后成功的”就无法区分。

第二,把失败分成两类。一类是重发就能解决的失败,另一类是必须由人来修正的失败。

原因 重发的话 该做什么
暂时挂起、限速、临时错误 能解决 重新放入队列
未知账户、错误的值 会得到同样的答复 修正数据
已处理(重复) 会得到同样的答复 什么都不做

没有这个分类,就会出现二者之一的情况。如果全部重试,无法修正的那些笔就会在队列里永远打转;如果什么都不重试,连只是暂时卡住的也要靠人工处理。限速通常以 RFC 6585 的 429 到来,并同时给出 Retry-After——那显然属于应该重发的一类。以机器可读的方式承载失败原因的标准格式,有 RFC 9457 的 problem details,用 type 来区分类别。

第三,只重发失败的部分。在这里如果把整批全部重发,已经进去的那些笔就会进去两次。对方如果能防止重复,那是万幸,如果不能,就是我们造成了事故。重试的单位不是批,而是笔。

第四,对账。把发送方的“记为成功的笔数和合计”与接收方的“实际进入的笔数和合计”核对。两个数字必须相同,如果不同,就要深入到是哪一笔只存在于哪一边。

발신함 120건
   ├─ 성공 기록 112건 · 합계 X
   └─ 실패 기록   8건 (고쳐야 함 8)
상대 원장     112행 · 합계 X      ← 건수와 합계가 **둘 다** 맞아야 한다

只核对笔数是不行的。少了一笔、另一笔进去了两次,笔数也不会变。所以要同时做合计或集合比较。

在现场相遇的样子

第一,假定响应中 results 的顺序与请求顺序相同。很多 API 实际上是按相同顺序给出的,但如果文档中没有这样写,就不能相信。要用 item_id 来对应着读取。

第二,把部分失败当作异常抛出。如果把“3 笔失败”作为异常抛出,并回滚整个批处理,连成功的 27 笔也会被重新发送,这就会产生重复。

第三,重试次数没有上限。如果被分类为临时失败的那一笔其实是永久失败,它就会在队列里永远打转。要记录尝试次数,在若干次之后移交给人。

第四,只在事故之后才对账。对账不是事故调查工具,而应该是每天运行的东西。只有能把昨天的和今天的做比较,才能知道是从什么时候开始出现偏差的。

第五,剩下的笔没有负责人。如果只列出“需修正 8 笔”,却不写谁来修正,这 8 笔就会永远留在那里。对账表的最后一栏是人的名字。

下一项实验要做什么

启动一台接收批量发送的合作方服务器,制作 120 笔待发送的数据。先以只看状态码的方式发送,用数字确认我们的记录与对方账本之间的差异。然后读取逐笔结果并记录在我们这边,把失败原因分成要重发的和要修正的,只挑出要重发的进行重发。最后制作核对发送方和接收方的笔数与合计的对账表,并写出剩余笔的处理计划。