坐标:kubectl delete pv 没报错,但 PV 还在
上篇讲了双栈 ClusterIP 少加载内核模块导致 IPv6 不通的排查,这篇我们来看存储层一个更隐蔽的"假删"——kubectl delete pv 不报错,但 PV 对象永远删不掉。
Ceph 集群告警存储使用率 85%——"加节点不就完了?"运维准备缩容一批历史 PV 释放空间。kubectl delete pv 敲下去,没报错。三小时后回来一看,PV 对象还在——metadata.deletionTimestamp 已经设了,但 PV 就是没从 etcd 里消失。不是 kubectl 没删——是 Finalizer 和 PVC Protection 把删除流程锁死了。
PV(PersistentVolume,集群级存储资源)受两层保护机制保护:PVC Protection(PVC 对象上的删除拦截 finalizer)和 CSI Finalizer(CSI 存储插件在 PV 上附加的 finalizer)。Finalizer 是 K8s 资源删除前的"必须清空的锁"——不清完 finalizer,资源永远不会被删除。
场景:Ceph RBD 后端 PV 缩容——kubectl delete pv 没报错但 PV 一直卡在删除流程,后端存储没释放 路径:deletionTimestamp 诊断 → PVC Protection → PV Finalizer → PV Controller → external-provisioner 日志 → patch 修复
以下排查基于 K8s v1.28、Ceph-CSI v3.10、Linux kernel 5.15
PV 有 deletionTimestamp,但没有消失
$ kubectl get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS AGE VOLUMEMODE
pvc-ceph 10Gi RWO Retain Released default/ceph-data-pvc csi-ceph 180d Filesystem
PV 的 STATUS 是 Released——PVC 已删,PV 已释放。但 kubectl get pv 看不到的是 deletionTimestamp:
$ kubectl get pv pvc-ceph -o json | jq '.metadata.deletionTimestamp'
"2025-03-25T10:23:45Z"
deletionTimestamp 存在,说明 kubectl delete 已经写入了删除标记。PV 没有消失,是因为 finalizer 清单还没清空。

PV 没有 Events 字段——这是第一个陷阱
Pod 有 Events,PVC 有 Events——但 PV 的 describe 输出没有 Events 段。PV 的删除卡顿不暴露在 Events 里。你必须从 Finalizers、Reclaim Policy 和 CSI driver 日志里找原因。
$ kubectl describe pv pvc-ceph
Name: pvc-ceph
Labels: <none>
Annotations: pv.kubernetes.io/provisioned-by: csi-ceph
Finalizers: [kubernetes.io/pv-protection external-attacher/csi-ceph-cluster]
StorageClass: csi-ceph
Status: Bound
Claim: default/ceph-data-pvc
Reclaim Policy: Retain
Access Modes: RWO
VolumeMode: Filesystem
Capacity: 10Gi
DeletionTimestamp: 25 Mar 2025 10:23:45 +0000
Source:
Type: CSI (Ceph CSI — Container Storage Interface 标准存储插件)
Driver: csi-ceph
VolumeHandle: 0001-0009-ceph-000000000001
ReadOnly: false
VolumeAttributes: diskType=ssd,pool=k8s-pool
关键信息:Finalizers 有两个——kubernetes.io/pv-protection 是 PV Protection Controller 加的,external-attacher/csi-ceph-cluster 是 CSI external-attacher 加的。Reclaim Policy 是 Retain——PV 释放后后端 RBD(RADOS Block Device,Ceph 块设备)image 不会自动删。

分层:从 PVC Protection 到 CSI Finalizer,逐层查谁在持有
PVC 层:kubernetes.io/pvc-protection
PV 被 PVC(PersistentVolumeClaim,存储卷声明)引用时,kube-controller-manager 中的 PVC protection controller 会给 PVC 附加一个 finalizer:
$ kubectl describe pvc ceph-data-pvc -n default | grep -A2 Finalizers
Finalizers: [kubernetes.io/pvc-protection]
只要 PVC 的 kubernetes.io/pvc-protection finalizer 在,PVC 删不掉——PV 也就不可能进入回收流程。这个 finalizer 的作用是:防止直接删 PVC 导致数据丢失。PVC 被 Pod 引用时,protection controller 绝不会移除这个 finalizer。
$ kubectl delete pvc ceph-data-pvc -n default
persistentvolumeclaim "ceph-data-pvc" deleted
如果 Pod 早就不在了,protection controller 会在检测到 PVC 无引用后移除 finalizer,PVC 被删。但如果 PVC 一直删不掉(比如 Pod 还在引用,或者 finalizer 被其他组件卡住),PV 也永远进不了回收。

