乐于分享
好东西不私藏

一篇给软件工程师和测试工程师的 NFSv3 协议文章

一篇给软件工程师和测试工程师的 NFSv3 协议文章

重点解读 RFC 1813 的关键行为:stateless、file handle、ACCESS、WCC、READDIRPLUS、WRITE/COMMIT、MOUNT/NLM

读者

建议读法

测试工程师

先看 WRITE/COMMIT、READDIRPLUS、WCC 和最后的场景表,把协议行为翻译成测试点。

开发工程师

重点看 file handle、stateless、ACCESS、缓存和错误语义,理解客户端为什么这样实现。

排障人员

先看现象到协议的映射,再按命令块抓取 mount、nfsstat、rpcinfo 和 pcap 证据。

本文边界

本文以 RFC 1813 为事实主线,讨论 NFSv3 协议本身。NFSv4 的 OPEN/CLOSE、delegation、lease、grace period 不属于 NFSv3 主协议;NFSv3 的锁通常落在 NLM/NSM 这些配套协议里。

1. NFSv3 到底是什么:不是远程 open,而是远程对象操作

很多人第一次接触 NFS,会把它想成“把远端目录挂到本地,然后像本地文件一样 open/read/write”。这个比喻在用户体验上没错,但对测试和排障来说太粗。NFSv3 更准确的心智模型是:客户端把本地 VFS 操作翻译成一组远程过程调用,服务端按 file handle 找到对象,执行 GETATTR、LOOKUP、READ、WRITE、COMMIT、READDIR 等过程,再返回结果和属性。

这也解释了很多看似奇怪的现象:路径为什么会被拆成多次 LOOKUP;为什么没有 NFSv4 那样的 OPEN/CLOSE 状态;为什么权限预检查不能保证下一次写一定成功;为什么大目录 ls -l 和普通 ls 的 RPC 形态不同;为什么同样是写成功,性能和持久性含义还要看 stable_how 与 COMMIT。

图 1:NFSv3 的基本分层和配套协议边界。

2. RFC 短摘地图:先抓住协议骨架

下面引用极短原文短摘和章节位置, 要记住这些词背后的行为约束:客户端如何重试,服务端何时能返回,缓存什么时候应该失效。

RFC 短摘

位置

工程解读

`stateless server`

1.6 Philosophy

NFSv3 服务端正确性不依赖客户端会话状态。服务端重启后,客户端主要靠超时和重试继续推进。

`file handle`

2.6 Basic Data Types

客户端拿到的是不透明对象引用,不应该解析内容;失效时典型错误是 NFS3ERR_STALE。

`ACCESS`

3.3.4

权限判断应由服务端按请求凭据执行,客户端不能只看 uid/gid/mode 位就下结论。

`weak cache consistency`

2.6 Basic Data Types

WCC 通过 before/after 属性帮助缓存判断,但它不是强一致性协议。

`safe asynchronous writes`

1.6 / 3.3.7

`stable storage`

4.8

返回成功意味着数据到达能抗常见掉电、重启、硬件故障的持久层;普通易失缓存不算。

`COMMIT`

3.3.21

客户端把之前 UNSTABLE 写入的数据刷到服务端稳定存储,并用 verifier 判断是否需要重传。

`cookie verifier`

3.3.16

目录遍历续读不仅靠 cookie,还要靠 verifier 判断目录组织是否发生不可安全续读的变化。

`READDIRPLUS`

3.3.17

目录项同时带属性和 file handle,减少后续 LOOKUP/GETATTR,但响应更大。

`duplicate request cache`

4.5

服务端用短期记忆降低重传非幂等请求造成的破坏,但这不是所有故障模式下的保证。

3. Stateless:服务端无会话,不代表没有风险

NFSv3 的一个核心设计是服务端不依赖客户端会话状态才能正确工作。这样做的好处很直接:服务端崩溃重启后,客户端不需要重新建立复杂会话,通常会超时、重传,直到服务端重新响应。对于读请求、GETATTR 这类幂等或近似幂等操作,这个模型很优雅。

