Kubernetes 基础体系 · 第 36/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes PV 与 PVC:绑定、访问模式、回收策略和生命周期

在 Kubernetes 中,容器的生命周期通常很短,但有状态应用的数据不能随着 Pod 删除而消失。PersistentVolume(PV)和 PersistentVolumeClaim(PVC)将“存储资源”与“应用对存储的需求”分离:

  • PV 是集群中的存储资源对象,描述一块已经存在或将要创建的持久化存储。
  • PVC 是用户对存储的请求,描述需要多大容量、什么访问模式、什么卷格式等。
  • StorageClass 描述动态创建 PV 的方式,例如由哪个 CSI Provisioner 创建云盘、网络文件系统或本地卷。

PV/PVC 不是数据本身,而是 Kubernetes 对外部存储以及使用关系的声明。真正的数据位于云盘、文件系统、SAN、NAS、本地磁盘等后端。


一、先区分四个对象:Pod、PVC、PV 和后端存储

一个典型关系如下:

flowchart LR
    Pod["Pod<br/>挂载 volume"] --> PVC["PVC<br/>应用的存储请求"]
    PVC --> PV["PV<br/>集群存储资源"]
    PV --> Backend["后端存储<br/>云盘/NFS/本地盘等"]
    SC["StorageClass<br/>动态供给规则"] --> Provisioner["CSI Provisioner"]
    Provisioner --> Backend
    Provisioner -.创建并登记.-> PV

Pod 通常不直接引用 PV,而是引用 PVC:

volumes:
  - name: data
    persistentVolumeClaim:
      claimName: app-data

这样做有两个重要后果:

  1. 应用只关心“我要一个名为 app-data 的存储请求”,不需要知道具体使用哪块云盘。
  2. 管理员或平台负责提供满足 PVC 的 PV,或者由 StorageClass 动态创建 PV。

PV 与 PVC 的关系是一对一绑定

  • 一个 PV 最多绑定一个 PVC;
  • 一个 PVC 最终绑定一个 PV;
  • 一个 PVC 不能自动拆分到多个 PV;
  • 一个 PV 也不能被多个 PVC 共享,即使后端本身支持共享。

如果多个 Pod 需要共享同一个卷,它们应引用同一个 PVC,并且底层卷必须支持相应的访问模式。


二、PV 和 PVC 的核心字段

1. PV 的主要字段

一个 PV 通常包含:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: ""
  hostPath:
    path: /srv/k8s/example

这些字段分别表示:

  • capacity.storage:PV 提供的容量。
  • volumeMode:卷以文件系统还是原始块设备形式提供。
  • accessModes:卷允许的访问模式。
  • persistentVolumeReclaimPolicy:PVC 删除后 PV 如何处理。
  • storageClassName:PV 所属的 StorageClass。
  • 卷插件字段:例如 CSI、NFS、local、云厂商卷等。

hostPath 只是把节点上的目录暴露给 Pod,适合实验环境,不适合把普通节点目录当作生产持久化存储。节点损坏、Pod 调度到其他节点、目录权限和备份问题都不会由 Kubernetes 自动解决。

2. PVC 的主要字段

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: example-pvc
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 5Gi
  storageClassName: ""

PVC 的字段表达的是需求:

  • resources.requests.storage:请求的最小容量。
  • accessModes:应用需要的访问模式。
  • volumeMode:需要文件系统还是块设备。
  • storageClassName:希望使用的存储类别。
  • selector:可选,用标签进一步选择预先创建的 PV。

PVC 请求 5Gi 不表示一定创建一个精确为 5Gi 的卷。静态绑定时,只要某个候选 PV 的容量至少为 5Gi,就可能满足该请求;动态供给时,Provisioner 通常会按照请求创建容量为 5Gi 或更大、但具体行为由实现决定的后端卷。


三、PVC 如何绑定到 PV

1. 静态供给和动态供给

