路线图

存储 PV/PVC:容器数据持久化

星辉 2026-07-02 阅读 5 min 1,017 字 路线图
存储 PV/PVC:容器数据持久化 封面

概述#

在前面的章节中,我们学习的 Pod 都是"无状态"的——Pod 重启后,容器内的所有文件都会丢失。但现实世界的应用需要保存数据:

  • 数据库:PostgreSQL 的数据文件不能丢
  • 文件存储:用户上传的头像、文档需要持久保存
  • 日志:应用日志需要收集到外部存储

Kubernetes 用存储抽象解决这个问题,核心概念有三个:

  • PV(PersistentVolume):集群级别的存储资源(如 AWS EBS 卷、NFS 挂载点)
  • PVC(PersistentVolumeClaim):用户对存储的"申请单"("我要 50GB 存储空间")
  • StorageClass:存储的"模板",定义如何动态创建 PV

类比一下:

  • PV 是"房子"(实际的存储资源)
  • PVC 是"租房合同"(用户申请使用多大的房子)
  • StorageClass 是"房产开发商"(按需自动建房)

Volume 类型概览:从临时到持久#

Kubernetes 支持多种 Volume 类型,核心差异是数据生命周期共享性

对比表#

Volume 类型数据持久性跨 Pod 共享典型使用场景
emptyDir❌ Pod 删除后数据丢失❌ 同 Pod 内容器共享临时文件、容器间数据交换
hostPath⚠️ 依赖节点磁盘,Pod 重建可能丢失❌ 只能被同节点 Pod 访问开发测试、需要访问节点文件(如日志采集)
PVC(持久化)✅ 独立于 Pod 生命周期✅ 取决于访问模式数据库、用户上传文件、需要持久化的数据

emptyDir:临时共享存储#

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: myapp:v1.0
        volumeMounts:
        - name: tmp-data
          mountPath: /tmp/data
      - name: sidecar
        image: busybox
        command: ["/bin/sh", "-c", "sleep 3600"]
        volumeMounts:
        - name: tmp-data
          mountPath: /tmp/data
      volumes:
      - name: tmp-data
        emptyDir: {}

特点

  • Pod 被调度时创建 emptyDir,Pod 删除时清空
  • 同一个 Pod 内的多个容器可以共享这个 Volume
  • 存储介质:默认是磁盘,也可以设置为内存(medium: Memory),性能更好但容量受限

hostPath:节点本地映射#

yaml
apiVersion: apps/v1
kind: DaemonSet   # 通常用 DaemonSet 部署(每个节点一个 Pod)
metadata:
  name: log-collector
spec:
  template:
    spec:
      containers:
      - name: filebeat
        image: elastic/filebeat:8
        volumeMounts:
        - name: var-log
          mountPath: /host/var/log
      volumes:
      - name: var-log
        hostPath:
          path: /var/log        # 节点上的路径
          type: Directory      # 必须是目录,不存在会报错

风险

  • Pod 被调度到不同节点后,数据不在了(除非用 nodeName 固定节点)
  • 多个 Pod 写同一个 hostPath 会互相覆盖
  • 生产环境慎用,通常用于日志采集、监控采集等只读节点文件的场景

PVC:持久化存储(推荐)#

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-data
  namespace: production
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: gp3-retain
  resources:
    requests:
      storage: 50Gi

然后在 Pod 中使用:

yaml
spec:
  containers:
  - name: app
    image: myapp:v1.0
    volumeMounts:
    - name: data
      mountPath: /var/lib/myapp
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: my-data

