ARTICLE DETAIL

资讯详情

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

Kubernetes NFS持久化存储实战:StorageClass动态供给PVC全解析

Kubernetes NFS持久化存储实战:StorageClass动态供给PVC全解析 NFS 持久化存储这事儿我其实纠结了很久才决定写。因为网上讲 Kubernetes 存储的文章一抓一大把但大多数都是“照着敲就能通”的程度一旦遇到版本坑、权限坑、回收策略坑就没人告诉你该怎么爬出来了。这次趁着在 CentOS 10 环境里部署新集群的机会我把从 NFS 服务端搭建到 Kubernetes 动态创建 PVC 的完整链路重新捋了一遍踩了几个不算深但足够让人头疼的坑记录下来分享给大家。先说结论如果你的集群环境是裸金属或者自建的虚拟化平台而且对存储性能没有那么极致的要求NFS 依然是做持久化存储最省心的方案。它不像 Ceph 那样需要单独的监控节点和复杂的网络配置也不用像 Local PV 那样去操心节点亲和性和数据调度只要一台 Linux 机器、一块还不错的硬盘、加上 Kubernetes 内置的 NFS 驱动或者社区维护的 external provisioner就能低成本地获得一个还算好用的存储后端。这篇博文会从环境规划、NFS 服务端部署、Kubernetes 侧配置 StorageClass、到最终的用例验证和问题排查完整走一遍流程。所有操作都是我在 CentOS 10 集群上的实测记录你会看到具体命令、关键参数的含义解释还有我踩过的坑。适合刚接触 Kubernetes 存储的运维同学也适合那些已经在用 NFS 但想梳理清楚原理的开发者。注意当前 Kubernetes 最新稳定版本仍然以 v1.28~v1.31 为主流v1.35 属于较新的迭代版本不过 NFS 相关的 API 和配置方式变化不大。我的操作以 v1.35 实测为准同时会标注出与旧版本兼容的说明。1. 整体方案设计与选型思路1.1 为什么在多节点集群里选择 NFS这次部署的集群一共三台节点一台控制平面两台工作节点。业务方给的需求很明确需要一个“所有节点都能读写、挂载之后数据不丢、运维成本低”的共享存储。当时我对比了几种方案方案优点缺点适用场景Ceph RBD性能好、支持快照部署复杂、至少3台独立节点、维护成本高大规模生产集群Local PV性能极佳、IO延迟低数据绑定节点、无法跨节点共享、备份麻烦单节点高IO应用NFS部署简单、跨节点共享、内核支持稳定单点故障、性能一般、需要额外机器中小规模集群、共享文件型应用这里多说一句NFS 的“单点故障”问题确实是它最大的短板。我见过很多团队为了绕过这个瓶颈去折腾 GlusterFS 或者干脆用 Ceph结果折腾了大半个月还没跑起来最后灰溜溜地换回 NFS。如果你真的担心高可用可以考虑用 DRBD 配合 keepalived 做 NFS 服务端的 HA或者直接上云厂商的 NAS 服务本质上也是 NFS 协议但底层由平台保证了可用性。1.2 Kubernetes 访问 NFS 的两种姿势在开始动手之前必须先搞清楚一个概念Kubernetes 的 PV 访问 NFS有两种完全不同的落地方式。第一种是手动创建 PV然后在 PV 的 spec 里指定 nfs 类型填上 NFS 服务器的 IP、路径和挂载选项。这种方式把 PV 看成是一个提前分配好的存储区域Pod 声明 PVC 去绑定它。优点是直观缺点是你得自己管理 PV 的创建和回收PVC 多了以后很烦。第二种是部署一个叫 nfs-subdir-external-provisioner 的第三方组件。它会监听集群里的 PVC 创建事件然后自动在 NFS 服务端创建子目录、生成对应的 PV 并绑定。这种方式下用户只需要创建 PVC剩下的事全由 provisioner 搞定。今天我主要讲第二种因为它才是真正意义上的动态存储供给也是目前社区里最主流的做法。我见过有人把这两种方式混着用比如某些有特殊需求的 PV 手动创建其余走动态供给。这没什么问题但要注意命名规范和回收策略的统一否则后续排查会非常痛苦。1.3 环境规划与版本核对这次部署的具体环境如下Kubernetes v1.35三节点集群使用 containerd 作为容器运行时操作系统CentOS 10内核版本 6.xNFS 服务端独立的一台 CentOS 10 机器IP 为 192.168.10.10一块 500GB 数据盘挂载在 /data客户端机器集群三台节点的内核都支持 NFS v4同时向下兼容 v3在动手之前先用 nfsstat 或者 mount 命令检查一下各节点对 NFS 协议版本的支持情况。这里有个小细节CentOS 10 的内核把 NFS v4.2 设成了默认协议但如果你要在嵌入式内核或者老版本系统上挂载 NFS建议显式指定vers3。这也是很多人在不同环境间迁移时踩坑的地方。2. CentOS 10 上搭建 NFS 服务端的完整过程2.1 安装软件包与基础环境准备NFS 服务端需要安装两个包nfs-utils 和 rpcbind。在 CentOS 10 里 rpcbind 已经作为 nfs-utils 的依赖自动装上了所以实际操作只需要执行一条命令yum install -y nfs-utils装完之后顺手把 rpcbind 和 nfs-server 服务设置成开机自启并启动systemctl enable --now rpcbind systemctl enable --now nfs-server这里有一个很多人容易忽略的点nfs-server的 systemd 服务文件在较新版本里是nfs-server.service不是老教程里的nfs.service或者nfs-kernel-server.service。如果用的是 CentOS 7 时代的笔记可能会在 systemctl 命令上栽跟头。接下来需要规划导出目录。我习惯把数据盘单独挂到一个目录而不是直接用根分区。这次我把 500GB 数据盘格式化后挂载到了 /datamkfs.xfs /dev/sdb mkdir /data echo /dev/sdb /data xfs defaults 0 0 /etc/fstab mount -a注意XFS 是目前 CentOS 默认推荐的文件系统如果你要用 NFS v3 并且对兼容性有顾虑可以换成 ext4。不过在纯 CentOS 10 环境里 XFS 没有毛病。2.2 配置 exports 导出文件NFS 的核心配置文件是 /etc/exports。每行代表一个导出目录语法不复杂但参数的选择直接决定了可用性和安全性。我这次导出了两个目录一个是给测试环境用的另一个是给开发环境用的/data/k8s-test 192.168.10.0/24(rw,sync,no_root_squash,no_subtree_check) /data/k8s-dev 192.168.10.0/24(rw,sync,no_root_squash,no_subtree_check)逐个解释一下这些参数rw允许读写如果写 ro 那就只读sync服务端在请求写入磁盘后才回复客户端保证数据一致性no_root_squash允许客户端 root 用户保持 root 权限写入文件。这个参数要特别注意生产环境不建议开否则 NFS 上任何一个文件都能被任意客户端的 root 用户篡改no_subtree_check关闭子树检查可以提升性能但安全性略降我在测试环境里开了 no_root_squash是因为经常需要以 root 身份在容器里调试文件权限。生产环境请务必改成 root_squash这是默认为更安全的选项。配置完导出执行 exportfs 命令让配置生效exportfs -r exportfs -vexportfs -v 的输出会让你看到实际生效的导出参数。如果某个参数被省略了那说明它使用了默认值比如 root_squash 是默认开启的你没写就表示开启。2.3 防火墙放行与自检CentOS 10 默认开着 firewalld如果服务器启用了防火墙那 NFS 需要的端口必须放行。很多人在这一步翻车因为 NFS 除了 2049 端口外还需要 rpcbind 的 111 端口以及 mountd 动态分配的端口。比较省事的做法是指定 mountd 的固定端口然后一起加入防火墙echo mountd892 /etc/nfs.conf echo statd662 /etc/nfs.conf echo lockd2587 /etc/nfs.conf然后重启服务并在防火墙中放行systemctl restart nfs-server firewall-cmd --permanent --add-servicenfs firewall-cmd --permanent --add-port111/tcp firewall-cmd --permanent --add-port892/tcp firewall-cmd --permanent --add-port662/tcp firewall-cmd --permanent --add-port2587/tcp firewall-cmd --reload不过如果你和我一样是内网环境、图省事也可以直接关掉防火墙。但我建议养成写规则的习惯因为 NFS 默认不加密防火墙是唯一一道访问控制屏障。最后在服务端本机验证一下showmount -e 127.0.0.1能看到类似这样的输出就说明服务端没问题Export list for 127.0.0.1: /data/k8s-test 192.168.10.0/24 /data/k8s-dev 192.168.10.0/243. Kubernetes 侧配置 NFS 动态存储供给3.1 安装 nfs-subdir-external-provisioner这部分是整篇博文的核心。我要在 Kubernetes 集群里部署一个名为 nfs-subdir-external-provisioner 的 Deployment它会通过 NFS 协议连接服务端并为每个 PVC 在指定目录下创建子目录。先准备 deployment 的 YAML 文件。社区官方给的模板是最权威的建议去 GitHub 仓库获取最新版本而不是直接复制老博客的内容。我自己用的版本是 v4.0.2和 Kubernetes v1.35 兼容良好。核心的 Deployment 配置如下apiVersion: apps/v1 kind: Deployment metadata: name: nfs-subdir-external-provisioner namespace: kube-system spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: nfs-subdir-external-provisioner template: metadata: labels: app: nfs-subdir-external-provisioner spec: serviceAccountName: nfs-client-provisioner containers: - name: nfs-subdir-external-provisioner image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 volumeMounts: - name: nfs-provisioner mountPath: /data/nfs env: - name: PROVISIONER_NAME value: k8s/nfs-client - name: NFS_SERVER value: 192.168.10.10 - name: NFS_PATH value: /data/k8s-test volumes: - name: nfs-provisioner nfs: server: 192.168.10.10 path: /data/k8s-test几个关键点说明PROVISIONER_NAME是 provisioner 的唯一标识之后的 StorageClass 里必须用同一个值Deployment 的 replicas 必须为 1并且strategy设置为Recreate。因为 provisioner 会在本地缓存状态如果跑多个副本可能因为同时创建同名 PV 导致冲突镜像地址用了官方 registry.k8s.io 的地址如果你的网络无法访问可以用 docker.io 的镜像镜像3.2 创建 RBAC 与服务账户Kubernetes 的操作是受权限控制的provisioner 需要创建 PV、查看 PVC、操作事件等权限所以必须给它绑定一个 ServiceAccount 并赋予相应的 RBAC 权限。我习惯把 ServiceAccount、ClusterRole、ClusterRoleBinding 放在同一个 YAML 文件里apiVersion: v1 kind: ServiceAccount metadata: name: nfs-client-provisioner namespace: kube-system --- 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: run-nfs-client-provisioner subjects: - kind: ServiceAccount name: nfs-client-provisioner namespace: kube-system roleRef: kind: ClusterRole name: nfs-client-provisioner-runner apiGroup: rbac.authorization.k8s.io应用之后我再检查一下 ServiceAccount 是否创建成功。3.3 创建 StorageClassStorageClass 是 Kubernetes 的存储模板里面定义了用什么 provisioner、回收策略是什么、是否允许在线扩容等。我的配置如下apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client provisioner: k8s/nfs-client parameters: archiveOnDelete: true reclaimPolicy: Delete volumeBindingMode: Immediate allowVolumeExpansion: true解读一下参数provisioner的值必须和 Deployment 里的 PROVISIONER_NAME 一致否则 StorageClass 找不到对应的 provisionerarchiveOnDelete设置为 true 时PVC 删除后数据不会直接清空而是重命名为 archived- 开头的目录。这个选项对生产环境特别友好防止误删reclaimPolicy设为 DeletePVC 删除时 PV 也会被删除allowVolumeExpansion开启后后续可以直接修改 PVC 的容量来扩容做完这些基础配置provisioner 就具备动态供给的能力了。接下来用一个真实的应用来验证整个链路是否通畅。4. 用例验证部署一个使用 PVC 的 Nginx4.1 创建 PVC 并观察动态供给过程验证的最好方式就是创建一个 PVC然后看它能不能自动绑定 PV。我先写一个测试 PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name: nginx-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 5Gi storageClassName: nfs-client这里我把 accessModes 写成了 ReadWriteMany这是 NFS 相对其他存储方案的一个杀手级特性。比如两个 Pod 同时挂载同一个 PVC 做读写在 Local PV 或者大多数云盘上是做不到的但 NFS 天然支持。执行 kubectl create 后大约几秒钟内去查看 PVC 状态kubectl get pvc nginx-pvc kubectl get pv正常情况你会在几秒内看到 PVC 状态从 Pending 变成 Bound同时集群里多出来一个 PV命名规则是 pvc- 。此时你去 NFS 服务端的 /data/k8s-test 目录看看会发现多出了一个子目录名字就是那个 pvc-uuid。这个过程就是 dynamic provisioning 的核心逻辑。provisioner 监听到 PVC 创建后通过 NFS 协议在服务端建目录然后创建一个 PV 指向该目录最后把 PVC 和 PV 绑定。4.2 部署 Nginx 挂载 PVCPVC 就绪后部署一个简单的 Nginx Deployment 来测试挂载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:latest volumeMounts: - name: data mountPath: /usr/share/nginx/html volumes: - name: data persistentVolumeClaim: claimName: nginx-pvc应用后等待 Pod 处于 Running 状态然后写入一个测试文件来验证实际读写kubectl exec -it deployment/nginx-storage-test -- bash echo NFS persistent storage test /usr/share/nginx/html/index.html exit接着去 NFS 服务端查看文件是否存在cat /data/k8s-test/pvc-uuid/index.html能看到内容就说明写入成功了。此时再验证跨节点共享把 Deployment 的副本数改成 2或者直接重新调度 Pod 到另一个节点再次执行 cat 命令数据依然存在。4.3 权限问题与 root_squash 的战斗经验上一节我在 exports 配置里开了 no_root_squash这让测试很顺利。但如果你在生产环境用了 root_squash可能会遇到容器内写入文件时报 “Permission denied” 的错误。这里的根源在于NFS 服务端会把客户端传来的 UID 映射到服务端的文件权限模型。root_squash 会把客户端 rootUID 0映射成 nobodyUID 65534而容器内进程往往以非 root 用户运行比如 Nginx 默认以 nginx 用户UID 101运行。服务端目录的属主如果不是这个 UID写入就会失败。解决办法有两种任选其一第一种调整 PV 目录的属主为容器进程的 UID在服务端执行chown -R 101:101 /data/k8s-test第二种在 Deployment 里允许容器以 root 运行或者配置 securityContext 的 fsGroup 让 PVC 目录自动变更属组。我建议第一种因为它更接近生产环境的安全实践。用 initContainer 在启动前修改目录权限也是一个更优雅的思路。4.4 验证 Pod 跨节点迁移后的数据一致性最后做一遍跨节点迁移测试这也是 NFS 区别于本地存储的最大价值。执行:kubectl cordon node-1 kubectl delete pod nginx-storage-test-xxxPod 被删除后Deployment 会自动在另一个节点重新创建一个新 Pod。等待新 Pod Running 后进入容器看看之前写入的文件是否还在kubectl exec -it deployment/nginx-storage-test -- cat /usr/share/nginx/html/index.html如果一切正常你会看到之前的文本内容。这意味着应用重启、故障迁移时数据不会丢失Pod 完全无感知。5. 常见故障排查与避坑实录5.1 PVC 一直 Pending查看 provisioner 日志PVC 卡在 Pending 是最高频的问题。遇到这种情况不要急第一步是看 PVC 的描述事件kubectl describe pvc nginx-pvc如果看到no volume plugin matched或者StorageClass nfs-client not found那大概率是 StorageClass 的 provisioner 名称和 Deployment 里的 PROVISIONER_NAME 不一致或者 StorageClass 根本没有创建成功。如果事件里显示了 wait 开头的消息再去查 provisioner 的日志kubectl logs -n kube-system deployment/nfs-subdir-external-provisioner日志里会出现类似 mount failed 的信息重点看最后几行的报错原因。我遇到过的情况是 NFS_PATH 在服务端不存在provisioner 在创建子目录时失败。检查 NFS_SERVER 和 NFS_PATH 是否与 /etc/exports 里的配置完全匹配。5.2 Pod 挂载 PVC 超时或卡在 ContainerCreatingPod 长时间处于 ContainerCreating通常是 kubelet 在挂载 NFS 卷时超时。先用 describe pod 看事件如果显示 mount: permission denied那大概率是防火墙放行的问题。一个容易被忽视的坑是 mountd 端口不一致。如果你没有像我在 2.3 节里那样固定 mountd 端口NFS 服务端重启后 mountd 的端口会改变。kubelet 可能仍然尝试连接旧的端口导致挂载失败。这时候重启各个节点的 kubelet强制重新解析端口即可systemctl restart kubelet这里我不太推荐在生产环境随意重启 kubelet。比较稳的方式是先确认能在节点上手动挂载排除网络问题后再重启 kubelet。5.3 删除 PVC 后数据还在怎么回事我在 StorageClass 里设置了 archiveOnDelete: true所以删除 PVC 后数据并没有立即消失而是被重命名的归档目录。这其实是刻意的设计。但如果你希望删除 PVC 时彻底删除数据有两种方式把 StorageClass 参数里的 archiveOnDelete 改为 false在删除 PVC 前手动删除对应的 PV 对象让 provisioner 认为 PV 已不存在从而不执行归档对少数敏感数据目录我更推荐手工清理而不是全局修改 StorageClass因为保存归档在很多场景下都是救命缆。5.4 NFS v3 与 v4 的协议选择问题我在标题里看到热词里提到了嵌入式 Linux 下 NFS v3 挂载根文件系统的场景这里多提一句。Kubernetes 节点本身几乎不会遇到 NFS v3 的问题因为现代内核的 NFS 客户端同时支持 v3 和 v4。但如果你在嵌入式开发板上 build 了一个旧内核或者用 busybox 挂载 NFS 失败记得显式加 vers3。Kubernetes 侧如果要指定 NFS 协议版本可以在 PV 的 spec 中增加 mountOptionsmountOptions: - vers4.2不过 nfs-subdir-external-provisioner 的 PV 是自动生成的想改 mount option 必须修改 Deployment 的环境变量 NFS_PROTOCOL。这里我没有实测过 v3 和 v4.2 在性能上的差异但如果你追求极致的兼容性建议在 NFS 服务端同时开启 tcp 下的多个协议版本让客户端按需协商。5.5 性能调优与读写优化方向NFS 性能的瓶颈通常不在 Kubernetes而在网络和 NFS 服务端的磁盘 IO。这里分享几个实测有效的调优方向第一NFS 服务端使用 SSD 硬盘尤其是多 Pod 并发读写时机械硬盘的 IOPS 会立刻成为瓶颈。我测试环境用的是 NVMe 盘几个 Pod 同时跑数据库压力测试也能保持稳定延迟。第二挂载参数中增加 noatime 可减少元数据写入的次数。这个参数可以在 PV 的 mountOptions 里加也可以在 NFS 服务端导出的同时设定我推荐前者因为更灵活。第三如果业务允许把 tcp 的 rsize 和 wsize 调大。默认值通常是 1MB在千兆网络下这已经不算小了但在万兆网络环境下可以尝试增大。具体参数需要在 PV 的 mountOptions 里配置例如rsize1048576,wsize1048576。第四关注 NFS 服务端的网络队列。多节点同时写入时单块网卡的软中断会成为瓶颈。建议用多队列网卡或至少确认 ethtool 中 rx-usecs 中断协调参数在合理范围。5.6 一个生产环境的保守配置模板综合这些经验我整理了一个适合中小型生产环境的 StorageClass 模板直接复制就能用apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client-prod provisioner: k8s/nfs-client-prod parameters: archiveOnDelete: true pathPattern: ${.PVC.namespace}-${.PVC.name} reclaimPolicy: Delete volumeBindingMode: Immediate allowVolumeExpansion: true mountOptions: - noatime - hard - nfsvers4.2这里的 pathPattern 允许你在 NFS 服务端生成带有 namespace 和 PVC 名称的目录从目录名就能一眼看出是哪个业务在用的排查问题非常方便。hard 挂载是默认行为表示服务器挂掉后客户端不会立即报 IO 错误而是持续重试对数据库类应用来说这个参数可以防止进程崩溃。6. 写在最后的实战体会我最初搭建这套环境的时候花了很长时间在防火墙和权限问题上打转一度觉得被 NFS 折磨得想放弃。但后来想明白了NFS 的问题永远不在 NFS 本身而在于配置细节的偏差一个 exports 参数、一个端口、一个 UID都能让整个链路断开。现在这套 NFS 动态供给方案已经在我的集群里稳定跑了几个月。每逢业务上线新应用开发只要在 PVC 模板里指定 storageClassName: nfs-client几秒后一个可用的存储目录就自动准备好完全不需要运维介入。这种“自动化供给”带来的运维效率提升是 NFS 之外很多方案难以匹敌的。如果你看完这篇博文正准备在自己的集群里试一试我的建议是先用一个临时目录和标准 Deployment 做全链路验证再切换到业务场景。把验证时间压缩在半小时以内规则清了剩下的就是按部就班。最后分享一个延伸方向。这套方案目前还只是单机 NFS 服务端数据安全完全依赖那台机器的硬盘和你的备份策略。如果你有足够的预算和精力可以研究一下如何用 DRBD 做两台 NFS 服务端的实时同步再配合 keepalived 做虚拟 IP 漂移。这样当一台服务端宕机时Kubernetes 节点能几乎无感地切换到另一台。存储这条路往下走总有新问题但每解决一个问题你的系统和自己的底气都会扎实一分。
返回列表