LabHub
学习 学习路径 课程

ICA — Istio 认证助理

响应失败与业务失败不是同一件事

在 LabHub 中继续学习

一句话总结

重试是为了再次获得响应,而不是撤销此前业务的命令。本模块会在真实 Istio mesh 中分别统计 HTTP 接收次数与 SQLite 业务记录,并建立一道边界:即使响应丢失或进程终止,也不会重复执行同一业务。

流程图: 一句话总结 · 为什么需要它 · 工作原理 · 三本账簿

为什么需要它

假设用户只点击了一次支付按钮,页面却显示超时。客服认为操作失败,建议再次点击;服务器运维人员解释,proxy 会自动再重试两次;但业务人员却发现,同一订单产生了三条支付记录。这三个人的观察可能全部属实。

页面的一次请求、proxy 的多次传输尝试,以及数据库中的业务写入,是彼此不同的事件。只统计 HTTP 200 看不到重复业务,只统计 504 又会误以为已经提交的业务被取消。因此,必须同时记录业务 key、传输 request ID、response code 和实际保存的业务 ID。在故障报告中写“执行了三次”之前,首先要说明统计的究竟是什么。

LabHub 先前的合成实验也观察到了这种差异。使用同一业务 key 的并发请求中,一部分客户端收到 504,其中一个请求在服务器接收记录中的最终状态却是 200,而业务记录只增加了一条。不能把这个比例背成适用于所有环境的规律。本实验不依赖随负载偶然变化的比例,而使用一条在业务提交后将响应延迟 2 秒的对照路径。

工作原理

三本账簿

在 client Pod 中执行一次 curl 时,会附加一个 request ID。如果 Istio 重新发送同一请求,upstream 的 receipts 账簿中就会多次出现相同 request ID。业务 key 则放在另一个 header 中。即使重新开始传输,只要目标是找回同一订单,就必须保留相同业务 key;反过来,即使内容相同,只要是不同订单,就必须使用不同 key。

服务器提供了故意设计错误的 /naive 路径,以及使用去重记录的 /flaky 路径。两者的前两次响应都是真实 upstream 503,下一次响应为 200,并非 proxy 自行生成的假错误。/naive 每次收到请求都会新增业务,而 /flaky 会复用该业务 key 的既有响应。即使 HTTP 响应序列相同,业务效果也可能不同。

次数配置是上限,不是保证

VirtualService 的 attempts 是在首次传输之外允许的重试上限。attempts 为 2 时,连同首次尝试在内最多传输三次。所有尝试共享整体 timeout 预算。perTryTimeout 是每次尝试可等待的上限;快速失败的请求不会被视为用完了全部这段时间。

retryOn 选择哪些失败类型可以重试。指定 HTTP 503,并不意味着所有连接错误与读取超时都会归入同一条件。本实验为了比较单一 upstream,允许再次选择之前的 host。不要误以为这就解决了真实服务中的重试风暴、backoff、circuit breaker 和容量规划。

504 之后仍然存在的业务

/lost 会先提交业务与去重记录,只把响应延迟 2 秒。该路径的 proxy 预算设为 500ms,并关闭自动重试。使用同一个 key 查询客户端的 504 与服务器中的业务记录,然后向快速的 /charge 路径发送相同 key,以取回既有业务响应。这里并不是要创建新订单,因此不能签发新的 key。

这不是外部支付服务商的 API,而是把合成业务写入本地 SQLite 的服务器。响应记录中的 200 只表示服务器原本打算生成的响应,并不能证明客户端收到了这些字节。这正是必须区分接收记录与客户端原始结果的原因。

原子性必须位于同一事务中

如果先提交业务,再写入去重 key,两次提交之间就会出现空隙。若进程在这个空隙中终止,业务仍在,key 却不存在;使用同一 key 的重试会被误认为新业务。因此,业务写入以及 key、内容 fingerprint、响应的保存必须放入同一个 transaction。

SQLite 在同一时刻只允许一个 write transaction。即使用 BEGIN IMMEDIATE 启动 write transaction,也不代表一定能无限等待获得锁;发生竞争时,仍需处理 BUSY 或 timeout。本实验只使用一个本地 DB 文件和短暂的合成任务,并没有实现将多个 DB 或外部支付调用一次性原子化的 distributed transaction。

现场会遇到的情况

服务重启后,内存字典不会恢复。实验会真正删除并重新创建 Pod,确认 UID 与 process instance 已改变,然后从保留在同一 VM 磁盘上的 SQLite 文件读取既有响应。经受住 Pod 替换,不代表也能应对 VM 删除、磁盘损坏或断电恢复。

内容 fingerprint 也必不可少。如果使用同一 key,却更改金额或客户信息,就应以冲突拒绝,而不是无条件返回既有响应;另一方面,如果只是 JSON 属性顺序改变,则应被视为相同内容。仅凭 payload fingerprint 把金额相同的不同订单合并,同样是错误。

最后,需要修复所提供 transaction.py 中拆分提交的缺陷。检查器会在新的临时 DB 中运行学员实现,并向真实子进程发送 SIGKILL。在提交前的两个位置,业务与 key 应同时不存在;提交后则应同时存在。随后通过新连接重试,并检查业务是否仍只有一条。不能仅通过抛出异常来代替强制终止。

下一项实验要做什么

观察 Pod inventory、重试 route、业务去重对照、timeout、内容冲突与 Pod 替换。随后修改 transaction 代码,并检查并发请求和不同业务 key。观测工具会把真实原文保存到文件;如果已存在同一观测文件,就不会重复执行。评分会将该文件与服务器保留的接收记录进行对照。

业务 DB、测试文件与 namespace 都只存在于本学习 VM 中。请勿放入个人 key 或真实支付资料。实验结束时,这些内容会全部回收。与外部系统集成时所需的对端 API idempotency key、outbox、reconciliation 与 compensation 属于后续设计课题,不应声称已在此实现。

通过官方文档进一步确认