但 stateless 并不等于“无状态系统”。服务端当然有持久状态:文件数据、目录、属性、稳定存储;也可能有缓存、预读、duplicate request cache。区别在于:这些缓存丢了不应该破坏协议正确性。真正麻烦的是非幂等请求的重传,例如 CREATE、REMOVE、RENAME。客户端没有收到回复时,它无法仅凭网络层判断服务端到底有没有执行过。

例子:REMOVE 的重传

客户端发送 REMOVE 后网络丢包。若服务端已经删除文件但回复丢了,客户端重传时可能得到对象不存在。duplicate request cache 能减少这类误判,但如果服务端重启、缓存丢失或缓存项被挤掉,它不能给出绝对保证。

4. File handle:NFSv3 世界里的对象身份证

NFSv3 不把完整路径放进每个操作里,而是先通过 MOUNT 和 LOOKUP 拿到 file handle,后续 READ、WRITE、GETATTR、SETATTR 等过程都围绕 file handle 执行。file handle 对客户端是不透明的:客户端可以保存、比较同一服务端返回的两个 handle 是否逐字节相等,但不能解析里面的字段。

这点对排障非常关键。所谓 stale file handle,通常不是客户端“路径字符串错了”,而是客户端持有的对象引用在服务端已经无法继续代表原对象:对象被删除、导出关系变化、文件系统重建或服务端撤销访问。因此,看到 NFS3ERR_STALE 时,要沿着对象生命周期、export 生命周期、客户端缓存和服务端文件系统变化去查,而不是只怀疑网络。

现象

协议含义

排查方向

NFS3ERR_STALE

file handle 指向的对象不存在或访问被撤销。

确认对象是否被删除/重建,export 是否变化,客户端是否缓存了旧 handle。

LOOKUP 成功但后续操作失败

路径解析和具体权限/类型检查是不同过程。

结合 ACCESS、GETATTR、目标类型、服务端日志和抓包看。

同一路径偶发失败

路径不是协议里的持久身份,file handle 才是后续操作引用。

检查 rename/remove、自动挂载、服务端 failover 和客户端属性缓存。

5. LOOKUP:路径是一段一段走出来的

RFC 明确把 LOOKUP 限定在单个路径组件上。客户端要访问 /data/project/a.log,通常不是把整个字符串交给服务端,而是先从挂载根 handle 开始,依次 LOOKUP data、project、a.log。这样做让客户端可以自己构造挂载层级,也避免服务端必须理解客户端本地的挂载视图。

# 观察路径解析、属性查询和目录遍历的基础命令                    SERVER=EXPORT=MNT=/mnt/nfs3            sudo mkdir -p "$MNT"            sudo mount -t nfs -o vers=3,proto=tcp "$SERVER:$EXPORT" "$MNT"            nfsstat -m            rpcinfo -p "$SERVER"            showmount -e "$SERVER"

这也解释了为什么 name cache 对 NFS 客户端很重要。没有缓存时,多层路径会放大 RTT;有缓存时,客户端又必须用属性超时和 WCC 等信息降低旧缓存带来的风险。

6. ACCESS:权限要问服务端,而且答案只在当时成立

NFSv3 加入 ACCESS,是为了解决客户端只看 mode 位不可靠的问题。服务端可能做 uid/gid 映射,也可能有额外 ACL 或 export 策略。所以客户端如果要判断当前凭据能不能读、写、遍历、执行,应该请求服务端检查。

但 ACCESS 的结果是 advisory:它回答的是“服务端在检查这一刻认为可以做什么”。下一秒权限可能被撤销,文件可能被 rename,凭据映射可能变化。所以测试时不要把 ACCESS_OK 当成后续 READ/WRITE 必然成功的证明;它更像提前检查,真正的裁决仍在具体操作返回值里。

ACCESS 位

常见含义

测试提醒

READ

读文件数据或读目录内容。

目录 READ 与文件 READ 不同,结合对象类型判断。

LOOKUP

在目录里查找名字。

没有 LOOKUP 权限时,多层路径会在中间组件失败。

MODIFY / EXTEND

改写已有数据,或追加新数据/目录项。

覆盖写、append、小文件创建要区分。

DELETE

删除目录项。

很多类 UNIX 系统实际由父目录权限决定。

EXECUTE

执行文件。

