NFS 原理与报文 — ONC RPC、文件句柄与 v3/v4 状态模型

NFS 工作原理:VFS 转发、ONC RPC/XDR 编码、文件句柄、v3 二十二个过程、v4 COMPOUND 与租约委托、挂载时序 / 应用层 / TCP 2049 / RFC 7530

先建立直觉

先别管报文格式和字段偏移,我们只要回答一个问题:你敲下 cat /mnt/data/report.txt 时,背后到底发生了什么?

  • 这台机器上并没有 report.txt 这个真实文件,/mnt/data 是一棵"虚拟书架",实实在在的数据在另一台机器的磁盘里。
  • 你用的命令(cat、vim、cp)一行代码都没改,它根本不知道自己在访问网络。是内核的 VFS 层发现"这个路径属于 NFS 挂载点",悄悄把 read() 这种系统调用翻译成了一次网络请求。
  • 但这不是一个"把整个文件传过来"的协议(那像 FTP)。NFS 传的是一次文件系统操作:READ(offset=0, count=4096)、WRITE(offset=4096, ...) 这种粒度,和你在本地读写文件是一一对应的。
  • 既然是网络请求,就得有人负责"寻址、打包、编码、传输"。NFS 自己不干这些脏活,它站在 ONC RPC(负责远程调用)和 XDR(负责统一编码)这两层肩膀上。所以你抓包时会看到双层结构:外层 RPC、内层 NFS。

一句话:NFS = 把本地文件操作原样"转手"成网络请求。 下文的所有细节,都是在回答"怎么转手、怎么打包、怎么保证不出错"。

它解决什么问题

在 NFS 之前,多台机器之间"共享一份文件"几乎没有优雅解法:文件锁在某台机器磁盘里,或靠 FTP 来回拷——拷来拷去出现 N 个版本、改了不同步、每台机器都重复占磁盘。

NFS(1984 年,Sun)的关键思路是:不发明新的文件访问方式,而是把已有的 Unix 文件系统调用透明地转发到网络上。 这样带来的几个子问题,以及 NFS 的解法:

问题NFS 的解法
应用要改代码才能访问远程文件VFS 层拦截:应用照常调 open()/read()/write(),内核判断这是 NFS 挂载点后转成 RPC
不同机器的字节序/数据表示不同XDR(External Data Representation,外部数据表示,RFC 4506)统一编码,大端序 + 4 字节对齐
网络调用怎么写才像本地函数ONC RPC(RFC 5531):定义程序号/版本号/过程号三元组,客户端像调本地函数一样发请求
服务器崩溃重启后客户端怎么办v2/v3 无状态设计:服务器不记住任何客户端信息,每个请求自带全部上下文(文件句柄 + 偏移量),重启后客户端重试即可恢复
磁盘空间浪费在每台机器集中存储,统一备份、统一扩容

工作流程(简化版)

一次最典型的"读文件"走下来,大致是 5 步(以 NFSv3 为例;v4 会把其中多步打包进一次 COMPOUND):

  1. 挂载(Mount):客户端先问 rpcbind(111 端口)“mountd 在哪”,再连 mountd 拿到根文件句柄,VFS 树上长出 /mnt/data。
  2. 解析路径(LOOKUP):想读 /mnt/data/app.log,客户端拿根句柄逐级 LOOKUP("data") → LOOKUP("app.log"),换到目标文件的句柄。
  3. 权限确认(ACCESS / GETATTR):用句柄问服务器"我能不能读、“文件多大、mtime 多少”。
  4. 读写数据(READ / WRITE):带上句柄 + 偏移量 + 长度,一次读/写一块(通常 1MB)。写可以"先写内存"(UNSTABLE)最后统一 COMMIT 落盘。
  5. 关 / 续租 / 刷回:v3 无状态,请求自带上下文,服务器宕机客户端重试即可;v4 有状态,靠租约(默认 90s)续命、靠 Delegation 减少来回确认。

是不是发现一个反复出现的东西——文件句柄?它才是 NFS 能"无路径访问"的真正钥匙,下文会专门拆开讲。

报文 / 头部长什么样

NFS 的"报文"是套娃结构:最外层是 ONC RPC 头和 XDR 编码,里面才是 NFS 过程参数。先建立"套娃"的层次感,再看各层字段。

1. 一次调用的完整旅程(VFS → RPC → XDR)

