PV / PVC / StorageClass / CSI 深入
这一篇是存储专题的核心。K8s 持久化存储的所有抽象(PV / PVC / StorageClass)+ 现代实现(CSI 框架)+ 几乎所有生产坑(Pending / 扩容 / reclaim / 权限 / cross-AZ)。
这篇要回答什么
- PV / PVC / StorageClass 三者怎么协作?
- 静态 PV vs 动态 PV 区别?
- accessMode 真的能强制 enforce 吗?
- reclaim policy 三种各意味着什么?
- CSI driver 在 K8s 里怎么运转(架构)?
- 扩容 PV / 缩容 PV 各能做吗?
- 多个 pod 共享 PVC 行不行?
- K8s 跨 AZ 调度怎么和云盘 AZ 绑定?
- PVC 卡 Pending 的 10 种原因
- PV 删了数据丢吗?三种 reclaim policy 的真相
- StatefulSet 的 volumeClaimTemplate 跟普通 PVC 区别?
- pod 删除后 PVC 卡 Terminating 怎么救?
1. PV / PVC / StorageClass 三件套
graph TB
subgraph 应用[应用层]
Pod["Pod<br>volumes:<br> persistentVolumeClaim:<br> claimName: my-pvc"]
end
subgraph 用户[用户层]
PVC["PersistentVolumeClaim<br>(申请: 我要 10Gi RWO)"]
end
subgraph 管理员[集群管理员层]
PV["PersistentVolume<br>(实际存储资源)"]
SC["StorageClass<br>(自动创建 PV 的模板)"]
end
subgraph 底层[实际存储]
Disk["云盘 / NFS / Ceph / Local"]
end
Pod --> PVC
PVC -.绑定.-> PV
PVC -.或: 通过.-> SC
SC -.动态创建.-> PV
PV --> Disk
style 应用 fill:#e1f5ff
style 用户 fill:#ffe1f5
style 管理员 fill:#fff4e1
style 底层 fill:#e1ffe1
三者职责:
| 资源 | 谁创建 | 比喻 |
|---|---|---|
| PV (PersistentVolume) | 管理员手动 / SC 动态 | "一块具体的盘"(已经存在) |
| PVC (PersistentVolumeClaim) | 应用开发者 | "我要 10Gi 的盘"(申请单) |
| StorageClass | 管理员 | "怎么造盘的配方"(按需自动造) |
静态 PV vs 动态 PV
# 管理员: 预先创建 PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-data-1
spec:
capacity:
storage: 10Gi
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
nfs:
server: nfs.example.com
path: /exports/data1
# 管理员: 预定义 StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com # CSI driver
parameters:
type: gp3
iops: "3000"
throughput: "125"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
bind 过程详解
sequenceDiagram
autonumber
participant U as 用户
participant PVC as PersistentVolumeClaim
participant PVCtl as PV Controller
participant SC as StorageClass
participant CSI as CSI driver
participant Cloud as 云厂商 API
participant PV as PersistentVolume
U->>PVC: kubectl apply pvc.yaml
Note over PVC: status: Pending
PVCtl->>PVC: 检测到新 PVC
PVCtl->>SC: 查 StorageClass
SC-->>PVCtl: provisioner: ebs.csi.aws.com
PVCtl->>CSI: CreateVolume 调用
CSI->>Cloud: 创建 EBS 卷
Cloud-->>CSI: vol-abc123
CSI->>PV: 创建 PV
Note over PV: 包装 vol-abc123
PVCtl->>PVC: bind PV
Note over PVC: status: Bound<br>volumeName: pv-xxx
PVC Pending 卡住 → 多半是这个过程中某步失败。详见 §11 排查。
2. accessMode —— 不只是注释
spec:
accessModes:
- ReadWriteOnce # RWO
- ReadWriteMany # RWX
- ReadOnlyMany # ROX
- ReadWriteOncePod # RWOP (K8s 1.27+)
4 种语义
| accessMode | 含义 | 适用 |
|---|---|---|
ReadWriteOnce (RWO) | 一个节点上读写(节点内多 pod 也行) | 云盘(EBS / GCE PD)/ 本地盘 |
ReadWriteMany (RWX) | 多节点同时读写 | NFS / CephFS / EFS |
ReadOnlyMany (ROX) | 多节点只读 | 共享只读资源(罕见) |
ReadWriteOncePod (RWOP) | 一个 pod 读写(K8s 1.27+ 才有) | 严格单 pod 排他 |
accessMode 是建议、不是强制
kind: PersistentVolume
spec:
accessModes: [ReadWriteOnce]
nfs: # ← 这其实是个 NFS
server: nfs.example.com
path: /shared
声明 RWO、但底层是 NFS(能多挂)。K8s 不会阻止你——只是调度器用 accessMode 决定 PVC 能否匹配 PV。
真"排他"靠底层存储(云盘只能一个节点挂)。NFS / CephFS 即使你声明 RWO 也能多 pod 挂。
不要用 NFS / Ceph 上的 PV 声明 RWO 然后期望排他
# PV
spec:
accessModes: [ReadWriteOnce]
nfs: ...
# 多个 Deployment 用同一个 PVC、3 副本
# → 3 pod 同时挂 NFS PV
# → 同时写同一文件
# → 数据竞态
K8s 不阻止。要真排他用 RWOP(K8s 1.27+)或者业务层加锁。
RWO 多 pod 同节点 OK
# RWO PVC、3 副本 deployment
# 3 个 pod 都调度到同一节点 m4 → OK(同节点)
# 1 个调度到 m4、2 个到 m5 → m5 上的 pod 卡 Pending(PV 已被 m4 挂)
3. reclaimPolicy —— PV 删了数据丢吗
spec:
persistentVolumeReclaimPolicy: Retain # / Delete / Recycle
三种含义
| Policy | PVC 删除时 PV 行为 |
|---|---|
Retain | PV 保留为 Released 状态、数据保留、需要管理员手动清 |
Delete | PV 自动删除、底层存储一起删 |
Recycle | 已 deprecated(早期版本会跑 rm -rf /、不安全) |
静态 PV 默认 Retain,动态 PV 默认看 StorageClass
# 动态 PV (StorageClass)
apiVersion: storage.k8s.io/v1
kind: StorageClass
reclaimPolicy: Delete # ← 默认 Delete
很多人生产用默认 Delete——PVC 删了云盘也删、数据再也回不来。
生产推荐 Retain
reclaimPolicy: Retain
PVC 删了 PV 还在、底层云盘还在。需要手动 kubectl delete pv 之后云上删盘。
代价:废 PV 会堆积、要清理脚本。
收益:误删 PVC 不会丢数据。
Retain 之后 PV 状态
$ kubectl get pv
NAME CAPACITY ACCESS RECLAIM STATUS CLAIM AGE
pv-001 100Gi RWO Retain Released default/mypvc 1d
^^^^^^^^ ^^^^^^^^^^^^
(1) (2)
| 标记 | 含义 |
|---|---|
| (1) | Released = 之前的 PVC 删了、PV 保留但不能被新 PVC 绑定 |
| (2) | 之前绑过的 PVC(带历史信息) |
要让 PV 能复用:
$ kubectl edit pv pv-001
# 删除 spec.claimRef 整段
或者重新创建一个 PV(更干净)。
4. volumeBindingMode —— 晚绑定
apiVersion: storage.k8s.io/v1
kind: StorageClass
volumeBindingMode: WaitForFirstConsumer # 或 Immediate
两种模式
| 模式 | 何时创建 PV |
|---|---|
Immediate(默认) | PVC 创建时立刻创建 PV |
WaitForFirstConsumer(推荐) | 等到 pod 实际要用 PVC 时才创建 |
为啥要 WaitForFirstConsumer
graph LR
subgraph Immediate模式问题
I1[PVC 创建] --> I2[CSI 立刻在 AZ-A 创建云盘]
I2 --> I3[pod 调度到 AZ-B]
I3 --> I4[云盘在 A、pod 在 B → 挂不上]
end
subgraph WaitForFirstConsumer
W1[PVC 创建] --> W2[等]
W2 --> W3[pod 调度到 AZ-B]
W3 --> W4[CSI 在 AZ-B 创建云盘]
W4 --> W5[成功挂]
end
style I4 fill:#ffe1e1
style W5 fill:#e1ffe1
Immediate 的经典坑:跨 AZ K8s 集群、PVC 先创建(PV 进了 AZ-A)、pod 调度到 AZ-B → pod 卡 Pending。
WaitForFirstConsumer:先等 pod 调度、再按 pod 所在 AZ 创建云盘。
生产强烈推荐 WaitForFirstConsumer
所有云盘 StorageClass / Local PV 都该设。例外:纯 RWX NFS(任意节点都能挂)。
5. Local PV —— 节点本地存储
# 静态 Local PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-m4-ssd1
spec:
capacity:
storage: 500Gi
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
volumeMode: Filesystem
local:
path: /mnt/disks/ssd1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [m4] # ← 绑定到 m4
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner # 不动态供应
volumeBindingMode: WaitForFirstConsumer
Local PV vs hostPath
| hostPath | Local PV | |
|---|---|---|
| 调度感知 | ❌(pod 调度后才知道节点有没有数据) | ✅(K8s 知道 PV 在哪个节点) |
| nodeAffinity | 手动配 | 强制绑定到 nodeAffinity 节点 |
| 健壮性 | 节点挂了 pod 漂、数据丢 | 节点挂了 pod 卡 Pending(数据保留) |
| 用例 | 系统组件(CSI / 监控) | 数据库 / 性能敏感的本地存储 |
生产高性能场景(数据库、日志)推荐 Local PV、不要 hostPath。
Local PV 自动供应:local-static-provisioner / TopoLVM
手动管理 Local PV 太烦。社区有自动化方案:
- local-static-provisioner:每个节点 daemonset、扫
/mnt/disks/*自动创建 PV - TopoLVM:用 LVM 动态切分本地盘成多个 PV
- OpenEBS LocalPV / Longhorn
6. CSI Framework —— 现代 K8s 存储的根本
K8s 早期内置了很多 storage provider(NFS / iSCSI / Ceph 等)——叫 in-tree provider。K8s 1.25+ 全部 deprecated、未来全靠 CSI。
CSI 是什么
CSI (Container Storage Interface) = K8s ↔ 存储后端的通用 gRPC 协议。任何存储厂商写个 CSI driver、K8s 就能用。
CSI driver 在 K8s 里的部署形态
graph TB
subgraph CtrlPlane[控制面节点]
Provisioner["csi-provisioner<br>(Deployment)<br>处理 PVC 创建"]
Attacher["csi-attacher<br>(Deployment)<br>处理 VolumeAttachment"]
ResizeC["csi-resizer<br>(Deployment)<br>处理扩容"]
Snap["csi-snapshotter<br>(Deployment)<br>处理 VolumeSnapshot"]
end
subgraph EachNode[每个节点]
NodeReg["csi-node-driver-registrar<br>(DaemonSet)<br>向 kubelet 注册"]
NodePlugin["csi-driver<br>(实际 mount 操作)"]
end
subgraph K8sAPI[K8s API]
PVC
PV
VA["VolumeAttachment<br>(K8s 内部资源)"]
end
PVC --> Provisioner
Provisioner --> PV
PV --> Attacher
Attacher --> VA
VA --> NodePlugin
NodePlugin -.mount.-> Disk["实际磁盘"]
style CtrlPlane fill:#e1f5ff
style EachNode fill:#ffe1f5
CSI driver 由多个 sidecar 组成:
| sidecar | 作用 |
|---|---|
external-provisioner | 监听 PVC → 调 CSI CreateVolume |
external-attacher | 监听 VolumeAttachment → 调 CSI ControllerPublishVolume |
external-resizer | 监听 PVC 容量变更 → 调 CSI ControllerExpandVolume |
external-snapshotter | 监听 VolumeSnapshot → 调 CSI CreateSnapshot |
node-driver-registrar | 在节点上向 kubelet 注册 CSI driver |
CSI driver 自己实现:
CreateVolume/DeleteVolumeControllerPublishVolume(把盘 attach 到节点)NodePublishVolume(在节点上 mount 到 pod 目录)NodeExpandVolume(节点上扩文件系统)
Pod 挂 PV 的完整 7 步
sequenceDiagram
autonumber
participant K8s as K8s API
participant Sched as Scheduler
participant Prov as csi-provisioner
participant Att as csi-attacher
participant Kubelet
participant Node as csi-node
participant Cloud as 云盘 API
K8s->>Prov: PVC 创建
Prov->>Cloud: CreateVolume
Cloud-->>Prov: vol-id
Prov->>K8s: 创建 PV、bind PVC
K8s->>Sched: 调度 pod
Sched->>K8s: pod 分配到 m4
K8s->>Att: 创建 VolumeAttachment(m4, pv)
Att->>Cloud: AttachVolume(vol-id, m4)
Cloud-->>Att: /dev/sdb attached to m4
Kubelet->>Node: NodeStageVolume(pv, /var/lib/kubelet/.../staging)
Note over Node: mkfs (首次) + mount 到 staging 目录
Kubelet->>Node: NodePublishVolume(pv, /var/lib/kubelet/pods/.../mount)
Note over Node: bind mount staging → pod 目录
Note over Kubelet: pod 启动、bind 给容器
7 个动作里任何一个失败 → pod 卡 ContainerCreating。
看 CSI driver 状态
# 节点上哪些 CSI driver 注册了
$ kubectl get csinode
NAME DRIVERS AGE
m1 2 1d
m2 2 1d
# 看具体 driver
$ kubectl get csidriver
NAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY AGE
ebs.csi.aws.com true false false 1d
efs.csi.aws.com false false false 1d
# 看 VolumeAttachment(PV 是否真的 attached 到节点)
$ kubectl get volumeattachment
NAME ATTACHER PV NODE ATTACHED AGE
csi-... ebs.csi.aws.com pv-xxx m4 true 1h
# CSI driver 日志
$ kubectl logs -n kube-system ⟨csi-controller-pod⟩ -c csi-provisioner
$ kubectl logs -n kube-system ⟨csi-node-pod⟩ -c csi-driver
7. PV 扩容(生产高频需求)
前提:StorageClass 允许扩容
apiVersion: storage.k8s.io/v1
kind: StorageClass
allowVolumeExpansion: true # ← 必须开
操作:直接改 PVC
$ kubectl edit pvc my-pvc
spec:
resources:
requests:
storage: 200Gi # 从 100Gi 改 200Gi
K8s 自动:
- CSI 在云上扩盘(在线、不停机)
- 节点上扩文件系统(
resize2fs/xfs_growfs)
$ kubectl get pvc my-pvc
NAME STATUS CAPACITY ACCESS MODES STORAGECLASS
my-pvc Bound 200Gi RWO ebs-sc ← 扩到 200Gi
部分 CSI driver 需要 pod 重启
某些 driver(特别老的)不支持"在线节点扩容"——你扩了 PVC、底层盘扩了、但 pod 看到的文件系统大小没变:
$ kubectl exec my-pod -- df -h /data
/dev/sdb1 100G 50G 50G 50% /data ← 还是 100G
# 重启 pod 之后
$ kubectl delete pod my-pod
$ kubectl get pod my-pod -w # 等新 pod
$ kubectl exec my-pod -- df -h /data
/dev/sdb1 200G 50G 150G 25% /data ← 200G 了
新 CSI driver 大多支持在线扩容——不需要重启 pod。看 CSIDriver spec:
$ kubectl get csidriver ebs.csi.aws.com -o yaml | grep volumeLifecycleModes
反面:PV 不能缩容
K8s 没实现缩容(部分文件系统也不支持)。如果声明小了:
spec:
resources:
requests:
storage: 10Gi # 从 100Gi 改 10Gi
→ kubectl 拒:
PVC update not allowed: requested storage size is smaller than current size
要缩容 → 新建小 PVC + 拷数据 + 删旧 PVC。
8. VolumeSnapshot —— PV 快照
K8s 标准 CSI snapshot API:
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: ebs-snapshot
driver: ebs.csi.aws.com
deletionPolicy: Delete
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: my-pvc-snapshot
spec:
volumeSnapshotClassName: ebs-snapshot
source:
persistentVolumeClaimName: my-pvc
从 snapshot 还原:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: restored-pvc
spec:
storageClassName: ebs-sc
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 100Gi
dataSource: # 从 snapshot 还原
name: my-pvc-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
需要 CSI driver 支持。AWS EBS / GCE PD / Azure Disk / Ceph RBD 都支持。
9. StatefulSet 的 volumeClaimTemplate
普通 Deployment 多副本共享一个 PVC(如果用 RWX)。StatefulSet 给每个 pod 自己的 PVC:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql # Headless Service
replicas: 3
selector:
matchLabels:
app: mysql
template:
spec:
containers:
- name: mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumeClaimTemplates: # ← 每副本独立 PVC
- metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
K8s 创建:
$ kubectl get pvc -l app=mysql
NAME STATUS VOLUME CAPACITY ACCESS MODES AGE
data-mysql-0 Bound pv-001 100Gi RWO 1h
data-mysql-1 Bound pv-002 100Gi RWO 1h
data-mysql-2 Bound pv-003 100Gi RWO 1h
每个 mysql-N pod 有自己的 PVC(命名规则 ⟨template-name⟩-⟨sts-name⟩-<index>)。
StatefulSet 删了 PVC 不会自动删
$ kubectl delete statefulset mysql
$ kubectl get pvc -l app=mysql
NAME STATUS VOLUME CAPACITY ACCESS MODES AGE
data-mysql-0 Bound pv-001 100Gi RWO 1h
data-mysql-1 Bound pv-002 100Gi RWO 1h
data-mysql-2 Bound pv-003 100Gi RWO 1h
K8s 故意保留——StatefulSet 是有状态服务、删除时数据应该保留。要清需手动:
$ kubectl delete pvc -l app=mysql
K8s 1.27+ 引入 persistentVolumeClaimRetentionPolicy 可以自动清:
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Delete # StatefulSet 删时
whenScaled: Delete # 缩容时
生产不推荐自动删——人为操作时再清。
10. Volume Mode:Filesystem vs Block
spec:
volumeMode: Filesystem # 默认(文件系统)
# 或
volumeMode: Block # 块设备直通
Filesystem(默认)
K8s 帮你格式化 + mount。容器看到的是文件系统:
$ kubectl exec pod -- ls /data
file1.txt
file2.txt
Block
K8s 把裸块设备给容器(不格式化、不挂载):
containers:
- name: app
volumeDevices: # 不是 volumeMounts
- name: data
devicePath: /dev/sdb
容器内:
$ kubectl exec pod -- ls /dev/sdb
/dev/sdb # 裸设备
应用自己做 mkfs / 直接 I/O / 用 mmap 等。
用例:
- 数据库自己管 raw device(高性能)
- 块存储模拟器测试
- 特殊文件系统(XFS / ZFS / btrfs)
99% 场景用 Filesystem。
11. PVC Pending 的 10 种原因
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-pvc Pending fast-ssd 5m
$ kubectl describe pvc my-pvc
Events:
Normal Provisioning ... storageclass.storage.k8s.io "fast-ssd" not found
$ kubectl get storageclass
NAME PROVISIONER AGE
default kubernetes.io/no-provisioner 1d
# fast-ssd 不在
修:kubectl apply -f storageclass.yaml
$ kubectl describe pvc my-pvc
Events:
Warning ProvisioningFailed ... waiting for first consumer
静态 PV + 用户请求 RWX、但只有 RWO PV → 没匹配。
$ kubectl describe pvc my-pvc
Events:
Warning ProvisioningFailed ... timed out waiting for ...
$ kubectl get pods -n kube-system | grep csi
# 看 driver pod 跑没
$ kubectl logs -n kube-system ⟨csi-controller⟩ -c csi-provisioner | tail -50
... rpc error: code = ResourceExhausted: ...
... rpc error: code = PermissionDenied: ...
云上常见:IAM 角色没 CreateVolume 权限 / VPC 配额不够 / API rate-limit。
云上 EBS 配额(如默认 50 个 / Region)超了。
... rpc error: ... You have exceeded your maximum gp3 storage allocation
申请提配额。
PVC 立刻创建在 AZ-A、pod 调度到 AZ-B → AZ-B 节点挂不上 AZ-A 云盘。
修:StorageClass 加 volumeBindingMode: WaitForFirstConsumer。
... 申请 10TiB、单卷最大 1TiB
云盘大多有单卷上限。
$ kubectl get pv
NAME ... STATUS CLAIM
pv-001 ... Released default/old-pvc ← 之前 PVC 删了
新 PVC 不会匹配 Released 的 PV(K8s 不知道你想"复用")。
修:
kubectl edit pv pv-001
# 删 spec.claimRef 整段
# PV 状态变 Available、可以被新 PVC 匹配
PVC 和 PV 必须在同一 namespace(PV 是集群级、PVC 是 namespace 级——所以 PVC 在哪个 ns 它绑的 PV 看 ns 不限制、但 pod 用 PVC 要同 ns)。
简化:PVC 和用它的 pod 必须同 ns。
Local PV nodeAffinity 绑定 m4、但 m4 已经在 maintenance / NoSchedule taint。
$ kubectl describe pvc my-pvc
... no nodes available to satisfy ...
修:让 pod 能调度到 m4、或者去 m4 的 taint。
12. 反面教材合集
反面 1:用 Delete reclaimPolicy 然后误删 PVC
$ kubectl delete pvc my-pvc
# reclaimPolicy: Delete → 云盘删了 → 数据丢
生产强制 Retain。删除 PV 是危险操作、应有 review。
反面 2:StorageClass 默认设了 volumeBindingMode: Immediate
PV 在错误 AZ 创建、pod 卡住。
修:所有云盘 SC 都设 WaitForFirstConsumer。
反面 3:单 RWO PVC 跑 3 副本 Deployment
spec:
replicas: 3
template:
spec:
volumes:
- persistentVolumeClaim:
claimName: my-pvc # RWO
3 pod 调度到不同节点 → 2 个起不来。只有 1 个 pod 能运行——但 K8s 不会告诉你"你设计错了"。
修:
- 用 StatefulSet(每副本独立 PVC)
- 或者换 RWX PVC(NFS / CephFS)
反面 4:跨集群迁移 PVC
# 把生产集群备份的 PV / PVC yaml 应用到新集群
kubectl apply -f backup.yaml
不行。PV 里的 volumeHandle 是云上的卷 ID—— 新集群没访问这个 ID 的权限。
正确:
- 用 Velero 跨集群备份(含底层存储)
- 或者 snapshot → 新集群从 snapshot 还原
反面 5:用 fsGroup 改 PVC 文件 owner 慢
spec:
securityContext:
fsGroup: 1000 # ← 让 PVC 里所有文件 owner = 1000
K8s 启动 pod 时 chown -R 1000 /data —— 大量文件时极慢(几十万文件可能几分钟)。
修:
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch # 只在根 owner 不对时改(K8s 1.20+)
或者应用层处理。
反面 6:StatefulSet 缩容直接清 PVC
$ kubectl scale statefulset mysql --replicas=1
# mysql-1 / mysql-2 pod 删了
PVC data-mysql-1 / data-mysql-2 保留(这是 StatefulSet 默认)。后续扩容时这些 PVC 被复用 → 老数据回来。
如果你确实想清,要手动:
kubectl delete pvc data-mysql-1 data-mysql-2
或者 K8s 1.27+ 设 persistentVolumeClaimRetentionPolicy.whenScaled: Delete。
反面 7:PV 删除卡 Terminating
$ kubectl delete pv pv-001
# 卡几小时
$ kubectl get pv pv-001
NAME STATUS ...
pv-001 Terminating ...
PV 有 finalizer + 底层存储 detach 失败。看 finalizer:
$ kubectl get pv pv-001 -o jsonpath='{.metadata.finalizers}'
[kubernetes.io/pv-protection]
强删:
$ kubectl patch pv pv-001 -p '{"metadata":{"finalizers":null}}' --type=merge
但这只清掉 K8s 视角的 PV、云盘可能还在云上。要清云盘需要单独操作。
13. 排查 cheatsheet
"PVC 卡 Pending" 完整流程
flowchart TD
Start[PVC Pending] --> S1{kubectl describe pvc<br>Events 说啥?}
S1 -->|storageclass not found| F1[创建 SC]
S1 -->|ProvisioningFailed| S2{看 CSI driver 日志}
S2 -->|IAM/网络| F2[修云上凭证 / 网络]
S2 -->|配额| F3[申请扩配额]
S1 -->|waiting for first consumer| S3{Pod 是不是<br>真在等?}
S3 -->|Pod 也 Pending| F4[调度问题 / taint / 资源不足]
S3 -->|Pod Running| F5[底层 CSI 调用有问题]
S1 -->|匹配 PV 失败| F6[手动创建 PV / 改 PVC accessMode]
关键命令
# 1. PVC 详情
kubectl describe pvc my-pvc
# 2. CSI driver 状态
kubectl get csinode
kubectl get csidriver
kubectl get pods -n kube-system | grep csi
# 3. CSI 日志
kubectl logs -n kube-system ⟨csi-controller⟩ -c csi-provisioner --tail=50
kubectl logs -n kube-system ⟨csi-node-pod-on-target-node⟩ -c csi-driver --tail=50
# 4. VolumeAttachment
kubectl get volumeattachment
# 5. 节点上看 PV 真实挂载
ssh m4 'findmnt /var/lib/kubelet/pods/⟨pod-uid⟩/volumes/'
ssh m4 'ls -la /var/lib/kubelet/plugins/⟨csi-driver⟩/'
# 6. Events 全集
kubectl get events --sort-by=".lastTimestamp" | grep -i pvc
"Pod 卡 ContainerCreating with volume"
$ kubectl describe pod my-pod
Events:
Warning FailedAttachVolume AttachVolume.Attach failed for volume "pv-xxx" :
rpc error: code = ResourceExhausted desc = ...
Warning FailedMount Unable to attach or mount volumes: ...
按 Event 显示的错误:
| Error | 含义 | 修 |
|---|---|---|
FailedAttachVolume | CSI attach 失败 | 看 CSI controller log |
FailedMount | mount 失败 | 看 CSI node pod log |
ResourceExhausted | 配额满 / 节点已挂 PV 数到上限 | 调度别处 / 申请配额 |
Forbidden | RBAC / IAM | 修 K8s ServiceAccount 或云 IAM |
node has no volume binding info | node 缺 csinode 注册 | 重启 CSI node pod |
14. 下一步
| 篇 | 内容 |
|---|---|
| 00-storage-mental-model.md | 心智模型 |
| 01-container-volumes.md | 容器挂载 |
| 02-k8s-volumes.md | K8s Volume 大全 |
| 本篇 | PV / PVC / CSI 深入 |
| 04-nfs-deep.md | NFS(RWX 主要实现) |
| 05-distributed-storage.md | Ceph / Longhorn |
| 06-storage-troubleshooting.md | 故障排查 runbook |