跳转至

存储与持久化

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

难度 · 进阶 预计 55 分钟 前置:Pod、工作负载

你将学到

  • emptyDir、hostPath、NFS、PVC 的适用场景与边界
  • PV / PVC / StorageClass 的绑定流程
  • 静态供给与动态供给的差异
  • 访问模式、回收策略、绑定模式的实际含义
  • StatefulSet 的卷模板与数据保留
  • PVC 一直 Pending 的排查路径

为什么需要卷

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

存储 生命周期 适合
容器文件系统 容器重启即丢 临时文件
emptyDir Pod 删除即丢 容器间共享、缓存
hostPath 跟节点走 节点级代理、实验
NFS 独立于 Pod 多 Pod 共享
PVC 独立于 Pod,可动态供给 数据库、业务数据
flowchart LR
  P[Pod] --> V[Volume]
  V --> E[emptyDir]
  V --> H[hostPath]
  V --> N[NFS]
  V --> PVC[PVC]
  PVC -->|绑定| PV[PV]
  PV --> SC[StorageClass]
  SC -->|动态创建| CSI[CSI Driver]
  CSI --> D[(云盘 / 本地盘 / NFS)]

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

临时卷

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 防止写满节点。

hostPath

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

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

风险:

  • Pod 漂移到别的节点后,看不到原数据;
  • 容器可能读写节点敏感目录;
  • 多 Pod 并发写同一目录容易冲突。

PV / PVC / StorageClass

三个角色

对象 谁创建 作用
PV 管理员 / 动态供给 一块实际存储资源
PVC 应用开发者 对存储的申请(大小、访问模式)
StorageClass 管理员 定义「如何动态创建 PV」

绑定流程

  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>

访问模式

模式 缩写 含义
ReadWriteOnce RWO 单节点读写
ReadOnlyMany ROX 多节点只读
ReadWriteMany RWX 多节点读写
ReadWriteOncePod RWOP 单 Pod 读写(较新)

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

回收策略

策略 行为
Delete 删除 PVC 时删除后端存储(动态供给默认)
Retain 保留 PV 与数据,需手工清理

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

绑定模式

volumeBindingMode: WaitForFirstConsumer
  • Immediate:PVC 创建后立即绑定,可能绑到不满足 Pod 调度条件的节点;
  • WaitForFirstConsumer:等 Pod 调度后再绑定,适合本地盘和拓扑受限的存储。

静态供给: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

动态供给: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 时使用默认值。

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

扩容与快照

扩容

前提:StorageClass 设置 allowVolumeExpansion: true。

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

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

快照

需要 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 的快照能力取决于后端;块存储通常支持得更完整。

实验:动态 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。

排错表

现象 原因 处理
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,考虑本地盘

进阶挑战

  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 是否被删除。

小结

  • 临时数据用 emptyDir,节点级数据用 hostPath,共享数据用 NFS/PVC。
  • PVC 是申请,PV 是资源,StorageClass 决定如何动态创建 PV。
  • RWO 是节点级限制;需要多节点读写时选 RWX。
  • 数据安全三件事:回收策略、备份、恢复演练。

延伸阅读