1. 存储版本漂移概述
1.1 定义:什么是存储版本漂移
在 Kubernetes 集群中,存储版本漂移(Storage Version Drift) 是指同一 CustomResourceDefinition(CRD)或内置资源(如 Deployment、Pod)在 etcd 中持久化的序列化版本,与集群中当前运行的 API Server 所支持的存储版本不一致的现象。这种不一致会导致 API Server 在读取旧版本对象时产生解码失败、字段丢失、控制器异常等连锁故障。
--storage-versions 配置或 CRD 的 spec.versions[].storage 字段指向的版本不同时,即发生了存储版本漂移。
1.2 漂移产生的三大原因
存储版本漂移并非随机发生,其根因集中在以下三类典型场景中。下表从触发时机、影响范围和典型表现三个维度进行对比:
| 原因类别 | 触发时机 | 影响范围 | 典型表现 |
|---|---|---|---|
| 升级中断 | 集群升级过程中 API Server 重启或版本切换 | 全局 / 特定 CRD | 部分对象无法通过 kubectl get 读取,etcd 中残留旧版本数据 |
| 回滚不一致 | 集群回滚后 API Server 版本低于 etcd 中对象的存储版本 | 全局 | API Server 启动失败或持续报 unable to decode 错误 |
| API 版本过期 | CRD 或内置资源废弃旧版本后未执行存储迁移 | 特定 CRD | 旧版本对象在 etcd 中成为“孤儿”,无法被新版本控制器处理 |
1.3 原因一:升级中断
Kubernetes 集群升级(尤其是跨大版本升级)时,kube-apiserver 会经历滚动重启。如果升级流程被意外中断(如节点宕机、网络分区、超时强制终止),可能出现以下状态:
- 部分 API Server 实例已升级,开始使用新存储版本写入 etcd;
- 部分 API Server 实例仍为旧版本,继续以旧版本格式读取/写入;
- etcd 中同一资源的不同对象可能分别以新旧两种版本存储,形成混合存储状态。
当集群恢复后,新版本的 API Server 尝试读取旧版本对象时,若转换器(conversion)缺失或转换失败,就会触发 InternalError 或 DecodeError,导致该对象不可用。
1.4 原因二:回滚不一致
集群回滚是比升级中断更危险的场景。假设集群从 v1.25 升级到 v1.26 后发现问题,决定回滚到 v1.25:
- v1.26 的 API Server 在运行期间可能已将某些资源(如
HorizontalPodAutoscaler)的存储版本从autoscaling/v2beta2升级为autoscaling/v2; - 回滚后,v1.25 的 API Server 不认识
autoscaling/v2的序列化格式; - etcd 中所有以
autoscaling/v2存储的对象在读取时直接报错,导致 HPA 控制器完全失效。
1.5 原因三:API 版本过期
Kubernetes 社区和 CRD 开发者会定期废弃(deprecate)并最终移除(remove)旧 API 版本。例如:
- 内置资源
extensions/v1beta1的 Ingress 在 v1.22 中被彻底移除; - CRD 的
spec.versions中标记served: false的版本不再被 API Server 服务。
如果 CRD 的 spec.versions[].storage 指向一个已被废弃的版本,或者 etcd 中仍残留旧版本对象但该版本已从 CRD 定义中删除,API Server 将无法解码这些对象。此时必须通过 kubectl convert 或自定义迁移工具将对象显式转换为当前存储版本。
1.6 存储版本漂移全景图
下图从集群生命周期视角展示了三种漂移原因的发生位置与传播路径:
1.7 漂移的检测与初步判断
在实际运维中,可以通过以下命令快速判断集群是否存在存储版本漂移:
# 检查 API Server 日志中的解码错误
kubectl logs -n kube-system kube-apiserver-<node> | grep -i "unable to decode"
# 检查 CRD 的存储版本配置
kubectl get crd <crd-name> -o jsonpath='{.spec.versions[?(@.storage==true)].name}'
# 直接读取 etcd 中对象的原始存储版本(需 etcdctl)
ETCDCTL_API=3 etcdctl get /registry/<resource>/<namespace>/<name> --prefix --print-value-only | \
jq '.apiVersion'