ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kubernetes存储自动化:NFS Provisioner动态供给实战指南

Kubernetes存储自动化:NFS Provisioner动态供给实战指南

1. 为什么不直接手动建PV,非要折腾一个动态供给

先讲一个我实际遇到的场景。某天业务团队提了个需求:要部署一套带状态的应用,需要几个持久化存储卷。当时Kubernetes集群里已经有一块NFS存储,网段通了、挂载路径也确认好了,但问题在于:每次有新应用要存储,都得有人手动去写PV和PVC的YAML,改路径、改容量、改权限,还要等PV状态变成Available才能绑定。偶尔某个PV的容量写小了,还得删掉重建,一套流程下来少说二十分钟,慢了能拖半天。

这种模式并不是不能跑,但一旦集群里应用数量上来,手动PV的维护成本会快速膨胀。你永远不知道哪个团队哪天会上线一个新模块,也不会有人提前告诉你它要几G空间。等他们找过来的时候,你手上根本没有现成的PV可用,只能现场去创建。时间一长,运维这边就变成了“专职创建PV”的岗位,毫无技术含量,还容易出错。

NFS Provisioner要解决的就是这个问题:把“手动创建PV”变成“自动创建PV”。开发人员只需要提交一个PVC,Provisioner会监听这个请求,自动在NFS服务端创建对应的目录,然后生成PV并和PVC绑定。整个过程从提交YAML到Pod起来,通常几十秒就能完成。存储卷的容量、路径、访问模式全部由预定义的StorageClass统一管理,不需要人工干预。

适合什么样的人来参考?如果你满足下面任意一条,这篇内容应该能帮上忙:

  • 集群里的应用开始出现有状态需求,但还没有一套自动供给存储的方案;
  • 已经在用NFS或者准备用NFS做共享存储,想把它接入Kubernetes的动态供给体系;
  • 对StorageClass、Proivisioner这些概念停留在“听说过”层面,想亲手跑通一遍完整链路。

我不打算把这篇文章写成单纯的命令堆叠,会把每一步背后的考虑也讲清楚,包括为什么选这个镜像、为什么这样配置参数、踩过哪些坑、遇到异常怎么排查。这样你照着做完,即使环境略有差异,也知道该往哪个方向调整。

2. 动手之前先把概念理清:StorageClass、Provisioner和PV的三角关系

在敲任何一条命令之前,有必要先把三个概念之间的关系弄明白。很多人在部署过程中卡住,并不是命令写错,而是不清楚这几个组件各自负责什么,导致排查问题时找错了方向。

2.1 三者的分工

StorageClass可以理解为“存储模板”。它里面定义了存储供应方的信息,比如谁来做动态供给、卷的类型是什么、回收策略是什么。PVC在提交时如果指定了storageClassName,调度器就知道该去找哪个Provisioner来处理。

Provisioner是真正干活的组件。它是一段跑在集群里的程序(通常是一个Pod),监听PVC对象的创建和删除事件。收到Create事件后,它会在后端存储上创建实际目录,然后生成一个PV对象指向这个目录。收到Delete事件后,它负责清理对应目录,回收资源。

PV则是最终呈现给Pod的存储资源。Pod不直接声明存储需求,而是通过PVC声明,PVC再绑定到PV,Pod通过PVC挂载这个PV。这层间接设计的好处是,Pod的编写者不需要关心存储的IP、路径、权限这些细节,只要写清楚“我要多少空间、用什么访问模式”就行。

用生活里的例子来套:StorageClass是菜单,Provisioner是后厨,PV是做好端上来的菜。你(PVC)照着菜单点了一份“香辣土豆丝”,后厨收到订单后去仓库取土豆、洗切炒,最后把一盘菜端到你面前。没有后厨的时候,你得自己进仓库找土豆、自己动手炒,这就是手动创建PV和自动创建PV的区别。

2.2 PVC绑定PV的几个阶段

一个PVC从创建到Pod使用,会经历这样几个状态变化:

  1. Pending:PVC刚创建,正在等待Provisioner处理。
  2. Provisioning:Provisioner正在后端创建目录、生成PV。
  3. Bound:PV创建成功,开始与PVC绑定。
  4. Pod挂载使用:Pod调度到节点后,kubelet把NFS目录挂载进容器。