下面这张图把"从 read() 到网络包"的每一层都画出来了,重点是看数据怎么一层层被包进去:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
应用进程
   │ read(fd, buf, 4096) ← 普通系统调用,应用无感知
   ▼
┌────────────────────────────────────────┐
│ 内核 VFS(虚拟文件系统层) │ 判断该 inode 属于 nfs 文件系统
└────────────────────────────────────────┘
   ▼
┌────────────────────────────────────────┐
│ NFS Client(内核模块 nfs / nfsv4) │ 查页缓存 → 未命中 → 构造 READ 请求
│ { filehandle, offset, count } │
└────────────────────────────────────────┘
   ▼
┌────────────────────────────────────────┐
│ ONC RPC 层(sunrpc) │ 加 RPC 头:xid / prog=100003 /
│ │ vers=3或4 / proc=READ / 认证凭据
└────────────────────────────────────────┘
   ▼
┌────────────────────────────────────────┐
│ XDR 编码 │ 大端序、4 字节对齐序列化
└────────────────────────────────────────┘
   ▼
   TCP(记录标记 Record Marking)→ IP → 网卡 ── 网络 ──► 服务端逆向解开
                                                          ▼
                                            nfsd 内核线程 → VFS → 本地 ext4/xfs

关键点:NFS 不是"文件传输协议"(不像 FTP 整文件搬运),而是远程文件系统调用协议——传的是 LOOKUP、GETATTR、READ(offset, count) 这样的操作,粒度是"一次系统调用"。

2. ONC RPC 三层定位:三元组

每个 RPC 请求用 {程序号, 版本号, 过程号} 唯一定位要调用的函数。下表是 NFS 世界里常见的几个"程序号"——记住 100003(NFS)和 100005(MOUNT)最常用:

服务程序号版本说明
portmapper / rpcbind1000002/3/4端口映射,固定 111
NFS1000032/3/4主服务,固定 2049
MOUNT1000051/3v3 挂载协议,动态端口
NLM(锁管理)1000211/3/4v3 文件锁,动态端口
NSM / statd(状态监视)1000241崩溃恢复通知,动态端口
rquotad(配额)1000111/2动态端口

RPC 消息头字段——下面是每个 RPC 请求外层都带的信息,xid 是配对和去重的关键:

字段含义
xidTransaction ID,请求/响应配对(也是重传检测和 DRC 去重缓存的键)
msg_type0 = CALL(请求),1 = REPLY(响应)
rpcversRPC 版本,固定 2
prog / vers / proc程序号 / 版本号 / 过程号
cred(凭据)认证信息,见下表
verf(验证器)校验器,AUTH_SYS 时为空

认证风味(Authentication Flavor)——NFS 的安全强薄弱,基本看这一项:

flavor值含义
AUTH_NONE0无认证
AUTH_SYS(AUTH_UNIX)1客户端自报 UID/GID/辅助组,服务器无条件信任 —— 这是 NFS 最大的安全弱点
RPCSEC_GSS6Kerberos 等强认证(sec=krb5/krb5i/krb5p)

AUTH_SYS 的本质:报文里就是一串明文的 uid=1001, gid=1001, gids=[10,100]。攻击者只要能连上 2049 端口,在自己机器上 useradd -u 0 就能以 root 身份访问(除非有 root_squash)。因此 NFS 必须靠网络隔离 + exports 白名单 + Kerberos 三重防护。

3. XDR 编码规则

XDR 是 RPC 的"统一语言",解决不同机器字节序/对齐差异。下面几条规则,最要紧的是"大端序 + 4 字节对齐":

规则说明
字节序大端序(Big-Endian),与网络字节序一致
对齐所有数据 4 字节对齐,不足补 0
整数int / unsigned int 4 字节;hyper / unsigned hyper 8 字节(v3 起文件大小用 64 位,突破 v2 的 2GB 限制)
变长数据前置 4 字节长度,后跟内容 + 补齐字节
字符串同变长数据(不含结尾 \0)
可选值前置 4 字节布尔(0 = 无,1 = 有)

4. 文件句柄(File Handle):NFS 的"远程 inode"

客户端不能用路径名读写文件。流程一定是"先拿句柄、再逐级 LOOKUP"——下面把 /data/logs/app.log 的解析过程画全:

