1. 问题背景与核心症结拆解
1.1 这个报错到底在说什么
先把这个报错原文拆开看:unexpected error getting claim reference: selfLink was empty, can't make reference。关键词就两个——selfLink和empty。这不是 PVC 本身创建失败,而是 PVC 创建之后,某个控制器(通常是persistentvolume-controller或kube-controller-manager里的相关组件)在尝试给这个 PVC 生成一个引用(reference)时,发现它期望的selfLink字段是空的,于是直接抛错。
selfLink是 Kubernetes 早期 API 对象里的一个字段,用来表示该对象自身的 API 路径,比如/api/v1/namespaces/default/persistentvolumeclaims/my-pvc。很多控制器和内部逻辑依赖这个字段来构造 OwnerReference、Event 的 InvolvedObject 等。但从 Kubernetes 1.20 开始,selfLink被逐步废弃,到 1.21 默认不再填充,1.24 之后基本彻底移除。所以这个报错的本质是:你用的某个组件(可能是旧版控制器、旧版 Operator、旧版存储插件)还在依赖已经被移除的selfLink字段,而你的集群版本已经不再提供它了。
1.2 为什么偏偏是 PVC 触发
PVC 这个对象比较特殊,它涉及动态供给(Dynamic Provisioning)流程。当你创建一个 PVC 时,如果指定了 StorageClass,external-provisioner会介入创建 PV,然后persistentvolume-controller会做绑定操作。在这个过程中,控制器需要给 PVC 打上一些引用信息,比如 Event 记录、OwnerReference、以及绑定时的claimRef。这些引用构造逻辑里,旧代码会调用ref.GetReference()之类的函数,而该函数内部依赖selfLink。一旦selfLink为空,引用就构造不出来,于是报错。
换句话说,PVC 只是“恰好”踩中了这个雷。你创建 Deployment、Service 时可能也会触发类似问题,只是 PVC 的控制器链路更长、依赖引用构造的环节更多,所以更容易暴露。
1.3 影响范围判断
这个报错的影响面取决于你的集群版本和组件版本组合。常见的情况是:
- 集群版本在 1.20 到 1.24 之间,
selfLink被禁用或移除,但某些附加组件(如旧版存储插件、旧版监控组件、旧版 Operator)没有同步升级。 - 集群版本在 1.24 以上,
selfLink彻底没了,任何还在用它的组件都会报错。 - 集群版本较老(1.19 以下),
selfLink还在,一般不会出现这个报错,除非有人手动关闭了相关特性门控。
所以这个问题的核心不是“PVC 坏了”,而是“版本不匹配导致的兼容性断裂”。下面我会从排查思路、修复方案、以及长期规避策略三个层面展开。
2. 排查思路与版本兼容性分析
2.1 先确认集群版本和组件版本
第一步永远是看版本。执行以下命令:
kubectl version --short kubectl get nodes -o wide然后重点看kube-controller-manager的版本和启动参数:
kubectl -n kube-system get pod kube-controller-manager-<node-name> -o yaml在输出里找--feature-gates参数,看是否有RemoveSelfLink=true或RemoveSelfLink=false。在 1.20 到 1.23 之间,这个门控默认是true(即移除 selfLink),但可以手动设为false来临时恢复。1.24 之后这个门控被移除,设了也没用。
同时检查你的存储插件版本。如果你用的是某类 CSI 插件,看它的external-provisioner容器镜像版本。旧版external-provisioner(比如 v1.x 早期版本)在构造引用时可能还在用selfLink。升级到 v2.x 以上通常能解决。
2.2 查看控制器日志定位具体组件
报错信息通常会出现在kube-controller-manager的日志里,也可能出现在存储插件的日志里。用以下命令过滤:
kubectl -n kube-system logs kube-controller-manager-<node-name> | grep -i "selfLink" kubectl -n kube-system logs <storage-provisioner-pod> | grep -i "selfLink"如果日志里明确是persistentvolume-controller报的,那问题在核心控制器;如果是某个 CSI sidecar 报的,那问题在存储插件。定位到具体组件后,修复方向就清晰了。
2.3 检查是否有人手动关闭了 selfLink 填充
在 1.20 之前,selfLink是默认填充的。如果你在 1.20 到 1.23 的集群上看到这个报错,有可能是有人手动设置了RemoveSelfLink=true,或者通过kube-apiserver的--feature-gates关闭了。检查kube-apiserver的启动参数:
kubectl -n kube-system get pod kube-apiserver-<node-name> -o yaml | grep -A 20 "command"如果看到RemoveSelfLink=true,而你的组件又依赖selfLink,那就需要临时改回false作为过渡方案。但注意,这只是缓兵之计,长期还是要升级组件。
2.4 版本兼容性对照表
下面这张表可以帮你快速判断自己的环境属于哪种情况:
| 集群版本 | selfLink 状态 | 常见报错组件 | 推荐处理方式 |
|---|---|---|---|
| 1.19 及以下 | 默认填充 | 极少报错 | 无需处理 |
| 1.20 - 1.23 | 默认移除,可门控恢复 | 旧版 CSI、旧版 Operator | 升级组件或临时关闭门控 |
| 1.24 及以上 | 彻底移除 | 任何依赖 selfLink 的组件 | 必须升级组件 |
这张表是我在实际排查中总结出来的,基本覆盖了 90% 以上的场景。如果你不确定自己的集群属于哪一档,直接跑kubectl version看 minor 版本号即可。
3. 修复方案与实操步骤
3.1 方案一:升级相关组件(推荐)
这是最根本的解决办法。具体升级哪些组件,取决于你的日志定位结果。常见需要升级的包括:
- CSI external-provisioner:从 v1.x 升级到 v2.x 或更高。很多存储厂商的 CSI 驱动会打包这个 sidecar,所以你可能需要升级整个 CSI 驱动。
- CSI external-attacher、external-resizer:同理,旧版可能依赖 selfLink。
- 旧版 Operator:比如某些数据库 Operator、监控 Operator,如果它们内部有自定义控制器在构造引用,也需要升级。
- kube-controller-manager:如果是核心控制器报错,那说明你的集群版本和控制器版本不匹配,需要统一升级。
升级 CSI 驱动的操作通常是:
kubectl -n kube-system delete deployment <csi-provisioner-deployment> kubectl apply -f <new-csi-driver-manifest>.yaml但注意,删除前要确认没有正在进行的卷操作,否则可能导致卷挂载异常。我一般会先kubectl get pvc -A看有没有 Pending 的 PVC,再kubectl get volumeattachment看有没有挂载中的卷,确认安全后再操作。
3.2 方案二:临时关闭 RemoveSelfLink 门控(过渡)
如果你暂时没法升级组件,可以在 1.20 到 1.23 的集群上临时恢复selfLink。操作方式是修改kube-apiserver和kube-controller-manager的启动参数,加上:
--feature-gates=RemoveSelfLink=false具体操作取决于你的集群部署方式。如果是 kubeadm 部署的,可以编辑/etc/kubernetes/manifests/kube-apiserver.yaml和/etc/kubernetes/manifests/kube-controller-manager.yaml,在command字段里加上这一行,然后重启 kubelet:
systemctl restart kubelet等 Pod 自动重建后,再创建 PVC 测试。这个方案能快速恢复业务,但有几个注意事项:
注意:这个门控在 1.24 之后被移除,设了也不生效。所以这个方案只适用于 1.20 到 1.23 的集群。
注意:修改 apiserver 参数会导致 apiserver 重启,期间集群 API 不可用,建议在维护窗口操作。
注意:这只是临时方案,长期还是要升级组件,否则后续升级集群时还会遇到同样问题。
3.3 方案三:替换依赖 selfLink 的镜像版本
如果你能定位到具体是哪个 sidecar 报错,比如external-provisioner,可以直接替换它的镜像版本。以某个 CSI 驱动为例,找到它的 Deployment:
kubectl -n kube-system get deployment | grep provisioner然后编辑 Deployment,把image字段改成新版本:
kubectl -n kube-system set image deployment/<provisioner-deployment> <container-name>=<new-image>:<tag>这个操作会触发滚动更新,通常几秒钟就能完成。但要注意,新版本可能对配置参数有要求,比如需要额外的--feature-gates或--extra-create-metadata参数。升级前最好看一下新版本的 release notes。
3.4 方案四:手动创建 PV 并绑定(应急)
如果以上方案都来不及,而业务又急着用存储,可以手动创建一个 PV,然后让 PVC 直接绑定到这个 PV。这样绕过了动态供给流程,也就避开了selfLink相关的引用构造。
操作步骤:
- 先创建一个 PV,指定
claimRef指向你的 PVC:
apiVersion: v1 kind: PersistentVolume metadata: name: manual-pv spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: "" claimRef: namespace: default name: my-pvc hostPath: path: /data/manual-pv- 然后创建 PVC,指定
storageClassName: ""和volumeName: manual-pv:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi storageClassName: "" volumeName: manual-pv这样 PVC 会直接绑定到手动创建的 PV,不经过动态供给控制器,也就不会触发selfLink报错。但这个方案只适合应急,因为手动管理 PV 很麻烦,而且失去了动态供给的灵活性。
4. 常见问题与排查技巧实录
4.1 为什么升级了 CSI 驱动还是报错
这种情况通常是因为集群里还有旧版的external-provisioner在运行。比如你升级了 CSI 驱动,但某个命名空间下还有一个旧的 Deployment 没删干净。用以下命令全局搜索:
kubectl get deployment -A | grep -i provisioner kubectl get pod -A | grep -i provisioner把所有旧版实例清理掉,只保留新版。另外,有些 CSI 驱动会以 DaemonSet 形式部署,也要检查:
kubectl get daemonset -A | grep -i csi4.2 报错消失了但 PVC 还是 Pending
selfLink报错消失只代表引用构造成功了,不代表 PVC 一定能绑定成功。PVC 一直 Pending 的常见原因还有:
- StorageClass 不存在或名字写错。
- 没有可用的 PV,且动态供给失败。
- 节点亲和性不满足,PV 无法调度到目标节点。
- 存储后端容量不足或权限配置错误。
排查命令:
kubectl describe pvc <pvc-name> kubectl get events --sort-by=.metadata.creationTimestamp kubectl -n kube-system logs <csi-provisioner-pod>describe pvc的 Events 部分通常会直接告诉你卡在哪一步。
4.3 门控改了但 apiserver 起不来
修改kube-apiserver参数后,如果 apiserver 起不来,最常见的原因是 YAML 格式错误,比如缩进不对、冒号后面少了空格。用以下命令检查:
kubectl -n kube-system logs kube-apiserver-<node-name>如果 apiserver 完全没起来,可以看 kubelet 日志:
journalctl -u kubelet -fkubelet 会告诉你为什么静态 Pod 启动失败。修复 YAML 后,kubelet 会自动重建 apiserver。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查命令 | 解决方式 |
|---|---|---|---|
| 报错 selfLink empty | 组件依赖已移除字段 | 查 controller-manager 日志 | 升级组件或关门控 |
| PVC 一直 Pending | 供给失败或绑定失败 | kubectl describe pvc | 看 Events 定位 |
| 升级后仍报错 | 旧版实例未清理 | kubectl get pod -A | grep provisioner | 删除旧实例 |
| apiserver 起不来 | YAML 格式错误 | journalctl -u kubelet | 修复 YAML |
| 门控不生效 | 集群版本 1.24+ | kubectl version | 只能升级组件 |
4.5 几个容易踩的坑
第一个坑是只升级了 CSI 驱动,没升级 RBAC。新版 sidecar 可能需要额外的 RBAC 权限,比如leases资源的访问权限。如果 RBAC 没更新,sidecar 会启动失败,但报错信息可能不是 selfLink,而是权限拒绝。所以升级时要把整个 manifest 一起 apply,不要只改镜像。
第二个坑是在 1.24 集群上试图关门控。前面说过,1.24 之后RemoveSelfLink门控被移除,设了也没用。如果你在 1.24 上遇到这个报错,唯一的路就是升级组件,没有别的办法。
第三个坑是忽略了 kube-controller-manager 的版本。有些人只升级了 apiserver,没升级 controller-manager,导致两者版本不一致。controller-manager 如果还是旧版,可能还在用 selfLink 逻辑。所以升级集群时要确保所有控制面组件版本一致。
第四个坑是手动创建 PV 时忘了设 storageClassName 为空字符串。如果你设了storageClassName: "",PVC 和 PV 才能直接绑定。如果设成别的值,或者不设,可能会被动态供给控制器接管,又绕回 selfLink 问题。
5. 长期规避策略与版本管理建议
5.1 建立版本兼容性矩阵
如果你维护的集群不止一个,建议建一个版本兼容性矩阵,记录每个集群的 Kubernetes 版本、CSI 驱动版本、关键 Operator 版本。每次升级前对照矩阵检查,避免出现“集群升级了但组件没升级”的情况。这个矩阵可以放在内部 Wiki 上,也可以用简单的表格维护。
| 集群 | K8s 版本 | CSI 驱动版本 | Operator 版本 | 上次升级时间 |
|---|---|---|---|---|
| 集群 A | 1.23 | v2.3 | v1.5 | 2024-01 |
| 集群 B | 1.25 | v3.0 | v2.0 | 2024-03 |
5.2 升级前做 dry-run 测试
在正式升级集群之前,先在测试环境跑一遍 PVC 创建流程。具体做法是:
kubectl apply -f test-pvc.yaml kubectl get pvc test-pvc -w观察 PVC 是否能正常从 Pending 变成 Bound。如果测试环境没问题,再升级生产环境。这个习惯能帮你提前发现大部分兼容性问题。
5.3 关注废弃 API 的迁移公告
Kubernetes 每个版本都会废弃一些 API 和字段,selfLink只是其中之一。建议订阅官方的 deprecated API 列表,或者用kubectl deprecations之类的插件扫描集群里的废弃资源。提前迁移比事后救火轻松得多。
5.4 组件升级要成套进行
不要单独升级某一个 sidecar,而是把整个 CSI 驱动或 Operator 成套升级。因为 sidecar 之间可能有版本依赖,比如external-provisionerv2.x 可能要求external-attacherv3.x 以上。成套升级能避免版本错配导致的奇怪报错。
5.5 保留回滚方案
每次升级前,备份关键 manifest:
kubectl -n kube-system get deployment <csi-deployment> -o yaml > backup-csi-deployment.yaml如果升级后出现问题,可以快速回滚:
kubectl apply -f backup-csi-deployment.yaml这个操作我做过好几次,尤其是在升级 CSI 驱动时,回滚能帮你快速恢复业务,然后再慢慢排查问题。
6. 个人实操体会与补充建议
我在实际处理这个报错时,最深的体会是:不要只盯着 PVC 看,要往上看控制器和组件版本。很多人一看到 PVC 报错就以为是存储问题,去查存储后端、查网络、查权限,结果绕了一大圈才发现是版本兼容性问题。所以第一步永远是看日志、看版本、看组件。
另外,selfLink这个问题在 1.20 到 1.24 的过渡期特别常见,因为很多组件的升级节奏没跟上 Kubernetes 的废弃节奏。如果你现在还在用 1.20 到 1.23 的集群,建议尽快规划升级到 1.24 以上,同时把所有依赖 selfLink 的组件升级到兼容版本。拖得越久,后续升级越痛苦。
最后分享一个小技巧:如果你不确定某个组件是否依赖 selfLink,可以在测试环境里手动把RemoveSelfLink设为true,然后创建 PVC 观察是否报错。如果报错,说明该组件需要升级;如果不报错,说明它已经兼容了。这个测试方法比翻源码快得多,适合快速筛查。
还有一个经验是,处理这类问题时尽量用kubectl describe和kubectl logs组合排查,不要一上来就改配置。先把现象和日志收集全,再决定改什么。我见过太多人一看到报错就乱改参数,结果问题没解决,反而引入了新的问题。稳一点,慢一点,反而更快。