一句话总结
容器天生"用完即弃", 数据却要长存--这对矛盾靠 Kubernetes 的存储抽象化解: PV 是一块真实存储, PVC 是应用对存储的"申领单", StorageClass 是"按需自动造盘"的模板.
理解它们, 先得分清块/文件/对象三种存储形态, 以及 ReadWriteOnce 这类访问模式的真实含义.
从网络转入存储: 留意一个对称
前五篇讲网络, 从这一篇起讲存储. 读的时候不妨带着大纲提到的对称感: 网络用 CNI 把实现外包给插件, 存储用 CSI(第 07 篇)外包给插件; 网络有"模型→接口→实现"的结构, 存储也有"抽象(本篇)→ CSI 接口(07)→ 实战选型(08)"完全对应的结构.
两套体系是同一种设计哲学的两个投影--本篇先把存储的"抽象"这一层讲透.
核心矛盾: 容器易逝, 数据要长存
先看为什么容器存储是个"问题":
容器的本质是"无状态, 用完即弃":
- 容器内的文件系统是临时的, 容器一删/重建, 里面写的数据全没了
- Pod 被重新调度到另一个节点, 本地写的东西也带不走
但很多应用是有状态的: 数据库的数据, 用户上传的文件, 应用的持久配置... 这些数据必须比容器活得久, Pod 重建, 迁移后还得在.
矛盾: 容器要"随时可丢弃", 数据要"绝不能丢".
解法的思路很自然: 把"数据"从"容器"里剥离出去, 存到一块独立于容器生命周期的存储上, 再"挂载"进容器. 容器可以随便生灭, 数据稳稳待在外部存储里. 但"外部存储"形态各异--先认识它们.
三种存储形态: 块, 文件, 对象
这是存储领域的基础分类, 选型时绕不开. 一次讲清三者的本质区别:
| 形态 | 是什么 | 类比 | 典型用途 | 共享性 |
|---|---|---|---|---|
| 块存储 Block | 一块裸磁盘(裸设备), 由使用者格式化成文件系统 | 一块空白硬盘 | 数据库(要独占, 高性能, 低延迟) | 通常只能单节点挂载 |
| 文件存储 File | 一个可挂载的文件系统目录(如 NFS) | 一个共享文件夹 | 多个 Pod 共享读写文件 | 可多节点同时挂载 |
| 对象存储 Object | 通过 API(HTTP)存取的"键-对象"(如 S3) | 一个云盘网盘(传 URL 存取) | 海量非结构化数据: 图片, 备份, 日志归档 | 天然全局可访问 |
块存储像一块空白硬盘--提供裸容量, 怎么分区, 装什么文件系统使用者自己定, 独占, 快.
文件存储像办公室的共享盘--已经是现成的文件夹, 多人能同时打开读写.
对象存储像快递柜--不关心它内部怎么放, 只用一个取件码(key)存取整个包裹(对象), 通过网络 API 访问.
为什么数据库偏爱块存储
数据库对存储的要求是低延迟, 高 IOPS, 独占控制. 块存储给的是裸设备, 数据库可以用自己优化过的文件系统和 IO 路径直接操作, 性能最好; 而且数据库通常不希望别人同时写同一份数据文件(会损坏), 块存储"单节点独占挂载"的特性正好契合.
反过来, 需要多个 Pod 同时读写同一批文件的场景(如多副本共享上传目录), 就得用文件存储. 形态选错, 要么性能差, 要么数据损坏.
PV 与 PVC: 供给与申领的解耦
现在进入 K8s 的抽象. K8s 把"存储"拆成两个对象, 刻意解耦--这是理解 K8s 存储的关键:
- PV(PersistentVolume, 持久卷): 代表一块真实的存储资源(一块云盘, 一个 NFS 共享). 它是集群级资源, 描述"这里有一块 100Gi, 支持单节点读写的存储". 通常由管理员/系统提供.
- PVC(PersistentVolumeClaim, 持久卷申领): 代表应用对存储的需求, 像一张"申领单": "我要 10Gi, 可读写的存储". 它是命名空间级资源, 由应用开发者创建.
应用(Pod)只引用 PVC, 不直接碰 PV. K8s 负责把 PVC 和一个满足条件的 PV 绑定(Bind) 起来:
|
为什么要拆成 PV 和 PVC? --关注点分离
这个拆分体现了 K8s 一贯的"关注点分离": 开发者只关心"我要多大, 什么访问模式的存储"(写 PVC), 完全不需要知道背后是 AWS 云盘, Ceph 还是 NFS; 管理员/系统负责提供真实存储(PV).
开发者的 PVC 是可移植的--同一份应用 YAML, 在 AWS 上绑到 EBS, 在自建机房绑到 Ceph, 应用层一字不改. 这和第 03 篇网络里"应用不关心用什么 CNI"是同一种解耦智慧.
访问模式: 最容易踩错的语义
PVC 要声明访问模式(Access Mode), 它直接关系到"几个 Pod 能同时用这块存储". 三个常见模式务必分清:
| 模式 | 简写 | 含义 |
|---|---|---|
| ReadWriteOnce | RWO | 只能被单个节点挂载读写 |
| ReadOnlyMany | ROX | 可被多个节点只读挂载 |
| ReadWriteMany | RWX | 可被多个节点同时读写 |
RWO 是'单节点'不是'单 Pod', 且不是所有存储都支持 RWX
两个高频误区:
- ReadWriteOnce 限制的是"节点"而非"Pod"--同一节点上的多个 Pod 其实可以共用一个 RWO 卷, 但跨节点就不行
- RWX(多节点读写)不是想要就有的: 它取决于底层存储形态--块存储基本只支持 RWO(裸盘没法安全地多节点同时写), 只有文件存储(NFS, CephFS)等才支持 RWX
所以当给一个多副本应用配了 RWX 却发现起不来, 八成是底层用的块存储根本不支持 RWX. 访问模式不是"想要什么", 而是底层存储"能给什么".
StorageClass: 从"手工配盘"到"按需自动造盘"
上面的流程有个隐含前提: 得先有 PV 才能绑定. 早期这意味着管理员要手动预先创建一堆 PV(静态供给), 既麻烦又难匹配("我建了 100Gi 的, 要 10Gi 的, 绑上了浪费 90Gi"). StorageClass 解决这个: 它是一个"按需自动创建 PV 的模板".
| 供给方式 | 怎么来 PV | 体验 |
|---|---|---|
| 静态供给 | 管理员手动预先创建 PV | 麻烦, 易浪费, 难规模化 |
| 动态供给 | PVC 指定一个 StorageClass, K8s 自动按需创建刚好匹配的 PV | 开发者只管提需求, 存储自动出现 |
动态供给的流程变成: 开发者写一个 PVC, 指定 StorageClass(如 fast-ssd) → K8s 调用该 StorageClass 对应的存储插件 → 实时创建一块刚好 10Gi 的真实存储并自动生成 PV → 绑定. 整个过程无需人工预备 PV.
StorageClass 还封装了'存储的种类'
一个集群可以有多个 StorageClass, 代表不同档次/类型的存储: fast-ssd(高性能 SSD 云盘), standard-hdd(便宜的 HDD), nfs-shared(共享文件存储)... 开发者在 PVC 里选一个 StorageClass, 就等于选了"我要哪种存储".
这把"存储选型"也变成了声明式的, 自助的--又一次体现 K8s "开发者自助, 平台提供能力"的理念. 而 StorageClass 背后"真正去创建那块盘"的, 正是第 07 篇的 CSI 插件.
回收策略: PVC 删了, 数据怎么办
最后一个关键概念: 当 PVC 被删除, 绑定的 PV 和上面的数据何去何从? 由 PV 的回收策略(Reclaim Policy) 决定:
- Delete: 删 PVC 时, 自动删除 PV 和底层真实存储(数据一并销毁). 动态供给的默认值, 适合临时数据.
- Retain: 删 PVC 时, PV 和数据保留(只是解绑, 需管理员手动处理). 适合重要数据, 防误删.
生产中重要数据务必用 Retain
动态供给默认常是 Delete--意味着误删一个数据库的 PVC, 底层云盘和数据会被自动连锅端, 且往往不可恢复.
对数据库等关键有状态应用, 务必把 StorageClass 或 PV 的回收策略设为 Retain, 给数据加一道"删了也还在"的保险. 这类"默认行为很危险"的坑, 和第 05 篇网络 default-open, CI/CD 的 latest tag 一样, 是必须主动规避的默认值.
快速回顾
- 核心矛盾: 容器用完即弃, 数据要长存 → 把数据剥离到独立于容器生命周期的外部存储, 再挂载进容器
- 三种形态: 块(裸盘, 独占高性能, 数据库爱用, 多为 RWO), 文件(共享文件夹, 可多节点读写 RWX), 对象(API 存取, 海量非结构化)
- PV / PVC: PV 是真实存储资源(管理员/系统提供), PVC 是应用的申领单(开发者写); 二者绑定, 应用只引用 PVC--关注点分离, 可移植
- 访问模式: RWO(单节点读写) / ROX / RWX(多节点读写); RWO 限"节点"非"Pod", RWX 需文件存储支持, 块存储给不了
- StorageClass: 动态供给模板, PVC 指定它即按需自动造盘, 免手工预备 PV; 还封装了"存储种类"的选择
- 回收策略: Delete(删 PVC 连数据一起删, 默认) vs Retain(保留数据); 重要数据务必 Retain
容器临时用的, 不需要持久化的数据(如缓存, 临时文件)也要 PVC 吗?
不需要. K8s 有更轻量的卷类型: emptyDir 给 Pod 一块临时空间(Pod 删了就没), 适合缓存, 临时文件, 容器间共享临时数据; configMap/secret 卷把配置/密钥挂进容器(Kubernetes 笔记讲过).
PV/PVC 是专门给"需要活过 Pod 生命周期"的持久数据用的. 别给临时数据套上 PVC 的重型机制--按数据的生命周期需求选对卷类型.
PVC 绑定到 PV 后, 能扩容吗? 比如 10Gi 不够了想加到 50Gi.
可以, 但有前提. 如果 PVC 用的 StorageClass 开启了 allowVolumeExpansion: true, 可以直接编辑 PVC 把请求容量改大, K8s 会驱动底层存储扩容(这背后是第 07 篇 CSI 的扩容能力).
注意: 通常只能扩大不能缩小; 文件系统层面可能需要额外步骤(部分情况要重启 Pod 才能让文件系统识别新容量). 能否扩容, 怎么扩, 最终取决于底层存储插件支不支持--这又回到了"接口能力"的话题, 第 07 篇细讲.
动手练习
- 查看 StorageClass: 在集群里
kubectl get storageclass看有哪些 StorageClass, 找出默认的那个(带 default 标记) - 亲历动态供给: 写一个 PVC(指定 StorageClass, 请求 1Gi, RWO), apply 后观察它从 Pending 到 Bound 的过程, 并
kubectl get pv看到自动创建出来的 PV - 验证数据持久: 创建一个 Pod 挂载这个 PVC, 往挂载目录写个文件; 然后删除 Pod 重建(引用同一 PVC), 验证文件还在--数据活过了容器
- 体验访问模式限制: 试着把 PVC 的 accessModes 改成 ReadWriteMany 配一个块存储的 StorageClass, 观察会发生什么(大概率绑不上或挂载失败)--亲身体会"访问模式取决于底层能给什么"
- 检查回收策略: 查看 PV 的回收策略(
kubectl get pv -o wide), 判断它是 Delete 还是 Retain; 思考: 如果这是数据库的卷, 当前策略安全吗?