4、多版本 CRD 存储策略:如何选择 storage 版本,数据在 etcd 中的存储形式

在 Kubernetes 中,一个 CRD 可以定义多个 API 版本(如 v1alpha1、v1beta1、v1),但 etcd 中只存储一个版本的完整对象。这个版本被称为 storage 版本。理解 storage 版本的选择机制以及数据在 etcd 中的实际存储形式,是进行 CRD 版本迁移和 Finalizer 清理的前提。

4.1 核心概念:served 版本 vs. storage 版本

每个 CRD 的 versions 字段中,每个版本都有一个 served: true/false 和一个 storage: true/false 标记。这两个标记决定了 API 服务器如何与用户和 etcd 交互。

属性 作用 说明
served: true 该版本可通过 REST API 读写 用户可以使用 kubectl get myresource.v1beta1.example.com 访问该版本
storage: true 该版本是 etcd 中持久化的版本 整个 CRD 只能有一个版本设置 storage: true
关键原则: 用户可以通过任意 served 版本写入对象,但 Kubernetes 内部始终将其转换为 storage 版本后存入 etcd。读取时,API 服务器从 etcd 读出 storage 版本对象,再转换为用户请求的 served 版本返回。

4.2 如何选择 storage 版本

选择 storage 版本需要遵循以下策略:

  1. 选择最稳定的版本:通常选择 v1 或 stable 版本作为 storage 版本。避免选择 v1alpha1 或 v1beta1,因为这些版本可能在未来被废弃。
  2. 选择字段最完整的版本:storage 版本应包含所有其他 served 版本的字段超集。如果 v1 比 v1beta1 多一个字段,那么 v1 作为 storage 版本时,v1beta1 写入的旧对象在读取时会被自动填充默认值。
  3. 避免频繁切换:一旦 CRD 有大量资源对象,切换 storage 版本需要执行存储迁移(Storage Migration),这是一个有风险的操作。
  4. 版本迁移时的选择:当引入新版本并废弃旧版本时,新版本应设置为 storage: true,旧版本应设置为 storage: false 并最终 served: false。
最佳实践: 在 Operator 开发初期,如果只有一个版本(如 v1alpha1),直接将其设为 storage 版本。当引入 v1 时,将 v1 设为新的 storage 版本,并通过 Webhook 转换(Conversion Webhook)处理版本间的数据转换。

4.3 数据在 etcd 中的存储形式

Kubernetes 将 CRD 对象以 JSON 格式 存储在 etcd 中,但存储的是 storage 版本 的完整对象。存储路径遵循以下模式:

/registry/<group>/<resource>/<namespace>/<name>

例如,一个名为 myapp 的 MyResource 对象,属于 example.com 组,存储在 default 命名空间下:

/registry/example.com/myresources/default/myapp

etcd 中存储的 JSON 结构包含完整的 Kubernetes 对象元数据(apiVersion、kind、metadata、spec、status),其中 apiVersion 字段的值是 storage 版本 的完整版本标识。

注意: 即使你通过 kubectl get myresource.v1beta1.example.com 写入对象,etcd 中存储的 apiVersion 仍然是 example.com/v1(如果 v1 是 storage 版本)。这是版本转换的必然结果。

4.4 存储版本与 Finalizer 的关系

Finalizer 是存储在对象 metadata.finalizers 字段中的字符串列表。由于 Finalizer 是对象元数据的一部分,它也会被转换为 storage 版本后存入 etcd。这意味着:

  • 如果 storage 版本中 metadata.finalizers 字段的定义与其他 served 版本一致,则 Finalizer 在所有版本中可见。
  • 如果 storage 版本中 metadata.finalizers 字段被重命名或移除,则其他版本中的 Finalizer 可能无法正确读取或写入。
  • 在版本迁移过程中,如果旧版本对象包含 Finalizer,迁移到新 storage 版本后,Finalizer 必须被正确保留,否则可能导致资源无法被删除。

4.5 存储版本切换流程

以下 SVG 图展示了 CRD 从 v1beta1 切换到 v1 作为 storage 版本时,数据在 etcd 中的存储变化:

CRD 存储版本切换流程 阶段 1:v1beta1 作为 storage 用户写入 v1beta1 对象 ↓ API Server 直接存储 ↓ etcd 中存储 v1beta1 格式 apiVersion: example.com/v1beta1 切换 storage 阶段 2:v1 作为 storage 用户写入 v1beta1 对象 ↓ API Server 转换为 v1 ↓ etcd 中存储 v1 格式 apiVersion: example.com/v1 存储版本切换的影响 旧对象(v1beta1)需要迁移到新格式,否则读取时可能丢失字段 Finalizer 在所有版本中保持一致,但存储版本决定了其持久化格式

4.6 多版本共存时的数据流

以下 SVG 图展示了当 CRD 同时 served v1beta1 和 v1,且 v1 为 storage 版本时,读写请求的数据流:

多版本 CRD 读写数据流(v1 为 storage 版本) 用户 API Server etcd 写入 v1beta1 存储 v1 格式 读取 v1 格式 返回 v1beta1 版本转换 API Server 负责 v1beta1 ↔ v1 的自动转换

4.7 存储版本选择决策表

以下表格总结了不同场景下的 storage 版本选择建议:

场景 推荐 storage 版本 理由
只有一个版本(如 v1alpha1) 该版本 无需转换,直接存储
多个版本共存,v1 已稳定 v1 v1 是稳定版本,字段最完整
多个版本共存,v1beta1 仍为主力 v1beta1 避免频繁转换,但需规划迁移到 v1
废弃旧版本,引入新版本 新版本 旧版本最终会被移除,新版本作为长期存储
需要兼容多个集群版本 最广泛支持的版本 确保跨集群一致性

4.8 验证 storage 版本

你可以通过以下命令查看当前 CRD 的 storage 版本:

# 查看 CRD 定义
kubectl get crd myresources.example.com -o yaml

# 在输出中查找 storage 版本
# 输出片段示例:
# versions:
# - name: v1beta1
#   served: true
#   storage: false
# - name: v1
#   served: true
#   storage: true

要查看 etcd 中实际存储的对象版本,可以使用 etcd 客户端直接读取:

# 假设 etcd 端点可用
ETCDCTL_API=3 etcdctl get /registry/example.com/myresources/default/myapp --print-value-only | jq .apiVersion
提示: 在生产环境中,不建议直接操作 etcd。上述命令仅用于调试和验证。更安全的方式是通过 kubectl get myresource.v1beta1.example.com myapp -o json | jq .apiVersion 查看返回的版本,但请注意返回的版本是 served 版本,不是 storage 版本。