端点换了,监听器为什么还没把请求送过去

独立 Envoy 的静态配置可以把 GET /quote?sku=paper 送到 inventory-v1,但每次换库存地址都要改文件、重载代理,无法看清资源更新过程。教学服务在 examples/service-mesh/cmd/teaching-xds/main.go:用官方 Go go-control-plane v0.13.4 的 SnapshotCache 和 ADS 服务发布一个 listener、一条精确匹配 /quote 的 route、一个 EDS cluster 与一个 endpoint。官方库携带 v3 protobuf(go-control-plane/envoy v1.32.3);真实 Envoy 1.37.0 已完成本例有限资源的转发、更新、拒绝和重连验证,不据此宣称所有 API 组合都兼容。

LDS 引用 RDS、RDS 引用 CDS、CDS 引用 EDS;nonce 回送与业务转发分属两条路径

图里的资源依赖是 LDS → RDS → CDS → EDS,箭头表示引用或订阅需要,不是四条业务网络跳数。Envoy 读取 deploy/envoy/teaching-xds.yaml 的静态 bootstrap,向 loopback 127.0.0.1:18000 建 ADS 流;xDS 进程以固定 node ID teaching-envoy 发布版本 1。客户端 127.0.0.1:19000 是待启用的 Envoy listener,127.0.0.1:18080 是候选库存应用:本教学服务不代理任何报价数据。启动顺序、资源 warming 和当前活跃 listener 必须从实际 Envoy 状态检查,不能以“服务端 SetSnapshot 成功”替代。

版本、nonce 与拒绝的边界

控制面的工作可以拆成三个独立动作:把期望状态编码为 protobuf、把资源快照交给缓存、处理代理订阅及反馈。internal/teachingxds/snapshot.go 只负责第一个动作;server.go 把 SnapshotCache 接到库提供的 ADS server,并记录反馈;cmd/teaching-xds/main.go 负责 loopback 监听和标准输入命令。分开这些动作后,资源构造失败不会被误记成网络失败,连接断开也不会被误记成资源已经删除。

bootstrap 中的 xds cluster 是静态配置。它必须先存在,Envoy 才能连接控制面取得动态的 inventory cluster。这里的两个 cluster 不能互相替代:前者承载 HTTP/2 gRPC 控制流,后者承载报价请求。把控制面地址放进动态资源却不提供最初的连接入口,会出现启动依赖无法满足的问题。

动态监听器使用 HTTP connection manager,把 /quote 路径交给 quote-route。RDS 的精确路径匹配不把查询串作为路径的一部分,因此示例请求仍可携带 ?sku=paper。route 再引用 inventory,CDS 将该集群声明为 EDS 类型,EDS 给出 127.0.0.1:18080。每类资源只有一个名称,读者可以在配置转储中逐一核对。

变化 服务端响应 模拟客户端反馈 仍须到 Envoy 验证
初始订阅 版本 1、nonce 与 EDS 版本 1 ACK 活跃 listener 和实际库存版本
端口更新 版本 2、新 nonce NACK 时带旧版本 1 旧请求去向与后端应用日志
断开并重连 最新快照版本 2 重新订阅 新连接是否完成 /quote

Snapshot.Consistent() 检查本例中 listener 指定的 RDS 名称、EDS cluster 指定的端点名称是否齐全。它不是整个资源图的语义校验器:固定版本源码明确说明,CDS 和 LDS 采用无名称引用的请求方式,快照中的 cluster 列表不因此获得“所有路由目标都存在”的保证。实验删除 RDS、删除 EDS 时分别断言一致性检查失败;合法快照仍需交给代理做具体字段和扩展校验。

调用 port N 产生版本 2、3……的新快照并变更 EDS。端口输入限制为 1–65535,地址固定为 loopback;命令不会接受任意远程地址。为了让示例容易观察,四类资源使用同一个版本字符串,即使只有端点变化也会更新四类版本。这会产生冗余响应,不能作为大型控制面的推送效率设计。不同类型的响应也不会因为版本文字相同就成为全局原子事务。

服务端日志在收到 DiscoveryRequest 的 response_nonce 后,用流 ID、类型 URL 和 nonce 查找已发送的版本,再结合 error_detail 记录 ACK 或 NACK,以及客户端报来的 version_info。同一个 nonce 字符串可能在新流中再次出现,所以不能只用 nonce 做全局索引。无法关联的反馈记为 UNMATCHED,不能补猜成 ACK;关闭流时清理该流尚未确认的关联记录。

NACK 必须保留错误及客户端最后接受的版本,不能把 NACK 上的 version_info 当新版本已接受。示例测试明确断言:服务端发送版本 2,模拟客户端拒绝它时仍报告版本 1。SnapshotCache 中的期望状态不会因此自动回滚。再次连接时客户端订阅到的仍然是版本 2;要恢复有效状态,需要发布一个有效的新快照。这里使用 SotW ADS,不实现 Delta xDS。

这一处理方式可迁移到其他配置发布系统:发布版本 → 接收反馈 → 核对实际使用状态 是三个证据层级。ACK 证明代理接受了某次响应,但没有证明后端监听成功、业务认证成功或请求已经到达库存。后两者必须加入真实请求及应用日志。

故意错误怎样进入实验

标准输入命令 invalid 构造一个缺少路径匹配条件的 RDS route。protobuf 可以编码这份资源,快照中的名称依赖也完整,但生成的 ValidateAll() 会拒绝它,真实 Envoy 预期对该 RDS 响应发送 NACK。这个入口专用于拒绝实验,不代表生产控制面应把已知无效资源发给代理。

