1. 为什么你需要一个动态的NFS存储方案?

如果你在玩Kubernetes,肯定遇到过要给Pod挂载存储的需求。比如,你的Web应用需要保存上传的图片,你的数据库需要持久化数据,或者你的日志分析工具需要读取历史日志文件。最开始,你可能直接用hostPath,把宿主机目录挂进去,简单粗暴。但很快你就会发现,这玩意儿只能在单个节点上用,Pod一旦漂移到其他节点,数据就“丢”了,根本谈不上高可用。

于是你转向了静态的PersistentVolume(PV)。手动在NFS服务器上创建好目录,然后在K8S里定义一个PV,再写个PVC去绑定它。这确实解决了跨节点的问题,但新的麻烦来了:每次开发新应用、新环境,你都得重复这套操作——登录NFS服务器、mkdir、写YAML、创建PV。应用少还好,一旦微服务多了,环境复杂了(开发、测试、预发、生产),这套手动流程就成了运维的噩梦,效率低下还容易出错。

这时候,动态存储供给的价值就凸显出来了。它的核心思想是“按需分配,自动创建”。你不需要再预先准备一堆PV等着用。你只需要定义一个StorageClass(存储类),它就像是一个存储模板,里面描述了“我要用哪种后端存储(比如NFS)”、“存储服务器地址是什么”、“创建目录的规则是怎样的”。然后,当你的应用通过PersistentVolumeClaim(PVC)申请存储时,K8S会拿着这个PVC去找匹配的StorageClass。StorageClass背后的CSI驱动(比如我们今天的主角csi-driver-nfs)就会自动行动起来,联系NFS服务器,按照预设的规则创建出实际的共享目录,并动态地生成一个PV来绑定这个PVC。

整个过程完全自动化,对开发者透明。开发者只需要在YAML里写一句storageClassName: nfs-csi,并声明需要5Gi空间,剩下的创建、挂载、绑定,系统全包了。这极大地提升了效率,也符合云原生“声明式API”和“自动化运维”的理念。而csi-driver-nfs,就是实现NFS动态供给的官方标准CSI驱动,它稳定、功能丰富,是目前社区最主流的选择。

2. 实战前的环境与架构梳理

在动手之前,我们先花几分钟把整个架构和环境理清楚,这样操作起来心里才有底。我假设你有一个已经搭建好的Kubernetes集群,至少包含一个Master节点和两个Worker节点。同时,你需要有一台独立的服务器作为NFS Server。当然,为了测试方便,你也可以把NFS Server部署在Master节点上,但生产环境我强烈建议分离。

为了让你看得更明白,我把这次实战涉及到的机器和角色列个表:

主机名IP地址角色与任务
nfs-server192.168.1.5NFS存储服务器。负责提供共享目录 /data/nfs/k8s
k8s-master01192.168.1.1Kubernetes Master节点。我们将在这里执行所有kubectl命令,部署CSI驱动。
k8s-node01192.168.1.2Kubernetes Worker节点。运行业务Pod,需要挂载NFS存储。
k8s-node02192.168.1.3Kubernetes Worker节点。同样运行业务Pod,实现跨节点存储访问。

核心工作流程是这样的:

  1. nfs-server上搭建好NFS服务,并暴露一个共享目录。
  2. 在K8S集群的所有节点(Master和Node)上安装NFS客户端工具,确保它们能挂载NFS。
  3. k8s-master01上部署csi-driver-nfs。这会创建两部分Pod:
    • Controller Pod:运行在Master节点(通过Deployment部署),负责监听PVC的创建/删除事件,并调用CSI接口去NFS服务器上实际创建或删除子目录。
    • Node Pod:运行在每个Worker节点(通过DaemonSet部署),负责在Pod调度到本节点时,执行实际的mount操作,将NFS目录挂载到Pod里。
  4. 创建一个StorageClass,指向我们的NFS服务器和共享目录。
  5. 最后,我们用一个Nginx应用来验证:创建一个引用该StorageClass的PVC和Deployment,观察PVC是否自动绑定、PV是否自动创建、NFS目录下是否自动生成子目录,以及Pod能否正常读写。

