news 2026/9/24 18:28:35

Kubernetes存储实战:PV/PVC与StorageClass从原理到配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kubernetes存储实战:PV/PVC与StorageClass从原理到配置

在Kubernetes里跑无状态应用,Deployment一滚动更新,老Pod一删,新Pod一起,怎么折腾都不慌。可一旦涉及数据库、文件服务、消息队列这种有状态应用,情况就完全不一样了。Pod本身是临时资产,容器里写的任何数据,随着Pod销毁就全没了。业内常说的那句"容器是无状态的",指的就是这个层面。

那生产环境里,MySQL、Redis、ES这些数据落哪儿?总不能每次重启都从零开始吧。这就绕不开今天要聊的PV(PersistentVolume)和PVC(PersistentVolumeClaim)。我最早接触Kubernetes存储的时候也被这两个概念绕得晕,什么静态供给、动态供给、StorageClass、AccessModes,一堆术语堆在一起,看文档能看懂,上手一配就各种Pending。这篇就把PV和PVC这套存储体系从头到尾拆开讲清楚,结合我这几年在集群里实际配置存储踩过的坑,尽量让你少走弯路。

1. 先搞明白一件事:Kubernetes为什么非要绕一层PV和PVC

很多刚上手Kubernetes的人都会问同一个问题:我直接在Pod的YAML里写hostPath,把宿主机目录挂进去不就行了?为什么非要多此一举,搞出PV和PVC两个概念?

直接挂宿主机目录当然能跑通,我在测试环境也这么干过。但你把视角拉到生产集群就明白了,一个集群少则三五台节点,多则几十台上百台,Pod会被调度到任意一台节点上。如果你在YAML里写死hostPath,那这个Pod换台机器调度,数据目录就找不到了。更麻烦的是,如果Pod被重新调度到别的节点,数据目录是空的,服务直接起不来。

PV和PVC这套抽象,本质上是把"存储资源"和"存储使用"两者解耦。

  • PV 是集群里的一块存储资源,由管理员提前准备好,或者通过StorageClass自动创建,它描述的是"我有存储,容量多大,什么类型,在哪台存储设备上"。
  • PVC 是用户提交的一份存储申请单,描述的是"我要多大容量,什么访问模式"。

Kubernetes的控制平面(准确说是PV controller)看到这份申请单之后,会去找一个匹配的PV来绑定。绑定成功之后,Pod里直接引用PVC就行。Pod完全不用关心数据到底存在NFS上、Ceph上还是云盘上,也不用关心底层的IP、路径、存储类型这些细节。调度器在给Pod选节点的时候,也会参考PVC所绑定的PV的位置,保证Pod能被调度到数据可达的节点上。

打个比方:PV相当于房东挂出来的房源信息,PVC相当于你提交的租房需求。Kubernetes控制平面就是中介,它负责帮你匹配房源。你(Pod)只知道自己住进去就行,具体是哪套房、房东是谁、水电怎么算,都不用操心。这个解耦带来的直接好处是:基础设施团队可以统一维护一套存储资源池,业务团队只需按需提申请,两边各管各的,互不干扰。

2. PV和PVC怎么配合工作:从声明到绑定的完整链路

搞清楚了为什么要抽象一层,接下来得弄明白这套机制具体怎么运转。

2.1 管存储的和用存储的分工

PV和PVC是两拨人分别维护的。集群管理员负责PV,业务研发负责PVC。这种分工在Kubernetes的RBAC权限体系下也天然成立:集群管理员有权限创建PV,普通业务团队通常只能创建PVC。两个角色各管一头,通过资源对象的spec字段进行"供需匹配"。

一个PV的YAML长这样:

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

一个PVC的YAML长这样:

apiVersion: v1 kind: PersistentVolumeClaim metadata: name:>apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: alicloud-disk-essd provisioner: diskplugin.csi.alibabacloud.com parameters: type: cloud_essd performanceLevel: PL1 enableAutoSnapshot: "true" reclaimPolicy: Delete allowVolumeExpansion: true volumeBindingMode: WaitForFirstConsumer

PVC只需声明storageClassName是alicloud-disk-essd,Kubernetes就会自动去阿里云买一块ESSD云盘,创建PV并绑定。这才叫"按需使用"。

3.3 provisioner的选型逻辑

