100 毫秒后客户端放弃,库存服务是否停止

frontend → orders 调用链(orders 先调用 inventory,成功后再调用 external-stub) 有一个容易混淆的事实:客户端不再等待,不推出每个上游都没有开始、没有完成,更不表示已提交的业务写入被撤销。第 01 篇的合成账本已经显示写入后延迟响应可使客户端超时而账本仍有一条记录。本篇改用同一应用的完整只读调用链,观察取消在哪一跳产生后果。

客户端 100ms 超时后,orders 不再调用 external-stub,但 inventory 的人工延迟仍完成

图中时间轴表示进程实验的既定等待条件,不是 Envoy 各种超时的默认值:inventory 收到请求后 time.Sleep(350ms),随后尝试写出 HTTP 200;客户端 curl --max-time 0.1 在先前退出。orders 使用入站请求的 context 创建下游请求,因此取消可阻止接下来访问 external-stub;inventory 的演示 handler 没有在睡眠时检查 request.Context().Done(),会在客户端退出后继续运行到日志记录处。日志记录了应用处理步骤,不能保证取消后的响应字节被客户端实际收到。

五个计时器不共享同一职责

先写出用户可接受的整体期限,再减去网关、排队、连接、序列化和留给调用方处理响应的预算;orders 顺序调用库存与外部依赖,不能给两者分别配置一个等于总预算的时间还声称链路有界。客户端整体 deadline 限制调用者等待;连接超时限制建连阶段;单次尝试超时限制一次上游请求;代理的路由总超时约束该代理所见请求;应用自己的 context 控制是否继续下游工作。这些计时器可能在不同位置启动,具体生效值以冻结版本及数据面活跃配置为准。

1
2
3
4
用户预算 B
> frontend/代理排队与建连 + orders 处理
+ inventory 尝试(含重试时各次开销)+ external-stub 尝试
+ 响应传输与安全余量

若 orders 在 80ms 时才收到请求,却又为两段顺序调用各设置 100ms 的独立超时,下游理论上可能超出用户 100ms 的原始等待期限。要传递的是剩余预算而不只是固定常数;重试时还须让所有尝试共同消耗整体 deadline。没有贯通的 context/deadline 传播,就算 Envoy 已返回超时,应用也可能继续进行有副作用的操作。

在累计服务链上验证边界

在 examples/service-mesh/ 运行以下命令,脚本只监听本机回环接口,正常请求与超时请求各用独立请求 ID:

1
bash scripts/run-13.sh

脚本启动同一组 frontend、orders、两个 inventory 版本及 external-stub 进程,其中 orders 的正常目标为 v1。成功组的客户端预算 2 秒;失败组 100 毫秒;只读报价人工延迟 350 毫秒。分别记录完整命令、服务端结构化日志、客户端标准输出/错误和退出码。断言器要求正常组看到库存 v1 与外部 stub,失败组 curl 返回非零 28,库存仍记录该请求的 started 和 200,而外部 stub 无此请求 ID;任何反例使断言器以非零退出。这个进程实验只说明应用层取消的一个具体实现,不推断代理的请求超时、连接超时和重试默认行为。

本次运行证据在 examples/service-mesh/evidence/13/20261003T093019Z-198/:正常请求客户端返回库存 v1 与 external-stub,超时请求的 curl 退出 28;库存日志同一超时 ID 出现 started 后又出现 200,外部依赖没有这个 ID;四项断言及 Go 测试通过。把复制的证据中 timeout.exit 改成 0 后,断言器预期退出 1(临时副本不提交)。这些是本机进程结果,不是网格代理结果;客户端超时不等于服务器未执行,库存处理完成也不等于客户端收到响应。

待装入隔离集群后,另需为 frontend → orders 和 orders → inventory 保存客户端 deadline、Istio 路由/单次尝试超时、代理实际 route 与访问日志,并分别在建连、响应头前、响应体中注入延迟,标注请求在哪层失败;当前 Kubernetes、Envoy、Istio 实验 NOT_RUN,不能据进程日志填写代理状态码或成本。

把 100ms 预算落实到出站路由

以下命令在 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-13/$(date -u +%Y%m%dT%H%M%SZ)"; mkdir -p "$OUT"

sm-probe 使用 orders ServiceAccount,明确注入 sidecar;它提供 curl,不依赖 scratch 应用镜像里不存在的 shell。每次请求同时保存客户端结果、来源代理配置和目标应用日志。

mainline/13/timeout.yaml 将 inventory 路由固定到 v1,timeout 为 0.1s,retries.attempts 为 0。先不加延迟,正常报价应成功;然后只给 v1 增加 350ms 处理等待。curl 的 2s 上限故意大于代理预算,避免先由客户端超时掩盖代理行为。

1
2
3
4
5
6
7
8
9
10
11
12
k apply -f mainline/13/timeout.yaml
k exec deploy/sm-probe -c curl -- curl -fsS --max-time 2 -H 'x-request-id: ch13-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 -o /dev/null -w '%{http_code}\n' -H 'x-request-id: ch13-timeout' 'http://inventory:8080/quote?sku=paper' > "$OUT/timeout-code.txt"
test "$(cat "$OUT/timeout-code.txt")" = 504
istioctl --context="$SM_CONTEXT" proxy-config routes deploy/sm-probe -n service-mesh-lab -o json > "$OUT/routes.json"
k logs deploy/inventory-v1 -c app > "$OUT/inventory.log"
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: ch13-recovered' -fsS --max-time 2 'http://inventory:8080/quote?sku=paper' > "$OUT/recovered.json"
k apply -f deploy/kubernetes/inventory-route.yaml

验收要同时满足正常和恢复请求返回 v1、超时请求为代理生成的 504、活跃路由出现 0.1s 与禁用重试配置,并在日志中关联 ch13-timeout。若 2s 后 curl 自己退出 28,不能把它当作代理 100ms 已生效;若出现 503,应优先排查连接、端点和安全策略。恢复用 rollout undo 仅限本实验刚产生、没有并发修改的 Deployment revision;执行前后保存 rollout history,其他部署变化存在时按原始 manifest 恢复。

这里只控制 orders 所访问库存这一跳。frontend 的用户 deadline 与 orders 对 external-stub 的剩余预算仍由应用负责,路由 timeout 不能替应用分配整条顺序调用链的预算。

固定源码入口:Istio 1.27.0 pilot/pkg/networking/core/route/route.go。该链接用于核对配置生成或执行机制;完整资源和命令保存在 examples/service-mesh/mainline/13/。静态检查通过不等于集群实验通过。

练习与参考解答

  1. 客户端 100ms 超时,库存日志为 200,是否说明超时配置无效?参考解答:不是。100ms 控制客户端等待,库存 handler 忽略取消并在 350ms 后继续记 200;需要检查相同请求 ID 的客户端退出码、链路取消传播和后端账本,不能把服务端日志的 200 当作客户端收到的 200。
  2. 把库存报价人工延迟从 350ms 降为 10ms,仍保持客户端 100ms 超时;断言“外部依赖必定没有请求”可靠吗?参考解答:不可靠。库存可能在超时前完成,orders 随即调用外部 stub;其结果还取决于调度和外部响应时间。实验应按请求 ID 保存每段开始/结束,并改变延迟分别验证,不能把一次特定执行顺序归纳为超时语义。

官方参考与系列导航

系列导航:00 调用基线 → 12 网格路由 → 13 调用链超时预算(本文)→ 14 重试与副作用。