听起来步骤不少,但别担心,跟着我一步步来,其实都是很直白的操作。我们先把最基础的NFS服务搭起来。

3. 第一步:搭建你的NFS存储服务器

NFS服务器的配置其实很简单,但有几个细节不注意,后面在K8S里挂载可能会踩坑。我们就在nfs-server(192.168.1.5)这台机器上操作。

首先,安装必要的软件包。在RHEL/CentOS/Rocky Linux等系统上,使用yumdnf

yum install -y nfs-utils

在Ubuntu/Debian系统上,使用apt

apt update && apt install -y nfs-kernel-server

安装完成后,我们需要创建一个打算共享给K8S集群的目录。我习惯放在/data下,结构清晰一点:

mkdir -p /data/nfs/k8s

接下来是最关键的配置——编辑NFS的导出配置文件/etc/exports。这个文件决定了哪个目录共享给谁、以什么权限共享。

vim /etc/exports

在里面添加如下一行:

/data/nfs/k8s *(rw,sync,no_subtree_check,no_root_squash)

我来解释一下这几个参数,理解了它们能避免很多权限问题:

  • /data/nfs/k8s:这是你要共享出去的本地目录路径。
  • *:表示允许所有IP地址的客户端访问。在生产环境中,为了安全,你应该替换成你的K8S集群Pod网络CIDR,比如192.168.0.0/16。这里为了演示方便,我们用了通配符。
  • rw:客户端拥有读写权限。
  • sync:同步写入。数据在写入到服务器内存后,会同时写入磁盘,保证数据一致性。相比async(异步)更安全,但性能稍差。对于K8S存储,我推荐用sync
  • no_subtree_check:禁用子树检查。这可以提升性能,尤其是在目录频繁重命名时。对于大多数情况,建议启用。
  • no_root_squash这个参数很重要。它表示当客户端的root用户访问NFS时,服务器端不会将其映射为匿名用户(如nobody),而是保留其root权限。在K8S中,很多容器(尤其是初始化容器或某些系统容器)可能会以root身份运行来挂载卷。如果这里设置成默认的root_squash,会导致权限不足,挂载失败。所以,在可信的内网环境中,我们通常设置为no_root_squash

保存退出后,需要让NFS服务重新加载配置。不同系统的服务名略有差异:

# RHEL/CentOS/Rocky 8+
systemctl restart nfs-server && systemctl enable nfs-server

# Ubuntu/Debian
systemctl restart nfs-kernel-server && systemctl enable nfs-kernel-server

为了确保防火墙不会挡住我们的路,需要开放NFS相关的端口。NFS服务用到多个端口,最简单的方式是直接放行NFS服务:

# 如果使用firewalld(RHEL系)
firewall-cmd --permanent --add-service=nfs
firewall-cmd --permanent --add-service=mountd
firewall-cmd --permanent --add-service=rpc-bind
firewall-cmd --reload

# 如果使用ufw(Ubuntu)
ufw allow from 192.168.1.0/24 to any port nfs
ufw reload

最后,我们可以在NFS服务器本机测试一下共享是否生效:

showmount -e localhost

你应该能看到输出/data/nfs/k8s *,表示共享配置成功了。

4. 第二步:为K8S集群所有节点安装NFS客户端

NFS是客户端-服务器架构。K8S的Worker节点需要挂载NFS共享目录到Pod里,因此每个节点都必须具备NFS客户端能力。这包括Master节点,因为CSI Controller Pod也可能运行在Master上(如果没设置污点的话)。

在所有K8S节点(k8s-master01, k8s-node01, k8s-node02)上执行:

# RHEL/CentOS/Rocky
yum install -y nfs-utils

# Ubuntu/Debian
apt update && apt install -y nfs-common

安装完成后,我们可以手动测试一下从任意一个节点(比如k8s-node01)挂载NFS共享,验证网络和权限是否通畅:

# 创建一个临时挂载点
mkdir -p /mnt/nfs-test

