2.7. 存储与持久化

Pod 随时会被删除、重建、漂移,容器里的文件也跟着消失。存储卷要解决的就是:哪些数据应该跟着 Pod 走,哪些应该独立于 Pod 存在。

层级:

L2 · 核心

难度:

进阶

预计:

55 分钟

前置:

Pod、工作负载

2.7.1. 你将学到

  • emptyDir、hostPath、NFS、PVC 的适用场景与边界

  • PV / PVC / StorageClass 的绑定流程

  • 静态供给与动态供给的差异

  • 访问模式、回收策略、绑定模式的实际含义

  • StatefulSet 的卷模板与数据保留

  • PVC 一直 Pending 的排查路径

2.7.2. 为什么需要卷

容器的文件系统是临时的。Pod 重建后,容器层的数据全部丢失。Kubernetes 用卷(Volume)把存储的生命周期从容器中解耦:

存储

生命周期

适合

容器文件系统

容器重启即丢

临时文件

emptyDir

Pod 删除即丢

容器间共享、缓存

hostPath

跟节点走

节点级代理、实验

NFS

独立于 Pod

多 Pod 共享

PVC

独立于 Pod,可动态供给

数据库、业务数据

架构图

图:Pod 通过 volume 使用存储;PVC 是「存储申请」,PV 是「存储资源」。

2.7.3. 临时卷

2.7.3.1. emptyDir

Pod 启动时创建,Pod 删除时销毁。适合同一 Pod 内多容器共享文件、临时缓存。

emptydir.yaml
apiVersion: v1
kind: Pod
metadata:
  name: emptydir-demo
spec:
  initContainers:
    - name: prepare
      image: busybox:1.36
      command: ["sh", "-c", "echo hello > /data/index.html"]
      volumeMounts:
        - name: data
          mountPath: /data
  containers:
    - name: web
      image: nginx:1.27
      volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumes:
    - name: data
      emptyDir:
        medium: Memory
        sizeLimit: 16Mi

medium: Memory 使用 tmpfs,速度快但占内存;sizeLimit 防止写满节点。

2.7.3.2. hostPath

把节点上的目录挂进 Pod。适合 DaemonSet 类型的节点代理,不适合业务数据。

hostpath.yaml
volumes:
  - name: host-data
    hostPath:
      path: /var/lib/app-data
      type: DirectoryOrCreate

风险:

  • Pod 漂移到别的节点后,看不到原数据;

  • 容器可能读写节点敏感目录;

  • 多 Pod 并发写同一目录容易冲突。

2.7.4. PV / PVC / StorageClass

2.7.4.1. 三个角色

对象

谁创建

作用

PV

管理员 / 动态供给

一块实际存储资源

PVC

应用开发者

对存储的申请(大小、访问模式)

StorageClass

管理员

定义「如何动态创建 PV」

2.7.4.2. 绑定流程

  1. 用户创建 PVC,声明 storageClassName、accessModes、resources.requests.storage;

  2. 控制器寻找匹配的 PV,或调用 StorageClass 的 provisioner 动态创建;

  3. 绑定成功后,PVC 变为 Bound;

  4. Pod 引用 PVC,kubelet 调用 CSI 把卷挂载到容器。

kubectl get pv,pvc -A
kubectl describe pvc <name> -n <ns>

2.7.4.3. 访问模式

模式

缩写

含义

ReadWriteOnce

RWO

单节点读写

ReadOnlyMany

ROX

多节点只读

ReadWriteMany

RWX

多节点读写

ReadWriteOncePod

RWOP

单 Pod 读写(较新)

RWO 是「单节点」,不是「单 Pod」。同一节点上的多个 Pod 可以同时挂载同一个 RWO 卷(取决于驱动)。

2.7.4.4. 回收策略

策略

行为

Delete

删除 PVC 时删除后端存储(动态供给默认)

Retain

保留 PV 与数据,需手工清理

生产环境的重要数据建议 Retain,避免误删 PVC 导致数据丢失。

2.7.4.5. 绑定模式

volumeBindingMode: WaitForFirstConsumer
  • Immediate:PVC 创建后立即绑定,可能绑到不满足 Pod 调度条件的节点;

  • WaitForFirstConsumer:等 Pod 调度后再绑定,适合本地盘和拓扑受限的存储。

2.7.5. 静态供给:NFS 示例

先创建一个 NFS PV:

pv-nfs.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs-demo
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 192.168.10.20
    path: /data/k8s/pv-nfs-demo

创建 PVC 绑定它:

pvc-nfs.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-nfs-demo
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
  storageClassName: ""

storageClassName: "" 表示不使用动态供给,只匹配静态 PV。

Pod 挂载:

volumes:
  - name: data
    persistentVolumeClaim:
      claimName: pvc-nfs-demo

2.7.6. 动态供给:StorageClass

以 NFS CSI 为例:

