Kubernetes NFS动态存储供给实战:nfs-subdir-external-provisioner部署指南
Kubernetes里的存储一直是让不少运维和开发头疼的环节。尤其是开发环境或测试环境里每次都要先找运维手工创建一块NFS目录、再定义一个PV、再写一个PVC去绑定遇到应用要扩容、要新开环境这套流程就得重来一遍。更麻烦的是不同项目组的存储需求五花八门PV的命名规则、容量、挂载路径全靠口头约定时间一长根本理不清。用nfs-subdir-external-provisioner就是冲着解决这件事去的。它相当于在Kubernetes和NFS存储之间放了一个自动售货机你只要提交一个PVC它就自动在NFS服务器上创建对应的子目录、自动生成PV、自动完成绑定整个过程不需要人工介入。这套机制在4.0.18版本里已经非常成熟我基于这个版本在生产环境跑了大半年稳定性、易用性都经得起验证。这篇就围绕这个工具从原理、部署到实际踩坑完整拆一遍。1. 为什么需要动态存储供给静态PV模式下的效率困局先聊聊最原始的静态PV模式因为理解了痛点你才能真正理解这个工具解决的是什么问题。1.1 静态PV的完整工作流程慢在每一步静态PV模式下一次存储申请的过程是这样的运维人员登录NFS服务器手动创建一个目录比如/data/k8s-volume/project-a-data还得手工设置好权限。创建一个PV的YAML文件指定path、server、capacity、accessModes等参数。开发人员创建PVC指定存储大小和访问模式等待着和PV成功绑定。应用Pod通过persistentVolumeClaim引用这个PVC完成挂载。这套流程看起来清晰但真正跑起来每一步都是效率黑洞。目录命名全靠人工记忆没有统一规范。PV一旦创建容量是固定的应用想扩容抱歉你得新建一个大容量的PV再把应用迁移过去。整个流程绕了一整圈从提交申请到Pod真正跑起来快则半小时慢则一天。1.2 动态供给解决的核心问题名称、路径、绑定三步自动化动态存储供给Dynamic Storage Provisioning的核心思路就是把创建PV这件事从人工操作变成自动化操作。你只需要定义一个PVC存储系统会根据PVC的请求自动完成后面所有动作。具体到nfs-subdir-external-provisioner这个工具命名自动化它会根据PVC的命名空间和PVC名称自动生成NFS子目录。比如命名空间是devPVC叫>apt update apt install -y nfs-kernel-server创建共享目录时有几个建议mkdir -p /data/k8s-volume chown -R nobody:nogroup /data/k8s-volume chmod 755 /data/k8s-volume注意nobody:nogroup这个属主不是随便设的。Kubernetes集群里的Pod默认是以nfsnobody的身份去访问NFS共享的如果属主不对Pod启动时就会报Permission denied。chmod 755是为了让其他用户有读和执行权限同时只有属主能写。这个权限设计在后续使用中会避免大量权限纠纷。编辑NFS导出配置文件/etc/exports/data/k8s-volume *(rw,sync,no_subtree_check,no_root_squash)这里几个参数值得解释一下rw允许读写。sync写入时同步落盘保证数据一致性。虽然会牺牲一点性能但在测试环境里更可靠。no_subtree_check禁用子树检查减少IO开销避免某些边界情况下的权限问题。no_root_squash允许客户端以root身份操作文件。如果去掉这个参数某些容器内以root运行的进程在写文件时会遇到奇怪的权限问题。当然安全性要求高的场景需要权衡这个参数。然后重启服务exportfs -ra systemctl restart nfs-kernel-server记得用showmount -e localhost验证一下共享目录是否正常导出。3.2 客户端安装每个节点都要装这一步是很多新手忽略的。Provisioner本身是一个Deployment理论上只需要在它运行的节点上安装NFS客户端工具。但是万一Provisioner被调度到了其他节点或者后期扩容了节点没有客户端工具就会导致Pod挂载失败。所以我的建议是集群里所有节点统一安装。# Debian/Ubuntu apt install -y nfs-common # CentOS/RHEL yum install -y nfs-utils装完后可以用showmount -e NFS_SERVER_IP测试通信。3.3 集群侧检查确认关键运维入口在部署Provisioner之前最好先确认几件事Kubernetes版本1.20及以上太老的版本可能不支持某些API组。集群能正常访问NFS服务器从某个节点上手动挂载一下NFS共享确认网络和认证都OK。具备创建RBAC对象的权限Provisioner需要ClusterRole、ClusterRoleBinding、ServiceAccount等资源。这些准备工作做完部署Provisioner就会非常顺畅。4. 部署实践从Helm Chart到手动YAML的两条路线部署方式主要有两条路Helm和纯YAML。各有优劣我都跑通过下面分别说明。4.1 Helm方式推荐优先选择Helm方式的好处是参数集中、升级方便。我用的是Bitnami维护的Chart但实际更推荐官方chart这里以实际操作为准需要先添加仓库。实际上这个工具通常通过官方提供的chart部署。我的习惯是先把chart拉下来看看默认值再传参安装。命令大概长这样helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ helm repo update然后创建一个values文件重点配置以下几项nfs: server: 192.168.1.100 path: /data/k8s-volume storageClass: name: nfs-storage defaultClass: false allowVolumeExpansion: true reclaimPolicy: Delete archiveOnDelete: true解释一下这几个配置reclaimPolicyPV的回收策略。Delete表示PVC删除时底层NFS目录也会被清理或者根据archiveOnDelete策略进行归档。Retain表示保留数据需要手动清理。对于测试环境DeletearchiveOnDelete: true的组合最灵活。allowVolumeExpansion开启后允许PVC在线扩容。这在数据增长迅速的测试环境里特别实用。defaultClass是否设置为默认StorageClass。如果集群里只有一个存储类设成true可以免去在PVC里显式声明storageClassName的麻烦。执行安装helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner -f values.yaml -n kube-system4.2 手动YAML方式理解组件依赖关系不想用Helm的话手动部署也就四个YAML文件ServiceAccount、ClusterRole、ClusterRoleBinding、Deployment。关键在Deployment里需要把NFS共享目录挂载进控制器Pod并且传入相关环境变量。核心Deployment片段如下apiVersion: apps/v1 kind: Deployment metadata: name: nfs-provisioner namespace: kube-system spec: replicas: 1 selector: matchLabels: app: nfs-provisioner template: metadata: labels: app: nfs-provisioner spec: serviceAccountName: nfs-provisioner containers: - name: nfs-provisioner image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.18 imagePullPolicy: IfNotPresent 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-volume volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes volumes: - name: nfs-client-root nfs: server: 192.168.1.100 path: /data/k8s-volume这里有个重要的环境变量PROVISIONER_NAME它必须是唯一的标识符而且必须和StorageClass里provisioner字段的值完全一致。如果拼写不一致PVC永远无法触发Provisioner。4.3 StorageClass定义与参数对照最后还需要定义StorageClassapiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: archiveOnDelete: true pathPattern: ${.PVC.Namespace}/${.PVC.Name} reclaimPolicy: Delete volumeBindingMode: Immediate allowVolumeExpansion: truepathPattern这个参数很灵活特别是配合命名空间使用。比如设置成${.PVC.Namespace}/${.PVC.Name}NFS服务器上会自动生成按命名空间归类的目录结构/data/k8s-volume/ ├── default/ │ └── my-pvc/ ├── dev/ │ └── api-data/ └── prod/ └── database-data/这样一来存储管理变得更加符合业务直觉也方便做备份和清理。4.4 部署后自检三板斧部署完成不要急着接业务先做三轮自检检查Pod状态kubectl get pods -n kube-system | grep nfs-provisioner确认Pod是Running。检查StorageClasskubectl get sc确认nfs-storage存在并且PROVISIONER列显示的是你设置的名字。手动测试快速创建一个临时PVC观察它能否自动变为Bound。这一步通过基本就稳了。5. 实际使用从PVC到Pod挂载的完整样板部署完成后日常使用就非常简单了。但是这里的细节同样决定体验。5.1 创建PVC的推荐模板项目里我习惯维护一套统一的PVC模板避免每个人各自发挥apiVersion: v1 kind: PersistentVolumeClaim metadata: name: app-data namespace: dev spec: accessModes: - ReadWriteMany storageClassName: nfs-storage resources: requests: storage: 10Gi注意accessModes这里选的是ReadWriteMany。NFS天然支持多节点同时读写所以如果你的应用是多副本的就选它。如果你的应用是单副本的选ReadWriteOnce也没问题但用ReadWriteMany更灵活后续扩容不用再改配置。5.2 Pod挂载PVC的推荐写法挂载时的写法也有讲究。以Deployment为例apiVersion: apps/v1 kind: Deployment metadata: name: demo-app namespace: dev spec: replicas: 3 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: containers: - name: app image: nginx:1.25 volumeMounts: - name: data mountPath: /usr/share/nginx/html volumes: - name: data persistentVolumeClaim: claimName: app-data就这么简单。提交之后你会发现在NFS服务器上自动多了一个目录/data/k8s-volume/dev-app-data如果设置了pathPattern则对应规则路径。Pod里的/usr/share/nginx/html等价于NFS服务器上的某个目录任何节点、任何副本写入的数据都是即时共享的。5.3 在线扩容真的可以在线如果PVC设置了allowVolumeExpansion: true那么扩容只需要改PVC的存储大小kubectl patch pvc app-data -n dev -p {spec:{resources:{requests:{storage:20Gi}}}}执行后PVC会变为FileSystemResizePending状态然后自动扩容。不过这里有一个细节要注意allowVolumeExpansion生效的前提是文件系统支持扩容。NFS底层一般没问题但如果用了某些特定类型的存储驱动可能会受限。6. 踩坑记录我在NFS动态供给上遇到的四个真实问题这部分是重点。工具本身不难但实际跑起来会遇到一些文档里没详细说明的坑。6.1 第一个坑PVC一直Pending但Provisioner日志却无异常有一次同事反馈PVC一直Pending我盯着Provisioner日志看了半天什么都没打出来。最后发现问题出在StorageClass里的provisioner字段和Deployment环境变量PROVISIONER_NAME不一致。一个拼写是nfs-subdir-external-provisioner另一个是nfs-subdir-external-provisioner-2虽然看起来很像但在Kubernetes里这是两个完全不同的Provisioner没有控制器去响应这个PVC。这类问题排查时最快的定位命令是kubectl describe pvc your-pvc-name如果Events里没有任何动静十有八九是Provisioner名称不匹配。6.2 第二个坑Pod挂载成功后写入文件却报 Permission Denied这个问题的本质是NFS服务端的目录属主和权限问题。前面提到过NFS共享根目录我设置了nobody:nogroup但某次同事手动在服务端创建了一个子目录属主是他自己的账号结果Pod写文件时就失败了。解决方案有两类统一规范明确约定所有NFS共享目录的属主必须是nobody:nogroup。使用no_root_squash如果集群里的Pod都是内网可信的这个参数能避免不少麻烦。我更推荐前者因为安全风险更可控。6.3 第三个坑PV的回收策略导致数据被意外删除默认的reclaimPolicy是Delete这意味着PVC删除时Provisioner会直接删除NFS上的对应目录。对于某些重要数据这种策略太过激进。我在生产环境后来为了安全换掉了和测试环境采用的策略完全不一样环境reclaimPolicyarchiveOnDelete行为说明测试环境DeletetruePVC删除后目录重命名为archived-xxx数据敏感区Retain不适用PVC删除后PV保留需要手动清理如果你不小心把重要数据所在的PVC给删了但在创建StorageClass时设置了archiveOnDelete: true去NFS服务器上找找被改名的目录大概率能捞回来。6.4 第四个坑大规模PVC创建时的性能瓶颈当一次性创建几十上百个PVC时Provisioner会显得有些吃力。原因是它在处理每个PVC时都需要进行NFS挂载和目录创建而这个流程是串行的。虽然测试环境很少遇到这种极端情况但如果你的CI流水线会频繁批量创建环境建议提前给Provisioner的Pod设置合理的资源请求和限制必要时增加副本数。另外提醒一下archiveOnDelete模式下如果短时间内删除大量PVCNFS服务器上会堆积大量archived-开头的目录。建议写一个定时任务定期清理否则共享目录会越来越乱。7. 进阶优化思路从够用到好用工具跑通了只是起点。要让这套存储方案真正好用还有几个值得做的优化。7.1 设置默认StorageClass的取舍把nfs-storage设为默认存储类storageclass.kubernetes.io/is-default-class: true带来的好处是开发人员可以不用写storageClassName字段。但坏处也很明显如果集群里还有其他存储类容易造成误用。我的建议是如果集群里只有NFS这一个存储方案就设默认如果还有其他存储方案就不要设默认让开发显式声明避免语义混淆。7.2 结合StatefulSet使用不同副本的不同数据卷StatefulSet和动态供给是天作之合。每个副本会自动生成一个独立PVC命名规则是卷名-名称序号。比如一个3副本的StatefulSet会自动生成3个不同目录的PVC每个副本拥有独立的数据空间apiVersion: apps/v1 kind: StatefulSet metadata: name: mongo spec: serviceName: mongo replicas: 3 volumeClaimTemplates: - metadata: name: data spec: accessModes: [ ReadWriteOnce ] storageClassName: nfs-storage resources: requests: storage: 5Gi这在部署数据库集群如MongoDB副本集、ZooKeeper时非常有用。不过提醒一下数据库类应用对存储延迟敏感NFS这种网络存储的IOPS不一定能满足高并发写入需求重度生产库慎用。7.3 监控与告警最后NFS动态供给方案虽然好用但底层依然依赖NFS服务器的健康状态。建议在监控平台里加上几项关键指标NFS服务器的磁盘使用率超过80%就要关注。Provisioner Pod状态如果挂了所有动态供给都会瘫痪。PVC状态批量出现Pending要立刻排查。我见过有人NFS磁盘满了还不知道PVC虽然能创建但数据写入时所有Pod都会卡在IO等待上那种故障排查起来是真的痛苦。8. 写在最后的个人体会这套方案我在生产环境跑了大半年最大的感受是它把Kubernetes存储的最后一公里问题解决得足够优雅。开发人员不需要理解PV、NFS这些底层概念只需要提交一个PVC剩下的全部自动完成。运维人员也不再需要每天手动创建目录、编写PV可以把精力放在更重要的容量规划和数据备份上。如果你现在的集群还在用静态PV的方式管理存储或者每天被各种存储申请搞得焦头烂额真的建议花一个下午部署一套NFS动态供给试试。它不一定适合所有场景但对于绝大多数中小规模集群和测试环境来说绝对是一笔性价比极高的投入。