记录重复请求的地方必须在Pod之外
一句话总结
要让重试变得安全,必须区分执行 ID 与业务 ID,并在保存副作用的边界上始终以同一个业务键进行处理。
为什么需要了解这一点
假设我们修改了配送程序:程序启动时生成随机 UUID,并查询该请求是否已经处理过。UUID 发生冲突的概率虽然很小,但重复配送的问题依然可能存在。因为每当新 Pod 运行时,都会生成一个新的 UUID。同一订单的重试因此看起来像一项不同的业务。标识符具有唯一性,与能够把同一项业务识别为同一对象,是两种不同的性质。
把处理完成状态写入 Pod 内临时文件的方法,也可能无法跨越重试边界。Job 创建新 Pod 后,不能假定上一次执行的内存或本地文件会原样跟随过来。为了不掩盖这一差异,练习会为每个请求记录 Pod UID,但在判断是否重复配送时使用另一个业务键。
工作原理
学习用请求包含四个值。case_id 是一项独立实验的业务范围,work_id 是该范围内的逻辑订单。pod_uid 用于追踪发出请求的那次执行,mode 则区分对照组与幂等处理。重新执行同一项业务时,应保持 case_id 和 work_id 不变,而新 Pod UID 应当不同。在真实服务中,还需要另行确定包含账户、租户和作业类型的键作用域,以及键的复用策略。
账本使用不同的表来管理请求尝试和配送副作用。即使请求进入了两次,也只能生成一次配送结果。再次收到同一个键时,系统会返回第一次的配送编号,同时继续记录新的请求尝试。这样就不会因消除重复而连同观测信息一起删除。
关键在于查询与保存之间的竞争。如果两个请求几乎同时到达,各自读取到“没有记录”,随后又分别保存新的配送,就可能产生重复。练习中的账本会在 SQLite 写事务内完成键的查询和插入,并对幂等键设置唯一性约束。它不会用客户端的一条 if 语句来代替并发安全保障。出现错误时,相应事务不会把该次配送报告为成功。
실행 추적: Pod A → 요청 1
Pod B → 요청 2
업무 판정: 같은 업무 키 → 같은 배송 번호
我们不会把这称为任意意义上的恰好一次交付保证。这个示例只在账本内部把一种行插入合并为一次。现实中的快递公司、邮件服务器或其他 DB,并不会自动加入账本的事务。有时还需要外部系统的幂等 API、结果查询、发件箱模式以及对账等附加契约。你必须能够说明自己保护的是哪个存储边界。
实际工作中的表现
运维人员可能会新建一个名称不同于已完成 Job 的 Job,以便重新执行同一订单。如果使用 Job UID 作为幂等键,新执行就会得到新键,因此无法识别之前产生的副作用。相反,只要保持业务键不变,即使 Kubernetes 对象不同,也可以复用同一个结果。这正是删除执行对象与删除业务处理历史具有不同生命周期的原因。
把所有请求归入同一个键同样是错误的。如果连不同订单也合并成同一次配送,重复虽然减少了,正常业务却丢失了。测试不仅要检查同一个键的重复请求,还要确认不同的键会分别得到处理。当同一个键对应的正文内容发生变化时,是允许还是拒绝,以及键要保留多久,也都属于实际契约的一部分。这个练习的小型请求模型不包含地址和商品,因此不会声称自己实现了这类冲突策略。
CronJob 的职责是在指定时间创建 Job,它不能替代幂等账本的职责。即使把 concurrencyPolicy 设为 Forbid,也只是限制同一个 CronJob 所创建 Job 的重叠,并不是针对外部账本的重复处理契约。还需要单独检查业务键能否贯穿其他 CronJob、手动 Job 以及运维人员的重新执行路径。
还要区分存储的生存能力。实验的账本位于另一个 Pod 的 emptyDir 中,并使用 SQLite。工作 Pod 的替换不会影响它,但账本 Pod 本身一旦被替换,数据就会消失。因此需要确认账本的 Pod UID 与进程 boot_id 没有改变。这种配置并没有连永久保存、多节点高可用和灾难恢复也一并解决。
下个练习将做什么
在保留“保存后失败”这一条件的同时,启用账本的幂等处理。一起确认两次请求和一条配送记录,并观察即使用另一个 Job 重新执行同一项业务,配送编号是否仍然保持不变。最后,不要删除账本的尝试列表,也不要只读取成功标记;请说明不同业务是否仍然保留,而只有重复业务被合并。
官方文档与适用范围
本教材把 Kubernetes 的执行特性与应用程序的存储契约联系起来,不会把幂等性简化为某个文件是否存在或 Job 的成功条件。