抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

一、环境与前置检查

本次环境如下,IP 和路径请替换成自己的。

项 值
集群 k3s,3 个 server 节点(debian-node1/2/3)
节点系统 Debian
NFS 服务端 192.168.5.5
NFS 导出路径 /fs/1000/nfs
实际可写子目录 /fs/1000/nfs/nfs_disk
csi-driver-nfs 版本 v4.13.6

1. 每个节点安装 NFS 客户端

kubelet 是在宿主机上执行 mount -t nfs 的,所以三个节点都得有挂载工具,不是装一台就够了。

1
sudo apt install -y nfs-common

2. 确认服务端导出正常

1
showmount -e 192.168.5.5
1
2
Export list for 192.168.5.5:
/fs/1000/nfs *

3. 手动挂载确认

建议不要省略,可提前暴露路径权限问题。

1
2
3
4
5
6
sudo mkdir -p /mnt/nfstest
sudo mount -t nfs 192.168.5.5:/fs/1000/nfs /mnt/nfstest
ls -ld /mnt/nfstest
# 若 NFS 上只有某个子目录可写,把下面的测试路径换成该目录
sudo touch /mnt/nfstest/test-file && echo "可写" || echo "不可写"
sudo umount /mnt/nfstest

二、安装 csi-driver-nfs

官方 deploy 目录按版本号分了四个文件。

这几个镜像都在 registry.k8s.io 上,国内直连会超时,建议先配好镜像加速再执行,写法见 Kubernetes/k8s - 快速入门 的国内源一节。

1
2
3
4
5
VER=v4.13.6
kubectl apply -f "https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/deploy/${VER}/rbac-csi-nfs.yaml"
kubectl apply -f "https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/deploy/${VER}/csi-nfs-driverinfo.yaml"
kubectl apply -f "https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/deploy/${VER}/csi-nfs-controller.yaml"
kubectl apply -f "https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/deploy/${VER}/csi-nfs-node.yaml"

若拉不到 raw.githubusercontent.com,先把文件下载到本地再 kubectl apply -f ./xxx.yaml,内容完全一致。

1. 检查安装结果

1
2
kubectl -n kube-system get pods | grep csi-nfs
kubectl get csidriver nfs.csi.k8s.io

期望看到 controller 一个、node 每个节点一个:

1
2
3
4
csi-nfs-controller-xxx   5/5   Running
csi-nfs-node-aaaaa 3/3 Running
csi-nfs-node-bbbbb 3/3 Running
csi-nfs-node-ccccc 3/3 Running
1
2
NAME             ATTACHREQUIRED   PODINFOONMOUNT   STORAGECAPACITY
nfs.csi.k8s.io false false false

ATTACHREQUIRED 要是 false,NFS 没有 attach/detach 这个流程,Pod 调度到哪个节点都能直接挂。

2. controller 和 node 各干什么

组件 类型 作用
csi-nfs-controller Deployment(1 副本) 处理 CreateVolume / DeleteVolume,在 NFS 上建、删子目录
csi-nfs-node DaemonSet(每节点 1 个) 处理 NodeStage / NodePublish,执行真正的 mount -t nfs

另外顺带解释一个常见疑问:部署时为什么不用填 NFS 地址。因为驱动本身是通用的,地址写在 StorageClass 的 parameters 里。一个驱动可以对应多个 StorageClass,也就能同时对接多个 NFS 服务端。

三、创建 StorageClass

sc-nfs.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
server: 192.168.5.5
share: /fs/1000/nfs/nfs_disk # 务必确保该目录对 nfs 可写,nfs_disk 可换成其他目录
subDir: ${pvc.metadata.namespace}/${pvc.metadata.name} # 每个 PVC 独立目录,按命名空间分层
onDelete: retain # 删 PVC 时保留数据
reclaimPolicy: Delete # 删 PV 对象本身
allowVolumeExpansion: true # 允许 PVC 改大(只改数字)
volumeBindingMode: Immediate
mountOptions:
- nfsvers=4.1
- hard
- timeo=50
- retrans=2
1
2
kubectl apply -f sc-nfs.yaml
kubectl get sc nfs-csi

几个参数值得单独说一下:

  • server:只能写 IP,不能写 DNS 名。kubelet 在宿主机上挂载,解析不了集群 DNS。
  • share:必须和 showmount -e 的输出一致,而且要可写。
  • subDir:每个 PVC 的子目录模板,支持变量替换(v4.6+)。
  • onDelete:delete / retain / archive 三选一,决定删 PVC 时 NFS 上的目录怎么处理,三者的具体差别见第六节。
  • reclaimPolicy:管的是 PV 对象,和 onDelete 是两件事。配成 Delete + retain,效果就是 PV 被删掉、NFS 目录留着。
  • allowVolumeExpansion:只放开 PVC 改 requests.storage 的数字,NFS 不会真的扩容,详见第六节。
  • mountOptions:nfsvers=4.1 必加,hard 用来保证断线重试。

1. subDir 的目录规则

配了 ${pvc.metadata.namespace}/${pvc.metadata.name} 之后,NFS 上会变成这样:

1
2
3
/fs/1000/nfs/nfs_disk/
└── default/
└── test-pv-claim/

支持的变量:

变量 含义
${pvc.metadata.namespace} PVC 所在命名空间
${pvc.metadata.name} PVC 名称
${pv.metadata.name} 生成的 PV 名称
${pvc.metadata.labels.xxx} PVC 标签
${pvc.metadata.annotations.xxx} PVC 注解

不配 subDir 的话,目录名默认用 PV 名,动态供应时 PV 名就是 pvc-<uuid>,所有命名空间的 PVC 全平铺在 share 根目录下。

改 subDir 只对之后新建的 PVC 生效,已经存在的 PVC 目录不会迁移。

2. parameters 创建后改不了

StorageClass 的 parameters 是不可变字段,改路径只能删了重建:

1
2
3
kubectl delete sc nfs-csi
# 修改 yaml 后重新 apply
kubectl apply -f sc-nfs.yaml

四、部署测试应用

test-nfs.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-pv-claim
spec:
storageClassName: nfs-csi # 若有多个 nfs 服务端,对应多个 storageclass,这里要改
accessModes:
- ReadWriteMany
resources:
requests:
storage: 2Mi
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: test-pod
labels:
app: test-pod
spec:
replicas: 1
selector:
matchLabels:
app: test-pod
template:
metadata:
labels:
app: test-pod
spec:
containers:
- name: test-pod
image: alpine:3.18
command:
- "/bin/sh"
args:
- "-c"
- "echo 'mount success!' > /mnt/SUCCESS.txt && sleep 3600"
volumeMounts:
- name: nfs-pvc
mountPath: "/mnt"
volumes:
- name: nfs-pvc
persistentVolumeClaim:
claimName: test-pv-claim
1
2
3
4
kubectl apply -f test-nfs.yaml

kubectl get pvc test-pv-claim
kubectl get pod -l app=test-pod

PVC 应该很快变成 Bound:

1
2
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS
test-pv-claim Bound pvc-86be1bc3-cc5a-424d-9894-4c8005408679 2Mi RWX nfs-csi

五、验证

容器里读一下文件:

1
kubectl exec deploy/test-pod -- cat /mnt/SUCCESS.txt
1
mount success!

再到 NAS 文件管理器里看 /fs/1000/nfs/nfs_disk/,因为配了 subDir,目录是按命名空间和 PVC 名分层的:

1
2
default/test-pv-claim/
└── SUCCESS.txt

想验证多节点并发写,可以把 replicas 改成 3,args 换成:

1
2
3
4
replicas: 3
args:
- "-c"
- "echo \"mount success from $(hostname)\" > /mnt/SUCCESS-$(hostname).txt && sleep 3600"

三个 Pod 共享同一个 RWX 卷,各写各的文件,能确认跨节点访问正常。

六、注意事项

1. onDelete 的三个取值

onDelete 只在 DeleteVolume(删 PVC、PV 回收时触发)阶段起作用,三个值的行为差别如下:

值 DeleteVolume 时做什么
delete(默认) 挂载 share,把 subDir 目录整个删掉,再向上清理变空的父目录
retain 直接返回,连挂载都跳过,目录和数据原样留着
archive 把 subDir 重命名为 archived-<subDir>,父目录不存在会自动创建

archive 的 archived- 是加在 subDir 最前面,不是替换目录名。比如 subDir 是 default/test-pv-claim,归档后变成 archived-default/test-pv-claim。

同一个 PVC 名第二次 archive 会失败:os.Rename 碰到已存在的非空目录会报错。除非给 controller 加上 --remove-archived-volume-path 参数,让它先删掉旧归档。

delete 的清理是向上递归的,配了 subDir: ${pvc.metadata.namespace}/${pvc.metadata.name} 时,删完 PVC 后命名空间目录若变空,也会一起消失。

而 retain 的代价是目录永远不会自动清理。删了 PVC 目录还留着,时间长了就是一堆垃圾目录,得定期人工或写脚本清。

2. allowVolumeExpansion 只是改数字

allowVolumeExpansion: true 允许修改 PVC 的 spec.resources.requests.storage:

1
kubectl patch pvc test-pv-claim -p '{"spec":{"resources":{"requests":{"storage":"5Gi"}}}}'

但对 NFS 来说,ControllerExpandVolume 只是把 PV/PVC 的 capacity 字段从 2Mi 改成 5Gi,NodeExpandVolume 是空操作。所以 PVC 的 CAPACITY 显示会变,Pod 里 df 看到的还是整个 NFS 的总量,实际可用空间一点没变。另外 K8s 只允许改大,不允许改小。

3. 容量限制做不到

csi-driver-nfs 不具备容量限制能力。PV 的 capacity.storage 只是元数据,所有 PVC 的 df 显示的都是整个 NFS 文件系统的总量,kubelet_volume_stats_used_bytes 对所有 NFS PVC 返回的也是同一个值。申请 10Gi 的 PVC 照样能写满整个 NAS。

真要限制,只能在 NAS 侧动手,比如 ZFS dataset quota、btrfs qgroup 或 XFS project quota。

评论