1
2
3
4
5
6
路径 /data/logs/app.log 的访问过程:
  ① 拿到根句柄(v3: MOUNT 协议返回;v4: PUTROOTFH 操作)
  ② LOOKUP(根句柄, "data") → 得到 /data 的句柄
  ③ LOOKUP(/data 句柄, "logs") → 得到 /data/logs 的句柄
  ④ LOOKUP(/data/logs 句柄, "app.log") → 得到文件句柄
  ⑤ READ(文件句柄, offset=0, count=4096)

句柄特性——它"不透明、有长度上限、会失效",这三点是 NFS 很多行为的根源:

特性说明
不透明(Opaque)对客户端是一串无意义的字节,只能原样回传。服务器内部通常编码 {fsid, inode号, generation号}
长度v2 固定 32 字节;v3 最长 64 字节;v4 最长 128 字节
持久性设计上应跨服务器重启保持有效(这是 v3 无状态恢复的基础)
失效文件被删除、文件系统重新导出、fsid 变化 → 句柄失效 → 返回 ESTALE(Stale file handle)

Stale file handle 的本质:客户端手里的句柄指向的 inode 已经不存在(文件被别人删了、服务端 remount 了、fsid 变了)。这是 NFS 最著名的错误之一。

5. v3 无状态 vs v4 有状态

这是理解 NFS 演进的主线。一张表看清两个版本在"服务器记不记客户端"上的根本差异:

维度NFSv3(无状态)NFSv4(有状态)
服务器是否记住客户端不记住。每个请求自带全部上下文记住:ClientID、OpenState、LockState
打开文件没有 OPEN 过程!客户端本地记录"打开",直接用句柄 READ/WRITE有 OPEN/CLOSE,服务器维护 stateid
文件锁靠外挂 NLM + NSM 协议整合进主协议(LOCK/LOCKU/LOCKT)
服务器崩溃恢复客户端无限重试(hard 挂载),服务器起来后自动继续Grace Period(宽限期,默认 90 秒):只接受锁恢复请求,不接受新锁
客户端崩溃服务器无需清理(本来就没状态)租约(Lease)到期(默认 90 秒)自动回收状态
端口2049 + 111 + mountd/statd/lockd 动态端口只有 2049
挂载需要 MOUNT 协议伪文件系统 + PUTROOTFH
权限模型数字 UID/GID字符串 user@domain(需 idmapd 或 AUTH_SYS 数字直传)
ACL无(只有 POSIX mode 位)支持 NFSv4 ACL(类 Windows ACL)
缓存一致性弱(close-to-open 语义 + 属性缓存超时)强(Delegation 委托机制)

6. 报文头部结构(RPC 层 + 各版本过程)

下面是实际线序上每个 NFS 报文外层长什么样(TCP 之上先有 4 字节 Record Marking):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
TCP 之上先有 4 字节 Record Marking:
┌─┬───────────────────────────────┐
│F│ fragment length (31 bit) │ F=1 表示最后一个分片
└─┴───────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ RPC CALL │
│ xid(4) │ msg_type=0 │ rpcvers=2 │ prog=100003 │
│ vers=3或4 │ proc=<过程号> │ cred{flavor,body} │ verf │
├──────────────────────────────────────────────────────┤
│ NFS 过程参数(XDR 编码) │
└──────────────────────────────────────────────────────┘

NFSv3 主要过程(RFC 1813,共 22 个)——这是 v3 的"能力清单",过程号在抓包过滤和排错时极常用(如 nfs.procedure_v3 == 6 就是 READ):

Proc名称作用关键参数
0NULL心跳/探活无
1GETATTR取文件属性(大小、mtime、mode)fh
2SETATTR设属性(chmod/chown/truncate)fh, sattr
3LOOKUP在目录中按名字查文件 → 返回句柄dir_fh, name
4ACCESS检查权限(服务端判定,避免客户端 UID 误判)fh, access_mask
5READLINK读符号链接目标fh
6READ读数据fh, offset(64bit), count
7WRITE写数据fh, offset, count, stable, data
8CREATE创建普通文件dir_fh, name, how
9MKDIR创建目录dir_fh, name, attr
10SYMLINK创建符号链接dir_fh, name, target
11MKNOD创建设备/FIFO 节点dir_fh, name, type
12REMOVE删文件dir_fh, name
13RMDIR删空目录dir_fh, name
14RENAME改名/移动from{fh,name}, to{fh,name}
15LINK创建硬链接fh, dir_fh, name
16READDIR列目录(只返回名字+cookie)dir_fh, cookie, count
17READDIRPLUS列目录同时返回每项属性和句柄(ls -l 快得多)dir_fh, cookie, dircount, maxcount
18FSSTAT文件系统空间统计(df 用)fh
19FSINFO文件系统能力(最大读写块、是否支持链接)fh
20PATHCONFPOSIX pathconf 信息(最大文件名长度等)fh
21COMMIT把之前 UNSTABLE 的写落盘fh, offset, count

