系列专栏《深入剖析 Kubernetes》· 基于张磊极客时间专栏思想整理 · 适合 CSDN 发布
09 | 持久化存储:PV、PVC 与 StorageClass
容器本身是"用完即焚"的——Pod 里的文件系统随容器生命周期而生灭。但数据库、日志、用户上传文件这些必须跨重启存活的数据,就需要持久化存储。K8s 用一套"解耦"的设计把这件事做得很优雅:核心就是PV、PVC、StorageClass三个对象。
本文回答三个问题:为什么要解耦、怎么申请存储、存储从哪来(静态 vs 动态)。
一、PV / PVC 解耦"存储"与"使用"
想象一个没有抽象的世界:开发者写 Pod 时,得知道底层是 NFS 还是 Ceph,得写对方的 IP、路径、认证。一旦运维换了存储设备,所有 YAML 都得改。这正是耦合的痛。
K8s 的解法借鉴了"存储领域的 Service"思想:
- PV(PersistentVolume,持久卷):集群级的"存储资源对象",描述真实的存储后端(容量、访问模式、类型、挂载点)。由运维或系统创建,是"供应侧"。
- PVC(PersistentVolumeClaim,持久卷声明):用户对存储的使用请求,好比"我要 10G、能读写的盘"。由开发者创建,是"消费侧"。
- 绑定(Binding):系统把合适的 PV 分配给 PVC,Pod 再通过
persistentVolumeClaim把 PVC 挂进容器。
+--------+ 1. 申请 +-------+ 2. 绑定 +-------------+ | Pod | --------------------> | PVC | <----------------- | PV | | 声明 | volumeClaim | (需求) | 系统匹配 | (真实存储) | | 挂载 | +-------+ +-------------+ +--------+ 3. 挂载 | | | | 4. 使用 v +-------------------------------------------------------> NFS/Ceph/本地盘PVC 之于存储,就像 Service 之于 Pod:使用者只管"我要什么",不用关心"背后是谁"。开发者永远只碰 PVC,运维在 PV/StorageClass 层管理真实设备,两者通过"声明—匹配"解耦。
二、Static vs Dynamic Provisioning
静态供给(Static Provisioning)
运维预先手工创建一批 PV(比如 5 块 NFS 卷),开发者创建 PVC 时,系统从现成 PV 里挑一个满足条件的绑定。
# 运维预先创建:一个 NFS 类型的 PVapiVersion:v1kind:PersistentVolumemetadata:name:pv-nfs-01spec:capacity:storage:10GiaccessModes:-ReadWriteOnce# RWO:单节点读写persistentVolumeReclaimPolicy:Retain# 回收策略:保留nfs:server:10.0.0.10# NFS 服务器地址path:"/exports/data01"# 开发者创建:PVC 请求存储apiVersion:v1kind:PersistentVolumeClaimmetadata:name:my-pvcspec:accessModes:-ReadWriteOnceresources:requests:storage:10Gi问题:PV 要人工预创建,数量/容量难匹配,容易"有 PVC 没 PV"或浪费。于是有了动态供给。
动态供给(Dynamic Provisioning)
开发者只写 PVC,系统在 PVC 创建时自动按需求创建 PV(后端真正分配磁盘)。这靠StorageClass驱动。
# StorageClass:定义"如何造盘"apiVersion:storage.k8s.io/v1kind:StorageClassmetadata:name:fast-ssdprovisioner:kubernetes.io/aws-ebs# 由哪个插件造盘parameters:type:gp3reclaimPolicy:Delete# PVC 删则 PV 自动删allowVolumeExpansion:true# 支持扩容# 开发者只声明需求,PV 自动生成apiVersion:v1kind:PersistentVolumeClaimmetadata:name:my-pvcspec:storageClassName:fast-ssd# 指定按哪个模板造盘accessModes:["ReadWriteOnce"]resources:requests:storage:20Gi创建 PVC 后kubectl get pv会看到一张自动生成的 PV,名字形如pvc-xxxx,这正是动态供给的痕迹。
三、本地卷 vs 网络存储
| 类型 | 代表 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 本地卷 | hostPath/local | 零网络开销、延迟极低 | 与节点绑定、Pod 漂移需配合调度 | 高 IOPS 本地 DB |
| 网络存储 | NFS / Ceph RBD / 云盘 | 可跨节点、可迁移 | 有网络开销、需额外组件 | 共享/弹性存储 |
- hostPath:直接挂宿主机目录,最简单但完全绑定节点,Pod 被调度走就丢了,仅适合单机测试/daemon 场景。
- local:比 hostPath 规范,需在 PV 上声明
nodeAffinity指定节点,且有 External 健康检测,但仍绑定单节点。 - NFS/Ceph:真正的网络存储,支持
ReadWriteMany(RWX,多节点同时读写),是"共享盘"的首选,但需运维维护存储集群。
经验法则:要极致性能且单实例 → 本地卷;要共享/可迁移 → 网络存储。
四、为什么 PV/PVC 不是多此一举
有人质疑:“我直接 Pod 里挂 hostPath 不就行了?” 问题在于规模化与职责分离:
- 研发/运维解耦:开发者写 PVC 表达"我需要存储",无需知道底层是 NFS 还是云盘;运维在集群侧统一管理容量、配额、回收策略。岗位边界清晰,YAML 不被存储细节污染。
- 生命周期独立:PV 的生命周期(Retain/Delete/Recycle)与 Pod 解耦,数据可独立于应用存在,避免误删应用连带删数据。
- 策略统一:通过 StorageClass 把"造盘速度"“加密”"备份"等策略标准化,全公司一套规则。
- 可移植:换云厂商只改 StorageClass 的 provisioner,业务 PVC 零改动。
这正是 K8s “声明式 + 解耦” 哲学在存储领域的体现。
五、FlexVolume 与 CSI 插件机制
PV 怎么对接千奇百怪的存储后端?K8s 通过存储插件机制扩展。
- FlexVolume(旧):早期方案,以可执行二进制方式安装在每个节点,kubelet 调用它完成 attach/mount。部署麻烦、依赖节点文件系统,已被淘汰。
- CSI(Container Storage Interface,容器存储接口):当前标准。一套与 K8s 无关的标准接口,任何存储厂商实现 CSI 驱动即可接入 K8s、甚至跨编排系统复用。架构上分三部分:
- CSI Controller 插件(通常是 Deployment):负责"造盘/删盘"(CreateVolume/DeleteVolume)。
- CSI Node 插件(DaemonSet):在每个节点负责"挂盘/卸盘"(NodePublishVolume)。
- external-provisioner / external-attacher等 sidecar:把 CSI 调用翻译成 K8s 的 PV 生命周期事件。
Pod --> PVC --> PV --> StorageClass(provisioner) | +------+------+ | CSI Driver | | Controller | CreateVolume (造盘) | NodePlugin | NodePublish (挂到节点/Pod) +------+------+ v 实际存储后端 (EBS/Ceph/NFS...)小结 / 核心要点
- PV 是"存储资源"、PVC 是"使用请求",两者通过绑定解耦供应侧与消费侧。
- 静态供给需预创建 PV;动态供给由 StorageClass 在 PVC 创建时自动造 PV,生产首选。
- 本地卷(hostPath/local)性能高但绑节点;网络存储(NFS/Ceph)可共享可迁移,按需选择。
- PV/PVC 不是多此一举:它实现研发/运维解耦、数据生命周期独立、策略标准化与跨云可移植。
- 存储扩展走 CSI 标准接口(取代旧 FlexVolume),由 Controller + Node 插件对接后端存储。