Pod 起来了、PV 绑定了、挂载失败了——NFS 版本号写错一个数字
上篇讲了 PV 删不掉——Finalizer 和 PVC Protection 锁死删除流程,这篇我们来看另一个方向的问题:PV 挂载上了,但容器里就是读不到数据。
StorageClass 配了 NFS 服务器地址和导出路径 → PVC 正常 Bound → Pod 正常 Running。但进容器 ls /data——空目录。不是文件没写入,是 NFS 挂载根本就没生效。
不是存储不可达——是 NFS 协议版本号写错了一个数字。
PV(PersistentVolume,集群级存储资源)的挂载流程分两步:Attach(CSI 驱动层分配设备)和 Mount(kubelet 在 Node 上执行 mount 命令)。NFS 不需要 Attach,但 Mount 阶段 kubelet 会调 CSI 驱动→调用 mount.nfs→和 NFS Server 协商协议版本。版本号不匹配,协商直接失败。
场景:NFS StorageClass 配了 mountOptions 后 Pod 挂载失败,kubelet 反复重试 路径:Pod Events → mountOptions → kubelet 日志 → mount.nfs 协议版本协商 → 修复
以下排查基于 K8s v1.28、NFS v3/v4、Linux kernel 5.15
kubectl describe pod Events 已经告诉你了
Events 中的 Protocol not supported 是 key signal。exit status 32 对应 mount.nfs 的 EPROTONOSUPPORT——NFS 协议版本协商失败。kubelet(K8s 节点代理,负责管理 Pod 和挂载卷)重试了 12 次,全失败。

PV 的 STATUS 是 Bound——说明 PVC 绑定成功。绑定成功只代表 K8s 侧分配了 Volume 对象,不代表 mount 完成了。Mount 发生在 Pod 调度到 Node 之后。

分层:从 Pod 到 NFS Server,逐层查挂载链路
Pod 层:Events 解读
Events 已经给了方向——Protocol not supported。NFS 协议协商由 mount.nfs 客户端和 NFS Server 的 rpc.mountd 完成。客户端发起的 NFS 版本不在 Server 支持的范围内,Server 返回 NFS4ERR_MINOR_VERS_MISMATCH 或 PROG_NOSUPPORT,客户端翻译为 "Protocol not supported"。
mount.nfs(Linux NFS 挂载工具,负责发起 RPC 调用协商 NFS 版本)的版本协商逻辑:
mount -t nfs -o nfsvers=4.0 server:/export /mnt
→ mount.nfs 向 server 的 portmapper(RPC 端口映射服务)查询 nfsd 的端口
→ 发 NFSv4.0 的 RPC call(SETCLIENTID + EXCHANGE_ID)
→ Server 如果不支持 4.0,返回 NFS4ERR_MINOR_VERS_MISMATCH
→ mount.nfs 兜底尝试 NFSv3?
→ 取决于 nfsvers=4 是否强约束。强约束则不兜底,直接失败。
CRI 层:crictl inspect 看实际 mount 状态
$ crictl ps | grep nginx
f8a2b1c0d3e4 nginx 28 minutes ago Running nginx-pod
$ crictl inspect f8a2b1c0d3e4 | grep -A 20 mounts
"mounts": [
{
"container_path": "/data",
"host_path": "/var/lib/kubelet/pods/xxx/volumes/kubernetes.io~csi/nfs-volume/mount",
"readonly": false
},
...
]
容器内的 /data 映射到了 /var/lib/kubelet/pods/xxx/volumes/.../mount——但那是 host 上的空目录,NFS 挂载没有真正生效。crictl(CRI 运行时命令行工具,可以直接查看容器状态和挂载)不会告诉你挂载是否成功,它只告诉你映射关系。
# 在 Node 上用 nsenter 进容器 netns 看 mount
$ nsenter -t $(crictl inspect f8a2b1c0d3e4 | jq '.info.pid') -m -- mount | grep nfs
# 无输出 → NFS 未挂载
Node 层:kubelet 日志 + mount 命令
kubelet 的 MountVolume 日志包含 CSI driver 返回的原始错误:

