服务网格 E03:Proxyless gRPC 怎样把治理放进客户端
同一报价,为何换一个客户端就不遵守路由
主线第 07 篇用独立 Envoy 讨论 LDS/RDS/CDS/EDS 的依赖。将 orders → inventory 的报价做成真正的 gRPC Quote unary 方法,让 Go grpc-go v1.76.0 客户端经 xDS resolver 发现后端:路由和负载均衡的部分决策进入应用进程。如果另一个 HTTP 客户端仍直连当前工程的 /quote,它不会因同机运行了 Go SDK 而接受 xDS 配置。本例使用独立 Go 模块固定该版本,与累计 HTTP 工程分开运行。
图的上行是资源分发及反馈,下行是报价请求;控制面不转发每笔订单。客户端以 bootstrap 知道 xDS 服务、使用 xds:///... target,通过 xDS Client 将受支持的 listener、route、cluster、endpoint 映射到 gRPC channel 的服务配置、路由和连接选择。按 gRPC 官方 xDS 功能矩阵,Go 客户端从 v1.30.0 支持 LDS→RDS→CDS→EDS 与 ADS,v1.31.0 支持按 path/header 路由及带权多 cluster,v1.36.0 支持 v3 API;固定候选 v1.76.0 晚于这些最低版本,仍必须以部署时的 xDS 资源和值验证。矩阵对某些不支持的配置值说明会 NACK,未识别的特性也可能被忽略;不能把 Envoy 全部 HTTP 过滤器、完整 L7 能力或任意 TLS 选项都自动赋予 SDK。传输身份与路由也须分别配置。gRPC 方法路径形如 /inventory.Inventory/Quote,不是现有 HTTP /quote,不能复用 HTTP 的 exact path 规则而不修改。
失败时谁继续拿着旧配置
以功能对齐后的真实 gRPC 方法为前提,观察范围如下;这张表不是 grpc-go 对所有 xDS 资源的支持承诺:
| 条件 | 客户端待查状态 | 后端待查结果 |
|---|---|---|
| 初始受支持的路由 | channel 订阅与连接状态 | v1 实际处理 Quote |
| 更新端点/路由 | ACK/NACK、有效版本 | 新请求命中 v2 |
| 错误配置或端点消失 | 保留旧状态还是等待配置 | 失败位置与真实错误码 |
资源引用示意(不是可直接安装的 xDS JSON):
1 | |
最小可验证用例:对相同 sku=paper、不同请求 ID 的 gRPC Quote 分配到 inventory-v1,之后更新 RDS/CDS/EDS 把它指向 v2;记录客户端应用 channel 状态、xDS ACK/NACK(若可见)、服务端方法和返回版本。负例用不存在的端点或错误资源;资源被 NACK 时不可宣称新配置生效,已有 channel 是否继续工作、首次建立 channel 是否因缺少配置失败,应分开测试。健康状态、连接缓存、调用 deadline 与客户端 fallback 配置决定观测,不能猜返回码。orders 成功取得库存结果后才调用 external-stub;失败后不应因代理或 SDK 的重试自动断言外部调用发生。
升级边界也不同:sidecar Envoy 的二进制可按工作负载或网格 rollout,不需要重编译每个应用;proxyless 新增不支持的 xDS 功能必须升级、部署客户端库和它的应用构建,且跨语言库的支持进度可能不同。两条路线都需要核对 xDS 控制面和客户端版本兼容;客户端进程丢失 xDS 控制面时,可用性由已缓存资源与现有连接决定,不能从 主线 09 的 TLS ALPN 协商推出 gRPC 方法调用或 xDS 订阅。主线 09 只证实普通 HTTP 实验的 TLS 行为。
下节提供真实 protobuf Quote 服务、Go xDS 客户端和 ADS 控制面。主线 09 的 TLS 实验只验证普通 HTTP 的 TLS 行为,不作为本例证据。先修 14 重试 说明为什么 Quote 的成功不推出 POST /reserve 恰好执行一次。
可运行的 Inventory 与 xDS 控制面
独立模块位于 examples/service-mesh/electives/E03/,不与 E08 的依赖文件混用。它固定 grpc-go v1.76.0、go-control-plane v0.13.4 及 Envoy proto v1.32.4。服务契约是:
1 | |
请求用 Struct 承载 sku 和 id,服务端检查二者非空,返回相同 ID、sku、版本和固定合成价格。使用已生成的标准 protobuf Struct 可省去 protoc 安装;这仍是实际 protobuf 编码和 gRPC unary 调用,不是 HTTP JSON 模拟。生产接口通常应生成有类型的 QuoteRequest/QuoteResponse,避免把字段合法性全部交给运行时。
main.go 用 grpc.ServiceDesc 注册 inventory.Inventory/Quote,启动 v1、v2 两个真实 TCP gRPC 服务,以及一个注册 AggregatedDiscoveryService 的控制面。客户端执行:
1 | |
文件同时通过空白导入 google.golang.org/grpc/xds 注册 resolver。这个例子全部监听 127.0.0.1,业务和控制通道都使用明文,只验证配置分发与路由;它不证明 mTLS、证书轮换或生产身份体系。把 insecure 改成 TLS 还需要相应的凭据来源与服务端配置,不能只改一个枚举。
bootstrap 必须在进程启动前设置。脚本以环境变量提供以下结构,默认控制面端口 18003,也可用 XDS_ADDRESS 改成另一个 loopback 地址:
1 | |
Node ID 与 SnapshotCache 的键一致,控制面才会把这份快照发给该客户端。端口冲突会直接报错,不去结束占用端口的其他进程。grpc-go 在初始化时读取 bootstrap 环境配置,因此在 main 里晚设变量不能替代脚本的预先设置。
路由资源怎样组成一次调用
控制面为客户端发布 ApiListener,内含 HTTP connection manager 的 RDS 引用。RDS 的前缀 /inventory.Inventory/ 选中库存 cluster,CDS 使用 EDS 发现,EDS 提供 loopback TCP 端点。它们是 Go 构造的 v3 protobuf 资源,不是拼接的伪 JSON。
1 | |
EDS locality 设置 region 和权重 1。一次运行保持同一个客户端 channel,先发版本 1 指向 v1,再发布版本 2 指向 v2;测试轮询实际 Quote 响应直到观察到预期版本。已经建立的连接不能凭“控制面发过消息”就被认定完成迁移,需要新调用返回新的业务版本。
go-control-plane v0.13.4 的通用 Snapshot.Consistent 辅助函数从网络 filter chain 提取 RDS 引用,不覆盖本例的 ApiListener。示例直接 SetSnapshot,并由真实客户端订阅、ACK 和业务调用完成验证;它没有把该辅助函数返回错误当作 gRPC NACK。控制面回调将每个请求的 type、version、nonce、error 写到 stderr,业务服务记录 handled ID 和实际版本。
[PATTERN] 配置接收与业务生效使用两种观测。ACK 说明资源被接收,Quote 返回的版本说明这次调用去了哪里;缺任意一项,都无法定位配置传播和业务连接之间的差异。
故障端点与恢复
2026-10-03 在 macOS arm64、Go 1.27.0 上实际运行该模块,观察到 v1→v2、故障端点的 DeadlineExceeded,以及恢复 v1。原始 stdout、控制面订阅/ACK 与 handler 日志保存于 examples/service-mesh/evidence/electives-20261003/E01-E03/E03-final/。这是 loopback 上真实 gRPC/xDS 实验,集群接入、mTLS 和跨语言兼容仍未验证。
1 | |
脚本保存 Go 版本、完整模块列表、bootstrap、标准输出、标准错误和退出码。成功时依次出现 PASS route v1、PASS route v2、PASS failure、再次 PASS route v1 和 PASS recovery。这是预期的检查序列,实际结果应读取该次输出目录。
失败阶段把 EDS 指向一个仅监听 TCP、没有启动 gRPC server 的端口。保留 listener 是为了避免“选择一个空闲端口后被其他进程抢占”的竞态。每次 Quote 设 800ms deadline,更新后容许 15 秒传播时间,直到观察到调用错误;错误码原样打印,不能预写成某个固定状态。之后版本 4 恢复 v1,并要求同一 channel 再次取得 v1 结果。
这证明的是不可服务端点导致调用失败以及恢复后的业务可达性,不是非法 xDS 配置的 NACK 实验。若要测试 NACK,应单独引入不受支持的资源字段,并要求 error_detail、拒绝版本和保留旧业务路径同时出现;不能把端点断联当作解析拒绝。
[PATTERN] 故障注入要命名所在层。资源无效、端点不可达和业务返回错误分别作用于配置解析、连接建立与 handler 执行,具有不同恢复方式;只写“请求失败”会丢掉实验解释力。
| 模式 | 本例证据 | 适用边界 |
|---|---|---|
| 同一 channel 观察版本迁移 | v1→v2 实际 Quote 返回 | 不代表所有跨语言 SDK |
| 端点故障与配置拒绝分开 | deadline/错误状态、EDS 更新 | 未测试 NACK |
| 回滚也要业务结果 | 版本 4 恢复 v1 | 未验证证书或生产控制面 |
练习与参考解答
- 画出 xDS server 暂停后已建立 channel 与新建 channel 的两条路径。参考解答:已建立 channel 可能依靠既有连接/缓存配置继续请求,新建 channel 是否拿到必要资源要实测;控制面不在 gRPC Quote 数据通道上,两者不能共用一次 ACK 作业务证明。
- 在 HTTP 进程实验加入
h2ALPN 后能否宣称完成 proxyless 路由?参考解答:不能。TLS 握手选择应用协议并不创建 gRPC 服务方法、xds resolver、bootstrap 或资源订阅;需让 gRPC 客户端真正调用方法并比较服务端 v1/v2 响应。
资料与系列导航
gRPC xDS 功能矩阵、Envoy 1.37 xDS 协议;源码入口 grpc-go xds 固定提交。研究记录 writing-plans/service-mesh/research/E03.md。
系列导航:E02 Cilium → E03 Proxyless gRPC(本文) → E04 VM 接入。
