不要抹去失败的零食订单
一句话总结
隔离不是把失败变成成功。留下失败的依据并停止自动重复之后,只有人允许的重新驱动才会以新预算开始。
为什么需要它
某一个零食订单里一直带着错误的内容。如果立即重复同一件事,就会占掉后面正常订单的处理位置。可如果把这一行删掉,就无法解释下单者提交的订单去了哪里。这次把业务分成 done 和 dead,并保留 reason 以说明为什么停下。dead 增多并不是正常完成率提高了,而是需要调查的事情变多了。
前面模块中的 outbox 投递循环在第一次失败时就停了下来。这次的队列处理的是相互独立的零食订单,所以可以处理预约时间已过的其他任务。这个选择并不适用于所有事件流。银行余额计算或文档编辑这类改变顺序就会改变含义的工作,不能同样套用。必须先确定,把失败项单独隔离会对顺序保证造成什么影响。
工作原理
finish 会接收传输适配器的结果 ok、retry 或 permanent。ok 变为 done,permanent 变为 dead。即使是 retry,如果已达到最大尝试次数,就以 exhausted 为原因,如果要预约的时刻已到达原定截止时间,就以 deadline 为原因变为 dead。如果还有预算,就保存 pending 和下一个 available_at。在任何路径上都不会删除业务 ID 或数量,只会释放 owner 和 lease_until。
只有当网络适配器返回严格的 True 时,run_once 才把它归类为 ok。1 或字符串 ok 都不视为成功。临时失败的异常明确为 Retryable,永久失败明确为 Permanent。意料之外的异常或 BadAck 会传递给调用方,并留下尚未确定的 leased 状态。下一次 claim 会确认租约到期并回收,所以没有掩盖失败,恢复所需的依据也得以保留。
要重新执行被隔离的业务,需要显式调用 redrive。对 pending、leased、done 不允许调用。运维人员留下的 note 是指向诊断工单或修复工作的标识符。实验中只接收 ASCII 标识符,以免原始错误消息或个人信息被无意中写入审计记录。在真实服务中,还需要有谁批准了的信息和访问权限,但这个本地函数并没有实现那套认证体系。
重新驱动会在一个事务中完成审计行的添加和业务状态的更新。redrives 中记录 id、之前的 token、at_ms 和 note。jobs 的 attempts 会归零并给出新的 deadline,但 token 不会回退。因为如果之前 A 的 token=1 还在,而重新驱动之后又使用 token=1,旧的完成消息就可能对新任务生效。次数预算的重新开始与任务所有权代次的复用是两个不同的问题。
在现场相遇的样子
即便叫作 DLQ,如果实际上没有运维人员在查看,那也只是把失败搬到了另一个地方。要观测隔离数量、按原因划分的比例、最久的隔离业务、最近一次调查的时刻以及重新驱动的结果。告警中不要把业务正文整个放进去,而是使用必要的最小标识符。如果因为暂时性故障而有大量业务被隔离,不要一次性全部退回而再次制造负载,还应限制恢复速度。
AWS 的 DLQ 指南说明了把无法处理的消息分离出来、分析原因并重新驱动的流程。同时它还警告,在需要严格顺序的工作中,隔离会打乱顺序。本实验中的 dead 行并不是单独的 AWS 队列,而是本地 jobs 的状态,保留期限和服务权限策略也不同。不能因为名称相同,就说已经具备了该产品的功能和保证。
在综合测试中,投递方 A 会发送真实的 HTTP 请求,接收服务器提交 ID 和库存效果之后,A 以退出码 73 结束。finally 清理和 finish 都不会被执行。另一个进程 B 在到期边界以 token=2 回收这个任务,并再次发送相同的 ID。HTTP 请求实际上会来两次,但接收端库存只增加 7,队列变为 done。迟到的 token=1 的完成请求必须返回 False。
不要把这个结果扩大成外部支付的恰好一次处理。只是因为接收服务器提供了原子记录相同 ID 的实现,所以该服务器上的效果才只有一次。如果外部提供方不支持同样的契约,就可能需要查询、调整和人工确认。队列做得好,并不意味着外部系统的状态也被原子地绑在了一起。
重启同样要明确范围。这次验证的是同一块本地磁盘上进程的终止与恢复。磁盘消失或 DB 损坏时用于恢复的备份、多台主机之间的共识、租约续期、发送取消以及全局请求速率限制,都没有实现。这里不沿用前面快照实验中的 WAL 设置,而是用适合简短队列写入的 rollback journal 单独创建 DB。这是根据读取需求和写入需求来选择存储方式的练习。
下一项实验要做什么
在 8 个步骤的综合实验中,要完成重试函数、持久化受理、先占、完成、重新驱动和单次执行。会检查两个真实投递方的先占竞争以及 HTTP 接收之后的终止,不仅看正确答案,还要看到期边界、令牌重置、宽松 ACK 这类错误答案是否也会被拒绝。会话预计需要 110 分钟,所以要在中途延长时间,并另行保存需要的代码。
参考:SQS DLQ 设计、SQLite 错误与回滚。这个实验中的重新驱动,前提是人已经批准的本地操作。