LabHub
学习 学习路径 课程

CAPA — Argo 项目认证助理

只有一个订单,工作流为什么执行了两次?

在 LabHub 中继续学习

一句话总结

收到 event、Workflow 已执行、订单已经写入 ledger,是三项不同证据。如果把三者视为同一种成功,就可能漏掉故障并造成重复处理。

流程图: 一句话总结 · 为什么需要它 · 工作原理 · Ready signal 也有边界

为什么需要它

假设正在运行一个虚拟订单接收 server。用户只点击了一次订单按钮,但网络不稳定,未能看到响应。页面提示重试,用户再次发送同一订单,于是 server 收到两个 HTTP 请求。在把每次请求都当作新事件的 event system 中,可能创建两次独立执行。用户体验中的一次点击,并不能保证 server 只执行一次。

也可能发生相反的误解:webhook 返回 200,业务却没有启动。filter 可能拒绝了输入,Sensor 也可能没有创建 Workflow 的权限。如果只凭 client response 报告成功,下一位故障调查人员就会从错误假设出发。

本实验不是真实支付系统,而是一份小型订单 ledger。我们会故意应用错误路径、发送意外输入,并遗漏 submit 权限;随后两次发送相同订单,比较业务写入次数。目的不是单纯经历失败,而是学习如何通过证据缩小失败所在层次。

工作原理

Ready signal 也有边界

EventSource 接收 HTTP 请求并发送到 event bus,Sensor 订阅后评估条件。Deployment Ready 只表示 Pod readiness condition,并不表示某个事件的订阅已经全部完成。预探测中,连 Pod Ready 后立即发送的正常 control group 也没有触发执行。该版本 JetStream connection implementation 只处理新 consumer 开始订阅后到达的 message。因此,发送第一个事件前,应一起确认 Sensor Pod 与订阅状态。

还要理解日志时态。Subscribing 表示正在尝试,并且记录在 subscribe call 之前。应确认成功订阅的日志与 consumer 实际 waiting 状态,再发送第一条业务事件。简单多 sleep 几秒,在较慢环境中仍可能失败。此项检查只是对齐实验起点的步骤,并不能保证阻止生产环境中的所有 event loss。

Filter 是输入契约的一部分

Webhook event 的 HTTP body 位于 event data 的 body 中。如果正文包含 kind,data filter path 就是 body.kind。如果写成 body.event.kind,则 HTTP body 中必须存在名为 event 的 nested object。要观察 path 不存在时的拒绝,也必须让同一个 Sensor 接受的正常 control group 成功执行。仅凭没有任何 Workflow,无法断定 filter 正确。

string filter 按 regex 评估。order.created pattern 中的点并不是 literal dot,也没有限定起止位置。实测中,pre-orderXcreated-tail 也能够通过。如果只允许一个精确 kind,就应把点作为 literal character,并限制整个字符串。输入种类增加时,扩大 allow set 的修改尤其需要谨慎验证。

type: bool 也不等同于严格 JSON schema validation。在本实验版本中,不仅 boolean true 能通过,string true 与 number 1 也能通过。如果只允许 JSON boolean,就要检查原始值 type,而不是转换后的结果。本实验通过 Lua filter 区分它们。这只是特定 field 的 type contract,不能替代整个请求的 schema validation。

组合多个条件时,OR 只需满足一个,AND 则要求全部满足。如果 kind 错误时不能仅因 enabled 为 true 就执行业务,应检查二者之间的 operator。不存在 field 的 error 也只是某一条件拒绝,不能假设它会使 OR 中其他条件的批准失效。

谁负责创建,谁负责执行

应分离 Sensor 创建 Workflow 所使用的 identity,以及 Workflow 内 task 使用的 identity。本次 Kubernetes resource creation trigger 只向 submit account 授予 Workflow create。业务账户则拥有记录必要 task result 的权限。为了消除权限错误而把 admin role 同时绑定给两个账户,既无法了解真正所需权限,也会扩大 impact scope。

被拒 event 的 HTTP response 仍可能是 200。因此,应一起查看 Sensor 的真实 forbidden message、请求 trace ID,以及是否创建 Workflow。以 least privilege 修复后,要用新事件确认恢复。若要声称过去失败的事件会自动重处理,就必须单独观测过去那个 ID。

Arrival ID、execution ID 与 business key

同一订单的两次 HTTP 请求获得不同 arrival ID 并不异常。两个不同 Workflow UID 也不表示有两个订单。business key 在重试时必须仍指向同一订单。本实验的 ledger 使用订单 ID 作为 business key,并把“检查该 key 是否已经写入”与“写入金额”放在同一个 database transaction。

简单实现会让两个执行都增加金额。改进后的实现中,两次执行都可以正常结束,但只有一方修改 ledger,另一方按 duplicate 处理。如果 key 相同而金额不同,就不能伪装成正常 retry,应以 conflict 拒绝。即使没有读到 success response,只要 commit record 已存在,下一次请求也可以不再增加业务效果。

现场会遇到的情况

业务 dashboard 如果只有一个 success count,就无法知道它代表哪种成功。应分别考虑 HTTP accepted count、approved event count、Workflow completed count 与 business committed count。在发生 duplicate request 的时段,这些数字彼此不同,系统仍可能按预期运行。反过来,即使数字完全相同,如果错误输入全部得到批准,业务仍是错误的。

本实验的 single-VM ledger 不是 high-availability database,也不是真实支付系统。如果在同一 SQLite transaction 之外发送 email 或调用 payment,就无法把这些 external effect 原子地包含进来。此类需求应另行评估外部 service idempotency key、outbox 等 delivery contract。这里验证的仅是本实验的 local ledger effect,不保证 session 结束后的保留,也不保证所有外部副作用只执行一次。

下一项实验要做什么

破坏 filter path、regex、type 与 logical operator,并与正常 control group 比较。使用 minimal Role 修复真实权限拒绝后,在 duplicate request 中分别确认两次执行与一次 business effect。最后编写简短 incident report,区分观测了哪些层次,以及还有哪些内容尚未证明。

官方参考:Data filterScript filterServiceAccount固定版本的订阅实现SQLite transaction