PV 层:kubernetes.io/pv-protection + CSI finalizer
PV 本身也有保护机制。PV protection controller 给 Bound 状态的 PV 加 kubernetes.io/pv-protection finalizer。当 PV 变 Released 后,PV protection controller 会移除这个 finalizer。
此外,CSI external-attacher 会添加 external-attacher/csi-ceph-cluster finalizer。这个 finalizer 由 external-attacher 管理,它在 Volume 完成 Detach(ControllerUnpublishVolume RPC 返回成功)后移除——和 DeleteVolume 的成功与否无关。
Finalizers: [kubernetes.io/pv-protection external-attacher/csi-ceph-cluster]
两个 finalizer 都在 → PV 永远无法从 etcd 删除。
Controller 层:PV controller 的回收流程 + external-provisioner 的删除
kube-controller-manager 中的 PV controller(PersistentVolumeController)负责执行 Reclaim Policy。而真正调用 CSI 后端删除 Volume 的是 external-provisioner。
kubectl delete pv
→ apiserver 设置 deletionTimestamp
→ PV controller 检测到 PV 变 Released
→ 执行 Reclaim Policy
→ Retain → 不做任何操作,PV 保持 Released
→ Delete → 调 external-provisioner 的 DeleteVolume RPC
→ external-provisioner 调 CSI Controller 删后端 Volume
→ 成功后返回 → PV controller 移除 kubernetes.io/pv-protection
→ external-attacher 在 Detach 完成后移除 CSI finalizer
→ 所有 finalizer 清空 → PV 从 etcd 删除
卡住的关键场景:Reclaim Policy=Retain → PV controller 什么都不做 → kubernetes.io/pv-protection 不会被移除(因为 PV controller 只在 Delete 策略下才调 external-provisioner,provisioner 返回后才移除自己的 finalizer)。Retain 策略下 PV controller 认为自己不需要做任何事——所以 pv-protection 和 external-attacher/csi-xxx 两个 finalizer 都在,PV 对象被永久卡在删除流程。

后端层:CSI external-provisioner 的 DeleteVolume 实现
最终决定 CSI finalizer 能否清掉的,不是 external-attacher——是 external-provisioner 调用的 CSI Controller DeleteVolume RPC 的结果。但即使 DeleteVolume 成功了,external-attacher 的 finalizer 还在——它要等 Volume 完成 Detach 后才移除。
$ kubectl logs -n kube-system -l app=csi-rbdplugin-controller --tail=100 | grep pvc-ceph
I0325 10:23:45.123456 1 controllerserver.go:286] DeleteVolume "pvc-ceph" called
I0325 10:23:45.123789 1 controllerserver.go:312] RBD image "csi-vol-f8a2b1c0d3e4-11ec-9b7d-0242ac110002" found
I0325 10:23:46.456789 1 controllerserver.go:345] RBD image deleted
I0325 10:23:46.457123 1 controllerserver.go:350] DeleteVolume "pvc-ceph" succeeded
如果 DeleteVolume 失败,root cause 可能在 provisioner 这一步就被卡住了:
$ kubectl logs -n kube-system -l app=csi-rbdplugin-controller --tail=20
E0325 10:25:46.789012 1 controllerserver.go:330] DeleteVolume "pvc-ceph" failed: rbd: image has snapshots, cannot remove
E0325 10:25:46.789015 1 controllerserver.go:331] rbd error: exit status 1, stderr: "removing rbd image 'csi-vol-f8a2b1c0d3e4-11ec-9b7d-0242ac110002': image has 2 snapshots"
I0325 10:25:46.789120 1 controllerserver.go:340] retrying DeleteVolume "pvc-ceph" after 30s backoff

