☰
Kubernetes存储入门:PV、PVC、StorageClass核心原理解析
2026/10/9 2:31:56 网站建设 项目流程

1. 存储入门:别再被PV、PVC、StorageClass绕晕了

接触Kubernetes时间不短了,发现存储这块一直是很多人头疼的地方。PV、PVC、StorageClass这几个概念单独看都能理解,一放到实际环境里就懵了。这不怪你,存储本身链路长、组件多,再加上Kubernetes抽象了一层又一层,很容易让人绕进去。这篇内容就是把这条链路掰开揉碎讲清楚,从底层原理到实际配置,再到碰到问题怎么查怎么解,一次说完。

先说个最朴素的理解方式。把Kubernetes集群想象成一个大仓库,PV就是仓库里已经堆好的货架,PVC就是你向仓库管理员提交的领货申请单,而StorageClass就是那个能根据申请单自动组装新货架的机器。没有StorageClass的时候,货架没了得人工去搭,有了它,申请单一提交,机器自动就把新货架造出来了。这么一比喻,三个核心对象的关系就清楚了。

这套东西解决的核心问题是什么呢?最根本的一条,Kubernetes要把存储资源和业务应用解耦。你在YAML里写Pod的时候,不应该关心底层到底用的是本地磁盘还是网络存储,也不应该关心存储空间是20G还是50G,这些应该由集群管理员统一规划,由存储系统动态分配。这不是Kubernetes矫情,而是分布式环境下应用调度到任意节点,数据的持久性和可迁移性必须靠一套标准机制来保障。

这篇文章适合谁看?刚接触Kubernetes、被存储概念搞得一头雾水的学习者,正在排查存储相关故障的运维朋友,还有需要对接存储给业务提供持久化能力的平台开发,都会从里面找到有价值的东西。我尽量把平时文档里不写的东西也翻出来聊聊,比如回收策略那些容易踩坑的细节,还有动态供给和静态供给怎么选这类实际问题。

2. 核心概念拆解:PV、PVC、StorageClass到底各管什么事

2.1 PV:承载真实存储资源的抽象层

PV(PersistentVolume)是存储资源的最终体现,它把真实的存储系统包装成一个Kubernetes资源对象。这个底层存储可以是本机磁盘、网络文件系统、云厂商的块存储,也可以是分布式存储系统。关键点在于,PV是集群级别的资源,不隶属于任何命名空间,它独立于Pod存在,Pod挂掉、重建、删除,PV的数据都还在。

创建一个PV时,核心参数有这几个:存储容量(capacity)、访问模式(accessModes)、回收策略(persistentVolumeReclaimPolicy)、存储类(storageClassName)。这里我多说一句访问模式,它决定了这个PV能被多少个节点以什么方式同时挂载。ReadWriteOnce表示只能被一个节点以读写方式挂载,ReadOnlyMany表示可以被多个节点同时只读挂载,ReadWriteMany则是多个节点同时读写挂载。要命的是,这三种模式的可用性完全取决于底层存储实现的,不是你在YAML里写什么就一定能用什么。

capacity字段在很多人看来就是个摆设,因为Kubernetes不会真的去检查底层存储有没有那么大。但你申请PVC的时候,Kubernetes是靠这个值做匹配的。换句话说,你写着100G,底层实际只有50G,短期内系统不会报错,数据写满就出大事了。所以这个值必须跟实际存储配额对齐,千万别编。

2.2 PVC:业务对存储资源的申请单

PVC(PersistentVolumeClaim)是用户对存储的请求,它声明了需要多少容量、什么访问模式、用什么存储类。PVC是命名空间级别的资源,跟Pod同生命周期,但它绑定的PV可以远超Pod的生命周期。

PVC和PV的绑定逻辑值得花点时间理解。Kubernetes会去找满足以下条件的PV:容量够用、访问模式匹配、storageClassName一致。如果条件不满足,PVC会一直处于Pending状态,对应的Pod也就一直起不来。这里有个容易忽略的点:如果不设置storageClassName,匹配行为取决于集群里是否配置了默认StorageClass。有默认的就走默认的动态供给,没有默认的就只能找未绑定且storageClassName为空的静态PV。

