跳转至

第 13 章 动态更新:从一次切换到可观察的发布

服务器启动成功,只完成了控制平面的第一半工作。第二半是持续变化:输入更新后重新计算资源,让代理收到、接受,并让真实请求表现出预期结果。本章以实验 02的蓝绿切换为主线,前置知识是 snapshot、版本和 ACK/NACK。目标是能走完一次发布,区分各阶段证据,并对失败选择正确的恢复方向。

实验故意把变化控制在一个清晰范围:LDS 中 listener_0 引用 RDS book_routes;blue_backend、green_backend 两个 Cluster 及其 EDS 地址始终存在;切换操作只改变 Route 选择哪个 Cluster。这样既能看到真正的 CDS、EDS、LDS、RDS,又能把“新后端尚未分发”与“路由切换失败”分开理解。blue、green 与 Envoy 共用网络命名空间,因此这里的 EDS 可以使用 127.0.0.1:8081 和 127.0.0.1:8082;这不是普通独立容器之间的网络默认行为。

13.1 先建立更新前的证据

进入实验目录启动,确认业务初态:

cd labs/02-xds
docker compose up --build -d
curl -fsS http://127.0.0.1:18080/

预期得到纯文本 blue。如果代理尚未就绪,先等其完成订阅并查看 Compose 日志。接着读取只绑定本机的 admin:

curl -fsS http://127.0.0.1:19901/config_dump

输出往往很长。先搜索 listener_0、book_routes、blue_backend、green_backend,再观察各资源类型的动态状态和版本。不要只凭“名字出现了”就判断它在生效:admin 中可能同时展示 active、warming、静态和被拒绝配置的相关信息,需要看对象位于哪一类列表以及错误状态。

此时可以画出最小事实链:浏览器请求到 Envoy 业务 Listener;Listener 的 HCM 使用 book_routes;该路由当前转发到 blue_backend;Cluster 从 EDS 获得 blue 服务地址;最终收到 blue。这条链会帮助你把后面的变化定位在某一环,而不是一旦出错就重启所有容器。

13.2 发起一次确定的切换

通过实验管理接口选择 green:

curl -fsS -X POST 'http://127.0.0.1:19000/switch?backend=green'
curl -fsS http://127.0.0.1:18080/

第一条请求向控制平面应用提出更新,不是直接修改 Envoy 的 admin 配置。应用生成新的 RouteConfiguration 和 snapshot,发布到 envoy-book 的缓存键;已有 RDS watch 被唤醒,服务器发送新版本。初始版本为 v1,第一次成功切换会返回类似 {"backend":"green","version":"v2"} 的 JSON。Envoy 接受之后,后续业务请求应返回 green。HTTP 200 只说明该接口约定的发布动作成功,不代表“所有代理已经 ACK”。

接口只接受 POST,其他方法返回 405;backend 只接受 blue 或 green,非法值返回 400,不发布也不推进版本。重复选择当前合法值仍会生成新版本。这让实验的变化容易观察,也会产生没有业务内容差异的发布;生产系统可根据内容摘要避免这类重复工作。GET /healthz 返回成功仅说明控制平面进程可以服务,不能用它替代代理就绪或配置确认检查。

切换接口返回和代理收敛之间可能存在短暂时间差。第一条之后立即发的业务请求若仍是 blue,需要结合日志和版本判断是否还在正常传播;若持续不变,就必须沿链条排查。不要用无限重试把一个确定的错误掩盖成“最终总会好”。在实际发布系统中,应当为各阶段设置合理截止时间并报告当前停在哪一步。

恢复初态使用同一个接口:

curl -fsS -X POST 'http://127.0.0.1:19000/switch?backend=blue'
curl -fsS http://127.0.0.1:18080/

这仍是一次新的发布,只是内容回到之前的业务选择。它不是向旧报文发送 ACK,也不是把 nonce 倒回去。版本应按应用的发布策略推进,或者稳定标识对应内容;nonce 仍由当前流上的响应生成。

13.3 把“发布成功”拆成证据链

一次修改至少经历输入已接收、资源已构造并校验、缓存已更新、响应已准备并发送、代理已 ACK、业务行为已验证几个阶段。每个阶段都回答不同问题,应该保留可关联的发布号和时间,而不是只有一条笼统的“成功”日志。

看到的证据 可以说明 还不能说明
管理接口返回成功 应用接受或完成其约定的发布动作 代理一定已接受
GetSnapshot 能读到新版本 当前缓存已经更新 更新已到达全部连接
响应回调有新版本与 nonce 服务器准备发送该响应 网络发送成功或客户端 ACK
匹配 nonce 的 ACK 客户端处理了对应配置更新 后端实际健康、全部请求成功
admin 中新路由 active,请求返回 green 本代理的新路径有实际效果 整个代理群都已收敛