PV 有两种来源。

静态供给由管理员预先创建 PV:

管理员创建 PV
    ↓
用户创建 PVC
    ↓
Kubernetes 查找匹配的 PV
    ↓
PVC 与 PV 绑定

动态供给由用户创建 PVC,Kubernetes 根据 StorageClass 调用 CSI Provisioner 创建后端卷,并自动创建 PV:

用户创建 PVC
    ↓
StorageClass 选择 Provisioner
    ↓
Provisioner 创建后端卷
    ↓
Provisioner 创建对应 PV
    ↓
PVC 与 PV 绑定

动态供给依赖 CSI 驱动和 StorageClass。Kubernetes 本身不会凭空创建云盘或网络存储。

2. 绑定匹配条件

设 PVC 的请求为:

  • 容量需求为 CrC_r
  • 访问模式集合为 ArA_r
  • 卷模式为 VrV_r
  • StorageClass 为 SrS_r
  • 可选标签选择器为 LrL_r

某个 PV 能否成为候选者,至少需要满足:

CpvCrC_{pv} \geq C_r

并且:

ArApvA_r \subseteq A_{pv}

Vpv=VrV_{pv} = V_r

Spv=SrS_{pv} = S_r

如果 PVC 指定了 selector,还需要:

labels(PV)Lrlabels(PV) \models L_r

其中:

  • CpvC_{pv} 是 PV 容量;
  • ApvA_{pv} 是 PV 声明支持的访问模式;
  • VpvV_{pv} 是 PV 的 volumeMode
  • SpvS_{pv} 是 PV 的 StorageClass;
  • labels(PV) \models L_r 表示 PV 标签满足 PVC 的选择器。

例如:

条件 PV PVC 是否满足
容量 10Gi 5Gi
访问模式 RWO RWO
volumeMode Filesystem Filesystem
StorageClass "" ""
selector

因此它可以绑定。

但以下情况会导致绑定失败:

  • PV 只有 ReadWriteOnce,PVC 请求 ReadWriteMany
  • PV 是 Block,PVC 请求 Filesystem
  • PV 属于 fast,PVC 请求 slow
  • PV 容量小于 PVC 请求;
  • PVC selector 不匹配任何 PV;
  • PV 已经绑定给另一个 PVC。

容量不是唯一条件。一个 10Gi 的 PV 不一定能绑定 5Gi 的 PVC;访问模式、卷模式和 StorageClass 同样必须匹配。

3. 一个可运行的静态绑定示例

下面的示例用于单节点实验集群。它使用 hostPath,要求 Kubernetes 节点上存在 /srv/k8s/pv-demo

sudo mkdir -p /srv/k8s/pv-demo
sudo chmod 777 /srv/k8s/pv-demo

创建 PV 和 PVC:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-demo-10gi
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: ""
  hostPath:
    path: /srv/k8s/pv-demo
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-demo
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 5Gi
  storageClassName: ""

应用并观察:

kubectl apply -f pv-pvc.yaml

kubectl get pv pv-demo-10gi
kubectl get pvc pvc-demo

预期状态类似:

NAME           CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM
pv-demo-10gi   10Gi       RWO            Retain            Bound    default/pvc-demo

这里 storageClassName: "" 很重要,它表示“不使用 StorageClass”。如果省略该字段,集群存在默认 StorageClass 时,PVC 可能被默认 StorageClass 处理,而不会绑定这个没有 StorageClass 的静态 PV。

再创建一个挂载 PVC 的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: pvc-demo-pod
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["/bin/sh", "-c"]
      args:
        - |
          echo "created by pod" >> /data/message.txt
          cat /data/message.txt
          sleep 3600
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: pvc-demo

验证:

kubectl apply -f pod.yaml
kubectl logs pvc-demo-pod
kubectl exec pvc-demo-pod -- cat /data/message.txt