PVC还有一个让我又爱又恨的特性,叫做容量不小于请求。你申请5G,系统可能给你绑了一个10G的PV,这种情况在静态供给场景下经常发生。业务看到的空间是10G,但声明里写的是5G,将来扩容量的时候逻辑容易乱。所以静态供给场景下,管理员创建PV时就得把容量精确控制在刚好够用的水平,宁少勿多。

2.3 StorageClass:动态供给的自动化引擎

StorageClass彻底改变了存储供给的模式。没有它之前,管理员必须预先创建好一批PV放着,用户申请PVC时从池子里挑一个。有了它,PVC一创建,系统就会按照StorageClass里定义的模板,实时调用底层存储接口去创建一块新存储,然后自动绑定,全程不需要人工介入。

StorageClass的关键配置项包括:provisioner(供给器,对接底层存储的插件)、parameters(传给供给器的参数,比如存储类型、副本数、回收策略等)、reclaimPolicy(回收策略)、allowVolumeExpansion(是否允许扩容)、mountOptions(挂载选项)。

选对provisioner是StorageClass配置里最重要的决定。云环境里有各自的云盘插件,自建的通常用支持CSI的存储插件,比如NFS、Ceph RBD、GlusterFS这些。我个人的实践体会是,凡是生产环境,优先找带CSI接口的存储方案,生态成熟、功能齐备、社区活跃,出了问题也好排查。至于那些还在用FlexVolume之类老接口的插件,能换就尽早换。

3. 实战入门:从静态供给到动态供给的完整演进

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

静态供给是理解PV、PVC机制最直观的路径,适合测试环境和小规模场景。下面用NFS举个例子,手把手走一遍。

先准备一个NFS服务端,导出目录/data/nfs,假设服务器IP是192.168.1.100,然后在Kubernetes里创建PV:

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

这个PV定义里有几个地方要特别注意。storageClassName写的是manual,随便取的名字,作用是跟动态供给的StorageClass做区分,让静态PV不会被默认StorageClass的PVC误绑。reclaimPolicy用的Retain,意思是你删掉PVC后,这个PV不会被自动清理,数据留着等人工处理,这在测试环境里能避免误删数据。

接着创建PVC:

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

PVC声明的需求是10Gi、ReadWriteOnce、manual类。Kubernetes会去找满足这些条件的PV,上面的nfs-pv-001容量20Gi够用,访问模式匹配,storageClassName一致,于是绑定成功。这里能看到容量匹配是"大于等于"的逻辑,你申请10Gi,绑定到20Gi的PV是合法的。

然后把这个PVC用进Pod:

apiVersion: v1 kind: Pod metadata: name: nginx-storage-test spec: containers: - name: nginx image: nginx:1.25 volumeMounts: - mountPath: /usr/share/nginx/html name: web-data volumes: - name: web-data persistentVolumeClaim: claimName: nginx-data-pvc

Pod起来后,可以先检查一下绑定状态和实际挂载情况。下面这套命令我几乎每次存储实操都会用到:

kubectl get pv kubectl get pvc kubectl describe pvc nginx-data-pvc kubectl get pod nginx-storage-test -o wide kubectl exec -it nginx-storage-test -- df -h | grep nginx

正常情况下,describe输出里Phase应该是Bound,Events里能看到成功绑定和挂载的记录。df那一步能看到容器里挂载的容量,如果显示的容量是20G而不是10G,别慌,这就是前面说的"容量不小于请求"的匹配逻辑在起作用。

静态供给最大的问题是运维成本高。每块存储都要人工登记、手工建PV,容量规划全靠感觉,配多了浪费,配少了业务扩个容都费劲。所以静态供给适合做概念验证和测试环境,生产环境一般都会切换到动态供给。

3.2 动态供给:一条StorageClass让存储自动化

动态供给是生产环境的标配。核心就两步,创建一个StorageClass,然后创建PVC,存储资源的生命周期管理就自动化了。

这里用NFS CSI的provisioner做示例。环境里需要先部署好NFS CSI驱动,然后创建StorageClass:

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-csi provisioner: nfs.csi.k8s.io parameters: server: 192.168.1.100 share: /data/nfs mountOptions: nfsvers=4.0 reclaimPolicy: Delete allowVolumeExpansion: true volumeBindingMode: Immediate

创建好StorageClass后,PVC就用它来申请存储:

