基于csi-driver-nfs的K8S存储动态供给实战指南
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-server | 192.168.1.5 | NFS存储服务器。负责提供共享目录 /data/nfs/k8s。 |
| k8s-master01 | 192.168.1.1 | Kubernetes Master节点。我们将在这里执行所有kubectl命令,部署CSI驱动。 |
| k8s-node01 | 192.168.1.2 | Kubernetes Worker节点。运行业务Pod,需要挂载NFS存储。 |
| k8s-node02 | 192.168.1.3 | Kubernetes Worker节点。同样运行业务Pod,实现跨节点存储访问。 |
核心工作流程是这样的:
- 在
nfs-server上搭建好NFS服务,并暴露一个共享目录。 - 在K8S集群的所有节点(Master和Node)上安装NFS客户端工具,确保它们能挂载NFS。
- 在
k8s-master01上部署csi-driver-nfs。这会创建两部分Pod:- Controller Pod:运行在Master节点(通过Deployment部署),负责监听PVC的创建/删除事件,并调用CSI接口去NFS服务器上实际创建或删除子目录。
- Node Pod:运行在每个Worker节点(通过DaemonSet部署),负责在Pod调度到本节点时,执行实际的
mount操作,将NFS目录挂载到Pod里。
- 创建一个
StorageClass,指向我们的NFS服务器和共享目录。 - 最后,我们用一个Nginx应用来验证:创建一个引用该
StorageClass的PVC和Deployment,观察PVC是否自动绑定、PV是否自动创建、NFS目录下是否自动生成子目录,以及Pod能否正常读写。
听起来步骤不少,但别担心,跟着我一步步来,其实都是很直白的操作。我们先把最基础的NFS服务搭起来。
3. 第一步:搭建你的NFS存储服务器
NFS服务器的配置其实很简单,但有几个细节不注意,后面在K8S里挂载可能会踩坑。我们就在nfs-server(192.168.1.5)这台机器上操作。
首先,安装必要的软件包。在RHEL/CentOS/Rocky Linux等系统上,使用yum或dnf:
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.yaml和deploy/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一直处于ContainerCreating或Pending,可以用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端口,有助于网络故障恢复。
现在,根据你的环境修改好server和share,并决定是否启用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
现在,让我们像看魔术一样,观察系统自动完成了哪些工作:
-
检查PVC状态:
kubectl get pvc几秒钟内,你应该会看到
nginx-html-pvc的状态从Pending变为Bound。这意味着系统已经为它找到了(或者说创建了)一个合适的PV并完成了绑定。 -
检查自动创建的PV:
kubectl get pv你会看到一个自动创建的PV,它的
CLAIM列显示为default/nginx-html-pvc,STORAGECLASS是nfs-csi,容量正是我们申请的2Gi。这个PV的名字是系统自动生成的(如pvc-xxxxx)。 -
检查Pod运行状态:
kubectl get pods -l app=nginx两个Nginx Pod应该都处于
Running状态。 -
验证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 -
验证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中reclaimPolicy和onDelete的配置,对应的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-controller和csi-nfs-nodePod的日志和资源使用情况。可以使用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驱动的行为,制定好应急预案。存储是状态的基石,多花点时间打磨是值得的。
更多推荐
所有评论(0)