NFS:远程文件的一致性、状态与恢复

本页目录

客户端超时了,服务端却可能已经创建文件;WRITE 成功了,内容又可能尚未稳定。NFS 需要分别追踪请求身份、缓存验证和持久确认⁠,再决定怎样重试。

要解释的现象阅读位置
多个操作放进一条 RPC,是否一起回滚?第 1~2 节:版本与 COMPOUND
另一个客户端何时看到修改?第 3 节:缓存与 delegation
崩溃后是重发数据还是重新确认?第 4 节:WRITE/COMMIT 推演
超时、恢复与安全如何定位?第 5~7 节:等待、布局与只读观测

1. VFS、RPC 与后端文件系统

Linux 客户端 fs/nfs/ 接入 VFS;net/sunrpc/ 提供 ONC RPC 相关设施;内核服务端 fs/nfsd/ 处理远程操作并访问导出的后端。ONC RPC 是远程调用框架,XDR 定义数据编码;TCP、UDP 或 RPC-over-RDMA 是其承载方式,不能把 SUNRPC 与 TCP 并列成同一层传输协议。

NFS filehandle 标识服务端文件对象,不是客户端 fd,也不只是绝对路径字符串。rename 可以改变名字而保留对象身份;删除重建、导出变化等可能使旧 handle 失效并返回 ESTALE。遇到 ESTALE 应分析对象和导出生命周期,不应只增加重试次数。

版本主要机制边界
v3读写、属性等基本操作;通常通过辅助 MOUNT 协议取得初始 handle核心协议相对无状态,不表示服务端无缓存、无锁服务或无恢复需求
v4.0OPEN/LOCK、租约与 delegation、COMPOUND、统一导出视图有状态,需要处理恢复与撤销
v4.1session、slot/sequence、pNFS 等重放保护有具体会话与缓存边界
v4.2增加服务器端复制、稀疏文件等能力协商版本不代表所有可选操作都可用

v4 通常直接从协议导出根遍历,不需要 v3 的辅助 MOUNT 协议;客户端看到的路径是服务端导出视图,未必等于服务端本地路径。新配置使用 -t nfs 并按需要指定 vers=,旧 nfs4 文件系统类型写法已不推荐。见 Linux nfs(5)。

2. COMPOUND 减少往返,不提供数据库事务

把“定位对象、打开、读取属性”等多个操作放进 COMPOUND,可以减少网络往返。服务端按协议执行并报告结果,后面失败不意味着前面已产生的效果自动回滚。因此不能把一条 RPC 中的多个操作当作全有或全无业务事务。

v4.1 的 session 使用 slot 与 sequence 等信息识别重试并复用结果,降低重复执行风险;它不是应用在新会话里任意重发请求都自动去重,更不能替代文件系统之外的业务去重。重启、状态丢失及应答缓存限制仍要按协议处理。v4.1 的更新规格见 RFC 8881,它取代了 RFC 5661。

一次应答丢失如何由 session 区分重试与新请求

假设客户端在一个有效 v4.1 session 的 slot 3 中发送 sequence 8,请求 R 要创建文件,并按协议要求缓存应答。服务端执行成功,记录这个 slot 对应的结果,但应答在途中丢失。客户端面对的现象是超时,实际文件却已经创建。

客户端后续动作服务端应如何区分
同一 session、slot 3、sequence 8 重传相同 R识别重试,按应答缓存条件返回此前结果
接受结果后,在 slot 3 推进到 sequence 9该 slot 的下一项工作
另一个 slot 发请求可并发占用另一个请求位置

slot 提供并发位置,sequence 区分各位置上的前后请求。它们不是永久保存全部业务 ID 的无限去重数据库。

如果客户端重新建立会话,或原应答没有按所需条件保留,就不能凭同一文件名假定仍享有上述重放保障。相关错误必须进入协议规定的恢复流程。这解释了为什么“RPC 有重试机制”与“任意业务操作可安全重发”之间仍有距离。

局部成功:创建完成后,改属性仍可能失败

COMPOUND 的另一个边界可以用创建后改属性来理解:前面的 OPEN 创建成功,后面的 SETATTR 因权限失败,返回值需要逐项解释。文件可能已经存在,不能把整个 COMPOUND 的最后错误简单翻译为“什么都没做”。重放缓存保存这次执行的结果,与回滚前面已完成操作是不同能力。见 RFC 8881 的 Session 与 COMPOUND 语义。