apiVersion: v1 kind: PersistentVolumeClaim metadata: name: app-data-pvc spec: accessModes: - ReadWriteOnce storageClassName: nfs-csi resources: requests: storage: 50Gi

这一步执行完,系统会自动完成存储创建和PV绑定。注意看PVC的Events,正常情况下能看到Provisioning成功的记录。想要更快验证效果,可以连续创建几个不同大小的PVC,然后kubectl get pv,会看到每个PVC都有对应的、自动生成的PV,容量跟你申请的一模一样。这个"容量精确匹配"就跟静态供给的"可能超额匹配"形成了鲜明反差。

动态供给还有个强悍的能力:容量扩容。如果StorageClass里设置了allowVolumeExpansion: true,就可以在线扩容PVC。CSIDriver支持扩容的前提下,只需修改PVC的请求容量然后apply:

kubectl patch pvc app-data-pvc -p '{"spec":{"resources":{"requests":{"storage":"80Gi"}}}}'

扩容操作能不能成功,取决于底层存储驱动对扩容的支持程度。有的文件系统支持在线扩展,有的需要把文件系统先扩到位。NFS CSI在这块一般问题不大。不过我建议,扩容完成后还是手动到容器里执行df -h确认一下实际可用容量,防止出现PVC状态变了但文件系统没扩的情况。扩容不可逆这件事,扩错了方向就真的没办法缩回去了,操作前务必确认好目标容量。

3.3 默认StorageClass:让PVC零配置生效

生产环境建议给集群配置一个默认StorageClass。这样用户在PVC里不用写storageClassName,系统就会自动走默认的存储类创建存储。

设置默认类的方式是给StorageClass加上一个注解:

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-csi annotations: storageclass.kubernetes.io/is-default-class: "true"

或者用kubectl命令修改已有对象:

kubectl patch storageclass nfs-csi -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

设置了默认类之后,新创建的PVC如果没写storageClassName,就会自动绑定到默认类。这点在生产环境非常重要,因为它能防止用户因为忘记写存储类名而导致PVC一直Pending。但同时也要注意,一个集群里设置了多个默认类的话,Kubernetes会拒绝为不带storageClassName的PVC动态供给,所有这类PVC都会Pending。所以默认类务必只保留一个。

4. 调参与选型:PV回收策略、访问模式、存储类参数的踩坑记录

4.1 回收策略三大选择:Retain、Delete还是Recycle

回收策略定义了PVC被删除后,PV和底层存储数据的去向,这个决策对数据安全影响极大。

Retain策略下,PV删除后,底层数据完整保留,PV进入Released状态。这个状态下的PV不会自动复活,管理员需要手动清理数据后重新使用或直接删除。对于重要数据、需要审计留存的场景,Retain是最安全的选择。代价是运维成本高,释放的PV不能自动接新活。

Delete策略下,PVC删除会连带删除底层存储卷,数据一起消失。这个策略适合临时数据、测试数据,或者数据有备份体系兜底的环境。动态供给的StorageClass默认就是Delete,这个默认值暗藏风险:用户误删PVC,整个存储卷连带数据会瞬间被清掉,很多存储有多种副本机制,但实际上没有备份的话也会是灾难。生产环境如果数据重要,强烈建议把Delete改成Retain。

Recycle策略是个历史遗留品,它会在PV释放后执行清理然后重新进入Available状态。听起来很美,但Kubernetes官方已经在逐步淘汰这个机制,现代CSI驱动基本不实现了。不要依赖Recycle。

4.2 访问模式的常见误导和存储真实能力

访问模式这个配置值得提高警惕。很多人在YAML里写了ReadWriteMany就走,完全没考虑底层存储是否支持。不同存储系统对多节点挂载的支持差异很大:块存储如云盘一般只支持单节点读写,文件存储如NFS天生能多节点读写,分布式块存储如Ceph RBD经过配置支持多节点多Pod访问,但有限制条件。

我见过最典型的故障是:新上了StatefulSet,多个副本Pod要共享数据,运维人员把accessMode配置成了ReadWriteMany,没确认底层存储实际能力。结果是Pod创建了一半,挂载时直接失败,Kubernetes调度和挂载插件全军覆没,整个应用的可用性被拖垮。

