没收到响应,是否可以再发一次

不行,除非请求本身可安全重复,或者应用明确实现并验证了去重。第 13 篇已经看到客户端退出后上游仍可能完成;本篇在同一 inventory 的合成 POST /reserve 上检查响应丢失之后重发。业务输入固定为 operation_id=op-01、X-Request-ID=sm-01-repeat,两次使用相同标识不是应用幂等的证明。

库存第一次已写账本但响应延迟;重发同一操作 ID 再写一次

库存 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
bash scripts/run-01.sh

脚本直接访问累计工程的合成库存服务,先让写操作成功提交而客户端超时,再人工重发同一 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
2
3
4
5
6
export SM_CONTEXT=kind-service-mesh-sidecar
k() { kubectl --context="$SM_CONTEXT" -n service-mesh-lab "$@"; }
k get deploy frontend orders inventory-v1 inventory-v2
k apply -f mainline/13/probe.yaml
k rollout status deploy/sm-probe --timeout=120s
OUT="evidence/mainline-14/$(date -u +%Y%m%dT%H%M%SZ)"; mkdir -p "$OUT"

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
2
3
4
5
6
7
8
9
10
11
12
13
k apply -f mainline/14/retry.yaml
k exec deploy/sm-probe -c curl -- curl -fsS -H 'x-request-id: ch14-normal' 'http://inventory:8080/quote?sku=paper' > "$OUT/normal.json"
k patch deploy inventory-v1 --type=json -p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"-quote-delay=350ms"}]'
k rollout status deploy/inventory-v1 --timeout=120s
k exec deploy/sm-probe -c curl -- curl -sS --max-time 2 -w '\n%{http_code}\n' -H 'x-request-id: ch14-retry' 'http://inventory:8080/quote?sku=paper' > "$OUT/retry-response.txt"
k logs deploy/inventory-v1 -c app > "$OUT/inventory.log"
istioctl --context="$SM_CONTEXT" proxy-config routes deploy/sm-probe -n service-mesh-lab -o json > "$OUT/routes.json"
k exec deploy/sm-probe -c curl -- curl -fsS -H 'x-request-id: ch14-write' -H 'content-type: application/json' -d '{"operation_id":"ch14-write"}' 'http://inventory:8080/reserve' > "$OUT/write.json"
k exec deploy/sm-probe -c curl -- curl -H 'x-request-id: ch14-ledger' -fsS 'http://inventory:8080/ledger' > "$OUT/ledger.json"
k rollout undo deploy/inventory-v1
k rollout status deploy/inventory-v1 --timeout=120s
k exec deploy/sm-probe -c curl -- curl -H 'x-request-id: ch14-recovered' -fsS 'http://inventory:8080/quote?sku=paper' > "$OUT/recovered.json"
k apply -f deploy/kubernetes/inventory-route.yaml

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/。静态检查通过不等于集群实验通过。

练习与参考解答

  1. 超时后的重发携带相同 operation_id,能保证只入账一次吗?参考解答:不能。例子先提交再延迟响应,重发后同一 ID 对应两条账本记录;需要服务端原子地关联幂等键、请求参数、状态与结果,不能只靠代理记住历史请求。
  2. 将客户端重试限制为最多 2 次、代理重试限制为每次最多 3 次,理论最多有几次后端尝试?怎样构造反例说明总请求数不一定等于此上界?参考解答:在每层所说的次数都是总尝试次数时上界为 2 × 3 = 6;第一次就成功只需一次,整体 deadline 提前耗尽也可能少于六次。若配置写的是“额外重试次数”,还须把首试加进去,不能混用定义;以代理尝试日志验证真实数量。

官方参考与系列导航

系列导航:00 无网格基线 → 13 超时与取消 → 14 重试与副作用(本文)→ 15 连接限制与异常摘除。