资讯动态

K8s存储实战:理清PV/PVC/StorageClass与NFS动态供给

发布时间:2026/10/5 0:10:35 来源:尧图企业网站定制
刚接触 Kubernetes 存储这块的人十个里有八个会被 PV、PVC、StorageClass 这一串名词绕晕。我最早学的时候也是看了好几篇博客例子跑通了但换个场景立刻又不会了。后来在生产环境里给有状态服务配过存储、排查过 Pod 一直ContainerCreating卡住不动的问题才慢慢把这条链路的逻辑理清楚。这篇文章我就从实战角度把这些概念拆开揉碎讲明白它们到底怎么配合、NFS 在里头又扮演什么角色顺便把我踩过的坑也一并说了。1. 从一次“翻车”说起K8s 存储为什么搞得这么抽象先说个我自己的经历。当时团队要把一套 Redis 集群迁到 K8s 上数据不能丢肯定要用持久化存储。我第一次认真去看 PV、PVC 的文档第一反应是直接用 NFS 不好吗搞这么多抽象层不是给自己找麻烦后来配置完一跑碰到问题你猜是什么我把 PV 和 PVC 都建好了但 Pod 一直显示ContainerCreating。查看事件报的是FailedMount说 volume 挂载不上。折腾了半天才发现是 Pod 调度到了另一台节点而我把 NFS 客户端依赖装漏了。这种时候你就能感觉到K8s 存储这套设计不是闲得慌才搞出这么多概念它是在帮你解耦各种责任。我们要搞清楚 PV、PVC、StorageClass 这套东西得先回到那个朴素的问题K8s 里跑容器应用怎么拿到存储最原始的办法是你在 Pod 的 YAML 里直接写死一个 NFS 路径apiVersion: v1 kind: Pod metadata: name: test-pod spec: containers: - name: app image: nginx volumes: - name: data nfs: server: 192.168.1.10 path: /data/k8s这样写有什么问题第一路径写死了。想在另一套环境复用这套 YAML你得改配置维护成本高环境多的时候容易出错。第二开发和运维的耦合。开发同学根本不关心你底层存储是 NFS 还是块存储他只想知道“我的数据放哪、能不能存”。可这样一写他被迫依赖你具体存储架构。第三权限太粗。所有 Pod 都有权限用这个路径你没法很精细地控制谁能申领多少存储。PV/PVC 这套机制就是来解决这些问题的。你得这么理解PV 是现成的存储资源PVC 是应用对存储的申请单StorageClass 是自动生成 PV 的流水线。开发说“我要 10G能读能写”运维在另一边准备好真正存储两边不用直接对话。这里还得提一嘴 K8s 和 Docker 的区别。很多人会把这两个混在一起其实 K8s 是编排平台它管的是“一组容器怎么协同工作”这件事而 Docker 只是运行时负责单机把容器跑起来。在存储这个层面也一样Docker 挂个 volume 很容易但跨节点、跨机器、容器挂了还能把数据找回给新 Pod这属于 K8s 编排层该管的范畴所以它才设计了这一整套存储抽象。2. NFS 在 K8s 里的角色最常用的共享存储“后端”聊 NFS 之前得先把它放到 K8s 存储体系里定位。K8s 本身不存数据它只负责调度和编排。数据最终落哪儿靠的是存储插件CSI 或者内置 volume 插件。NFS 就是这些外部存储中的一种——它更像是“共享文件夹”可以把一台机器上的目录通过网络挂载给其他机器用。生产环境里为什么总拿 NFS 举例因为它部署简单、兼容性好、跨节点共享不像 Local PV 那样绑死在某台机器上。你有一个 NFS 服务端随便哪台节点上的 Pod 都能挂载访问天然适合多副本共享读写的场景。我自己搭 NFS 服务端时一般这样操作比如在 Ubuntu 上apt-get update apt-get install -y nfs-kernel-server mkdir -p /data/nfs chmod 755 /data/nfs然后编辑/etc/exports/data/nfs *(rw,sync,no_root_squash,no_subtree_check,insecure)关键是这行配置里几个参数的含义rw允许读写。不加的话别人只能读。sync数据先落盘再响应牺牲一点性能换取可靠性。no_root_squash允许客户端 root 保留权限。没有这个客户端 root 会被映射成 nobody很多容器内的写操作会莫名Permission denied。insecure允许客户端用大于 1024 的端口连接 NFS 服务。K8s 节点访问 NFS 时常用高端口不加这个很容易挂载失败。改完记得exportfs -a生效。客户端那边也就是每个 K8s 节点需要装nfs-commonapt-get install -y nfs-common这一步很多人会漏。K8s 节点上如果没装 NFS 客户端工具PV 里写了 NFS 地址也没用挂载直接失败。我前面说的那次FailedMount问题就出在这。还有一个要注意的点K8s 集群里的所有节点只要是可能调度 Pod 的都得装nfs-common。别只装一台不然 Pod 调度到没装的节点又得报错。顺手说一下 NFS 版本的问题。现在主流的 NFSv3 和 NFSv4 在 K8s 里都能用但一些老项目、嵌入式环境喜欢用 v3。如果你是处理嵌入式这块——热词里有“嵌入式linux 根文件系统挂载 使用nfs v3”——那么服务端/etc/default/nfs-kernel-server里可能得注意v3 默认是兼容的。不过 K8s 场景下挂载参数可以通过 PV 里的mountOptions来控制比如想指定 NFSv3mountOptions: - nfsvers3NFS 本身还有一个常见问题就是性能瓶颈。所有 Pod 的 IO 都走一台机器数据量大之后很容易成瓶颈。但这不影响它作为学习 K8s 存储的最佳实践底座——你不需要搞一套 Ceph 或云盘才能理解 PV/PVC 的概念。3. PV、PVC、StorageClass 三者的明确分工和联动逻辑先给个表格把三个概念的任务边界划清楚后面再逐个展开。概念谁在管核心作用生命周期PV管理员/运维真正的存储资源实体描述存储在哪、多大、什么类型独立于 Pod手动或由 SC 自动创建PVC开发者/使用者应用对存储的“申请”声明需要多大、什么访问模式跟随应用命名空间绑定到某个 PVStorageClass管理员定义“怎么动态创建 PV”的模板关联 Provisioner属于集群级资源可配置默认 SC3.1 PV底层存储资源的“代言人”PV 不直接创建存储。它只是告诉 K8s“有这么一块存储它长这样。”比如说你有一台 NFS 服务器那就在 PV 里描述它的地址、路径、容量apiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv-10g spec: capacity: storage: 10Gi accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain nfs: server: 192.168.1.10 path: /data/nfs注意这里的accessModes它决定了这块 PV 能被多少节点同时挂载、以什么方式挂载。三种模式得搞清楚ReadWriteOnceRWO只能被一个节点读写挂载。适合单 Pod 使用多副本共享不行。ReadOnlyManyROX多个节点可以同时挂载但只读。适合配置文件分发场景。ReadWriteManyRWX多个节点同时读写。NFS 天然支持这一项这也是它在多副本场景受欢迎的原因之一。但是你要小心不是定义成 RWX 就真的安全了。NFS 允许并发读写可能不代表你的应用能正确处理多进程同时写同一文件。数据落在 NFS 上但 PV 本身并不真正关心底层文件系统怎么分配。它更像一张“资产清单”把一块存储资源汇报给 K8s 集群知道。3.2 PVC用户侧的申请单PVC 是应用开发者眼中的存储接口。它只管写apiVersion: v1 kind: PersistentVolumeClaim metadata: name:>apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-csi provisioner: nfs.csi.k8s.io parameters: server: 192.168.1.10 share: /data/nfs reclaimPolicy: Delete volumeBindingMode: Immediate之后用户创建 PVC 时只要声明storageClassName: nfs-csiK8s 就会根据 StorageClass 的配置自动去 NFS 上划分一个子目录再生成一个对应的 PV然后绑定。整个过程不用管理员插手。还有一个小技巧你可能会用到把某个 StorageClass 标记为默认存储类PVC 不写storageClassName的时候会自动用它。方法是加上注解metadata: annotations: storageclass.kubernetes.io/is-default-class: true生产环境中很多团队就是先搭一个 NFS 后端再定义一个默认 StorageClass此后所有有状态服务只需要写 PVC 申请存储的分配完全自动化。StorageClass 里的reclaimPolicy也值得展开说说。它决定了 PVC 删除后 PV 怎么处理Delete动态生成的 PV 会连底层数据一起删掉适合临时数据。RetainPV 会保留管理员得手动回收。适合数据要备份归档的场景。如果你用的是 NFS 动态供给并且reclaimPolicy: Retain那 PVC 删除后目录还会留在 NFS 上你得自己清理不然空间会被一点点占光。4. 动态供给实战一条 PVC 少写十行配置之前手写 PV 再建 PVC 的方式叫“静态供给”。静态供给的问题很明显PV 是死的PVC 是活的容量不对、访问模式不匹配就永远Pending。所以现在生产环境只要你后端不是太特殊基本都是“动态供给”打底。下面我把完整的实战路径走一遍从 StorageClass 到 Pod让整套链路跑通。第一步确认 NFS CSI 驱动已经部署。如果你用的集群是自建的可能需要安装对应的驱动。这里我用nfs.csi.k8s.io举例这是目前比较标准的 NFS 动态供给方案。如果你的集群没装可以找到官方部署清单直接应用。第二步创建 StorageClass。前面已经给过 YAML这里补充点细节share参数写的是 NFS 服务端导出的根路径而每个 PVC 动态创建的 PV会在这个路径下自动生成一个子目录。也就是说你不用预先为每个应用建目录CSI 驱动会帮你建好。第三步写一个 PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name: app-data spec: accessModes: - ReadWriteMany storageClassName: nfs-csi resources: requests: storage: 5Gi如果一切正常你可以用kubectl get pv看看K8s 会瞬间生成一个以pvc-开头的 PV。用kubectl get pvc看看状态应该是Bound不会卡在Pending。第四步在 Deployment 或 StatefulSet 里引用这个 PVCapiVersion: apps/v1 kind: StatefulSet metadata: name: redis spec: serviceName: redis replicas: 3 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:7 volumeMounts: - name: data mountPath: /data volumeClaimTemplates: - metadata: name: data spec: accessModes: - ReadWriteOnce storageClassName: nfs-csi resources: requests: storage: 5Gi这里我写的是volumeClaimTemplates这是 StatefulSet 特有的一种用法每一个副本都会生成一个独立的 PVC。比如 3 个副本就会创建>kubectl exec -it redis-0 -- df -h kubectl exec -it redis-0 -- mount | grep nfs能看到 NFS 挂载路径说明整条链路已经通了。这里有个值得注意的点访问模式不是你想设就能设的。StatefulSet 下 Redis Cluster 每个节点至少是 RWO。你如果用 NFS 动态供给StorageClass 并不限制 accessMode但 PV 底层是 NFS所以哪怕你写 RWO实际它也是可以跨节点 RWX 访问的。这不是问题只是提醒你别把申请模式和实际能力搞混。5. 排障链路从“卡在 ContainerCreating”到定位根因的完整思路存储这块出问题表现基本都是Pod 一直 ContainerCreating但底层原因可能千差万别。我总结了一套排查链路照着走能省不少时间。5.1 第一步看 PVC 和 PV 的状态kubectl get pvc -A kubectl get pv如果 PVC 是Pending说明 PV 没绑定上。这时候去看 StorageClass 是否存在、参数对不对kubectl get sc kubectl describe pvc namedescribe能直接看到事件里的报错。最常见的是no persistent volumes available for this claim and no storage class is setPVC 没写storageClassName或者集群没设默认 StorageClass。解决方法是显式指定一个存在的storageClassName。provisioning nfs-csi ...相关报错说明动态供给环节出问题要去看 CSI 驱动 Pod 的日志。5.2 第二步如果 PVC 是 BoundPod 却卡住这时候问题大概率不在抽象层而在实际挂载这一步。先看事件kubectl describe pod pod-name重点找事件里的FailedMount、MountVolume.MountDevice字样。然后再按情况排查我踩过一个很经典的坑节点上没装nfs-common。报错信息会特别迷惑类似mounting argument 192.168.1.10:/data/nfs failed: no supported filesystem type for 192.168.1.10:/data/nfs第一次看到这个报错我以为是 NFS 服务端不支持查了半天。其实只是因为客户端节点上没有安装 NFS 客户端工具。所以只要在集群全部节点装上nfs-common问题就消失了。另一个坑是服务端导出的路径权限不对。容器里的进程可能以 root 运行但如果 NFS 服务端没有开启no_root_squash客户端 root 会被映射为 nobody在目录上没有写权限。这时你进到 Pod 里touch一个文件会报Permission denied。查看/etc/exports确保no_root_squash写上了。5.3 第三步看 CSI 驱动日志如果排除了客户端、权限问题PVC 还是没法挂载就得看 CSI 驱动 Pod 的日志kubectl get pods -n kube-system | grep nfs kubectl logs csi-controller-pod -n kube-system kubectl logs csi-node-pod -n kube-system一般能看到更具体的错误比如 NFS 服务端连不上、路径不存在、磁盘满等。5.4 第四步对照一下访问模式和容量有时候一切正常但 PVC 和 PV 就是绑定不上。原因多半是访问模式不匹配。申请 RWX 但现存 PV 只有 RWO绑定永远不会成功。这种问题describe不会给你明显的报错只说waiting for a volume to be created, either by external provisioner or manually created by system administrator。容量也是一样的道理如果你 PV 只有 8GiPVC 申请 10Gi即便 NFS 上空间很大它也不会绑定因为 PV 声明的capacity不满足请求。5.5 第五步别忘了 K8s 网络和防火墙NFS 用的是 2049 端口如果集群节点和服务端之间有防火墙拦着挂载也会失败。这个在云环境里尤其常见安全组默认不放行 2049 端口。排查时除了盯 K8s 层这一步也不能跳过。热词里有一个“k8s控制节点master初始化显示 the api server is not healthy after 4m0.00747357s”虽然不是存储问题但我想说明一点K8s 初始化阶段的报错往往出在容器或网络组件而不是 K8s 配置本身。一次 master 初始化不健康最常见的原因是kubeadm init指定的 API Server 地址不可达或者coredns拉不下来镜像。排查存储问题的思路也一样不要盯着表象沿着依赖链路一层层找。6. 生产环境的存储选型建议直接上 NFS 动态供给也有讲究很多人问我“生产环境是不是直接用 NFS 动态供给就行”我的看法是这样如果是小团队、中小规模、或者一套跑内部系统NFS 动态供给完全够用。它的维护成本低学习成本也低出了问题你能直接登录 NFS 服务器看目录、看日志不像云盘那样黑盒。但规模上来之后有几个问题你需要提前知道单点风险。整个集群的持久化数据都放在一台 NFS 服务器上服务器挂掉所有 Pod 的读写都会受影响。要规避至少把 NFS 服务端做成主备或者用分布式文件系统如 CephFS、GlusterFS 替代。CephFS 在 K8s 里也有 CSI 驱动和 NFS 的用法几乎一样都是声明式申请只是后端能力更强。性能瓶颈。NFS 走网络延迟比本地磁盘高。数据库这种对 IOPS、延迟极敏感的服务最好别用 NFS 做主力存储。你可以在同一个集群里定义多个 StorageClassNFS 给文件类应用、日志、临时缓存本地盘或者云盘给数据库这种核心有状态服务。回收策略别默认 Delete。如果你用动态供给回收策略建议根据数据重要性区分。临时数据Delete没问题重要数据建议Retain防止误删 PVC 后数据一块被清了。我就吃过这个亏一个测试库的 PVC 被清理掉底层 NFS 目录也被删了数据全没。虽然是测试环境但教训是真疼。容量规划这块还有个容易忽略的细节。NFS 动态供给默认没有配额控制每个 PVC 只写requests但并不会真正限制某个子目录的写入量。也就是说一个应用疯狂写数据理论上是可能把整个 NFS 磁盘写满的。如果你的应用不太可控有点规模了可以考虑对 NFS 目录加配额或者直接换用支持底层配额控制的存储后端。最后给你分享一个我一直用的配置思路也是我觉得比较稳妥的组合apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-data annotations: storageclass.kubernetes.io/is-default-class: true provisioner: nfs.csi.k8s.io parameters: server: 192.168.1.10 share: /data/nfs reclaimPolicy: Retain volumeBindingMode: Immediate mountOptions: - hard - nfsvers4.2几个配置项说一下为什么这么设Retain保证数据安全nfsvers4.2在大多数现代 Linux 上性能更好锁语义也更完善hard是 NFS 的默认行为网络抖动时会持续重试而不是报 IO 错误对数据库这类应用更重要当然注意配合intr或timeo等参数否则极端情况下进程可能卡死。7. 一个排查真实案例Redis 集群 PVC 被卡住的全过程光讲概念不行还是得拿一个真实的排查场景串一遍。这案子是给 K8s 上的 Redis 集群配存储时遇到的正好涵盖了上面提到的大部分知识点。当时集群里建了 6 个 Redis 节点用volumeClaimTemplates动态创建 PVC。创建完我发现 3 个 Pod 一直Pending。查看 PVC 状态果然有 3 个Pending另外 3 个正常Bound。我干的第一件事是kubectl describe pvc看其中一个Pending的 PVC发现报错是storageclass nfs-csi not found这就很奇怪了。因为另外 3 个 PVC 明明绑定成功了说明 StorageClass 是存在的。我再kubectl get sc一看才发现nfs-data这个 StorageClass 的名字是nfs-data而不是nfs-csi。StatefulSet 的volumeClaimTemplates里写的是nfs-csi自然就找不到了。前 3 个为什么成功因为那 3 个不带 templates而是手动写了 PVCstorageClassName写对了。这问题一眼就能看出来但真正生产环境里往往一个少字母的错误就能让你排查半天。跟“k8s控制节点master初始化显示the api server is not healthy”这种初始化问题一样配置名的大小写拼写、命名空间对不对往往是第一道坎。改完storageClassNamePVC 立刻变成BoundPod 也正常调度起来了。可过了一会用户反馈 Redis 集群数据写入失败。进 Pod 看磁盘发现/data目录是空的不说写入直接报Read-only file system。查问题的思路也很有意思。我先看 NFS 服务端目录权限发现目录权限是root:nfsnobody而容器内 root 进程在挂载之后映射到 NFS 服务端后是nobody身份对整个目录没有写权限。最后修掉/etc/exports里的no_root_squash重启 NFS 服务重新挂载写入恢复。这个案例里真正出问题的点其实很简单但如果没有一套清晰的排查链路你会反过来怀疑 PV、怀疑 PVC、甚至怀疑 CSI 驱动本身。我现在的习惯是排错之前先列清单把 Pending、Bound、挂载失败、权限问题这几类分别列出对应检查项再动手。这样比东看一眼西看一眼高效太多。8. 实操经验总结记住这几条能帮你少填一半的坑啰嗦了这么多最后分享几条我总结出来的实战经验每一条都是踩过坑换来的。第一把 PV、PVC、StorageClass 想成“简历投递”的过程PV 是岗位PVC 是求职者StorageClass 是猎头。岗位符合要求求职者才有回应没有猎头岗位得自己一个个挂出来。这个类比能帮你理解八成以上绑定问题。第二能动态供给就不要静态供给。除非你有特殊需求比如某块 PV 必须绑定特定存储路径否则统一用 StorageClass 动态创建 PV。省事、不容易错、扩容也方便。第三NFS 客户端工具在每个节点上都要装好。我至少遇见三次FailedMount最终原因都出在这。先把这个装了再排查其他可能性。第四访问模式不是拍脑袋定的。NFS 可以支持 RWX但部分应用对并发访问同一目录支持很差比如 Redis、MySQL 这种数据库你硬让它 RWX数据损坏风险和并发问题会让你头疼。该用 RWO 就 RWO别贪多。第五回收策略直接影响数据存亡。Delete意味着 PVC 删除后 PV 底层目录会被清理一切数据都没了。刚上手时建议统一用Retain不理解之前先别动刀。第六挂载参数要结合业务实际。NFS 有hard、soft、noatime、nfsvers等一堆选项默认不一定是最适合你的。资料显示、日志写入这类频繁小 IO 的场景加noatime能显著降低 NFS 服务端压力。数据库场景hard加适当的timeo、retrans比soft安全得多避免网络抖动导致进程直接挂掉。这些经验不一定适用所有环境但如果你跟我一样用 NFS 走过一段生产环境的路大概率会踩到其中几条。把这套链路理解透了后面换 Ceph、换云盘其实思路都是一样的——你不是在跟具体某个存储后端打交道而是在跟 K8s 的存储抽象层打交道。搞懂这层抽象换后端只是改一个 StorageClass 的事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