判断一个存储能不能支持某种访问模式,最靠谱的依据是看CSI驱动文档和对应的StorageClass描述,不要光看PV YAML里写了什么。YAML是声明,底层不支持就是不支持,kubelet挂载时会把问题原原本本暴露出来。

4.3 StorageClass参数选择的实际经验

StorageClass里的parameters直接决定底层卷的物理形态和服务质量,这块配置差异巨大。以Ceph RBD为例,常见的参数有pool、imageFeatures、csi.storage.k8s.io/fstype等,其中fstype决定新建卷的文件系统类型,默认是ext4,要改成xfs也能配。云厂商的卷类型参数更多,比如类型是高效SSD还是吞吐型HDD,这些直接关系账单。

实际配置时我给几个建议:一是关注参数是否匹配环境,Ceph的pool参数如果指向不存在的pool,供给会直接失败;二是不要随意改fstype,除非有明确理由,比如要做快照恢复或者对文件系统特性有特定需求;三是mountOptions的配置要谨慎,配错了会导致挂载失败,比如NFS的nfsvers参数不匹配服务端版本,整个PVC就Pending了。

5. 生产环境下的存储方案:StatefulSet场景和灾备注意事项

5.1 StatefulSet固定存储与Pod底层挂载逻辑

StatefulSet和有状态应用是PV、PVC最典型的落地场景。它的独特之处在于,每个副本Pod都能通过volumeClaimTemplates自动创建独立的PVC,并且Pod重建后依然绑定到同一个PVC上,数据天然不丢。volumeClaimTemplates的写法如下:

apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql-cluster spec: serviceName: mysql-cluster-svc replicas: 3 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0 volumeMounts: - name: data mountPath: /var/lib/mysql volumeClaimTemplates: - metadata: name: data spec: accessModes: - ReadWriteOnce storageClassName: nfs-csi resources: requests: storage: 20Gi

这套机制下的匹配关系很清晰:mysql-cluster-0绑data-mysql-cluster-0,mysql-cluster-1绑data-mysql-cluster-1,以此类推。扩容副本时新Pod会自动带新PVC,缩容时删除Pod不会删除PVC,这个行为是刻意的,因为StatefulSet本身不负责清理存储卷。

StatefulSet存储清理往往是个容易忽略的坑。许多人以为直接删掉StatefulSet就能把数据清干净,结果Pod没了,PVC还在,数据还在物理存储上躺着。对于测试环境要彻底清理,需要手动删除PVC并确认回收策略的行为,认真看StorageClass的Delete配置是否能联动清理卷。

5.2 数据备份与容灾:别让存储卷变成数据孤岛

Kubernetes里的存储卷本质上是一块远程挂载的磁盘空间,它本身不等于数据安全。有三件事必须做:备份、拷贝、巡检。

备份是第一位的事。不管底层是NFS还是分布式存储,建议定期把关键数据卷的内容备份到独立的位置。一个常见的思路是用CronJob定期创建PV快照或者直接把数据打包传送到备份存储,速度慢一点没关系,关键是别把生产卷和备份放同一个存储池,否则存储设备单点故障就全完了。

拷贝需求来自跨环境迁移,比如测试环境要复制一份数据到预生产环境。最简单的方式是把PVC挂到临时Pod上,用tar打包后在新环境解包。注意挂载到临时Pod时要选用不同的PVC,不要一边写业务一边拷数据,数据一致性没法保证。

巡检是最容易被忽视的一环。每季度检查一下关键PVC的剩余容量、PV的健康状态、存储节点的使用率,把趋势记录下来。有几回我遇到过存储容量被打满导致业务写入hang住的情况,就是因为只关注了CPU内存这些常规指标,存储这一个维度完全被忽略了。

5.3 高可用与多副本设计的前提条件

Kubernetes调度本身提供了Pod多副本能力,但存储层的多副本能力和高可用取决于存储后端,不是Kubernetes能做主的。如果底层是单节点NFS,那么所有Kubernetes的存储高可用策略都是无根之木。这种环境下一旦NFS节点宕机,全部依赖该存储的业务都会挂掉。

生产环境选存储,至少要考虑这些点:存储后端是否有分布式多副本能力,是否支持快照和克隆,CSI驱动是否支持在线扩容,故障域是否跨可用区。别贪图省事直接用单点存储扛生产,将来承担的风险远大于省下的那点运维成本。