storageclass-nfs.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: nfs.csi.k8s.io
parameters:
  server: nfs-server.nfs.svc.cluster.local
  share: /
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true

创建 PVC 时指定:

pvc-dynamic.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data
spec:
  storageClassName: nfs-csi
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
kubectl apply -f pvc-dynamic.yaml
kubectl get pvc app-data -w
kubectl get pv | grep app-data

预期 PVC Bound,并自动出现一个对应的 PV。

小技巧

设置默认 StorageClass

kubectl patch storageclass nfs-csi -p \
  '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

集群只能有一个默认 StorageClass。PVC 不写 storageClassName 时使用默认值。

2.7.7. StatefulSet 与卷模板

StatefulSet 用 volumeClaimTemplates 为每个副本创建独立 PVC:

volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: nfs-csi
      resources:
        requests:
          storage: 10Gi
  • sts-demo-0 → PVC data-sts-demo-0

  • sts-demo-1 → PVC data-sts-demo-1

缩容时 PVC 默认保留,扩容后能重新挂回。需要自动删除时配置:

persistentVolumeClaimRetentionPolicy:
  whenScaled: Delete
  whenDeleted: Delete

2.7.8. 扩容与快照

2.7.8.1. 扩容

前提:StorageClass 设置 allowVolumeExpansion: true。

kubectl patch pvc app-data -p \
  '{"spec":{"resources":{"requests":{"storage":"5Gi"}}}}'
kubectl get pvc app-data -w

文件系统扩容通常需要 Pod 重启或 CSI 支持在线扩容。

2.7.8.2. 快照

需要 VolumeSnapshotClass、VolumeSnapshot CRD 与支持快照的 CSI:

snapshot.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: app-data-snapshot
spec:
  volumeSnapshotClassName: nfs-csi-snapclass
  source:
    persistentVolumeClaimName: app-data

NFS CSI 的快照能力取决于后端;块存储通常支持得更完整。

2.7.9. 实验:动态 PVC + 数据持久化

  1. 确认有默认 StorageClass:

kubectl get storageclass
  1. 创建 PVC:

kubectl apply -f pvc-dynamic.yaml
kubectl get pvc app-data
  1. 创建 Pod 写入数据:

writer.yaml
apiVersion: v1
kind: Pod
metadata:
  name: writer
spec:
  containers:
    - name: writer
      image: busybox:1.36
      command: ["sh", "-c", "echo persisted > /data/hello.txt; sleep 3600"]
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: app-data
kubectl apply -f writer.yaml
kubectl exec writer -- cat /data/hello.txt
  1. 删除 Pod,重建一个 reader:

kubectl delete pod writer
kubectl run reader --image=busybox:1.36 --restart=Never --overrides='
{"spec":{"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"app-data"}}],
"containers":[{"name":"reader","image":"busybox:1.36","command":["cat","/data/hello.txt"],
"volumeMounts":[{"name":"data","mountPath":"/data"}]}]}}'

预期仍然输出 persisted,证明数据独立于 Pod。

2.7.10. 排错表

现象

原因

处理

PVC 一直 Pending

没有匹配 PV / SC 不存在 / 容量不足

describe pvc 看 Events

PVC Pending 且 SC 是 WaitForFirstConsumer

还没有 Pod 使用它

创建 Pod 后再看

Pod FailedMount

NFS 不可达 / Secret 缺失 / CSI 未就绪

describe pod,检查 CSI Pod

Multi-Attach error

RWO 卷被另一个节点占用

等旧 Pod 完全终止,或换 RWX

删除 PVC 后数据还在

回收策略是 Retain

手工清理 PV 与后端目录

扩容失败

SC 不允许扩容 / 驱动不支持

检查 allowVolumeExpansion 与 CSI 能力

写入很慢

NFS 网络 / 后端磁盘瓶颈

测网络与 IOPS,考虑本地盘

2.7.11. 进阶挑战

  1. 创建一个 WaitForFirstConsumer 的本地 PV,观察 PVC 在 Pod 调度前一直 Pending。

  2. 把 PVC 从 2Gi 扩到 5Gi,验证文件系统是否真的变大。

  3. 用 Retain 策略删除 PVC,手工恢复 PV 并重新绑定。

  4. 用 kubectl get pv -o custom-columns 列出所有 PV 的回收策略与绑定状态。

  5. 给 StatefulSet 配 persistentVolumeClaimRetentionPolicy,观察缩容时 PVC 是否被删除。

2.7.12. 小结

  • 临时数据用 emptyDir,节点级数据用 hostPath,共享数据用 NFS/PVC。

  • PVC 是申请,PV 是资源,StorageClass 决定如何动态创建 PV。

  • RWO 是节点级限制;需要多节点读写时选 RWX。

  • 数据安全三件事:回收策略、备份、恢复演练。

2.7.13. 延伸阅读