容器本性易失,所以要用 PersistentVolumeClaim 把真实存储钉到它身上,即使 Pod 被重建也不丢数据。这个拆分是刻意的:PersistentVolume 声明已经存在的容量,PersistentVolumeClaim 请求需要的容量,由控制器把两者配上。有了 StorageClass,声明还能通过 provisioner 自己创建卷,于是没人需要预先造好存储。

真正会咬人的是:声明与卷各自有独立的生命周期,由各自的规则管着。finalizer 决定声明什么时候才允许消失,回收策略决定卷之后会怎样。删除顺序搞错,就会得到一个卡在 Terminating 的声明;策略搞错,可能删掉本想保留的数据,也可能留下一份没人在用却仍在计费的存储。

声明、卷与绑定

绑定是一次「全部满足」而不是「最接近」。一块卷只有在容量不小于请求、访问模式包含所请求的模式、storageClassName 匹配,并且没有被别的声明占用时,才是候选。除了这几项,volumeMode(Filesystem 与 Block)也必须一致;而已经释放却还没清理的卷并不会变回可用的备件,它的 claimRef 仍指向上一个声明,所以新的声明根本看不到它。什么都不满足的声明会停在 Pending,并带一条指明究竟哪一项没满足的事件,这比一句笼统的失败有用得多;常见原因是要的访问模式该类交付不了,以及类名根本不存在。

accessModes 没有默认值,必须选一个,而多数单节点块存储 provisioner 实际能交付的就是 ReadWriteOnce。它同时是单节点的,所以副本分布在不同节点上的 StatefulSet 无法共用一份声明。类支持动态供应时,volumeBindingMode 决定时机:Immediate 在声明一出现就创建卷,可能把受拓扑限制的卷放到没有节点能用的可用区;WaitForFirstConsumer 则让声明一直 Pending,直到出现消费 Pod,再结合调度约束协调绑定或供应,然后才能完成放置;它不是先完成调度再随意创建卷。

删除顺序与 finalizer

顺序是 Pod、声明、卷,保护机制由 finalizer 实现。在 Pod 仍挂载着声明时删除声明,什么都不会被删掉:声明拿到删除时间戳、翻成 Terminating,然后停在那里,因为 kubernetes.io/pvc-protection 仍在它的 finalizer 列表里。只有再没有任何 Pod 引用这份声明时,控制器才会摘掉那个 finalizer,所以那个「永远 Terminating」的 PVC 可能仍被 Pod 引用,但控制器延迟和其他 finalizer 也需要调查,不能只看状态就排除控制面问题。

反方向也有同样的保护。PersistentVolume 带着 kubernetes.io/pv-protection,所以删除一块已绑定到声明的卷会被推迟到它不再被绑定为止。这个顺序只是延迟误删除,并不代表可以撤销已经设置的 deletionTimestamp,也解释了为什么只要声明还在,卷看起来就会无视删除命令。

回收策略:Delete 还是 Retain

声明最终消失后,卷被释放,回收策略接手。Delete 会删掉 PV 对象以及外部基础设施上的存储资产。Retain 保留 PV 对象、把它标成 Released,并且刻意不让它被新的声明使用,因为上一个占用者的数据还在上面——回收于是变成一串手工动作:确认归属与备份,再按存储提供方流程有意回收、复用或删除资产,而不是把删除写成唯一结局。

动态供应的卷会继承其 StorageClass 的策略,而后者默认是 Delete。这个默认是否适合数据库,取决于保留和恢复要求,而且它容易被忽略:PVC 与 Pod 清单里都没有一个字提到策略,决定是几个月前在一个集群级对象里做出的,审阅工作负载的人从来看不到。所以在评审一份有状态工作负载时,回收策略应当和容量、访问模式放在一起看。

坏掉的样子

最经典的是删不掉的声明。有人删了 PVC,看到 Terminating,然后开始等——而某个 Pod 仍然挂着它,通常是没缩容到零的 StatefulSet,或者卡在不可达节点上的 Pod。没有报错,没有超时,而这份 PVC 一直占着背后的存储。正确做法是找到那个 Pod,而不是手工删 finalizer;绕过保护可能让仍被工作负载使用的存储过早进入删除流程,并非每个提供程序都会立即执行相同的分离动作。

第二种是 Retain 留下的孤儿。声明没了,Pod 没了,命名空间可能也没了,一块 Released 的 PV 和它的存储资产仍等待有意回收。保留可能本来就符合要求;如果没有资产盘点告警,就容易被遗漏,任何工作负载的清单里也看不到它。反向的错误同样安静:某个类的默认 Delete 策略从未被覆盖,等到有人想找回数据时才发现回收早已把它删掉了。

快速验证

同时看绑定的两侧,以及是谁占着这份声明:

kubectl get pvc -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,STATUS:.status.phase,VOLUME:.spec.volumeName,CLASS:.spec.storageClassName
kubectl get pv -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,POLICY:.spec.persistentVolumeReclaimPolicy,CLAIM:.spec.claimRef.name
kubectl get pvc data -o jsonpath='{.metadata.finalizers}{"\n"}'
kubectl get pods -A -o json | jq -r '.items[] | select(.spec.volumes[]?.persistentVolumeClaim.claimName=="data") | "\(.metadata.namespace)/\(.metadata.name)"'

这些保留的诊断本轮没有执行。保护标记说明清理待完成,不证明控制器健康,也不证明一定还有 Pod。第四条查找所有命名空间内名为 data 的声明,因此必须先核对目标命名空间,再确认某个 Pod 是否是消费者。Released 加 Retain 需要归属与恢复决策,不代表应自动删除;旧 claimRef 也可能仍然存在。

来源

  • Persistent Volumes,绑定、访问模式、回收策略、Released 状态与 pvc/pv 保护 finalizer,已核验 2026-10-15。
  • Storage Classes,动态供应、reclaimPolicy 与 volumeBindingMode,已核验 2026-10-15。