6. 故障排查手册:从PVC的Pending到Pod的挂载失败

6.1 PVC卡在Pending状态

PVC一直Pending是第一常见的存储故障。排查顺序如下:

第一步检查StorageClass是否存在且名字拼写正确。这个错误太低频又太致命,多数人排查时完全不想看这个,结果往往是storageClassName写错了一个字母之类的小问题。看describe PVC的Events就知道,提示找不到StorageClass,基本就是拼写或者命名空间环境问题。

第二步检查事件输出,确认是找不到PV还是供给失败。静态供给场景下Pending通常是找不到匹配PV,动态供给场景下Pending通常是供给器报错。事件里会指名道姓地给出原因,比如授权失败、连接存储失败、参数错误这些。

第三步,尝试手工执行一下provisioner的操作,比如用NFS客户端直接挂载一下存储路径,确认网络连通、权限正确、路径存在。很多时候Kubernetes层的问题根源在存储底层,绕开Kubernetes直接验证存储,往往能大幅缩短排查时间。

6.2 Pod挂载PVC时一直ContainerCreating

这个情况的典型特征是Pod状态卡在ContainerCreating,事件里出现FailedMount。常见原因集中在三点:节点上没有安装必要的CSI驱动组件,挂载被安全策略阻止,底层存储节点网络不通。

排查第一步看节点到存储服务器的连通性加权限校验。如果NFS路径权限是root_squash且Pod以非root运行,mount就会报Permission denied。第二步看kubelet日志或者节点上的存储挂载日志,确认驱动是否正常加载。第三步检查CSI驱动版本是否兼容Kubernetes版本,版本不匹配是最容易漏掉的问题,很多老集群升级Kubernetes主版本后,CSI驱动没跟着升级,挂载组件静默失效。

6.3 PVC删不掉,一直Terminating

PVC删除卡住也是个高频故障。常见原因是PVC被某个Pod或StatefulSet仍引用着,或者PV进入Released状态后没有清理,Finalizer卡住。排查方式简单直接:kubectl describe pvc ,看Finalizers和相关事件,确认引用的资源清掉,必要时手动删除Finalizer,但务必先确认引用关系已彻底解除。

6.4 容器内存储空间不足的误区

最后提一个认知层面的问题。容器内df -h看到的挂载容量和宿主机看到的存储总量,是两个完全不同的视角。容器看到的是存储卷的配额,宿主机看到的是整个存储池的剩余空间。如果磁盘写满导致业务异常,优先确认挂载卷容量是否够用,其次再看存储池整体水位。很多人把注意力放在宿主机上,结果卷配额早就满了,还得绕一大圈才回过神来。

7. 从入门到精通的五条实用心得

把这套存储体系的实践做了个总结,几条经验放这里,省得大家重复踩坑。

第一,先画清楚存储拓扑再动手。PV层的存储后端是什么,供给器选谁,访问模式和回收策略怎么配,拓扑没搞清楚就上生产,后面大概率要推倒重来。

第二,静态供给适合测试,动态供给才是生产主流。能用CSI就用CSI,能用StorageClass就让系统去创建卷,人工介入越少,出错的缝隙越少。

第三,回收策略务必根据数据特性做选择。数据无价的地方坚决用Retain,Delete只留给那些"可以随时丢掉"的数据。宁可多留备份,也别心疼那点存储空间。

第四,节点维护和存储变更前先确认挂载状态。存储节点重启、网络配置变更这类操作,要先确认正在运行的业务Pod会不会受影响,必要时先驱逐Pod再操作。

第五,把存储指标纳入日常巡检范围。容量使用率、卷健康状态、CSI驱动版本、节点存储错误,这些都要有监控和告警。没有存储维度的可观测性,存储故障发现的时候往往已经晚了。

我自己在这个领域的教训就是早期的版本只看应用层,不看存储层,觉得底层是平台的事。等到生产环境的数据库因为存储空间耗尽无法启动时才追悔莫及。从那以后,任何Kubernetes集群上线前,存储规划都是第一优先级,储备容量、监控告警、备份恢复,一个都不能少。这套东西,越是在生产里摔打得多,越能体会透彻。希望这篇内容能帮你少走点弯路,把存储这块硬骨头啃下来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询