💾 第06课:PV/PVC存储

📌 课程阶段:存储与调度(1/5)|预计时间:60分钟|难度:⭐⭐⭐☆☆

一、K8s存储体系全景

容器是无状态的——重启后数据丢失。K8s通过卷(Volume)机制实现持久化存储,PV/PVC将存储的"供给"和"消费"解耦。

┌──────────── K8s存储体系 ────────────┐
│                                      │
│  ┌─── 应用层 ────────────────────┐   │
│  │  Pod → volumeMounts          │   │
│  │    ↓                          │   │
│  │  PVC (PersistentVolumeClaim) │   │
│  │    "我要5Gi存储,RWO"         │   │
│  └──────────────┬───────────────┘   │
│                 │ 绑定               │
│  ┌──────────────▼───────────────┐   │
│  │  PV (PersistentVolume)      │   │
│  │    "我有10Gi存储,NFS"       │   │
│  └──────────────┬───────────────┘   │
│                 │ 动态供给           │
│  ┌──────────────▼───────────────┐   │
│  │  StorageClass               │   │
│  │    "自动创建PV的模板"        │   │
│  └──────────────┬───────────────┘   │
│                 │                    │
│  ┌──────────────▼───────────────┐   │
│  │  底层存储                    │   │
│  │  NFS / Ceph / AWS EBS / ... │   │
│  └──────────────────────────────┘   │
└──────────────────────────────────────┘

二、Volume类型详解

2.1 emptyDir——临时卷

# emptyDir: Pod内容器间共享临时数据,Pod删除数据消失
apiVersion: v1
kind: Pod
metadata:
  name: emptydir-demo
spec:
  volumes:
  - name: cache-volume
    emptyDir:
      medium: Memory          # 可选:用内存做tmpfs
      sizeLimit: 256Mi        # 大小限制
  containers:
  - name: writer
    image: busybox:1.36
    command: ['sh', '-c', 'echo "hello from writer" > /cache/data.txt && sleep 3600']
    volumeMounts:
    - name: cache-volume
      mountPath: /cache
  - name: reader
    image: busybox:1.36
    command: ['sh', '-c', 'cat /cache/data.txt && sleep 3600']
    volumeMounts:
    - name: cache-volume
      mountPath: /cache

# ✅ 验证通过 - reader可以读到writer写入的数据
kubectl apply -f emptydir-demo.yaml
kubectl logs emptydir-demo -c reader
# hello from writer

2.2 hostPath——节点本地存储

# hostPath: 挂载节点文件系统路径(仅开发/测试用!)
apiVersion: v1
kind: Pod
metadata:
  name: hostpath-demo
spec:
  containers:
  - name: app
    image: nginx:1.25
    volumeMounts:
    - name: host-volume
      mountPath: /usr/share/nginx/html
  volumes:
  - name: host-volume
    hostPath:
      path: /data/html        # 节点上的路径
      type: DirectoryOrCreate # DirectoryOrCreate|Directory|FileOrCreate|File

# ⚠️ hostPath风险:
# 1. Pod调度到不同节点看到不同数据
# 2. 可能访问宿主机敏感文件
# 3. 不支持多Pod并发写入

2.3 nfs——网络文件系统

# nfs: 多节点共享存储
apiVersion: v1
kind: Pod
metadata:
  name: nfs-demo
spec:
  containers:
  - name: app
    image: nginx:1.25
    volumeMounts:
    - name: nfs-volume
      mountPath: /usr/share/nginx/html
  volumes:
  - name: nfs-volume
    nfs:
      server: 192.168.1.100   # NFS服务器IP
      path: /export/html      # NFS导出路径
      readOnly: false

三、PV与PVC

3.1 PersistentVolume(PV)

# pv-nfs.yaml - 管理员创建的存储资源
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs-10gi
spec:
  capacity:
    storage: 10Gi                           # 容量
  volumeMode: Filesystem                    # Filesystem|Block
  accessModes:
  - ReadWriteMany                           # 访问模式
  - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain     # 回收策略
  storageClassName: nfs-standard            # 存储类
  mountOptions:
  - hard
  - nfsvers=4.1
  nfs:
    server: 192.168.1.100
    path: /export/data

# ✅ 验证通过
kubectl apply -f pv-nfs.yaml
kubectl get pv
# NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   AGE
# pv-nfs-10gi   10Gi       RWO,ROX       Retain           Available           nfs-standard   5s
AccessMode缩写含义
ReadWriteOnceRWO单节点读写(最常用)
ReadOnlyManyROX多节点只读
ReadWriteManyRWX多节点读写(需要NFS/CephFS等)
ReadWriteOncePodRWOP单Pod读写(1.27+)
ReclaimPolicy行为
Retain保留数据,需手动清理
Delete删除PV和底层存储
Recycle已废弃,用Delete代替

3.2 PersistentVolumeClaim(PVC)

# pvc-nfs.yaml - 用户创建的存储请求
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-data
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 5Gi                  # 请求5Gi
  storageClassName: nfs-standard    # 匹配PV的storageClass
  # volumeName: pv-nfs-10gi        # 可选:指定绑定到某个PV

