资讯详情

K8s持久化存储实战:NFS配置与PV/PVC挂载全记录

📅 2026/10/22 4:02:26 | 华诺云谱 👁 阅读
K8s持久化存储实战:NFS配置与PV/PVC挂载全记录
最近一直在整理 k8s 学习笔记前面几篇都在讲集群怎么搭、Pod 怎么调度、Service 怎么暴露这些都属于怎么把应用跑起来的范畴。但真正用起来之后你会立刻撞上另一个问题容器是随时可以消失的里面的数据怎么办数据库重启丢数据、日志文件随 Pod 销毁、多个副本之间要共享同一个上传目录……这时候就必须引入持久化存储。而在所有网络存储方案里NFS 是上手最快、最不需要改造底层架构的一个。这篇笔记就是我在自己实验环境里完整配置 NFS 并让集群用上它的全过程包含服务端搭建、客户端打通、PV/PVC 映射、真实挂载验证以及几个我踩过的坑。如果你已经能把 Deployment 和 Service 玩明白但还没搞过存储这篇文章应该能帮你少走不少弯路。1. 先想清楚你的集群为什么需要一套网络共享盘在动手敲命令之前我建议先花两分钟想清楚 NFS 在你的集群里扮演什么角色。k8s 本身有各种存储抽象比如 emptyDir、hostPath、PV/PVC但它们的定位差异非常大。emptyDir 是跟着 Pod 走的临时目录Pod 一删文件就没了适合做缓存之类的场景hostPath 是把宿主机目录直接映射进容器虽然数据在节点上留下了但换个节点调度就找不到了而且多个副本同时写同一路径非常容易出问题。真正需要的是那种和节点无关、任何人都能访问的存储也就是网络存储。NFSNetwork File System在这里的优势很直接它就是一个服务端导出一块目录客户端通过网络挂载进来所有节点看到的是同一份数据。挂在 k8s 里之后不管 Pod 漂移到哪个节点挂载路径指向的都是同一块网络盘数据天然共享。这对中小规模集群来说几乎是零成本方案不需要搭分布式存储不需要买商业存储设备一台 Linux 机器就够用。当然也要说清楚边界。NFS 不是万能的它不适合做数据库主存储因为网络文件系统和数据库对 IO 延迟、锁机制的要求不太一致生产环境的数据库还是老老实实用云盘或分布式存储。NFS 最舒服的场景是日志汇聚、上传文件共享、静态资源、临时分析数据、多人协作开发环境。所以下面所有操作也都是按这个使用预期来配置的。我这次实验环境规划如下后面所有命令都基于这个拓扑角色节点名IP 地址用途NFS 服务端storage192.168.100.150专门提供共享目录k8s mastermaster192.168.100.11集群控制面k8s worker 1worker-1192.168.100.21运行应用k8s worker 2worker-2192.168.100.22运行应用操作系统选的是 Ubuntu 22.04k8s 版本 1.28 左右不同发行版命令可能有点差异但思路完全一样。2. 服务端配置一台机器作为存储池的关键细节2.1 安装 NFS 服务端组件在 storage 机器上先更新软件源再装服务端包sudo apt update sudo apt install -y nfs-kernel-server装完之后可以确认一下服务状态sudo systemctl status nfs-server sudo systemctl enable nfs-serverUbuntu 上装了 nfs-kernel-server 之后服务会自动创建systemctl enable 是确保重启机器之后依然自启。这一步没有任何坑但如果你的机器是 CentOS/RHEL包名是 nfs-utils服务名是 nfs-server原理相同。2.2 规划共享目录与权限我习惯单独建一个数据盘目录而不是散落在根分区各处方便后续管理sudo mkdir -p /data/k8s sudo chmod 1777 /data/k8s这里我用 1777 而不是普通的 777是为了让任意客户端进程都能往里面写文件同时带上 sticky bit 防误删。如果你想更严谨一点也可以不用 1777而是把目录 owner 改成 nobody:nogroup再给 755 权限。具体看你的容器里跑的是什么用户。需要解释一下为什么在 NFS 服务端这里不是简单粗暴地给 777服务端权限、NFS 导出选项、客户端挂载权限三者是叠加关系任何一层拒绝都不行。如果你在服务端就把权限卡死后面排错会很痛苦。所以我偏向于服务端放宽靠 NFS exports 选项和 ingress 网络策略来限制来源这样权责更清晰。2.3 编辑 /etc/exports精确控制谁可以挂/etc/exports 是 NFS 的核心配置文件每行一条导出规则。我用的配置/data/k8s 192.168.100.0/24(rw,sync,no_subtree_check,no_root_squash)这一行拆开看/data/k8s 是导出的目录192.168.100.0/24 表示只允许这个网段的客户端挂载比用 * 安全得多rw 允许读写sync 表示写请求必须落盘后才返回成功速度比 async 稍慢但不会断电丢数据no_subtree_check 减少目录层级变化时的额外检查提升性能no_root_squash 是很多人在 k8s 环境里踩坑的关键后面我会专门展开讲写完配置文件后执行sudo exportfs -rav输出类似exporting 192.168.100.0/24:/data/k8s可以用 showmount 检查是否导出成功sudo showmount -e localhost看到导出的路径就说明服务端配置完成了。这里再加一句exportfs 命令每次改完 /etc/exports 都要执行别改完文件不刷新就跑去客户端测试然后一脸懵。2.4 防火墙与网络放行如果你机器上开了 ufw 防火墙需要放行 NFS 相关端口。Ubuntu 上 NFS v4 通常只需要 2049 端口sudo ufw allow from 192.168.100.0/24 to any port nfs但有些环境下 rpcbind 还占着 111 端口保险起见可以同时放行sudo ufw allow from 192.168.100.0/24 to any port 111如果你用的是云厂商安全组记得在安全组规则里同样放行。这里忽略的话服务端看着一切正常客户端 mount 时就会卡在超时上。3. 集群侧打通所有节点补齐 NFS 客户端能力3.1 为什么要每个节点都安装 nfs-common这一步是很多人容易漏掉的。你可能会想NFS 客户端工具是不是只要在需要挂载的机器上装就行在普通 Linux 环境下确实是这样但 k8s 里不一样。k8s 的 Pod 调度是动态的同一个 Deployment 的副本可能被调度到任意一个 worker 节点上。kubelet 在启动 Pod 之前要根据 PV/PVC 的 spec 去执行实际的 mount 动作而这个动作依赖节点上安装的 NFS 客户端工具。如果某个 worker 节点没装 nfs-commonPod 恰好调度过去kubelet 就会报挂载失败Pod 一直处于 ContainerCreating。所以在 master 和所有 worker 节点上都执行sudo apt install -y nfs-commonmaster 虽然不跑业务 Pod但有些时候会有调试用的临时 Pod或者你会用 kubectl debug 跳到节点上排查装了没坏处。尤其是你后面可能部署一些需要动态创建 PV 的工具控制面节点也可能参与挂载验证。3.2 手动挂载测试先把底层链路搞定在 master 节点上先手动测试一下从 k8s 侧访问 NFS 服务端是否正常sudo mkdir -p /mnt/nfs-test sudo mount -t nfs 192.168.100.150:/data/k8s /mnt/nfs-test然后写个文件验证读写echo hello k8s nfs | sudo tee /mnt/nfs-test/test.txt cat /mnt/nfs-test/test.txt看到内容回显说明服务端、客户端、网络三层链路都通了。这时候再卸载测试挂载sudo umount /mnt/nfs-test之所以强调先手动挂载测试是因为这一步能帮你把问题隔离在最底层。很多 k8s 层面的报错归根到底是 NFS 服务端或网络的问题如果这些没验证过就直接上 PV遇到问题后排查链路更长人也更容易懵。3.3 关于自动挂载和 fstab 的必要性有些文章会建议把 NFS 挂载写进 /etc/fstab让节点开机自动挂载。但在 k8s 环境里我不推荐额外加 fstab 自动挂载。原因很实际kubelet 会自己处理 NFS volume 的挂载和卸载你在 fstab 里再加一个手动挂载点一方面占了服务端的连接数另一方面如果服务端暂时不可用节点开机时 fstab 挂载失败可能会影响系统启动流程。NFS 挂载交给 kubelet 就足够了你所需要做的只是确保 nfs-common 装好剩下的配置文件都由 PV 的 yaml 描述。4. 在 K8s 里描述存储静态 PV 和 PVC 的完整创建过程4.1 先理解 PV、PVC 和 StorageClass 的关系初次接触 k8s 存储的人很容易被 PV、PVC、StorageClass 三个概念绕晕。我打个比方PV 就像是一间已经装修好的实体仓库PVC 是你在应用侧写的一张我要一个多大空间、什么读写模式的领料单而 StorageClass 是自动建仓库的施工队。NFS 服务端把 /data/k8s 目录导出来之后它还只是一个 Linux 共享目录k8s 并不知道它的存在。要让 k8s 用它必须通过 PV 这个对象来声明这里有一块存储来源是某台 NFS 服务器的某个路径。然后 PVC 提出需求k8s 会把满足条件的 PV 和 PVC 绑定在一起。Pod 最终引用的是 PVC而不是直接写 NFS 地址。这样设计的好处是应用不需要关心底层存储细节换存储实现时只需要改 PV 层Pod 的 yaml 可以完全不动。这也是存储与计算解耦的体现。4.2 创建 NFS 类型的 PV先创建一个静态 PV 文件例如 pv-nfs.yamlapiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv spec: capacity: storage: 10Gi volumeMode: Filesystem accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain storageClassName: nfs-manual nfs: server: 192.168.100.150 path: /data/k8s创建并查看状态kubectl apply -f pv-nfs.yaml kubectl get pv nfs-pv刚开始 PV 状态是 Available表示已经就绪、等待 PVC 来绑定。这里解释几个关键字段capacity.storage 写 10Gi这个数字其实只是一个调度声明NFS 本身不会真的截断到 10Gi但你最好设置一个与实际规划相符的值否则后面容量管理会混乱accessModes 用 ReadWriteMany这是 NFS 最重要的卖点可以多个节点同时读写如果写了 ReadWriteOnce某些调度场景下多个副本会被限制在同一个节点persistentVolumeReclaimPolicy 用 Retain表示 PVC 删除后 PV 及其数据保留需要人工处理用 Delete 的话某些存储插件会自动清理后端数据NFS 静态配置下我建议 Retain防止误删storageClassName 写 nfs-manual这个字段必须和 PVC 里的一致否则绑定会失败nfs.server 和 nfs.path 就是服务端 IP 和导出的绝对路径务必与 /etc/exports 里的路径完全一致4.3 创建 PVC 并验证绑定关系PVC 文件 pvc-nfs.yamlapiVersion: v1 kind: PersistentVolumeClaim metadata: name: nfs-pvc spec: accessModes: - ReadWriteMany storageClassName: nfs-manual resources: requests: storage: 5Gi执行kubectl apply -f pvc-nfs.yaml kubectl get pvc nfs-pvc kubectl get pv nfs-pv正常情况下PVC 的 STATUS 会变成 BoundPV 的状态也从 Available 变成 Bound。绑定背后的匹配逻辑有三条容量要满足 PVC 的请求、访问模式要一致、storageClassName 要一致。哪条不满足都绑定不上。有一点需要提醒PVC 请求 5GiPV 给的 10Gi这种大于是被允许的绑定成功后 PVC 只能感知到 10Gi 里的逻辑容量但实际写多少只有 NFS 真正来约束。反过来如果 PVC 请求 15Gi那这个 10Gi 的 PV 就不能匹配。4.4 为什么这里用静态配置而不是 StorageClass你可能在别的文章里看到过 nfs-subdir-external-provisioner 这种工具它可以配合 StorageClass 实现动态创建子目录、自动生成 PV。那确实更自动化但我专门先把静态 PV 流程讲清楚是因为动态方案本质上也是先创建一个 NFS 支持的 StorageClass再由它内部的 provisioner 去代替你拼 PV 的 yaml。不先把静态 PV/PVC 的绑定规则弄明白动态方案出了问题你根本不知道它在背后替你做了什么。而且在我的实际项目里静态 PV 完全不丢人一块共享盘、多个应用目录划分这在中大型集群里反而更可控。你想让 A 项目的数据独占某个子目录就让它的 PV 指到 NFS 的 /data/k8s/a想让 B 项目共享另一个目录就再建一个 PV 指到 /data/k8s/b。数据目录的规划权在你手里而不是让 provisioner 随机生成一堆业务含义不明的目录名。5. 真实跑一次Deployment 挂载 PVC 并验证持久化5.1 编写挂载 PVC 的 Deployment光有 PV/PVC 不挂到 Pod 上存储还是死的。下面这个 Deployment 是我用来验证持久化的典型例子两个副本同时往 NFS 同一路径写日志apiVersion: apps/v1 kind: Deployment metadata: name: nfs-demo spec: replicas: 2 selector: matchLabels: app: nfs-demo template: metadata: labels: app: nfs-demo spec: containers: - name: busybox image: busybox command: [/bin/sh,-c] args: - while true; do echo $(date %Y-%m-%d %H:%M:%S) from $(hostname) /data/access.log; sleep 10; done volumeMounts: - name: storage mountPath: /data volumes: - name: storage persistentVolumeClaim: claimName: nfs-pvc这里最关键的是 volumes 部分它声明了使用叫 nfs-pvc 的 PVC以及容器内 /data 目录映射到这个卷上。busybox 镜像很小适合做验证两个副本同时往同一个 access.log 文件追加内容天然测试了 NFS 多副本并发写的能力。执行kubectl apply -f nfs-demo.yaml kubectl get pods -l appnfs-demo等 Pod 都进入 Running 后到服务端看数据cat /data/k8s/access.log tail -f /data/k8s/access.log你会在文件里看到两个副本的主机名交替出现说明它们确实共享了同一份文件。5.2 验证数据不随 Pod 消失而丢失这一步是持久化验证的重点也是很多人心中存疑的地方。先强制删除所有副本kubectl delete pods -l appnfs-demoDeployment 控制器会自动重新创建两个新的 Pod。等待它们 Running 后再查看日志文件tail -n 5 /data/k8s/access.log你会看到新老数据是连续追加的Pod 的 IP、名称、甚至节点都变了但文件内容没有断。这就是网络存储相比 emptyDir 和 hostPath 的核心价值数据生命周期和 Pod 解耦只要 NFS 服务端还在数据就在。5.3 从 Pod 内部反向验证挂载信息有时候你想确认 Pod 里的挂载来源可以直接进容器看kubectl exec -it nfs-demo-xxxxxxxxx -- sh mount | grep nfs输出里会看到类似192.168.100.150:/data/k8s on /data type nfs这一条信息在排查问题时非常有用。如果以后有 Pod 一直处于 ContainerCreating第一步就是看 describe 里的挂载失败信息第二步就是确认节点上能不能手动 mount 通。5.4 读写权限的实际检查容器内默认以 root 用户运行前面配置了 no_root_squash所以理论上写文件没有障碍。但如果你部署的镜像里指定了非 root 用户比如 securityContext.runAsUser: 1000那就要确保 /data/k8s 以及它下面的子目录对该 UID 有写权限。我曾经遇到过一个典型的权限坑容器以 UID 1000 运行NFS 服务端 /data/k8s 的属主是 root权限是 755结果 Pod 启动没问题但一写文件就报 Permission denied。解决方式就是在服务端调整目录属主比如 chown -R 1000:1000 /data/k8s或者把目录权限改成 777。处于安全考虑更推荐按实际容器 UID 来设置属主而不是图省事全给 777。6. NFS 在 K8s 环境里最容易翻的五个坑6.1 root_squash 引发的权限黑洞这是我在 k8s 环境里遇到最多的坑。NFS 默认开启了 root_squash即客户端以 root 身份访问时服务端会把它映射成匿名用户nobody/nogroup这本来是安全机制防止客户端 root 在服务端为所欲为。但 k8s 里很多容器默认就是 root 运行它们写文件时被 root_squash 映射成 nobody文件属主和权限立刻变得混乱甚至直接 Permission denied。我教学环境里为了省心在 /etc/exports 里加了 no_root_squash让容器 root 保持 UID 0写什么都能落盘。生产环境如果对安全要求高可以按实际容器 UID 定制方案但一定不要不做测试就默认不管这个选项。6.2 回收策略 Retain 和 Delete 的语义混淆如果你把 persistentVolumeReclaimPolicy 配成了 Delete又用的是某些自动清理的存储类那删除 PVC 后后端的 NFS 目录子文件可能被一并清理。但静态 NFS 场景下Delete 其实不会真正删除 NFS 上的数据只是把 PV 状态变 Released这会让管理员误以为后端数据也被清了。我更推荐统一用 Retain。删除 PVC 后 PV 变成 Released数据还在你可以人工备份再处理。等确认不需要了再手动删 PV然后到 NFS 服务端清理对应目录。注意Released 状态的 PV 不能直接再次绑定需要先删除再重建绑定逻辑才干净。6.3 mount 选项对故障恢复的影响kubelet 挂载 NFS 时默认使用的选项是 hard 和 intr 一类的组合但不同环境下表现可能不同。如果 NFS 服务端短暂不可用hard 挂载会让客户端一直阻塞重试进程不退出看起来像卡死soft 挂载则可能让 IO 返回错误应用反而能感知并处理。在 k8s 场景里我倾向于保留 hard 默认因为很多应用没有完整的错误恢复逻辑与其让写入静默失败不如让 IO 阻塞等网络恢复。但这也意味着你必须有 NFS 服务端的监控否则服务端彻底挂了所有关联 Pod 都会阻塞在 IO 上删除都删不掉。这种情况不是 mount 选项能解决的而是 HA 层面的问题。6.4 NFS 服务端单点故障要不要做 HANFS 服务端是一台机器它就天然是一个单点。机器一挂所有挂载它的 Pod 都受影响。我在前面的方案里没有引入 DRBD、keepalived 这些 HA 架构因为实验环境不值得。但如果在生产环境里业务对存储连续性要求高你至少要考虑NFS 服务端做冗余或者迁移到云上的托管 NAS对 NFS 服务端做定期备份监控挂载点可用性及时告警在这个标题范围内我不会展开讲 HA但你想用 NFS 承担更重要的业务时一定要意识到它只是方便不是高可用。6.5 网络分区场景下的挂载卡死与 Pod 删除困难最后一个坑是最难排查的节点与 NFS 服务端之间网络短暂中断Pod 里的进程正卡在写文件的系统调用上此时你执行 kubectl delete pod 会发现 Pod 一直 Terminating因为容器无法响应 SIGTERM。这种情况不要慌先在 NFS 服务端确认网络恢复了再强制删除kubectl delete pod pod-name --force --grace-period0但如果底层网络一直没恢复强制删除可能也不生效因为节点 kubelet 根本联系不上。这也是为什么前面说 NFS 服务端必须有监控最好服务端和集群节点在同一个可靠内网里。跨公网或跨地域挂 NFS体验会很痛苦。写在最后的一点实际操作体会整套流程走下来我的体会是NFS 本身不复杂但 k8s 里的存储链路涉及服务端、客户端、PV 定义、权限、网络好几个层面任何一层有问题都会表现为 Pod 起不来或写入失败。所以我现在每次配置新的共享目录都会严格按这个顺序操作——服务端先 exportfs 检查、客户端手动 mount 验证、再建 PV/PVC、最后才跑业务 Pod。这一步一步隔离排查的方式能省掉大量回头查问题的时间。有时候想把某个共享目录单独分配给一个应用又不想改 NFS 服务端路径我就直接用 /data/k8s 下建子目录的方式然后 PV 里的 path 指向对应子目录。这样整个集群共用一个 NFS 服务端但不同业务之间数据互相隔离管理起来也很方便。希望这篇笔记能帮你顺利把 k8s 的存储基础补齐。后续如果再深入我可能会写 NFS 动态供给方案或者聊聊 NFS 和其他存储方案的性能对比到时候再继续更新。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