如果你提交PVC后它长时间停在Pending,大概率就是Provisioner没有正常工作,或者StorageClass里的provisioner字段和实际部署的Provisioner名字对不上。这个“对不上”是非常经典的坑,后面我会单独展开。

2.3 为什么选择NFS作为Provisioner的存储后端

在Kubernetes的生态里,动态供给的后端选择很多,云厂商有云硬盘、OSS等,私有环境有Ceph RBD、GlusterFS等。但NFS依然是不少团队的第一选择,原因是它足够简单。

NFS服务端只需要一台Linux主机,装好nfs-utils,配置好exports文件,就可以提供服务。客户端方面,几乎所有Linux发行版都内置了NFS客户端支持,不需要额外装复杂依赖。相比之下,Ceph RBD需要部署Monitor、OSD、MGR等多个组件,对网络和硬件都有要求,入门门槛明显高一个量级。

NFS唯一的短板是性能,尤其是小文件随机写的场景下,吞吐和延迟比不上本地盘或者分布式块存储。但如果业务是常规的Web应用日志、文件上传下载、测试环境数据,NFS完全够用。而且Kubernetes的NFS Provisioner走的是标准的NFS协议,不依赖任何云厂商,多云和本地环境都能跑,这让它成了“最省心”的起步方案。

3. 服务端准备:NFS共享目录的规划和配置

NFS Provisioner本身只是个“中介”,真正的数据最终落在NFS服务端。所以最先要动手的,是准备一台可用的NFS服务器。

3.1 安装并启动NFS服务

我习惯用CentOS或者Ubuntu Server作为NFS服务端,具体步骤如下(以CentOS系为例):

# 安装NFS服务端 yum install -y nfs-utils # 创建共享根目录 mkdir -p /data/nfs-provisioner # 配置exports文件 cat >> /etc/exports <<EOF /data/nfs-provisioner *(rw,sync,no_root_squash,insecure,no_subtree_check) EOF # 启动服务并设置开机自启 systemctl enable --now nfs-server systemctl status nfs-server

这里有几个参数需要特别说明:

  • rw:允许读写。
  • no_root_squash:客户端root用户访问时不映射为匿名用户。这个参数对Provisioner很重要,因为Provisioner默认用root身份去创建目录,如果不加这个参数,目录权限可能不对,后面Pod里非root用户写文件时会遇到Permission denied。
  • insecure:允许客户端使用大于1024的端口连接。Kubernetes节点访问NFS时,源端口往往是随机的高位端口,不加这个参数容易连不上。
  • no_subtree_check:关闭子树检查,减少不必要的RPC请求,提升一点性能。

配置完成后,用exportfs -v确认共享是否正常。

3.2 验证NFS服务端可用性

在NFS服务端本机可以先测试一下:

showmount -e localhost

如果输出里正确列出了/data/nfs-provisioner的共享路径,说明服务端配置没有问题。

然后任选一个Kubernetes节点,安装nfs-utils客户端并尝试挂载:

yum install -y nfs-utils mkdir -p /mnt/nfs-test mount -t nfs <NFS_SERVER_IP>:/data/nfs-provisioner /mnt/nfs-test touch /mnt/nfs-test/hello.txt umount /mnt/nfs-test

这一步的意义在于提前排除网络层问题。尤其是云环境里,安全组规则、防火墙策略经常默认拦截NFS常用的2049端口,等到Provisioner跑起来才发现挂载失败,排查链路会很长。提前用命令行测通,后面遇到问题就知道不是网络层的锅。

3.3 一个容易被忽略的点:路径规划和目录命名

Provisioner默认会在你指定的根目录下,为每一个PV创建独立的子目录,命名格式类似{pvc-namespace}-{pvc-name}-{pv-name}。所以在规划NFS根目录时,建议给Provisioner单独开一个目录,不要和其他手动挂载的数据混在一起。

比如我当前的NFS服务端上,/data下既有手动挂载的业务目录,也有Provisioner用的根目录。如果把两者混在一个目录下,Provisioner创建出来的子目录会和业务目录混在一起,后续清理或排查时很难区分。单独开一个/data/nfs-provisioner根目录,里面全是自动生成的卷目录,谁是谁的一目了然。

