存储 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:临时共享存储#
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:节点本地映射#
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:持久化存储(推荐)#
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-data
namespace: production
spec:
accessModes:
- ReadWriteOnce
storageClassName: gp3-retain
resources:
requests:
storage: 50Gi然后在 Pod 中使用:
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 存储的关键。
架构关系图#
用户/应用
↓ 创建 PVC(申请存储)
StorageClass(定义"怎么创建 PV")
↓ 自动触发(如果设置了 storageClassName)
PV(实际的存储资源)
↓ 绑定
PVC
↓ 被 Pod 通过 volume 引用
PodPV(PersistentVolume):集群的存储资源#
PV 是集群级别的资源(不属于任何 namespace),描述实际的存储。
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 来"申请"存储。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-data
namespace: production # PVC 属于某个 namespace
spec:
accessModes:
- ReadWriteOnce
storageClassName: gp3-retain
resources:
requests:
storage: 50Gi绑定流程:
- 用户创建 PVC
- K8s 查找是否有满足条件的可用 PV(静态供给),或者触发 StorageClass 创建新 PV(动态供给)
- PVC 和 PV 绑定(一对一关系)
- Pod 引用 PVC,就能读写对应的存储
StorageClass:存储的"模板"和"自动供给器"#
StorageClass 定义"用什么方式创建存储"、"创建什么样的存储"。
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 | 什么时候创建 PV | WaitForFirstConsumer(多 AZ 集群必须) |
reclaimPolicy | PVC 删除后 PV 怎么处理 | Retain(重要数据) |
allowVolumeExpansion | 是否允许 PVC 扩容 | true |
完整流程示例:动态供给#
第一步:管理员创建 StorageClass(只需做一次)
# storageclass-gp3.yaml(如上例)第二步:用户创建 PVC
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 卷)
# 查看自动创建的 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
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)定义了这个存储可以被多少个节点同时访问。
三种模式对比#
| 模式 | 全称 | 含义 | 典型存储支持 |
|---|---|---|---|
| RWO | ReadWriteOnce | 只能被一个节点读写(但同节点多个 Pod 可共享) | AWS EBS、Azure Disk、本地磁盘 |
| ROX | ReadOnlyMany | 可以被多个节点只读 | NFS、AWS EFS |
| RWX | ReadWriteMany | 可以被多个节点读写 | NFS、AWS EFS |
| RWOP | ReadWriteOncePod | 只能被一个 Pod读写(K8s 1.22+) | EBS(最严格的隔离) |
关键理解#
RWO 是"节点级别"的限制,不是"Pod 级别":
节点 A
├─ Pod 1(挂载 RWO PV)✅
└─ Pod 2(同节点,挂载同一个 RWO PV)✅
节点 B
└─ Pod 3(试图挂载同一个 RWO PV)❌ 不允许!如果你需要严格的 Pod 级别独占(即使同节点也不允许其他 Pod 挂载),用 RWOP(K8s 1.22+)。
常见存储支持的访问模式#
| 存储 | RWO | ROX | RWX | 典型使用场景 |
|---|---|---|---|---|
| AWS EBS | ✅ | ❌ | ❌ | 数据库(PostgreSQL/MySQL) |
| AWS EFS | ✅ | ✅ | ✅ | 共享文件(用户上传、配置文件) |
| NFS | ✅ | ✅ | ✅ | 自建共享存储 |
| Ceph RBD | ✅ | ❌ | ❌ | 私有云块存储 |
| CephFS | ✅ | ✅ | ✅ | 私有云文件存储 |
PVC 中的访问模式示例#
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?#
| 特性 | Deployment | StatefulSet |
|---|---|---|
| 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。
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:
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 记录。
apiVersion: v1
kind: Service
metadata:
name: postgresql-headless
namespace: data
spec:
clusterIP: None # Headless
selector:
app: postgresql
ports:
- port: 5432
targetPort: 5432DNS 解析结果:
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-3 和 data-postgresql-4 |
| 缩容(replicas: 5 → 2) | data-postgresql-3 和 data-postgresql-4 不会自动删除(保护机制) |
手动清理残留 PVC:
# 查看残留 PVC
kubectl get pvc -n data -l app=postgresql
# 确认数据已备份后再删除
kubectl delete pvc data-postgresql-3 data-postgresql-4 -n data小结#
本章我们学习了 Kubernetes 的存储体系:
Volume 类型选择:
- 临时数据/容器间共享 → emptyDir
- 需要持久化 → PVC(配合 PV 或 StorageClass)
- ⚠️ 避免用
hostPath(生产环境有数据丢失风险)
PV/PVC/StorageClass 三层关系:
- PV:集群的存储资源(如 EBS 卷)
- PVC:用户的存储申请单
- StorageClass:自动供给器(动态创建 PV)
- 生产环境推荐动态供给(不需要手动创建 PV)
访问模式选择:
- 数据库(EBS)→ RWO(单节点读写)
- 共享文件(EFS/NFS)→ RWX(多节点读写)
- 严格 Pod 独占(K8s 1.22+)→ RWOP
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 只支持扩容,不支持缩容(需要备份数据后重建)