
在Kubernetes里跑无状态应用Deployment一滚动更新老Pod一删新Pod一起怎么折腾都不慌。可一旦涉及数据库、文件服务、消息队列这种有状态应用情况就完全不一样了。Pod本身是临时资产容器里写的任何数据随着Pod销毁就全没了。业内常说的那句容器是无状态的指的就是这个层面。那生产环境里MySQL、Redis、ES这些数据落哪儿总不能每次重启都从零开始吧。这就绕不开今天要聊的PVPersistentVolume和PVCPersistentVolumeClaim。我最早接触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: WaitForFirstConsumerPVC只需声明storageClassName是alicloud-disk-essdKubernetes就会自动去阿里云买一块ESSD云盘创建PV并绑定。这才叫按需使用。3.3 provisioner的选型逻辑provisioner是StorageClass的核心它决定了底层存储从哪里来。不同云厂商的provisioner各不相同阿里云有diskplugin.csi.alibabacloud.comAWS有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和PVCNFS服务器就绪后在集群里创建PVapiVersion: 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然后创建PVCapiVersion: 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的PodapiVersion: 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-claimPod启动后进入容器验证一下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一直Pendingkubectl describe才是第一排查手段PVC创建之后一直处于Pending状态这是最最常见的故障。大多数人的第一反应是是不是网络有问题实际上80%的情况都出在匹配规则上。每当我看到PVC卡在Pending第一件事就是跑kubectl describe pvc nfs-data-claimEvents字段会把匹配失败的原因直接告诉你是找不到匹配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是DeletePVC一删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-provisionerRancher出的那个来模拟动态供给几乎零成本体验也和云盘动态供给基本一致。PV和PVC这套抽象熟练之后再看Kubernetes的存储体系整个脉络会清爽很多底层是CSI插件在跟真实存储设备打交道往上是StorageClass定义存储模板再往上是PV代表一块具体的存储资源PVC则是业务侧提出的使用申请最上面的Pod只是按需引用PVC。每一层各司其职出问题的时候顺着这条链路一查基本就能定位到根因。希望这篇能帮你把这块拼图补上。