首页 / 资讯中心 / 文章详情

Kubernetes存储核心:PV/PVC原理、绑定机制与实操避坑指南

Kubernetes存储核心:PV/PVC原理、绑定机制与实操避坑指南 ★ FEATURED ARTICLE
Kubernetes系列写到第八篇今天聊存储。前面几篇把Pod、Deployment、Service、Ingress这些常用资源都过了一遍大部分场景下应用跑起来没问题可一旦涉及到数据持久化很多人就开始犯怵了。尤其是当你用Deployment跑MySQL、Redis这类有状态服务Pod一重建数据就丢那种感觉我太熟悉了。所以这篇我打算好好拆解一下Kubernetes里的持久化存储核心——PV和PVC把概念、机制、实操和踩坑一次讲透适合刚接触K8s存储的开发者也适合已经被数据持久化问题坑过的运维同学。1. 容器存储的痛点与PV/PVC的整体设计思路1.1 容器世界里的数据天生就是短命的先说一个最朴素的场景。你在Kubernetes里部署了一个数据库数据写在容器的文件系统里。某天Pod因为节点资源紧张被驱逐或者你发了个新版本Deployment滚动更新把旧Pod换掉了等新Pod起来之后你会发现之前写入的数据全都没了。这是我见过太多新手踩进去的第一个坑而且坑得很彻底。根本原因在于容器本身的设计哲学就是不可变基础设施。容器里的文件系统跟着容器走容器没了文件系统也就没了。你就算在运行中的容器里用kubectl exec进去写了个文件只要Pod一重建这些文件就跟着旧容器一起被销毁了。Kubernetes官方也提供了一些基本的临时存储方案但都有明显局限。emptyDirPod创建时分配一个空目录Pod存在期间一直在Pod一旦被删除目录里的数据也跟着销毁。适合做Pod内多个容器之间的共享临时文件或者缓存目录绝不适合放正经数据。hostPath直接把宿主机的一个目录挂载进容器。Pod重建数据还在但问题更大——Pod不一定会被调度回原来的节点你写在这台机器上的数据下一次Pod跑到另一台机器上就找不到了。而且hostPath完全绕过了Kubernetes对存储资源的调度和管理。所以Kubernetes需要一个更抽象、更灵活的方案让存储资源和Pod解耦让数据可以在Pod之外独立存在、独立管理。这就是PV和PVC出现的根本原因。1.2 PV和PVC到底是什么把存储资源从Pod中彻底解耦出来PV和PVC全称是PersistentVolume和PersistentVolumeClaim翻译过来就是持久化卷和持久化卷声明。两者配合使用解决了前面说的数据跟着Pod陪葬的问题。我习惯用一句话概括它们的关系PV是存储资源的提供方PVC是存储资源的申请方Pod通过PVC间接使用PV。打个比方PV就像停车场里的固定车位PVC就是你申请的停车许可。你不需要亲自去和车位打交道只需要拿着一张停车许可找到一个符合要求的车位停进去就可以。应用通过PVC去声明我需要这么大小的存储空间访问模式是什么样Kubernetes就会帮你找到合适的PV把两者绑定起来。为什么要拆成两个对象核心原因是解耦。在传统的运维场景里存储通常由专门的存储管理员负责。应用开发者只需要告诉管理员给我一个20GB的共享存储不需要关心这块存储底层是NFS还是Ceph不需要关心它在哪个机柜。PV和PVC本质上就是这种分工在Kubernetes中的体现管理员负责创建和维护PV开发者只需要声明PVC即可。还有一个重要优势是PVC可以脱离了具体的存储实现来使用。你在YAML里引用的是PVC这个名字底层PV换了、存储后端换了应用本身不需要做任何改动。这对应用的可移植性是巨大的提升。2. PV/PVC的核心机制从绑定到使用的完整链路2.1 PVC怎么找到合适的PV绑定规则与storageClassName的桥接作用弄清楚了PV和PVC的关系接下来要搞清楚的是它们之间到底怎么绑定。这直接关系到你写出来的YAML能不能用。PVC和PV的绑定不是随机配对的而是按照一套匹配规则来完成的。一套完整的绑定过程我拆成了四个维度。第一个维度是容量匹配。PVC在声明的时候会写清楚需要的存储大小比如requests.storage: 5Gi那么Kubernetes在找PV的时候就会去找容量大于等于这个数值的PV。注意是大于等于不是严格相等。第二个维度是访问模式匹配。PVC会声明自己需要的访问模式比如ReadWriteOnce那PVC只能和同样支持ReadWriteOnce的PV绑定。如果PV支持的是ReadWriteMany只要它也同时支持RWO理论上也可以绑定因为匹配是按PV的访问模式集合包含PVC的请求来判断的。第三个维度是存储类匹配这是容易被忽视但是最关键的一条。PVC里如果写了storageClassName那它只会绑定同名的StorageClass创建的PV或者显式指定了同样storageClassName的静态PV。两边都写了、写的不一致直接绑定失败。如果PVC没写storageClassName而集群里配置了默认StorageClass那会用默认值走动态供给如果既没写也没有默认StorageClass那PVC只能尝试匹配那些没有设置storageClassName的静态PV。第四个维度是一对一的绑定关系。每个PV在一个时间点只能绑定一个PVC一旦绑定成功这个PV就不会再接受其他PVC的请求了。整个绑定完成的标志是PVC的Status从Pending变成Bound。在这个状态下你看PV的STATUS也会从Available变成Bound并且PV上会记录当前绑定的是哪个PVC。2.2 访问模式、回收策略与卷模式三个容易被忽略的关键参数除了绑定规则PV/PVC还有三个参数在实际使用中极其关键而且很多人会在这里栽跟头。我分开来说。先看访问模式Access Modes。这是用来描述存储卷可以以什么方式被节点和Pod使用。访问模式缩写含义ReadWriteOnceRWO卷可以被单个节点以读写方式挂载ReadOnlyManyROX卷可以被多个节点以只读方式挂载ReadWriteManyRWX卷可以被多个节点以读写方式挂载ReadWriteOncePodRWOP卷可以被单个Pod以读写方式挂载K8s 1.22这里有个常见的误解RWO里的Once指的是一个节点而不是一个Pod。在同一节点上你可以跑多个Pod同时使用同一个RWO卷。但是如果Pod被调度到了不同节点RWO卷就挂不上去了。很多人在多节点集群里遇到挂载失败排查到最后发现就是访问模式的问题。再看回收策略Reclaim Policy。这是PV的一个属性决定了PVC被释放之后PV和底层数据要怎么处理。回收策略行为适用场景RetainPVC释放后PV变为Released状态底层数据保留需要管理员手动处理数据需要备份、审计、无法接受自动删除的正式环境DeletePVC释放后PV和底层存储数据会被自动删除动态供给的云盘/块存储删了不心疼的测试环境Recycle对卷执行rm -rf清空数据然后恢复为Available已废弃新版本不推荐使用默认情况下静态创建的PV回收策略是Retain动态供给的StorageClass可以在定义时指定回收策略通常默认是Delete。在实操部分我会具体演示这两种策略的差异。最后是卷模式Volume Mode。这个参数控制创建的是文件系统卷还是块设备卷。默认是Filesystem也就是挂载成一个有目录结构的文件系统。如果你设置为Block那么挂载出来的就是一个裸块设备适合数据库这类需要直接操作块设备的场景。一般场景用默认值就够了。2.3 StorageClass动态供给为什么你需要它PV的创建有两种方式。第一种叫静态供给管理员提前创建好一批PV等着PVC来匹配。这种方式在小规模场景下够用但问题很明显你无法预知应用到底需要多大的存储PV创建少了不够用创建多了浪费。而且每次有新需求都要手动建PV运维成本很高。PVC匹配不到PV就会一直Pending应用起不来。第二种方式就是动态供给。你不需要手动创建PV只需要定义一个StorageClass然后声明PVC的时候指到这个StorageClass上。Kubernetes会根据PVC的请求通过StorageClass里指定的存储插件provisioner自动创建一个满足要求的PV然后完成绑定。StorageClass的YAML长这样apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: standard-storage provisioner: example.com/nfs parameters: server: 192.168.1.100 path: /exports reclaimPolicy: Delete volumeBindingMode: Immediate几个关键字段我解释一下。provisioner指定谁来帮你动态创建存储空间。不同的存储后端有不同的provisioner名称比如一些云厂商自己的CSI插件或者开源的NFS动态供给插件。写错了PVC创建不出来。parameters传给provisioner的参数不同存储后端支持的参数不一样。NFS类的一般要指定服务器地址和导出路径。reclaimPolicy和PV里的同名参数含义一致决定动态创建的PV在释放后是否删除底层数据。volumeBindingMode指定绑定模式有Immediate和WaitForFirstConsumer两种。Immediate是PVC一创建就立即绑定PVWaitForFirstConsumer是等Pod调度完成之后才进行绑定。关于WaitForFirstConsumer这里有一个比较容易忽视的点。如果你给PVC声明了一个很大的存储容量而集群里只有某些节点的存储是充足的Immediate模式可能会让PVC绑定到一个Pod根本调度不过去的存储位置导致Pod一直Pending。WaitForFirstConsumer会把绑定的时机推迟到Pod调度之后确保存储所在的节点Pod可及。3. 实操环节静态供给和动态供给完整跑一遍3.1 环境准备与前置检查理论聊得再多不如亲手跑一遍。这一节我会带你把静态供给和动态供给都完整走一遍用到的命令和YAML都可以直接复制参考。在开始之前你需要准备一个可用的Kubernetes集群。无论你是用云厂商的托管集群还是本地环境里的单节点集群我这边用一个单节点的模拟环境做演示平时自己搭测试环境也可以用类似方式下面这些检查项都建议先过一遍。kubectl cluster-info kubectl get nodes kubectl get sc第一条命令确认集群连接正常第二条确认节点状态是Ready第三条确认集群里有没有可用的StorageClass。我个人习惯在动手之前先清空可能干扰的实验对象避免看到旧数据造成误判。你可以新建一个独立的命名空间来做实验做完直接整个命名空间删掉干净利落。kubectl create ns storage-demo3.2 静态供给实操手动创建PV并绑定PVC第一步创建PV。我先用hostPath类型做演示别急着在生产环境用hostPath这里只是走通概念。生产环境请换成你的真实存储后端。apiVersion: v1 kind: PersistentVolume metadata: name: pv-demo-hostpath spec: capacity: storage: 5Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: manual hostPath: path: /data/k8s-pv-demo说一下这个PV的每个关键字段。capacity.storage5Gi代表这个PV可以提供5GiB的存储空间PVC如果要绑定它请求的容量不能超过这个值。accessModes只允许单个节点读写所以后续PVC也必须声明ReadWriteOnce才能匹配。persistentVolumeReclaimPolicyRetainPVC释放后数据会保留在宿主机目录里适合实验验证数据不丢。storageClassName写的是manual。注意这里手动指定了一个自定义的存储类名称PVC那边如果也写了storageClassName: manual两边才能匹配上。如果两边都没写而集群又配置了默认StorageClass那这个PV大概率不会被匹配到因为它不属于默认存储类。hostPath.pathPV在宿主机上映射的目录。实验前建议提前建好这个目录否则Pod启动时可能会因为目录不存在而报错。创建PV后查看状态确认是Available。kubectl apply -f pv-demo-hostpath.yaml kubectl get pv pv-demo-hostpath第二步创建PVC。apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-demo namespace: storage-demo spec: accessModes: - ReadWriteOnce storageClassName: manual resources: requests: storage: 5GiPVC声明了5Gi的存储需求访问模式ReadWriteOnce存储类manual。应用之后正常情况下PVC会直接从Pending变成Bound因为前面创建的PV完全匹配。kubectl get pvc -n storage-demo kubectl get pv pv-demo-hostpath这时候去看PV状态应该变成Bound了下面会多出一行CLAIM字段显示绑定的命名空间和PVC名称。第三步创建一个Pod来使用这个PVC。apiVersion: v1 kind: Pod metadata: name: pod-pvc-demo namespace: storage-demo spec: containers: - name: writer image: busybox command: [/bin/sh, -c] args: - while true; do echo data written at $(date) /mnt/data/log.txt; sleep 5; done volumeMounts: - name: data mountPath: /mnt/data volumes: - name: data persistentVolumeClaim: claimName: pvc-demo这个Pod做的事情很简单往挂载目录下的log.txt里不断追加一行带时间戳的文本。等它跑起来之后我们验证一下数据到底有没有写进去。kubectl exec -it pod-pvc-demo -n storage-demo -- tail -n 3 /mnt/data/log.txt如果你看到类似data written at Mon ...这样的内容说明数据已经写入PV对应的宿主机目录了。这时候你可以把Pod删掉重新用同一个PVC再起一个Pod看看数据还在不在。kubectl delete pod pod-pvc-demo -n storage-demo kubectl apply -f pod-pvc-demo.yaml kubectl exec -it pod-pvc-demo -n storage-demo -- cat /mnt/data/log.txt | wc -l你会发现文件还在行数还在增长。这就是持久化的意义Pod没了数据还在换个Pod接着用。3.3 动态供给实操用StorageClass自动创建存储静态供给演示完了接着看动态供给。动态供给的核心是StorageClass它负责在PVC创建的时候自动去存储后端申请空间。我这里用一个通用的NFS动态供给组件做演示。实际环境中你需要先把这个组件部署到集群里并且它要能访问到NFS服务器。具体部署步骤不同组件有差异但关键是StorageClass里的provisioner名称要和组件注册的名称一致。apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: example.com/nfs parameters: server: 192.168.1.100 path: /exports reclaimPolicy: Delete volumeBindingMode: Immediate注意这里的provisioner是example.com/nfs对应你部署的那个动态供给组件注册的标识。配置好后创建PVC。apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-dynamic-demo namespace: storage-demo spec: accessModes: - ReadWriteOnce storageClassName: nfs-storage resources: requests: storage: 2Gi创建完PVC后立刻查看PVC和PV的状态。kubectl get pvc -n storage-demo kubectl get pv正常情况下你可以看到PVC处于Bound状态同时集群里自动多出了一个PV而这个PV你从来没有手动创建过。它是由StorageClass动态生成的。你甚至可以去存储后端看对应目录已经自动创建出来了。这就是动态供给的核心价值你不需要预先创建任何PV存储是按需供应的。3.4 验证持久化与回收策略的效果实验做到这里建议把静态供给和动态供给两种方式下的数据持久化都验证一遍还有一个关键点是要理解回收策略。拿刚才的静态PV来说我把PV的回收策略设置成了Retain。现在做一步实验删除PVC。kubectl delete pvc pvc-demo -n storage-demo kubectl get pv pv-demo-hostpath你会看到PV的状态从Bound变成了Released。注意Released不是Available这个PV目前不能用于绑定新的PVC。这就是Retain策略的特点数据还在但Kubernetes不自动处理需要管理员决定怎么处理这些数据——要么手动清空后重新设置为Available要么把数据归档出来。为什么会这样设计因为保留数据是安全优先的选择。万一PVC里跑着的数据你还没备份直接删掉就全完了。Retain策略给了你一个缓冲期。而动态供给场景下如果StorageClass指定了reclaimPolicy: Delete删除PVC时PV会被直接删除对应的后端存储也会被清理。数据不存在了所以在生产环境用动态供给的时候一定要确认底层存储有自己的备份机制。4. 常见问题排查与实践避坑速查手册4.1 PVC一直Pending先按这三个方向排查PVC一直Pending是存储场景里最常见的故障而且在大多数情况下原因就藏在你的YAML里。第一个排查方向是看有没有匹配的PV。如果你用的是静态供给那要先确认集群里有没有满足条件的PV。这个条件包括容量足够、访问模式匹配、storageClassName一致。缺一个PVC就悬在那儿一直Pending。用下面这条命令看PV的当前状态kubectl get pv如果列表是空的那显然没有匹配的PV。如果有PV但状态是Released或者Failed也不能和新的PVC绑定。Released的PV需要管理员处理后才能回到Available。第二个排查方向是storageClassName的对应关系。这是新手最容易犯的错误。PVC指定了storageClassName: fast-storagePV那边写的也是fast-storage才能匹配。如果你想要的是动态供给那要确认集群里有没有名为fast-storage的StorageClass没有的话PVC也会一直Pending。还有一个隐蔽的问题是PV没有写storageClassNamePVC也没写但集群里配置了默认StorageClass。这种情况下PVC会走动态供给不会去匹配你那个没写storageClassName的静态PV。要想让静态PV和这样的PVC匹配可以在PV和PVC上都显式指定storageClassName: 空字符串表示不使用默认存储类。第三个排查方向是看PVC和PV的容量、访问模式。容量不能超过PV的容量访问模式PV要包含PVC请求的模式。这些都是硬条件。排查的时候kubectl describe pvc的输出非常关键。Events字段里经常会直接写明匹配失败的原因养成看Events的习惯能省不少时间。4.2 访问模式与调度限制RWO卷为什么挂不上去前面提过RWO是单节点读写这个限制在实际使用中经常引发问题。最常见的场景是你的PVC声明了ReadWriteOncePod副本数是3并且Deployment没有配置任何调度约束。这时候你会发现只有一个副本能正常挂载卷另外两个副本一直处在ContainerCreating状态报错信息通常是volume node affinity conflict或者multi-attach error。为什么会这样因为三个副本被调度到了三个不同的节点而RWO卷只允许一个节点挂载。解决办法有三个方向。第一把访问模式改成ReadWriteMany前提是底层存储支持。hostPath不支持NFS和CephFS这类共享文件系统支持。第二给Deployment加反亲和性让所有副本调度到同一个节点。这种做法不推荐在生产环境用因为你把一个节点的故障风险无限放大了。第三用StatefulSet配合专用存储方案每个副本拥有独立的RWO卷互不干扰。这是有状态应用的标准解决方案。还有一个我需要特别提醒的细节在云厂商的托管Kubernetes集群里RWO卷通常会绑定到可用区。你创建了一个RWO卷在A可用区然后Pod的调度规则恰好限制在B可用区这个卷就永远挂载不上。所以用云盘的时候要么让Pod跟着卷走用拓扑约束要么在设计之初就明确卷和Pod所在的可用区保持一致。4.3 数据残留与删除顺序删完PVC和PV数据去哪了很多人在测试环境里做实验的时候都经历过这种情况创建了PV和PVC写了一些数据然后顺手把PVC和PV都删了想重新来一遍。结果发现主机上的数据目录还在甚至重新创建同名PV之后还能看到上次残留的文件。这就是回收策略在起作用。静态PV默认是Retain删除PVC不会自动清理数据PV也只会变成Released。如果你要彻底清理需要手动删除PV然后到存储后端去删除数据目录。有一个小技巧对于实验环境如果你希望删PVC的时候连数据一起清掉可以把回收策略改成Delete。但这里有个前提只有动态供给创建的PV会被Delete策略真正删除底层存储。静态创建的PV即便你在YAML里写了reclaimPolicy: DeletePVC删除时也可能不会自动删除底层数据因为静态PV没有对应的存储插件去执行删除操作。所以我的建议是生产环境用动态供给让回收策略的语义清晰可预期测试环境用静态PV记得在删除PV之后手动清理宿主机目录避免下次实验被残留数据干扰。4.4 权限问题Pod里写不进数据多半是权限配置不对还有一个非常隐蔽的问题很有代表性。PVC成功绑定Pod也正常启动了但是应用启动之后一直报写入失败或者创建不了文件。终端里看挂载目录明明就在那里权限看起来也没问题可就是写不进去。排查下来十有八九是存储后端目录的所有者和Pod里运行的用户ID不一致。NFS或一些共享文件系统挂载时卷的根目录默认属于root权限是755。你的应用以非root用户运行在挂载目录下写文件的时候就会被权限挡住。解决办法是在Pod的安全上下文里设置fsGroup让Kubernetes把挂载卷的组所有权改成指定值。spec: securityContext: fsGroup: 1000 containers: - name: app securityContext: runAsUser: 1000这里有个组合逻辑需要理清楚fsGroup是在卷挂载之后由Kubelet对卷的根目录执行的属组变更。它解决的是卷层面的组权限问题runAsUser则是定义容器内进程的用户ID。两者配合才能让应用以非root身份正常写入持久化数据。4.5 我的经验清单给刚接触PV/PVC的你的几点建议最后把我这几年用K8s存储踩过坑后的几条经验整理成清单你可以直接抄作业。第一能用动态供给就别用静态供给。手动创建PV在一个几十台节点的集群里还能应付到了几百台节点的规模PV的数量和存储的利用率会让你头疼到怀疑人生。StorageClass PVC才是坐标系的正确打开方式。第二永远给PV和PVC加上storageClassName不要依赖隐式匹配。显式声明让绑定规则清晰透明也避免集群里出现多个默认StorageClass时的混乱。第三PVC的容量申请要克制。动态供给的存储通常按量计费往大了申请意味着资源浪费和成本上升。5Gi够用就写5Gi不够了可以扩加上StorageClass支持扩容的话运维成本并不高。第四删除操作前要想清楚回收策略。生产环境用Retain配合底层备份测试环境可以接受Delete但要能容忍数据没了。最怕的是你以为是Delete结果是Retain数据一直占着空间账单一直在跳。第五RWO卷一定是配合StatefulSet使用的。顺便提一句Kubernetes官方对PV的维护思路也是尽量把存储细节下沉到存储层K8s层面只做编排和调度Kubernetes主力去保证你的应用和数据之间的连接是稳定、可恢复的——这也是PV/PVC设计得这么抽象的根本原因。结合前面几篇的内容到这里你已经能把无状态应用和有状态应用都在Kubernetes上跑起来了。我个人在实际操作中的体会是PV/PVC这套抽象模型真的很值得花时间理解透它不只是一个存储功能更是理解所有存储类问题的主线。多花点时间把这篇里的实验从头到尾做一遍后面你在生产环境里遇到存储问题的时候脑子里会有一个很清晰的排查地图。
阅读完成 · 觉得有帮助?
咨询建站