另外注意权限:Provisioner的Pod是以root身份运行的,所以它创建的目录默认Owner是root。如果业务容器是以非root用户运行,就可能出现目录能挂载但无法写文件的情况。解决方法有两个:一是让业务镜像自己处理好目录权限,二是给StorageClass里配一个合适的mountOptions,比如加入nfsvers=4.0和sec=sys,然后在NFS服务端设置anonuid和anongid,将目录归属落到指定UID上。这个后面排错章节再细说。

4. 部署NFS Provisioner:RBAC、Deployment和StorageClass的完整配置

服务端准备好后,就可以在Kubernetes集群内部署Provisioner了。整个部署分三块:授权相关对象(ServiceAccount、ClusterRole、ClusterRoleBinding)、Provisioner本身的Deployment、以及对外提供动态供给入口的StorageClass。

4.1 创建ServiceAccount和RBAC权限

Provisioner需要监听集群里的PVC、PV事件,还要能创建PV对象,所以必须有对应的RBAC权限。下面是完整的YAML:

apiVersion: v1 kind: ServiceAccount metadata: name: nfs-provisioner namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: nfs-provisioner 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"] - apiGroups: [""] resources: ["endpoints"] verbs: ["get", "list", "watch", "create", "update", "patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: nfs-provisioner subjects: - kind: ServiceAccount name: nfs-provisioner namespace: kube-system roleRef: kind: ClusterRole name: nfs-provisioner apiGroup: rbac.authorization.k8s.io

几个动词权限的解释:

  • persistentvolumes的create是必须的,Provisioner的核心功能就是动态创建PV。
  • persistentvolumeclaims的watch和update用于监听PVC状态变更,以及把PVC的Status更新为Bound。
  • events的create和update用于向集群写入事件,方便你kubectl describe pvc时看到Provisioner的运作日志。
  • endpoints的权限用于支持HA模式下的Leader选举,单副本时也建议保留,避免以后扩展时还要再改RBAC。

这里强调一点:如果你之前部署过其他Provisioner,注意ClusterRole名字不要和现有的冲突。为了避免互相干扰,我在每个Provisioner相关对象的name上都加了nfs-provisioner前缀,这样即使是同一个集群里跑了多套供给方案,RBAC也不会打架。

4.2 部署Provisioner主体Deployment

Deployment的YAML如下:

apiVersion: apps/v1 kind: Deployment metadata: name: nfs-provisioner namespace: kube-system spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: nfs-provisioner template: metadata: labels: app: nfs-provisioner spec: serviceAccountName: nfs-provisioner containers: - name: nfs-provisioner image: quay.io/external-storage/nfs-client-provisioner:v4.0.2 imagePullPolicy: IfNotPresent env: - name: PROVISIONER_NAME value: nfs-provisioner - name: NFS_SERVER value: <你的NFS服务端IP> - name: NFS_PATH value: /data/nfs-provisioner volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes volumes: - name: nfs-client-root nfs: server: <你的NFS服务端IP> path: /data/nfs-provisioner

镜像我选的是quay.io/external-storage/nfs-client-provisioner:v4.0.2。这个镜像属于开源社区维护的external-storage系列,虽然这个项目后来进入了维护状态,但正因为稳定、没有大的功能变动,反而更适合生产环境长期使用。很多发行版和二次开发版本都基于这一套逻辑,网上资料也多,遇到问题容易搜到答案。

strategy: Recreate要注意一下。Provisioner是有状态的,它依赖本地与NFS的挂载关系,滚动更新时新Pod和旧Pod同时存在容易产生重复处理PVC事件的问题,所以这里配置成Recreate,先停旧再起新,保证同一时间只有一个Provisioner在工作。

环境变量里的PROVISIONER_NAME是一个关键值。Deployment中写的是nfs-provisioner,这个值必须和StorageClass里provisioner字段完全一致,否则PVC提交后没有Provisioner认领,会一直Pending。下面配置StorageClass时会再强调一次。

4.3 创建StorageClass

StorageClass的YAML如下:

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: nfs-provisioner reclaimPolicy: Delete volumeBindingMode: Immediate

reclaimPolicy我建议先选Delete。这个策略的含义是:当PVC被删除时,对应的PV也会被删除,同时Provisioner会把NFS上的目录一并清理。对测试环境来说,Delete策略非常省心,不会残留一堆无用的目录。等确认整个链路稳定了,再考虑是否给某些重要数据切换到Retain策略。