路径:🔍 下次 PV 删不掉,先跑这套命令
Step 1: 找所有 PV 及已设 deletionTimestamp 的
$ kubectl get pv -o wide | grep Released
pvc-ceph 10Gi RWO Retain Released default/ceph-data-pvc csi-ceph 180d Filesystem
$ kubectl get pv pvc-ceph -o json | jq '.metadata.deletionTimestamp'
"2025-03-25T10:23:45Z"
异常判断:STATUS=Released + deletionTimestamp 存在 → 删除流程卡住了。
Step 2: 看 PV 的 Finalizers 和 Reclaim Policy
$ kubectl describe pv pvc-ceph | grep -E "Finalizers|Reclaim Policy|Status|DeletionTimestamp"
Finalizers: [kubernetes.io/pv-protection external-attacher/csi-ceph-cluster]
Reclaim Policy: Retain
Status: Bound
DeletionTimestamp: 25 Mar 2025 10:23:45 +0000
异常判断:Finalizers 不为空 → 卡住;Reclaim Policy=Retain → PV controller 不会调 DeleteVolume。
Step 3: 检查绑定的 PVC 状态
$ kubectl get pvc -n default -o wide | grep ceph-data
ceph-data-pvc Bound pvc-ceph 10Gi RWO csi-ceph 180d
$ kubectl describe pvc ceph-data-pvc -n default | grep Finalizers
Finalizers: [kubernetes.io/pvc-protection]
异常判断:PVC 还在且 finalizer 在 → 先删 PVC。
Step 4: 检查 external-provisioner 日志
$ kubectl logs -n kube-system -l app=csi-rbdplugin-controller --tail=50 | grep "pvc-ceph"
预期输出:应该看到 DeleteVolume called + succeeded。如果只有 called 没有 succeeded → CSI 调用超时(常见原因:RBD image 有快照引用 / 存储后端超时 / 权限不足)。
Step 5: 最终手段——手动移除 PV finalizers
$ kubectl patch pv pvc-ceph -p '{"metadata":{"finalizers":null}}' --type=merge
persistentvolume/pvc-ceph patched
$ kubectl get pv pvc-ceph
Error from server (NotFound): persistentvolumes "pvc-ceph" not found
⚠️ 这只删 K8s 侧的 PV 对象,后端 RBD image 不会删。如果 Reclaim Policy=Retain,必须手动去 Ceph 删 image。

定位:大多数人会怎么查 vs 正确做法
❌ 大多数人会怎么查
$ kubectl delete pv pvc-ceph --force --grace-period=0
warning: Immediate deletion does not wait for confirmation...
persistentvolume "pvc-ceph" force deleted
PV 对象从 API 侧消失了。但后端 RBD image 还在 Ceph 里。这是最危险的做法——资源泄漏,账单翻倍。
还有人会改 PV 的 Reclaim Policy:
$ kubectl patch pv pvc-ceph -p '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}' --type=merge
对已进入删除流程的 PV,改了也不一定触发重新执行——PV controller 可能已经完成了 Reclaim Policy 的判断。更关键的是,即使 Delete 策略下 external-provisioner 成功调用了 DeleteVolume,external-attacher/csi-xxx finalizer 仍然存在,PV 对象依然不会被删除。
✅ 正确的排查思路
PV 删不掉的根因是 finalizer 链没走完,不是 kubectl 的问题。每次遇到时,问三个问题来决定从哪层入手:
| 层 | 问题 | 怎么查 |
|---|---|---|
| PVC | PVC 的 finalizer 清了吗? | kubectl describe pvc <name> -n <ns> \| grep Finalizers |
| Controller | PV controller 执行 Reclaim 了吗? | 看 Reclaim Policy:Retain → 什么都没做;Delete → 查 external-provisioner 日志 |
| Backend | 后端存储还在吗? | Ceph: rbd ls -p <pool> / NFS: ls /export |
正确顺序:
- 先删 PVC(移除 PVC protection finalizer)
- 看 PV 的 Reclaim Policy:Retain → 改 Delete 或手动删后端;Delete → 等 external-provisioner 调 DeleteVolume
- 查 CSI 日志(external-provisioner 是否成功调用了 DeleteVolume)
- 如果 CSI 成功但 finalizer 还在——等 external-attacher 完成 Detach 后清自己的 finalizer
- 如果所有组件都无响应——手动 patch PV 移除 finalizers
- 如果 Reclaim Policy=Retain——手动清理后端
标点:修复 + Check-list + 完整命令清单
修复步骤
场景 A:PVC 还在,PV 卡在删除
# 1. 先删 PVC
kubectl delete pvc ceph-data-pvc -n default
# 2. 验证 PVC 已删
kubectl get pvc -n default -o wide | grep ceph-data
# 3. 检查 PV 状态变化
kubectl get pv pvc-ceph -o wide
# STATUS 应从 Bound → Released
场景 B:PVC 已删,PV 还卡住(finalizers 没清)
# 1. 检查 external-provisioner 日志
kubectl logs -n kube-system -l app=csi-rbdplugin-controller --tail=50 | grep pvc-ceph
# 2. 如果 CSI 正常——手动移除 PV finalizers
kubectl patch pv pvc-ceph -p '{"metadata":{"finalizers":null}}' --type=merge
# 3. 验证
kubectl get pv pvc-ceph
# Error from server (NotFound)

