Kubernetes NFS CSI 踩坑:FCOS + NFSv4.2 的那些坑
家里的 K8s 集群跑在三台 Fedora CoreOS 虚拟机上,一直没搞持久化存储。这周末抽空搭了个 NFS 存储服务器,整个过程以为半小时搞定,结果断断续续折腾了一下午。NFSv4 的伪文件系统平时没怎么关注,这次真是被教做人了。
环境
- 存储服务器:FCOS 虚拟机,IP
192.168.122.84,hostnamestorage-001.k8s.junjie.pro - CSI driver:csi-driver-nfs v4.13.2,Helm 部署
- 目标:动态 Provisioning,PVC 声明后自动在
/var/nfs-data/下创建namespace/pvc-name子目录
架构
┌─────────────────────────────┐
│ k8s-storage-001 (FCOS VM) │
│ /var/nfs-data/ │ ← fsid=0 → NFSv4 pseudoroot
│ ├─ default/ │
│ │ └─ nfs-pvc-test/ │ ← subDir 自动创建
│ └─ ... │
│ nfs-server (NFSv4.2) │
└──────────────┬──────────────┘
│ NFSv4.2, vers=4.2
┌──────────────┴──────────────┐
│ csi-driver-nfs │
│ ├─ controller plugin │ ← provisioning
│ ├─ node plugin (DaemonSet) │ ← mount/unmount
│ └─ StorageClass: nfs-csi │
└─────────────────────────────┘
存储服务器配置
FCOS 是 immutable 系统,所有配置走 Butane / Ignition。这里有两个 systemd 单元比较关键。
第一个在首次启动时安装 nfs-utils 包。FCOS 用 rpm-ostree 分层安装系统级包:
- name: rpm-ostree-install-packages.service
enabled: true
contents: |
[Unit]
Description=Install required packages via rpm-ostree
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/rpm-ostree install --reboot --idempotent --assumeyes nfs-utils
[Install]
WantedBy=multi-user.target
注意 --reboot——rpm-ostree 安装完需要重启才能生效,所以首次启动是:Ignition → 安装包 → 自动重启 → 系统起来后包才可用。
第二个单元在重启后启用 NFS 服务:
- name: nfs-server-enable.service
enabled: true
contents: |
[Unit]
Description=Enable NFS server after packages are installed
After=network-online.target rpm-ostree-install-packages.service
Wants=network-online.target
ConditionPathExists=/usr/sbin/rpc.nfsd
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/systemctl enable --now nfs-server
[Install]
WantedBy=multi-user.target
ConditionPathExists=/usr/sbin/rpc.nfsd 是精髓:第一次启动时 nfs-utils 还没分层,condition 不满足,单元跳过;重启后二进制存在,单元执行,enable 并 start nfs-server。之后每次启动都是空操作。
导出配置写 Ignition 阶段的 storage.files,扔到 /etc/exports.d/k8s.exports:
/var/nfs-data 192.168.122.0/24(rw,sync,no_root_squash,no_subtree_check,fsid=0)
/var/nfs-data 目录用 Butane 的 storage.directories 创建,不用自己写 mkdir 脚本。
部署 CSI Driver
Helm 一把梭,不赘述:
helm repo add csi-driver-nfs \
https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
helm upgrade --install csi-driver-nfs csi-driver-nfs/csi-driver-nfs \
--namespace kube-system --create-namespace --version 4.13.2
StorageClass 初始版本长这样(后面会改很多次):
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
server: storage-001.k8s.junjie.pro
share: /var/nfs-data
subPathPattern: "${pvc.metadata.namespace}/${pvc.metadata.name}"
reclaimPolicy: Retain
mountOptions:
- nfsvers=4.2
- hard
- rsize=524288
- wsize=524288
然后建个测试 PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc-test
spec:
accessModes: [ReadWriteMany]
storageClassName: nfs-csi
resources:
requests:
storage: 1Gi
第一坑:subPathPattern 不存在
$ kubectl get pvc nfs-pvc-test
NAME STATUS VOLUME CAPACITY STORAGECLASS
nfs-pvc-test Pending nfs-csi
$ kubectl describe pvc nfs-pvc-test
Events:
Warning ProvisioningFailed ...
rpc error: code = InvalidArgument desc = invalid parameter "subPathPattern" in storage class
subPathPattern?这参数名是我凭印象编的。翻 csi-driver-nfs 的 driver-parameters 文档,正确的参数叫 subDir,而且支持 ${pvc.metadata.namespace} 和 ${pvc.metadata.name} 这种变量替换,对的就是这两种,没有别的。
修正:
parameters:
subDir: "${pvc.metadata.namespace}/${pvc.metadata.name}"
StorageClass 的 parameters 字段不允许原地修改,得删了重建:
kubectl delete storageclass nfs-csi
kubectl apply -f storage-class.yaml
PVC 也要删了重建才能触发新的 provisioning:
kubectl delete pvc nfs-pvc-test --force --grace-period=0
# wait for finalizer...
kubectl apply -f nfs-pvc-test.yaml
第二坑:NFSv4.2 返回 “No such file or directory”
参数修好了,provisioner 不再报 invalid parameter,开始真正尝试挂载。然后又挂了:
$ kubectl describe pvc nfs-pvc-test
Events:
Warning ProvisioningFailed ...
Mounting command: mount
Mounting arguments: -t nfs -o nfsvers=4.2,hard,rsize=524288,wsize=524288,... \
storage-001.k8s.junjie.pro:/var/nfs-data /tmp/pvc-xxx
Output: mount.nfs: mounting storage-001.k8s.junjie.pro:/var/nfs-data failed,
reason given by server: No such file or directory
“reason given by server” 说明 NFS 服务器收到了请求,也给出了回复,但它说目录不存在。
但目录明明在啊:
$ ssh [email protected] ls /var/nfs-data/
a # 之前手动建的测试目录
showmount 也正常:
$ showmount -e storage-001.k8s.junjie.pro
Export list for storage-001.k8s.junjie.pro:
/var/nfs-data 192.168.122.0/24
RPC 服务全在:
$ rpcinfo -p storage-001.k8s.junjie.pro | grep nfs
100003 3 tcp 2049 nfs
100003 4 tcp 2049 nfs # NFSv3 和 v4 都注册了
第三坑:NFSv3 能通但不能用
换个思路,会不会是 NFSv4.2 的版本问题?降级到 NFSv3 试试。把 StorageClass 里的 nfsvers 改成 3:
mountOptions:
- nfsvers=3
重建 SC 和 PVC,这次不再是 “No such file”,而是:
Output: mount.nfs: rpc.statd is not running but is required for remote locking.
Mount.nfs: Either use '-o nolock' to keep locks local, or start statd.
NFSv3 需要 rpc.statd 做文件锁,但 CSI driver 跑在容器里,没有这个服务。
加上 nolock 能临时跑通,但我不想要 NFSv3——都什么年代了,NFSv4.2 的文件锁是协议内置的,不走 statd。所以这只能是 fallback,不能是最终方案。
不过通过这个测试确认了一件事:网络和 mountd 都是正常的,问题出在 NFSv4 协议层面的路径解析。
第四坑:手动 mount 的障眼法
我在 K8s 节点上手动 mount 了一把:
$ sudo mount -t nfs storage-001.k8s.junjie.pro:/var/nfs-data /mnt
# 没有报错,挂载成功?
这让我困惑了好一阵。CSI provisioner 在 pod 里挂着同样的参数却报错,手动 mount 却成功了?
查了下实际协商的版本:
$ findmnt -n -o FSTYPE,OPTIONS /mnt
nfs ...,vers=3,...
果然,mount -t nfs 不带版本号时,客户端会自动协商:先试 4.2 → 4.1 → 4.0 → 回退到 3。NFSv4 失败后它默默地降级到 v3 了。CSI driver 在 mountOptions 里显式指定了 nfsvers=4.2,没有这个降级逻辑。
从这里可以确定:NFS 服务器配置有问题,NFSv4 特定地挂了,NFSv3 是通的。
根因:NFSv4 伪文件系统与 FCOS 的 /var
NFSv4 和 v3 最大的架构区别是,v4 有一个伪文件系统(pseudofilesystem)的概念。所有 NFSv4 导出都在一个虚拟的文件树上,客户端挂载时提供路径,服务端在伪文件树里查找。
默认配置下(不加 fsid 参数),NFS 服务器的伪文件树根是 /。当客户端请求挂载 /var/nfs-data 时,服务端在伪文件树里找 /var/nfs-data:
/→ 能找到,根当然在/var→ 找不到
为什么找不到 /var?在标准 Linux 发行版上,/var 就是文件系统上的一个普通目录,NFSv4 可以正常穿越。但在 Fedora CoreOS 上,/var 是一个独立的 bind mount(/sysroot/var → /var),NFSv4 的伪文件系统默认不穿越 mount 边界。路径在 /var 这里断了,后面 /var/nfs-data 自然也找不到。
这就是 “No such file or directory” 的来源——不是目录不存在,是 NFSv4 在伪文件树的路径解析里迷路了。
相关文档:NFSv4 伪文件系统(fsid 参数部分)、Linux NFS Wiki。
解决:fsid=0
fsid=0 把指定的导出路径设为 NFSv4 伪文件树的根节点。思路就是不让 NFSv4 去穿越 /var——直接让 /var/nfs-data 成为根:
/var/nfs-data 192.168.122.0/24(rw,sync,no_root_squash,no_subtree_check,fsid=0)
exportfs -ra 或重启 nfs-server 生效。手动验证:
# 注意:挂载路径变成了 / 而不是 /var/nfs-data
$ sudo mount -t nfs -o nfsvers=4.2 storage-001.k8s.junjie.pro:/ /mnt
$ findmnt -n -o FSTYPE,OPTIONS /mnt
nfs4 ...,vers=4.2,...
确认是 NFSv4.2。但是注意:挂载路径从 server:/var/nfs-data 变成了 server:/。这是因为伪文件树的根就是 /var/nfs-data,挂载 / 就自然对应到它。
StorageClass 也要改
CSI driver 的 mount 路径是由 share 参数决定的。之前是:
parameters:
share: /var/nfs-data # → mount server:/var/nfs-data → NFSv4 失败
现在 NFSv4 根变了,share 得跟着改:
parameters:
share: / # → mount server:/ → NFSv4 根 = /var/nfs-data
subDir 的逻辑不受影响——它是在 share 路径下创建 namespace/pvc-name 子目录,物理上还是在 /var/nfs-data/ 下面:
服务端: /var/nfs-data/default/nfs-pvc-test/
CSI挂载: server:/default/nfs-pvc-test ← 相对于 share: /
删了 SC 和 PVC 重建:
kubectl delete storageclass nfs-csi
kubectl apply -f storage-class.yaml
kubectl delete pvc nfs-pvc-test --force --grace-period=0
kubectl apply -f nfs-pvc-test.yaml
$ kubectl get pvc nfs-pvc-test
NAME STATUS VOLUME CAPACITY STORAGECLASS
nfs-pvc-test Bound pvc-... 1Gi nfs-csi
终于 Bound 了。确认挂载版本:
$ kubectl exec -n kube-system csi-nfs-controller-xxx -c nfs -- mount | grep nfs
storage-001.k8s.junjie.pro:/default/nfs-pvc-test on /var/lib/kubelet/...
type nfs4 (...,vers=4.2,rsize=524288,wsize=524288,hard,proto=tcp,timeo=600,retrans=2,...)
type nfs4, vers=4.2,就是它。
最终 StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: nfs.csi.k8s.io
allowVolumeExpansion: true
parameters:
server: storage-001.k8s.junjie.pro
share: /
subDir: "${pvc.metadata.namespace}/${pvc.metadata.name}"
reclaimPolicy: Retain
volumeBindingMode: Immediate
mountOptions:
- nfsvers=4.2
- hard
- rsize=524288
- wsize=524288
- timeo=600
- retrans=2
总结
| 现象 | 排查方向 | 根因 | 解法 |
|---|---|---|---|
invalid parameter "subPathPattern" |
kubectl describe pvc |
参数名不存在,csi-driver-nfs 用 subDir |
改参数名 |
No such file or directory from NFS server |
showmount -e 正常,rpcinfo 正常 |
FCOS 上 /var 是 bind mount,NFSv4 伪文件系统无法穿越 |
fsid=0 将导出设为伪文件系统根 |
| 手动 mount 成功但 CSI 失败 | findmnt 看实际协商版本 |
mount -t nfs 自动降级到 v3,CSI 显式指定 v4 |
修好 NFSv4 服务端配置 |
NFSv3 rpc.statd missing |
Pod 内无 statd 服务 | v3 文件锁依赖外部的 statd | 回 NFSv4(v4 锁内置于协议) |
fsid=0 后路径变了 |
伪文件系统根变了,挂载路径也要变 | NFSv4 根 = /var/nfs-data,挂载路径是 / 而非 /var/nfs-data |
SC 里 share: / |
整个过程最核心的一步是弄懂了 NFSv4 的伪文件系统——平时用 NFS 基本是 mount 一把梭,不出问题就不会去想底层。这次 FCOS 的 /var bind mount 刚好触发了这个边界条件。NFSv3 能通也是因为 v3 不走伪文件系统,直接用 MOUNT 协议请求实际文件路径。
参考链接: