一、环境与前置检查
本次环境如下,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 | Export list for 192.168.5.5: |
3. 手动挂载确认
建议不要省略,可提前暴露路径权限问题。
1 | sudo mkdir -p /mnt/nfstest |
二、安装 csi-driver-nfs
官方 deploy 目录按版本号分了四个文件。
这几个镜像都在
registry.k8s.io上,国内直连会超时,建议先配好镜像加速再执行,写法见 Kubernetes/k8s - 快速入门 的国内源一节。
1 | VER=v4.13.6 |
若拉不到
raw.githubusercontent.com,先把文件下载到本地再kubectl apply -f ./xxx.yaml,内容完全一致。
1. 检查安装结果
1 | kubectl -n kube-system get pods | grep csi-nfs |
期望看到 controller 一个、node 每个节点一个:
1 | csi-nfs-controller-xxx 5/5 Running |
1 | NAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY |
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 | apiVersion: storage.k8s.io/v1 |
1 | kubectl apply -f sc-nfs.yaml |
几个参数值得单独说一下:
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 | /fs/1000/nfs/nfs_disk/ |
支持的变量:
| 变量 | 含义 |
|---|---|
${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 | kubectl delete sc nfs-csi |
四、部署测试应用
test-nfs.yaml:
1 | kind: PersistentVolumeClaim |
1 | kubectl apply -f test-nfs.yaml |
PVC 应该很快变成 Bound:
1 | NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS |
五、验证
容器里读一下文件:
1 | kubectl exec deploy/test-pod -- cat /mnt/SUCCESS.txt |
1 | mount success! |
再到 NAS 文件管理器里看 /fs/1000/nfs/nfs_disk/,因为配了 subDir,目录是按命名空间和 PVC 名分层的:
1 | default/test-pv-claim/ |
想验证多节点并发写,可以把 replicas 改成 3,args 换成:
1 | replicas: 3 |
三个 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。