Pod 删除后重新创建,只要 PVC 和 PV 仍然存在,message.txt 仍应保留。这个结果来自 PV/PVC 的生命周期独立于 Pod,而不是来自 Pod 本身。


四、绑定状态与故障路径

1. PV 的阶段

PV 常见的 status.phase 包括:

  • Available:PV 尚未绑定 PVC。
  • Bound:已经绑定 PVC。
  • Released:原 PVC 已删除,但 PV 仍引用原来的 claim,尚未重新变为可用。
  • Failed:回收或处理过程失败。

PVC 常见的阶段包括:

  • Pending:尚未绑定 PV,或者动态供给尚未完成。
  • Bound:已经绑定 PV。
  • Lost:关联的 PV 不再可用或关系异常。

可以使用以下命令查看事件和绑定原因:

kubectl describe pvc pvc-demo
kubectl describe pv pv-demo-10gi
kubectl get events --sort-by=.lastTimestamp

当 PVC 长时间处于 Pending 时,重点检查:

  1. storageClassName 是否正确;
  2. 是否有容量足够的 PV;
  3. 访问模式和 volumeMode 是否匹配;
  4. selector 是否过于严格;
  5. StorageClass 是否存在默认值;
  6. CSI Provisioner 是否运行;
  7. 是否存在拓扑约束或可用区约束;
  8. 动态创建的后端卷是否因配额、权限或 API 错误创建失败。

kubectl describe pvc 的 Events 往往比 kubectl get pvc 更能说明原因。

2. 预绑定

管理员可以在 PV 上预先指定 PVC:

spec:
  claimRef:
    namespace: default
    name: pvc-demo

也可以在 PVC 中指定目标 PV:

spec:
  volumeName: pv-demo-10gi

预绑定不是绕过所有约束。即使指定了名称,PV 与 PVC 的容量、访问模式、卷模式等基本条件仍应兼容。预绑定适合迁移、恢复或把特定卷交给特定应用,但会降低普通调度的灵活性。

3. selector 的效果

PVC 可以使用 selector:

spec:
  selector:
    matchLabels:
      tenant: team-a

这表示只接受带有 tenant=team-a 标签的 PV。selector 会限制候选范围,并且通常会阻止动态供给,因为动态供给无法根据任意标签 selector 推断应该创建什么资源。需要动态供给时,不应无意中加入与 Provisioner 不兼容的 selector。


五、访问模式:它描述“怎么访问”,不是应用锁

访问模式表示卷允许怎样被一个或多个节点、Pod 挂载。常见模式如下:

模式 含义
ReadWriteOnce(RWO) 一个节点以读写方式挂载
ReadOnlyMany(ROX) 多个节点以只读方式挂载
ReadWriteMany(RWX) 多个节点以读写方式挂载
ReadWriteOncePod(RWOP) 只能被单个 Pod 以读写方式使用

1. RWO 不等于单个 Pod

ReadWriteOnce 的“Once”指一个节点,不是一个 Pod。

同一个节点上的多个 Pod 可能同时挂载一个 RWO 卷。例如两个 Pod 都在节点 node-a 上,它们可能都能读写同一个卷。调度器和卷插件不应被理解为“自动为应用提供进程级互斥”。

因此:

  • RWO 可以约束卷不能被多个节点同时读写;
  • RWO 不能保证只有一个 Pod 使用数据;
  • RWO 不能替代数据库锁、分布式锁或应用级 leader election。

2. RWOP 的语义更严格

ReadWriteOncePod 表示整个集群中只能有一个 Pod 使用该卷进行读写。它依赖支持该能力的 CSI 驱动和相关组件版本。它适合需要单 Pod 独占语义的场景,但不是所有后端或旧 CSI 驱动都支持。

如果使用 RWOP,应该确认:

kubectl get csidrivers
kubectl describe csidriver <driver-name>