场景 C:Reclaim Policy=Retain,后端手动清理
# 1. 查 VolumeHandle
kubectl get pv pvc-ceph -o yaml | grep volumeHandle
# csi-vol-f8a2b1c0d3e4-11ec-9b7d-0242ac110002
# 2. Ceph RBD 手动删除
rbd ls -p k8s-pool | grep f8a2b1c0
rbd rm -p k8s-pool csi-vol-f8a2b1c0d3e4-11ec-9b7d-0242ac110002
Check-list
- [ ]
kubectl get pv -o wide | grep Released— 找疑似卡住的 PV - [ ]
kubectl get pv <pv> -o json | jq '.metadata.deletionTimestamp'— 确认 deletionTimestamp 已设 - [ ]
kubectl describe pv <pv> | grep -E "Finalizers|Reclaim Policy|DeletionTimestamp"— 看 finalizer 和回收策略 - [ ]
kubectl describe pvc <pvc> -n <ns> | grep Finalizers— 检查 PVC protection - [ ]
kubectl delete pvc <pvc> -n <ns>— 先释放 PVC - [ ]
kubectl logs -n kube-system -l app=csi-rbdplugin-controller --tail=50 | grep <pv>— 检查 external-provisioner - [ ]
kubectl patch pv <pv> -p '{"metadata":{"finalizers":null}}' --type=merge— 最终手段 - [ ] 后端存储验证:Ceph
rbd ls -p <pool>/ NFSls /export
金句
"K8s 不是没有告诉你 PV 为什么删不掉——Finalizers 已经列了,只是你没去看"
下篇预告
下篇我们聊 PV 挂载失败——存储插件协议不匹配导致 Attach 卡死。
附:完整命令清单
# PV 删除卡顿排查
kubectl get pv -o wide # 看所有 PV 状态
kubectl get pv <pv> -o json | jq '.metadata.deletionTimestamp' # 确认 deletionTimestamp
kubectl describe pv <pv> # 看 Finalizers / Reclaim Policy
kubectl describe pvc <pvc> -n <ns> # 看 PVC Protection finalizer
# 删除链
kubectl delete pvc <pvc> -n <ns> # 先删 PVC
kubectl patch pv <pv> -p '{"metadata":{"finalizers":null}}' --type=merge # 手动移除 PV finalizers
# CSI driver 日志(Pod 名取决于部署方式)
kubectl logs -n kube-system -l app=csi-rbdplugin-controller --tail=100 | grep <pv>
kubectl logs -n kube-system -l app=csi-rbdplugin --tail=100 # Node 侧日志
# 后端存储手动清理(Ceph RBD 示例)
kubectl get pv <pv> -o yaml | grep volumeHandle # 查 VolumeHandle
rbd ls -p <pool>
rbd rm -p <pool> <image-name>
# PV 故障转移(Reclaim Policy=Retain → 手动重新绑定)
kubectl patch pv <pv> -p '{"spec":{"claimRef": null}}' --type=merge
kubectl patch pv <pv> -p '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}' --type=merge
📺 公众号「Ai拆代码的曹操」 🌟 知识星球「Ai拆代码的曹操」