第 49 章 网络文件系统 (NFS)

分布式文件系统基础

分布式文件系统允许客户端通过网络访问服务器磁盘上的数据,实现数据共享和集中管理。系统主要由两部分组成:

  • 客户端文件系统:接收应用程序的系统调用(如 open()read()),并将其转换为网络协议消息。
  • 文件服务器:接收网络请求,读取或写入本地磁盘(或缓存),并将结果返回给客户端。

无状态协议设计

Sun 开发的 NFS (Network File System) 采用了开放协议。NFSv2 的主要设计目标是简单快速的服务器故障恢复

为实现此目标,NFS 采用了无状态 (Stateless) 协议:服务器不记录客户端的状态(如打开的文件、文件指针位置等)。每个客户端请求必须包含完成该请求所需的全部信息。相比之下,有状态协议(如使用文件描述符)在服务器崩溃后会丢失状态,需要复杂的恢复机制。

协议操作与文件句柄

无状态操作的核心是文件句柄 (File Handle),用于唯一标识要操作的文件或目录。文件句柄包含 3 个组件:

  • 卷标识符 (Volume identifier):指示目标文件系统。
  • inode 号 (Inode number):指示具体文件。
  • 生成号 (Generation number):防止复用 inode 号时发生冲突。

关键协议操作包括:

  • LOOKUP:传入目录句柄和文件名,返回目标的文件句柄和属性。
  • READ / WRITE:传入文件句柄、偏移量 (offset) 和字节数,直接进行读写,无需维护文件指针。
  • GETATTR:获取文件属性(如最后修改时间)。

幂等操作与故障恢复

NFS 客户端通过重试 (Retry) 统一处理网络丢包或服务器崩溃等故障。如果请求超时未收到响应,客户端直接重新发送请求。

这种重试机制依赖于操作的幂等性 (Idempotency):多次执行同一操作的效果与执行一次相同。

  • LOOKUPREAD 仅读取数据,天然幂等。
  • WRITE 请求包含具体的数据、字节数和精确的偏移量,因此也是幂等操作。多次写入同一偏移量不会改变最终结果。

(注:某些操作如 MKDIR 难以实现完全幂等,但 NFS 接受这种不完美以换取整体设计的简洁。)

客户端缓存与一致性

为提升性能,NFS 客户端会将服务器数据和元数据缓存在本地内存中,并作为写入的临时缓冲区。但这引入了缓存一致性 (Cache Consistency) 问题:

  1. 更新可见性 (Update visibility):一个客户端的写入何时对其他客户端可见?
    • 解决方案:NFS 采用关闭时刷新 (Flush-on-close / Close-to-open) 语义。当客户端关闭文件时,将所有脏页刷新到服务器。
  2. 陈旧缓存 (Stale cache):客户端如何知道本地缓存已过期?
    • 解决方案:客户端在使用缓存前,向服务器发送 GETATTR 请求检查文件的最后修改时间。如果服务器上的文件更新,则使本地缓存失效。
    • 优化:为避免 GETATTR 请求淹没服务器,NFS 引入了属性缓存 (Attribute cache),在设定的超时时间(如 3 秒)内直接使用本地属性,但这可能导致短时间内读取到旧版本数据。

服务端写入缓冲

NFS 服务器在处理 WRITE 请求时,必须将数据提交到稳定存储(持久化设备)后,才能向客户端返回成功。

如果服务器将数据缓存在内存中就返回成功,一旦服务器崩溃并在重启后接收后续写入,可能会导致文件内容出现新旧数据交错的损坏。因此,快速写入持久化存储是 NFS 服务器性能优化的关键。