对目录通常无意义,目录遍历看 LOOKUP。

7. READ 和 WRITE:大文件时代的 64 位语义

NFSv3 相比 v2 的重要变化之一,是文件大小、offset、cookie 等关键字段扩展到 64 位。这不是小修小补,而是让协议能自然表达大文件、大目录和更大的传输窗口。READ/WRITE 的最大传输大小由 FSINFO 返回,客户端不应该凭经验硬猜。

WRITE 还有一个经常被忽略的行为:服务端可以 short write。只要写入了部分数据,服务端可以返回实际写入 count,客户端必须继续从剩余 offset 发送。如果测试工具假设一次写请求要么全成功要么全失败,就会漏掉很关键的协议分支。

8. WRITE/COMMIT:NFSv3 最值得深挖的性能语义

NFSv2 的同步写语义让服务端在返回前必须把数据推进到稳定存储,这对可靠性友好,但对性能不友好。NFSv3 引入了 WRITE stable_how:FILE_SYNC、DATA_SYNC、UNSTABLE。它们不是简单的快慢开关,而是客户端和服务端对“返回时数据安全到什么程度”的协议约定。

stable_how

返回前服务端要做什么

测试含义

FILE_SYNC

数据和相关元数据都到稳定存储。

最接近强同步写;延迟通常更能暴露稳定存储路径。

DATA_SYNC

数据和取回数据所需的元数据到稳定存储。

比 FILE_SYNC 可少提交部分元数据,但实现可能选择等同 FILE_SYNC。

UNSTABLE

服务端可先返回,未提交数据之后由 COMMIT 推进。

吞吐可能更好,但客户端不能过早丢掉未提交缓冲区。

图 2:UNSTABLE WRITE 后,客户端 buffer 要等 COMMIT/verifier 才能真正转为完成。

例子:为什么 close 后还可能看到 COMMIT

客户端把多个 UNSTABLE WRITE 发给服务端,服务端返回 count 和 verifier。客户端在回收 buffer 或 close 文件时,可能发起全文件 COMMIT。若 COMMIT 返回的 verifier 与之前不一致,客户端要假设未提交数据可能丢失,并重传相关 buffer。

9. Stable storage:成功返回的重量

stable storage 是理解 NFSv3 写语义的核心词。它不是“写进服务端内存缓存”,也不是“进了某个普通队列”。它要求在常见掉电、系统崩溃、部分硬件故障后仍能恢复。因此,带电池保护的写缓存、UPS 和恢复软件、真正介质落盘,这些实现细节会直接改变 FILE_SYNC、DATA_SYNC、COMMIT 的真实延迟。

对测试工程师来说,这意味着写测试不能只看吞吐。你要先确认测试关注的是客户端页缓存、网络、服务端缓存、日志、稳定存储还是端到端持久性。如果目标是稳定存储路径,必须设计能触发同步边界的工作负载,并在抓包里确认 stable_how 和 COMMIT 行为。

# 抓 NFSv3 包,验证 WRITE stable_how、COMMIT 和错误码                    SERVER=IFACE=any           OUT=/tmp/nfsv3-$(date +%Y%m%d-%H%M%S)           mkdir -p "$OUT"           sudo tcpdump -i "$IFACE" -s 0 -w "$OUT/nfsv3.pcap" "host $SERVER and port 2049"           nfsstat -rc > "$OUT/nfsstat-rpc-client.txt"           nfsstat -m> "$OUT/nfs-mount-options.txt"

10. WCC:弱缓存一致性不是弱智缓存

WCC 的精妙之处在于,它不假装自己能提供强一致性,而是给客户端更好的线索。修改类操作返回 before 和 after:before 是操作前的关键属性,after 是操作后的完整属性。如果 before 和客户端上次看到的属性一致,客户端更有信心只更新局部缓存;如果不一致,说明在这之间可能有别的客户端或服务端事件改过对象,客户端应该保守失效。

图 3:WCC 用 before/after 帮助客户端判断缓存是否可信。

这里最容易犯的错,是把 WCC 当成强一致性协议。它不是。多客户端并发写同一个文件,如果没有锁或应用级同步,结果仍可能不可预测。WCC 只能减少缓存误判,不能替代并发控制。

