ARTICLE DETAIL

资讯详情

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

Kubernetes 集群 NFS 持久化存储方案:动态供给与避坑实践

Kubernetes 集群 NFS 持久化存储方案:动态供给与避坑实践 最近在 CentOS 10 集群上做 Kubernetes v1.35 的存储改造NFS 持久化存储算是解决多个 Pod 共享数据最直接的手段之一。调试过程中踩了一堆坑从服务端 exports 配置到 K8s 侧 StorageClass 的部署再到 PV/PVC 绑定异常每个环节都有容易翻车的点。这篇文章不绕弯子直接梳理一套可以照着做的 NFS 持久化存储部署方案适合正在搭集群或者要给已有集群补存储能力的同学参考。这套方案以 Kubernetes v1.35 集群为背景存储端用 NFS 共享目录通过动态 Provisioner 把 NFS 变成可自动创建 PV 的 StorageClass最终实现用一条 PVC 声明就能拿到一个持久化目录的效果。整条链路并不复杂但很多细节会影响挂载成败比如 NFS 版本、客户端工具、目录写权限、Provisioner 的权限模型稍不注意就会卡在 Pending 或者挂载失败上。我会把实际环境里验证过的步骤、参数和排查思路全部写出来。1. 整体设计思路为什么用 NFS 承接 K8s 持久化存储1.1 云原生存储场景下 NFS 的位置容器天生是无状态的Pod 重建后本地文件就会消失所以有状态应用必须把数据放到外部存储上。Kubernetes 里最常见的持久化方案无非三种本地目录、分布式存储、共享文件系统。本地目录用 local PV 或 hostPath性能最好但数据绑死在节点上Pod 调度到其他节点就访问不到Ceph 或者云厂商的块存储能力很强但要么需要独立维护一套分布式集群要么需要对接云 IaaS对一个小规模内部集群来说运维成本明显偏高。NFS 属于共享文件系统天生就是给多客户端同时读写用的在 Kubernetes 里对应 ReadWriteMany 访问模式天然适合“多个 Pod 读同一份配置”“多个副本写同一个共享目录”这类场景。NFS 最讨喜的地方是通用和简单。一个 Linux 节点装好 nfs-utils 就能配服务端客户端通过标准 NFS 协议挂载不需要额外驱动。对于 CentOS 10 这种自带 firewalld 和 SELinux 的系统只要把端口和布尔值配置好基本不会遇到什么诡异的兼容性问题。我习惯把 NFS 作为K8s集群的默认 StorageClass不只是因为它配置少更关键的是后续扩容只需要加目录和磁盘不用动 K8s 配置。1.2 动态供给与静态 PV 的取舍接入 NFS 有两条路静态 PV 和动态 StorageClass。静态 PV 的思路是先在 NFS 服务端手动建好目录然后在 K8s 里用 PersistentVolume 描述这个目录再创建 PVC 去绑定。这种方式适合少量固定存储比如日志目录、备份目录PV 和 PVC 一一对应可控性强。但缺点也明显每次申请新存储都要手动建目录、手动写 PV集群大了以后 PV 数量会变得非常难维护。动态供给则完全不同。运维只需要部署一个 Provisioner 组件它会监听集群里的 PVC 对象一旦发现 PVC 声明了某个 StorageClass就自动在 NFS 服务端创建子目录并生成对应的 PV。我这次用的是 nfs-subdir-external-provisioner它会在 NFS 共享目录下按“namespace-pvcname”的格式创建子目录实现一个共享目录下隔离多个业务数据的效果。动态供给的价值不只是省事它还能和 K8s 的声明式理念对齐。开发者不需要关心底层存储长什么样只需要在 YAML 里写 storageClassName 和 size存储资源就像 API 一样被动态申请。对小团队来说这种体验非常接近云上的 PVC 自动创建运维负担一下子降下来。1.3 Kubernetes v1.35 对 NFS 接入的影响有人会问Kubernetes 都到 v1.35 了NFS 这种老协议是不是该淘汰了实际上没有NFS 反而因为稳定性和兼容性一直留着。从 v1.28 以后K8s 内部把 in-tree 的 NFS volume 插件逐步边缘化官方现在更推荐通过 CSI 驱动或者外部 Provisioner 来接入 NFS。我这里没有走 CSI原因是 nfs-subdir-external-provisioner 已经足够成熟运行模式和部署方式都很简单不需要维护 CSI 节点插件。Kubernetes v1.35 里依然保留了向下的存储插件兼容层外部 Provisioner 通过 API 调用创建 PV核心机制没有变化实测下来没有任何兼容上的问题。要注意的是如果集群开启了严格的准入控制或者安全策略Provisioner 所在的命名空间需要允许创建 PVC 和 PV 权限这些权限会在 Deployment 的 RBAC 里体现。后面部署时我会把 ServiceAccount、ClusterRole、ClusterRoleBinding 一并贴出来避免权限不全导致 Provisioner 无法工作。2. NFS 服务端部署与目录规划2.1 服务端安装和基础配置NFS 服务端我会选一台独立的节点推荐不要和 Kubernetes 控制平面混布避免磁盘高负载时影响 etcd 性能。本次环境里服务端 IP 是 192.168.1.10共享目录为 /srv/nfs/k8s后续所有 K8s 节点的动态卷都会落在这个目录之下。CentOS 10 安装 NFS 服务端很简单dnf install -y nfs-utils systemctl enable --now nfs-server安装完成后先创建一个共享根目录。关于磁盘规划我建议单独挂一块数据盘目录放到数据盘上而不是直接用系统盘。举例来说如果你有 /dev/sdb 还没有分区mkfs.xfs /dev/sdb mkdir -p /srv/nfs/k8s mount /dev/sdb /srv/nfs echo /dev/sdb /srv/nfs xfs defaults 0 0 /etc/fstab然后建 k8s 子目录并设置权限mkdir -p /srv/nfs/k8s chmod 1777 /srv/nfs/k8s1777 权限是一种实用做法类似 /tmp 的粘滞位所有用户都可以写但不能互相删除文件。在测试环境里可以快速规避大部分权限问题。生产环境建议收紧到 0770 并指定用户组但需要在 provisioning 侧做额外配合这里先按下不表。2.2 exports 导出规则和权限模型NFS 的核心配置在 /etc/exports 文件里。我的配置是这样的/srv/nfs/k8s 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check)逐项解释参数的含义rw允许客户端读写默认其实是 ro必须显式声明。sync服务端只有在数据落盘后才回应客户端避免断电丢数据。性能会有一点损失但可靠。no_root_squash允许客户端的 root 用户保留 root 权限写入。这是给 nfs-subdir-external-provisioner 用的它创建子目录时要以 root 身份设置属主和权限。no_subtree_check关闭子树检查提升性能避免文件被重命名后访问延迟。注意如果你担心安全可以把 no_root_squash 换回默认的 root_squash让客户端 root 被映射为 nfsnobody 用户。但这样一来 Provisioner 创建出来的目录属主会变成 nfsnobodyK8s 里大多数容器以 root 运行后续写文件时可能遇到 Permission denied。实测下来为了功能正常测试环境我建议先用 no_root_squash等整体跑通后再根据业务场景加固。配置写完以后执行exportfs -ra showmount -e 192.168.1.10showmount 能看到导出列表就说明服务端配置没问题。导出的目录应该是/srv/nfs/k8s 192.168.1.0/242.3 防火墙与网络检查CentOS 10 默认开 firewalld如果没放行 NFS 相关端口客户端挂载时会一直卡在等待状态。NFSv3 和 NFSv4 涉及端口不一样最稳妥的做法是把 RPC 相关服务都放行。我先用防火墙模块管理firewall-cmd --permanent --add-servicenfs firewall-cmd --permanent --add-servicemountd firewall-cmd --permanent --add-servicerpc-bind firewall-cmd --reloadNFSv4 固定使用 2049 端口但如果客户端回退到了 NFSv3还需要 rpc.mountd 的端口一般是 20048。为了减少排查工作量上面三个服务都放行是比较省心的。另外还要检查 SELinux。CentOS 10 默认 enforcingNFS 共享目录如果挂载在普通目录下一般没问题但如果你创建的目录路径比较特殊SELinux 会阻止 nfsd 读写。比较简单的处理方式setsebool -P nfs_export_all_rw 1 setsebool -P nfs_export_all_ro 1这两个布尔值表示允许 NFS 导出所有类型的文件系统目录。执行后可以再用getsebool -a | grep nfs确认。3. Kubernetes 客户端接入与 Provisioner 部署3.1 每个节点安装 nfs-utilsKubernetes 节点作为 NFS 客户端必须安装 nfs-utils否则 kubelet 执行 mount 时会报“unknown filesystem type nfs”。所有节点包括控制平面节点都执行dnf install -y nfs-utils安装完成后建议先手动试挂一次确认客户端和服务端能通信mkdir -p /mnt/nfs-test mount -t nfs 192.168.1.10:/srv/nfs/k8s /mnt/nfs-test ls /mnt/nfs-test umount /mnt/nfs-test如果这里能成功挂载并看到目录K8s 侧的挂载问题就基本排除了。很多人在 K8s 里遇到挂载失败其实节点上根本没有 nfs-utilskubelet 报错根本看不出来所以这一步不要跳过。3.2 部署 nfs-subdir-external-provisioner动态供给我选择 nfs-subdir-external-provisioner它本质是一个监听器的 Deployment不占用独立存储。可以用 Helm Chart 直接部署也可以手动应用 YAML。我用 Helm 比较多因为参数直观。先添加 Helm 仓库并更新helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ helm repo update然后创建命名空间并部署kubectl create ns nfs-provisioner helm install nfs-subdir-external-provisioner \ nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \ -n nfs-provisioner \ --set nfs.server192.168.1.10 \ --set nfs.path/srv/nfs/k8s \ --set storageClass.namenfs-client \ --set storageClass.defaultClasstrue \ --set storageClass.archiveOnDeletefalse这里几个参数需要专门解释nfs.serverNFS 服务端 IP注意不是域名。nfs.path导出的共享目录必须和 exports 里的路径一致。storageClass.name自动创建的 StorageClass 名称这里叫 nfs-client。storageClass.defaultClass设为 true 后所有不指定 storageClassName 的 PVC 会默认使用这个 StorageClass。archiveOnDeletefalse 表示删除 PVC 时直接删除 NFS 子目录true 则会把目录改名归档成 archived- 前缀。测试环境建议设 false避免残留一堆垃圾目录。如果你不想装 Helm也可以直接用项目里的 deploy 目录部署 YAML核心是环境变量 NFS_SERVER 和 NFS_PATH以及 arg 里的 provisioner 名称。用 Helm 的好处是 RBAC、Deployment、StorageClass 都自动创建好了省去手写 ServiceAccount 和 ClusterRole 的麻烦。部署完成后确认 Pod 在运行kubectl get pods -n nfs-provisioner正常会有一个 Running 状态的 Pod。再确认 StorageClasskubectl get storageclass输出里应该能看到 nfs-client并且标记为 default。3.3 StorageClass 参数和回收策略Helm 会自动生成一个 StorageClass它的内容类似这样apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client provisioner: k8s-sigs.io/nfs-subdir-external-provisioner reclaimPolicy: Delete volumeBindingMode: ImmediatereclaimPolicy 决定 PV 被释放时的行为。Delete 意味着 PVC 删除后Provisioner 会回收 PV 并清理 NFS 上的目录Retain 则保留数据需要运维手动清理。对我们这次的需求来说用 Delete 最顺滑因为动态卷本身就应该随业务生命周期销毁。要注意的是StorageClass 的 reclaimPolicy 和 archiveOnDelete 是两个层面的配置。reclaimPolicy 控制 K8s 对象的删除archiveOnDelete 控制 NFS 目录的物理处理。比如 reclaimPolicy 是 DeletearchiveOnDelete 是 truePVC 删除后 PV 会被回收但 NFS 目录不会立刻删除而是移动到归档目录。如果希望彻底清理这两个都必须设成符合预期。4. 实战验证PVC 到 Pod 的数据写入4.1 创建 PVC 验证动态供给部署完 Provisioner 后先创建一个测试 PVC确认能自动生成 PV。apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc namespace: default spec: accessModes: - ReadWriteMany storageClassName: nfs-client resources: requests: storage: 1Gi应用后马上查看 PVC 状态kubectl apply -f test-pvc.yaml kubectl get pvc test-pvc kubectl get pv | grep test-pvc正常情况下几秒内 PVC 会从 Pending 变为 Bound。如果停在 Pending 很久一般就是 Provisioner 没正常工作可以看它日志kubectl logs -n nfs-provisioner deployment/nfs-subdir-external-provisioner绑定成功后查看生成的 PV可以看到 NFS 路径已经是/srv/nfs/k8s/default-test-pvc-...说明 Provisioner 自动在服务端创建了一个 Named 子目录。这一步验证的是整个动态供给链路PVC - StorageClass - Provisioner - NFS 服务端 - PV。任何一个环节有问题都会体现在 PVC 的 Status 上。4.2 部署测试 Pod 写入文件光有 PV 还不行要真正让业务 Pod 跑起来。我创建一个 busybox Pod 挂载这个 PVCapiVersion: v1 kind: Pod metadata: name: nfs-test-pod spec: containers: - name: busybox image: busybox:latest command: [sleep, 3600] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: test-pvcPod 起来后进入容器写数据kubectl exec -it nfs-test-pod -- sh echo hello from k8s /data/test.txt cat /data/test.txt然后我回到 NFS 服务端直接查看cat /srv/nfs/k8s/default-test-pvc-*/test.txt能看到同样的内容说明数据确实写进了 NFS 服务端而不是留在节点本地。4.3 跨节点挂载和共享验证NFS 的 ReadWriteMany 特性就是允许多个 Pod 跨节点同时读写。我把测试 Pod 删掉再创建一个 Deployment 多副本同样是挂载 test-pvcapiVersion: apps/v1 kind: Deployment metadata: name: nfs-multi-pod spec: replicas: 3 selector: matchLabels: app: nfs-test template: metadata: labels: app: nfs-test spec: containers: - name: busybox image: busybox:latest command: [sleep, 3600] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: test-pvc3 个副本会被调度到不同节点但因为是同一个 PVC它们都指向同一个 NFS 子目录。验证方式kubectl get pods -l appnfs-test -o wide kubectl exec nfs-multi-pod-xxxx -- sh -c echo instance-2 /data/instance.log然后在另一个 Pod 里读取kubectl exec nfs-multi-pod-yyyy -- sh -c cat /data/instance.log如果你能看到刚才第一个 Pod 写入的内容说明多节点共享没有问题。这一步经常被忽略但很关键。很多存储方案只能支持 ReadWriteOnceNFS 的优势正好在这种场景体现。共享测试通过后整个持久化存储链路才算真正可用。5. 常见故障与避坑实录5.1 权限不对导致 Pod 无法写入最常见的报错是 Pod 内创建文件时报 Permission denied或者 Provisioner 创建子目录失败。问题通常出在 exports 的 root_squash 和目录属主上。如果你使用 no_root_squash 仍然遇到权限问题先看 NFS 目录的属主和权限ls -ld /srv/nfs/k8s测试环境我建议用 1777确保任何用户都能写。生产环境如果使用 root_squash就要把目录属主改成 nfsnobody并保证容器进程不是以 0 运行。K8s 的 Pod 也可以显式配置 securityContext 指定 fsGroup让挂载目录的可写权限映射到容器内用户。另一个容易忽略的点是 NFS 服务端上的父目录权限。如果 /srv/nfs 本身没有执行权限客户端即使挂载成功也无法进入下一级。检查路径上每一级目录的权限别只盯着末尾的 k8s 目录。5.2 挂载卡住或超时挂载卡住是 NFS 里最折腾人的问题。现象是 Pod 一直 ContainerCreatingEvents 里报 FailedMount节点上 mount 命令无法结束。先按顺序排查showmount -e 192.168.1.10 ping 192.168.1.10 telnet 192.168.1.10 2049能 ping 通不代表 2049 端口通必须确认端口可达。如果端口不通回服务端检查 firewalld同时确认 rpcbind 和 nfs-server 都在运行systemctl status nfs-server rpcbind还有一个隐蔽问题如果 NFS 服务端在启用防火墙前就已经启动了 nfs-server某些 RPC 服务监听的端口会随机变化导致客户端 mount 超时。解决办法是重启 nfs-server 并 reload 防火墙systemctl restart nfs-server exportfs -raK8s 节点上也可以通过手动 mount 验证节点本身能否挂载排除客户端问题后再回到 K8s 侧查 kubelet 日志journalctl -u kubelet -f | grep -i nfs\|mount5.3 NFS 版本兼容与嵌入式场景参考K8s 里挂载 NFS 默认走的是 NFSv4这在大多数 Linux 环境没问题。但如果你遇到挂载时报 NFS version not supported 或者旧内核设备访问异常可以临时指定版本。手动挂载测试时mount -t nfs -o vers4.0 192.168.1.10:/srv/nfs/k8s /mnt我调试 RK3568 这类嵌入式开发板时经常用 NFS v3 挂载根文件系统因为嵌入式内核默认配置里对 v3 的兼容性更干净。K8s 这边虽然很少需要切版本但如果公司内部有大量老旧节点提前知道怎么切也不至于两眼一抹黑。切换版本不是完全没有代价v3 需要额外的 mountd 端口前面防火墙放行 mountd 和 rpc-bind 就是为了这种场景。5.4 性能调优与高可用扩展NFS 作为 K8s 持久化存储性能不是最强项但完全够用。如果碰到吞吐瓶颈可以从几个方向调。第一是线程数。NFS 服务端会校验 RPC 线程数量echo options nfsd thread32 /etc/modprobe.d/nfsd.conf改完重启 nfs-server 或者重载模块生效。第二是挂载参数。K8s 里通过 StorageClass 的 mountOptions 来传递mountOptions: - rsize1048576 - wsize1048576 - hard - nfsvers4大读写块和 hard 模式能提高数据吞吐和稳定性。soft 模式虽然避免进程卡死但会导致 I/O 错误除非应用层能处理否则不建议。高可用方面简单场景可以用 keepalived 做 NFS 服务端 VIP 漂移但文件锁定和数据一致性需要额外做同步复杂程度高。小集群里我倾向于优先保证备份而不是做无意义的双活。NFS 本身就是很好的备份目标定期把子目录 rsync 到其他机器比折腾高可用更实际。最后提一下 Provisioner 的日志管理。nfs-subdir-external-provisioner 默认日志级别不高排障时可以用-v参数调整但生产中不建议长期开启 debug日志量会很可观。整个 NFS 持久化存储方案跑下来我最深的一个体会是别迷信复杂的分布式存储很多团队连一套 NFS 都没用好就急着上 Ceph结果把问题复杂化了。先把 NFS 的权限、防火墙、目录规划做扎实对绝大多数中小集群来说已经足够能打。等以后真的需要跨机房多活或者海量小文件性能时再考虑迁移到专业存储也不迟。
返回列表