资讯动态

K8s存储四件套:PV、PVC、StorageClass与NFS关系详解

发布时间:2026/10/3 14:21:54 来源:尧图企业网站定制
K8s 里最容易把人绕晕的不是 Pod、Deployment 这些调度概念而是存储那一坨东西。PV、PVC、StorageClass、NFS四个英文缩写凑一块光看名词就够劝退一批人。我刚带团队用 K8s 跑有状态服务那会儿第一周几乎全耗在这几个概念上——不是不懂单个功能而是搞不清它们谁依赖谁、谁为谁服务。今天干脆把这块掰开揉碎结合我实际部署和踩过的坑一次把关系讲明白。这篇文章适合刚学 K8s 的运维、准备把应用容器化的后端同学以及被老板丢来搭测试环境又不想被存储问题卡死的人。1. 先把四个角色认清楚各管哪一段存储这块之所以乱是因为大多数人习惯单机视角。单机环境里磁盘就是磁盘格式化、挂载、写文件完事儿。但到了 K8s 这种多节点、多应用的集群环境存储必须变成“可描述的资源”和“可申请的请求”于是就有了 PV 和 PVC 这对搭档再往上一层有了 StorageClass再往下接入 NFS 这种具体存储后端。1.1 PV与PVC存储界的供给侧和需求侧PVPersistentVolume持久化卷是集群里的一块存储资源。它不是某个节点的本地磁盘而是被抽象成集群级别的资源对象独立于任何 Pod 存在。管理员可以在集群里提前创建一堆 PV每个 PV 描述了自己有多大容量、支持什么访问模式、底层真正存到哪里比如 NFS 的哪个目录、云盘的哪个卷、Ceph 的哪个池。PVCPersistentVolumeClaim持久化卷声明则是用户或者说工作负载的“需求单”。应用不关心底层存储是 NFS 还是云盘只关心三件事给我多大空间、我要什么访问模式、数据要持久化。PVC 把这些需求写下来K8s 负责帮忙寻找一个满足条件的 PV 并绑定。用一个类比收一下PV 是仓库里已经码好的货PVC 是客户下的订单。订单提交后系统要么从现有货物里挑一单符合要求的完成“认领”要么按订单临时生产一件出来。这个过程就引出了静态供给和动态供给两条路径后面细说。这里有个新手常踩的认知坑PV 并不直接跟某个 Pod 绑定而是先跟 PVC 一对一绑定Pod 再通过 PVC 来使用存储。也就是说Pod 里的 volume 字段填的是 claimName不直接填 PV 名字。这三层关系捋顺K8s 存储就算入门一半。1.2 StorageClass让存储按需生产的“模具”StorageClass 出现的根本原因是静态 PV 太不好使。试想几十个开发同事每人来一句“给我 20G 空间”管理员就得一次次登录集群创建 PV容量算多了浪费算少了又要重建效率极低。StorageClass 就是在这一步介入的自动化工具。StorageClass 里最重要的字段是 provisioner也就是谁来负责动态创建底层存储。不同的 provisioner 对应不同的存储后端比如阿里云云盘有 alicloud-disk 的 provisionerCeph 有 rbd 的 provisionerNFS 有 nfs.csi.k8s.io 的 provisioner。当用户创建 PVC 且指定了 storageClassName 时对应的 provisioner 会监听到这个请求主动去存储后端创建一块存储并自动包装成一个 PV 完成绑定。继续用上面的类比StorageClass 相当于一张产品规格表上面写着“这类存储由哪家工厂生产、用什么原料、产出后怎么回收”。提交了 PVC 这张订单后工厂立刻按规格表给你造一件过程不需要人工干预。除了 provisionerStorageClass 还有 reclaimPolicy回收策略、mountOptions挂载参数、volumeBindingMode绑定模式这些配置。里面最容易忽略的是 volumeBindingModeWaitForFirstConsumer它可以让 PV 等第一个使用它的 Pod 被调度到某个节点后再就近创建存储对某些存储后端非常关键后面实操部分我会再展开。1.3 NFS最常见的存储后端为什么大家都在用NFSNetwork File System网络文件系统是一套老牌协议核心玩法是一台服务器把自己某个目录导出成共享目录其他机器通过网络挂载后当成本地目录一样读写。Linux 环境里从嵌入式开发板到 Oracle RAC用 NFS 做共享存储的历史太悠久了K8s 社区里它也自然是轻量存储的常客。NFS 之所以在 K8s 里普及率这么高一句话总结就是“够用、便宜、无硬绑定”。你公司里大概率已经有一台 NAS或者说随便找台 Linux 服务器都能开 NFS。配置好导出目录后K8s 集群里的每个节点只要装了 nfs-common / nfs-utils就能挂载访问。从开发测试环境到生产环境里的一些共享文件业务它都能顶住。NFS 还有一点很讨喜它天然支持 ReadWriteMany也就是多个节点上的多个 Pod 能同时挂载同一个共享目录进行读写。对比之下普通云盘基本只能单机读写。后面会讲到这一特性在日志聚合、静态资源分发等场景里是救命稻草。当然它也有明显的短板单点、性能上限有限、不适合高并发数据库。所以真实生产环境里NFS 更多作为入门方案和中小团队低成本方案出现。2. 它们到底是怎么协作的静态绑定与动态供给这四个角色不是孤立存在的它们的协作方式分两条主线讲解。一条是偏老派的静态绑定一条是现代集群高频率走的动态供给。理解这两条线后你再看任何存储后端都能快速对上号。2.1 静态绑定管理员造好的PV怎么被PVC认领静态绑定这条线存储的“生产”和“消费”完全分离。管理员先明白业务预估提前创建 PV。比如下面的 PV 描述了一块来自 NFS 服务器的 10Gi 空间访问模式是 ReadWriteMany回收策略是 RetainapiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv spec: capacity: storage: 10Gi # 容量 10G accessModes: - ReadWriteMany # 多节点可读写 persistentVolumeReclaimPolicy: Retain nfs: server: 192.168.1.100 # NFS 服务器 IP path: /data/k8s/pv-001 # 导出的子目录之后业务方创建一个 PVC声明自己的需求apiVersion: v1 kind: PersistentVolumeClaim metadata: name: web-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 10GiK8s 存储控制器拿到 PVC 后会从集群里现有的 PV 列表中寻找匹配项。匹配条件有三条容量是否足够PV 的 capacity 大于等于请求量即可、访问模式是否包含 PVC 要求的模式、如果 PVC 指定了 StorageClass则 PV 的 storageClassName 必须一致。满足条件的 PV 会被绑定到该 PVC之后这个 PV 就“名花有主”了别的高 PVC 再用它的请求会一直 Pending。静态绑定的好处是可控性强管理员知道每块存储是谁在用坏处是前面说过的那套创建麻烦、容量规划跟不上需求、浪费率普遍偏高。实际使用中静态绑定更适合那些 PV 都是固定 NFS 目录、后续不需要频繁扩缩容的场景。2.2 动态供给StorageClass什么时候接管怎么自动创建PV动态供给是如今主流。管理员只需要事先定义好一个 StorageClass后续所有 PV 都由系统自动生成一条命令级别的运维动作也省了。整个链路可以这么走业务方创建 PVCPVC 里写明 storageClassName: nfs-csi。K8s 的 persistentvolume-controller 发现 PVC 是一个动态供给请求找到匹配的 StorageClass。StorageClass 中配置的 provisioner 收到请求按 parameters 定义去 NFS 服务端创建子目录并封装成一个 PV 对象。新生成的 PV 与 PVC 完成绑定。Pod 挂载这个 PVC开始读写数据。这里最重要的角色是 provisioner它承担着“干活”的任务。以 NFS 为例provisioner 需要具备在 NFS 服务器上创建目录的能力。有的实现是通过挂载宿主机上的 NFS 路径后在容器里为用户创建子目录有的实现走 NFS 协议直接操作。不管怎么实现对使用方来说PV 的创建、绑定、挂载都是自动完成的。动态供给还有一层好处与回收策略配合后删 PVC 时可以自动把 PV 和数据一起回收掉或者按照 Retain 策略保留数据等待人工处理。这个行为可以通过 StorageClass 的 reclaimPolicy 控制生产环境里一定要看清楚再配置不然删错就麻烦大了。2.3 为什么NFS在K8s里这么普及写到这里不少读者会问既然动态供给这么好我是不是必须上 Ceph 或者云厂商存储实际上不见得。NFS 在很多实际业务里都够用尤其是中小团队和开发测试环境。第一个原因是部署成本极低。我见过不少公司K8s 节点还是虚拟机规格都比较紧张的环境里硬上 Ceph 一堆 OSD 把资源吃光。NFS 则用现成的存储服务器或 NAS 就能开部署时间以分钟计。第二个原因是适配面非常广。NFS 天然支持多节点同时读写意味着你不需要为不同的工作负载分别准备不同规格的盘。做静态文件共享、日志收集汇总、AI 训练数据下发一个 NFS 全搞定。第三个原因跟 K8s 的设计理念有关Pod 是无状态的但业务总有有状态数据要存。K8s 官方没有绑定某个商用存储依赖屏蔽在 CSI 接口之下。NFS 作为最简单的 CSI 实现能让一个团队用极低的学习成本跑通“容器持久化”的完整链路这种价值在初期是非常大的。它的边界也明显单点故障风险高、吞吐受网卡和 NFS 服务端能力限制、不适合做数据库核心存储。明白这个边界后你就能对 NFS 有一个合理定位而不是被网上各种“NFS 又慢又不安全”的说法误导。3. 一套能直接抄作业的NFS StorageClass配置理论说得再多不如动手跑一遍。下面这套流程我实测过很多次从零搭出一个可用的 NFS 动态供给大概半小时能完成。这里以 Rocky / CentOS 系的 K8s 集群为例Ubuntu 系只需把包名换成 nfs-kernel-server 和 nfs-common 即可。3.1 半小时搭好NFS服务器首先准备一台专门的存储服务器比如 192.168.1.100。先安装 NFS 服务端软件并创建共享目录# 在 NFS 服务器上执行Rocky / CentOS dnf install -y nfs-utils mkdir -p /data/nfs echo /data/nfs *(rw,sync,no_root_squash,no_subtree_check) /etc/exports systemctl enable --now nfs-server exportfs -rav/etc/exports 里的这一行是访问控制规则星号表示允许所有来源 IP 挂载rw 表示可读写sync 表示写入时同步刷盘no_root_squash 表示客户端 root 不会被压制映射no_subtree_check 是常规性能优化项。修改完 exports 后用exportfs -rav让规则生效再用showmount -e 192.168.1.100检查共享是否可见。如果 showmount 显示目录列表说明服务端就绪。提示K8s 的每个工作节点都需要装客户端工具否则后面挂载必然报错。节点上执行dnf install -y nfs-utils完成安装即可。Ubuntu 节点则安装nfs-common。这里我踩过一个大坑exports 里用的是/data不是/data/nfs导致 K8s 里怎么挂载都提示找不到目录。还有一次是忘了加no_subtree_check高并发下偶发Stale file handle错误。这些细节虽然小但排查时很耗人。3.2 集群里装动态供给组件创建StorageClassNFS 动态供给现在官方推荐使用 NFS CSI driver。它的组件包含 controller、node 和 CSI registrar 等部署方式可以按官方仓库文档拉取 manifests 或 Helm chart。对于不熟悉 Helm 的团队直接使用官方的install-driver.sh脚本最省事# 在 K8s 节点上执行注意先按官方文档拉取相应版本脚本 curl -skSL https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/deploy/install-driver.sh | bash -s -- --snapshot 0.0.0安装完成后需要创建一个 StorageClass 指向我们刚才那台 NFS 服务器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 reclaimPolicy: Delete volumeBindingMode: Immediate mountOptions: - nfsvers4这段配置的解读如下provisioner 指定了使用 NFS CSI driverparameters 里的 server 和 share 告诉驱动 NFS 服务器的地址和根共享路径每个动态创建的 PV都会在 share 路径下面创建一个独立子目录reclaimPolicy 为 Delete意味着删除 PVC 时会同时删除 PV 和底层数据mountOptions 里强制使用 NFSv4 协议如果服务器支持挂载更稳健。如果你的集群里还跑着老一套的 nfs-client-provisioner那是早期比较流行的方案provisioner 名称通常是example.com/nfs配置原理类似。已经搭好的可以继续用新项目建议直接用 CSI 驱动毕竟官方维护、API 更新跟得上。3.3 写一份PVC和Deployment验证整个链路存储类建好后创建 PVC 看看动态供给是否正常apiVersion: v1 kind: PersistentVolumeClaim metadata: name: web-data spec: accessModes: - ReadWriteMany storageClassName: nfs-csi resources: requests: storage: 5Gi执行kubectl apply -f之后等几秒查看状态kubectl get pvc kubectl get pv正常情况下你会看到 PVC 进入 Bound 状态同时多出一个由系统自动创建的 PV状态同样是 Bound。这个 PV 的名字和 PVC 名不是一一对应关系但 storageClassName 和容量会吻合provisioner 已经把这个 PV 的底层目录指向 NFS 共享下的某个子目录了。然后写一个 Deployment 把 PVC 挂进去apiVersion: apps/v1 kind: Deployment metadata: name: nginx-storage-test spec: replicas: 1 selector: matchLabels: app: nginx-storage-test template: metadata: labels: app: nginx-storage-test spec: containers: - name: nginx image: nginx:alpine volumeMounts: - name: webdata mountPath: /usr/share/nginx/html volumes: - name: webdata persistentVolumeClaim: claimName: web-data应用起来后进入容器写入一个文件然后删除 Pod 再重建只要 PVC 不删数据都还在。我在验证这一步会顺手执行kubectl exec -it deploy/nginx-storage-test -- sh -c echo hello-nfs /usr/share/nginx/html/index.html kubectl delete pod -l appnginx-storage-test # 等新 Pod Ready 后再读一次文件 kubectl exec -it deploy/nginx-storage-test -- cat /usr/share/nginx/html/index.html能看到 hello-nfs就代表这整条链路通了。这个验证动作也是以后排查存储问题时的标准操作。3.4 已有NFS共享的对接静态PV还是直接动态着来不少团队已经有一套 NAS 或者老 NFS 共享目录业务上对不同项目给了不同子目录。这种情况下有两种做法一是继续用静态 PV把每个子目录分别创建成独立 PVPVC 去认领适合目录结构已经固定、数量不多的情况二是让 NFS 服务器直接提供一个大共享目录再靠动态供给在每个业务 PVC 创建时生成子目录日常管理更省心。我个人更喜欢动态供给。原因很简单业务方创建 PVC 后系统自动生成 PV根本不需要管理员反复写 PV 文件。唯一的注意点是对底层目录的命名要心里有数有强迫症的同事会定期到 NFS 服务器上看目录清单确认没有异常占用。4. 有状态应用和多副本场景别漏掉这些坑开发测试环境用 NFS 动态供给跑跑无状态应用没问题一旦切换到 StatefulSet、数据库这类有状态负载坑就多起来。倒不是技术多深而是很多人没提前理解访问模式和状态管理的逻辑。4.1 RWO/RWX访问模式怎么选NFS适合什么负载K8s 里 PV 有三种常见访问模式ReadWriteOnceRWO表示单节点读写ReadOnlyManyROX表示多节点只读ReadWriteManyRWX表示多节点可读写。另外 K8s 1.22 之后加了 ReadWriteOncePodRWOP单 Pod 独占。NFS 的卖点就是支持 RWX。所以在选择存储方案时如果业务明确需要多副本同时读写同一份数据比如多个 Web 实例共享上传文件目录、多副本日志采集写同一份日志目录NFS 会比较合适。但 RWX 并不代表应用本身就能安全并发写。NFS 协议层允许多个客户端读写同一个文件但应用自身是否需要协调并发写那是另一回事。比如多个 Pod 同时往同一个目录里创建不同文件没问题但多个 Pod 同时追加写同一个文件就可能出现内容交错。做文件共享要注意这个边界。RWO 更适合数据库这类应用因为单例跑不牵扯并发写。此时用云盘、本地盘或 RBD 这类块存储会更顺手性能也更可控。拿 NFS 硬抗数据库的问题后面细说。4.2 StatefulSet volumeClaimTemplates如何配合有状态应用在 K8s 里的标准姿态是 StatefulSet它可以给每个副本都申请一个独立的 PVC数据天然隔离。核心配置是 volumeClaimTemplates写法类似 Pod 模板K8s 会给每个副本自动创建 PVC名称格式是claimTemplateName-statefulsetName-序号。apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: serviceName: web replicas: 3 selector: matchLabels: app: web volumeClaimTemplates: - metadata: name: data spec: accessModes: - ReadWriteMany storageClassName: nfs-csi resources: requests: storage: 10Gi template: metadata: labels: app: web spec: containers: - name: web image: nginx:alpine volumeMounts: - name: data mountPath: /data创建后执行kubectl get pvc会看到>

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

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

免费获取报价 →
↑