volumeBindingMode用Immediate,表示PVC创建后立即开始绑定和供给。另一种是WaitForFirstConsumer,等Pod调度到节点后才执行供给。NFS这种共享存储不关心节点位置,用Immediate就够,绑定速度快,行为也直观。

到这里,三角关系就完整了。我梳理一下整个链条:

  1. 开发者创建PVC,指定storageClassName: nfs-storage。
  2. kube-apiserver收到PVC后,发现name是nfs-storage的StorageClass对应的provisioner是nfs-provisioner,开始找这个Provisioner。
  3. Deployment里的容器启动了,通过RBAC权限watches到PVC,开始创建NFS子目录,并生成PV。
  4. PV创建成功后,PVC状态变为Bound,Pod正常挂载。

如果你用kubectl get pvc去看,正常情况下PVC的STATUS列会从Pending变成Bound,整个过程通常几秒内完成。如果卡住不动,就要进入排查环节了。

5. 验证动态供给链路:从PVC到Pod挂载的全流程实测

部署完不代表结束,必须做一次完整的验证。这一步能确认你所有的配置都是真实有效的,而不是在“看起来没问题”的状态下自我欺骗。

5.1 创建一个测试PVC

先准备一个最小的PVC:

apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc spec: accessModes: - ReadWriteMany storageClassName: nfs-storage resources: requests: storage: 1Gi

这里accessModes我用了ReadWriteMany,因为NFS天然支持多节点同时读写,这也是NFS相比块存储的一个重要优势。如果你的业务只需要单节点读写,用ReadWriteOnce也完全可以,但既然用了NFS,验证一下多写特性会更完整。

创建后查看状态:

kubectl apply -f test-pvc.yaml kubectl get pvc test-pvc

正常情况会看到STATUS为Bound。如果还处于Pending状态,执行kubectl describe pvc test-pvc查看Events部分:

kubectl describe pvc test-pvc

Events里通常会写明Provisioning失败的具体原因。常见的有:

  • no such file or directory:NFS路径写错或不存在。
  • access denied:NFS权限配置问题,通常是no_root_squash没加上。
  • failed to provision volume with StorageClass: unexpected error getting claim reference:RBAC权限缺失,检查ClusterRole里persistentvolumeclaims的verbs是否包含get和list。

5.2 查看自动生成的PV

PVC变成Bound后,检查一下关联的PV:

kubectl get pv

你会发现多了一个名字类似pvc-xxxx的PV,容量是1Gi,StorageClass是nfs-storage,Reclaim Policy是Delete,Access Modes是RWX。再用describe看详情:

kubectl describe pv pvc-xxxx

Source部分会显示NFS的server路径和自动生成的子目录,比如/data/nfs-provisioner/default-test-pvc-pvc-xxxx。回到NFS服务端检查这个目录是否真实存在,确认客户端和服务端的数据链路是通的。

5.3 用Deployment实测挂载写入

PV和PVC绑定成功后,真正检验成果的是Pod能否正常挂载并写入数据。创建一个简单的测试Pod:

apiVersion: v1 kind: Pod metadata: name: nfs-test-pod spec: containers: - name: nginx image: nginx:1.25 volumeMounts: - name: data mountPath: /usr/share/nginx/html volumes: - name: data persistentVolumeClaim: claimName: test-pvc

创建后等待Pod Running:

kubectl apply -f test-pod.yaml kubectl exec -it nfs-test-pod -- bash echo "hello nfs provisioner" > /usr/share/nginx/html/index.html

回到NFS服务端,在对应的子目录里应该能看到index.html文件。再把Pod删掉重新创建一个,文件依然还在,说明数据确实落到了NFS上,而不是Pod的临时文件系统里。

5.4 验证动态供给的“动态”特性

最后做个破坏性测试:删掉PVC,看看整个回收链路是否正常。

kubectl delete pod nfs-test-pod kubectl delete pvc test-pvc kubectl get pv

由于reclaimPolicy是Delete,PVC删除后,对应的PV应该自动消失,NFS服务端的对应子目录也应该被清空。这一条如果验证通过,就说明Provisioner的回收逻辑也是正常的,以后业务下线,运维不用手工清理NFS目录。

6. 实战中的高频排错:五个我反复踩过的坑和对应解法