WRITE 的三种稳定性(stable_how)—— 性能与安全的核心权衡:

值语义服务器行为
UNSTABLE(0)可以只写内存立即返回,最快。数据可能还在服务器 page cache,掉电即丢
DATA_SYNC(1)数据必须落盘(元数据可后延)中等
FILE_SYNC(2)数据+元数据全部落盘最慢最安全

配套的 COMMIT 过程:客户端先用 UNSTABLE 高速批量写,最后发一个 COMMIT 让服务器 flush。服务器返回 write verifier(8 字节);若两次 verifier 不同,说明服务器重启过、缓存中的数据丢了,客户端必须重发全部未确认的写。这是 NFS 兼顾性能与可靠性的精妙设计。

NFSv4 的 COMPOUND(RFC 7530)——v4 只有两个 RPC 过程:NULL(0) 和 COMPOUND(1)。所有实际操作都是 COMPOUND 里的"操作数组":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
COMPOUND {
    tag: "read file"
    minorversion: 0 ← 0=v4.0 1=v4.1 2=v4.2
    argarray: [
        PUTFH(dir_handle) ← 设置"当前文件句柄"(Current FH)
        LOOKUP("app.log") ← Current FH 变为该文件
        GETATTR(size|mtime)
        READ(stateid, offset=0, count=65536)
    ]
}

核心概念:Current Filehandle(当前文件句柄) —— 像一个游标,PUTFH/LOOKUP/PUTROOTFH 会改变它,后续操作隐式作用于它。这让一次 RPC 能完成"查找+读取"的组合。

COMPOUND 的执行语义:顺序执行,遇到第一个错误就停止,返回已执行部分的结果。不是事务,没有回滚。

v4 主要操作(部分):

Op名称作用
3ACCESS权限检查
4CLOSE关闭(释放 stateid)
5COMMIT落盘
6CREATE创建非普通文件
9GETATTR取属性
10GETFH取当前句柄
12LINK硬链接
13LOCK / 14 LOCKT / 15 LOCKU加锁 / 测试锁 / 解锁(已整合进主协议)
15LOOKUP查找
18OPEN打开文件,返回 stateid,可能获得 Delegation
22PUTFH设置当前句柄
24PUTROOTFH设置为伪文件系统根(v4 挂载起点)
25READ读
26READDIR列目录
31RENEW续租(v4.0;v4.1 用 SEQUENCE 隐式续租)
34SETATTR设属性
35SETCLIENTID客户端注册(v4.0)
38WRITE写
42EXCHANGE_IDv4.1 的客户端注册
43CREATE_SESSIONv4.1 建会话
53SEQUENCEv4.1 每个 COMPOUND 的第一个操作,提供 EOS 精确一次语义

交互时序

