服务网格 14:重试为何会放大副作用
没收到响应,是否可以再发一次
不行,除非请求本身可安全重复,或者应用明确实现并验证了去重。第 13 篇已经看到客户端退出后上游仍可能完成;本篇在同一 inventory 的合成 POST /reserve 上检查响应丢失之后重发。业务输入固定为 operation_id=op-01、X-Request-ID=sm-01-repeat,两次使用相同标识不是应用幂等的证明。
库存 handler 的顺序是解析操作 ID → 追加内存账本并记录 201 → 人为延迟 400ms → 写响应。客户端第一次等待只有 100ms;看到超时,不知道服务器停在提交前还是提交后。第二次把等待放宽到 2s 并发送同一输入,若没有去重便得到第二笔。对 read 请求的重试也可能增加下游负载,但不会由此自动推出“读请求永远安全”:日志、计费、外部副作用或应用错误实现都要另查。
两层重试如何相乘
假设仅作为配置上界的示意:SDK 对失败调用最多尝试 2 次,每一次调用都被代理各尝试 2 次;一笔用户操作最多触发 2 × 2 = 4 次后端尝试,而不是“额外重试两次”。这不是本仓库实际代理配置或观测比例。并发客户端、多层退避同步、连接池排队和超时预算会扩大排队时间及尾延迟;到达响应或取消时,部分已经发出的尝试无法自动撤回其应用副作用。合理的约束是控制整体 deadline、确定可重试错误条件、限制层数与预算、加入退避抖动,并以相同请求 ID 与 attempt 标识核对每一跳。
幂等键也不是魔法。服务端必须把“该键的操作是否已执行”与结果持久化,并与业务写入作原子决策;相同键携带不同输入要有明确拒绝规则,TTL 到期后的重发要有业务约束。当前示例只把 operation_id 存入内存列表,刻意不去重,重启后列表也消失;不能拿这份账本当作可上生产的幂等实现。Envoy、Istio 和客户端 SDK 的重试可以改善某些瞬时故障的可用性,不能为非幂等操作提供事务或“恰好一次”。
正常与失败路径的进程实验
在 examples/service-mesh/ 执行:
1 | |
脚本直接访问累计工程的合成库存服务,先让写操作成功提交而客户端超时,再人工重发同一 operation_id;还用缺少必要字段的输入验证 400 不能写入账本。examples/service-mesh/evidence/01/20261001T140937Z-2620941/ 保存代码校验值、Go/Node 版本、完整命令、每次原始输出/退出码及账本:第一次 curl 退出 28;首次查询账本已有序号 1;再次发送退出 0;最终账本有序号 1、2 且同为 op-01。非法输入 curl 返回 22,账本未增加第三笔,四项断言与 go test ./... 通过。失败断言改造临时证据后返回非零的机制在 checks/verify-01.mjs 可见。
这是一组人工重发,不是代理自动重试;读者重跑可比较账本和请求 ID,但不能据此声称 Istio 对 POST 的任何默认重试政策已经验证。待隔离集群可用后,需分别关闭 SDK/代理重试、单独开启各层、再组合:保存代理重试策略、active route、每次上游 attempt 的 ID/原因、库存账本与客户端结果;配错策略的负例必须让断言非零。Envoy 未可用、Istio/Kubernetes 尚未冻结,本项自动重试验证 NOT_RUN。
练习与参考解答
- 超时后的重发携带相同
operation_id,能保证只入账一次吗?参考解答:不能。例子先提交再延迟响应,重发后同一 ID 对应两条账本记录;需要服务端原子地关联幂等键、请求参数、状态与结果,不能只靠代理记住历史请求。 - 将客户端重试限制为最多 2 次、代理重试限制为每次最多 3 次,理论最多有几次后端尝试?怎样构造反例说明总请求数不一定等于此上界?参考解答:在每层所说的次数都是总尝试次数时上界为
2 × 3 = 6;第一次就成功只需一次,整体 deadline 提前耗尽也可能少于六次。若配置写的是“额外重试次数”,还须把首试加进去,不能混用定义;以代理尝试日志验证真实数量。
官方参考与系列导航
- RFC 9110 §9.2.2:请求方法的幂等语义和自动重试的边界。
- Istio VirtualService HTTPRetry:条件、次数与单次尝试超时,具体默认行为待部署版本验证。
- Envoy v1.37.0 retry policy:代理重试触发与限制,进程实验尚未覆盖。
系列导航:00 无网格基线 → 13 超时与取消 → 14 重试与副作用(本文)→ 15 连接限制与异常摘除。