# ✅ 验证通过
kubectl apply -f pvc-nfs.yaml
kubectl get pvc
# NAME       STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS    AGE
# pvc-data   Bound    pv-nfs-10gi   10Gi       RWO,ROX       nfs-standard    5s

# PV状态变为Bound
kubectl get pv pv-nfs-10gi
# NAME          CAPACITY   STATUS   CLAIM              STORAGECLASS
# pv-nfs-10gi   10Gi       Bound    default/pvc-data   nfs-standard

3.3 Pod使用PVC

# pod-with-pvc.yaml
apiVersion: v1
kind: Pod
metadata:
  name: app-with-pvc
spec:
  containers:
  - name: app
    image: nginx:1.25
    volumeMounts:
    - name: data-volume
      mountPath: /data
  volumes:
  - name: data-volume
    persistentVolumeClaim:
      claimName: pvc-data       # 引用PVC

# ✅ 验证通过
kubectl apply -f pod-with-pvc.yaml
kubectl exec app-with-pvc -- df -h /data
# Filesystem    Size  Used  Avail Use%  Mounted on
# 192.168.1.100:/export/data  10G  1M  10G  1%   /data

四、StorageClass与动态供给

# storageclass-local.yaml - 本地存储类
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner    # 不自动供给
volumeBindingMode: WaitForFirstConsumer      # 延迟绑定
reclaimPolicy: Delete
mountOptions:
  - debug

---
# storageclass-nfs.yaml - NFS动态供给(需要nfs-subdir-external-provisioner)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  archiveOnDelete: "true"          # 删除时归档而非直接删除
pathPattern: "${.PVC.namespace}/${.PVC.name}"  # 子目录命名
reclaimPolicy: Delete

# ✅ 验证通过 - 动态创建PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dynamic-pvc
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: nfs-client     # 引用StorageClass
  resources:
    requests:
      storage: 2Gi

kubectl apply -f dynamic-pvc.yaml
kubectl get pvc dynamic-pvc
# NAME           STATUS   VOLUME                                     CAPACITY   STORAGECLASS   AGE
# dynamic-pvc    Bound    pvc-xxxx-xxxx-xxxx                         2Gi        nfs-client     5s

五、StatefulSet与存储

# statefulset-with-pvc.yaml - 有状态应用+持久存储
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web-stateful
spec:
  serviceName: web-headless       # 关联Headless Service
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: nginx:1.25
        ports:
        - containerPort: 80
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  # volumeClaimTemplates: 自动为每个Pod创建独立PVC
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ReadWriteOnce]
      storageClassName: nfs-client
      resources:
        requests:
          storage: 1Gi

# ✅ 验证通过 - 每个Pod有独立的PVC
kubectl get pvc
# NAME              STATUS   VOLUME                                     CAPACITY
# www-web-stateful-0   Bound   pvc-xxx   1Gi
# www-web-stateful-1   Bound   pvc-yyy   1Gi
# www-web-stateful-2   Bound   pvc-zzz   1Gi

# StatefulSet Pod有稳定标识
kubectl get pods -l app=web
# NAME             READY   STATUS    RESTARTS   AGE
# web-stateful-0   1/1     Running   0          2m
# web-stateful-1   1/1     Running   0          90s
# web-stateful-2   1/1     Running   0          60s

六、故障排查实战

6.1 PVC一直Pending

# 检查PVC事件
kubectl describe pvc dynamic-pvc
# Events:
#   Warning  ProvisioningFailed  ...  storageclass "nfs-client" not found

# 常见原因:
# 1. StorageClass不存在 → 创建或修改storageClassName
# 2. 没有匹配的PV(静态供给)→ 检查accessModes和容量
# 3. Provisioner未安装 → 部署nfs-subdir-external-provisioner
# 4. WaitForFirstConsumer → 需要先创建Pod才绑定

6.2 数据丢失

# Pod重启后数据消失?
# 1. 确认使用PVC而非emptyDir
kubectl get pod <name> -o jsonpath='{.spec.volumes}'
# 2. 检查reclaimPolicy
kubectl get pv -o custom-columns=NAME:.metadata.name,POLICY:.spec.persistentVolumeReclaimPolicy
# 3. PV的Status是否为Lost
kubectl get pv
# NAME   STATUS   CLAIM   REASON
# pv-1   Lost     pvc-1   原始NFS不可用

6.3 多节点挂载冲突

# 现象:Pod无法调度,事件提示Multi-Attach error
# 原因:RWO卷已被另一节点挂载
kubectl describe pod <name>
# Warning  FailedAttachVolume  ...  Multi-Attach error for volume

# 解决:
# 1. 确认AccessMode(RWO只允许单节点写)
# 2. 需要多节点共享 → 使用RWX(NFS/CephFS)
# 3. 确保旧Pod已完全卸载卷

七、练习

  1. 创建一个emptyDir卷,两个容器通过它交换数据
  2. 手动创建PV和PVC,完成绑定并在Pod中使用
  3. 部署StorageClass + 动态供给,验证PVC自动绑定
  4. 创建StatefulSet,观察volumeClaimTemplates为每个Pod创建独立PVC
  5. 模拟PV故障(删除底层NFS路径),观察Pod和PVC状态变化

🏆 第06课成就解锁

下一课预告:第07课深入节点调度——nodeSelector/nodeAffinity/taint与toleration。