服务网格 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/20261003T091739Z-10960/ 保存代码校验值、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。
让代理重试有可见的次数和边界
以下命令在 examples/service-mesh 目录执行,使用 Kubernetes 1.33.4、Istio 1.27.0 的 sidecar 实验基线。先完成 04 的安装与累计应用部署;容器名为 app,Service 端口为 8080。先恢复上一章策略,再执行本章,避免超时、授权或出口规则互相污染。所有预期结果都是验收条件,未运行集群时保留 NOT_RUN。
1 | |
sm-probe 使用 orders ServiceAccount,明确注入 sidecar;它提供 curl,不依赖 scratch 应用镜像里不存在的 shell。每次请求同时保存客户端结果、来源代理配置和目标应用日志。
mainline/14/retry.yaml 将 GET 的总体预算设为 1s、单次尝试 100ms、额外重试最多 2 次;POST 单独匹配并显式 attempts: 0。配置值是上限,不保证每次都有三次尝试。inventory-v1 的 350ms 等待能制造单次尝试超时,必须从代理实际 retry policy 核对超时是否进入所选可重试条件;不能仅凭应用日志重复次数反推所有失败都会重试。
1 | |
POST 的 quote-delay 不生效,因此写入组应产生一条 ch14-write 账本记录;它只是“这次配置没有重试该写入”的观察,并未赋予接口幂等性。GET 要比较同一 ID 的 started 记录、代理 upstream_rq_retry 变化和最终状态;预期多次尝试且不超过 3 次,若只有一次,检查超时、retry_on、请求体是否可重放,而不是修改记录补满。恢复后同一 SKU 应重新成功,账本因 Pod 重建消失也是本示例不持久化的限制。
还可把 mainline/15 故障桩的 fail 模式作为确定的上游 503 来源;这与客户端侧 fault.abort 不同,后者可能在重试过滤器之前结束请求。本章避免把代理主动制造的 503 当成可靠的后端重试触发器。
固定源码入口:Istio 1.27.0 pilot/pkg/networking/core/route/route.go。该链接用于核对配置生成或执行机制;完整资源和命令保存在 examples/service-mesh/mainline/14/。静态检查通过不等于集群实验通过。
练习与参考解答
- 超时后的重发携带相同
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 连接限制与异常摘除。





