JuiceFS format 为什么在 NFSv3 失败时 panic:从一次空指针到 NFSv3/v4

在一个 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_ACCESMOUNT v3 过程已经被服务端执行服务端支持 v3,但导出规则拒绝该客户端
rpc: PROG_MISMATCHONC RPC 的程序/版本分发客户端请求 MOUNT program 100005、版本 3;服务端没有这个版本
mount.nfs: Protocol not supportedLinux 内核 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 程序做 LOOKUPREADWRITE。文件锁由独立的 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 过程为 NULLCOMPOUND,并说明客户端不应再依赖 RPC binding protocol 发现 NFS 服务端口。RFC 8881 §2.9.3

NFSv3NFSv4
获取导出根MOUNT MNT(路径) 返回 file handlePUTROOTFH + LOOKUP
服务发现rpcbind、MOUNT 等多个程序NFS v4 固定服务路径,默认 2049
独立 NLMCOMPOUND 内的 LOCK

这不只是“端口更少”。NFSv4 的设计动机是降低防火墙配置复杂度、减少广域网往返,并把锁与恢复纳入同一协议状态模型;RFC 2624 分别讨论了这三点。

Resolution

  1. 升级到包含 PR #7573 的 JuiceFS 版本,或回合等价的空指针修复。验收条件是:NFSv3 MOUNT 失败时以非零退出返回 unable to mount ...,而不是 panic。
  2. 在运行 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
  3. 在服务端确认 cat /proc/fs/nfsd/versions 输出包含 +3,并用 exportfs -v 确认导出规则。
  4. 如果服务端只提供 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 是否允许当前客户端。