第 17 / 23 篇

第 16 章:CRD 版本切换与存量迁移

三种版本含义

API served 版本决定客户端能使用哪些路径,storage 版本决定新写入使用何种持久表示,status.storedVersions 记录可能仍存在于存储中的历史版本。三者有关联,但不是同一开关。把 beta 设为 storage,并不会自动重写所有 alpha 历史对象。

本项目第二份 CRD 增加 v1beta1,并将它设为唯一 storage 版本,同时保留 v1alpha1 served。两个版本 schema 相同,转换使用默认的 None 策略,主要变化是请求和响应的版本标识。它没有定义字段重命名或结构转换,不需要 conversion webhook。

如果两个版本字段语义不同,默认转换并不能凭空推导对应关系。删除字段、改变单位或拆分结构时,应设计转换协议、兼容策略和回退路径。仅保持 JSON 看起来相近,不代表用户依赖的语义仍然一致。

真实实验具体做了什么

测试先以 alpha 创建网关并收敛,再应用包含 alpha 与 beta 的 CRD。随后通过 beta 路径读取同一对象,核对 UID 不变,修改副本并写回,再让仍使用 alpha 客户端的控制器继续协调。最后读取 CRD,确认 storedVersions 同时包含 alpha 与 beta。

这条路径证明新版本被服务、写入版本切换后旧客户端仍能处理同结构对象,以及对象身份没有因版本变化而重建。它没有完成所有存量对象的重写,也没有删除 alpha served。测试日志明确保留两个历史版本,避免把尚未完成的过程写成已迁移成功。

通过 beta Update 的这一对象会按当前 storage 版本写入,但单个对象重写不能代表整个集群迁移。其他命名空间、分页列表中的历史对象或同时发生的写入都需要覆盖。没有全量核对和控制并发,就不应清除 CRD 的历史 storedVersions 记录。

完整存量迁移的职责

实际迁移通常需要先升级所有理解新版本的控制器和客户端,再启用新 storage,使用专门迁移工具或受控读取重写覆盖全部对象,核查存储状态,最后才考虑去除旧版本。每一步都需要可观察进度和回滚判断,不能靠一次 kubectl apply 把整条流程压扁。

storageVersionHash 可以辅助识别服务端存储版本的一致性,但它不是每个历史对象已被重写的证明。读取对象时 API server 也可能按请求版本转换,因此从 beta GET 成功不能反推出底层只剩 beta。证据必须对应真正需要证明的存储事实。

本书不直接访问 kind 的 etcd 来制造一个看似完整的迁移报告。教学范围停在同结构 served/storage 切换与一次明确重写,完整全量迁移作为后续独立项目设计。这样读者不会误把一段缩短的示例用在不可逆的生产 API 删除上。

控制器发布与 API 发布分开

修改 CRD 会影响集群中所有实例,即使应用程序代码没有变化。发布计划应先确认旧控制器能否读取新对象、新控制器能否处理旧对象,以及准入策略如何变化。把 schema 收紧也可能让历史对象在下一次更新时被拒绝,需要提前扫描。

控制器镜像回滚并不一定让 CRD 回滚安全。存储里已经写入新结构后,旧程序可能丢字段或误解释状态。尤其是删除字段、改变枚举和单位转换,必须有双向兼容或明确的不可回退边界。版本号只是标识,兼容性来自具体协议。

给用户留下迁移窗口

弃用旧 served 版本前,应记录使用方、告警和迁移期限。自动化客户端可能埋在脚本、GitOps 清单或其他控制器中,不能因为交互式 kubectl 已改用 beta 就判断没有旧依赖。较长窗口还需要保证文档示例与实际服务版本一致。

练习是把 message 改名为 responseBody,先在纸上写出 alpha 到 beta 和反向转换规则。考虑空值、默认值以及新旧客户端同时更新。只有这些规则可解释并能实际往返,才适合实现 conversion webhook;单纯复制字段名不等于完整版本工程。

不可逆动作留到最后

删除旧served版本和清除历史storedVersions都应发生在证据充分之后。前者可能立即让旧客户端失败,后者是在声明历史存储已经迁移。顺序弄反会使后续恢复更难,而不是让迁移更快。

版本实验的摘要刻意保留未完成项,帮助使用者判断下一步。一个明确写着尚未全量迁移的结果,比把所有阶段压成“升级成功”更适合工程决策。