重复发货事件 — Job成功不等于业务成功
目标
复现一个处理订单的 Job 虽然成功、却创建了两次配送的事件,并依据重试、幂等键和执行期限进行解释。
为什么重要
即使 completions 和 parallelism 都是 1,也不能保证外部业务只会处理一次,因为失败的进程可能已经完成了持久化。只有分别观测 Kubernetes 管理的执行生命周期与账本管理的业务生命周期,才能判断是否应该重新执行。
本实验使用个人 VM 中真实的 k3s,以及由 HTTP 和 SQLite 构成的合成账本,不涉及真实配送、支付或个人信息。账本的 safe 模式通过服务器端写事务和业务键唯一性合并重复行。本实验讲解该契约的应用和验证,并不保证整个分布式系统能够精确处理一次。删除账本 Pod 后,数据也会消失。
首次准备最多允许 6 分钟。所有文件均放在 /root/ckad-job-effects 下。会话结束后工作内容会消失,请提前下载需要的记录。实验按 55 分钟设计,如需更多时间,请在到期前延长。
使用的模板与运行辅助程序
job-template.json 是包含真实 worker 命令、固定镜像和权限限制的 Job JSON。每一步都复制该文件,然后修改 metadata.name、spec.backoffLimit,以及容器 env 中的 CASE_ID、MODE、FAULT。Pod 模板 spec.template.metadata.labels 中的 labhub.io/case 也应改为与 CASE_ID 相同的值。WORK_ID 保持 order-001,POD_UID 保持使用 Downward API。parallelism 和 completions 保持为 1,restartPolicy 保持 Never。其他安全和应用设置也不要修改。仅在需要期限的步骤中添加 spec.activeDeadlineSeconds。
不要直接使用 kubectl create 执行。编写文件后,运行 python3 /opt/fixtures/ckad_job_effects_lab.py run <단계>。辅助程序会将学员文件原样提交给 API,并保存执行期间的 Pod 观测。再次运行已经完成的同一步骤时,不会创建新 Job,只会进行评分。如果观测等待被中断,请用同一命令继续,不要删除或重新创建 Job。如果连创建响应本身都丢失了,应要求开始新的实验,而不是重复创建。
步骤
- baseline.json:name=baseline,CASE_ID=baseline,MODE=unsafe,FAULT=none,backoffLimit=0。通过 run 1 观测正常对照组。
- unsafe-retry.json:name=unsafe-retry,CASE_ID=unsafe-retry,MODE=unsafe,FAULT=after-commit,backoffLimit=1。通过 run 2 观测持久化后失败以及替代 Pod。
- 读取 observation-2.json,在 retry-analysis.json 中填写 job_uid、failed_pod_uid、succeeded_pod_uid、request_count、shipment_count、retry_layer。retry_layer 应根据观测填写 job-controller 或 kubelet-container。通过 run 3 记录分析。
- safe-retry.json:name=safe-retry,CASE_ID=safe-retry,MODE=safe,FAULT=after-commit,backoffLimit=1。通过 run 4 确认相同故障下的幂等结果。
- safe-replay.json:name=safe-replay,CASE_ID=safe-retry,MODE=safe,FAULT=none,backoffLimit=0。通过 run 5 检查在新 Job 中是否仍沿用相同业务键。
- no-retry.json:name=no-retry,CASE_ID=no-retry,MODE=unsafe,FAULT=after-commit,backoffLimit=0。通过 run 6 检查即使失败,配送是否仍然保留。
- deadline.json:name=deadline,CASE_ID=deadline,MODE=safe,FAULT=deadline,backoffLimit=0,activeDeadlineSeconds=20。通过 run 7 观测超出期限及账本。即使当前 Pod 列表为空,也不要删除 observed_pods 中保存的历史观测。
- 比较六次执行并编写 final-analysis.json。request_count 和 shipment_count 是账本总计,safe_replay_shipment_id 是重新执行时保持不变的配送编号。timeout_cancels_effect 和 ledger_survives_ledger_pod_replacement 分别是关于超出期限是否取消业务、账本 Pod 被替换后数据是否保留的布尔值。idempotency_scope 应使用实际用于重复判定的字段名,并用 + 连接。outcomes 以六个 Job 名称为键,填写 condition(Complete/Failed)和 shipments(每次执行后该业务的配送数)。通过 run 8 记录综合分析。
参考
- 可以使用
python3 /opt/fixtures/ckad_job_effects_lab.py observe查询当前 API 和账本,但它不会生成答案报告。 - observation-N.json 是辅助程序记录的数据。pods 是采集时的列表,observed_pods 是执行期间保存的最后一次 Pod 观测。不要把因超出期限而删除的 Pod 过去的 Running 状态解释为当前状态或退出代码。
- 写错的学员输入文件可在执行前修正。请保留已完成步骤的文件和观测。删除同名 Job 会切断账本历史与执行之间的关联。
- 步骤准备功能可以并行运行不同业务的前置 Job。记录顺序是学习顺序,并非实际启动顺序。观测账本中可能先出现其他案例的行,因此应按 CASE_ID 划分比较。同一业务的 safe-replay 只能在 safe-retry 的观测保存后运行。如果准备被中断,请继续运行提示的同一 prepare 命令,且不要生成当前步骤的答案。
- backoffLimit=0 或删除 Job 都不等于取消业务。不要手工删除账本行来凑数。
正常配送对照组
按照模板编写 /root/ckad-job-effects/baseline.json,并使用第 1 步指定的名称、业务键、模式、故障条件和重试预算运行 run 1。
即使是正常执行,也要分别保留 Job 和账本的观测。
明明成功,却有两条记录
在 /root/ckad-job-effects/unsafe-retry.json 中声明持久化后失败及一次重试,并运行 run 2。
Never 是同一 Pod 的容器重启策略。请查看替代 Pod 的 UID。
究竟是哪一层在重试
从 observation-2.json 中读取 Job、失败 Pod、成功 Pod 的 UID,以及请求数和配送数,将其记录到 retry-analysis.json 并运行 run 3。
同时比较相同的 ownerReferences、不同的 Pod UID 和 restartCount。
相同故障,只产生一次配送
在 /root/ckad-job-effects/safe-retry.json 中声明 safe 模式和稳定的业务键,并运行 run 4。
检查即使请求重复,是否仍关联到同一个配送编号。
使用另一个 Job 重新执行
以单独的 Job 名称编写 /root/ckad-job-effects/safe-replay.json,但保持 CASE_ID=safe-retry,并运行 run 5。
执行 ID 应当不同,而业务 ID 必须保持不变。
关闭重试后仍会留下什么
在 /root/ckad-job-effects/no-retry.json 中声明 backoffLimit=0 和持久化后失败,并运行 run 6。
Failed 条件并不表示持久化已回滚。
时间结束后仍会留下什么
在 /root/ckad-job-effects/deadline.json 中声明 activeDeadlineSeconds=20 和持久化后等待,并运行 run 7。
区分执行终止与业务取消,并保留删除前的 Pod 观测。
配送事故综合报告
根据第 1、2、4、5、6、7 步的观测,填写 final-analysis.json 中的总计、复用的配送编号、取消和保留判断、业务键范围以及六个 outcomes,然后运行 run 8。
总请求数、各业务的配送数和执行成功条件是不同的指标。