坐标: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 Released + deletionTimestamp

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 不会自动删。

kubectl describe pv Finalizers

分层:从 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 也永远进不了回收。

PVC Protection finalizer

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-protectionexternal-attacher/csi-xxx 两个 finalizer 都在,PV 对象被永久卡在删除流程。

PV 删除流程与 Finalizer 链

后端层: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

CSI driver DeleteVolume 日志

路径:🔍 下次 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。

PV 排查命令链

定位:大多数人会怎么查 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

正确顺序:

  1. 先删 PVC(移除 PVC protection finalizer)
  2. 看 PV 的 Reclaim Policy:Retain → 改 Delete 或手动删后端;Delete → 等 external-provisioner 调 DeleteVolume
  3. 查 CSI 日志(external-provisioner 是否成功调用了 DeleteVolume)
  4. 如果 CSI 成功但 finalizer 还在——等 external-attacher 完成 Detach 后清自己的 finalizer
  5. 如果所有组件都无响应——手动 patch PV 移除 finalizers
  6. 如果 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)

PV 修复步骤

场景 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> / NFS ls /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拆代码的曹操」