这个表的价值在于故障时可以停在最早缺失的证据上。只有缓存更新、没有响应,检查 Node 分组键、订阅和版本;有响应、没有确认,检查连接及代理日志;ACK 存在、业务不通,检查 active/warming、路由匹配、地址和后端,而不是继续修改 nonce。

13.4 NACK 不是控制平面自动撤销

假设最新 RDS 被拒绝,代理通常继续使用这个类型最近成功的配置。可是 SnapshotCache 可能仍保存着被拒绝的新 snapshot。库不会因为一个 NACK 就自动推导业务正确版本,更不会替所有代理同步回滚。应用需要记录每个目标的确认状态,查明原因,再发布修复内容或明确的回滚内容。

这里要区分协议拒绝与业务错误。一个资源字段非法,可能触发 NACK;一个语法合法但指向错误后端的路由,则可能被 ACK,最后返回 503。路由引用缺失 Cluster 也不能一概保证触发 NACK,其行为与资源及校验设置有关。实验应先在隔离环境里构造确定的错误,再观察实际拒绝信息,不能拿一段猜测的错误字符串写死自动化判断。

NACK 的 error_detail.message 对人工定位很有用,但不是跨 Envoy 版本稳定的机器协议。自动化应围绕是否存在错误、类型、发布记录与关联 nonce 建立状态,再把原始消息作为诊断信息保留。含配置内容的错误也要避免把 Secret 直接写入日志。

13.5 并发更新与重启会改变问题形状

如果两个管理请求同时到来,慢请求可能基于旧输入构造快照,却比新请求更晚写入 cache,造成旧配置覆盖新配置。简单实验可用互斥锁串行完成“修改期望状态、生成版本、构造校验、发布”这一段;复杂系统则可用队列、代次号和 compare-and-swap 等方式拒绝过时代次。只给计数器加锁,未必保护了整个发布顺序。

进程重启后也不能让同一个版本名代表另一份内容。实验的版本是进程内的 v1、v2 等,重启回到 v1,这是为了把样例保持简单,不能直接作为生产版本方案。代理可能保留旧配置并报告已有版本,如果新服务恰巧复用旧版本,某些更新就不容易被识别。持久发布号、唯一启动前缀或稳定内容摘要都是可考虑的策略,需要结合多副本一致性选择,而不是随意拼接当前时间。

实验控制平面的内存状态不是持久配置数据库,重建容器或重启进程后应按 README 说明重新确认初态。生产系统需要从持久的期望状态重建 cache,确保新副本在接受客户端前已经准备好正确快照。cache 是加速和分发层,不应成为唯一不可恢复的业务真相。

13.6 从实验切换扩展到正式迁移

实验始终保留蓝绿后端,是一种很好的教学简化。真实迁移要新建 Cluster、改变协议或更换证书时,依赖变化更多。比较清晰的步骤是先增加新依赖,让目标代理获得它并验证可用,再修改引用,观察错误率与延迟,最后在旧引用消失且连接排空策略允许时清理旧资源。

“先增加、再切换、后清理”不是任何场景都能无条件保证零故障,还要考虑客户端重连、连接复用、上游长连接、资源 warming 以及多个代理的收敛差异。但它显式保留了过渡时期所需的两套依赖,通常比先删除再等待所有引用追上更易控制。ADS 的有序发送可以帮助这一过程,仍不能替代业务探测和逐组发布。

发布完成后,按照实验 README 停止容器;docker compose down 会移除这组实验容器和默认网络,工作区源码仍保留。它不是回滚生产配置的通用命令。把本地实验、代理运行状态和持久期望状态分清,才能把学到的方法迁移到真实系统。

13.7 练习与参考答案

  1. /switch 已返回成功,业务仍为 blue。列出至少三处应检查的位置。
  2. 新路由已经 ACK,后端连接被防火墙拒绝,这属于 NACK 吗?
  3. 为什么实验在切换后仍保留旧 Cluster,而不马上删掉?

参考答案:第一题检查目标 Node/缓存键与版本、RDS 订阅及响应、关联 nonce 的确认、admin 活跃路由和实际后端;按证据链定位最早缺口。第二题不是,ACK 反映配置处理结果,网络连通性需要另行验证。第三题保留依赖有利于理解和回退,也减少引用切换期间找不到目标的风险;正式环境何时清理还要考虑代理群收敛与连接排空。

本章源码与实验