在一个 NFS 导出上执行 juicefs format --storage nfs,本来应该得到一个挂载错误,进程却先 panic 了:
panic: runtime error: invalid memory address or nil pointer dereference
github.com/juicedata/juicefs/pkg/object.newNFSStore
pkg/object/nfs.go:469
同一台机器直接挂载这个导出时,内核协商成了 nfs4;显式指定 vers=3 则报 Protocol not supported。这看起来像一个问题,实际是两层:JuiceFS 有一个错误处理顺序的 bug;而它的 NFS object storage 客户端走的是 NFSv3 的 MOUNT 协议,不能因为内核能挂 NFSv4 就自动获得 v3 能力。
Phenomenon
JuiceFS 1.3.0 的 newNFSStore 路径大致是:
target, err := mount.Mount(path, auth.Auth())
target.Config.DirCount = 1 << 17
target.Config.MaxCount = 1 << 20
if err != nil { return nil, fmt.Errorf("unable to mount %s: %v", addr, err) }
mount.Mount 在 NFSv3 的 MNT 调用失败时会返回空的 target。两次读取 target.Config 已经解引用空指针,根本到不了 if err != nil。Go 不强制“必须先检查 err,才能读取同一调用返回的其他值”;这是一条需要主动遵守的资源有效性边界。
上游的 PR #7573 已合入主干,修复就是把错误检查移到解引用之前:
target, err := mount.Mount(path, auth.Auth())
if err != nil { return nil, fmt.Errorf("unable to mount %s: %v", addr, err) }
target.Config.DirCount = 1 << 17
target.Config.MaxCount = 1 << 20
补丁改变的是失败时的表现:从 panic 变成带有底层 MOUNT 错误的正常退出。它不会让一个不支持 NFSv3、或不允许当前客户端访问的导出忽然变得可用。
两种失败,不在同一层
| 现象 | 失败位置 | 含义 |
|---|---|---|
MNT3ERR_ACCES | MOUNT v3 过程已经被服务端执行 | 服务端支持 v3,但导出规则拒绝该客户端 |
rpc: PROG_MISMATCH | ONC RPC 的程序/版本分发 | 客户端请求 MOUNT program 100005、版本 3;服务端没有这个版本 |
mount.nfs: Protocol not supported | Linux 内核 NFS 客户端 | 内核强制使用 v3 时协商失败;这不是 JuiceFS 的原始错误文案 |
PROG_MISMATCH 先于 MNT3ERR_ACCES。前者说明 RPC 请求连 MOUNT v3 过程都没有进入;后者则说明版本已经匹配,服务端开始按 export 规则授权。因此,不能从 panic 本身推断是 export 权限、路径拼写、服务端未开启 v3,还是网络故障;先升级到包含该修复的版本,把真实错误露出来。
NFSv3:挂载不是 NFS 读写本身
NFSv3 由多个 ONC RPC 程序配合。挂载时,客户端先通过 rpcbind 找服务端口,再调用 MOUNT v3 的 MNT(路径) 获取文件句柄,随后才对 NFS v3 程序做 LOOKUP、READ、WRITE。文件锁由独立的 NLM 协议处理。
client
├─ rpcbind (100000, 通常是 111):找到服务端口
├─ mountd (100005 v3):MNT("/export/path") → file handle
├─ nfsd (100003 v3, 通常是 2049):LOOKUP / READ / WRITE
└─ NLM (100021):LOCK
NFSv3 规范明确把 MOUNT 作为辅助协议:它负责把远端目录树附着到本地文件系统,并按 export control 向受限客户端授予访问;NLM 则把有状态的文件锁单列为一个协议。RFC 1813 的介绍与附录 I、II 定义了这条边界。
JuiceFS 这里没有调用系统的 mount.nfs,而是直接通过 Go NFS 库拨 MOUNT 服务并调用 v3 MNT。所以系统命令不带 vers 时成功协商为 NFSv4,只能证明“内核客户端可以使用 v4”;不能证明 JuiceFS 要求的 MOUNT v3 存在。
NFSv4:把多条控制路径收进 2049
NFSv4 不再依赖单独的 MOUNT 协议来取得导出目录的文件句柄。客户端连接 NFS program 100003 的 v4 服务(默认 TCP 2049),通过 COMPOUND 请求在服务器提供的伪文件系统内走路径:
COMPOUND { PUTROOTFH; LOOKUP "export"; LOOKUP "path"; GETFH; OPEN / READ / LOCK }
LOOKUP、读写和锁没有消失,而是变成一个 COMPOUND 中的操作。NFSv4.1 规范规定 RPC 过程为 NULL 和 COMPOUND,并说明客户端不应再依赖 RPC binding protocol 发现 NFS 服务端口。RFC 8881 §2.9.3
| NFSv3 | NFSv4 | |
|---|---|---|
| 获取导出根 | MOUNT MNT(路径) 返回 file handle | PUTROOTFH + LOOKUP |
| 服务发现 | rpcbind、MOUNT 等多个程序 | NFS v4 固定服务路径,默认 2049 |
| 锁 | 独立 NLM | COMPOUND 内的 LOCK |
这不只是“端口更少”。NFSv4 的设计动机是降低防火墙配置复杂度、减少广域网往返,并把锁与恢复纳入同一协议状态模型;RFC 2624 分别讨论了这三点。
Resolution
- 升级到包含 PR #7573 的 JuiceFS 版本,或回合等价的空指针修复。验收条件是:NFSv3 MOUNT 失败时以非零退出返回
unable to mount ...,而不是 panic。 - 在运行
format的客户端检查 v3:showmount -e <nfs-server>、rpcinfo -p <nfs-server>、mount -t nfs -o vers=3,tcp <nfs-server>:/<export-path> /mnt/nfs-v3-test。 - 在服务端确认
cat /proc/fs/nfsd/versions输出包含+3,并用exportfs -v确认导出规则。 - 如果服务端只提供 NFSv4,当前这条 JuiceFS NFS storage 路径并不兼容;需要提供 NFSv3 export,或选择与服务端协议能力匹配的对象存储/网关方案。
Debugging notes
- panic 是程序的错误处理 bug,不是协议错误码。
PROG_MISMATCH是 RPC 的程序版本不匹配;MNT3ERR_ACCES才是 MOUNT 过程内的访问拒绝。mount.nfs的报错是 Linux 内核客户端翻译后的文案,不能逐字套到 JuiceFS 的 Go 客户端上。- 升级修复 panic 是为了恢复可诊断性;协议和授权问题仍要通过 v3 MOUNT 的真实结果分别确认。
把这几层拆开之后,排查路径就很短:先让程序不崩,再看 MOUNT v3 是否存在,最后看 export 是否允许当前客户端。