11. READDIR 与 READDIRPLUS:大目录测试要看 RPC 形态

READDIR 返回目录项名称、fileid、cookie;READDIRPLUS 额外返回属性和 file handle。对普通 ls,READDIR 可能够用;对 ls -l 或客户端马上要打开/查询大量目录项的场景,READDIRPLUS 可以减少后续 LOOKUP/GETATTR,但代价是单次响应更大、客户端缓存刷新更多。

图 4:READDIRPLUS 用更重的响应换更少的后续 RPC。

目录遍历还有 cookie verifier。客户端第一次 READDIR/READDIRPLUS 用 0 开始,服务端返回每个条目的 cookie 和一个 verifier。后续请求必须带上对应值。如果目录组织在两次请求之间发生变化,服务端可以返回 NFS3ERR_BAD_COOKIE,客户端不能继续假装上次的位置仍然可靠。

12. CREATE、REMOVE、RENAME:幂等性和重试陷阱

NFSv3 是 RPC 协议,天然会遇到超时重传。READ 多做一次通常问题不大,REMOVE、RENAME、CREATE 这类操作就微妙得多。请求可能已执行但回复丢了,也可能压根没到服务端。duplicate request cache 是服务端常见缓解手段,用来记住近期非幂等请求的完成状态。

CREATE 的 exclusive 模式和 create verifier,也是在解决“重试时到底是不是同一次创建”的问题。测试时可以专门设计网络丢包、超时、服务端重启与重复请求场景,观察创建、删除、rename 的返回值是否符合协议语义。

13. FSINFO、FSSTAT、PATHCONF:不要硬编码服务端能力

FSINFO 告诉客户端读写最大/推荐传输大小、目录读推荐大小、时间粒度、是否支持链接等信息;FSSTAT 返回容量和文件数等动态信息;PATHCONF 返回类似 POSIX pathconf 的限制。这三类过程决定了客户端应该怎样选择请求大小、怎样解释名字长度限制、怎样处理时间精度。

过程

返回信息

为什么测试要关心

FSINFO

rtmax/wtmax、rtpref/wtpref、dtpref、time_delta、properties。

影响 READ/WRITE/READDIR 大小,不同服务端能力不同。

FSSTAT

总空间、可用空间、文件槽位、invarsec 等。

容量类工具和客户端缓存策略会用到。

PATHCONF

link_max、name_max、no_trunc、chown_restricted、case 语义等。

长文件名、大小写、硬链接、chown 等场景不能凭本地 FS 经验推断。

14. MOUNT、NLM、认证:NFSv3 主协议之外的现实世界

实际挂载 NFSv3 时,客户端通常先通过 MOUNT 协议获取 export 信息和挂载根 file handle。文件锁则通常由 NLM/NSM 处理,因为 NFSv3 主协议本身不维护 NFSv4 那种 open/lock lease 状态。所以锁冲突、锁恢复、服务端重启后的 reclaim 等问题,不应该只在 NFSv3 procedure 里找答案。

认证层也要单独看。AUTH_UNIX 这类凭据把 uid/gid 等信息带在 RPC 请求里,但它不是强身份体系。服务端的映射、root squash、ACL、export 规则都会影响最终权限。这就是为什么 ACCESS 和实际 READ/WRITE 返回值比客户端本地 mode 位更可信。

15. 从现象反推协议:测试和排障表

现象

优先看的协议点

可收集证据

挂载成功但访问路径慢

LOOKUP 拆路径、客户端 name/attr cache、RTT。

`nfsstat -rc`、pcap 中 LOOKUP/GETATTR 数量、mount options。

ls 很慢,ls -l 更慢

READDIR vs READDIRPLUS、GETATTR 放大、大目录 cookie。

pcap 中 READDIRPLUS/GETATTR 比例,目录规模和响应大小。

写吞吐高但故障后疑似丢数据

UNSTABLE WRITE、COMMIT、write verifier、稳定存储。

抓包确认 stable_how、COMMIT 返回、服务端重启时间线。

权限看起来对但操作 EACCES

ACCESS advisory、服务端 ID 映射、ACL、export 策略。