error -22 是 EINVAL,表示 NFSv4.0 的 lease 协商参数被 Server 拒绝了。Server 可能只支持 NFSv3。
集群层:StorageClass mountOptions + CSIDriver
nfsvers=4 是问题根源。这个选项告诉 mount.nfs 只协商 NFSv4——不兜底 NFSv3。如果 NFS Server 不支持 v4(或者只支持 v4.1/v4.2 但不支持 v4.0),mount 直接失败。
StorageClass(存储类,定义了存储的 provisioner 和挂载参数)的 mountOptions 会透传给 CSI driver,CSI driver 在调用 mount 时原样传递给 mount.nfs。
attachRequired: false——NFS 不需要 Attach 步骤,只执行 MountDevice。

这里只列出了 NFS v3 和 v4(v4.0 泛指)。但如果 Server 只实现了 v4.1/4.2 而不兼容 v4.0 的 SETCLIENTID 语义,挂载 nfsvers=4.0 依然失败。
挂载链路全景

路径:🔍 下次 PV 挂载失败,先跑这套命令
- 看 Pod Events →
kubectl describe pod <pod> | grep -A 10 Events,找Protocol not supported/Permission denied/No such file or directory - 查 StorageClass →
kubectl get sc <sc> -o yaml | grep mountOptions,看nfsvers或vers值 - 查 kubelet 日志 →
journalctl -u kubelet --no-pager | grep MountVolume | grep <volume-name>,看 error 的 exit status 和 message - 查节点 mount 状态 →
mount | grep nfs、ls -la /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<volume-name>/mount - 查 NFS Server 版本 →
rpcinfo -p <nfs-server> | grep nfs,对比 StorageClass 的 nfsvers

定位:大多数人会怎么查 vs 正确做法
❌ 大多数人会怎么查
做法 1:怀疑网络问题
$ ping nfs-server.internal
64 bytes from 10.0.1.100: icmp_seq=1 ttl=64 time=0.5ms
"能 ping 通,网络没问题 → 那问题肯定在存储"
事实:网络通 ≠ NFS 协议协商通。ICMP(互联网控制消息协议,ping 使用的协议)工作在 IP 层,NFS RPC(远程过程调用,NFS 的通信协议)工作在应用层。端口能 ping 通只表示 IP 层可达,不代表 nfsd 的 RPC 服务正常响应。
做法 2:只看 PV Bound 就认为挂载成功
"PV 状态是 Bound、Pod 是 Running → 挂载肯定成功了"
事实:PV Bound 只代表 PVC 绑定成功——这是调度阶段的动作。MountVolume 在 Pod 启动后才由 kubelet 异步执行。Pod 可以 Running(容器进程已启动),但挂载失败不会阻止容器启动——只是容器里读不到数据。典型表现:应用不报错但日志里一直写 /data is empty。
做法 3:在 Pod 里重装 NFS 客户端
$ kubectl exec nginx-pod -- apt-get install -y nfs-common
Pod 内装 NFS 客户端不会解决 kubelet 侧的挂载。挂载是 kubelet 做的,不是 Pod 进程做的。
✅ 正确的排查思路
PV 挂载失败不是存储问题——是 kubelet 到 NFS Server 的挂载管道问题。问三个问题:
| 层 | 问题 | 怎么查 |
|---|---|---|
| StorageClass | CSI driver 配了什么 mountOptions? | kubectl get sc <sc> -o yaml \| grep mountOptions |
| Node | kubelet 执行 mount 时报了什么? | journalctl -u kubelet \| grep MountVolume |
| Server | NFS Server 支持什么协议版本? | rpcinfo -p <server> \| grep nfs |
正确顺序:
- 先看 Pod Events——MountVolume.MountDevice failed 的 message 包含 root cause(Protocol not supported / Permission denied / No such file or directory)
- 看 StorageClass mountOptions——确认 nfsvers 或 vers 的值
- 查 kubelet 日志——CSI driver 返回的原始错误
- 查 NFS Server 的 rpcinfo——确认支持的协议版本
- 调整 mountOptions 后重新部署
标点:修复 + Check-list + 金句
修复方案
场景 A:NFS Server 只支持 v3,配了 nfsvers=4
kubectl patch storageclass nfs-sc -p '{"mountOptions":["nfsvers=3"]}' --type=merge——或者清空 mountOptions 让 mount.nfs 自动协商(先尝试 NFSv4,被拒后回退到 v3)。
场景 B:NFS Server 只支持 v4.1/4.2,配了 nfsvers=4.0
NFSv4.1 引入了 EXCHANGE_ID 替代 v4.0 的 SETCLIENTID,部分实现(如 NetApp、Qumulo)只实现了 v4.1 及以上语义,拒绝 v4.0 的 SETCLIENTID。改为 nfsvers=4.1 即可。
场景 C:mountOptions 常见陷阱
| mountOptions | 行为 | 适用场景 |
|---|---|---|
| 不配 | mount.nfs 自动协商(v4→v3 兜底) | 大多数场景 |
nfsvers=3 |
强制 v3 | Server 只支持 v3 |
nfsvers=4 |
强制 v4.0,不兜底 | 需要 v4 特性(ACL、Kerberos) |
nfsvers=4.1 |
强制 v4.1,不兜底 | v4.1 专用 Server |
vers=3,proto=tcp |
vers 是 nfsvers 的别名 |
兼容不同发行版配置 |
soft,timeo=100,retrans=3 |
软挂载 + 超时配置 | 非关键业务 |
⚠️ 注意:
nfsvers和vers是等价的,但不同发行版的手册页推荐不同。Linux kernel 5.x 及以上建议用nfsvers。