provisioner是StorageClass的核心,它决定了底层存储从哪里来。不同云厂商的provisioner各不相同:阿里云有diskplugin.csi.alibabacloud.com,AWS有ebs.csi.aws.com,自建集群常用的是NFS provisioner或Ceph CSI。

自建Kubernetes集群又不想依赖云厂商的情况下,最省事的方案是装一个nfs-subdir-external-provisioner。它的原理很直接:在NFS服务器上按PVC的名字自动创建子目录,把这个目录导出为PV。研发环境、测试环境用起来非常方便。

选provisioner的时候,重点看三点:底层存储类型支不支持你的访问模式需求;provisioner的稳定性和维护活跃度;存储性能是否满足业务要求。我在自建集群里用过一次社区版的Ceph CSI,因为版本和内核模块不匹配,折腾了两天才跑通,后面果断换回了NFS provisioner。小集群别硬上Ceph,学习成本和高可用成本都是实打实的。

4. 从零到一:NFS作为存储后端的完整实践

理论聊了不少,下面直接上手。我用NFS作为共享存储后端,完整演示从搭建NFS服务器到Pod挂载PVC的整个流程。这套方案在自建集群里非常普适,不管是裸机Kubernetes还是虚拟化环境都能用。

4.1 先搭好NFS服务器

假设有一台独立的存储服务器,IP是192.168.1.100。先安装NFS服务并配置好导出目录:

# Ubuntu/Debian系 apt-get update && apt-get install -y nfs-kernel-server # CentOS/RHEL系 yum install -y nfs-utils # 创建共享目录并赋予权限 mkdir -p /data/k8s chmod 755 /data/k8s

编辑 /etc/exports 配置允许Kubernetes节点访问:

/data/k8s 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)

重启NFS服务并验证:

systemctl restart nfs-kernel-server exportfs -v

这里有个关键参数要解释一下。no_root_squash这个选项意味着当Pod以root身份写文件时,NFS服务器上保留root权限。如果不开这个参数,容器内root用户创建的文件在宿主机上会变成nobody所有,后续Pod读写文件可能出现权限错乱。很多第一次搭NFS的兄弟在这里踩坑,Pod起来之后报Permission denied,排查半天发现是这个参数没配。

4.2 创建PV和PVC

NFS服务器就绪后,在集群里创建PV:

apiVersion: v1 kind: PersistentVolume metadata: name: nfs-data-pv spec: capacity: storage: 20Gi volumeMode: Filesystem accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain storageClassName: nfs-manual nfs: path: /data/k8s/pv-data server: 192.168.1.100

记住要先在NFS服务器上手动创建 /data/k8s/pv-data 这个目录,并把属主和权限调整好,否则挂载后容器内目录是空的,而且报错信息往往要到Kubelet日志里才看得到:

mkdir -p /data/k8s/pv-data chown nobody:nogroup /data/k8s/pv-data chmod 755 /data/k8s/pv-data

然后创建PVC:

apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nfs-data-claim namespace: default spec: accessModes: - ReadWriteMany resources: requests: storage: 10Gi storageClassName: nfs-manual

注意PVC里声明的storageClassName要和PV一致,都是nfs-manual,这样控制平面才知道要匹配哪一类PV。执行 kubectl get pvc 就能看到状态变成Bound。

4.3 在Pod里挂载PVC

最后创建一个使用PVC的Pod:

apiVersion: v1 kind: Pod metadata: name: nfs-test-pod spec: containers: - name: app image: nginx:1.25-alpine volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: nfs-data-claim

Pod启动后,进入容器验证一下:

kubectl exec -it nfs-test-pod -- sh echo "hello pv pvc" > /data/test.txt cat /data/test.txt

这时候回到NFS服务器上,执行 ls /data/k8s/pv-data,应该能看到test.txt文件。到这步,整个数据通路就打通了:Pod内写文件 → 写到容器挂载点 → 通过NFS协议写到远端服务器目录。即使Pod被删除重建,数据依然还在NFS上,新Pod挂载同一PVC就能读到原数据。

5. 我在实际集群里踩过的那些坑:PVC一直Pending、容量翻车、回收策略误删数据

配置PV/PVC本身不难,难的是出问题之后的排查。下面几个坑全是我自己集群里真实遇到过的,每条都花了不少时间才定位到根因。

5.1 PVC一直Pending,kubectl describe才是第一排查手段

PVC创建之后一直处于Pending状态,这是最最常见的故障。大多数人的第一反应是"是不是网络有问题",实际上80%的情况都出在匹配规则上。