下面这张图把 v3 挂载/读/写 和 v4 挂载/读/续租/委托回调 的完整交互都画了出来。一句话看懂这张图:v3 为了一次挂载要"挨个问"rpcbind、mountd、nfsd 三个服务,而 v4 只用 2049 一个端口,把"找路径、读文件、续租、收回委托"全塞进 COMPOUND 往返里;写文件则是 v3 用 UNSTABLE 高速写 + 最后 COMMIT 落盘,靠 write verifier 防丢数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
sequenceDiagram
    autonumber
    participant C as NFS Client<br/>(Linux 内核)
    participant P as rpcbind :111
    participant M as mountd<br/>(动态端口)
    participant S as nfsd :2049

    rect rgb(240, 245, 255)
    Note over C,S: ═══ NFSv3 挂载流程(需要 3 个服务)═══
    C->>P: GETPORT(prog=100005 MOUNT, vers=3, proto=tcp)
    P-->>C: port = 20048
    C->>M: MOUNT MNT("/export/data")
    M-->>C: status=OK, root filehandle(64B), auth_flavors=[AUTH_SYS]
    Note right of M: mountd 在此检查 /etc/exports<br/>的 IP 白名单,不通过返回 EACCES
    C->>P: GETPORT(prog=100003 NFS, vers=3)
    P-->>C: port = 2049
    C->>S: NFS FSINFO(root_fh)
    S-->>C: 最大读写块大小、支持的特性
    Note over C: 挂载完成,VFS 树上出现 /mnt/data
    end

    rect rgb(245, 255, 245)
    Note over C,S: ═══ NFSv3 读文件 /mnt/data/app.log ═══
    C->>S: LOOKUP(root_fh, "app.log")
    S-->>C: file_fh + post_op_attr
    C->>S: ACCESS(file_fh, READ)
    S-->>C: 允许
    C->>S: READ(file_fh, offset=0, count=131072)
    S-->>C: data + eof=false + attr
    C->>S: READ(file_fh, offset=131072, count=131072)
    S-->>C: data + eof=true
    end

    rect rgb(255, 250, 240)
    Note over C,S: ═══ NFSv3 写文件(UNSTABLE + COMMIT)═══
    C->>S: WRITE(fh, off=0, len=64K, stable=UNSTABLE)
    S-->>C: count=64K, committed=UNSTABLE, verifier=0xA1B2...
    C->>S: WRITE(fh, off=64K, len=64K, stable=UNSTABLE)
    S-->>C: count=64K, committed=UNSTABLE, verifier=0xA1B2...
    C->>S: COMMIT(fh, offset=0, count=128K)
    S-->>C: verifier=0xA1B2... (与之前相同 → 数据安全落盘)
    Note left of C: 若 verifier 变了 = 服务器重启过<br/>客户端必须重发全部写
    end

    rect rgb(255, 245, 250)
    Note over C,S: ═══ NFSv4 —— 只用 2049,无 rpcbind/mountd ═══
    C->>S: COMPOUND[ SETCLIENTID(client_id) ]
    S-->>C: clientid + confirm verifier
    C->>S: COMPOUND[ SETCLIENTID_CONFIRM ]
    S-->>C: OK(租约开始计时,默认 90s)

    C->>S: COMPOUND[ PUTROOTFH, LOOKUP("export"), LOOKUP("data"), GETFH, GETATTR ]
    S-->>C: 一次往返完成挂载点定位 ✅

    C->>S: COMPOUND[ PUTFH, OPEN("app.log", READ), GETFH, READ(off=0) ]
    S-->>C: stateid + delegation(READ) + 数据
    Note right of S: 授予读委托后,客户端可本地缓存<br/>无需每次 GETATTR 校验

    C->>S: COMPOUND[ SEQUENCE, RENEW ] (周期性续租)
    S-->>C: OK

    Note over S: 另一客户端要写此文件时
    S->>C: CB_RECALL(回调,收回委托)
    C->>S: DELEGRETURN(刷回脏数据后归还)

    C->>S: COMPOUND[ PUTFH, CLOSE(stateid) ]
    S-->>C: OK
    end

关键机制 / 变体

下面 6 个机制,解释了 NFS"为什么有时慢、有时卡死、有时数据对不上"。每条先给一句话定位,再看细节。

1. 缓存一致性:Close-to-Open(CTO)语义

(一句话:NFS 不保证实时一致,只保证"你关了文件我再打开时能看到你的完整修改"。)

1
2
3
4
客户端 A:write() ... close() ← close 时把脏页全部 flush 到服务器
客户端 B:open() ... read() ← open 时用 GETATTR 校验 mtime/size,失效则丢弃缓存
保证:B 在 A close 之后 open,一定能读到 A 的完整修改
不保证:A 还没 close 时 B 读到什么(可能是旧数据、可能是半截)

属性缓存超时(这就是"改了文件另一台机器要等几秒才看到"的原因):

挂载参数默认值含义
acregmin / acregmax3s / 60s普通文件属性缓存的最小/最大时长
acdirmin / acdirmax30s / 60s目录属性缓存时长
actimeo=N—一次性把上面四个都设为 N
noac—关闭属性缓存(并强制同步写,性能极差,仅特殊场景用)

重要:NFS 上不能可靠地运行需要严格并发一致性的程序——SQLite/MySQL 数据文件放 NFS 是经典的踩坑(锁语义 + 缓存都不可靠)。

