做 Kubernetes 时间长了你会发现,其他资源上手都不难,最难讲清楚的永远是存储。明明 Deployment、Service 这些 YAML 写熟了,一到 PV、PVC、StorageClass,很多人还是会卡壳:这三个东西到底是什么关系?为什么我明明创建了 PV,PVC 却一直 Pending?为什么 PVC 删了,PV 也跟着不见了?这些问题我在不同阶段都遇到过,也帮别人排查过不少次。这篇把 Kubernests YAML 里的卷部分完整拆一遍,从设计意图讲到 YAML 关键字段,再给一套排错链路的实操思路。适用对象很明确:Kubernetes 基础还不稳、想彻底搞懂存储卷配置的人,以及那些看到 PVC 长时间 Pending 就头大的运维同学。
1. 先看懂设计意图:为什么存储要拆成 PV、PVC、StorageClass 三块
很多人一上来就急着背字段、抄 YAML,结果换个存储后端就不知道怎么写了。我建议先把设计意图搞清楚。Kubernetes 里绝大多数资源都是围绕“应用生命周期”设计的,Pod 删了可以重建,Deployment 滚动更新也不会丢配置。但存储不一样,存储承载的是业务数据,数据不能因为 Pod 被重新调度就消失,所以存储必须独立于任何 Pod 存在,这就是持久化卷被单独抽象出来的根本原因。
既然存储要独立,就必然涉及两拨人:一拨人负责提供存储资源,比如申请 NFS 目录、划分云盘、准备物理磁盘;另一拨人只负责消费存储,也就是在 YAML 里申请“我要多少容量、我要什么访问方式”。这两种角色往往不是同一个人,如果让开发者直接去配置底层存储设备,既不安全也不现实。于是 Kubernetes 把存储拆成了三个层面:
- PV(PersistentVolume):代表一份真实存在的存储资源,可以是一块云盘、一个 NFS 目录、一个本地磁盘分区。它是集群管理员视角里的“已备好货物”。
- PVC(PersistentVolumeClaim):代表用户对存储的一次需求申请,里面写清楚要多大容量、哪种访问模式。它是应用开发者视角里的“领用申请单”。
- StorageClass:负责“按需供货”。用户申请 PVC 后,如果集群里没有现成 PV,StorageClass 可以自动创建一块新的存储并生成 PV。
我常用一个生活化类比:PV 是仓库货架上已经摆好的货,PVC 是你手里的申购单,StorageClass 是一套自动补货系统。管理员把货放上架或者配置好自动供货规则,使用者永远只需要填一张申购单。这张单子写清楚“我要什么”,系统负责把“对应的货”送到你手上。
1.1 先记住两个容易混淆的资源范围
这部分很多人会踩坑,我专门拎出来讲。PV 是集群级别资源,PVC 是命名空间级别资源。这句话什么意思?你执行kubectl get pv不需要指定 namespace,因为 PV 不隶属于任何命名空间;但执行kubectl get pvc必须带上 namespace,PVC 只存在于某个特定命名空间里。
这就导致了一个典型错误:在dev命名空间创建了一个 PVC,然后跑到prod命名空间的 Pod 里去引用它,结果怎么都挂不上。因为跨命名空间是访问不到那个 PVC 的。而 StorageClass 同样也是集群级别资源,它的名字在整个集群里只能有一个,不存在“不同命名空间各有一个同名 SC”的情况。查资源的时候我建议先用kubectl get pv -A和kubectl get sc -A看全量,再配合kubectl get pvc -n <namespace>看目标命名空间,这样范围问题一目了然。
1.2 三种卷使用方式对比
除了 PV、PVC、StorageClass,其实还有一种更简单的卷用法,就是直接在 Pod 里写 inline volume,比如hostPath、emptyDir、configMap。很多测试场景和临时任务会这么干,但它有一个致命问题:存储和 Pod 耦合在一起。Pod 被调度到别的节点,数据就留在原节点上了;Pod 被删,emptyDir 直接清空。所以生产持久化存储基本都要走 PV/PVC/StorageClass。
三种方式我用一张表总结:
| 方式 | 创建者 | 持久性 | 维护成本 | 适合场景 |
|---|---|---|---|---|
| inline volume | 应用开发者直接写在 Pod 里 | emptyDir 不持久,hostPath 依赖节点 | 最低 | 临时调试、缓存、单机验证 |
| 静态供给 PV + PVC | 管理员手动创建 PV,用户创建 PVC | 持久 | 较高 | 底层存储不能自动创建、需要人工规划目录 |
| 动态供给 StorageClass | 用户只创建 PVC,系统自动创建 PV | 持久 | 较低 | 有 CSI 驱动或外部 provisioner 的集群 |
你会发现,大多数人日常维护的集群里,PV 反而不常见,因为动态供给已经把建 PV 这件事自动化了。但如果你用的是裸机自建集群,又没有搭好 CSI provisioner,那静态 PV 就是最靠谱的选择。这也是为什么本文会把静态和动态都讲透。
2. PV 的 YAML 拆解:写清楚一份“底层存储”的契约
先给一个完整的静态 PV 示例,这个例子用 NFS 作为后端存储,是自建集群里最常见的方案:
apiVersion: v1 kind: PersistentVolume metadata: name: pv-nfs-data labels: storage-type: nfs usage: app-data spec: capacity: storage: 10Gi volumeMode: Filesystem accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain storageClassName: standard nfs: server: 192.168.10.20 path: /data/k8s/app-data这里每个字段都不是摆设,我逐个说:
metadata.name:PV 是集群级资源,名字在整个集群唯一。生产环境我建议命名带上用途和环境,比如pv-nfs-webdata-prod,否则集群里 PV 一多根本分不清哪个是哪个。spec.capacity.storage:声明这份存储的容量。这只是一个“名义容量”,对应底层存储的实际配额。后面 PVC 申请容量时,系统会比较两者大小。spec.volumeMode:默认是Filesystem,表示格式化成文件系统挂载。如果你要用裸设备跑数据库这类应用,可以改成Block,挂载后是一个块设备,需要应用自己去建文件系统。spec.accessModes:决定这个卷能怎么被访问。ReadWriteOnce表示单节点读写,ReadOnlyMany表示多节点只读,ReadWriteMany表示多节点读写。前两个缩写太多人记混了,我建议你直接记全称含义。spec.persistentVolumeReclaimPolicy:PVC 被删除后,PV 里的数据怎么处理。Retain表示保留数据,PV 状态变为 Released;Delete表示自动删除底层存储;Recycle基本废弃,不要用。spec.storageClassName:这个字段不是标签,而是绑定匹配的关键条件。PV 声明自己属于哪个存储类,PVC 也声明自己想要的存储类,两者必须对得上才能绑定。spec.nfs:这里写 NFS 服务端地址和导出路径。如果你用的是其他存储,这个字段会换成hostPath、local、csi、cephfs等不同的配置块。
这里有一个非常隐蔽的知识点:PVC 绑定 PV 时,存储类必须完全相等。如果你创建的 PV 没写storageClassName,那它默认等于空字符串;而如果 PVC 里也没写,那要看集群有没有默认 StorageClass,有默认类就会被默认类接管,导致和你手动创建的 PV 永远匹配不上。我见过太多人栽在这上面,看着 PV 是 Available,PVC 却一直 Pending,怎么查都查不出来。
2.1 容量、访问模式与回收策略如何参与匹配
先说容量。系统匹配 PV 和 PVC 时,只要 PV 容量大于等于 PVC 请求容量就能绑定,并不要求相等。比如你有个 10Gi 的 PV,PVC 申请 8Gi,绑定没问题;但剩下的 2Gi 也不会再分配给别的 PVC。这就意味着,静态供给模式下,PV 规划得越碎,容量浪费越严重。我建议把 PV 容量和 PVC 请求量放在一起规划,要么严格一致,要么干脆为每个应用单独建一块 PV。
再说访问模式。PVC 声明的访问模式必须被 PV 支持。比如 PV 声明了ReadWriteOnce,PVC 却声明ReadWriteMany,那就绑不上。反过来,PV 声明ReadWriteMany,PVC 申请ReadWriteOnce,这个可以绑。因为 PV 支持的能力覆盖了 PVC 的需求即可。这里要特别提醒:ReadWriteMany不是所有后端存储都支持。像常见云盘大多只能单节点读写,NFS 基本都支持多节点读写,用错了后端,等 Pod 调度到第二个节点时就会挂载失败。
最后说回收策略。Retain是最安全的策略,静态 PV 我基本都推荐它。PVC 删除后 PV 会变成Released,数据还在后端存储上,需要管理员手动处理数据后,再删除 PV 对象并重新创建,才能恢复成Available。Delete策略常见于动态供给,PVC 删除后底层存储会被一并删除,省心但风险很大。如果你不确定数据是否还有用,先改成Retain再说。
2.2 hostPath、local 与网络存储怎么选
很多新手在实验环境里用hostPath用得很顺手,但一上生产就出问题。hostPath就是把宿主机的一个目录挂载给 Pod,适合在单节点 Kubernetes 环境下做快速功能验证。一旦集群有多节点,Pod 被调度到 B 节点,而数据在 A 节点,Pod 启动后看到的就是一个空目录。所以官方文档也明确说,hostPath不应该被用于生产环境。
local卷和hostPath看起来相似,但机制完全不同。它要求 PV 里声明节点亲和性,系统只会把 Pod 调度到 PV 所在的节点上。这样就不会出现“数据在 A 节点、Pod 跑到 B 节点”的问题,适合高性能本地盘。不过节点一旦故障,数据恢复会比较麻烦,也不适合需要多副本跨节点共享的场景。
网络存储才是自建集群最主流的方案,NFS、CephFS、GlusterFS 都属于这一类。它们天然支持多节点访问,PVC 可以跟着 Pod 跨节点挂载,数据不依赖具体节点,也是实现ReadWriteMany的基础。我用一张表总结选择思路:
| 类型 | 多节点共享 | 性能 | 运维复杂度 | 推荐场景 |
|---|---|---|---|---|
| hostPath | 不支持 | 高 | 低 | 本地调试、单节点实验 |
| local | 不支持 | 高 | 中 | 高性能有状态应用的单副本存储 |
| NFS | 支持 | 中 | 中 | 自建集群通用持久化存储 |
| CephFS | 支持 | 中高 | 高 | 需要大规模共享存储的集群 |
如果刚开始搭环境,我建议先用 NFS 把功能链路跑通,后面再用 local 卷解决高并发磁盘 IO 问题。不要一上来就上 Ceph 或者分布式存储,调试成本会非常高。
3. PVC 的 YAML:需求怎么声明,绑定玄机在哪
PVC 的 YAML 比 PV 简单得多,但简单不代表不会出错。看一个标准示例:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: webdata-pvc namespace: web spec: accessModes: - ReadWriteMany volumeMode: Filesystem resources: requests: storage: 8Gi storageClassName: standardmetadata.namespace再次强调,PVC 必须属于某个命名空间。spec.resources.requests.storage是你要申请的容量。spec.storageClassName是你要用的存储类。如果你想让 PVC 强制绑定某个特定 PV,可以在spec里加一个volumeName字段,直接写 PV 的名字,这样系统就不会去匹配其他 PV。另外还可以用spec.selector加标签匹配,比如指定storage-type: nfs,系统会只认带这个标签的 PV。
绑定匹配的逻辑其实就几条,按优先级排:
- 如果 PVC 指定了
volumeName,直接绑定这个 PV,其他条件可以不看,但 PV 必须存在且可用。 - 如果没有指定,系统会找
storageClassName相同的 PV。 - 在这个范围内,挑访问模式覆盖 PVC 需求的 PV。
- 再挑容量大于等于请求容量且
volumeMode一致的 PV。 - 如果 PVC 用了
selector,还要匹配标签。
这条匹配链路解释了绝大多数 Pending 问题的根因。你可以对照kubectl get pvc的 STATUS 列排查:如果一直是 Pending,第一反应不应该是“等一等”,而是先检查有没有名字对得上的 PV、访问模式是否支持、SC 是否存在。很多人在这一步就开始慌了,其实用kubectl describe pvc <name> -n <namespace>看一眼事件,通常几秒钟就能定位。
3.1 为什么我建议显式写 storageClassName
这里要讲一个实际工作中的坑。有些集群为了方便,会设置默认 StorageClass,这样不写storageClassName的 PVC 会被默认类接管。听起来很贴心,但副作用很大。我在一个共享集群里遇到过这样的情况:业务方只是想在命名空间里创建一个临时 PVC 做测试,没写 SC,结果默认类自动在云平台上创建了一块真实云盘。这个云盘没人用,但费用一直扣,直到月底云账单出来才发现。测试环境搞出生产级成本,就是因为“默认”两个字。
所以我现在的习惯是:所有 PVC 都显式写storageClassName,即使和集群默认类相同也写。如果我想强制使用静态供给、不许动态补货,就把storageClassName写成空字符串"",这在 YAML 里是两个引号:
spec: storageClassName: ""这样系统就不会去找任何默认类,只会匹配storageClassName为空字符串的 PV。如果你创建静态 PV 时也把 SC 留空,两者就能对上。这个细节在排错时非常有用,建议刻进肌肉记忆。
3.2 accessModes 引发的多副本共享问题
accessModes看起来只是三个枚举值,实际踩坑的人非常多。最常见的误区是“我的 Deployment 有 2 个副本,PVC 用的ReadWriteOnce,为什么第二个副本一直 ContainerCreating”。
ReadWriteOnce的字面意思是“同一时刻只能被一个节点读写”。不是“只能被一个 Pod 使用”。如果两个 Pod 恰好被调度到同一个节点,它们可能都能挂载同一个 RWO 卷;一旦第二个副本被调度到另一个节点,它就会发现卷已经被其他节点占用,挂载直接失败。这就导致一个诡异现象:副本数少的时候一切正常,流量上来一扩容就挂。所以多副本共享同一个 PVC 时,要么底层存储支持ReadWriteMany(比如 NFS),要么就不要共享数据目录,改成每个副本自己的 PVC,比如用 StatefulSet 的volumeClaimTemplates。
还要注意,即使是ReadWriteMany,多个 Pod 并发写同一个文件也不是安全操作。Kubernetes 只管“能挂载”,不管“文件锁”。像 MySQL、PostgreSQL 这类数据库,永远不要用共享卷多副本跑,它们有自己的复制协议,不需要也不应该直接共享数据目录。这是很多人在持久化设计上容易犯的错。
4. StorageClass:动态供给的省心与藏坑
StorageClass 是整个存储体系里“自动化”的关键。先看一个基于 NFS 的 StorageClass 示例:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-csi provisioner: nfs.csi.k8s.io parameters: server: 192.168.10.20 share: /data/k8s reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer allowVolumeExpansion: true四个核心字段要理解透:
provisioner:真正负责“创建存储”的组件 ID。它可以是 CSI 驱动,比如上面例子里的 NFS CSI 驱动;也可以是内置 provisioner,比如kubernetes.io/no-provisioner用于静态场景。如果集群里没有部署对应的 provisioner,StorageClass 就是一张废纸,PVC 永远 Pending。parameters:一组传给 provisioner 的参数,不同的驱动有不同的参数。比如 NFS 驱动要写 NFS 服务地址和服务端共享目录;云盘驱动可能需要写磁盘类型、文件系统类型。参数必须查驱动文档,凭感觉写很容易失败。reclaimPolicy:定义动态创建的 PV 继承哪种回收策略。默认值是Delete,PVC 删除后 PV 和后端存储一起被删。想更安全可以改成Retain。volumeBindingMode:定义卷创建和绑定的时机。Immediate是立即创建和绑定;WaitForFirstConsumer是等到第一个 Pod 被调度后才创建。这个字段经常被忽略,但对本地卷、区域云盘特别重要,我后面专门讲。allowVolumeExpansion:是否允许 PVC 扩容。如果这里是false,即使后端支持扩容,你修改 PVC 容量也会被拒绝。
动态供给的完整流程是这样的:用户创建 PVC → 控制器发现没有匹配的 Available PV,但存在对应 SC → 调用该 SC 的 provisioner 在后端创建存储 → 系统生成一个 PV 并绑定到 PVC → Pod 挂载使用。整个过程对应用侧透明,开发者只需要创建 PVC。
4.1 默认 StorageClass 的注解和它的副作用
设置默认 StorageClass 只在名字上加一个注解:
metadata: annotations: storageclass.kubernetes.io/is-default-class: "true"有了这个注解,集群里所有没写storageClassName的 PVC,都会被自动分配到这个 SC。这样做的本意是让用户更省事,但也带来了风险。
第一个风险是资源失控。一个集群只能有一个默认 SC,一旦被误删或误改,所有未指定 SC 的 PVC 都会开始报错。更麻烦的是,如果存在多个默认 SC,行为会不可预测,所以在生产集群里一定要保证默认 SC 只有一个。
第二个风险是隐式创建。我在 3.1 里讲的“临时 PVC 自动创建云盘”就是这么来的。如果你的集群底层存储资源有限,或者钱不是大风刮来的,建议共享集群不配置默认 SC,或者至少把所有 PVC 里的storageClassName显式写死,谁也不许漏。
4.2 静态供给与动态供给的适用边界
很多教程会把动态供给捧得很高,好像静态 PV 已经过时了。实际上两者各有适用场景。动态供给的前提是后端存储的 provisioner 能自动创建卷,如果你用的是手工划定目录的 NFS,没有部署 CSI 驱动,那要么装一个,要么老老实实静态供给。
静态供给适合中小集群、存储规划清晰、管理员不希望自动创建出各种底层资源的场景。它的缺点也很明显:PV 要人肉维护,容量你说了算,PVC 申请超过已有 PV 容量就会一直 Pending。动态供给适合规模较大的集群,自动化程度高,也更容易配合配额管理。只不过,一旦把自动创建权限放开,底层存储的账单和容量监控就要跟上,否则容易失控。
我给个最朴素的建议:功能验证阶段用静态供给,规模上来了再上动态供给;如果后端驱动可靠且文档清晰,直接动态供给也没问题,但务必把reclaimPolicy和allowVolumeExpansion先想清楚。
5. 从 Pending 到 Failed:卷排错完整链路
存储排错其实不复杂,难的是很多人不知道从哪里开始看。我把排查链路整理成固定顺序,按这个顺序走,大部分问题都能定位:
- 先看
kubectl get pvc -A,确认哪些 PVC 处于 Pending。 - 对具体 PVC 执行
kubectl describe pvc <name> -n <namespace>,看 Events 尾部,事件往往是直接的错误原因。 - 看
kubectl get pv,确认是否存在 Available 且匹配的 PV;如果看到 Released,说明这个 PV 此前被用过,不会自动复用于新 PVC。 - 看
kubectl get sc,确认 PVC 指定的 StorageClass 存在,且 provisioner 已部署。 - 如果 SC 没问题,到 kube-system 或 storage 相关命名空间查看 provisioner 的 Pod 日志。
- 如果看到
waiting for first consumer to be created before binding,说明volumeBindingMode是WaitForFirstConsumer,此时 Pending 是正常的,等 Pod 调度后再看状态。
PV 的状态机值得单独记忆:Available表示空闲可绑定,Bound表示已被 PVC 占用,Released表示 PVC 已删除但数据还未清理,Failed表示自动供给失败。很多人最大的误区是看到 Released 就以为重新创建 PVC 能自动绑定,实际上不会。系统对 Released 的 PV 不信任,因为里面的数据可能还没清理,属于需要人工介入的“脏”状态。
关于删除顺序,我见过太多因为顺序错误导致数据的惨案。先说结论:如果要留下数据,优先保证 PV 的回收策略是 Retain,再考虑删 PVC 或 PV 的顺序。如果 PV 回收策略是 Delete,你删掉 PVC 的瞬间,PV 和后端存储会被一起删除,数据不会留下一丁点。所以动态供给场景下,如果想暂时不删除底层数据,正确的做法是在删 PVC 前先把 PV 的回收策略改成 Retain:
kubectl patch pv <pv-name> -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'然后删 PVC,PV 会进入 Released,底层数据还在。之后你可以根据情况决定是备份数据、清理 PV,还是手动重建 PV 对象。
5.1 一个真实案例:十几个 PVC 同时 Pending
我曾经排查过一个环境,一晚上同时创建了十几个 PVC,结果全部 Pending。第一反应是看事件,kubectl describe之后看到统一的事件提示:waiting for first consumer to be created before binding。这说明 StorageClass 配了WaitForFirstConsumer,而且这些 PVC 没有被任何 Pod 引用。理论上只要创建对应的 Deployment 就能触发绑定,但奇怪的是,任务方说业务已经在跑了。
继续查下去才发现,是 Deployment 引用了 PVC,但 Deployment 的副本一直调度不上去,事件里报的是 PVC 挂载失败。再追一层,发现 provisioner Pod 本身没有起来,原因是 NFS 服务端目录权限不对,provisioner 创建卷时权限校验失败。整个链路从“PVC Pending”到“provisioner 报错”,隔了整整两层。如果你只盯着第一个现象,不去看事件和底层组件日志,很容易绕半天圈子。
所以我的排查心得是:PV、PVC、SC、provisioner、后端存储,这五层从下往上逐层查,永远不要在第一层就下结论。事件里同时出现了多条信息时,以最靠后的那条为准,因为那是最终原因。
5.2 删除与回收顺序:数据保命的最后防线
我在实际环境中总结过几类删除操作的结果,整理成一张表:
| 操作 | 回收策略 | 结果 |
|---|---|---|
| 删除 PVC | Retain | PV 保留,变为 Released,底层数据完整 |
| 删除 PVC | Delete | PV 和后端存储一并删除,数据消失 |
| 删除 PV(PVC 仍然绑定) | Retain | PV 因保护机制暂不删除,等 PVC 删除后才释放 |
| 删除 PV(PVC 仍然绑定) | Delete | 通常会被 PVC 保护机制拦住,不建议主动操作 |
生产环境最稳妥的做法是:先备份数据或开快照,把回收策略改成 Retain,然后删除 PVC,确认数据完整后再删除 PV 对象。如果你连 PV 对象都不想删,只是想换绑一个新的 PVC,那也还是要先把这个 Released 的 PV 清理掉并重建。不要试图在 Released 状态下直接把它改回 Available,直接用kubectl patch改状态的方法是行不通的。
6. 从零复现存储挂载实验
理论讲再多,不如自己动手跑一遍。我在这里给一个最简单的静态供给实验路径,用 hostPath 就能验证整个绑定流程,不需要额外准备 NFS 服务。适合单节点的实验集群,比如你本地的 kind 或单节点 k3s。
先在节点上创建目录,假设是/srv/k8s-lab/data,然后执行:
mkdir -p /srv/k8s-lab/data接着创建 StorageClass,名字叫manual,用kubernetes.io/no-provisioner表示仅供匹配、不自动创建卷:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: manual provisioner: kubernetes.io/no-provisioner volumeBindingMode: Immediate再创建 PV,注意storageClassName: manual要和 SC 对应,容量 2Gi:
apiVersion: v1 kind: PersistentVolume metadata: name: pv-hostpath-lab spec: capacity: storage: 2Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: manual hostPath: path: /srv/k8s-lab/dataPVC 申请 1Gi,同样声明storageClassName: manual:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: lab-pvc namespace: default spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi storageClassName: manual依次 apply 之后查看状态:
kubectl apply -f sc.yaml kubectl apply -f pv.yaml kubectl apply -f pvc.yaml kubectl get pvc kubectl get pv如果一切正常,PVC 会变成 Bound,PV 也会变成 Bound。接着创建一个简单的 Pod 挂载这个 PVC:
apiVersion: v1 kind: Pod metadata: name: lab-storage-pod namespace: default spec: containers: - name: app image: busybox command: ["/bin/sh", "-c", "sleep 3600"] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: lab-pvcPod 起来后往/data下写一个文件,比如echo "hello storage" > /data/test.txt。然后删掉这个 Pod,重新 apply 同一份 YAML,再进到 Pod 里查看/data/test.txt,你会发现文件还在。这个实验可以直观验证持久化是怎么生效的。
6.1 如果想真正跑动态供给,需要什么前提
动态供给的复现比静态要麻烦一些,核心在于你必须先有可用的 provisioner。以 NFS 动态供给为例,大致流程是:
- 准备一台 NFS 服务端,导出目录。
- 在 Kubernetes 集群里部署对应的 CSI 驱动或外部 provisioner。
- 创建 StorageClass,
provisioner字段写驱动的名字,parameters写 NFS 服务地址和共享目录。 - 创建 PVC,不指定 volumeName,也不指定静态 PV。
- 系统自动创建 PV 并绑定,
kubectl get pv能看到一个自动生成的 PV。
很多人以为写完 StorageClass 就能自动供给,结果发现 PVC 一直 Pending。原因就是 provisioner 没装。通俗讲,StorageClass 只是“下单规则”,真正干活的还是得有个执行者。部署进展不顺利时,先看 provisioner 组件是否还在运行,几乎能解决一半问题。
6.2 清理实验环境时最容易忽略的事
实验室环境一般不怎么在乎数据,但我还是建议练习时坚持正确的清理顺序。先删除使用 PVC 的 Deployment、StatefulSet 或 Pod,再删除 PVC。如果你在实验里故意使用 Retain 策略,删完 PVC 后 PV 会变成 Released,数据还留在节点目录里,你需要手动删除 PV 对象来清理:
kubectl delete pvc lab-pvc kubectl get pv kubectl delete pv pv-hostpath-lab如果想一次性标记所有实验资源,最实用的办法是给所有 YAML 加上统一的标签,比如app: lab-storage,清理时用-l app=lab-storage批量操作。这样既安全又高效,也不会误删生产资源。
我在实验环境里踩过最深刻的一个坑是:删除了 Deployment 后没有等 PVC 释放,就急着删 PV,结果 PV 因为保护机制一直卡在 Terminating。后来才明白,Kubernetes 为了保护数据,会在 PVC 仍绑定时阻止 PV 删除。所以干脆把顺序固定下来:先删工作负载,再删 PVC,最后处理 PV。这条顺序我已经写进自己的操作手册里了,每次删存储相关资源都会先念一遍。
这篇文章从设计意图讲到字段细节,从静态供给讲到动态供给,再给了一套排错和复现路径。说实话,存储这块没有那么多玄学,绝大多数问题就是匹配条件不满足、回收策略没想清楚、provisioner 没部署好这三类原因。我个人习惯是在任何存储改动的操作前,先把自己要删的 PV 的回收策略看一眼,把绑定链路在脑子里过一遍,再动手。一次养成这个习惯,基本不会再有“数据凭空消失”的事故。