容器是“用完即扔”的,但数据不是。如果把 MySQL 直接跑在容器里,容器一删数据就没了,这显然不行。K8s 怎么管理存储?PV、PVC、StorageClass 这些都是什么关系?这篇文章把这些概念一次讲清楚。
1、容器存储的根本问题
容器的文件系统是临时的(ephemeral)。还记得第二篇讲的分层原理吗?容器有一个可读写层,但容器的可读写层和容器的生命周期绑定——容器一删,可读写层就没了。
┌──────────────────┐
│ Container R/W │ ← 容器删了,这里的内容就没了!
├──────────────────┤
│ App Layer │
├──────────────────┤
│ Base Layer │
└──────────────────┘
那数据放哪?答案是让数据脱离容器的生命周期,独立存在。 这就是 Volume(存储卷)的职责。
2、Volume —— 把数据“挂”进容器
K8s 的 Volume 是把外部存储挂载到 Pod 的容器里,使得容器可以像访问本地目录一样访问外部数据。
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
spec:
containers:
- name: myapp
image: myapp:v1
volumeMounts:
- name: app-data # 要挂载哪个卷
mountPath: /data # 挂载到容器里的哪个路径
volumes:
- name: app-data
hostPath: # 卷的类型
path: /mnt/data # 宿主机上的路径
这个例子用 hostPath 把宿主机的 /mnt/data 目录挂载到了容器的 /data 下。Pod 删了数据还在宿主机上。
常见的 Volume 类型
K8s 支持很多种 Volume 类型,下面是其中最常用的几种:
| Volume 类型 | 用途 | 数据生命周期 |
|---|---|---|
emptyDir |
Pod 内临时数据,容器间共享 | Pod 删除时清除 |
hostPath |
挂载宿主机目录 | 持久化,但和 Node 绑定 |
configMap |
挂载配置文件 | 跟随 ConfigMap |
secret |
挂载敏感数据(密码、证书) | 跟随 Secret |
nfs |
挂载 NFS 共享存储 | 独立于 Pod 和 Node |
persistentVolumeClaim |
动态/静态申请持久化卷 | 独立于 Pod |
hostPath 适合单节点开发测试,但不适合生产——你的 Pod 可能被调度到任意 Node 上,不能假设某个 Node 上一定有你要的数据。
3、PersistentVolume(PV)—— 集群级别的存储
Volume 是 Pod 级别的,定义在每个 Pod 的 YAML 里。如果每个开发者都要在 YAML 里写清楚存储的细节(NFS 地址、云盘 ID 等),这就让应用和基础设施耦合了。
K8s 的解决方案是把存储的供应和存储的消费分离开。
PersistentVolume(PV)是管理员预先准备的一块存储,就像管理员在集群里插了一块“存储 U 盘”:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-10g
spec:
capacity:
storage: 10Gi # 存储容量
accessModes:
- ReadWriteOnce # 访问模式
persistentVolumeReclaimPolicy: Retain
nfs: # 存储后端
server: 192.168.1.100
path: /nfs/data
关键字段解释:
- capacity.storage:这块 PV 的大小。
- accessModes:访问模式(见下表)。
- persistentVolumeReclaimPolicy:PVC 被删除后 PV 怎么处理(Retain 保留、Delete 删除、Recycle 清空后重用)。
- 存储后端:可以是 NFS、AWS EBS、GCE PD、Ceph、GlusterFS、本地磁盘等。
访问模式
| 模式 | 缩写 | 含义 |
|---|---|---|
| ReadWriteOnce | RWO | 单节点读写(最常用) |
| ReadOnlyMany | ROX | 多节点只读 |
| ReadWriteMany | RWX | 多节点读写(需要分布式文件系统) |
大部分应用只需要 RWO。只有像多个 Pod 需要同时写入同一个共享文件系统时才需要 RWX。
4、PersistentVolumeClaim(PVC)—— 消费端
有了 PV(供应端),应用开发者不需要关心存储的底层细节,只需要声明“我要多大、什么访问模式”的存储。这就是 PersistentVolumeClaim(PVC):
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myapp-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
PVC 创建后,K8s 自动寻找一个满足条件的 PV 并绑定。绑定的逻辑是:
- PV 的容量 ≥ PVC 的请求。
- PV 的访问模式包含 PVC 要求的模式。
- PV 的存储类别(StorageClass)和 PVC 匹配(如果指定了的话)。
一旦绑定,这个 PV 就归这个 PVC 独占了,别的 PVC 不能再绑。
在 Pod 中使用 PVC
apiVersion: v1
kind: Pod
metadata:
name: mysql-pod
spec:
containers:
- name: mysql
image: mysql:5.7
env:
- name: MYSQL_ROOT_PASSWORD
value: "root123"
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql # MySQL 数据目录
volumes:
- name: mysql-data
persistentVolumeClaim:
claimName: myapp-pvc # 引用 PVC
无论 Pod 被调度到哪个 Node 上,MySQL 的数据都持久化在 PV 里。Pod 重建了,重新挂上同一个 PVC,数据完好无损。
PV 与 PVC 的生命周期
管理员创建 PV → 开发者创建 PVC → K8s 自动绑定
↓
Pod 使用 PVC
↓
Pod 删除(数据还在)
↓
PVC 删除
↓
根据 ReclaimPolicy 处理 PV
(Retain: PV 保留 / Delete: PV 删除)
5、StorageClass —— 动态存储供应
手动创建 PV 实在太麻烦了。每个应用都要管理员提前创建好 PV,容量算多了浪费,算少了不够用。StorageClass 实现了 PV 的动态创建——开发者创建 PVC 时,K8s 自动按需创建 PV。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/aws-ebs # 存储供应者
parameters:
type: gp3 # SSD 类型
fsType: ext4
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer # 延迟绑定
有了 StorageClass,PVC 只需要指定 storageClassName:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dynamic-pvc
spec:
storageClassName: fast-ssd # 引用 StorageClass
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
K8s 看到这个 PVC:
- 找到
storageClassName对应的 StorageClass。 - 调用
provisioner(比如 AWS EBS)创建一块 20Gi 的 SSD 云盘。 - 自动创建一个 PV 并绑定到这个 PVC 上。
- Pod 挂载这个 PVC 时,云盘自动 attach 到对应的 Node 上。
一切全自动,无需管理员手动介入。
volumeBindingMode 的选择
| 模式 | 含义 |
|---|---|
Immediate(默认) |
PVC 一创建就立刻创建 PV 并绑定 |
WaitForFirstConsumer |
等到 Pod 真正要用 PVC 时才创建 PV |
WaitForFirstConsumer 更推荐——因为它等 Scheduler 选定了 Node 之后才创建 PV,可以避免 Pod 需要的存储不被调度目标 Node 支持的情况。比如 AWS EBS 是 AZ 级别的,如果提前创建 PV 在 us-east-1a,但 Pod 被调度到了 us-east-1b,就挂不上了。
6、实际案例:在 K8s 中部署持久化的 MySQL
以下是一个生产可用的 MySQL Deployment + PVC 示例:
# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
---
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
replicas: 1 # 注意:MySQL 通常单副本,不能用 Deployment 扩缩
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:5.7
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef: # 密码不要写死,从 Secret 读取
name: mysql-secret
key: root-password
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
volumes:
- name: mysql-data
persistentVolumeClaim:
claimName: mysql-pvc
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
selector:
app: mysql
ports:
- port: 3306
targetPort: 3306
关键点:
- MySQL 通常用
replicas: 1,多副本需要额外的集群方案(如 Galera Cluster)。 - 密码通过 Secret 注入,不打在 YAML 里。
- 数据目录挂载了 PVC,Pod 重建不丢数据。
小结
K8s 存储的核心逻辑是供应和消费的解耦:管理员通过 PV/StorageClass 供应存储,开发者通过 PVC 消费存储。StorageClass 的动态供应机制让这个过程全自动,真正做到了声明式管理。
数据不会丢了,但应用跑起来还得保证健康。下一篇,我们进入生产部署的最后一环——健康检查与零停机部署。
每天前进一小步,就是一个新的高度!