前言
前面整理 Pod 时,我反复提醒自己:Pod 是可以被删除和替换的。
这句话放到应用进程上还好理解,Pod 没了再拉一个。但如果应用在容器里写了数据,问题就来了:Pod 重建后,数据还在吗?
如果数据只写在容器自己的可写层里,大概率会跟着容器一起消失。
1 | Container 写入本地文件 |
所以这一篇要看的就是 Kubernetes 里的存储抽象:
1 | Volume:给 Pod 挂一块存储 |
我一开始觉得 PV、PVC 这两个名字有点绕,后来把它们理解成“仓库”和“领用单”之后顺了很多。
容器文件系统为什么不够用
容器启动后,看起来也有自己的文件系统:
1 | /app |
应用当然可以往里面写文件。但这层文件系统通常跟容器生命周期绑得很紧。
1 | Pod |
如果容器重启,某些数据可能还在;但如果 Pod 被删除、重新调度或替换,就不能指望这些数据一直存在。
这对无状态服务没什么问题。比如 Web API 只处理请求,状态都在数据库里,Pod 被替换也没关系。
但有些数据需要更明确地保存:
- 应用上传的文件;
- 数据库数据目录;
- 需要跨容器共享的临时文件;
- 需要跟 Pod 生命周期解耦的缓存或工作目录。
于是 Kubernetes 提供了 Volume 这层抽象。
Volume:Pod 里的挂载点
Volume 最基本的作用是把一块存储挂到 Pod 里的容器。
1 | Pod |
同一个 Pod 里的多个容器可以挂载同一个 Volume。上一篇 Pod 里提到的 emptyDir 就是一个简单 Volume。
1 | apiVersion: v1 |
这里的关系是:
1 | volumes: |
我之前经常把这两个字段混在一起。现在这样区分:
1 | volumes |
emptyDir:跟着 Pod 走的临时目录
emptyDir 会在 Pod 被分配到节点后创建,Pod 删除时一起删除。
1 | Pod 创建 |
它适合:
- 同一个 Pod 内多个容器共享文件;
- 临时工作目录;
- 计算过程中的中间文件;
- 不需要跨 Pod 保留的数据。
它不适合:
- 数据库持久化目录;
- 用户上传文件;
- Pod 删除后还要保留的数据。
所以 emptyDir 虽然名字里有 Volume,但它不是“永久硬盘”。它更像是给这个 Pod 临时分了一张桌子,Pod 走了,桌子也收走。
为什么还需要 PV 和 PVC
Volume 能解决“容器怎么挂载存储”,但还没解决“持久存储从哪里来”。
如果每个应用都直接写底层存储细节,Pod YAML 会很难维护:
1 | 应用开发者需要知道: |
这明显不太合理。应用更关心的是:
1 | 我需要一块 10Gi 的存储 |
于是 Kubernetes 把持久存储拆成了两个对象:
1 | PersistentVolume PV |
可以用仓库类比:
1 | PV:仓库里真实存在的一块货位 |
关系大致是:
1 | Pod |
PVC:应用怎么申请存储
应用侧通常先写 PVC。
1 | apiVersion: v1 |
这份声明可以翻译成:
1 | 我需要一块 10Gi 的持久存储。 |
应用并没有直接说这块存储来自哪台机器、哪块云盘或哪个 NFS 路径。
这就是 PVC 的价值:应用表达需求,而不是直接绑定底层实现。
Pod 如何使用 PVC
有了 PVC 后,Pod 可以把它当成 Volume 使用。
1 | apiVersion: apps/v1 |
这里依然是两层:
1 | volumes |
完整关系:
1 | Deployment |
应用只知道 /data 可以读写,不需要知道背后是云盘、NFS 还是别的存储系统。
PV:集群里的真实存储资源
PV 是集群级别的持久存储资源。它可以由管理员提前创建,也可以通过 StorageClass 动态创建。
一个静态 PV 示例:
1 | apiVersion: v1 |
这里用 hostPath 只是为了说明结构。真实多节点集群里,hostPath 通常不适合作为通用持久化方案,因为它绑定在某个节点本地路径上。
PV 更像集群管理员准备的一块存储资源:
1 | PV |
Pod 一般不直接引用 PV,而是通过 PVC 间接使用。
静态供给和动态供给
PV 的准备方式大致有两类。
静态供给:
1 | 管理员先创建 PV |
动态供给:
1 | 用户创建 PVC |
动态供给在云环境或成熟集群里更常见。开发者只写 PVC,底层根据 StorageClass 自动创建对应存储。
一个带 StorageClass 的 PVC:
1 | apiVersion: v1 |
可以理解成:
1 | 我需要 10Gi 存储 |
fast 背后到底是 SSD 云盘、某个 CSI 驱动,还是其他存储实现,由集群管理员配置。
访问模式怎么理解
PVC 和 PV 里经常看到 accessModes。
常见几类:
ReadWriteOnce:可以被一个节点以读写方式挂载;ReadOnlyMany:可以被多个节点以只读方式挂载;ReadWriteMany:可以被多个节点以读写方式挂载;ReadWriteOncePod:只能被一个 Pod 以读写方式挂载。
这里的“Once”很容易被误解成“只能一个 Pod”。更准确地说,ReadWriteOnce 关注的是节点挂载能力。某些情况下,同一节点上的多个 Pod 可能共享这块卷,但跨节点就不一定行。
可以先简化理解:
1 | RWO |
但最终能不能这么用,还要看具体存储插件是否支持。
回收策略:PVC 删除后 PV 怎么办
PV 有一个重要字段:persistentVolumeReclaimPolicy。
常见策略:
Retain:PVC 删除后,PV 和数据保留,需要人工处理;Delete:PVC 删除后,底层存储也可能被删除;Recycle:旧策略,基本不建议继续使用。
这影响很大。
1 | PVC 删除 |
如果是生产数据,我会非常谨慎地看这个策略。因为删 PVC 和删数据不是一回事,但在某些动态供给场景下,Delete 策略可能真的会清理底层存储。
Deployment 和持久存储要小心
无状态服务很适合 Deployment,但有状态服务就要更谨慎。
假设一个 Deployment 有 3 个副本,却都挂同一个只能单节点读写的 PVC:
1 | Deployment replicas: 3 |
这可能会遇到调度、挂载或数据一致性问题。
对数据库这类服务,更常见的是 StatefulSet:
1 | StatefulSet |
这篇先不展开 StatefulSet,但需要先建立一个边界:PVC 能给 Pod 提供持久存储,不代表所有有状态应用都应该直接用 Deployment 承载。
ConfigMap、Secret 也是 Volume 吗
上一篇说 ConfigMap 和 Secret 可以挂载成文件。它们在 Pod 里也会以 Volume 的形式出现。
1 | volumes: |
所以 Volume 不只表示磁盘,也可以表示很多“挂进容器文件系统的东西”:
1 | Volume |
这也是 Kubernetes 抽象比较统一的地方:容器看到的是文件路径,背后来源可以完全不同。
我现在的判断方式
遇到存储需求时,我会先问几个问题。
第一,数据要不要在 Pod 删除后保留?
1 | 不需要 -> emptyDir |
第二,数据是配置还是业务数据?
1 | 普通配置 -> ConfigMap |
第三,是否需要多个 Pod 同时读写?
1 | 单实例读写 -> RWO 可能够用 |
第四,删除 PVC 时数据应该怎么处理?
1 | 需要人工确认 -> Retain |
这几个问题比一上来背所有 Volume 类型更实用。
小结
这一篇整理下来,我对 Kubernetes 存储的理解可以概括成几句话:
- 容器可写层不适合作为长期数据保存位置;
- Volume 是 Pod 里的挂载抽象;
emptyDir适合临时数据,Pod 删除后也会删除;- PVC 是应用对持久存储的申请;
- PV 是集群里的持久存储资源;
- Pod 通常通过 PVC 间接使用 PV;
- StorageClass 可以让 PVC 触发动态供给;
- 访问模式和回收策略会直接影响可用性和数据安全;
- 有状态应用不只是“挂个 PVC”这么简单,后面还要看 StatefulSet。
下一篇准备整理 Namespace。前面这些对象越来越多,Namespace 要解决的是“资源怎么分组、怎么隔离、怎么让同名应用存在于不同环境中”。
参考资料
- Volumes:https://kubernetes.io/docs/concepts/storage/volumes/
- Persistent Volumes:https://kubernetes.io/docs/concepts/storage/persistent-volumes/
- Storage Classes:https://kubernetes.io/docs/concepts/storage/storage-classes/
- Dynamic Volume Provisioning:https://kubernetes.io/docs/concepts/storage/dynamic-provisioning/