2. Delegation(委托)—— v4 的一致性利器

(一句话:服务器把文件的读写控制权"下放"给客户端,单客户端独占时大幅减少来回确认。)

类型客户端可以做什么
READ Delegation本地缓存读,不必反复 GETATTR 校验。服务器保证期间无人写
WRITE Delegation本地缓存写和加锁,批量写完再刷。服务器保证期间独占

当第二个客户端要访问该文件时,服务器发起 CB_RECALL 回调(服务器主动连客户端,v4.0 需要客户端开放回调端口;v4.1 用会话反向通道解决了 NAT 穿透问题),客户端刷回脏数据后 DELEGRETURN。

收益:单客户端独占访问的场景(最常见),网络往返次数下降一个数量级。

3. hard vs soft 挂载 —— 最重要的挂载决策

(一句话:hard 保数据但可能卡死进程,soft 不卡死但可能悄悄坏数据——默认且推荐 hard。)

参数服务器无响应时的行为后果
hard(默认,推荐)无限重试,进程在 I/O 上永久阻塞进程进入 D 状态(不可中断睡眠),kill -9 都杀不掉,df/ls 全部 hang 死。但数据绝不丢失
soft重试 retrans 次后返回 EIO进程能继续跑,但应用可能拿到不完整数据 → 静默数据损坏
intr允许信号中断(Linux 2.6.25 起已废弃/无效)现代内核用 hard + Ctrl-C 已可中断部分操作

结论:读写数据一律用 hard;只有对纯只读、可容忍失败的场景(如只读的配置分发)才考虑 soft,且必须配 timeo 和 retrans。要避免 hang 死,正确做法是配 _netdev + 网络监控 + autofs 自动卸载,而不是改成 soft。

4. root_squash:UID 映射和安全

(一句话:root_squash 把客户端的 root 贬成 nobody,防止它借 NFS 在服务端以 root 身份乱来。)

exports 选项行为
root_squash(默认)客户端 UID 0(root) 被映射为 nobody/nfsnobody(65534)。客户端 root 无法以 root 身份操作服务端文件
no_root_squash客户端 root = 服务端 root。极度危险:任何能挂载的机器上的 root 就是服务器的 root,可写 SUID 二进制提权
all_squash所有 UID 都映射为 anonymous,适合公开只读共享
anonuid=N / anongid=N指定 squash 后的目标 UID/GID

UID 一致性问题:NFSv3 传的是数字 UID。服务器上 zhangsan 是 1001,客户端上 zhangsan 是 1050 —— 那么客户端看到的文件属主会是"1001"这个陌生数字,且权限判断全错。解法:用 LDAP/NIS 统一 UID,或用 NFSv4 + idmapd(传 user@domain 字符串)。

5. NFSv4 的 Grace Period 与租约

(一句话:v4 是"有状态"的,靠 90 秒租约续命、靠 90 秒宽限期在重启后安全恢复锁状态。)

机制说明
Lease(租约)客户端持有的状态(打开的文件、锁)有有效期,默认 90 秒。客户端必须周期性 RENEW(v4.1 靠 SEQUENCE 隐式续租)。租约过期 → 服务器回收所有状态
Grace Period(宽限期)服务器重启后进入宽限期(默认 90 秒),此期间只接受老客户端的状态恢复请求(RECLAIM),拒绝新的 OPEN/LOCK。防止 A 崩溃期间 B 抢走了 A 的锁
调整/proc/fs/nfsd/nfsv4leasetime、/proc/fs/nfsd/nfsv4gracetime

运维现象:NFSv4 服务器重启后,客户端会 hang 住约 90 秒才恢复——这就是 Grace Period 在起作用,是正常行为,不要慌。

6. DRC:重复请求缓存

(一句话:网络会丢响应导致客户端重传,服务器用 DRC 把"已经做过的请求"缓存下来直接回放,避免 REMOVE/RENAME 这类非幂等操作被重复执行。)

UDP/TCP 都可能发生"服务器执行了操作但响应丢了",客户端重传会导致非幂等操作重复执行(如 REMOVE 第二次返回 ENOENT、RENAME 出错)。

DRC(Duplicate Request Cache,重复请求缓存):服务器按 {xid, 客户端地址, 过程号} 缓存最近的响应,重传命中缓存则直接回放上次结果,不重复执行。

