AI Infra 训练营
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
HiHuo 主站
GitHub
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
HiHuo 主站
GitHub
  • 存储专题

    • 存储心智模型:从一次 write() 到磁盘
    • 容器挂载完整指南:bind mount / volume / mount propagation / 多视角
    • K8s Volume 类型大全 + subPath / 多视角
    • PV / PVC / StorageClass / CSI 深入
    • NFS 深入:server / client / K8s NFS / 经典坑
    • 分布式存储概览:Ceph / Longhorn / Rook
    • 存储故障排查 runbook

PV / PVC / StorageClass / CSI 深入

这一篇是存储专题的核心。K8s 持久化存储的所有抽象(PV / PVC / StorageClass)+ 现代实现(CSI 框架)+ 几乎所有生产坑(Pending / 扩容 / reclaim / 权限 / cross-AZ)。

这篇要回答什么

基础
  1. PV / PVC / StorageClass 三者怎么协作?
  2. 静态 PV vs 动态 PV 区别?
  3. accessMode 真的能强制 enforce 吗?
  4. reclaim policy 三种各意味着什么?
进阶
  1. CSI driver 在 K8s 里怎么运转(架构)?
  2. 扩容 PV / 缩容 PV 各能做吗?
  3. 多个 pod 共享 PVC 行不行?
  4. K8s 跨 AZ 调度怎么和云盘 AZ 绑定?
生产
  1. PVC 卡 Pending 的 10 种原因
  2. PV 删了数据丢吗?三种 reclaim policy 的真相
  3. StatefulSet 的 volumeClaimTemplate 跟普通 PVC 区别?
  4. 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(手动)
# 管理员: 预先创建 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
动态 PV(生产主流)
# 管理员: 预定义 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

三种含义

PolicyPVC 删除时 PV 行为
RetainPV 保留为 Released 状态、数据保留、需要管理员手动清
DeletePV 自动删除、底层存储一起删
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

hostPathLocal 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 / DeleteVolume
  • ControllerPublishVolume(把盘 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 自动:

  1. CSI 在云上扩盘(在线、不停机)
  2. 节点上扩文件系统(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
1. StorageClass 不存在
$ 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

2. accessMode 没匹配的 PV
$ kubectl describe pvc my-pvc
Events:
  Warning  ProvisioningFailed  ... waiting for first consumer

静态 PV + 用户请求 RWX、但只有 RWO PV → 没匹配。

3. CSI driver 没装 / Pod 挂了
$ kubectl describe pvc my-pvc
Events:
  Warning  ProvisioningFailed  ... timed out waiting for ...

$ kubectl get pods -n kube-system | grep csi
# 看 driver pod 跑没
4. CSI driver 报错(IAM / 网络等)
$ 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。

5. 容量配额不够

云上 EBS 配额(如默认 50 个 / Region)超了。

... rpc error: ... You have exceeded your maximum gp3 storage allocation

申请提配额。

6. AZ 不一致(WaitForFirstConsumer 未配)

PVC 立刻创建在 AZ-A、pod 调度到 AZ-B → AZ-B 节点挂不上 AZ-A 云盘。

修:StorageClass 加 volumeBindingMode: WaitForFirstConsumer。

7. PVC 大小超 StorageClass 限制
... 申请 10TiB、单卷最大 1TiB

云盘大多有单卷上限。

8. PV 状态卡 Released、不能 bind 新 PVC
$ 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 匹配
9. namespace 错

PVC 和 PV 必须在同一 namespace(PV 是集群级、PVC 是 namespace 级——所以 PVC 在哪个 ns 它绑的 PV 看 ns 不限制、但 pod 用 PVC 要同 ns)。

简化:PVC 和用它的 pod 必须同 ns。

10. nodeAffinity 不匹配

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含义修
FailedAttachVolumeCSI attach 失败看 CSI controller log
FailedMountmount 失败看 CSI node pod log
ResourceExhausted配额满 / 节点已挂 PV 数到上限调度别处 / 申请配额
ForbiddenRBAC / IAM修 K8s ServiceAccount 或云 IAM
node has no volume binding infonode 缺 csinode 注册重启 CSI node pod

14. 下一步

篇内容
00-storage-mental-model.md心智模型
01-container-volumes.md容器挂载
02-k8s-volumes.mdK8s Volume 大全
本篇PV / PVC / CSI 深入
04-nfs-deep.mdNFS(RWX 主要实现)
05-distributed-storage.mdCeph / Longhorn
06-storage-troubleshooting.md故障排查 runbook

命令文档

  • kubectl —— get pv/pvc/sc
  • findmnt —— 看节点上 PV 真实挂载
  • nsenter —— 进 pod 看挂载视角
  • lvm —— Local PV / TopoLVM 底层
  • iostat —— PV 性能
在 GitHub 上编辑此页
Prev
K8s Volume 类型大全 + subPath / 多视角
Next
NFS 深入:server / client / K8s NFS / 经典坑