纯粹照YAML部署一次通过的好运气,我给不了你。更现实的情况是,第一次部署时大概率会遇到下面这些坑里的某一个。我把它们单独拎出来讲,每一个都是我或者身边同事实际踩过的。

6.1 StorageClass的provisioner名字和Deployment的PROVISIONER_NAME不一致

这个是最容易犯的低级错误。StorageClass里的provisioner字段写的是某个名字,Deployment环境变量里PROVISIONER_NAME写的是另一个名字,两者对不上,PVC提交后永远处于Pending状态。

排查方法很简单:

kubectl get storageclass nfs-storage -o yaml kubectl get deploy nfs-provisioner -n kube-system -o yaml | grep PROVISIONER_NAME

对比两个值是否一致。按照我上面的YAML,两者统一用的是nfs-provisioner,不需要再额外加前缀。但如果你在同一个集群里部署了多套Provisioner,比如一个给业务A用,一个给业务B用,请务必取不同的名字,否则会互相抢单。

6.2 NFS挂载失败,Pod一直ContainerCreating

Pod创建后一直ContainerCreating,describe时看到MountVolume.MountDevice失败的报错,通常原因是Kubernetes节点上没有安装nfs-utils客户端,或者安全组/防火墙拦截了NFS端口。

解决办法分两步:

# 在全部Kubernetes节点上安装nfs客户端 yum install -y nfs-utils

然后从节点上手动挂载测试:

mkdir -p /mnt/nfs-check mount -t nfs <NFS_SERVER_IP>:/data/nfs-provisioner /mnt/nfs-check

如果能挂上,说明节点侧没问题;如果卡住,先用showmount -e <NFS_SERVER_IP>验证连通性。showmount都失败的话,重点检查安全组和防火墙。

另外,有些发行版默认没有开启rpcbind服务,NFS依赖它做端口映射。在服务端执行systemctl status rpcbind确认它处于运行状态。

6.3 非root用户的Pod无法写入文件

Provisioner创建目录时默认Owner是root,如果业务容器以非root用户运行,写入文件时会报Permission denied。这个坑在测试环境不明显(很多测试镜像直接用root),一上生产就容易翻车。

解法有两个角度:

一是在NFS服务端的exports配置里加入anonuid和anongid,配合root_squash,把请求的root身份映射成指定UID。例如:

/data/nfs-provisioner *(rw,sync,no_root_squash,insecure,no_subtree_check,anonuid=1000,anongid=1000)

但这要求Provisioner创建的所有目录都归属UID 1000,而如果业务Pod用的UID不是1000,仍然会碰上权限问题。

更通用一点的做法,是让Provisioner通过mountOptions传入nfsvers=4.0和sec=sys,并在StorageClass层面做一些约束。不过说实话,NFS的权限粒度本身有限,最稳妥的方案还是让业务镜像在启动时用initContainer把数据目录chown成容器内运行用户的UID,把权限控制交给镜像自身。这也是生产环境里更常见、更容易维护的做法。

6.4 reclaimPolicy=Delete误删数据

Delete策略很省心,但也有一个需要注意的边界:如果PVC被误删,数据会跟着一起被清空,没有任何找回手段。我见过有同事在清理测试环境时误删了一个包含了重要数据的PVC,结果NFS目录连根清掉,半天时间都在恢复数据。

所以我的建议是,StorageClass默认可以先给业务用Delete,方便日常清理,但对于确认要保留的数据,要么把PVC的元数据保护好,要么单独创建一个reclaimPolicy=Retain的StorageClass用于特殊场景。Retain策略下,PVC删除后PV还在,只是状态变为Released,你需要手动去NFS服务端处理目录,然后删除PV对象。麻烦一些,但数据安全有保障。

6.5 Provisioner Pod反复重启

Deployment里的容器反复CrashLoopBackOff,查看日志最常见的原因是NFS挂载不上,导致容器启动时无法访问/persistentvolumes路径。用kubectl logs看具体的error信息,如果是mount: permission denied或者transport endpoint is not connected,回到6.2的排查链路处理。

还有一种可能是镜像拉取失败,尤其是在国内网络环境下,默认镜像源可能不稳定。解决办法是提前把镜像pull到本地,或者配置好镜像加速器,改成你本地私有仓库的地址。集群节点多的时候,可以用DaemonSet预先在所有节点上拉一遍镜像,避免后续扩容时新节点拉取失败。

