ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kubernetes持久化存储实战:NFS+PV/PVC配置与踩坑指南

Kubernetes持久化存储实战:NFS+PV/PVC配置与踩坑指南 聊到 Kubernetes 的持久化存储NFS 配合 PV、PVC 这套组合几乎是自建集群和中小团队逃不开的第一站。原因很直接NFS 部署简单、天然支持多个节点同时挂载而 PVPersistentVolume和 PVCPersistentVolumeClaim又是 K8s 里把存储和业务解耦的标准姿势。三样东西叠在一起你就能得到一个存储不绑定任何节点、业务不用关心服务器 IP的稳定底座。接下来我准备把 NFS 服务端搭建、PV/PVC 的 yaml 写法、Deployment 里怎么挂载以及我在实际运维中踩到的一堆坑从头到尾捋一遍。这篇内容不是抄官网文档是我在测试环境和生产环境反复验证过的完整过程适合刚入门 K8s 存储、或者想在自建集群里把 NFS 存储链路跑通的同学直接抄作业。1. 理清协作关系NFS、PV、PVC 各自负责什么1.1 Pod 直接挂 NFS 不行吗为什么非要绕一层 PV/PVC先说个最原始的方案K8s 的 Pod 定义里确实可以直接写 NFS 挂载比如 volumes 字段直接指定nfs.server和nfs.path。这在单机测试时很快但放到真实环境里问题立刻冒出来NFS 服务器的 IP 和路径写死在业务 yaml 里哪天迁移存储、换 IP你得把每个用到它的 Deployment 都翻出来改一遍。没有任何容量概念一个业务随时可以占满整块 NFS 空间其他业务被拖死。没有生命周期管理。谁在用这块存储、用了多少、删业务之后数据怎么处理全靠人工记录出了事只能对着日志猜。PV/PVC 这套机制就是为了解决这些问题产生的。PV 相当于管理员预先登记好的房源信息声明了这套房有多大、在哪个小区、是什么户型PVC 则是业务方提交的租房申请声明我需要多大面积、要哪种户型。K8s 调度器负责把两者撮合在一起之后业务侧只需要在 yaml 里写claimName引用 PVC完全不接触 NFS 服务器的具体信息。我习惯把它叫存储描述与存储使用的分离。这个分离带来的最大好处是业务团队只需要提需求PVC基础设施团队负责准备资源PV两边通过匹配规则对接互不干扰。1.2 静态供给和动态供给本文讲的是哪条路PV 的产生有两种方式静态供给管理员提前手工创建 PVPVC 申请时去匹配现成的 PV。简单可控但需要人工预估容量提前建好。动态供给通过 StorageClass 配合 provisioner在 PVC 创建时自动拉起一个 PV。省事但需要额外部署 provisioner 组件。很多入门文章一上来就教你搞 StorageClass 动态供给但我个人建议先把静态链路吃透。原因很简单动态供给的 provisioner 也是通过一个容器去调用 NFS API 创建目录最终的挂载原理和静态方式完全一样。如果你不理解 PV/PVC 的匹配规则不理解回收策略的差异直接跳去用 StorageClass遇到问题你会毫无线索。所以本文老老实实从静态讲起最后再补一段怎么从静态平滑过渡到动态。1.3 NFS 这种存储适合什么场景我常用 NFS 的场景可以帮你做对照多 Pod 共享读写比如多个 Web 副本都要读写同一个上传目录此时需要 ReadWriteManyRWX能力NFS 是自建环境里最容易实现的方案。没有云厂商的云盘/EBS纯裸机或虚拟机自建 K8s需要一个跨节点共享的存储介质。开发测试环境想快速给各种服务提供存储NFS 一条命令就能架起来。日志归档、文件分发这类对性能不敏感、但读写频次不低的场景。反过来数据库这种对 IOPS 和延迟极敏感、需要强一致性的负载别放 NFS 上。NFS 再稳妥也有网络开销和写入锁机制跑关系型数据库容易成为整个系统的瓶颈。这点后面我会单独分析。2. Ubuntu 端搭建 NFS 服务exports 参数与连通性验证2.1 安装和最小配置步骤服务端我用的是 Ubuntu 22.04 LTS内核版本不影响 NFS 的基本使用流程上是一致的。先安装 NFS 内核服务sudo apt update sudo apt install -y nfs-kernel-server然后创建一个共享目录。注意这个目录所在分区最好有足够的空闲空间不要放在/tmp或者系统根分区这种容易被清理的地方sudo mkdir -p /data/nfs sudo chown nobody:nogroup /data/nfs sudo chmod 755 /data/nfs接下来编辑/etc/exports。这个文件是 NFS 服务的核心配置每一行定义了一个导出目录和访问规则/data/nfs 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check)这里我用了整个内网网段作为授权范围你也可以直接写具体节点 IP比如192.168.1.31(rw,sync,no_root_squash,no_subtree_check)。配置生效后重启或刷新服务sudo exportfs -ra sudo exportfs -v sudo systemctl restart nfs-kernel-server用exportfs -v查看是否导出成功。正常情况下会输出类似/data/nfs 192.168.1.0/24这样的信息。2.2 exports 参数背后的为什么新手最容易犯的错是照抄网上配置却不理解每个参数的含义。我逐个说清楚参数作用不设的后果rw允许读写访问默认只读容器里写文件直接报权限错误sync写操作同步刷盘后再响应如果写 async性能稍好但宕机会丢数据no_root_squash保留客户端 root 用户的权限不压制为 nobody容器内如果以 root 写文件会被 NFS 映射成 nobody出现 Permission deniedno_subtree_check关闭子树检查避免文件重命名/删除时出现奇怪错误某些版本下导致可重入目录的警告fsid0给 NFSv4 提供伪根目录多目录导出时 NFSv4 客户端挂载可能失败尤其注意no_root_squash和sync这两个。容器里跑的进程如果是以 root 身份运行而 NFS 默认的 root_squash 会把 root 映射成匿名用户最终你会在 Pod 里反复看到 Permission denied。排查起来你会怀疑是 PVC 的问题其实根子在 exports。sync也是一条安全底线我见过有人为了压测性能临时开async结果服务器断电后目录里全是损坏的半截文件。2.3 客户端连通性测试别急着配 PV先把服务端的连通性验证一遍再回到 K8s 里操作。到任意一台 K8s 节点上执行sudo apt install -y nfs-common sudo mkdir -p /mnt/nfs_test sudo mount -t nfs 192.168.1.100:/data/nfs /mnt/nfs_test echo hello nfs | sudo tee /mnt/nfs_test/test.txt sudo cat /mnt/nfs_test/test.txt sudo umount /mnt/nfs_test如果这几步顺利说明 NFS 服务端和客户端之间的网络、权限、版本协商都没有问题后续配置 K8s 时就少了一半排查工作。如果手动挂载都失败不要急着去 K8s 里查先把本步骤解决。常见报错和处理我放在第 5 章。2.4 NFSv3 和 NFSv4 的选择问题服务端默认会同时监听 v3 和 v4 协议但客户端挂载时具体走哪个版本是可以指定的。我的建议是K8s 节点和容器镜像里的挂载操作统一指定vers4.1或回退到vers3避开 NFSv4.0。这里有个历史坑Linux 内核从 4.x 开始把 NFSv4.0 标记为不安全部分发行版默认禁用了它导致在 K8s 里挂载时卡在 mount 阶段、Pod 一直 ContainerCreating。而嵌入式和 IoT 场景下rk3568 这类开发板做根文件系统挂载时又偏偏依赖 NFSv3网络热词嵌入式 linux 根文件系统挂载 使用 nfs v3说的就是这个。如果你既要在 K8s 集群里用又要给嵌入式板子提供根文件系统服务端保持默认的双栈支持即可客户端按需指定版本不用在服务端做额外限制。3. PV 与 PVC 的 yaml从绑定规则到状态机3.1 PV 的 yaml 字段逐行拆解服务端就绪后我们来创建 PV。下面的 yaml 是我在项目里最常用的一份每行都有讲究apiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv-data labels: storage-type: nfs spec: capacity: storage: 10Gi volumeMode: Filesystem accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain storageClassName: nfs: server: 192.168.1.100 path: /data/nfs readOnly: false mountOptions: - vers4.1capacity.storage你给这个 PV 标注的容量。注意这只是给调度器看的标签NFS 本身不做配额限制。想真正限制单个目录用量得在 NFS 服务端用 quota 之类的机制。accessModesNFS 支持 ReadWriteMany 和 ReadOnlyMany理论上也支持 ReadWriteOnce但 RWX 是 NFS 区别于大多数块存储的核心优势。一 Pod 如果需要多个节点同时挂载必须声明 RWX。persistentVolumeReclaimPolicy这里用了 Retain保留。意思是 PVC 删除后 PV 里的数据还在管理员需要手动清理。这是新手最容易误解的点后面细说。storageClassName: 显式声明不使用 StorageClass走纯静态绑定。如果漏掉这行而集群里又存在默认 StorageClassPVC 会跑去匹配默认类导致绑定不上。mountOptions挂载参数我在这里指定 vers4.1 就是为了避开前面说的 NFSv4.0 问题。nfs.path填服务端 shared 目录nfs.server填服务端 IP。路径写错通常不会有明确报错但挂载后目录会是空的所以要仔细核对。把 PV 创建出来kubectl apply -f pv.yaml kubectl get pv nfs-pv-data状态应该是 Available表示还没被任何 PVC 绑定。3.2 PVC 的 yaml 与匹配规则PVC 的 yaml 看起来更简单apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nfs-pvc-data namespace: default spec: accessModes: - ReadWriteMany volumeMode: Filesystem resources: requests: storage: 10Gi storageClassName: apply 之后查看绑定情况kubectl apply -f pvc.yaml kubectl get pvc nfs-pvc-data kubectl get pv这里要讲清楚 K8s 是怎么把 PVC 和 PV 匹配上的三个条件缺一不可容量足够PV 的 capacity 大于等于 PVC 的 requests。accessModes 匹配PV 的 accessModes 要包含 PVC 请求的模式。storageClassName 一致都为空字符串就互相匹配如果有 selector还需标签匹配。PVC 的resources.requests.storage写多少实际就会在 PV 里占用多少这一语义但物理上 NFS 目录不会真的划出 10Gi 的空间。你可以在 NFS 服务端用du查看实际用量不受 PV 声明限制。3.3 PV 状态机的解读Available 到 Bound 再到 ReleasedPV 的状态一共四个理解这个状态机对排障帮助很大Available未被绑定。Bound已被某个 PVC 绑定此时 PVC 也能看到对应的 VolumeName。ReleasedPVC 被删了PV 被释放但因为回收策略是 RetainPV 里的数据和资源没有清理。Failed自动回收失败常见于后端存储有问题。我经常收到问题我把 PVC 删了想重建结果新 PVC 一直 Pending。 这就是因为旧 PV 进入了 Released 状态而默认策略下 Released 的 PV 不会被自动重新绑定。你需要手动做两步删除掉 PV 对象然后重新 apply 一份同样的 PV yaml它就从 Released 变回 Available新 PVC 才能绑定上去。看起来是同样的 yaml 再创建一次但实际数据始终在 NFS 目录里。这就是 Retain 策略的语义也是我先把它讲透的原因。4. 挂载进 Deployment多副本共享读写与权限修复4.1 写一个双副本 Nginx 挂载 PVCPVC 绑定好后我们来一个实战例子两个 Nginx 副本同时挂载同一个 PVC把 NFS 当作共享的网站根目录。这能直接验证 RWX 的真实效果。apiVersion: apps/v1 kind: Deployment metadata: name: nginx-nfs-demo spec: replicas: 2 selector: matchLabels: app: nginx-nfs template: metadata: labels: app: nginx-nfs spec: containers: - name: nginx image: nginx:1.24 command: [/bin/sh, -c] args: - sleep 3600 volumeMounts: - name: nfs-volume mountPath: /usr/share/nginx/html volumes: - name: nfs-volume persistentVolumeClaim: claimName: nfs-pvc-data这里我让容器起来后先 sleep方便我们进入容器验证。实际生产里不需要 command/args替换成你自己的启动命令即可。部署后检查一下kubectl apply -f deployment.yaml kubectl get pod -l appnginx-nfs kubectl exec -it nginx-nfs-demo-xxxxx -- /bin/bash进入第一个容器写入一个文件echo shared file from pod-1 /usr/share/nginx/html/index.html cat /usr/share/nginx/html/index.html再进入第二个容器你能直接看到index.html的内容。这个动作证明了两个 Pod 共享同一块存储写入后立即可见。如果你想更醒目一点可以从第二个 Pod 再写一个文件回到第一个 Pod 查看。4.2 权限问题为什么容器里明明有 root 却写不了这是 NFS 挂载场景里出现频率最高的权限问题。现象是这样的Pod 正常运行容器内touch /data/file时报Permission denied但你在宿主机上手动写同一个 NFS 共享目录完全没问题。原因在 NFS 的 root_squash 机制上。NFS 服务端默认会拒绝 root 用户直接操作共享目录把所有来自 root 的请求映射成匿名用户通常叫 nobody。而很多基础镜像包括 nginx、busybox容器内进程默认就是 root于是容器内是 root → 到服务端被 squash → 变成 nobody → nobody 没有写目录的权限。解决思路有两个方式一改 exports加no_root_squash。这是全局生效的会给所有 root 请求真实 root 的权限。适合测试环境和自己可控的内网环境。生产环境如果你想让某个 Pod 内的特定用户有权限更推荐方案二。方式二在 Pod 定义里配置 fsGroup 和 runAsUser让写入时的 uid 与你 NFS 目录的属主保持一致。例如你希望写入的 uid 是 2000那么securityContext: runAsUser: 2000 runAsGroup: 2000 fsGroup: 2000然后回到 NFS 服务端把共享目录属主改成 2000sudo chown -R 2000:2000 /data/nfs这样容器内以 uid 2000 写入就不会被 root_squash 影响也避免了给全部客户端开放 root 权限的风险。两种方式我都在项目里用过你现在只需要知道它们对应的就是 exports 参数和 securityContext 字段。4.3 PVC 删除、PV 删除、数据到底还在不在测试完功能我还建议你亲手走一遍清理链路这比背一百遍文档都管用。假设刚才的 Deployment 还在PVC 也已绑定 PV。依次执行kubectl delete deployment nginx-nfs-demo kubectl delete pvc nfs-pvc-data kubectl get pv nfs-pv-data此时 PV 的状态会变为 Released。去 NFS 服务端看一眼/data/nfs里的文件你会发现数据原封不动。这正是 Retain 策略的意义K8s 只是解绑了引用不会主动删你的数据。如果你试图直接用旧 PV 对象再绑定一个同名 PVC会发现新 PVC 永远 Pending。因为 Released 状态不会重新变为 Available。正确的复用方式是kubectl delete pv nfs-pv-data kubectl apply -f pv.yaml重新创建的 PV 会以 Available 状态上线新 PVC 就能绑定了。数据从头到尾没丢。相比之下如果回收策略是 DeletePV 删除时后端存储会一起被清理但这是由 provisioner 决定的行为。静态 NFS 场景本身没有提供自动删除目录的实现所以规范上必须用 Retain否则容易出现PV 没了、NFS 里数据也找不到归属的窘境。5. 实操中反复踩到的坑从 Pending 到 mount 失败5.1 Pod 一直 ContainerCreatingdescribe 里说 mount 失败挂载类问题在 K8s 里表现非常一致Pod 起不来kubectl describe pod的事件里有MountVolume相关报错。但报错文案不同根因也完全不同我按频率整理了一个排查表报错特征根因解决mount.nfs: Operation not permitted客户端缺少 nfs-common或内核 nfs 模块未加载apt install nfs-commonmount.nfs: access denied by serverexports 授权范围没包含该节点 IP或 root_squash 压制检查 exports 配置加 IP 白名单并 exportfs -ramount.nfs: Network is unreachable防火墙或网络隔离111/2049 端口不通放行端口或检查节点的 routesrpc.nfsd: writing to /proc/fs/nfsd failed服务端 nfs-kernel-server 未重启/加载失败重启 nfs-kernel-servernfs server ... not responding, still trying版本协商卡住v4.0 典型表现挂载选项加 vers4.1或改用 vers3我的排查顺序永远是从外到内先在 K8s 节点上手动执行mount -t nfs能挂上再查 K8s 配置不能挂上就逐参数检查。手动挂载成功后再去观察 Pod 的 yaml 是不是把 server/path 写错了或者 PVC 名字是不是和 Deployment 里的 claimName 不一致。5.2 PVC 一直 Pending明明 PV 已经创建了PVC 卡在 Pending 是最常见的问题原因无非以下几种accessModes 不一致。比如 PV 只允许 RWX但 PVC 里申请的是 RWO这时绑定永远不成功。解决把两边模式改成一致。storageClassName 对不上。我见过最隐蔽的情况是PV 里写了storageClassName: nfsPVC 里忘了写结果 PVC 去匹配集群的默认 StorageClass两条线索永远碰不上。容量不足。PV 标了 10GiPVC 要 15Gi永远匹配不上。操作建议是快而准地看输出kubectl get pv kubectl get pvc -A kubectl describe pvc nfs-pvc-dataEvents 里通常会用一句话告诉你为什么 Pending。如果你初次排障describe是信息量最大的工具别只盯着 get。另外提醒一点PV 是集群级资源不区分 namespacePVC 是命名空间级资源只在它所属的 namespace 内生效。所以我明明看到 PV 是 Available为什么 default 空间里的 PVC 不绑定这件事要先确认 PV 没有被其他 namespace 里的 PVC 占走。5.3 exportfs 后依然 access denied 的隐性原因你有没有遇到过这种情况exports 里的 IP 网段明明是包含节点地址的showmount -e也能看到共享目录但挂载时仍然 access denied我在 Ubuntu 上踩过一次原因是客户端来源 IP 走的是另一个出口 IP特别是节点有多网卡时服务端看到的是带内管理网段的 IP而 exports 里写的是业务网段的地址。排查方法很简单在 K8s 节点上执行ip addr show看挂载请求实际从哪个 IP 出去确保 exports 的网段包含这个 IP。另一个容易忽略的地方是 NFS 依赖的rpcbind服务。如果你把 2049 端口放行了但 111 端口rpcbind没放showmount可能正常真正 mount 时 RPC 协商会被防火墙拦下来。Ubuntu 的规则可以简单加一条sudo ufw allow from 192.168.1.0/24 to any port 111,2049 proto tcp sudo ufw allow from 192.168.1.0/24 to any port 111,2049 proto udp5.4 性能预期NFS 到底能跑多快别把它当本地盘用NFS 在局域网里的吞吐表现正常配置下能达到接近千兆/万兆网络的极限顺序读写通常可以跑满网卡。但随机读写和元数据操作大量小文件创建、删除、重命名会明显慢于本地盘因为它每次操作都要经历一次网络往返且受制于服务端的单机 IO。我的建议是上线前先做一轮简单的 fio 压测至少知道这条链路的基线水平后续业务说慢的时候你才有对比数据。一个很常用的测试命令fio --nametest --directory/data/nfs/fio --rwrandwrite --bs4k --size512M --numjobs4 --group_reporting如果这个数字远低于你的预期先查网络再查服务端磁盘最后才查 K8s 层。很多刚接触 NFS 的同学一看到共享存储就默认它性能很强实际上它强在共享与简单而不是强在随机 IO。跑高并发小文件业务的 Pod建议挂在节点本地盘上别往 NFS 里塞。5.5 从静态走向动态StorageClass nfs-subdir-external-provisioner 是怎么工作的把静态链路走通之后你会发现有一个繁琐点每来一个新项目都要手工建 PV 和 PVC。如果是几十上百个项目这项工作会变成灾难。这时候就该考虑动态供给了。最常用的方案是部署nfs-subdir-external-provisioner它的本质是一个 Deployment监听 PVC 创建事件然后调用 NFS 服务端的 API在共享目录下自动创建子目录再以这个子目录为 path 动态生成一个 PV。集群里的 PVC 只要声明storageClassName: nfs-client就会触发这个 Provisioner 自动完成建目录→建 PV→绑定的全过程。功能上确实省事但我要提醒一句动态供给解决的是创建 PV的自动化问题之前讲的所有细节——权限、回收策略、NFS 版本、性能基线——一个都没有消失。反而因为 PV/PVC 都是自动生成的出问题时你更难看清楚是谁创建的、参数对不对。所以纸上得来终觉浅先把静态这条路走一遍再用动态工具把它固化下来你的理解会比直接复制文档深得多。我自己把静态链路跑通之后就有意识地给所有 NFS 相关对象打上了便于识别的标签比如 PV 加storage-type: nfs、PVC 加team: data这样kubectl get pv -l storage-typenfs一眼就能筛出所有 NFS 存储资源。NFS 挂载 PV 和 PVC 这套组合本身不是新鲜技术但它衔接了存储底层和 K8s 调度层细节非常多。希望这篇内容能帮你少走点弯路尤其是 exports 参数和 PV 状态机这两个地方多看一眼前面梳理的对照表通常能省下一次通宵排障的折腾。
返回列表