TT Lab
开始
学习 学习路径 课程

CRD 与 Operator

存下来的和展示出来的并不是一回事

在 TT Lab 中继续学习

一句话总结

CRD 的多个版本,是用不同的窗口展示同一份字节的机制;提升存储版本,与真正把数据迁移过去,是两回事。把这两者连接起来的记录就是 status.storedVersions。

为什么需要这个流程

API 一旦公开就很难收回。用户在 Git 中提交了清单,etcd 中也堆积着以那种表示形式存储的对象。所以当需要修改字段时,要做的不是“修改”,而是“发布新版本”。

这里必须分开思考一件事:提供(served)与存储(storage)是不同的两个维度。

标志 含义 最多几个
served 是否接收以这个版本发起的请求 多个
storage 是否以这个版本的表示形式写入 etcd 恰好一个

存储只会以一种表示形式进行。所以,如果用 v1beta1 查询由 v1alpha1 创建的对象,会把存储的字节取出,转换成请求的版本后返回。决定转换方式的是 spec.conversion.strategy,什么都不写就是 None。

None 不做转换。它只是改一下 apiVersion 标签,然后原样展示。只有当字段名称在两个版本中相同时才能使用这种策略,所以它很危险。即使新版本新增的字段有默认值,这个值也看不到。默认值是在存储时填充的,而这个对象仍然以旧的表示形式存储着。界面上看起来就像没有这个字段,而完全相信这种状态的控制器就会做出错误的判断。

工作原理

迁移流程有四步,顺序就是一切。

1. 새 버전을 served 로 추가한다            (옛 버전도 그대로 제공)
2. storage 를 새 버전으로 옮긴다           (앞으로 저장할 표현만 바뀐다)
3. 기존 오브젝트를 전부 한 번씩 다시 쓴다   (여기서 실제 이전이 일어난다)
4. status.storedVersions 를 정리하고
   옛 버전을 served: false → 제거한다

第 3 步是这个流程的核心。执行到第 2 步的那一刻,status.storedVersions 中会同时包含旧版本和新版本。这个列表的含义是“用这个 CRD 到目前为止实际存储过的表示形式”。重新写入对象后,会以新的表示形式存储,但 API 服务器不会去统计是否还有以旧表示形式存储的对象。所以第 4 步的清理,是由人来声明“重新写入已经完成”的行为。

如果不做这个声明,API 服务器就会把最后一道门锁上。如果试图把仍留在 storedVersions 中的版本从 spec.versions 中去掉,请求会被拒绝,并且会如实告诉你错误出在哪个字段。如果没有这道安全装置,一删掉旧 schema,以那种表示形式存储的对象就读不出来了。

降低版本时使用的两个字段也值得一并了解。加上 deprecated: true 之后,用该版本发起请求时客户端会收到警告,并且可以用 deprecationWarning 自己指定文字。警告不会阻止请求——它是为人们提供迁移时间的机制。

在现场相遇的样子

第一,跳过重新写入的事故。最常见的形式是这样:提升了存储版本,一段时间内没有任何问题(因为新创建的对象用的是新的表示形式),几个月之后,在清理工作中删除了旧版本。就在那一刻,较旧的对象从列表中消失了。它们仍在存储中,但已经没有可以读取它们的 schema。尤其危险的是,先手动删掉 storedVersions 而越过了安全装置的情况。

第二,悄悄出错的控制器。在 strategy: None 的 CRD 中,给新字段设置默认值,并让控制器读取这个字段来改变行为。新创建的对象已经填好了默认值,而旧对象是空的。控制器会把同一种类的对象分成两种方式对待,而这个差别只取决于创建的时间点。重新写入连这个问题也一并消除。

第三,对象很多时的实际做法。对象有几万个时,不会一次性运行重新写入。因为每一条都是一次 etcd 写入,所有观察 watch 的控制器都会收到事件。通常会分批、在使用量低的时段、一边统计进度一边运行。Kubernetes 有一个替你做这件事的独立 API,叫 StorageVersionMigration(官方说明)。不过流程的骨架与现在要学的相同,只有亲手走过一遍的人,才知道那个工具替你做了什么。在本实验中,要亲自确认每一步改变了什么。

本实验环境的局限

实验 Pod 中没有启动 Webhook 服务器的手段。所以不涉及 conversion.strategy: Webhook,只在 None 的范围内确认。换句话说,字段名称发生变化的迁移,在这个环境中无法测试。取而代之的是,用对象来证明 None 究竟做什么、不做什么——重新写入之前不会填充新字段的默认值,重新写入之后会填充。这个差别就是迁移确实发生的证据。

下一项实验要做什么

创建一个有两个版本的 CRD,收到弃用警告之后,提升存储版本并重新写入所有对象,用默认值被填充来证明迁移。然后尝试删除旧版本,被 API 服务器拦下,再按正确的顺序清理记录,关闭旧窗口。最后编写一次性判定迁移是否完成的检查脚本。