6.6 动态供给与StatefulSet的配合

NFS动态供给和StatefulSet搭配使用时,有一个细节容易踩坑:StatefulSet的volumeClaimTemplates会为每个副本自动创建PVC,PVC名字带有固定的序号后缀,比如data-mysql-0、data-mysql-1。Provisioner按PVC名字创建目录,所以每个副本会得到独立的NFS子目录,互不干扰,这一点通常没有问题。

真正需要注意的,反而是StatefulSet删除后PVC默认不会自动删除,需要手动清理。此时如果reclaimPolicy是Delete,删掉PVC就会连带删除NFS目录,如果你的stateful应用挂载了持久化数据,一定要先确认数据已备份,再执行删除操作。

7. 进阶优化:给NFS Provisioner换上Retain策略、配额限制和监控视图

基础链路跑通之后,整个集群的动态供给就算初步建立起来了。但要用得更顺手,还有几个进阶点值得去做。

7.1 为不同业务创建多个StorageClass

默认情况下,一个NFS根目录会被所有PVC共用,这会导致不同业务的存储卷挤在一起,目录结构混乱,也没有容量上的隔离。在规模上来之前,建议直接规划多套StorageClass,每套指向NFS服务端的不同子目录。

比如:

  • nfs-storage-default:常规业务,reclaimPolicy=Delete。
  • nfs-storage-retain:重要数据,reclaimPolicy=Retain。
  • nfs-storage-backup:备份专用目录。

指的是,在NFS服务端分别创建对应的根目录,在Deployment的环境变量里为每一套Provisioner指定不同的NFS_PATH。每个StorageClass对应一个独立的Provisioner Deployment,目录、回收策略互不影响。

这样做的好处是,业务团队在提交PVC时通过storageClassName就能明确表达自己的存储需求,运维侧管理起来也更清晰:哪块数据能清理,哪块数据不能动,一目了然。

7.2 容量配额和配额限制

NFS自身不提供原生的Quota机制,Provisioner在创建PV时申请的容量,并不会真正限制NFS目录的最大使用量。也就是说,PVC里写10Gi,实际能写满100Gi,只要磁盘够大。这在某些场景下会引起资源失控。

要解决这个问题,通常要依赖底层文件系统支持。比如NFS服务端使用XFS或ext4,并开启project quota,然后为Provisioner创建的不同子目录设置单独的配额。命令大致如下:

# 在NFS服务端为目录设置project quota mkdir -p /data/nfs-provisioner/default-test-pvc-xxxx xfs_quota -x -c 'project -s 1001 /data/nfs-provisioner/default-test-pvc-xxxx' /data xfs_quota -x -c 'limit -p bhard=10g 1001' /data

但这需要你对底层文件系统有较深掌握,也增加了运维复杂度。多数团队在NFS动态供给的初期并不会做这层限制,而是通过监控告警兜底。我的建议是:如果业务量不大,可以暂时不搞配额,但一定要上监控;如果业务量增长明显,再考虑在服务端做硬限制。

7.3 给Provisioner加上可观测性

Provisioner本身不带监控面板,但你可以通过外部方式收集它的状态。

  • 事件观察:Provisioner会往PVC/PV上写Events,用kubectl get events --sort-by=.metadata.creationTimestamp能看到Provisioner的工作记录。
  • 日志采集:Provisioner的stdout日志会记录每一次Provision/Delete操作,部署一套日志采集组件抓进中心日志系统,异常时可以快速回溯。
  • 指标暴露:如果你用的是新版且有metrics接口的Proivisioner镜像,可以直接接入Prometheus。老版本的nfs-client-provisioner不带metrics,建议在基础设施层加一层NFS服务端的指标采集,例如node_exporter里的nfs-client相关指标。

当你同时管理多个Provisioner和多个StorageClass时,可观测性会直接决定你排查问题的时间。没有监控的情况下,一切都要靠日志和Events一层层翻,效率很低。

7.4 升级路径和备份策略

Provisioner的版本升级,我建议遵循这样的顺序:

  1. 先在一个测试集群里部署新版本镜像,跑一遍PVC创建、写入、删除的完整链路。
  2. 确认无误后,生产环境用RollingUpdate方式逐个更新。
  3. 因为Provisioner是Recreate策略,更新时会有短暂的供给中断,此时如果正好有新的PVC提交,会排队等待,等Provisioner起来后自动处理,不会丢失请求。