# 执行挂载
mount -t nfs 192.168.1.5:/data/nfs/k8s /mnt/nfs-test

# 检查是否挂载成功
df -h | grep nfs-test
# 应该能看到类似:192.168.1.5:/data/nfs/k8s  50G  5.0G   45G  10% /mnt/nfs-test

# 测试读写
touch /mnt/nfs-test/testfile.txt
ls -l /mnt/nfs-test/
# 应该能看到你创建的文件

# 测试完成后卸载
umount /mnt/nfs-test

如果这一步成功了,说明NFS服务器配置、网络、防火墙、客户端软件都没问题,我们可以放心地进行下一步。如果失败,请回头检查NFS服务器的/etc/exports配置、防火墙规则以及网络连通性。

5. 第三步:部署核心——csi-driver-nfs插件

现在来到重头戏,在Kubernetes集群里安装csi-driver-nfs驱动。我们将在Master节点(k8s-master01)上操作。官方提供了多种安装方式,这里我们用最直接的YAML文件部署,这样你能清楚地看到每一步创建了什么资源。

首先,我们需要获取驱动的部署文件。访问项目的GitHub Release页面(https://github.com/kubernetes-csi/csi-driver-nfs/releases),找到最新的稳定版本。写这篇文章时,最新版是v4.11.0。我们下载并解压:

wget https://github.com/kubernetes-csi/csi-driver-nfs/archive/refs/tags/v4.11.0.tar.gz
tar -zxf v4.11.0.tar.gz
cd csi-driver-nfs-4.11.0

进入deploy目录,你会看到很多YAML文件。这里有个非常重要的问题:默认的YAML文件里,容器镜像地址是registry.k8s.io,这对于国内环境来说,拉取速度可能极慢甚至超时。我们必须将其替换为国内的镜像仓库。我常用的是registry.aliyuncs.com/google_containers或者k8s.m.daocloud.io

我们可以用一条sed命令批量替换:

# 替换所有yaml文件中的镜像仓库地址
sed -i 's|registry.k8s.io/sig-storage/|registry.aliyuncs.com/google_containers/|g' deploy/*.yaml
sed -i 's|registry.k8s.io/csi-|registry.aliyuncs.com/google_containers/csi-|g' deploy/*.yaml

替换完成后,建议你检查一下关键的Deployment和DaemonSet文件,比如deploy/csi-nfs-controller.yamldeploy/csi-nfs-node.yaml,确认镜像地址已经变更。

接下来,开始部署。官方提供了一个方便的脚本install-driver.sh,但我们为了理解过程,可以手动按顺序应用几个主要的YAML文件。首先部署RBAC权限、ServiceAccount等基础资源:

kubectl apply -f deploy/rbac-csi-nfs-controller.yaml
kubectl apply -f deploy/rbac-csi-nfs-node.yaml
kubectl apply -f deploy/csi-driver-info.yaml

然后部署CSI驱动本身的核心组件:

# 部署Controller部分(Deployment)
kubectl apply -f deploy/csi-nfs-controller.yaml

# 部署Node部分(DaemonSet,会在每个节点运行一个Pod)
kubectl apply -f deploy/csi-nfs-node.yaml

应用完成后,检查Pod是否正常运行。所有相关Pod都会部署在kube-system命名空间下:

kubectl get pods -n kube-system -l app=csi-nfs-node
kubectl get pods -n kube-system -l app=csi-nfs-controller

你应该会看到类似下面的输出。注意csi-nfs-controller是一个多容器Pod(包含provisioner, attacher, resizer等),所以READY数可能是3/3或更多。csi-nfs-node则会在每个节点上运行一个Pod。

NAME                       READY   STATUS    RESTARTS   AGE
csi-nfs-controller-xxxxx   3/3     Running   0          2m
csi-nfs-node-aaaaa         3/3     Running   0          2m
csi-nfs-node-bbbbb         3/3     Running   0          2m

看到所有Pod都是Running状态,并且READY数和预期一致,就说明CSI驱动部署成功了。如果遇到镜像拉取失败,请确认之前的镜像地址替换是否正确。如果Pod一直处于ContainerCreatingPending,可以用kubectl describe pod <pod-name> -n kube-system查看具体事件来排查。

6. 第四步:定义存储类(StorageClass)——动态供给的蓝图

CSI驱动就位后,我们需要创建一个StorageClass(存储类)。你可以把它理解为一个“存储蓝图”或“存储产品目录”。它不直接代表一块存储空间,而是定义了当有人申请存储时,应该按照什么规格、从哪个后端来提供。

deploy目录下,官方已经提供了一个storageclass.yaml的示例。我们直接基于它修改。先看看它的内容:

cat deploy/storageclass.yaml

内容大致如下,我们需要修改几个关键参数:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
  server: 192.168.1.5  # 你的NFS服务器IP
  share: /data/nfs/k8s  # NFS服务器上的共享目录路径
  # onDelete: "retain"  # 可选:删除PVC时对后端数据的处理策略
  # subDir: "${.PVC.namespace}-${.PVC.name}" # 可选:自动创建的子目录命名模板
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
mountOptions:
  - nfsvers=4.1
  - hard
  - rsize=1048576
  - wsize=1048576
  - timeo=600
  - retrans=2
  - noresvport

我们来详细拆解每个配置项,这是理解动态供给如何工作的关键:

  • provisioner: nfs.csi.k8s.io: 这是最重要的字段,告诉K8S这个StorageClass由哪个CSI驱动来提供存储。必须和我们部署的csi-driver-nfs驱动声明的名称一致。
  • parameters部分
    • server: 你的NFS服务器地址。改成你自己的,比如192.168.1.5
    • share: NFS服务器上暴露的根共享目录。就是我们之前在/etc/exports里配置的/data/nfs/k8s
    • onDelete: (可选)当删除PVC(进而删除PV)时,对NFS服务器上对应子目录的处理方式。有两个值:
      • retain(默认):保留子目录和数据。这是最安全的,防止误删。
      • delete:删除子目录和数据。请谨慎使用,除非你确定数据不再需要。
    • subDir: (可选)一个强大的功能,用于定义在share目录下自动创建的子目录的命名规则。支持变量替换,比如"${.PVC.namespace}-${.PVC.name}"会生成像default-web-pvc这样的目录名。这能让NFS服务器上的目录结构非常清晰。如果不指定,驱动会使用一个随机的PV名称作为子目录名。
  • reclaimPolicy: Delete: PV的回收策略。Delete表示删除PVC时,对应的PV以及**后端存储(NFS子目录)**也会被删除(前提是onDelete设为delete)。另一个选项是Retain,即保留PV和存储,需要手动清理。
  • volumeBindingMode: Immediate: 卷绑定模式。Immediate表示创建PVC时立即绑定并创建PV。另一个模式是WaitForFirstConsumer,延迟到真正使用该PVC的Pod被调度时再绑定,这适用于拓扑受限的存储(比如本地盘)。
  • allowVolumeExpansion: true: 允许卷扩容。这是一个非常实用的功能。如果未来你的PVC空间不够了,可以直接修改PVC的spec.resources.requests.storage字段申请更大空间,驱动会尝试扩容NFS端的存储(注意:NFS本身是文件系统,扩容通常只是逻辑上的,实际空间取决于NFS服务器的磁盘)。
  • mountOptions: 挂载选项。这里可以精细控制NFS客户端的挂载行为。我强烈建议根据你的网络环境调整:
    • nfsvers=4.1: 指定使用NFS v4.1协议,比v3有更好的性能和特性(如并行NFS)。
    • hard: 硬挂载。如果NFS服务器无响应,客户端会无限重试,保证数据一致性。生产环境推荐。
    • rsize/wsize=1048576: 读写缓冲区大小,设为1MB可以提升大文件传输性能。
    • timeo=600: 超时时间(十分之一秒),这里设60秒。
    • retrans=2: 重试次数。
    • noresvport: 在重连时使用新的TCP端口,有助于网络故障恢复。

现在,根据你的环境修改好servershare,并决定是否启用subDir等高级功能,然后应用这个StorageClass:

kubectl apply -f deploy/storageclass.yaml

创建后,查看一下:

kubectl get storageclass

你应该能看到名为nfs-csi的存储类,PROVISIONER显示为nfs.csi.k8s.io。至此,动态供给的“蓝图”就准备好了。

7. 第五步:实战验证——用Nginx应用测试动态供给

蓝图有了,我们来实际“下单”体验一下动态供给的魔力。我们将创建一个简单的Nginx Deployment,并为它申请一个持久化存储卷。

创建一个YAML文件,比如nginx-dynamic-pvc.yaml。这个文件将同时定义PVC和引用该PVC的Deployment。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-html-pvc
spec:
  accessModes:
    - ReadWriteMany  # NFS支持多节点读写,这是其巨大优势
  storageClassName: nfs-csi  # 关键!指定使用我们刚创建的StorageClass
  resources:
    requests:
      storage: 2Gi  # 申请2GB存储空间
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-with-nfs
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html  # 将NFS卷挂载到Nginx的默认网页目录
      volumes:
      - name: html-volume
        persistentVolumeClaim:
          claimName: nginx-html-pvc  # 引用上面定义的PVC

这个配置的巧妙之处在于,我们没有预先创建任何PersistentVolume(PV)。我们只定义了一个PVC(nginx-html-pvc),并指定它要使用nfs-csi这个StorageClass。

应用这个配置:

kubectl apply -f nginx-dynamic-pvc.yaml

现在,让我们像看魔术一样,观察系统自动完成了哪些工作:

  1. 检查PVC状态

    kubectl get pvc
    

    几秒钟内,你应该会看到nginx-html-pvc的状态从Pending变为Bound。这意味着系统已经为它找到了(或者说创建了)一个合适的PV并完成了绑定。

  2. 检查自动创建的PV

    kubectl get pv
    

    你会看到一个自动创建的PV,它的CLAIM列显示为default/nginx-html-pvcSTORAGECLASSnfs-csi,容量正是我们申请的2Gi。这个PV的名字是系统自动生成的(如pvc-xxxxx)。

  3. 检查Pod运行状态

    kubectl get pods -l app=nginx
    

    两个Nginx Pod应该都处于Running状态。

  4. 验证NFS服务器上的数据目录: 登录到你的NFS服务器(192.168.1.5),查看共享目录下发生了什么:

    ls -la /data/nfs/k8s/
    

    你会看到一个以PV名称命名的子目录(或者如果你在StorageClass中配置了subDir,则会是你定义的格式)。这个目录就是动态创建出来的,对应着K8S里的那个PV。

    # 进入该目录,创建一个测试文件
    cd /data/nfs/k8s/<自动生成的目录名>
    echo "Hello from NFS Dynamic Provisioning!" > index.html
    
  5. 验证Pod内的数据访问: 回到K8S Master节点,我们进入一个Nginx Pod内部,查看挂载点:

    kubectl exec -it nginx-with-nfs-<pod-id> -- sh
    # 进入容器后
    cat /usr/share/nginx/html/index.html
    

    你应该能看到刚才在NFS服务器上创建的index.html文件内容!这说明Pod成功挂载了由CSI驱动动态创建和供给的NFS存储卷。

    更进一步,你可以在一个Pod里修改这个文件,然后在另一个Pod里查看,会发现修改是同步的,这完美验证了ReadWriteMany模式。

动态供给的整个闭环就这样完成了。从我们创建PVC,到系统自动创建PV和NFS子目录,再到Pod成功挂载并使用,全程无需人工干预。当你删除这个Deployment和PVC时(kubectl delete -f nginx-dynamic-pvc.yaml),根据StorageClass中reclaimPolicyonDelete的配置,对应的PV和NFS服务器上的数据也会被自动清理(或保留)。

8. 进阶技巧与生产环境注意事项

基本的动态供给跑通了,但想在生产环境用得稳,还得了解一些进阶配置和避坑指南。这里分享几个我踩过坑后总结的经验。

1. 存储类隔离与多NFS服务器 一个集群里可能有不同性能、不同用途的NFS服务器。你可以创建多个StorageClass来区分它们。例如:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi-fast
provisioner: nfs.csi.k8s.io
parameters:
  server: 192.168.1.10 # 高性能NFS服务器
  share: /fast-ssd-pool
mountOptions:
  - nfsvers=4.2
  - hard
  - rsize=1048576
  - wsize=1048576
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi-archive
provisioner: nfs.csi.k8s.io
parameters:
  server: 192.168.1.20 # 大容量归档NFS服务器
  share: /archive-hdd-pool
mountOptions:
  - nfsvers=3
  - hard

这样,开发团队可以根据应用需求选择nfs-csi-fast(用于数据库)或nfs-csi-archive(用于日志备份)。

2. 子目录命名模板的妙用 前面提到的subDir参数非常有用。一个推荐的模板是:"${.PVC.namespace}-${.PVC.name}-${.PV.name}"。这样在NFS服务器上,目录结构会像default-web-pvc-pvc-a1b2c3d4一样清晰,一眼就能看出是哪个命名空间、哪个PVC、对应哪个PV,排查问题时非常方便。

3. 权限与安全加固

  • NFS导出限制:生产环境绝对不要用*。将/etc/exports中的IP范围限制为你的K8S节点IP或Pod网络CIDR。
  • SELinux:如果节点启用了SELinux,需要确保容器有正确的上下文来访问NFS挂载点。通常需要给Pod的securityContext添加selinuxOptions,或者将NFS目录的SELinux上下文设置为svirt_sandbox_file_t。这是一个常见的挂载失败原因,错误信息通常是Permission denied
  • ServiceAccount:确保CSI Driver使用的ServiceAccount拥有足够的RBAC权限。我们部署时使用的官方YAML已经配置好了。

4. 监控与运维

  • 监控CSI驱动:关注kube-system命名空间下csi-nfs-controllercsi-nfs-node Pod的日志和资源使用情况。可以使用kubectl logs -f <pod-name> -n kube-system -c <container-name>来查看具体容器的日志。
  • 监控NFS服务器:监控NFS服务器的磁盘空间、IO性能、网络带宽。动态供给虽然方便,但也容易导致存储被快速消耗。
  • 设置配额:虽然K8S PVC可以请求任意大小,但NFS服务器本身没有配额限制。可以考虑在NFS服务器端使用文件系统配额(如XFS quota)或在K8S层面使用ResourceQuota来限制每个命名空间的存储使用量。

5. 故障排查思路 如果PVC一直处于Pending状态:

  • kubectl describe pvc <pvc-name>:查看事件,最常见的是provisioning failed
  • kubectl logs -n kube-system -l app=csi-nfs-controller:查看CSI Controller的日志,通常会明确报错,比如“无法连接NFS服务器”、“共享目录不存在”、“权限被拒绝”等。
  • 检查NFS服务器网络、防火墙、/etc/exports配置是否正确,以及共享目录的权限(确保no_root_squash已设置)。

如果Pod启动失败,提示挂载错误:

  • kubectl describe pod <pod-name>:查看Pod事件。
  • kubectl logs -n kube-system -l app=csi-nfs-node --tail=100:查看运行在问题节点上的CSI Node插件的日志。

6. 性能调优 对于IO密集型的应用,挂载选项mountOptions的调整至关重要。除了上面示例中的参数,你还可以根据网络延迟和丢包情况调整timeo(超时)和retrans(重试)的值。对于高并发读写场景,可以考虑在NFS服务器端使用SSD,并启用NFS v4.1或v4.2的并行特性(pNFS)。不过,对于绝大多数Web应用和中间件,默认配置加上rsize/wsize调整已经足够。

最后,记得在将这套方案用于核心生产业务前,一定要在测试环境进行充分的压力测试和故障模拟(如模拟NFS服务器宕机、网络中断),观察K8S Pod和CSI驱动的行为,制定好应急预案。存储是状态的基石,多花点时间打磨是值得的。

更多推荐