从一个可定位的请求开始

loadgen 请求 frontend 查询合成商品 paper,后者调用 orders,orders 再读取 inventory-v1 和 external-stub。要让这次调用成功,客户端至少需要知道地址、能解析目标、连接正确的端口、找到可用后端,并获得与请求对应的响应。先把代理和控制面排除在外,后续才能判断新增的网格层究竟改变了哪一段。

本文要求读者能区分 HTTP 响应与 TCP 连接失败,并认识 Kubernetes 的 Pod 和 Service。实验代码使用 Go 标准库;本篇只有本机进程实测,尚无 Kubernetes、Envoy 或 Istio 的运行证据。

读请求从调用者到两个合成依赖的路径,失败检查点标在入口之前

图中 orders 是 frontend 必须连到的上游:域名不正确,连接甚至不能发起;目标是空集合,应用没有可以选择的地址;端口不对,即使 IP 正确也可能被拒绝。图里的地址都是本机监听地址,不是 Kubernetes Service 的 ClusterIP。

把“找到服务”拆成四次判断

  1. 名称解析:orders.invalid 无法转换为可连接的地址;程序不会因为另一个进程叫 orders 就自动找到它。Kubernetes 内部通常通过 Service DNS 名称提供稳定入口,但 DNS 回答并不包含“请求一定能成功”的保证。
  2. 端口与连接:127.0.0.1 是明确的目标地址,连接 :1 时没有监听者,TCP 建连遭拒。端口错误与域名解析失败发生在不同阶段;此时还没有订单业务响应。
  3. 后端候选:frontend 的上游地址留空时,本例直接返回 503,并没有进行 DNS 或 TCP 尝试。在 Kubernetes 中,Service 与 EndpointSlice 用来表达入口和端点;“Service 存在但没有可用后端”必须在实际集群中另验,不能拿这个应用分支当作其实现或错误码。
  4. 响应与副作用:成功建立连接也只说明能和某个监听者通信。必须核对请求标识、业务输入、后端版本和结果;读请求后还要查看合成账本,排除意外写入。

这些判断有先后关系,却不是所有失败都会暴露在同一层。例如调用者可能拿到 HTTP 502,但它不说明是 DNS、TCP、上游响应解析还是中间代理产生:必须结合谁返回 502及该处日志判断。本实验的 502 来自示例 frontend,不是 Envoy。

运行同一组进程并保留失败

在仓库的 examples/service-mesh/ 目录执行,需 Go 1.25.1、Node 22 与 curl。脚本只启动本机 127.0.0.1 上的合成服务,不连接 Kubernetes context,结束时只终止自己启动的进程:

1
GO_BIN=/tmp/service-mesh-tools/go/bin/go bash scripts/run-00.sh

若 Go 已在 PATH,可运行 bash scripts/run-00.sh。脚本每次在 evidence/00/<UTC 时间>-<PID>/ 新建证据目录;其中 *.command 是完整启动和请求命令,*.exit 是原始退出码,*.stdout/*.stderr 是响应及每跳调用日志,assertions.txt 保存断言结论,environment.txt 保存环境。完整工程说明见同仓库的 examples/service-mesh/README.md,源码见 examples/service-mesh/apps/mesh-demo/main.go。

2026-10-01 的一次进程实验记录在 examples/service-mesh/evidence/00/20261001T121421Z-2487240/。断言结果如下;数字是该次实验客户端的退出码,不是 HTTP 状态:

请求场景 客户端退出码 frontend 响应 可区分的本地原因
sm-00-success 0 200,inventory_version=v1、external=external-stub orders 的两个下游均回答,结果携带同一 request_id
sm-00-wrong_domain 1 502 lookup orders.invalid … no such host,尚未建立 TCP 连接
sm-00-wrong_port 1 502 127.0.0.1:1: connect: connection refused,地址存在但端口无监听者
sm-00-empty_endpoints 1 503 no endpoints configured for orders,frontend 的配置分支没有上游候选

inventory-v1 的 GET /ledger 在这次只读实验返回 []。一个空账本只能约束当前运行、当前这个内存实例;它不能证明别的实例、别的时间没有业务副作用。检查 sm-00-success 在四个服务 stderr 中的记录,可以将调用者输入、每跳处理和最后响应串起来;这不是对代理路径的证明。

如果改成实际 Kubernetes Service,需要另外保存 Service、EndpointSlice、DNS 查询、Pod/节点网络配置、实际请求的后端记录,以及断言失败时的退出码。当前宿主未安装容器运行时、kind 或 kubectl,也没有现成集群:空 EndpointSlice、CNI 和 Service 数据面路径均为 NOT_RUN。复跑约束和步骤见 writing-plans/service-mesh/research/00.md。不要把“静态 YAML 可解析”或本站 Hexo 构建当作这一步已完成。

反例与适用范围

如果把 frontend 的 orders URL 从正确端口换成 127.0.0.1:1,服务名即使写成正确名字,TCP 仍不能凭空找到监听端口。反过来,即使端口可连,orders 返回的请求标识与输入不匹配时,本例会拒绝该上游结果;“网络通了”不等于“完成了这次调用”。本例没有 Service 的 DNS、EndpointSlice 收敛、代理注入或 xDS,因此无法推论那些组件的默认行为。下一篇用同一进程链讨论治理应放在 SDK、网关、代理还是业务代码。

练习与参考解答

  1. 某请求得到本例的 502,frontend 日志显示 lookup orders.invalid … no such host。能否判定 inventory 已写入?参考解答:不能。frontend 尚未连到 orders,不能从这个 502 推导 inventory 的任何操作;应核对同一请求 ID 的下游记录与合成账本。更不能由此推导其他并发请求的结果。
  2. 把 frontend 的 orders 地址改成 http://127.0.0.1:1,或清空 orders 地址,各应在哪一步失败?参考解答:前者解析/地址输入阶段已得到 loopback IP,TCP 连接被拒,示例 frontend 生成 502;后者应用发现候选为空,直接生成 503,不发生对 orders 的 DNS 或连接。若把这个反例迁移到 Kubernetes,必须另外查实际 Service、EndpointSlice 与运行时网络路径,不能假设其 HTTP 码与此处相同。

官方资料与系列导航

系列导航:00 一次服务调用需要哪些基础条件(本文)→ 01 SDK、网关与服务网格分别处理什么。