我额外提醒一点:Provisioner本身不存储业务数据,升级它不会影响已经创建好的PV和PVC,底层NFS数据也不受影响。所以升级动作不需要停业务,但尽量选在低峰期操作,减小PVC创建请求的等待时间。

8. 整体链路复盘:一次实际排错过程的全记录

光讲理论不够,我穿插一段真实的排错记录,还原一下我最近一次部署NFS Provisioner时遇到的问题。环境是某公司内部的Kubernetes集群(多节点、双栈网络),NFS服务端是一台独立的CentOS虚拟机,存储目录直接挂在本地盘上。

按照前面的步骤部署完成后,创建测试PVC,结果PVC一直Pending。describe输出如下:

Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning Provisioning 12s nfs-provisioner failed to provision volume with StorageClass "nfs-storage": unexpected error getting claim reference: selfLink was empty, can make reference

看到selfLink was empty这个关键词,我立刻意识到问题出在Kubernetes API版本上。新版Kubernetes(1.20+)移除了selfLink字段,而v4.0.2这个镜像比较老,在获取PVC引用时依赖selfLink,所以报错。

这个问题的解法和版本配套有关。大致思路:

  • 如果你的集群是1.20及以上,需要为kube-controller-manager加一个启动参数--feature-gates=RemoveSelfLink=false。但这个方法在1.24之后已经不建议使用,因为API Server对selfLink的支持已经彻底移除。
  • 更好的做法是换用支持新版本API的镜像,或者直接打补丁。我后来改用了社区维护的新版兼容镜像,问题随即消失。

这个案例说明了一个问题:Kubernetes的版本迭代很快,老镜像和新API偶尔会不兼容。部署前先确认你的镜像和集群版本匹配度,是一个省时省力的习惯。

排查链路梳理下来,其实就三步:

  1. 看PVC的Events,找到Provisioner写回来的原始报错。
  2. 根据报错关键词,定位是配置问题、权限问题还是版本兼容问题。
  3. 解决后重新创建一个PVC验证,而不是反复去改老PVC。

这套思路适用于绝大多数Provisioner异常。先看事件、再看日志、最后翻版本,不要一上来就重启Pod或者重建集群组件。

9. 一些收尾的实操建议和我的习惯做法

整篇写到这里,核心流程已经完整跑通了。最后分享几个我从多次部署中沉淀下来的习惯做法,不一定适合所有人,但多少能帮你少走弯路。

第一,所有Provisioner相关的YAML对象,我都用一个独立的目录维护,并且按照resources/rbac.yaml、resources/deployment.yaml、resources/storageclass.yaml拆分存放。这样每次变更都有版本记录,出问题可以快速回滚到上一个版本。

第二,Provisioner部署完成后,我会顺手留下一套“冒烟测试”用的YAML文件,包括一个测试PVC、一个测试Pod。每次改过StorageClass或者Provisioner配置,都先跑一遍冒烟测试,确认链路没有断。这个动作几十秒钟就完成,但能避免很多“改完不起作用”的模糊状态。

第三,NFS服务端的目录权限,我习惯统一落在UID 1000或者某个专用账号上,业务容器内的用户尽量向这个UID靠拢。团队内部约定一个通用UID,能极大减少挂载后写权限的纠纷。

第四,关注Provisioner日志的滚动大小。Provisioner本身日志量不大,但运行时间久了也会积累,尤其是集群里PVC变更频繁时。在Kubernetes层面配置好日志轮转,或者对接日志采集系统,避免日志占满磁盘这种低端事故。

说到底,NFS Provisioner只是把“存储供给”这个动作自动化了,它不改变Kubernetes存储的底层生态,也解决不了所有存储问题。它的价值在于:当你已经决定用NFS做共享存储时,用最小的成本把动态供给跑起来,让开发人员从“等着别人建PV”变成“自己提交PVC就完事”。从运维体验上来说,这个转变是质的飞跃。后续如果存储需求继续膨胀,再考虑换成Ceph或者其他分布式存储也不迟,因为PVC/PV这套抽象层已经建立好了,上层业务并不需要知道底层存储后端是什么。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进