3. 缓存一致性:谁在什么时候验证了什么

客户端可以缓存文件内容、属性以及名字查找结果。属性缓存期限控制何时重新验证,数据是否失效还会受变化属性等判断影响。close-to-open 的典型意图是:写者关闭前推进修改,之后另一客户端打开时检查变化。它不等于所有已打开 fd 和 mmap 映射实时共享同一份内存。

两个客户端:重新打开与持续持有 fd 不同

顺序可据此分析什么
A 写入,完成所需同步/关闭A 推进了修改
B 此后重新打开,按正常策略验证B 有机会通过变化检查看到更新
B 一直持有 fd、反复读取缓存不能仅凭 A 的 write 成功推断 B 下次必见更新

多个写者重叠写入仍需共同遵守锁或应用协议。NFS 客户端还可能模拟 O_APPEND,并发追加时存在定位与写入之间的竞争,不能直接套用本地追加直觉。

“每个 open 必发一条 GETATTR”也过于绝对:缓存、delegation、协议组合和挂载选项都会影响实际请求。noac 不是关闭所有页缓存的统一开关;减小属性缓存期限也不会凭空创建多操作事务。

Delegation 是可收回的权限

服务端可在合适条件下授予读或写 delegation,使客户端在许可范围内本地处理更多操作。读 delegation 不必意味着全局只有一个读者;写 delegation 也不等于所有内容都已缓存、本地所有访问永远零 RPC。

发生冲突时,服务端通常先 recall,客户端完成所需回写并归还;不能正常归还时才涉及撤销和恢复。租约、回调通道和服务端状态共同参与此过程,不能简化为“另一客户端访问就立即 revoke,旧客户端自然同步”。

4. WRITE 成功还要看稳定级别

NFS 写入:客户端接受、服务端接收与稳定存储不同 客户端 A NFS 服务端 稳定存储 WRITE(可请求 UNSTABLE) 应答:实际稳定级别 + verifier 必要时 COMMIT 完成所需持久化 确认稳定;检查 verifier WRITE 已稳定时可不再发 COMMIT;服务端重启导致 verifier 改变时可能需要重传数据。

WRITE 可报告不同稳定级别。UNSTABLE 表示服务端尚未作出所需稳定存储承诺,客户端通常仍需保留用于重传的数据,并随后请求 COMMIT;若 WRITE 已按要求稳定完成,未必还要额外发一条 COMMIT。因此 fsync 不是固定“只执行 COMMIT”的别名,它还可能需要发送脏数据、等待结果并处理状态。

write verifier 用于判断服务端相关写入状态是否跨重启变化。若服务端丢失未稳定数据,客户端不能仅再次提交一个没有数据的 COMMIT 就认为内容恢复,应依协议重新发送数据。稳定写入定义见 RFC 1813 的 WRITE/COMMIT及 v4.1 规格。

客户端得到成功仍依赖服务端遵守承诺。导出 async、后端同步策略、设备易失缓存等可能改变实际故障保障;客户端挂载的同步选项与服务端 exports 的同步选项是不同控制点。NFS 不会自动修复后端对持久性作出的错误承诺。

两次崩溃推演:为什么客户端必须保留未稳定数据

设服务端磁盘中的文件内容为旧版本 O,客户端准备把某一范围写成 N。先忽略其他写者,假设服务端严格遵守稳定存储承诺,且底层设备没有损坏。

时刻客户端知道什么服务端状态此刻恢复需要什么
t0:WRITE 发出N 尚未被确认可能尚未收到请求客户端保留 N
t1:收到 UNSTABLE、verifier V₁服务端收到了 N,但未承诺稳定N 可能只在易失缓冲,磁盘仍为 O客户端仍须能重传 N
t2:服务端在 COMMIT 前崩溃客户端可能只看到暂时无响应重启后只有 O,写入状态 verifier 改为 V₂检出 V₂ ≠ V₁,重新写入 N
t3:重传并完成稳定确认N 的恢复承诺终于成立N 已稳定才能结束对应的稳定化责任

关键在 t2:COMMIT 没有携带完整新内容。若客户端在 t1 就丢掉唯一的 N,再补发 COMMIT 也无法让服务端从 O 猜回 N。verifier 把“仍是上次那个易失写入时期吗”变成可检查条件;客户端保留数据则提供真正的恢复材料。

把服务端重启和应答丢失分别施加到这次写入:前者可能需要重传内容,后者可能只是客户端还不知道稳定化已完成。图中客户端会一直保留尚需承担责任的数据。

正在呈现知识画面
COMMIT 没带数据,怎样找回丢失的写入?

UNSTABLE 应答后客户端仍需保留重传材料。verifier 改变时,仅重发 COMMIT 不能恢复已丢失内容,需要重新发送数据。

对照:数据已稳定,只丢了 COMMIT 应答

换一个崩溃点:服务端已经把 N 稳定化,但 COMMIT 应答丢失。此时重新询问/按协议恢复是在消除客户端的不确定性,磁盘并未退回 O。两种场景对客户端都可能表现为超时,处理却不能依据“没收到成功”就认定写入从未发生。这个差异也是 fsync 返回错误后应用需要考虑结果不确定的原因。以上是协议推演,未包含服务器故意使用不兑现稳定承诺的配置。

5. 超时、hard/soft 与任务等待

请求超时只说明客户端没有及时拿到可用应答,不证明服务端没执行。TCP 连接可靠传输也不能消除服务端执行后应答丢失或重启的窗口。

设置控制什么不应怎样理解
hard通常持续重试可恢复请求retrans 不是所有请求的总重试上限
soft 等策略可能更早向应用返回错误返回错误不证明服务端未部分执行
timeo以十分之一秒计的超时参数不一定是整个系统调用的总截止时间

缩短等待会改变应用遇到的失败方式,不能把它当作无条件消除卡顿的优化。

有些等待可以被致命信号打断,有些仍受不可中断资源等待约束。TASK_KILLABLE 早于 5.x 就存在;不能从进程显示 D 就断言“永远不能杀,只能重启”,也不能保证 SIGKILL 立即完成所有 NFS 清理。应结合内核栈、具体等待点与服务端恢复状态分析。

服务端恢复:租约与锁需要重新协调

v4 服务端重启还可能进入 grace period,让客户端重申之前的打开与锁。持久客户端跟踪数据帮助恢复决策,不等于把所有运行时锁对象原样写到一个固定路径。nfsd worker 数量可配置,也不是强制每个 CPU 恰好一个线程。

6. pNFS 与安全模型

pNFS 由元数据服务提供 layout 等信息,客户端按所用布局访问数据服务器或相关存储目标,从而分散数据路径。它不是取消元数据协调,也不保证任意目录访问都绕开 MDS;布局类型、撤回、失败回退及设备能力决定实际路径。

安全方式提供什么不提供什么
AUTH_SYS / sec=sys传递 UID/GID 等身份信息供权限判断不加密,也不凭自身验证远端主机用户声明真实性
sec=krb5RPCSEC_GSS 认证不提供与 krb5p 相同的内容隐私
sec=krb5i认证与完整性保护不加密操作内容
sec=krb5p认证、完整性与隐私不代替导出授权、文件权限和密钥管理

root_squash 映射客户端 root 身份,不是对任意不可信客户端身份伪造的完整防护。NFSv4 也不意味着当前挂载一定使用 Kerberos,应检查实际协商的 sec=;规范的实现要求不能直接当成运行时配置事实。

7. 只读定位当前 NFS 视图

from pathlib import Path

found = False
for line in Path("/proc/self/mountinfo").read_text().splitlines():
    left, separator, right = line.partition(" - ")
    fields = right.split()
    if separator and fields and fields[0] in {"nfs", "nfs4"}:
        found = True
        print("挂载记录:", left)
        print("类型、来源与超级块选项:", right)
if not found:
    print("当前 mount namespace 未发现 NFS 挂载")

输出保留 mountinfo 的转义形式,避免把包含空格的路径误拆成普通字符串。进一步读 /proc/self/mountstats、nfsstat -c 或 nfsstat -s,区分 RPC 数量、重传和服务时间;这些指标应取相同时间窗口差分。全量 rpcdebug 会修改调试状态并可能生成大量日志,不应作为默认只读观测步骤。