v4.1 用 Session + SEQUENCE 操作(带 slot id + sequence id)提供了协议级的 EOS(Exactly Once Semantics,精确一次语义),彻底解决了这个问题——这是 v4.1 相对 v4.0 最重要的改进之一。

常见误区

  1. “NFS 是文件传输协议,像 FTP 一样传整个文件” —— 错。NFS 传的是一次文件系统操作(READ(offset, count)、WRITE(...)),粒度是系统调用。这是它"对应用透明"的根本原因。
  2. “用路径名就能直接读写远程文件” —— 错。NFS 靠不透明的文件句柄寻址,客户端必须先 LOOKUP 逐级换成句柄。路径名只在 LOOKUP 的参数里出现,不能塞进 READ/WRITE。
  3. “NFS 多客户端共享文件是实时一致的” —— 错。v3 只有 close-to-open 弱语义,属性缓存默认 60 秒;哪怕 v4 有 Delegation,也不是"任意时刻强一致"。把数据库/SQLite 直接放 NFS 是经典踩坑。
  4. "soft 挂载能避免 hang 死,比 hard 好" —— 错。soft 拿到 EIO 后应用可能吞下不完整数据造成静默损坏;hard 虽可能让进程进 D 状态,但数据绝不丢。正确解法是 autofs/automount + 监控,不是改 soft。
  5. “v4 还是要开 111 和 mountd 端口” —— 错。v4 只需 2049 一个端口,rpcbind/mountd/NLM 全废弃(锁整合进主协议、挂载用 PUTROOTFH)。这是 v4 最好部署的原因。

速记口诀

  • “无句柄,不读写”:NFS 一切寻址靠 File Handle,路径只用来 LOOKUP 换句柄。
  • “v3 三件套,v4 一端口”:v3 要 rpcbind(111)+mountd+statd/lockd 一堆动态端口;v4 只要 2049。
  • “hard 保数据、soft 坏数据”:怕 hang 就上 autofs,别拿 soft 赌数据。
  • “UNSTABLE 快写,COMMIT 落盘,verifier 变就重来”:写性能与可靠性的平衡点。
  • “squash 降级、krb5 加密”:root_squash 是底线,sec=krb5p 才是真安全;AUTH_SYS 明文 UID 谁都能冒充。

知识框架

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
mindmap
  root((NFS))
    分层基础
      VFS 透明转发
      ONC RPC RFC5531
        xid 配对
        prog vers proc 三元组
        AUTH_NONE AUTH_SYS RPCSEC_GSS
      XDR RFC4506
        大端序
        4 字节对齐
        变长前置长度
    核心抽象
      文件句柄 File Handle
        不透明字节串
        v3 64B v4 128B
        失效即 ESTALE
      LOOKUP 逐级解析路径
      无路径名直接访问
    版本演进
      v2 RFC1094
        32位偏移 2GB 上限
        仅 UDP
      v3 RFC1813
        64位文件大小
        READDIRPLUS
        UNSTABLE 写 加 COMMIT
        无状态 靠 NLM NSM 加锁
        依赖 rpcbind 111 动态端口
      v4.0 RFC7530
        有状态 OPEN CLOSE
        COMPOUND 复合操作
        单端口 2049
        伪文件系统 PUTROOTFH
        Delegation 委托
        租约 90 秒
        NFSv4 ACL
        字符串 UID idmapd
      v4.1 RFC8881
        Session 与 SEQUENCE
        EOS 精确一次语义
        反向通道解决 NAT
        pNFS 并行访问
      v4.2 RFC7862
        服务端拷贝 COPY
        稀疏文件 空洞
        应用数据块
    关键机制
      缓存一致性
        close-to-open 语义
        属性缓存 acregmin acregmax
        Delegation 与 CB_RECALL
      写语义
        UNSTABLE DATA_SYNC FILE_SYNC
        COMMIT 与 write verifier
      崩溃恢复
        v3 无状态自然恢复
        v4 Grace Period 宽限期
        DRC 重复请求缓存
      挂载语义
        hard 无限重试 D 状态
        soft 返回 EIO 有数据风险
        rsize wsize timeo retrans
    安全
      AUTH_SYS 自报 UID 极弱
      root_squash all_squash
      no_root_squash 高危
      exports IP 白名单
      Kerberos sec krb5 krb5i krb5p
      UID 映射靠 LDAP NIS idmapd