测试特意保留这一层差异:Consistent() 通过不等于配置字段合法。若把所有错误都提前截在生成器里,能够证明输入校验,却无法观察代理 NACK;若忽略所有提前校验,则很难定位到底是引用错误还是字段错误。实验固定其他资源,只破坏 route match,便于把拒绝归因到一个具体字段。

代理拒绝 RDS 后应继续使用先前接受的路由。运行脚本在看到 NACK ... sent_version=3 后,再发一条带请求 ID 的报价请求,并检查库存版本仍为 v2。随后发布有效版本 4,验证恢复到 v1,最后正常停止并重启 Envoy,检查新进程重新订阅后仍能访问 v1。这里只覆盖代理进程重启后的重连;没有模拟 TCP 半开、控制面集群切换或长时间网络分区。

运行与验收

在 examples/service-mesh/ 目录运行本地协议断言(需 Go 1.25.1 与网络可获取锁定模块;若 go 不在 PATH,设置 GO_BIN):

1
bash scripts/run-E08.sh

测试通过内存 gRPC 流订阅 EDS,检查初始响应包含版本 1 和资源、回送相同 nonce 的 ACK、变更端口后的版本 2、对新 nonce 的 NACK 仍报旧版本及断流重连后收到最新资源;缺少 RDS 或 EDS 依赖的快照必须失败。测试客户端模拟反馈,不是 Envoy;NACK 反馈也不表示服务器自动回滚快照。测试使用 go test -race -count=1,避免把旧缓存结果算作新证据。若断言失败脚本退出非零并保存原始输出,证据放在 examples/service-mesh/evidence/E08/<UTC时间>-<PID>/。其它 E01–E07 的真实代理、集群实验没有因为此测试而通过。

真实 Envoy 必须与 Go 服务位于同一网络命名空间:这里全部地址都是 loopback。macOS 上单独启动容器中的 Envoy,再让它访问宿主 127.0.0.1,不会连接到宿主的库存与控制面。可以使用同一 Linux 宿主上的二进制,或把整套实验放进同一容器网络环境。环境具备后运行:

1
ENVOY_BIN=/absolute/path/to/envoy bash scripts/run-E08.sh --envoy

脚本先执行协议断言和 bootstrap 校验,再启动两个库存进程与控制面。检查点分别保存 initial.json、updated.json、rejected-keeps-v2.json、recovered.json、reconnected.json;xds.log 保存发送与反馈,*-config.json 保存代理配置,两个库存日志保留应用侧证据。任何一步未得到预期结果,脚本退出非零,只有全部通过才写 envoy.result=PASS。默认运行只写协议层结果,并把数据面标为 NOT_RUN。

真实 Linux Envoy 1.37.0 运行证据位于 examples/service-mesh/evidence/E08/20261003T090715Z-2/,run.exit 为 0。初始请求返回 v1,端点更新后返回 v2;非法版本 3 的 RDS 被 Envoy 拒绝,日志保留 sent_version=3 accepted_version=2 和缺少 path_specifier 的错误,随后请求仍返回 v2;有效版本 4 恢复为 v1,代理进程重启后新 ADS 流再次 ACK 版本 4,报价仍为 v1。上述结果由五份响应 JSON、xds.log、配置转储和库存日志共同支持,已超过模拟客户端反馈的证据范围。

该运行也暴露了教学实现的限制:无效期望快照仍留在缓存时,日志可出现同一坏版本的多次 NACK;服务端没有发布去重、退避或自动回滚策略。测试以有效新版本结束拒绝过程,不能把它称为生产控制面的故障隔离。没有覆盖多代理收敛、网络分区或长连接保活中的更新,因此这些场景仍未验收。

项目故意不含多租户隔离、控制面认证与授权、证书/SDS、密钥管理、跨进程 HA、持久存储、故障隔离和增量订阅;端口更新输入甚至来自标准输入,只可放在隔离宿主。不能以它替代 Istiod,也不能向生产 Envoy 提供配置。先修 03 Envoy、06 路由 和 07 xDS。

练习与参考解答

  1. 画出从换 EDS 端口到确认 /quote 命中新库存的证据链。参考解答:版本 2 的 EDS 响应及 nonce → 客户端 ACK/当前活跃 endpoint → 相同请求 ID 的代理访问记录、上游地址和新库存应用日志;单独 SetSnapshot 或 EDS ACK 都不能推出调用成功。
  2. 删掉 RDS 资源只保留 listener,测试应怎样失败?参考解答:Snapshot.Consistent() 应拒绝资源引用不完整;即使强行推送,Envoy 可能进入 warming/NACK,不能宣布“删掉路由就能访问默认后端”。检查缺失资源与客户端反馈,而不是降低断言强行通过。
  3. 版本 3 的 RDS 被拒绝后,把控制面缓存仍为版本 3 当成“代理已运行版本 3”,错在哪里?参考解答:缓存保存期望状态,NACK 表明对应响应未被接受。应检查该类型最后接受版本、配置转储及请求结果;恢复要发布有效的新版本,不能修改日志上的版本号。
遇到的问题 应核对的边界 本例入口
资源缺失 名称引用是否完整 Snapshot.Consistent()
配置拒绝 字段校验和代理错误详情 invalid、NACK
更新没有影响请求 接受状态、活跃配置、后端响应 port N、config dump、库存版本
重连收到旧状态 流与 nonce、当前缓存版本 关闭流后重新订阅

资料与系列导航

Envoy 1.37 xDS 协议、go-control-plane 固定提交的 cache 与 server。核验记录 writing-plans/service-mesh/research/E08.md。

系列导航:E07 Ambient 多集群 → E08 教学 xDS(本文) → 35 采用决策。