Check-list
- [ ]
kubectl describe pod <pod> | grep -A 10 Events— 看 MountVolume 失败原因 - [ ]
kubectl get storageclass <sc> -o yaml | grep mountOptions— 检查 mountOptions 配置 - [ ]
journalctl -u kubelet --no-pager | grep MountVolume— 查 kubelet 的完整错误 - [ ]
mount | grep nfs— 节点上 NFS 挂载状态 - [ ]
rpcinfo -p <nfs-server> | grep nfs— Server 端支持的 NFS 版本 - [ ]
dmesg | grep NFS— 内核级 mount.nfs 协商日志 - [ ] 修改后进行验证:重建 Pod →
kubectl exec <pod> -- mount | grep nfs
金句
"NFS 挂载失败不是存储不可达——是协议版本号写错了一个数字,kubelet 和 NFS Server 之间差了一句 rpcinfo 的距离"
下篇预告
下篇我们聊 StatefulSet 有状态服务 Pod 启动顺序导致的故障——有状态的容器和普通容器有什么本质区别。
附:完整命令清单
# PV 挂载失败排查
kubectl describe pod <pod> | grep -A 10 Events # 看 MountVolume 失败原因
kubectl get pv <pv> -o yaml # 看 PV Source 和 VolumeHandle
kubectl get storageclass <sc> -o yaml # 看 mountOptions
kubectl get csidriver <driver> -o yaml # 看 CSI driver attachRequired
# Node 层日志
journalctl -u kubelet --no-pager | grep MountVolume # kubelet 的卷挂载日志
journalctl -u kubelet --no-pager | grep <volume-name> # 按卷名过滤
dmesg | grep NFS # 内核 NFS 协商日志
# 节点验证 mount 状态
mount | grep nfs # 查看 NFS 挂载
ls -la /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<vol>/mount # kubelet 挂载点
# CRI 层查看容器 mount 信息
crictl ps | grep <pod-name> # 找容器 ID
crictl inspect <container-id> | grep -A 20 mounts # 查看容器挂载映射
nsenter -t $(crictl inspect <id> | jq '.info.pid') -m -- mount | grep nfs # 进容器 netns 看 mount
# NFS Server 协议版本验证
rpcinfo -p <nfs-server> | grep nfs # 看 Server 支持的 NFS 版本
showmount -e <nfs-server> # 看 Server 导出的路径
# 修复命令
kubectl patch storageclass <sc> -p '{"mountOptions":["nfsvers=3"]}' --type=merge # 改 nfsvers
kubectl delete pod <pod> --force --grace-period=0 # 重建 Pod 触发重新挂载
📺 公众号「Ai拆代码的曹操」 🌟 知识星球「Ai拆代码的曹操」