实际兼容性还取决于 CSI Driver、external-provisioner、external-attacher 等组件版本。不能仅因为 API 接受 ReadWriteOncePod,就推断底层存储一定提供了完整的独占保证。

3. RWX 不是“任何磁盘都能共享”

普通云块设备通常不能直接以 RWX 方式安全地挂载到多个节点。RWX 通常需要支持并发网络文件协议或共享文件系统的后端,例如某些 NFS、CephFS 或云文件服务。

“PVC 写了 ReadWriteMany”只是需求声明。最终能否满足,取决于:

  • StorageClass 使用的 Provisioner;
  • CSI 驱动支持的访问模式;
  • 后端存储的数据一致性和锁语义;
  • 文件系统或数据库是否适合并发访问。

即使卷支持 RWX,也不意味着任意应用都能安全地并发写入同一个目录。数据库、日志文件和状态文件仍需要应用级并发设计。

4. 访问模式不是权限模型

访问模式不负责:

  • Linux 用户和组权限;
  • 文件所有者;
  • Unix mode;
  • SELinux 标签;
  • 应用身份认证;
  • 读写数据的业务授权。

这些通常由 Pod 的 securityContext、文件系统权限、CSI 挂载参数和应用自身共同决定。


六、volumeMode:文件系统卷与块卷

PV/PVC 可以请求两种卷模式:

1. Filesystem

这是默认模式。Kubernetes 或 CSI 驱动把底层设备格式化并挂载为文件系统,Pod 中看到的是目录:

volumeMounts:
  - name: data
    mountPath: /var/lib/app

应用可以使用普通文件 API 创建文件和目录。

2. Block

块模式不会把卷挂载成目录,而是以设备形式暴露:

volumeDevices:
  - name: data
    devicePath: /dev/xvda

对应完整示例:

apiVersion: v1
kind: Pod
metadata:
  name: block-consumer
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["/bin/sh", "-c"]
      args: ["ls -l /dev/xvda; sleep 3600"]
      volumeDevices:
        - name: data
          devicePath: /dev/xvda
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: block-pvc

Filesystem PVC 不能绑定 Block PV,反之亦然。块设备通常由数据库、存储引擎或应用自行初始化;如果直接当普通文件目录使用,会得到错误的设备访问方式。


七、回收策略:PVC 删除后数据怎么办

persistentVolumeReclaimPolicy 描述 PVC 删除后,PV 和后端存储的处理方式。

1. Retain

Retain 保留 PV 和后端数据。PVC 删除后,PV 通常进入 Released,不会自动交给其他 PVC 使用。

它适合:

  • 生产数据;
  • 需要人工确认的删除操作;
  • 需要进行数据恢复或审计的卷。

但它也意味着删除 PVC 后,卷可能长期占用云资源和存储配额。

典型恢复流程是:

  1. 确认原 PVC 确实不再使用;
  2. 备份或验证后端数据;
  3. 确认新 PVC 的命名空间、应用和数据归属;
  4. 清理 PV 上对旧 PVC 的引用;
  5. 让 PV 重新进入可绑定状态,或创建新的 PV 指向恢复后的数据;
  6. 创建新 PVC 并验证挂载内容。

手工清理 claimRef 之前必须确认没有旧 Pod 仍在使用该卷。错误地清理引用,可能造成两个应用同时访问同一份数据。

2. Delete

Delete 表示删除 PVC 后,PV 对象以及动态供给的底层资源通常会被删除。具体删除动作由 Kubernetes 控制器和 CSI 驱动完成。

它适合可重建的临时环境,但生产环境必须确认:

  • 删除卷是否真的符合数据保留要求;
  • CSI 驱动是否正确实现删除;
  • 云厂商是否存在快照、回收站或延迟删除;
  • 是否有防止误删的权限和审批机制。

动态创建的 PV 往往会继承 StorageClass 的回收策略。不要只根据某个集群的默认值推断所有集群行为,应直接检查:

kubectl get storageclass
kubectl get pv <pv-name> -o jsonpath='{.spec.persistentVolumeReclaimPolicy}{"\n"}'

3. Recycle

Recycle 曾表示通过删除文件清空卷后重新使用,但已经弃用,并不应作为当前方案设计。现代集群应使用 RetainDelete,或者由存储系统提供快照、克隆和安全清理能力。

4. 回收策略不等于备份策略

Retain 不是备份:

  • 它不能防止磁盘损坏;
  • 不能提供时间点恢复;
  • 不能替代跨可用区或跨区域复制;
  • 不能自动验证数据是否可恢复。

Delete 也不一定意味着底层数据立即不可恢复,实际行为由后端和 CSI 驱动决定。数据保护应通过快照、复制、备份和恢复演练实现。


八、完整生命周期:从创建到删除

阶段 1:创建 PVC

用户提交 PVC 后:

kubectl get pvc

可能看到:

NAME        STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS
pvc-demo    Pending

Pending 表示尚未完成绑定,不代表 Pod 一定已经失败。

阶段 2:静态匹配或动态供给

控制器会尝试:

  • 查找匹配的 Available PV;
  • 或根据 StorageClass 请求动态创建 PV。

在动态供给中,CSI Provisioner 可能先创建云盘,再创建 PV;如果云厂商 API 超时、权限不足或配额耗尽,PVC 会继续处于 Pending,事件中通常会记录失败原因。

阶段 3:绑定

绑定完成后:

PVC: Pending → Bound
PV:  Available → Bound

PV 的 claimRef 会记录绑定关系,防止其他 PVC 再次选择同一个 PV。

阶段 4:Pod 调度和挂载

PVC Bound 不代表 Pod 已经成功运行。Pod 仍需要经过:

  1. 调度到满足资源和拓扑约束的节点;
  2. 节点上的 kubelet 与 CSI Node Plugin 协作;
  3. Attach 阶段将卷连接到节点,适用于需要 attach 的后端;
  4. Mount 阶段把卷挂载到 Pod 的路径;
  5. 容器启动并访问数据。

因此应分别检查:

kubectl get pvc
kubectl get pod
kubectl describe pod <pod-name>
kubectl get volumeattachments

PVC Bound 但 Pod Pending,可能是调度问题;Pod 已调度但 ContainerCreating,可能是 attach、mount、权限或文件系统问题。

阶段 5:Pod 删除

删除 Pod 通常不会删除 PVC,也不会删除 PV。新 Pod 只要引用同一个 PVC,就可以重新挂载该卷。

这正是 StatefulSet 使用 PVC 保存状态的基础,但它并不自动解决数据库复制、事务一致性或故障恢复问题。

阶段 6:PVC 删除

删除 PVC 后,PV 根据回收策略处理:

PVC 删除
    ├── Retain → PV 通常进入 Released,数据保留
    └── Delete → 删除 PV,并请求删除动态后端资源

如果 CSI 驱动或后端删除失败,PV 可能因为 finalizer 继续存在。此时不应直接强制删除对象绕过 finalizer,因为这可能留下无法管理的云盘、孤儿资源或数据泄漏风险。


九、StorageClass 与动态供给:为什么 PVC 可能不需要预先创建 PV

一个 StorageClass 示例:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: example-csi
provisioner: example.csi.example.com
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

关键字段含义:

  • provisioner:负责创建卷的 CSI 驱动名称。
  • reclaimPolicy:动态 PV 的默认回收策略。
  • volumeBindingMode
    • Immediate:PVC 创建后立即供给和绑定;
    • WaitForFirstConsumer:等到有 Pod 使用 PVC 后,再结合 Pod 调度结果决定卷位置。
  • allowVolumeExpansion:是否允许通过编辑 PVC 请求更大容量。

使用该 StorageClass 的 PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dynamic-pvc
spec:
  storageClassName: example-csi
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi

如果 volumeBindingModeImmediate,动态卷可能在 Pod 尚未调度前就被创建。如果后端卷带有可用区限制,而 Pod 最终被调度到另一个不可访问该卷的区域,就可能形成调度或挂载问题。

WaitForFirstConsumer 将绑定延迟到 Pod 出现后,使调度器可以同时考虑:

  • 节点可用区;
  • PV 的 node affinity;
  • Pod 的 nodeSelector;
  • taint 和 toleration;
  • 资源与其他调度约束。

这不是“延迟绑定一定更好”,而是它更适合具有拓扑限制的存储。具体效果取决于 CSI 驱动是否正确提供拓扑信息。

默认 StorageClass 的隐含行为

如果 PVC 没有设置 storageClassName,启用了默认 StorageClass 的集群可能自动填入默认类。若要明确表示不使用 StorageClass,应写:

storageClassName: ""

这是静态绑定示例中常被忽略的差异,也是“同一份 YAML 在不同集群表现不同”的常见原因。


十、扩容:修改 PVC 不等于后端一定能扩容

如果 StorageClass 设置:

allowVolumeExpansion: true

通常可以增加 PVC 的容量请求:

kubectl patch pvc dynamic-pvc \
  --type merge \
  -p '{"spec":{"resources":{"requests":{"storage":"30Gi"}}}}'

查看结果:

kubectl get pvc dynamic-pvc
kubectl describe pvc dynamic-pvc

扩容通常包含两个层次:

  1. 后端卷扩容:CSI Controller 调用存储系统,把云盘或后端卷变大;
  2. 节点文件系统扩容:CSI Node Plugin 或 kubelet 让挂载的文件系统识别新空间。

因此 spec.resources.requests.storage 变大后,status.capacity 可能不会立即变化。最终是否需要重新挂载、重启 Pod 或满足特定文件系统条件,由 CSI 驱动和文件系统实现决定。

扩容通常只支持增加,不支持缩小。直接把 PVC 请求从 30Gi 改成 10Gi 不能安全地缩小已创建的块设备;需要通过备份、创建更小的新卷、迁移数据等方式完成。

扩容失败时,检查:

kubectl describe pvc dynamic-pvc
kubectl get events --sort-by=.lastTimestamp
kubectl logs -n <csi-namespace> <csi-controller-pod>

不要只修改 PV 的 spec.capacity 来“伪造扩容结果”。那可能改变 Kubernetes 的记录,却没有改变后端容量,导致数据写满或挂载状态不一致。


十一、常见误解与反例

误解一:删除 Pod 会删除数据

通常不会。Pod 删除只会删除容器和 Pod 对象;PVC、PV 和后端卷是独立资源。

反例是某些应用把数据写在容器可写层或 emptyDir 中。这样的数据没有经过 PVC,Pod 删除后自然不会保留。

误解二:PVC Bound 就代表应用可写

不代表。PVC 绑定只说明资源关系满足。以下问题仍可能存在:

  • 节点无法访问后端;
  • CSI 驱动挂载失败;
  • 文件系统只读;
  • 容器用户没有目录权限;
  • Pod 被调度到不支持该卷的节点;
  • 后端发生故障。

必须同时检查 Pod、事件、CSI 组件和节点挂载状态。

误解三:RWO 可以防止两个 Pod 同时写入

RWO 只限制节点范围。两个 Pod 如果位于同一节点,可能都能访问卷。需要单 Pod 独占时,应评估 RWOP、StatefulSet 的副本设计以及应用级锁。

误解四:把 PVC 请求写成 RWX 就能获得共享文件系统

访问模式不能改变后端能力。一个只支持块存储的 Provisioner 不会因为 PVC 写了 RWX 就突然获得多节点共享写能力。

误解五:PV 的 Retain 可以防止所有误删

