首页 / 资讯中心 / 文章详情

NFS动态供给实战:从静态PV到Kubernetes存储自动化

NFS动态供给实战:从静态PV到Kubernetes存储自动化 ★ FEATURED ARTICLE
之前我在集群里给应用配存储流程大概是这样业务提需求我跑到NFS服务器上建目录手动写一个PV的yaml再创建PVC等状态从Pending变成Bound然后告诉业务可以用了。一个新应用还好应用一多这套流程就成了瓶颈而且手动创建PV很容易出现容量口径不一致、目录命名混乱、环境之间难同步的问题。后来我用NFS动态供给把这块彻底自动化——部署一个外部供给器创建一个StorageClass业务直接提交PVC声明PV会自动生成并完成绑定NFS共享目录下也会自动出现对应的数据目录。这篇文章把完整实操步骤、背后原理和踩过的坑都整理出来适合正在用或打算用NFS做Kubernetes存储后端的团队参考。1. 为什么需要动态供给静态PV模式的三道坎1.1 静态PV的典型配置过程在Kubernetes没有动态供给之前存储基本靠管理员手工维护。比如业务需要一个2Gi的NFS卷我得先登录NFS服务器在共享目录下创建一个类似/data/k8s-nfs/web-nginx-2gi的目录然后回到集群里写一份PVapiVersion: v1 kind: PersistentVolume metadata: name: nfs-web-nginx-2gi spec: capacity: storage: 2Gi accessModes: - ReadWriteOnce nfs: server: 192.168.10.100 path: /data/k8s-nfs/web-nginx-2gi写完之后再创建PVC等PV控制器把两者绑定。这套流程看起来没问题但放到多团队、多环境的场景里就有三个很现实的问题。第一个问题是资源利用率。管理员为了不让业务等太久通常会预分配容量比如一次建10Gi的PV但实际业务只用1Gi剩下9Gi就悬在那里。更尴尬的是如果业务真的需要扩容你不可能把一个已绑定的PV改大只能新建一个更大容量的PV然后把数据迁移过去成本非常高。第二个问题是流程耦合。每次新应用上线都要走一次“提需求-建目录-写PV”的流程业务和运维被绑死在一起。有些团队为了省事会提前建几十个PV放着但PV本身只是一个静态资源描述释放了还要手动Recycle非常繁琐。第三个问题是环境一致性。开发、测试、生产三套集群PV目录路径、命名规则、容量口径很难保持一致经常出现测试环境绑定正常、生产环境因为NFS路径写错一直Pending的情况。1.2 动态供给的核心机制动态供给解决的核心问题是把“存储后端准备”这件事从管理员手里移交到一个控制器程序手中。用户只需要声明自己需要多大容量、以什么方式访问剩下的事情由供给器处理。这里涉及三个角色PVC是用户的存储申请单StorageClass是存储模板Provisioner是实际干活的工人。当Kubernetes的控制平面发现一个PVC使用了某个StorageClass而该StorageClass又没有现成PV可以绑定时它会通过 storage.k8s.io 的接口调用对应名称的Provisioner让Provisioner去后端存储上创建实际卷目录再生成一个PV对象最后由PV控制器完成PVC和PV的绑定。如果你翻过Kube Controller Manager里PV控制器的源码会发现这个协作就是典型的Informer加分发逻辑PV控制器监听PVC和PV对象的变化根据PVC引用的StorageClass找到Provisioner名字再通过外置的供给器完成卷的创建和删除。这也是为什么Kubernetes的存储体系能够容纳那么多不同后端云厂商、NFS、Ceph都能通过同样的接口接入。NFS动态供给属于其中实现门槛最低的一种。因为NFS共享目录本身就可以通过子目录划分出无数个“卷”供给器要做的只是在共享根目录下新建一个子目录然后把这个子目录挂给Pod。相比云硬盘需要调用云厂商API、Ceph需要走RBD协议NFS动态供给对普通团队友好得多这也是内网环境里它最流行的原因。2. 环境准备NFS服务端配置与客户端验证2.1 版本与环境要求做NFS动态供给先别急着在Kubernetes里写yaml把NFS服务端和客户端的环境理顺了后面能少踩一半的坑。我的建议版本是Kubernetes 1.20以上最好在1.23或更高版本上做。原因不只是对StorageClass v1 API的支持稳定还因为Kubernetes 1.25之后存储插件全部走向Out-of-Tree外部供给器这种模式更加规范。NFS服务端用常见的Ubuntu 20.04/22.04或CentOS 7/8都可以客户端要求所有Kubernetes节点上都安装NFS相关的客户端工具否则Pod调度到某个节点时会因为找不到mount.nfs而挂载失败。具体来说Ubuntu/Debian节点需要安装nfs-commonCentOS/RHEL节点需要安装nfs-utils。很多集群装好后没有预装这两个包应用一调度过去就报exec: mount.nfs: executable file not found in $PATH这是第一个常见坑。防火墙方面NFS依赖2049端口。如果使用NFSv3还要处理rpcbind111端口的通信很多安全组只放行了2049结果挂载时客户端先要去查111端口查询失败就报Connection refused。我建议在Kubernetes与NFS服务器之间的安全组、防火墙同时放行2049和111端口并且后续在StorageClass的挂载参数里指定nfsvers4减少对rpcbind的依赖。2.2 NFS服务端安装与exports配置以Ubuntu Server为例安装和配置NFS服务端只需要几条命令sudo apt update sudo apt install -y nfs-kernel-serverCentOS/RHEL这边稍微不同sudo yum install -y nfs-utils sudo systemctl enable --now nfs-server服务端安装完成后创建一个共享目录。这里要注意不要把共享目录直接建在根目录下建议用独立路径后面动态供给创建的所有业务子目录都会在这个目录之下sudo mkdir -p /data/k8s-nfs目录权限是很容易被忽略的点。在测试环境里我习惯先把目录归属临时调成所有人都能写sudo chown nobody:nogroup /data/k8s-nfs sudo chmod 755 /data/k8s-nfs然后编辑/etc/exports把共享目录发布出去/data/k8s-nfs 192.168.10.0/24(rw,sync,no_root_squash,no_subtree_check)这里有几个参数需要说明。rw表示读写默认也可以只读但Kubernetes动态供给需要创建子目录必须用rw。sync表示服务端在数据落盘后再回复客户端写请求数据安全性更好。追求性能时可以用async但存储数据卷不建议这么做我会一直用sync。no_root_squash表示来自客户端的root用户不做降权映射。这个参数测试环境用起来很省事因为Kubernetes里很多Pod默认以root运行供给器创建子目录时如果不加这个选项root会被映射为nobody用户而共享目录的属主是root就会提示没有权限创建目录。no_subtree_check用来减少目录检查带来的性能损耗单目录共享场景下问题不大加上它也是社区默认做法。修改完exports文件后执行sudo exportfs -rav sudo systemctl restart nfs-serverexportfs -rav会把exports配置重新加载。这里有一个经验不要同时导出共享根目录和它的子目录比如既导出/data/k8s-nfs又导出/data/k8s-nfs/special挂载的时候会出现奇怪的交叠问题NFS服务端也有可能报错。2.3 客户端验证与挂载测试服务端配置好之后先在任意一台Kubernetes节点上做一次挂载测试确认网络和权限都没问题再进入下一步。这类验证我每次换环境都会做基本可以筛掉八成以上的NFS配置错误。showmount -e 192.168.10.100这条命令可以看到NFS服务端导出了哪些共享目录。如果输出为/data/k8s-nfs 192.168.10.0/24说明exports生效了。如果提示Export list空或者直接卡住多半是111端口不通或者客户端缺少工具先解决网络问题。然后手动挂载验证sudo mkdir -p /mnt/nfs-test sudo mount -t nfs 192.168.10.100:/data/k8s-nfs /mnt/nfs-test touch /mnt/nfs-test/write-test.txt sudo umount /mnt/nfs-test在测试目录里能成功写入文件说明NFS共享、权限、SELinux这些环节都正常。如果写入的时候报Permission denied多半是exports里的no_root_squash没配好或者目录自身权限不对这时候先不要进Kubernetes操作把问题留在物理层解决。2.4 SELinux对NFS挂载的影响如果你用的是CentOS/RHEL这类带SELinux的系统Kubernetes节点上还有一个隐藏关卡。默认SELinux会拦截某些进程访问非标准挂载类型的文件系统表现形式是Pod启动时挂载一直失败但NFS服务器上根本看不到任何写入尝试。最简单的方法是在所有Kubernetes节点上执行sudo setsebool -P virt_use_nfs 1或者临时关闭SELinuxsudo setenforce 0生产环境不建议直接关闭SELinux用setsebool -P virt_use_nfs 1精准放行NFS访问是更稳妥的做法。这个问题在纯Ubuntu节点上不存在但很多团队是混合部署检查节点类型的时候要一并考虑。3. 部署外部供给器RBAC、Deployment与StorageClass的完整配置3.1 组件选型subdir外部供给器与CSI驱动怎么选NFS动态供给现在有不少实现方案老的nfs-client-provisioner已经停止维护目前社区最常用的是nfs-subdir-external-provisioner它保留了一个稳定的镜像仓库地址功能也一直在更新。另外还有NFS CSI Driver走的是标准CSI接口功能和标准化程度更高。到底选哪个我的判断标准很简单如果只是给内部Kubernetes集群做一个够用且运维成本低的NFS动态存储用nfs-subdir-external-provisioner因为它就是一个Deployment加一个StorageClass部署简单也没那么多依赖很多中小团队到现在都在用它实践验证已经很充分。如果后续需要做存储快照、需要严格遵循CSI规范或者打算以这套存储为底座做更复杂的开发再考虑NFS CSI Driver。NFS CSI Driver的问题是需要额外安装CSI Node Driver Registrar和Liveness Probe这些组件还要在每个节点上以DaemonSet方式运行CSI Node插件链路更长排查问题面更广。作为起步subdir方案性价比最高。3.2 RBAC授权每个权限都是干什么的外部供给器本质上是一个运行在集群里的控制器它必须通过Kubernetes API读取PVC、创建PV、更新PV状态、记录事件。所以第一步是给它建一个服务账号和对应的RBAC授权。apiVersion: v1 kind: ServiceAccount metadata: name: nfs-provisioner namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: nfs-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-provisioner subjects: - kind: ServiceAccount name: nfs-provisioner namespace: kube-system roleRef: kind: ClusterRole name: nfs-provisioner-runner apiGroup: rbac.authorization.k8s.io逐个解释一下权限的用途。persistentvolumes的get/list/watch/create/delete是供给器的核心能力它要监听PV对象创建新的PV来承接PVC请求删除PVC时还要清理对应PV。persistentvolumeclaims的get/list/watch/update用来监听业务提交的PVC声明并在创建PV成功后更新PV与PVC的绑定信息。写权限只需要update不需要deletePVC的删除是用户自己通过kubectl delete pvc操作供给器只负责响应删除后的事件。storageclasses的get/list/watch是为了在PVC创建时通过StorageClass的provisioner字段确认这个申请是否由自己处理。events的create/update/patch一直很容易被忽略但它很重要。供给器工作时会把进度和错误写进PVC或PV的事件里你在kubectl describe pvc里看到的Provisioning succeeded就是通过这个权限写进去的。如果RBAC里漏了事件权限PVC会一直Pending但日志里却看不到明显的失败信息排查起来很抓狂。3.3 Deployment配置环境变量与卷挂载RBAC就绪后创建外部供给器的Deployment。这里需要关注环境变量和NFS卷挂载两部分。apiVersion: apps/v1 kind: Deployment metadata: name: nfs-subdir-external-provisioner namespace: kube-system spec: replicas: 1 selector: matchLabels: app: nfs-subdir-external-provisioner template: metadata: labels: app: nfs-subdir-external-provisioner spec: serviceAccountName: nfs-provisioner containers: - name: nfs-provisioner image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 imagePullPolicy: IfNotPresent env: - name: PROVISIONER_NAME value: nfs-storage-provisioner - name: NFS_SERVER value: 192.168.10.100 - name: NFS_PATH value: /data/k8s-nfs volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes volumes: - name: nfs-client-root nfs: server: 192.168.10.100 path: /data/k8s-nfsPROVISIONER_NAME这个环境变量非常关键。它定义了供给器的名字而这个名字必须和后面创建StorageClass时的provisioner字段完全一致。很多人在这里随手写了一个名字又在StorageClass里写另一个名字创建PVC时供给器收到请求后一看名字对不上直接扔掉事件PVC就只能一直Pending。NFS_SERVER和NFS_PATH分别填NFS服务器的IP和共享目录路径。注意NFS_PATH填的是共享根目录也就是/data/k8s-nfs不要加上后面要自动生成的子目录那些子目录是供给器自己创建的。Deployment里还把这个NFS共享目录以普通NFS卷的形式挂载进了容器/persistentvolumes路径。供给器创建PV时就是在/persistentvolumes下创建子目录它会把这个子目录的路径转换成NFS服务器上的实际路径写入PV对象。所以如果NFS配置有变化这两个位置要保持一致。镜像地址是官方仓库registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2。如果你所在环境不能直接访问外网可以先在能连外网的机器上docker pull再推到内网镜像仓库然后把Deployment里的image改成内网地址。副本数保持1即可不需要高可用因为同一时间多个供给器同时处理同一个PVC的创建请求会产生竞争反而容易出问题。Kubernetes原生的PV控制器设计也倾向于卷供给是串行操作。3.4 StorageClass参数逐项拆解外部供给器跑起来之后创建StorageClass。我给出一个完整示例apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client provisioner: nfs-storage-provisioner reclaimPolicy: Delete volumeBindingMode: Immediate allowVolumeExpansion: true mountOptions: - nfsvers4 - hard - nolock parameters: archiveOnDelete: true这里每个参数都值得说道说道。provisioner必须和Deployment里的PROVISIONER_NAME一模一样这是整个动态供给的“路由钥匙”。reclaimPolicy决定PVC删除后PV如何处理。Delete表示PV会被删除NFS目录也会被清理或归档Retain表示PV和底层数据都不动需要管理员手动介入回收。对于NFS动态供给我建议数据保护优先级高先用Delete配合archiveOnDelete: true这样PVC删除后数据不会被物理清除而是被重命名归档。volumeBindingMode分为Immediate和WaitForFirstConsumer。NFS是全局共享存储通常不需要等Pod调度后再创建PV所以推荐ImmediatePVC创建完就立即供给并绑定。如果存储有拓扑限制比如云盘只能挂载到特定可用区的节点才需要用WaitForFirstConsumer。allowVolumeExpansion: true表示允许在线扩容。用户修改PVC的resources.requests.storage后供给器会更新PV的容量字段Pod重新挂载后可识别到新容量。不过要注意NFS共享目录本身并没有强制的配额机制扩容更多是提供一种容量管理手段真正限制使用量还需要结合NFS服务端的配额或者外部监控。mountOptions会作为挂载参数传给Pod所在节点的kubelet。nfsvers4强制使用NFSv4能在很大程度上规避rpcbind和NFSv3带来的防火墙问题我强烈建议加上。hard表示NFS服务器短暂不可达时客户端不会放弃写操作而是持续重试适合持久化卷。nolock对某些后端锁协议有兼容性问题一般内部NFS用不上但一些云厂商的NFS服务会建议加上按需调整。archiveOnDelete的值为字符串true或false。设为true时删除PVC后NFS目录会被重命名为类似/data/k8s-nfs/default-pvc-name-pv-name-archived-20240101120000并保留设为false时删除PVC会直接删除NFS目录这个操作不可恢复。生产环境数据丢失往往就是从这一个小参数开始的谨慎为上。4. PVC触发PV自动生成从提说到目录落地的完整链路4.1 创建一个测试PVC部署完成不代表万事大吉一定要实际创建一个PVC来验证整条链路。首先是测试PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-nfs-pvc spec: accessModes: - ReadWriteOnce storageClassName: nfs-client resources: requests: storage: 2Gi创建命令和执行结果kubectl apply -f test-pvc.yaml kubectl get pvc test-nfs-pvc正常情况下PVC很快会变成Bound。如果保持在Pending参照下一章的排查思路。这里刻意把存储类名写成nfs-client和上一章StorageClass里的metadata.name对应很多人在测试时随手写错或者创建PVC时漏掉storageClassNamePVC就会找不到对应的供给器永远Pending。4.2 观察PV自动生成的完整链路执行kubectl get pv你会看到一个自动生成的PVkubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEMODE pvc-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 2Gi RWO Delete Bound default/test-nfs-pvc nfs-client FilesystemPV名字是系统生成的UUID容量来自PVC的请求值回收策略来自StorageClassStorageClass列会显示nfs-client。此时再到NFS服务端看共享目录ls -la /data/k8s-nfs drwxr-xr-x 4 root root 4096 ... drwxr-xr-x 2 root root 4096 ... default-test-nfs-pvc-pvc-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx目录名由命名空间-PVC名称-PV名称三段组成这样即使同一个NFS共享目录被多个命名空间使用也不会出现目录冲突。这个目录就是供给器在容器内部通过挂载的/persistentvolumes路径创建的它创建完成后将NFS服务器IP、共享根目录和子目录拼接成PV的spec.nfs.path字段写入Kubernetes API。如果PVC还是Pending可以看它的事件kubectl describe pvc test-nfs-pvc正常事件里会出现类似Successfully provisioned volume pvc-...的记录潜在的数据目录就绪等信息也会写进Events。如果Events为空大概率是供给器根本没有收到事件优先检查RBAC和StorageClass的provisioner名称。4.3 挂载验证文件真的写进NFS目录PVC绑定成功只是前半程还要用Pod实际挂载一次。最直接的方式是跑一个busybox的临时PodapiVersion: apps/v1 kind: Deployment metadata: name: nfs-test-pod spec: replicas: 1 selector: matchLabels: app: nfs-test template: metadata: labels: app: nfs-test spec: containers: - name: app image: busybox:1.36 command: [sh, -c, echo hello-from-nfs /data/test.txt sleep 3600] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: test-nfs-pvcPod起来后进入容器kubectl exec -it deployment/nfs-test-pod -- cat /data/test.txt输出hello-from-nfs说明挂载成功。再到NFS服务器上看cat /data/k8s-nfs/default-test-nfs-pvc-pvc-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/test.txt如果文件内容也能看到说明数据确实写到了NFS后端而不是停留在Pod本地。这一步验证的是整个数据路径的连通性。我之前遇到过一种情况PVC绑定、PV创建、事件都正常但Pod起来后读写数据没有落到真实NFS目录最后发现是NFS挂载参数里nfsvers配置不对容器看到的目录是本地空目录NFS服务器上根本没有任何文件。排查这种问题最直接的办法就是两边同时看文件。4.4 动态供给的目录命名规则与容量语义很多人在实际使用中会把动态供给想象成“给每个PVC划了一个固定大小的箱子”但NFS动态供给的容量语义并不完全如此。从PV对象看capacity.storage确实是2GiPV控制器在计算容量时会严格按这个值来比对PVC的请求。但从NFS文件系统看这个目录只是共享根目录下的一个普通子目录NFS本身并不知道“这里只能写2Gi”。也就是说动态供给帮你解决了“目录自动创建”和“PV对象自动生成”但并没有帮你做硬配额。这个区别在排障时很关键。比如某个Pod的PV声明是100Gi但在NFS根目录所在磁盘空间只有80Gi的机器上运行实际能写的最大数据量受限于磁盘空间而不是受限于PV容量。在调研容量告警和做容量规划时要同时关注NFS服务端磁盘的真实占用不能只看PV声明。目录命名规则也值得注意。Kubernetes官方外部供给器默认段是命名空间-PVC名-PV名其中PV名是一段UUID。这个命名规则不仅用于识别目录属于哪个PVC还决定了删除PVC时的归档行为。如果业务需要更清晰的目录结构可以在StorageClass里配置pathPattern参数比如parameters: pathPattern: ${.PVC.namespace}/${.PVC.name}这样NFS下会生成/data/k8s-nfs/default/test-nfs-pvc这样的目录结构团队维度管理更直观。不过改了目录命名规则后要注意旧PVC的目录仍然按旧规则命名不要混用否则回收清理的时候会比较混乱。5. 踩坑排错实录从Pending到Permission Denied的排查思路5.1 PVC一直Pending先看这四处PVC Pending可以说是NFS动态供给最常见的问题我遇到的场景几乎可以归纳为四类。第一类是StorageClass名称不匹配。PVC引用了nfs-client但集群里根本没有这个StorageClass或者名字拼写有差异PVC会一直Pending。排查方式kubectl get sc看看实际存在的StorageClass名字和PVC里的storageClassName一一对应。第二类是Provisioner名称不一致。StorageClass存在但它的provisioner字段和Deployment里的PROVISIONER_NAME不一致。这时PVC也会有P语状态而且kubectl describe pvc的事件里通常看不到任何供给器操作的记录原因就是供给器收到事件后发现自己不负责这个PVC。对比这两个名字即可定位。第三类是供给器Pod没有正常运行。用kubectl get pods -n kube-system | grep nfs查看供给器状态再用kubectl logs看日志。常见错误是镜像拉不下来、NFS挂载失败、RBAC权限被拒绝。这些在日志里都会有明确输出。第四类是动态供给器根本没有收到PVC事件。这通常和RBAC授权有关但表现比较隐蔽因为供给器Pod日志可能是空的PVC也没有Events。检查ClusterRole里的权限是否完整尤其是否缺少persistentvolumeclaims的update或者events的create。我将常见问题放到一张表里方便对照现象可能原因优先排查位置PVC一直Pending无Events供给器未部署、RBAC缺失供给器Pod日志PVC一直Pending有failed事件StorageClass的provisioner与Deployment不一致kubectl get sc -o yaml对比Deployment环境变量PVC一直Pending提示no volume plugin存储类未关联任何外部供给器kubectl get sc确认名称PVC创建即绑定到错误PV存在同名静态PVkubectl get pv检查现有PV5.2 挂载报错的排查链路PVC绑定正常但Pod一直ContainerCreating事件里报挂载错误这种情况比PVC Pending更难排查因为涉及节点、NFS协议和防火墙。最常见的错误是mount.nfs: access denied by server while mounting 192.168.10.100:/data/k8s-nfs/xxx。access denied说明目录本身访问不了原因通常在exports配置或者客户端IP不在授权网段。先在NFS服务器上执行exportfs -v看实际导出的参数确认客户端节点IP在内。然后再用showmount -e验证。另一个常见错误是mount.nfs: Connection timed out这往往是防火墙只放行了2049没有放行111端口或者NFS服务器上rpcbind没启动。在Kubernetes节点上先手动执行mount -t nfs -o nfsvers4 192.168.10.100:/data/k8s-nfs /mnt/nfs-test手动挂载成功说明节点层面没问题再回过来查Pod事件手动挂载也失败问题就限定在节点和NFS服务器之间和Kubernetes无关。还有一种情况是Pod事件报no route to host这通常不是防火墙而是NFS服务端IP地址配置错误或者NFS服务器有多个网卡、多个IP客户端访问了不通的IP。用ip addr检查服务端网卡地址确保和exports授权、k8s节点路由一致。5.3 目录权限引发的各类读写异常NFS动态供给能创建PV不代表业务Pod就一定能正常读写。实际使用中因为权限问题导致的读写失败占比很高。如果Pod容器以root用户运行NFS默认的root_squash会把root映射成nobody用户。nobody对共享目录没有写权限时Pod内写文件就会报Permission denied但PVC绑定是完全正常的这种问题非常迷惑人。我在测试环境解决这个问题最直接的办法是在exports里加no_root_squash。生产环境更推荐的做法是把共享目录属主改成业务使用的UID然后在Pod的securityContext里用fsGroup指定同样的组。例如securityContext: fsGroup: 1000但这里要补充一个经验NFS挂载对fsGroup的支持不是绝对的如果挂载选项里禁用了某些NFS协议扩展chown可能会失败。遇到这种情况与其在Kubernetes侧反复调securityContext不如先回到NFS服务端用chown -R 1000:1000 /data/k8s-nfs把目录权限固定下来再配合no_root_squash或anonuid/anongid做用户映射这样更可控。如果在CentOS节点上还出现了SELinux相关报错优先按2.4节的方法处理不要先怀疑NFS权限。5.4 删除PVC时的回收异常动态供给的删除行为同样值得提前验证。我在测试动态供给时一定会做一次“删除测试”创建一个临时PVC写入标记文件然后删除PVC再检查NFS目录是否按预期被归档或删除。当reclaimPolicy: Delete且archiveOnDelete: false时删除PVC后PV会被删除NFS目录也会直接消失这个过程不会有提示数据不可恢复。如果你在生产环境犯了这种错误错误本身往往不是Kubernetes的故障而是一次被动清理。当archiveOnDelete: true时删除PVC后NFS目录会被重命名名字可能带-archived后缀数据完整保留。此时PV对象已经删除但NFS目录里的文件还在恢复机制有两种要么在NFS服务器上手动改回目录名再手动创建一个静态PV指向该目录要么直接把归档目录整个复制到新的业务目录。后者更安全因为手动改回目录名可能会让供给器误以为这是自己管理的目录导致后续删除PVC时被自动清理。我这里强烈建议把archiveOnDelete: true作为NFS动态供给的默认配置除非你对数据丢失完全无所谓。性能上这点归档开销可以忽略但它能防止“手滑删PVC导致全部数据丢失”的灾难性失误。6. 生产化调整回收策略、归档规则与存储类规划6.1 回收策略与归档协同StorageClass的reclaimPolicy和archiveOnDelete需要组合理解它们不是同一层概念。reclaimPolicy是Kubernetes层面的PV回收策略Delete会让PV对象在PVC删除后自动删除Retain会让PV对象保留即使PVC删除PV也仍然存在且必须人工处理。archiveOnDelete更多是供给器自身的行为逻辑它决定供给器收到PVC删除通知后是物理删除NFS目录还是把目录改名保留。只有当reclaimPolicy是Delete时供给器才会接到删除PV的指令archiveOnDelete才有意义。我推荐的组合是reclaimPolicy: Delete parameters: archiveOnDelete: true这个组合既能保证业务删除PVC时PV对象不残留又能让NFS底层数据被归档保留。归档目录会在共享根目录下以-archived结尾或带时间戳的形式存在后续可以从容决定是恢复还是定时清理。如果确有针对某些业务不保留数据再为它单独创建一个reclaimPolicy: Delete且archiveOnDelete: false的StorageClass。这种按业务区分数据保护策略的方式比全局一刀切灵活得多。6.2 多存储类与多租户规划当集群里跑着多个团队、多种存储需求时不要只创建一个StorageClass。NFS动态供给的优势就是可以按命名空间、按团队灵活规划目录命名规则用不同的StorageClass来区分约束。我通常会按业务类型创建多个存储类StorageClassProvisioner目标场景回收策略目录规则nfs-clientnfs-storage-provisioner普通开发测试Deletearchive默认命名nfs-team-anfs-storage-provisioner团队A生产Deletearchive${.PVC.namespace}/${.PVC.name}nfs-project-lognfs-storage-provisioner日志类非持久Delete不归档默认命名这里的Provisioner可以复用同一个但StorageClass不同目录规则和归档策略也就不同。供给器是共享的StorageClass是策略入口这个设计让存储的使用方式可以从“运维手工维护”变成“业务自助申请”。多租户场景下还要注意权限隔离。Kubernetes的StorageClass本身是集群级资源所有命名空间都能看到但PV的创建者和归属信息会被记录。如果业务需要严格隔离存储数据建议为不同命名空间配置不同的NFS共享根目录避免所有命名空间共享一个根目录导致数据混在一起。你可以部署多个供给器Deployment实例让它们分别指向不同NFS路径也可以在一个供给器上通过pathPattern做目录隔离后再交给权限系统去做访问控制。6.3 监控与容量治理NFS动态供给部署完成不代表存储问题消失只是把“创建卷”自动化了。容量管理这块我踩过不少坑最大的教训是不要只看kubectl get pv里声明的容量NFS服务端磁盘是全局共享的任何一个应用写满都会影响所有应用。我会在NFS服务器上做好两件基础事。第一是磁盘和inode监控用df -h和df -i定期采集配合Prometheus的node exporter就能把NFS服务端的磁盘使用率纳入监控告警。第二是目录增长分析按照共享根目录下各个业务目录的大小变化建立报表这样能及时识别出那些“声明2Gi但实际写了20Gi”的异常存储。NFS动态供给的目录命名规律清晰目录大小统计天然方便。另外要关注PVC和PV的长期残留。公司内部经常出现一种情况业务迭代后不再使用存储但PVC没有删除PV也就一直保留NFS目录一直增长。可以写一个定时任务周期扫描kubectl get pvc --all-namespaces与NFS服务器上的目录做对比把“有目录但无PVC”的归档数据清理掉。这个动作属于运维治理不是Kubernetes本身能自动完成的。6.4 从subdir迁移到CSI驱动要考虑的事情如果团队早晚要上NFS CSI Driver先不要急着迁移需要评估清楚差异。subdir外部供给器创建的PV底层NFS目录命名有自己的逻辑迁移到CSI后pv.spec.nfs字段会变成pv.spec.csi.driver: nfs.csi.k8s.io但NFS服务器的IP和路径本身仍然可以是原来subdir创建的目录。也就是说迁移时可以把旧PV的数据卷保留由CSI接管操作上存在可行性但需要通过静态PV方式重新绑定而不是直接让CSI动态供给接管旧目录。CSI迁移的收益在于标准化CSI接口统一管理挂载、卸载、快照、扩容快照能力是subdir方案没有的。如果业务对存储快照有硬性需求那迁移到NFS CSI就是必然选择。但如果只是要一个跑得稳、不折腾的NFS动态存储subdir方案已经够用不必为了“新”而迁移。我在生产环境给团队的建议是先在一个非核心业务命名空间里长时间跑subdir方案跑够一两个月把回收、归档、扩容、故障处理这套流程摸清楚再决定要不要向CSI演进。存储是整个业务的数据底座稳定压倒一切。回头说一个个人习惯我会把供给器的Deployment、RBAC、StorageClass全部用Git管理起来/etc/exports也会纳入配置管理不手工在服务器上临时改。NFS动态供给最怕的不是一次操作失误而是长期手工维护后集群里的StorageClass和NFS服务器的实际目录对应关系变得完全不可追溯。把规则固化成代码后续排障和交接都会轻松很多。
阅读完成 · 觉得有帮助?
咨询建站