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.nfsEPROTONOSUPPORT——NFS 协议版本协商失败。kubelet(K8s 节点代理,负责管理 Pod 和挂载卷)重试了 12 次,全失败。

Pod Events 显示 Protocol not supported

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

PV Bound 正常

分层:从 Pod 到 NFS Server,逐层查挂载链路

Pod 层:Events 解读

Events 已经给了方向——Protocol not supported。NFS 协议协商由 mount.nfs 客户端和 NFS Server 的 rpc.mountd 完成。客户端发起的 NFS 版本不在 Server 支持的范围内,Server 返回 NFS4ERR_MINOR_VERS_MISMATCHPROG_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 返回的原始错误:

kubelet 日志 + mount 验证

error -22EINVAL,表示 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。

StorageClass mountOptions + rpcinfo

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

挂载链路全景

NFS 挂载链路图

路径:🔍 下次 PV 挂载失败,先跑这套命令

  1. 看 Pod Eventskubectl describe pod <pod> | grep -A 10 Events,找 Protocol not supported / Permission denied / No such file or directory
  2. 查 StorageClasskubectl get sc <sc> -o yaml | grep mountOptions,看 nfsversvers
  3. 查 kubelet 日志journalctl -u kubelet --no-pager | grep MountVolume | grep <volume-name>,看 error 的 exit status 和 message
  4. 查节点 mount 状态mount | grep nfsls -la /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<volume-name>/mount
  5. 查 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

正确顺序:

  1. 先看 Pod Events——MountVolume.MountDevice failed 的 message 包含 root cause(Protocol not supported / Permission denied / No such file or directory)
  2. 看 StorageClass mountOptions——确认 nfsvers 或 vers 的值
  3. 查 kubelet 日志——CSI driver 返回的原始错误
  4. 查 NFS Server 的 rpcinfo——确认支持的协议版本
  5. 调整 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 versnfsvers 的别名 兼容不同发行版配置
soft,timeo=100,retrans=3 软挂载 + 超时配置 非关键业务

⚠️ 注意nfsversvers 是等价的,但不同发行版的手册页推荐不同。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拆代码的曹操」