ARTICLE DETAIL

资讯详情

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

Kubernetes集群NFS持久化存储部署实战与踩坑指南

Kubernetes集群NFS持久化存储部署实战与踩坑指南 上个月我帮朋友在 CentOS 10 上搭了一套 Kubernetes v1.35 集群跑日志分析业务。一开始为了图快所有有状态组件全用 emptyDir 兜底结果节点一次重启任务队列直接清零跑了大半的分析结果全没了。被逼着上持久化存储最终选的就是 NFS。整套折腾下来从选型对比到动态供给跑通再到各种权限坑前后花了两天。这篇把我在 CentOS 10 集群里部署 NFS 持久化存储的全过程以及踩过的坑完整记录下来。这套方案解决的是 Kubernetes 里最基础也最刚需的问题容器天生无状态Pod 重建后数据怎么保住NFS 的优势非常直接——部署成本低支持多节点读写共享动态供给生态成熟一个人半小时就能跑通。无论你是刚接触 K8s 的新手还是想给测试环境补一块共享存储的老手都有参考价值。1. NFS 方案选型为什么我的测试集群最终选了它先说结论在中小规模集群里NFS 往往是第一个能跑到生产水平的持久化方案。但第一个能跑通不代表它是万能的。我当时的需求很明确日志分析服务需要多节点共享数据部分组件要跑 StatefulSet还要支持测试环境里频繁的 PVC 创建和删除。这种组合基本把 hostPath 和 Local PV 排除在外了。1.1 对比 hostPath、Local PV、Ceph 后NFS 的取舍我做个简单对比方便你照着选型方案部署成本跨节点共享动态供给适用场景hostPath最低不支持无单节点调试千万别上多节点集群Local PV低不支持需额外部署 local-static-provisioner高 IO 有状态服务绑定节点NFS低支持成熟测试集群、共享存储、通用工作负载Ceph RBD高支持成熟生产级块存储运维能力要求高GlusterFS中支持生态萎缩不建议新项目再碰hostPath 就是直接把宿主机目录挂给容器不用额外存储看着省事但副本调度到别的节点就找不到数据了。Local PV 性能不错但和节点强绑定Pod 很难漂移。Ceph RBD 是生产级选择可你得先养三台 MON 和一堆 OSD测试环境扛不住这个运维成本。NFS 恰好卡在中间一台机器把目录共享出去所有节点 mount 之后当本地目录用。本质上它就是个网络文件系统不是分布式存储但 K8s 要的持久化能力它都能满足。1.2 动态供给 vs 静态供给Provisioner 的价值持久化存储有两种接入方式。静态供给是你手工创建 PV然后写 PVC 去绑定典型流程是先登录 NFS 服务器建目录、设置权限再到集群里写好 PV YAML 指定 NFS server 和 path最后才能创建 PVC。听起来不复杂可一旦测试环境里应用多PVC 频繁增删手工维护 PV 列表就成了一场灾难还容易把容量和目录对应关系搞混。动态供给就直接多了部署一个 Provisioner 组件集群里创建 PVC 时它自动在 NFS 共享目录里生成子目录同时创建 PV 并完成绑定。你只需要声明我要多大空间剩下全自动。我用的就是社区维护的 nfs-subdir-external-provisioner这也是 K8s 生态里跑 NFS 动态供给最主流的方式。1.3 读写模式边界RWX 是优势也是约束K8s 的访问模式里ReadWriteOnce 通常简写 RWOReadWriteMany 是 RWXReadOnlyMany 是 ROX。NFS 天然支持 RWX多个 Pod 可以同时挂载同一个 PVC这一点在企业内网做文件共享时特别实用。但注意RWX 不等于分布式一致性。NFS 没有分布式锁的服务端协同机制多个实例并发密集写入同一份文件时数据一致性靠的还是文件锁语义数据库这类强一致应用别指望拿 NFS 做双主读写。我后面测试 MySQL 时也只跑单实例这是 NFS 的边界提前了解能省很多事。2. NFS 服务端和 CentOS 10 集群环境准备动手之前先交代拓扑。这套集群是三个节点的标准结构master192.168.1.10worker-1192.168.1.11worker-2192.168.1.12NFS 服务器192.168.1.100共享目录 /data/k8s-nfsNFS 服务器我建议独立一台尤其是你已经跑着的集群别把存储目录挂在 master 上。如果纯粹是资源紧张的单节点测试环境偶尔兼职用一下也能接受但多节点集群千万别这么干——你无法保证调度到另一节点的 Pod 能访问本地目录共享路径要求所有节点看得见同一份数据。2.1 服务端配置exports 参数不能照抄CentOS 10 下安装 NFS 服务端用的是 nfs-utils 包。RHEL 系系统这几代变化不大下面是完整命令dnf install -y nfs-utils mkdir -p /data/k8s-nfs chmod 755 /data/k8s-nfs echo /data/k8s-nfs 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check) /etc/exports systemctl enable --now nfs-server exportfs -rav关键就在 exports 这行参数。rw 是读写权限sync 表示服务端每次写操作都落盘后才返回结果和默认的 async 相比性能差一点但数据安全性高——K8s 里的文件挂了就是挂了不建议用 async 赌不宕机。no_root_squash 是绝大多数权限坑的根源也是解药后面第五节我会详细拆这里先记住容器里以 root 写文件时如果服务端把这个 root 映射成了匿名用户权限不足的错误会让你摔得很疼。no_subtree_check 减少目录树检查开销对根目录直接导出的场景是合理的取舍。做完这些验证一下导出状态showmount -e 192.168.1.100输出应该能看到Export list for 192.168.1.100: /data/k8s-nfs 192.168.1.0/242.2 防火墙NFS 不止开放 2049NFS 用到的端口比很多人以为的多。除了标准的 2049还要放行 rpcbind111和 mountd。CentOS/RHEL 默认防火墙是 firewalld可以直接用服务名放行firewall-cmd --permanent --add-servicenfs firewall-cmd --permanent --add-servicerpc-bind firewall-cmd --permanent --add-servicemountd firewall-cmd --reloadmountd 在较新系统上默认有固定端口 20048定义在 /etc/nfs.conf 的 [mountd] 段如果遇到 mount 卡住的问题优先检查这个端口的放行情况。旧习惯里只放 2049是行不通的showmount 和挂载协商阶段都依赖 rpcbind漏了 111 会出现客户端一直卡在 RPC 等待上。2.3 集群各节点的前置准备这一步最容易遗漏。Kubernetes 各节点包括 master都需要安装 nfs-utils因为 kubelet 挂载 NFS 卷时执行的就是 mount 命令系统里没有 NFS 工具链PVC 会一直卡在 ContainerCreating事件里报 mount: unknown filesystem type nfs。dnf install -y nfs-utils装完先在节点上手动验证一次网络挂载mkdir -p /mnt/nfs-test mount -t nfs 192.168.1.100:/data/k8s-nfs /mnt/nfs-test df -h | grep nfs umount /mnt/nfs-test这一步通过说明服务端导出、防火墙、RPC 协商都正常。如果手动挂载都失败先别碰 K8s问题在底层网络或服务端配置。这也是排查的黄金原则故障边界越早缩小越好。3. 部署 nfs-subdir-external-provisioner打通动态供给Provisioner 说白了就是一个监听 Kubernetes API 的控制器。当它看到新的 PVC 且 StorageClass 指向它时就在 NFS 共享目录里创建子目录然后生成对应的 PV 对象让 PV 和 PVC 完成绑定。整个过程不需要你碰任何 NFS 服务器上的目录。部署方式有两种Helm 一键装或者手写 YAML。Helm 适合只想快速跑通的人手写 YAML 适合想搞懂内部机制的人。我建议你至少手写一遍因为生产环境里你大概率需要改镜像源、调参数知道每块配置含义比盲目用 chart 更重要。3.1 RBAC、Deployment 和运行日志验证需要的资源有三块ServiceAccount 和权限、Deployment、StorageClass。先看 RBAC 部分Provisioner 需要权限来操作 PVC、PV、StorageClass 和 EventapiVersion: v1 kind: ServiceAccount metadata: name: nfs-client-provisioner namespace: default --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: nfs-client-provisioner-runner rules: - apiGroups: [] resources: [persistentvolumes] verbs: [get, list, watch, create, delete] - apiGroups: [] resources: [persistentvolumeclaims] verbs: [get, list, watch, update] - apiGroups: [storage.k8s.io] resources: [storageclasses] verbs: [get, list, watch] - apiGroups: [] resources: [events] verbs: [create, update, patch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: nfs-client-provisioner-runner subjects: - kind: ServiceAccount name: nfs-client-provisioner namespace: default roleRef: kind: ClusterRole name: nfs-client-provisioner-runner apiGroup: rbac.authorization.k8s.ioDeployment 的关键在于环境变量和方法。PROVISIONER_NAME 要和后面 StorageClass 里指定的 provisioner 名称完全一致NFS_SERVER 和 NFS_PATH 指向服务端的共享目录apiVersion: apps/v1 kind: Deployment metadata: name: nfs-subdir-external-provisioner namespace: default spec: replicas: 1 selector: matchLabels: app: nfs-subdir-external-provisioner template: metadata: labels: app: nfs-subdir-external-provisioner spec: serviceAccountName: nfs-client-provisioner containers: - name: nfs-provisioner image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: k8s-sigs.io/nfs-subdir-external-provisioner - name: NFS_SERVER value: 192.168.1.100 - name: NFS_PATH value: /data/k8s-nfs volumes: - name: nfs-client-root nfs: server: 192.168.1.100 path: /data/k8s-nfs镜像地址如果拉不下来换镜像加速或者离线导入都可以但核心逻辑不受影响。apply 之后重点看日志确认 Provisioner 和 API Server 通信正常kubectl apply -f rbac.yaml -f deployment.yaml kubectl logs -l appnfs-subdir-external-provisioner正常日志里能看到它启动并监听 StorageClass没有报错就说明跑到这一步没问题。3.2 StorageClass 参数详解与默认类设置StorageClass 是动态供给的触发器。我的配置如下apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client annotations: storageclass.kubernetes.io/is-default-class: true provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: archiveOnDelete: true pathPattern: ${.PVC.namespace}/${.PVC.name} reclaimPolicy: Delete volumeBindingMode: Immediate mountOptions: - vers4.2 - hard - noresvport - timeo600逐项拆解。provisioner 字段必须等于 Deployment 里 PROVISIONER_NAME 的值这是绑定关系的关键。archiveOnDelete 为 true 时PVC 删除后对应目录会被改名保留而不是直接清空防止手滑删库。pathPattern 定义了 NFS 上的目录层级我习惯用 namespace 和 PVC 名字区分比如 /data/k8s-nfs/default/www-pvc/资源如下才能快速定位这是谁的数据。reclaimPolicy 是 Delete也就是 PVC 释放后 PV 也删掉如果数据要长期保存可以改成 Retain但 PV 会残留需要人工清理。volumeBindingMode 用 ImmediateNFS 不像云盘那样需要等 Pod 调度到节点创建即绑定即可。mountOptions 放在 StorageClass 里会自动应用到所有用它创建的 PV 卷上这些参数的作用我在踩坑章节再展开。另外我把这个 StorageClass 设成了默认类。这意味着 PVC 里不写 storageClassName 也会自动用它对于测试环境特别省心但生产环境要谨慎避免所有 PVC 都默默落到 NFS 上。如果你更偏好 Helm一句话也能跑helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \ --namespace nfs-provisioner --create-namespace \ --set nfs.server192.168.1.100 \ --set nfs.path/data/k8s-nfs \ --set storageClass.namenfs-client \ --set storageClass.defaultClasstrue \ --set storageClass.archiveOnDeletetrue两条路径殊途同归手写 YAML 多了对权限模型的理解Helm 多了升级的便利按需选择。4. 实战验证从 PVC 自动供给到有状态应用落地配置写完了关键看能不能跑通。我习惯先建一个 PVC 观察动态供给链路再用一个无状态应用验证数据持久最后压一个有状态应用测真实场景。4.1 创建 PVC观察 PV 自动生成先创建一个测试 PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name: www-pvc namespace: default spec: accessModes: - ReadWriteMany resources: requests: storage: 5Gi storageClassName: nfs-client应用后观察kubectl get pvc kubectl get pv不出意外PVC 秒变 BoundPV 也在无人工干预的情况下自动出现名字带了一串随机后缀。这就是 Provisioner 干完活了。想确认底层目录就去 NFS 服务器上看ls -l /data/k8s-nfs/default/www-pvc/因为配置了 pathPattern目录结构是 namespace 加 PVC 名称一眼就能认出来。没配置的话则是类似 default-www-pvc-pvc-xxxxxxxx 的平铺命名多了随机串。这里要说一下 PVC 事件的价值。如果 PVC 一直 Pending别急着重启集群先用 kubectl describe pvc 看事件流Provisioner 在动态供给过程中会把进度和错误写到事件里。常见的是等待 provisioner 启动、等待 StorageClass 注册这类信息。事件是排查动态供给故障最快的入口。4.2 部署 nginx验证 Pod 重建后数据还在有了 PVC 就接应用。我用 nginx 做验证挂载路径指向网站根目录apiVersion: apps/v1 kind: Deployment metadata: name: nginx-with-nfs spec: replicas: 1 selector: matchLabels: app: nginx-with-nfs template: metadata: labels: app: nginx-with-nfs spec: containers: - name: nginx image: nginx:1.27 ports: - containerPort: 80 volumeMounts: - name: www-data mountPath: /usr/share/nginx/html volumes: - name: www-data persistentVolumeClaim: claimName: www-pvc然后做一轮完整的持久化验证kubectl exec -it nginx-with-nfs-xxxx -- sh -c echo hello from k8s nfs /usr/share/nginx/html/test.txt kubectl delete pod nginx-with-nfs-xxxx kubectl get pods -w kubectl exec -it 新pod -- cat /usr/share/nginx/html/test.txtPod 被删除后 Deployment 会拉一个新的出来因为 PVC 是集群里独立存在的资源新 Pod 调度到任何一个节点挂载的都是同一个 NFS 目录test.txt 自然还在。这背后是 PV/PVC 的绑定关系而 PV 的底层实现就是 NFS 服务端那个目录只要服务端不挂数据就不掉。4.3 上真货StatefulSet 跑 MariaDB验证完基础功能我把 MariaDB 也搬了上来。StatefulSet 和 Deployment 的区别在于它用 volumeClaimTemplates 给每个副本生成独立的 PVC适合需要稳定网络标识和独立存储的有状态应用apiVersion: apps/v1 kind: StatefulSet metadata: name: mariadb spec: serviceName: mariadb replicas: 1 selector: matchLabels: app: mariadb template: metadata: labels: app: mariadb spec: containers: - name: mariadb image: mariadb:11.4 env: - name: MARIADB_ROOT_PASSWORD value: change-me volumeMounts: - name: data mountPath: /var/lib/mysql volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] resources: requests: storage: 10Gi storageClassName: nfs-client创建后看 PVC 状态会发现多了个>exportfs -rav还有一种解法是从应用侧调整 fsGroup。在 Pod 的 securityContext 里设置 fsGroup 为预期用户组让 K8s 对挂载目录进行组权限调整适合不想动服务端 exports 的场景。但两条路总得选一条我建议测试环境直接用 no_root_squash 简单粗暴生产环境优先 fsGroup 和精细的 UID 管理。如果以上排查都做了还没解决就该怀疑 SELinux 了。CentOS 10 默认 SELinux enforcingNFS 文件系统打上安全上下文后容器内进程的访问可能被 LSM 策略拦截而应用日志里通常看不到明确原因。先验证setenforce 0 临时关闭 SELinux复现问题是否消失。如果消失说明是 SELinux 策略的问题再用 ausearch -m avc 或者 dmesg | grep avc 之类的命令确认拒绝记录。RHEL 系在虚拟化或容器场景放行 NFS 访问有一些布尔值开关按你的安全基线启用即可。这套排查链路从我实测来看能解决 90% 的 NFS 权限类故障。5.2 挂载参数调优vers、hard 和 noresvportStorageClass 里的 mountOptions 是我完整跑了一遍才总结出的推荐组合mountOptions: - vers4.2 - hard - noresvport - timeo600为什么这几项很关键。vers4.2 指定使用 NFS v4.2相比 v3它支持服务端复制、原子写等特性性能表现和元数据处理都更好。嵌入式 Linux 里常见挂 NFS v3那是因为老内核或设备端限制K8s 集群的 Linux 节点没有这个包袱直接用 v4.2。但如果你连的 NAS 是老设备可能只支持 v3那就得按服务端能力降到 vers3这一点选型时就要确认。hard 决定 NFS 服务端无响应时客户端的处理方式。hard 模式下 IO 会阻塞等待应用可能卡住也不报错但是数据不会丢soft 模式会直接返回 EIO 报错应用可能崩溃甚至数据损坏。持久化场景我用 hard宁卡勿丢。noresvport 是因为 NFS 客户端断开重连时默认可能复用旧的网络端口这个端口在服务端还处于等待状态时会导致重连失败挂载看起来像是失效了。noresvport 强制客户端在重连时换一个端口能明显降低这种挂载失效的概率。timeo600 设置请求超时重传间隔600 是 60 秒NFS 默认重传策略偏保守这个数值在大多数集群网络下表现稳定。5.3 回收策略Delete、Retain 和一次误删教训最后说回收策略这是最容易出大事的地方。StorageClass 的 reclaimPolicy 有 Delete 和 Retain 两种。Delete 模式下删除 PVC 会自动删除 PV如果 archiveOnDelete 还是 falseNFS 对应目录一起被清空。Retain 模式下PVC 删了 PV 还留在集群里数据安全但 PV 变成 Released 状态需要你手动清。我在测试环境吃过一次亏。当时手滑删了一个看起来是临时数据的 PVC实际里面是跑了几天的分析结果。幸运的是我这套集群安装了 nfs-subdir-external-provisioner 且设置了 archiveOnDeletetruePVC 删除时目录只是被改名为 archive-前缀保留了下来数据全须全尾地救了回来。这直接改变了我对回收策略的态度。现在我的建议是测试环境用 Delete但加上 archiveOnDeletetrue给误操作留条退路真正重要的数据不要依赖任何回收机制Retain 加定期备份双保险。Data 目录归档的命名规则可以在 Provisioner 的 args 里配置归档默认保留在 NFS 共享目录下这一点操作之前一定要想清楚。NFS 这个方案听起来不现代但它是 K8s 持久化场景里性价比最高的一条路。如果你的集群规模在几十台以内不想引入重量级分布式存储照着这篇把服务端导出、客户端工具、Provisioner 和 StorageClass 跑完测试环境共享存储就齐活了。我最后分享一个实用心法把 nfs-client 设为默认 StorageClass 之后任何 PVC 不指定 storageClassName 都会自动动态供给少写一行配置减少一类错误。这套方案我已经连续跑了快一个季度节点重启、Pod 频繁调度都没再丢过数据。规模再往上走再考虑往 Ceph 或云盘平滑迁移就好。
返回列表