特点

  • 数据独立于 Pod 生命周期,Pod 删除后 PVC 和底层存储(如 EBS 卷)仍然存在
  • 可以跨 Pod 共享(取决于 accessModes
  • 支持动态供给(不需要手动创建 PV)

PV/PVC/StorageClass 三层关系#

理解这三个概念的关系是掌握 K8s 存储的关键。

架构关系图#

text
用户/应用
    ↓ 创建 PVC(申请存储)
StorageClass(定义"怎么创建 PV")
    ↓ 自动触发(如果设置了 storageClassName)
PV(实际的存储资源)
    ↓ 绑定
PVC
    ↓ 被 Pod 通过 volume 引用
Pod

PV(PersistentVolume):集群的存储资源#

PV 是集群级别的资源(不属于任何 namespace),描述实际的存储。

yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-001
spec:
  capacity:
    storage: 50Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: gp3-retain
  csi:
    driver: ebs.csi.aws.com
    volumeHandle: vol-0123456789abcdef   # AWS EBS 卷 ID

关键点

  • PV 是集群级别资源(用 kubectl get pv,不带 -n
  • 通常由 CSI 驱动StorageClass 自动创建,不需要手动写这个 YAML
  • persistentVolumeReclaimPolicy:PVC 删除后 PV 怎么处理(Retain 保留,Delete 删除)

PVC(PersistentVolumeClaim):用户的存储申请#

PVC 是命名空间级别的资源,用户通过创建 PVC 来"申请"存储。

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-data
  namespace: production    # PVC 属于某个 namespace
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: gp3-retain
  resources:
    requests:
      storage: 50Gi

绑定流程

  1. 用户创建 PVC
  2. K8s 查找是否有满足条件的可用 PV(静态供给),或者触发 StorageClass 创建新 PV(动态供给)
  3. PVC 和 PV 绑定(一对一关系)
  4. Pod 引用 PVC,就能读写对应的存储

StorageClass:存储的"模板"和"自动供给器"#

StorageClass 定义"用什么方式创建存储"、"创建什么样的存储"。

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gp3-retain
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"   # 设为默认
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
  encrypted: "true"
volumeBindingMode: WaitForFirstConsumer   # 多 AZ 必须用这个
reclaimPolicy: Retain                   # 重要数据保留
allowVolumeExpansion: true              # 允许 PVC 扩容

关键参数

参数含义推荐值
provisioner使用哪个 CSI 驱动创建存储ebs.csi.aws.com(AWS)
volumeBindingMode什么时候创建 PVWaitForFirstConsumer(多 AZ 集群必须)
reclaimPolicyPVC 删除后 PV 怎么处理Retain(重要数据)
allowVolumeExpansion是否允许 PVC 扩容true

完整流程示例:动态供给#

第一步:管理员创建 StorageClass(只需做一次)

yaml
# storageclass-gp3.yaml(如上例)

第二步:用户创建 PVC

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-data
  namespace: production
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: gp3-retain   # 引用 StorageClass
  resources:
    requests:
      storage: 50Gi

第三步:K8s 自动创建 PV(CSI 驱动负责调用云厂商 API 创建 EBS 卷)

bash
# 查看自动创建的 PV
kubectl get pv
# NAME        CAPACITY   ACCESS MODES   RECLAIMPOLICY   STATUS   CLAIM                      STORAGECLASS
# pvc-abc12   50Gi        RWO            Retain          Bound    production/my-data       gp3-retain

# 查看 PVC 已绑定
kubectl get pvc -n production
# NAME      STATUS   VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS
# my-data   Bound    pvc-abc12   50Gi       RWO            gp3-retain

第四步:Pod 引用 PVC

yaml
spec:
  containers:
  - name: app
    image: myapp:v1.0
    volumeMounts:
    - name: data
      mountPath: /var/lib/myapp
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: my-data

访问模式:RWO、RWX、ROX 到底是什么?#

访问模式(accessModes)定义了这个存储可以被多少个节点同时访问

三种模式对比#

模式全称含义典型存储支持
RWOReadWriteOnce只能被一个节点读写(但同节点多个 Pod 可共享)AWS EBS、Azure Disk、本地磁盘
ROXReadOnlyMany可以被多个节点只读NFS、AWS EFS
RWXReadWriteMany可以被多个节点读写NFS、AWS EFS
RWOPReadWriteOncePod只能被一个 Pod读写(K8s 1.22+)EBS(最严格的隔离)

关键理解#

RWO 是"节点级别"的限制,不是"Pod 级别"

text
节点 A
  ├─ Pod 1(挂载 RWO PV)✅
  └─ Pod 2(同节点,挂载同一个 RWO PV)✅

节点 B
  └─ Pod 3(试图挂载同一个 RWO PV)❌ 不允许!

如果你需要严格的 Pod 级别独占(即使同节点也不允许其他 Pod 挂载),用 RWOP(K8s 1.22+)。

常见存储支持的访问模式#

存储RWOROXRWX典型使用场景
AWS EBS数据库(PostgreSQL/MySQL)
AWS EFS共享文件(用户上传、配置文件)
NFS自建共享存储
Ceph RBD私有云块存储
CephFS私有云文件存储

PVC 中的访问模式示例#

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-data
  namespace: production
spec:
  accessModes:
    - ReadWriteOnce      # 数据库用 RWO(EBS 只支持这个)
  storageClassName: gp3-retain
  resources:
    requests:
      storage: 100Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shared-files
  namespace: production
spec:
  accessModes:
    - ReadWriteMany      # 共享文件用 RWX(EFS 支持)
  storageClassName: efs-sc
  resources:
    requests:
      storage: 10Gi    # EFS 实际不限制大小,这个值只是声明

实战:StatefulSet 挂载 PVC#

Deployment 的 Pod 是"无状态"的(Pod 重建后名字会变,且没有固定的存储)。StatefulSet 是为"有状态"应用设计的,每个 Pod 有固定的名字和独立的 PVC

为什么需要 StatefulSet?#

特性DeploymentStatefulSet
Pod 命名随机后缀(my-app-xxxxxxxxxx固定序号(my-app-0, my-app-1
Pod 顺序无序有序部署/删除(0 → 1 → 2)
存储共享同一个 PVC每个 Pod 独立的 PVC(volumeClaimTemplates
网络Service 提供负载均衡每个 Pod 有固定的 DNS(pod-name.service-name.namespace.svc.cluster.local

volumeClaimTemplates:StatefulSet 的存储魔法#

volumeClaimTemplates 会为每个 Pod 自动创建独立的 PVC

yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgresql
  namespace: data
spec:
  serviceName: postgresql-headless   # 需要配合 Headless Service
  replicas: 3
  selector:
    matchLabels:
      app: postgresql
  template:
    metadata:
      labels:
        app: postgresql
    spec:
      containers:
      - name: postgresql
        image: postgres:15
        env:
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  # 关键:VolumeClaimTemplates
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: gp3-retain
      resources:
        requests:
          storage: 50Gi

自动创建的 PVC

text
data-postgresql-0    ← 给 postgresql-0 用
data-postgresql-1    ← 给 postgresql-1 用
data-postgresql-2    ← 给 postgresql-2 用

即使 Pod 被删除重建,PVC 仍然保留,新 Pod 会重新绑定到原来的 PVC,数据不丢失。

配套的 Headless Service#

StatefulSet 通常需要 Headless Service(ClusterIP 设为 None),为每个 Pod 提供固定的 DNS 记录。

yaml
apiVersion: v1
kind: Service
metadata:
  name: postgresql-headless
  namespace: data
spec:
  clusterIP: None   # Headless
  selector:
    app: postgresql
  ports:
  - port: 5432
    targetPort: 5432

DNS 解析结果

bash
nslookup postgresql-headless.data.svc.cluster.local
# 返回:
# postgresql-0.postgresql-headless.data.svc.cluster.local
# postgresql-1.postgresql-headless.data.svc.cluster.local
# postgresql-2.postgresql-headless.data.svc.cluster.local

每个 Pod 有固定的 DNS 名称,适合数据库集群、消息队列等有状态应用。

StatefulSet 扩缩容的存储行为#

操作PVC 行为
扩容(replicas: 3 → 5)自动创建 data-postgresql-3data-postgresql-4
缩容(replicas: 5 → 2)data-postgresql-3data-postgresql-4 不会自动删除(保护机制)

手动清理残留 PVC

bash
# 查看残留 PVC
kubectl get pvc -n data -l app=postgresql

# 确认数据已备份后再删除
kubectl delete pvc data-postgresql-3 data-postgresql-4 -n data

小结#

本章我们学习了 Kubernetes 的存储体系:

  1. Volume 类型选择

    • 临时数据/容器间共享 → emptyDir
    • 需要持久化 → PVC(配合 PV 或 StorageClass)
    • ⚠️ 避免用 hostPath(生产环境有数据丢失风险)
  2. PV/PVC/StorageClass 三层关系

    • PV:集群的存储资源(如 EBS 卷)
    • PVC:用户的存储申请单
    • StorageClass:自动供给器(动态创建 PV)
    • 生产环境推荐动态供给(不需要手动创建 PV)
  3. 访问模式选择

    • 数据库(EBS)→ RWO(单节点读写)
    • 共享文件(EFS/NFS)→ RWX(多节点读写)
    • 严格 Pod 独占(K8s 1.22+)→ RWOP
  4. StatefulSet 存储管理

    • volumeClaimTemplates 为每个 Pod 自动创建独立的 PVC
    • 配套的 Headless Service 为每个 Pod 提供固定 DNS
    • 缩容后 PVC 不会自动删除,需要手动清理

下一步:我们已经学习了 K8s 的核心概念(Pod、Deployment、Service、Ingress、网络、配置、存储),下一章将进入生产级实践——怎么用这些知识搭建一个高可用的生产集群。


实战要点

  • 生产数据用 Retain 策略:宁可手动清理,不要让 PVC 删除导致数据丢失
  • 多 AZ 集群必须用 WaitForFirstConsumer:避免 EBS 跨 AZ 挂载失败
  • StatefulSet 缩容后要检查残留 PVC:避免存储浪费和意外计费
  • PVC 扩容前提:StorageClass 开启 allowVolumeExpansion: true,且底层存储支持在线扩容
  • 不能缩容 PVC:K8s 只支持扩容,不支持缩容(需要备份数据后重建)