
重点解读 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= |
这也解释了为什么 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= |
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
夜雨聆风