上一章讲了 ConfigMap 和 Secret。本章讲存储。容器本身是无状态的,但业务需要持久化数据,Volume 就是解决这个问题的。
8.1 为什么容器需要 Volume
Docker 容器默认是可写的,但容器删除后,所有数据都会丢失。这叫容器存储层的短暂性。K8s 的 Volume 机制让 Pod 可以访问持久化存储,即使 Pod 被调度到不同节点,数据仍然可以保留。
K8s 里的 Volume 分为两大类:
- 临时存储:和 Pod 生命周期绑定,Pod 删了就消失。如 EmptyDir。
- 持久化存储:独立于 Pod 生命周期,Pod 删了数据还在。如 PV、PVC。
8.2 EmptyDir:和 Pod 同生共死
EmptyDir 是最简单的 Volume 类型。Pod 创建时分配一个空目录,Pod 内所有容器可以共享读写。Pod 删除后,目录里的数据也删除。
典型用法:
- 主容器写日志,sidecar 读取后上传
- 两个容器共享临时数据
- 缓存目录
apiVersion: v1
kind: Pod
metadata:
name: share-cache
spec:
containers:
- name: writer
image: busybox:1.28
command: ["sh", "-c", "while true; do echo $(date) >> /cache/log.txt; sleep 5; done"]
volumeMounts:
- name: cache
mountPath: /cache
- name: reader
image: busybox:1.28
command: ["sh", "-c", "while true; do cat /cache/log.txt; sleep 5; done"]
volumeMounts:
- name: cache
mountPath: /cache
volumes:
- name: cache
emptyDir: {}
创建后,两个容器共享 /cache 目录。writer 写入文件,reader 读取文件。
emptyDir 也可以指定介质类型:
volumes:
- name: cache
emptyDir:
medium: Memory
medium: Memory 表示用内存做临时文件系统,性能高,但 Pod 删除数据丢失,适合临时缓存。
8.3 HostPath:生产环境的坑
HostPath 把节点上的文件或目录挂载到 Pod 里。用法简单,但问题很多:
volumes:
- name: log
hostPath:
path: /var/log/myapp
type: DirectoryOrCreate
问题:
- 多节点不一致:Pod 在
k8s-node1上写/var/log/myapp,重启后被调度到k8s-node2,数据就找不到了 - 安全风险:Pod 可以读取节点上的敏感路径,如
/etc/kubernetes - 调度依赖:Pod 被绑定到特定节点,K8s 的调度优势被浪费
HostPath 只适合:
- 单节点测试环境
- 需要访问节点特定设备的场景(如监控 agent 读取 /proc)
- 节点本地日志收集
生产环境的多节点集群里,HostPath 是反模式。应该用 PV/PVC。
8.4 PV、PVC、StorageClass 的关系
这是 K8s 持久化存储最核心的三个对象:
8.4.1 PV(PersistentVolume)
PV 是集群里一块真实的存储资源,由管理员预先创建,或由 StorageClass 动态分配。它代表一块磁盘、一个 NFS 目录、一个 iSCSI LUN 等。
8.4.2 PVC(PersistentVolumeClaim)
PVC 是用户申请存储的声明。用户说"我要 5Gi 的 ReadWriteOnce 存储",K8s 会找到合适的 PV 绑定,或者通过 StorageClass 动态创建 PV。
8.4.3 StorageClass
StorageClass 定义存储的"类型"和"分配方式"。它指定了 provisioner(谁来创建存储),比如 local-path、NFS、AWS EBS 等。
用户创建 PVC → StorageClass 通知 provisioner → provisioner 创建 PV → PV 与 PVC 绑定 → Pod 挂载 PVC
| 对象 | 谁创建 | 作用 |
|---|---|---|
| PV | 管理员或 provisioner | 真实存储 |
| PVC | 用户 | 申请存储 |
| StorageClass | 管理员 | 定义动态分配策略 |
8.5 动态供给与回收策略
8.5.1 动态供给
StorageClass 的核心价值是动态供给。用户提交 PVC 时,如果没有匹配的 PV,StorageClass 会自动创建一个。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
volumeBindingMode: WaitForFirstConsumer 表示等 Pod 调度后再创建 PV,这样可以避免 PV 和 Pod 不在同一个可用区的问题。
8.5.2 回收策略
PV 的回收策略决定 PVC 删除后,PV 怎么处理:
| 策略 | 含义 |
|---|---|
| Retain | PVC 删除后,PV 保留,数据还在,需要管理员手动清理 |
| Delete | PVC 删除后,PV 自动删除,数据清空 |
| Recycle | deprecated,不建议用 |
生产环境默认 Delete 省事,但重要数据建议用 Retain,避免误删。
8.6 ReadWriteOnce vs ReadWriteMany
PVC 需要声明访问模式:
| 访问模式 | 说明 |
|---|---|
| ReadWriteOnce(RWO) | 只能被一个节点挂载为读写 |
| ReadOnlyMany(ROX) | 可以被多个节点挂载为只读 |
| ReadWriteMany(RWX) | 可以被多个节点同时挂载为读写 |
| ReadWriteOncePod | 只能被一个 Pod 挂载为读写(K8s 1.27+) |
单节点数据库用 RWO,多个副本需要共享存储的场景用 RWX。例如 NFS 支持 RWX,本地磁盘通常只支持 RWO。
8.7 local-path-provisioner 实战
我们的 3 节点集群没有云厂商存储,可以用 local-path-provisioner 来模拟动态供给。它会根据 PVC 在节点本地创建目录,并分配 PV。
安装:
$ kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.24/deploy/local-path-provisioner.yaml
查看默认 StorageClass:
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer
创建 PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: web-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 1Gi
把 PVC 挂载到 Pod:
apiVersion: v1
kind: Pod
metadata:
name: web-with-pvc
spec:
containers:
- name: nginx
image: nginx:alpine
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:
claimName: web-pvc
创建后验证:
$ kubectl apply -f pvc-pod.yaml
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY STORAGECLASS
web-pvc Bound pvc-7d4f8c2b-... 1Gi local-path
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY
pvc-7d4f8c2b-... 1Gi RWO Delete
$ kubectl exec -it web-with-pvc -- sh -c "echo hello > /usr/share/nginx/html/index.html"
$ kubectl delete pod web-with-pvc
$ kubectl apply -f pvc-pod.yaml
$ kubectl exec -it web-with-pvc -- cat /usr/share/nginx/html/index.html
hello
数据保留了下来。
8.8 完整 YAML 示例:Deployment + PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 1
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: nginx:alpine
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:
claimName: app-pvc
注意:Deployment 副本数为 1,因为 PVC 是 RWO。如果副本数大于 1,多个 Pod 可能调度到不同节点,无法共享同一个 RWO PVC。
8.9 本章小结
- Volume 解决容器数据持久化问题
- EmptyDir 适合临时共享存储,生命周期和 Pod 一致
- HostPath 在多节点生产环境是反模式,除非特殊场景
- PV 是真实存储,PVC 是用户申请,StorageClass 控制动态分配
- 动态供给和回收策略是生产存储的核心配置
- 访问模式 RWO/RWO/RWX 要根据应用选择
- local-path-provisioner 适合裸金属或本地测试环境
8.10 课后练习
- 创建一个 EmptyDir Pod,让两个容器共享文件,验证数据随 Pod 删除而消失。
- 安装 local-path-provisioner,查看默认 StorageClass 和 provisioner。
- 创建 PVC 并挂载到 Pod,写入数据后删除重建 Pod,验证数据持久化。
- 尝试把 Deployment 副本数改为 2,挂载同一个 RWO PVC,观察报错并理解原因。