每当我看到PVC卡在Pending,第一件事就是跑:

kubectl describe pvc nfs-data-claim

Events字段会把匹配失败的原因直接告诉你是找不到匹配PV,还是storageClass不存在,还是accessModes不匹配,还是容量超出。最常见的情况是PVC声明的storageClassName在集群里根本不存在,或者PV的storageClassName和PVC的不一致。另一个容易被忽略的点:PV的容量必须大于等于PVC的请求容量,少了的话哪怕其他条件全匹配,也照样Pending。

如果你用了StorageClass动态供给,PVC还是Pending,那就得检查provisioner对应Pod的状态:

kubectl get pods -n kube-system | grep nfs kubectl logs -n kube-system nfs-subdir-external-provisioner-xxxxx

很多情况下是provisioner容器本身连不上NFS服务器,或者权限不对,导致创建子目录失败。

5.2 volumeNodeAffinity冲突:RWO卷被调度到了错误的节点

这个问题前面提到过,这里详细展开。假设你有一个RWO的云盘PV,它已经绑定了PVC并被某个Pod使用。如果这个Pod被删除,PVC被另一个新Pod引用,而新Pod被调度到了和云盘所在可用区不同的节点上,调度器会直接拒绝,报错信息类似:

0/3 nodes are available: 1 node(s) had volume node affinity conflict.

这个问题的根因在于:云盘这类存储本身是绑定某个可用区的,Kubernetes在调度Pod时必须保证Pod所在节点和云盘所在可用区一致。解决办法有三个方向。

  • 给Pod加上nodeSelector或者nodeAffinity,强制调度到存储所在的节点或可用区。
  • 使用volumeBindingMode: WaitForFirstConsumer的StorageClass,这样云盘会在Pod调度完成后再按需创建,确保云盘和Pod在同一可用区。
  • 改用支持跨节点访问的存储方案,比如NFS或CephFS。

第三种方案在自建集群里最省心,但如果你用的云托管Kubernetes,第一种方案配合WaitForFirstConsumer是更标准的选择。

5.3 误删PVC后数据还在不在?取决于reclaimPolicy

这是我刚上手Kubernetes时亲身经历过的一次事故。测试环境里我删了一个绑定了云盘的PVC,结果云盘被删了,数据全没了。当时PV配的reclaimPolicy是Delete,PVC一删,PV自动删,云盘跟着释放。我压根没想到这一层。

所以现在我的习惯是:凡是生产环境的PV,一律用Retain策略。PVC删了之后PV变成Released状态,管理员还能去存储后端把数据备份下来再恢复。如果PV配的是Delete,数据被删了想找回来,就只能靠存储后端的快照或者备份了。

顺带一提,如果你要手动让一个Released状态的PV重新投入使用,步骤是:删除PV,清理存储后端的数据(或者保留数据但要清楚里面的内容),再创建同名同配置的新PV。因为Released状态下的PV是不能再被PVC绑定的,Kubernetes的设计就是强制管理员人工介入。

5.4 扩容没那么简单:allowVolumeExpansion和控制面的限制

PVC的文件系统满了怎么办?很多人以为直接改PVC的spec.resources.requests.storage就行,结果发现PVC到了Bound状态之后,容量字段是不允许修改的。

实际上,Kubernetes从1.11版本开始支持在线扩容,但需要满足几个条件:

  • StorageClass必须设置了allowVolumeExpansion: true
  • 底层存储插件要支持扩容(云盘基本都支持,NFS要看provisioner实现)
  • PVC的volumeMode不能是Block

满足条件后,可以这样操作:

kubectl edit pvc>securityContext: fsGroup: 1000 runAsUser: 1000

不过要注意,fsGroup对NFS的某些实现并不生效,因为NFS的权限校验发生在服务端。这种情况只能去改NFS服务器上的目录属主。

6. 生产环境里PV/PVC设计的一些建议

最后聊点我在生产环境里沉淀下来的设计规范,不一定适合所有团队,但至少能帮你少踩一些前面说过的坑。

第一,按照存储类型和性能等级划分多个StorageClass,不要一个StorageClass打天下。比如有SSD的高性能存储类、有普通HDD的通用存储类、有NFS的共享存储类。业务方根据自身需求去选择合适的存储类,而不是所有人共享一个默认类。这样既方便成本核算,也能避免慢存储拖垮高IO应用。

第二,重要业务的PVC名字加上用途标识。比如mysql-data-pvc、redis-aof-pvc、es-data-pvc,后面排查问题的时候,一眼就能看出这个PVC是哪个业务用的。PVC本身没有Label强制命名规范,但团队内部可以约定俗成一套规则。

第三,监控PV/PVC的容量和状态。PVC容量写满是集群存储最常见的故障之一,Prometheus里有kubelet_volume_stats_used_bytes这个指标可以用,配上告警规则,在容量达到85%时提前通知,远比磁盘真的满了之后被动处理要舒服得多。

第四,自动备份不能省。PV和PVC这套抽象处理的是"存储供给和挂载"的问题,它不负责数据备份。PVC删了、PV删了、存储池坏了,数据依然可能永久丢失。在生产环境里,存储后端的快照、定期备份、跨地域复制这些能力还是要靠外部体系来补。StorageClass里的参数可能支持自动快照,但这要看provisioner的实现,不能想当然。

第五,不要在Pod的YAML里直接写hostPath来提供持久化存储,即使是在测试环境。因为你一旦习惯了这种方式,生产环境的YAML很容易顺手就写上去了,等到数据出了问题,代价是灾难性的。对于测试环境,我推荐用local-path-provisioner(Rancher出的那个)来模拟动态供给,几乎零成本,体验也和云盘动态供给基本一致。

PV和PVC这套抽象,熟练之后再看Kubernetes的存储体系,整个脉络会清爽很多:底层是CSI插件在跟真实存储设备打交道,往上是StorageClass定义存储模板,再往上是PV代表一块具体的存储资源,PVC则是业务侧提出的使用申请,最上面的Pod只是按需引用PVC。每一层各司其职,出问题的时候顺着这条链路一查,基本就能定位到根因。希望这篇能帮你把这块拼图补上。

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

YOLO训练过拟合?带标签数据增强原理与工程实践

简介:面向YOLOv5等目标检测与分割任务的数据增强工具,针对训练样本数量不足、手工标注成本高的问题,提供带标签图片的自动扩增方案。工具支持LabelImg与LabelMe两种常用标注格式,内置随机翻转、剪切、仿射变换、高斯模糊、平移、自…

作者头像 李华
网站建设 2026/9/24 18:28:03

TCP协议与Socket编程实战:从三次握手到粘包排查

干了这么多年网络编程,每次带新人都会发现同一个问题:很多人能把“三次握手、四次挥手”倒背如流,可一让他写个TCP聊天程序就懵。理论知识背了一堆,到了排查问题时依然无从下手。这篇东西我想换个讲法,把TCP协议和Sock…

作者头像 李华
网站建设 2026/9/24 18:26:56

Revo Uninstaller Pro 2023完全指南:彻底卸载与注册表清理实战

Revo Uninstaller这名字,Windows老用户应该都有印象。我用了它差不多七八年,中间试过CCleaner、IObit Uninstaller、Geek Uninstaller,最后还是老老实实换回来了。原因很简单:论"把软件卸干净"这件事,Revo确…

作者头像 李华
网站建设 2026/9/24 18:26:37

YOLO+深度估计实现单目3D目标检测:完整可跑工程解析

简介:面向计算机视觉与自动驾驶领域的3D目标检测实战资源,基于YOLO实时检测框架与深度估计算法,给出从二维检测扩展到三维空间定位的完整工程实现,适用于希望快速上手YOLO三维改写的工程师、研究人员及高校学生。资源压缩包共7个文…

作者头像 李华
网站建设 2026/9/24 18:26:35

OpenCV数码管数字识别实战:七段码特征与小数点处理全解析

简介:一套面向毕业设计/课程设计的优秀OpenCV数码管数字识别系统,完整支持小数点识别,适用于计算机、自动化、电子信息、物联网等专业学生及OpenCV进阶学习者。资源共18个文件,以Python源码(py/ipynb)、SVM…

作者头像 李华
网站建设 2026/9/24 18:26:18

XML实战指南:从基础语法到MyBatis配置与XXE安全防护

先说我为什么要把“day36-xml”当成一个正经话题来聊。每天坚持输出,到了第36天还在跟XML打交道,这说明什么?说明XML这门技术,你躲得了一时,躲不了一世。很多人一看到XML就皱眉,觉得现在都是JSON的天下了&a…

作者头像 李华