ACCESS 请求凭据、服务端权限日志、实际失败过程。

偶发 stale file handle

file handle 生命周期、对象删除/重建、导出变化。

客户端 dmesg、服务端 rename/remove/export 事件、pcap 错误码。

目录遍历漏项或重复

cookie verifier、目录修改并发、NFS3ERR_BAD_COOKIE。

READDIR cookie/cookieverf、并发 CREATE/REMOVE 时间线。

多客户端写结果不符合预期

WCC 不是强一致性,锁在 NLM/应用层。

NLM 证据、应用锁策略、WCC before/after 属性。

16. 可复制的最小实验

下面这些实验适合放进日常回归或排障预案里。它们不追求压满性能,而是验证协议行为是否符合预期。

# 实验 1:观察目录遍历 RPC 形态                    MNT=/mnt/nfs3                    mkdir -p "$MNT/dirwalk"                    for i in $(seq -w 1 5000); do echo "$i" > "$MNT/dirwalk/file-$i.txt"; done                    sync                    ls "$MNT/dirwalk" >/dev/null                    ls -l "$MNT/dirwalk" >/dev/null                    # 实验 2:触发属性和缓存变化                    echo old > "$MNT/cache-demo.txt"                    stat "$MNT/cache-demo.txt"                    chmod 600 "$MNT/cache-demo.txt"                    stat "$MNT/cache-demo.txt"                    # 实验 3:观察写入同步边界                    dd if=/dev/zero of="$MNT/write-demo.bin" bs=1M count=256 conv=fsync                    sync

如果要把这些实验做成自动化,建议每个场景同时保存:mount options、nfsstat、dmesg 尾部、pcap、服务端对应时间段日志、工具 stdout/stderr。NFSv3 问题经常是“协议行为 + 客户端缓存 + 服务端实现 + 网络重传”的组合题,单一日志很容易误导。

17. 问题来了

下面的问题适合你先自己推演:

1. 为什么说 NFSv3 服务端是 stateless,但它仍然可以有缓存和 duplicate request cache?

2. NFSv3 为什么没有把完整路径放在每个 READ/WRITE 请求里?

3. file handle 对客户端不透明,这个设计带来了什么好处和排障难点?

4. NFS3ERR_STALE 通常意味着什么?它和普通路径不存在有什么区别?

5. ACCESS 为什么比客户端直接检查 uid/gid/mode 更可靠?为什么它又不能保证未来操作成功?

6. WRITE stable_how 里的 FILE_SYNC、DATA_SYNC、UNSTABLE 分别意味着什么?

7. 客户端收到 UNSTABLE WRITE 成功后,为什么不能马上丢掉本地未提交 buffer?

8. COMMIT 的 write verifier 变化时,客户端为什么要重传未提交数据?

9. stable storage 为什么不能简单理解成服务端内存缓存?

10. WCC 的 before/after 属性能帮助客户端做什么?它为什么仍然不是强一致性?

11. READDIRPLUS 为什么能改善某些目录遍历场景?它的代价是什么?

12. cookie verifier 解决了 READDIR 的哪类问题?什么情况下可能出现 NFS3ERR_BAD_COOKIE?

13. 为什么 CREATE、REMOVE、RENAME 这类非幂等操作在超时重传下更危险?

14. duplicate request cache 能解决所有重复请求问题吗?为什么?

15. FSINFO 对 READ/WRITE/READDIR 请求大小有什么影响?

16. NFSv3 的锁为什么不能只看 NFSv3 主协议?

17. 如果 ls -l 很慢,你会从哪些 NFSv3 procedure 和客户端行为入手?

18. 如果写测试吞吐很好,但故障恢复后怀疑数据丢失,你会检查哪些协议证据?

19. 多客户端并发写同一文件时,WCC、缓存和锁分别扮演什么角色?

20. 为什么 NFSv3 排障时 pcap、nfsstat、mount options、服务端日志最好一起保存?

18. 参考资料

·RFC 1813: NFS Version 3 Protocol Specification, June 1995, https://www.rfc-editor.org/rfc/rfc1813

·RFC 1094: NFS Version 2 Protocol Specification, March 1989, https://www.rfc-editor.org/rfc/rfc1094