Retain 主要影响删除 PVC 后的回收动作。管理员仍可能直接删除 PV,后端管理员也可能删除云盘或文件系统。它不是权限控制,也不是备份。

误解六:同一个 PVC 可以让多个副本安全共享数据库目录

PVC 能否被多个 Pod 挂载,与应用是否支持多个进程并发操作同一份数据,是两个问题。数据库通常要求明确的单实例、复制协议或分布式存储设计;不能仅凭 RWX 解决数据库一致性。


十二、生产诊断的最短路径

遇到“应用无法使用持久化存储”时,可以按资源边界逐层定位:

# 1. PVC 是否绑定
kubectl get pvc -A
kubectl describe pvc <pvc-name> -n <namespace>

# 2. PV 是否存在、属于哪个 PVC
kubectl get pv
kubectl describe pv <pv-name>

# 3. Pod 是否被调度、是否卡在挂载
kubectl get pod <pod-name> -n <namespace> -o wide
kubectl describe pod <pod-name> -n <namespace>

# 4. StorageClass 和 CSI 驱动是否存在
kubectl get storageclass
kubectl get csidrivers

# 5. 查看最近事件
kubectl get events -A --sort-by=.lastTimestamp

可以把故障分成四类:

  • 绑定故障:容量、访问模式、StorageClass、selector 不匹配;
  • 供给故障:Provisioner 权限、配额、后端 API 或拓扑失败;
  • 调度故障:节点亲和性、可用区、资源和污点不满足;
  • 挂载故障:CSI Node Plugin、设备连接、文件系统、权限或网络失败。

这种分类比反复删除并重建 PVC 更安全。删除 PVC 可能触发 Delete 回收策略,导致本来可以恢复的数据和后端卷被清理。


十三、与 StatefulSet 的边界

StatefulSet 常为每个副本生成独立 PVC,例如:

volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 10Gi

副本 db-0db-1 通常分别拥有:

data-db-0
data-db-1

这样可以获得稳定的存储归属,但需要明确:

  • PVC 只保存卷与请求关系;
  • StatefulSet 提供稳定 Pod 名称、网络身份和副本管理;
  • StatefulSet 不自动提供数据库复制;
  • 删除或缩容 StatefulSet 通常不会自动删除其 PVC,数据保留行为还取决于具体控制器字段和集群版本;
  • 数据库故障转移、日志复制、脑裂防护和一致性仍由数据库系统负责。

PV/PVC 解决的是“这份数据存在哪里以及如何挂载”,不是“多个副本如何达成一致”。


十四、设计时应明确的边界

一个可靠的 PV/PVC 设计至少要明确以下事实:

  1. 容量:PVC 请求的是容量下限,实际可用空间还受到文件系统、预留空间和后端限制影响。
  2. 访问模式:是节点级挂载约束,不是应用锁,也不是权限控制。
  3. 卷模式:文件系统和块设备不能混用。
  4. 供给方式:静态 PV 需要管理员管理;动态供给依赖 StorageClass 和 CSI。
  5. 拓扑:云盘常与可用区、节点或区域绑定,WaitForFirstConsumer 可能影响正确调度。
  6. 回收策略RetainDelete 都有成本,前者可能遗留资源,后者可能造成数据删除。
  7. 扩容能力:由 StorageClass、CSI 驱动、后端和文件系统共同决定。
  8. 备份责任:PV/PVC 生命周期不是备份系统,数据保护需要独立设计。
  9. 应用一致性:卷挂载成功不代表应用具备并发、恢复和复制能力。

PV 是集群层面的存储资源,PVC 是应用层面的存储契约,StorageClass 是动态供给规则,CSI 是连接 Kubernetes 与真实存储后端的实现。理解这四者的边界,才能正确解释“为什么 PVC 一直 Pending”“为什么 Bound 后 Pod 仍不能启动”“为什么删除 PVC 后数据还在”以及“为什么设置 RWX 后仍无法共享写入”。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。