news 2026/10/16 10:30:45

K8s存储核心:PV、PVC与StorageClass概念及实操指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
K8s存储核心:PV、PVC与StorageClass概念及实操指南

1. 厘清盘、目录和卷的边界:K8s为什么要绕一层PV/PVC/StorageClass

第一次用Kubernetes跑数据库的时候,我一度以为存储是最不该有门槛的部分:容器里把某个路径挂上不就行了?真到配置的时候才发现,K8s存储管理并不是“把磁盘塞进Pod”这么直白,而是要先和PV、PVC、StorageClass这三个概念打交道。如果一开始没有把这个抽象想明白,后面遇到“PVC一直Pending”“PV删不掉”“Pod挂载失败”这类问题,排查起来就会非常吃力。

我自己最早踩过的坑,是拿一个NFS目录直接往Pod里挂。当时觉得,反正宿主机能访问NFS,容器里也能访问,加上一个emptyDir不也有个目录用吗?后来服务一扩容,问题立刻炸了:多个副本同时读写同一个目录,数据互相污染;Pod被调度到其他节点后,原来的挂载点根本不存在;更麻烦的是,没人说得清这块存储到底归谁管,是运维该维护,还是应用开发自己维护。

K8s把PV、PVC、StorageClass这三个对象拆出来的核心原因,就是把“存储资源的供给”和“存储资源的使用”彻底分开。PV是集群里的一块存储资源,它可以是NFS目录、云盘、Ceph RBD,也可以是节点上一块本地盘;PVC是应用发出的“存储需求单”,声明我要多大、要什么样的读写模式;StorageClass则负责“按需生产”,当PVC提需求时,自动创建对应的后端存储。这个设计解决的不只是配置方便的问题,更关键的是生命周期边界:开发不需要知道底层是哪台存储服务器,运维不需要每个应用都手动手工建卷。

1.1 不搞抽象时,容器挂载为什么难以为继

如果你只跑一个单节点的测试环境,直接在Pod里挂宿主目录确实很简单,这也是很多人对K8s存储的第一印象。但一旦环境变成多节点集群,问题就不一样了。

假设有两个节点,节点A和节点B。你手动在节点A的/data/mysql目录放了一份数据,然后在Pod里把这个路径挂进去。这个Pod如果始终被调度到节点A,暂时没问题。但哪天节点A出问题,Pod漂移到节点B,K8s发现节点B根本没有/data/mysql这个目录,于是只能创建一个空目录挂进去。数据库起来之后,数据目录是空的,轻则启动失败,重则直接覆盖新数据。这种事故,我见过不止一次。

更麻烦的是权限和归属问题。多个团队共用一个集群时,如果每个人都能直接指定宿主目录,那不同应用之间根本没有隔离,A应用写坏B应用的数据,这种事会变成日常。K8s把存储抽象成PV/PVC后,普通用户只能看到PVC和StorageClass,看不到具体的宿主机路径,也不能随便指定任意目录。换句话说,存储的“所有权”和“访问权”终于被分开了。

1.2 PV是资源,PVC是需求单,StorageClass是生产线

我喜欢用类比来解释这三个对象:

  • PV是已经准备好的一块“空地”,它可以是一块云硬盘、一台NAS上的共享目录、一个Ceph块设备,总之是已经存在的存储资源。
  • PVC是用户填写的“需求单”,上面写着我要10G空间、支持单节点读写、最好性能高一点。
  • StorageClass是“按需生产车间”,收到PVC这张需求单后,它负责动态创建一个PV出来,而不是让运维手工去准备。

没有StorageClass时,工作方式是“静态供给”:运维先建好一批PV,用户再创建PVC去绑定其中一个。有了StorageClass之后,工作方式是“动态供给”:用户创建PVC时声明要用哪个StorageClass,系统自动创建PV,整个过程不用人工干预。

这三个对象还有一个生命周期上的微妙区别:PV是集群级别的资源,不属于任何命名空间;PVC是命名空间级别的资源,只属于某个命名空间。这意味着,你在namespace-a创建的PVC,不能直接绑定namespace-b里的PV,但PV可以被多个命名空间下的不同PVC绑定,前提是规格匹配且尚未被占用。这个区别在后面排查问题时会非常关键。

对象作用域核心作用谁来创建
PV集群级描述一块具体存储资源运维或动态供给
PVC命名空间级描述应用需要的存储规格应用开发者
StorageClass集群级定义动态创建PV的模板和策略运维平台团队

2. 落地前先定三件事:后端类型、回收策略和绑定模式

很多人一上来就写YAML,写完发现不起作用,原因是没想清楚存储后端、回收策略、绑定模式这三个前提。它们是决定PV/PVC行为的底层参数,一旦选错,后面改起来往往比重新配置还麻烦。

2.1 后端类型:网络存储、本地存储、CSI插件怎么选

K8s标准安装并不会自带上百种存储驱动,真正怎么建PV,取决于你的后端是什么。

  • 网络文件存储,比如NFS、CephFS,优点是多个节点能同时读写,适合共享数据场景;缺点是性能受网络影响,故障点也多一层。
  • 块存储,比如云厂商的云盘、Ceph RBD,优点是延迟低、IOPS稳定,适合数据库、消息队列这类需要高性能的应用;通常只能被一个节点读写。
  • 本地存储,比如hostPath、Local PV,性能最好,但数据跟节点绑定,节点挂了数据可能就没了,只能靠上层应用自己做副本。

在选择上,我一般遵循几个原则:测试环境用NFS最省事,因为部署简单;生产环境的无状态服务用块存储动态供给;有状态服务尽量让运维团队评估,不要自己拍板。尤其是数据库,如果后端存储不支持快照,备份策略会非常难做。

CSI是现在的标准趋势,它让存储厂商只需要实现一套接口,K8s就能动态创建、挂载、扩容、快照。如果你用的存储后端提供了CSI驱动,优先用它,而不是用旧式的in-tree插件或自己拼hostPath。换句话说,能动态供给就动态供给,避免手工维护一大堆PV。

2.2 回收策略:Retain、Delete、Recycle到底怎么生效

回收策略决定了PVC删除后,PV和底层存储怎么处理。看YAML里的persistentVolumeReclaimPolicy字段就能识别。

  • Retain:PVC删除后,PV不会自动删除,状态会变成Released,底层数据完整保留。适合需要人工确认数据的场景,比如数据库备份。
  • Delete:PVC删除后,PV以及后端的存储资源也会被删除,数据跟着没了。适合临时测试卷。
  • Recycle:早期版本会清理目录后重新可用,现在基本废弃了,不推荐。

生产环境我强烈建议默认用Retain,尤其是数据库和日志这类数据不可再生的场景。Delete虽然省事,但一次手滑删除PVC,可能连带把整个存储卷清掉,如果后端存储没有回收站,后果很难补。

有个细节容易忽略:回收策略是在PV或StorageClass上配置的。静态创建的PV,你在PV里写;动态供给时,你在StorageClass里写。如果两端不一致,以实际创建出来的PV为准。排查时不要只盯着PVC看,先查PV当前的persistentVolumeReclaimPolicy是什么值。

2.3 绑定模式:Immediate和WaitForFirstConsumer的区别

StorageClass里有一个volumeBindingMode,很多人会跳过它,但它直接影响Pod能否调度成功。

  • Immediate:PVC创建后,系统立刻创建并绑定PV。这个模式直观,但有一个隐患:如果存储是局部可访问的,比如本地盘,而Pod还没确定会被调度到哪个节点,系统可能把PV绑到Pod根本去不了的节点上。
  • WaitForFirstConsumer:PVC创建后,先不绑定,等第一个使用这个PVC的Pod出现,调度器确定了Pod落点,再创建PV并绑定。

我见过最典型的翻车场景,就是用Local PV时没有设置WaitForFirstConsumer。某个应用创建了PVC,系统立刻在节点A创建了一块本地存储,但Pod启动后被调度到节点B,于是Pod挂载时发现PV不在本节点,直接挂载失败,日志里只有一句“node(s) didn't have volume zone”或者干脆超时。所以,所有和节点数据绑定的存储后端,都必须在StorageClass里写volumeBindingMode: WaitForFirstConsumer。

3. 静态供给与动态供给实操:从手动建PV到StorageClass自动分配

理论讲完,下面进入实操。我用两个完整例子来展示静态供给和动态供给,以及最后Pod怎么消费PVC。你可以直接拿这段配置做原型,再根据自己集群的后端调整参数。

3.1 静态供给:手动创建PV并让PVC绑定

假设我有一台NFS存储,路径是/exports/data,希望让某个应用挂载使用。第一步是创建一个PV:

apiVersion: v1 kind: PersistentVolume metadata: name: pv-manual-001 spec: capacity: storage: 10Gi volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: "" nfs: path: /exports/data server: 192.168.50.20

这里有几个字段必须注意。capacity.storage是PV对外承诺的容量,只能写800Gi这种量级,不能写成“10GB”这样不规范的格式。accessModes表示访问模式,ReadWriteOnce意味着只能被一个节点挂载读写;如果应用要跨节点共享,就得用ReadWriteMany。storageClassName我刻意写成空字符串,是为了避免和集群默认StorageClass产生混淆。静态绑定时,如果PV和PVC都写storageClassName: "",绑定关系会更明确。

然后创建PVC:

apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-manual-001 spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi storageClassName: ""

创建后,一条命令就能看到绑定结果:

kubectl get pv pv-manual-001 kubectl get pvc pvc-manual-001

如果一切正常,PV的STATUS会从Available变成Bound,PVC的STATUS会变成Bound。PVC可以理解为拿着一张“需求单”去PV池子里找匹配项,PV容量、访问模式、storageClassName都必须匹配。容量匹配时,PVC请求的存储不能超过PV声明的容量;但PV容量略微大于PVC请求,通常也可以绑定,这取决于调度逻辑。为了避免这种不够精确的判断,生产上我一般把两者容量写成完全一致,减少边界问题。

3.2 动态供给:用StorageClass自动创建PV

静态供给的好处是可控,缺点是需要运维手工维护PV清单。集群规模一大,PV数量多了以后,手工创建根本不现实。

动态供给只需要创建StorageClass,然后创建PVC时指定这个ClassName。下面是一个既支持扩容、又要求延迟绑定的StorageClass示例:

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: network-storage-fast provisioner: csi-driver.example.com reclaimPolicy: Retain allowVolumeExpansion: true volumeBindingMode: WaitForFirstConsumer

这里的provisioner按你实际集成的存储驱动来填,不同驱动有各自的命名规则。我这里的csi-driver.example.com只是占位符。allowVolumeExpansion: true允许后续扩大PVC容量,这个能力依赖后端存储驱动,有些驱动并不支持,必须提前确认。volumeBindingMode设置为WaitForFirstConsumer后,PVC创建后不会立刻绑定,而是等Pod调度完成后才真正分配存储。

接着创建PVC:

apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-dynamic-001 spec: accessModes: - ReadWriteOnce resources: requests: storage: 20Gi storageClassName: network-storage-fast

关键区别就在storageClassName,它填的是StorageClass的名字,不是PV的名字。执行后,系统会自动创建一个PV,名字通常是一长串随机ID。

kubectl get pvc pvc-dynamic-001 -w kubectl get pv | grep pvc-dynamic-001

当PVC处于Pending状态且Events里写着“waiting for first consumer”时,不要急着怀疑配置错误。你应该直接创建使用这个PVC的Pod,Pod被调度到具体节点后,PVC会自动完成绑定。这个行为把“存储在哪里”和“容器在哪里”这两个问题绑定到了一起。

3.3 在Deployment或StatefulSet里消费PVC

PVC建好之后,应用侧的使用方式几乎没有差别。不管是静态创建的PVC还是动态创建的PVC,在Pod里都是同一个写法。

以Deployment为例:

apiVersion: apps/v1 kind: Deployment metadata: name:>kubectl exec -it deploy/data-writer -- ls -l /data kubectl exec -it deploy/data-writer -- cat /data/date.log

如果能在/data里看到写入的文件,说明整条链路已经通了。接下来再换回真实业务镜像,通常就不会有太大问题。

对于有状态应用,如果要动态创建多个独立卷,比如每个副本一个PVC,就应该用StatefulSet里的volumeClaimTemplates,而不是在Deployment里手动写PVC。volumeClaimTemplates会自动为每个副本生成一份PVC,名字格式是<template-name>-<pod-name>。这个设计特别适合Kafka、Elasticsearch这类需要一实例一卷的应用。

4. 真实环境最容易翻车的地方:Pending、Terminating、挂载权限异常

先说一句实话:PV/PVC的YAML本身并不复杂,难的是跟真实环境、真实存储后端打交道。下面把我自己在集群上踩过的几类问题完整讲一遍,每个问题的排查链路和修复思路都会拆开讲。

4.1 PVC一直Pending的完整排查链路

PVC挂在Pending状态是最常见的问题。看到Pending,先执行:

kubectl describe pvc <pvc-name>

重点看Events输出,常见的有这么几类:

第一种:no persistent volumes available for this claim

这是典型的静态供给场景,说明集群里没有现成的PV能匹配这个PVC。原因通常是容量不够、accessModes不匹配或storageClassName不匹配。我最常遇到的是storageClassName没对上:PV里写了某个StorageClass,PVC里写了另一个,或者写成了空字符串,导致两边认不到彼此。

第二种:waiting for first consumer to be created before binding

这不是错误,更像是一个“还没开始”的状态。说明StorageClass设置了WaitForFirstConsumer,但使用PVC的Pod还没创建。这个场景很容易被误判,尤其是习惯性用kubectl get pvc看状态的人,看到Pending就容易慌。正确做法是先创建Pod,让调度器介入。

第三种:storageclass not found

说明PVC里写的storageClassName在集群里不存在。可以用kubectl get storageclass确认一下名字是否写错,特别是大小写和中间的下划线、横线。

第四种:provisioning failed或invalid provisioner

动态供给时出现这个,说明StorageClass的provisioner和后端存储驱动不匹配,或者驱动组件没部署好。这时候要检查CSI驱动是否在运行,常见排查命令是看对应命名空间下的Pod是否Ready,以及StorageClass里的provisioner名称是不是集群中注册过的名字。

4.2 动态供给成功但Pod挂载失败:目录、权限、驱动三层问题

有时候PVC已经Bound了,PV也创建出来了,Pod却一直ContainerCreating。这个时候别再看PVC状态,要去看Pod事件:

kubectl describe pod <pod-name>

挂载失败一般分三层。

存储路径问题:比如NFS挂载后宿主机上找不到/exports/data这个目录,或者权限是root写的,普通容器用户进不去。NFS这类网络存储对目录权限的校验比较直接,目录必须真实存在,并且应用用户的UID要能访问。

fsGroup权限问题:K8s会在Pod的securityContext里配置fsGroup,用来改变挂载卷在容器内的属组。这个机制在本地盘或云盘场景通常有效,但NFS这类网络存储是否生效,取决于NFS服务器端的配置。如果服务器端强制root_squash,那么即使Pod设置了fsGroup,文件还是会被映射成nobody,写不进去。

CSI驱动问题:动态供给成功说明provisioner工作正常,但挂载环节还涉及另一个组件,通常是CSI Node插件。容器里执行mount命令时,CSI插件负责和后端通信。如果Node插件没启动,或者和Kubelet的socket路径对不上,Pod就会卡在挂载阶段。

排查时我一般从Pod事件一步步往底层找,先看挂载命令是否执行,再看宿主机能不能直接访问存储目录,最后看CSI插件日志。

4.3 PVC删不掉、PV一直Released的根因

删除顺序和回收策略是最容易让人迷惑的地方。先记住一个原则:有Pod在使用PVC时,PVC是删不掉的。K8s会给PVC加一个kubernetes.io/pvc-protectionfinalizer,只有当所有引用它的Pod都消失后,才会真正删除。

如果发现PVC一直处于Terminating,先查:

kubectl get pod --all-namespaces | grep <pvc-name>

找到引用这个PVC的Pod,把它删掉或扩缩容到0,PVC自然就能删除。如果确实找不到Pod,但PVC还是卡在Terminating,一般是finalizer卡住了。不建议直接强制删除,因为可能造成PV和后端存储不一致。稳妥做法是仔细检查是不是有无状态负载还在引用。

PV出现Released状态,说明PVC已经删除,但回收策略是Retain,所以PV没有被自动删除。这种状态下的数据还在,底层存储没有动过。如果想重新使用这块盘,需要人工清理claimRef并让PV回到Available,但这个操作在不同版本上行为略有差异。我的建议是:不要在生产环境反复做这类手工操作,该保留数据就保留,该归档就归档,人为干预PV的claimRef很容易埋雷。

4.4 容量扩容为什么没生效

PVC扩容在K8s里是支持的,但前提条件必须同时满足:后端存储驱动支持扩容、StorageClass的allowVolumeExpansion为true、PVC的volumeMode是Filesystem或Block且匹配后端能力。

扩容操作本身很简单:

kubectl edit pvc <pvc-name>

把spec.resources.requests.storage改成更大的值。保存后,系统会自动触发后端扩容。

但你可能遇到改了之后状态仍然是原来的容量,或者PVC被更新了,但Pod里的文件系统没变大。这一般是后端驱动的异步流程:云盘或Ceph需要先扩展块设备,然后文件系统才能扩容。某些文件系统需要在节点上进行额外的resize操作,而CSI驱动不一定自动执行。遇到这种情况,先看PVC的Conditions字段是否提示扩容中,再看CSI相关Pod日志,不要以为编辑完就万事大吉。

5. 我实际用下来的选型思路:静态供给和动态供给怎么搭配

讲完技术和故障,再聊一点更偏“决策”的经验。很多人会问:到底用静态供给还是动态供给?我的答案不是二选一,而是按场景分配。

5.1 静态供给适合有存量基础设施的场景

如果你的公司已经有一套NAS或集中式存储,并且希望所有容器应用都共享这一套存储,静态供给是更简单的方案。你只需要按业务容量把NFS目录切成多个PV,然后把PV清单交给应用团队,让他们在PVC里选择即可。

静态供给还有一个好处是命名可读性强。动态供给创建的PV名通常是一长串随机ID,排障时很难一眼看出来属于哪个业务。静态PV名可以写得很规则,比如pv-log-001、pv-mysql-prod-001,人类可读性好很多。

但静态供给的缺点也很明显,knife就是容量规划和回收依赖人工。假设你创建了20个10Gi的PV,业务实际只用了一半,剩下的10个就闲置了。磁盘是物理存在的,无法“缩回去”给其他业务用。所以静态供给更适合容量需求相对稳定的应用。

5.2 动态供给适合弹性环境和自动化平台

如果集群面向多个团队,业务类型杂、规划难度大,动态供给几乎是唯一选择。业务方提交PVC时只需要写容量和StorageClass,系统自动调配后端存储,不用运维提前准备PV。

动态供给特别适合测试环境和CI流水线。一次测试跑完,直接删掉PVC和Namespace,底层云盘跟着释放,资源马上回收给平台。这个体验和云计算平台的“按需分配”是一致的,运维成本能降好几个台阶。

代价是后端存储本身必须支持API创建卷,传统NAS如果只提供手工目录创建,就没法做动态供给。这也是为什么现在很多存储产品都开始支持CSI驱动,因为只有实现了CSI,才能融入K8s的动态供给体系。

