NFS 1985 — 让远程磁盘看起来像本地磁盘
是什么
NFS(Network File System,网络文件系统)是 Sun Microsystems 1985 年发表的一套协议,让一台机器像访问本地硬盘一样访问另一台机器的硬盘。
日常类比:你在公司工位 A 打开”我的文档”,看到的其实是机房里某台服务器上的目录。但你点开、保存、改名都和操作 C 盘一模一样——中间那段网络对你完全透明。
代码层面你写:
int fd = open("/mnt/projects/report.txt", O_RDONLY);read(fd, buf, 4096);/mnt/projects 实际在另一台主机上,但 open / read 这些系统调用一个字都不用改。
NFS 的核心创新有三个:无状态服务器 + RPC 远程过程调用 + VFS 文件系统抽象层。这三件套之后成为整个分布式系统课程的入门范例。
为什么重要
- 不理解 NFS,看 AFS / Lustre / HDFS / Ceph 的设计文档会完全没坐标系
- 不理解 RPC + 序列化的分层,今天写 gRPC / Thrift 时也只能照抄不会调
- “无状态 vs 有状态” 的抉择,每个分布式系统都要做一次——NFS 是把”无状态”贯彻到底的最早样本
- 1985 年 Sun 决定把协议开放(不收专利费),是开放标准战胜封闭协议的早期案例
- 今天 AWS EFS / 阿里云 NAS / NetApp 还在跑 NFS v4,40 年没死
核心要点
1. 无状态服务器(Stateless Server)
服务器不记客户端打开过什么、读到哪一字节。每次请求自带所有上下文:
read(file_handle=0xab12, offset=4096, length=4096)file_handle 是一个不可猜的二进制 id,相当于服务器上某个 inode 的”门牌号”。客户端拿到这个号码后,每次读都自己说”我要读哪一段”。
好处:服务器掉电重启,客户端完全不用对账——重发请求就行。这是 NFS 比同期的 AFS 简单得多的根因。
2. 操作必须幂等
无状态意味着”我刚才发过这个请求吗”服务器自己不知道。所以协议设计:所有操作执行多次和执行一次结果一样。
写入时不说”在末尾追加”(这会累积),而说”在 offset=4096 写这 4KB”——重发也只是覆盖同一段。
3. RPC + XDR:让远程调用看起来像本地
应用调一个本地函数 nfs_read(...),底层 RPC(Remote Procedure Call)把参数用 XDR(External Data Representation)编码成字节流,扔到 UDP 包里发出去。服务器解码、跑 disk I/O、返回结果。整个过程对应用透明。
这套”应用看不见网络”的抽象,今天叫 gRPC / Thrift / JSON-RPC,但鼻祖在这里。
4. VFS:插槽化文件系统
Sun 在内核里加了一层 VFS(Virtual File System),把”文件系统”抽象成一个接口:本地 ufs 实现它,远程 nfs 也实现它。open / read 系统调用进 VFS 后才分流,应用代码完全不用改。
这是”接口与实现分离”在内核里的早期落地,今天 Linux 还是这一套。
实践案例
案例 1:mount 一个远程目录
# 客户端机器上mount -t nfs server.lab:/export/projects /mnt/projectsls /mnt/projects第一行做的事:客户端联系 server.lab,要 /export/projects 的初始 file handle,把它和 /mnt/projects 这个挂载点绑起来。之后所有访问 /mnt/projects/* 的系统调用进 VFS 后都走 NFS 客户端,发 RPC 出去。
案例 2:服务器崩了会怎样
[T0] client: read(handle, offset=0, len=4096) → 服务器 OK,返回数据[T1] server crash + reboot[T2] client: read(handle, offset=4096, len=4096) → 重发几次,服务器起来后正常返回客户端发现请求超时就自己重试。服务器起来后看到一个”陌生”的请求——但因为 file handle 是持久化的(指向 inode),它能直接处理,根本不知道之前崩过。
对比有状态系统(AFS):服务器要恢复”哪些客户端打开了哪些文件”的整张表,重启慢且复杂。
案例 3:close-to-open 一致性的尴尬
机器 A: echo "hello" > /mnt/shared/note.txt # 写完关闭机器 B: cat /mnt/shared/note.txt # 几秒后才看到 helloNFS 客户端会缓存文件属性和目录项几秒钟避开网络往返,所以 B 读到的可能是旧版本。NFS 给的保证只到”A 关闭文件 + B 重新打开” → 才能保证看到新内容。这就是著名的 close-to-open consistency。
踩过的坑
-
NFS v2 没有锁。两个进程在不同机器上
flock同一个文件,互不影响——会脏写。后来加了独立的 NLM(Network Lock Manager)协议,但 NLM 自己又是有状态的,复活了无状态本来要躲的复杂度。 -
uid 完全靠客户端自报。客户端说”我是 uid=1000”,服务器就信。同一个 uid 在两台机器上代表不同人时直接漏权限。NFS v4 才加 Kerberos 解决。
-
UDP 默认 + 大请求 = 包碎片。8KB 写请求拆成 6 个 UDP 包,丢 1 个就要全部重发。早期局域网很常见。后来主流转成 TCP。
-
stale file handle。客户端缓存了一个 handle,服务器那边对应的文件被删了又重建——客户端再用就报 ESTALE。新人看见这个错误经常一脸懵。
适用 vs 不适用场景
适用:
- 局域网内多机共享文件(开发环境 / 编译机集群 / 渲染农场)
- 主要是读、偶尔写、不需要强一致
- 想要一个”看起来像本地盘”的接口避免改代码
不适用:
- 跨广域网、延迟 > 50ms(每次 read 一个 RTT,慢到不能用)
- 需要严格并发锁定(多人同时写一个数据库文件)
- 海量小文件 + 极高 QPS(每个操作一个 RPC,吞吐封顶)
- 大块只追加写场景 → 用 HDFS / 对象存储更合适
历史小故事(可跳过)
- 1983:CMU 发布 AFS(Andrew File System)——有状态、强一致,但部署复杂、只在大学里跑得动。
- 1984:Sun 工程师 Russel Sandberg 决定走另一条路:把所有状态扔掉、用 RPC + VFS 做最薄的远程文件协议。
- 1985 USENIX:发表 NFS 论文。同年 Sun 决定公开协议(不收专利费),任何厂商都能实现。这一刀决定了 NFS 的命运。
- 1989:RFC 1094,NFS v2 成为正式互联网标准。
- 1995 / 2003:v3(大文件、异步写)/ v4(合并 mount/lock、加 Kerberos)。
- 今天:AWS EFS、阿里云 NAS、NetApp ONTAP 都还在跑 NFS。40 年只换协议版本不换思想。
学到什么
- 状态是分布式系统的债——能少则少。NFS 把无状态贯彻到底,换来重启免对账这个巨大优势。
- 幂等是协议设计的基本盘——只有幂等才能允许重发,只有允许重发才能在不可靠网络上活下来。
- 抽象层(VFS)让”远程”变得不可见,应用代码零改动是采用的关键。这个思路今天叫 driver / adapter / provider。
- 开放协议比闭源协议更长寿。Sun 让 NFS 免费可用,所有 Unix 都装上了;那些收专利费的网络文件系统全死了。
- 简单的设计要为复杂场景留扩展点——v2 没锁,v4 才加;如果一开始就硬上锁,今天可能没 NFS 了。
延伸阅读
- NFS v2 RFC 1094 —— 协议正式标准,比论文细
- NFS v4 RFC 7530 —— 现代版本,加了状态、Kerberos
- 论文 PDF:Sandberg 1985(约 12 页)
- 对比阅读:AFS / Coda / GFS / HDFS——每一个都在讨论”无状态 vs 有状态、强 vs 弱一致”
- unix-1974 —— NFS 是 Unix 文件系统抽象的网络延伸
关联
- unix-1974 —— NFS 把 Unix “一切皆文件” 的语义延伸到网络
- rest-fielding-2000 —— REST 也走”无状态服务器”路线,但层次更高
- lamport-tla-1994 —— 分布式协议的形式化,验证 NFS 这类系统的工具
反向链接
- afs-1988 —— AFS 1988 — 客户端缓存 + 回调失效让分布式文件系统真正能扩展
- coda-1990 —— Coda 1990 — 笔记本拔网线照样写文件,重连后自动合并
- lamport-tla-1994 —— TLA — 把状态机和时序逻辑捏成一个公式
- locus-1980 —— LOCUS 1980 — 让一群机器看起来像同一台机器
- rest-fielding-2000 —— REST — Fielding 2000 给 Web API 写下的设计宪法
- sprite-1988 —— Sprite 1988 — 把一屋子工作站伪装成一台大主机