5.3 我总结的检查清单

每次配置完存储,我都会按下面这组问题再过一遍:

  • PV和PVC的storageClassName是否精确匹配?有没有被默认StorageClass干扰?
  • PVC需要的accessModes是否和后端能力匹配?ReadWriteOnce和ReadWriteMany一定分清楚。
  • StorageClass的volumeBindingMode是否和存储后端匹配?Local PV务必用WaitForFirstConsumer。
  • 回收策略是不是符合数据安全预期?生产数据尽量Retain。
  • 后端存储目录是否存在,权限是否足够?NFS场景尤其要看Root Squash。
  • 是否需要设置allowVolumeExpansion?必须确认驱动支持再开。
  • 数据库这类有状态应用,是否做了备份?存储层快照和逻辑备份至少有一个。

最后分享一个我自己养成的习惯:所有存储相关资源都不随手删除,确认数据不需要了再清理。因为存储不同于CPU和内存,删掉就真的没了,哪怕回收策略是Delete,也只能保证流程正确,不能保证数据不可再生。K8s的存储设计本质是把复杂留给平台,把简单留给应用,而我们要做的,是确保平台这一步的每一个参数都清楚自己在干什么。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 10:30:26

PYNQ-Z2部署DPU v1.4实战:Zynq-7020边缘AI加速指南

简介&#xff1a;本资源是面向嵌入式AI开发者与FPGA初学者的PYNQ-Z2平台DPU1.4硬件加速工程套件&#xff0c;聚焦边缘端深度学习推理部署&#xff0c;解决Zynq UltraScale MPSoC上DPU IP集成、Vivado 2019.1工程构建与软硬协同调试等核心问题。压缩包共222个文件&#xff0c;涵…

作者头像 李华
网站建设 2026/10/16 10:30:26

Spring Boot 整合 ElasticSearch 7.6.x 实战:从依赖配置到复杂查询

1. 为什么要在 Spring Boot 里整合 ElasticSearch 7.6.x 1.1 从一次真实的数据检索需求说起 事情的起因很简单。我手头有一个内容管理类的后台系统&#xff0c;数据量在百万级&#xff0c;早期用 MySQL 的 LIKE %关键词% 做全文检索&#xff0c;前几个月还能忍&#xff0c;到…

作者头像 李华
网站建设 2026/10/16 10:29:52

STM32软件模拟四拍驱动28BYJ-48步进电机实战

简介&#xff1a;本资源是一套基于STM32F10x系列微控制器驱动28BYJ-48四相五线制步进电机的完整嵌入式工程&#xff0c;面向嵌入式初学者、电子设计爱好者及课程实践者&#xff0c;解决电机精准角度控制、正反转调速等典型运动控制需求。压缩包共120个文件&#xff0c;涵盖32个…

作者头像 李华
网站建设 2026/10/16 10:29:33

TensorFlow 2.0二分类神经网络实战:从数据到评估的完整指南

1. 从回归到分类&#xff1a;为什么二分类是神经网络的最佳练兵场很多人学完TensorFlow 2.0的线性回归之后&#xff0c;下一步就卡住了——不知道该怎么过渡到真正的神经网络。我当初也是这样&#xff0c;线性回归跑通了&#xff0c;损失函数用的是MSE&#xff0c;输出层一个神…

作者头像 李华
网站建设 2026/10/16 10:29:32

矩阵快速幂模板详解:从快速幂原理到代码实现

1. 题目理解与核心原理1.1 这道模板题到底在考什么P3390 这道题&#xff0c;几乎每个刷信奥题的人都绕不过去。题目名称里写着“模板”两个字&#xff0c;说明它就是拿来当标准件用的&#xff0c;理解了这道题&#xff0c;后续很多算法题都能直接套同一套思路。题目要求很直接&…

作者头像 李华
网站建设 2026/10/16 10:28:59

HART物理层六大约束与信号完整性排查:现场仪表通信工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华