Linux 基础体系 · 第 59/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux NFS:协议、挂载、缓存、一致性、权限和高可用

NFS(Network File System)把远端服务器上的文件系统目录提供给客户端,使客户端能够通过本地路径访问远端文件。它提供的是文件语义,而不是块设备语义:客户端执行 open()read()write()rename() 等系统调用,内核再将这些操作转换为网络请求;客户端通常看不到远端磁盘的分区、LVM 或 RAID 结构。

这一区别很重要:

  • NFS 适合共享目录、用户家目录、构建产物、备份目标和应用数据目录。
  • NFS 不等同于把远端磁盘映射到本地。不能在多个客户端上直接对同一个 NFS 文件系统再次创建分区、LVM 或普通文件系统。
  • 如果应用需要直接控制块设备,应使用 iSCSI、FC、NVMe-oF 等块存储协议,而不是 NFS。

一、NFS 的组件和一次文件访问的完整路径

一次典型的 NFS 访问至少涉及以下组件:

  1. 应用进程:调用 open()read()write()close() 等系统调用。
  2. 客户端 VFS:Linux 虚拟文件系统统一抽象本地文件系统和 NFS。
  3. NFS 客户端内核模块:将 VFS 操作转换为 NFS RPC 请求。
  4. RPC、XDR 和传输层
    • RPC(Remote Procedure Call)表示远程过程调用;
    • XDR(External Data Representation)定义跨主机字节序和数据布局;
    • NFS 请求通常通过 TCP 发送,现代 Linux 默认倾向使用 TCP。
  5. 服务端 NFS 内核服务:处理请求并访问服务端本地文件系统。
  6. 服务端文件系统和存储层:可能经过 XFS、ext4、LVM、RAID、SAN 或本地磁盘。

数据流可以简化为:

flowchart LR
    A[应用进程] --> B[VFS]
    B --> C[NFS 客户端]
    C --> D[RPC/XDR]
    D --> E[TCP/IP]
    E --> F[NFS 服务端]
    F --> G[服务端 VFS]
    G --> H[本地文件系统]
    H --> I[块设备/LVM/RAID/磁盘]

例如客户端执行:

fd = open("/data/report.txt", O_RDONLY);
read(fd, buf, 4096);
close(fd);

并不意味着每个 read() 都必然产生一个网络包。NFS 客户端会先查本地 dentry、inode 属性和页缓存;只有缓存未命中、缓存失效或需要向服务端确认时,才发送 RPC。NFS 的性能和一致性问题,正是由这层缓存与远端文件语义共同决定的。


二、NFS 协议版本:NFSv3 与 NFSv4 的根本差异

2.1 NFSv3:相对无状态、依赖辅助 RPC 服务

NFSv3 常见操作包括:

  • LOOKUP:根据目录项查找文件;
  • GETATTR:获取属性;
  • READWRITE:读写文件数据;
  • CREATEREMOVERENAME:修改目录;
  • READDIR:读取目录;
  • FSSTATFSINFO:获取文件系统信息。

NFSv3 本身通常使用服务端口 2049,但挂载协商和锁服务还可能涉及:

  • mountd:处理挂载请求;
  • rpcbind:将 RPC 程序映射到端口;
  • lockd / rpc.statd:文件锁和锁状态恢复。

因此 NFSv3 的防火墙配置可能不只需要放行 TCP/UDP 2049,还要固定并放行相关辅助服务端口。不同发行版的服务拆分和默认协议可能不同,应通过 rpcinfo -p、服务配置和防火墙规则核对,而不是凭端口列表猜测。

NFSv3 使用文件句柄(file handle)标识远端对象。客户端拿到文件句柄后,可以直接请求后续操作,这使协议相对简单;但它没有 NFSv4 那样完整的会话、状态恢复和统一命名空间机制。

2.2 NFSv4:统一端口、复合操作和有状态语义

NFSv4 将大量操作合并为一个复合请求,并通常使用 TCP 2049。它引入或强化了:

  • 统一的伪文件系统命名空间;
  • 客户端和服务端状态;
  • 打开状态(open state);
  • 租约(lease);
  • 锁和锁所有者状态;
  • delegation(委派);
  • NFSv4 ACL;
  • idmapd 参与的身份名称映射;
  • NFSv4.1 的会话和服务器端并行访问等能力。

NFSv4 的“有状态”不表示服务端保存了每一次 read() 的全部上下文,而是表示打开、锁、委派等操作存在需要恢复和协调的状态。因此服务端重启后,客户端可能需要进行状态恢复;服务端会经历 grace period(宽限期),允许原有客户端重新声明锁和打开状态。

NFSv4 的挂载路径常见两种形式:

sudo mount -t nfs4 server:/export/data /mnt/data

或者挂载服务端的伪根,再访问其下的导出目录:

sudo mount -t nfs4 server:/ /mnt/nfs-root
ls /mnt/nfs-root

具体可见路径由服务端 fsid、伪根和导出配置决定。不能简单假设 NFSv4 中客户端看到的路径就是服务端的绝对路径。

2.3 NFSv4.1、NFSv4.2 和 pNFS

NFSv4.1 增加了会话、客户端标识和服务器端并行访问等机制。pNFS(parallel NFS)允许元数据服务器将数据布局告知客户端,使客户端能够直接访问多个数据服务器;它需要服务端存储后端、客户端和部署方案共同支持,不能因为挂载了 nfs4 就自动获得 pNFS。

NFSv4.2 在不同内核和服务端实现中支持程度可能不同。部署时应以目标发行版的内核、nfs-utils、服务端实现和实际协商结果为准,不应仅根据协议版本名称推断某项扩展一定可用。

查看挂载结果:

findmnt -t nfs,nfs4
nfsstat -m
mount | grep -E ' type nfs| type nfs4'

nfsstat -m 可以显示 NFS 客户端的版本、传输协议、缓存和挂载选项,是确认实际生效参数的重要工具。


三、服务端导出:/etc/exports 到客户端挂载

3.1 一个最小的服务端配置

以 Debian 系发行版为例,安装服务端软件:

sudo apt install nfs-kernel-server

RHEL、Rocky、AlmaLinux 等发行版通常使用:

sudo dnf install nfs-utils

准备目录:

sudo install -d -m 0755 /srv/nfs/project
sudo chown root:root /srv/nfs/project

编辑 /etc/exports

/srv/nfs/project 192.0.2.0/24(rw,sync,root_squash,no_subtree_check)

每个选项的含义如下:

  • 192.0.2.0/24:允许访问的客户端网段。实际部署应替换为受控网络。
  • rw:允许远端写入;默认通常是只读或由发行版默认值决定,应显式配置。
  • sync:服务端在认为写请求完成前,将数据同步到稳定存储语义所要求的位置。它通常降低写入性能,但比 async 更不容易在服务端崩溃后丢失已确认写入。
  • root_squash:将远端 UID 0 映射为匿名身份,防止客户端 root 直接成为服务端 root。
  • no_subtree_check:不执行子目录检查,通常可减少目录重命名等场景中的检查复杂度;这不是权限控制。

重新加载并检查:

sudo exportfs -rav
sudo exportfs -v

预期输出会包含类似:

/srv/nfs/project
        192.0.2.0/24(sync,wdelay,hide,no_subtree_check,sec=sys,rw,root_squash,no_all_squash)

exportfs -v 显示的是服务端实际识别到的导出选项,而不是仅仅显示配置文件文本。配置语法错误、网络范围写错或导出未重新加载,都应以这里的结果为准。

3.2 sync 不是“所有客户端立即看到相同数据”

sync 约束的是服务端完成写请求时对稳定存储的处理方式,主要影响崩溃持久性;它不等于:

  • 禁止客户端缓存;
  • 强制所有客户端每次读取都访问服务端;
  • 提供数据库级事务;
  • 让多个并发写入自动合并。

因此即使导出使用 sync,多个客户端仍可能因为属性缓存、页缓存、应用打开文件的方式不同而读到不同版本。

3.3 root_squash 的边界

root_squash 只针对远端通过 NFS 请求携带的 root 身份。它不解决所有权限问题,也不阻止:

  • 客户端普通用户修改自己拥有的文件;
  • 目录权限允许的删除和重命名;
  • 服务端本机 root 访问文件;
  • 未受控客户端使用合法 UID 发起访问;
  • 应用漏洞或凭据泄漏。

no_root_squash 会让远端 root 获得服务端 root 等价的文件访问能力,除非有非常明确且受控的兼容性需求,否则具有严重风险。


四、客户端挂载:从内核挂载到 /etc/fstab

安装客户端工具:

sudo apt install nfs-common

或:

sudo dnf install nfs-utils

创建挂载点:

sudo mkdir -p /mnt/project

执行挂载:

sudo mount -t nfs4 \
  -o rw,hard,proto=tcp,vers=4.1 \
  nfs.example.com:/srv/nfs/project \
  /mnt/project

这里需要区分“请求参数”和“最终结果”:

  • vers=4.1 请求使用 NFSv4.1;
  • proto=tcp 请求使用 TCP;
  • hard 指定请求失败时继续重试,而不是立即向应用返回错误;
  • 服务端是否接受该版本、路径是否正确,仍要以挂载结果和日志为准。

检查:

findmnt /mnt/project
nfsstat -m
df -hT /mnt/project
touch /mnt/project/client-$(hostname)
ls -l /mnt/project

如果需要开机挂载,可以在 /etc/fstab 中写:

nfs.example.com:/srv/nfs/project /mnt/project nfs4 rw,hard,_netdev,x-systemd.automount,x-systemd.idle-timeout=600 0 0

逐项说明:

  • _netdev:声明这是网络设备,避免系统在网络尚未就绪时按普通本地文件系统处理。
  • x-systemd.automount:先建立 automount 点,在真正访问时触发挂载,减少启动阶段等待。
  • x-systemd.idle-timeout=600:空闲 600 秒后允许自动卸载;它不应被理解为清除 NFS 缓存或终止所有使用。
  • hard:服务端不可达时,系统调用可能长期阻塞。对于数据库、Web 服务和关机流程,必须测试这种阻塞行为;不能只因为 soft 返回更快就直接采用。

soft 或相关软超时策略可能让应用在网络抖动时收到 I/O 错误。对某些只读、可重试、明确支持错误恢复的应用,它可能有价值;对普通文件写入则可能造成部分操作失败、数据损坏或应用误判。生产选择应以应用语义和故障演练为依据。

4.1 直接挂载与自动挂载的区别

直接挂载:

sudo mount server:/export/data /mnt/data

在挂载阶段就要完成网络解析、连接和服务端协商。自动挂载:

server:/export/data /mnt/data nfs4,x-systemd.automount,_netdev 0 0

启动时先建立本地挂载触发点,第一次访问 /mnt/data 时才访问服务端。这样可以缩短启动阶段,但第一次访问仍可能阻塞;它不是高可用,也不会在服务端永久不可用时自动创造一个本地副本。


五、NFS 缓存:客户端为什么不必每次读网络

5.1 三类主要缓存

NFS 客户端通常涉及三类缓存:

  1. 页缓存(page cache):缓存文件数据页。
  2. inode 属性缓存:缓存文件大小、时间戳、权限、所有者等属性。
  3. dentry 缓存:缓存目录项名称到 inode 的查找结果。

此外,NFSv4 还可能使用 delegation。服务端把某段打开或属性处理权委派给客户端,使客户端在一段时间内减少与服务端交互;服务端需要时可以召回 delegation。

缓存带来性能,但也带来观察延迟。客户端 A 写入文件后,客户端 B 不是必然立即看到变化,因为 B 可能仍使用本地页缓存、属性缓存或目录项缓存。

5.2 close-to-open 一致性

常见 NFS 实现通常提供接近“close-to-open”的一致性语义:

  • 一个客户端关闭文件后,另一个客户端随后打开该文件时,通常能够获得较新的内容;
  • 但这不是每次 read() 都强制重新验证;
  • 文件持续保持打开状态时,其他客户端的写入何时可见取决于缓存、写回、属性验证和具体访问模式;
  • 并发写入同一字节范围仍可能互相覆盖。

“close-to-open”是常见实现语义和工程模型,不应误认为等同于严格线性一致性。NFS 协议、客户端内核、挂载选项和服务端文件系统共同决定实际行为。

5.3 acactimeonoac

Linux NFS 客户端提供属性缓存控制选项:

-o actimeo=1

actimeo 将多个属性缓存时间参数统一设置为近似值,单位为秒。实现内部仍可能根据文件类型、元数据状态和服务器返回值进行处理,因此它不是“每隔 1 秒绝对刷新一次”的硬实时保证。

常见相关参数包括:

  • ac:启用属性缓存,通常是默认行为;
  • noac:禁用属性缓存。它会显著增加 RPC 和服务器负载,也不能把 NFS 变成事务系统;
  • lookupcache=none:减少目录查找缓存,可能改善某些多客户端目录可见性问题,但代价是更多查找请求;
  • cto / nocto:控制 close-to-open 相关处理,改变后可能改善特定工作负载性能,也可能扩大可见性窗口。

例如:

sudo mount -t nfs4 \
  -o vers=4.1,proto=tcp,hard,actimeo=1 \
  server:/export/data /mnt/data

降低 actimeo 的因果关系是:

  1. 属性缓存更快过期;
  2. 客户端更频繁地向服务端验证文件属性;
  3. 属性变化更快可见;
  4. RPC 数量和服务端压力增加;
  5. 网络抖动和服务端延迟对应用的影响更明显。

因此 actimeo=0noac 不能作为“解决并发正确性”的通用开关。应用仍需要锁、临时文件加 rename()、版本号、数据库事务或其他协调机制。

5.4 写入缓存、syncfsync()

应用执行:

write(fd, buf, len);

通常只表示数据已经被内核接受,不一定表示服务端磁盘已经完成持久化。应用调用:

fsync(fd);

是向文件系统请求把相关数据和必要元数据推进到稳定存储语义。NFS 客户端会根据协议和服务端能力发送相应请求;服务端导出 sync 也会影响普通写请求的完成语义。

但应区分三件事:

  • 客户端进程看到 write() 返回成功
  • 服务端确认 NFS 写请求
  • 底层存储在服务端故障后仍保留数据

这三者不是天然相同。硬件缓存、服务端文件系统、RAID 控制器、断电保护和 fsync() 实现都会影响最终结果。


六、一致性:从可见性到并发写入

6.1 一个完整的缓存算例

假设文件 counter 初始内容为 0,客户端 A 和 B 都挂载同一导出。

时间线如下:

  1. A 打开 counter,读取 0,数据进入 A 的页缓存。
  2. B 打开 counter,也读取 0,数据进入 B 的页缓存。
  3. A 将内容改为 1,执行 write() 并关闭。
  4. B 因为仍持有有效属性和数据缓存,再次读取时可能暂时得到 0
  5. B 将 0 加一后写回 1
  6. 最终文件内容是 1,而不是两个增量操作期望的 2

这里失败的不是 NFS 的“加法功能”,而是应用把“读取、修改、写回”错误地当成了一个原子操作。形式化地说:

read(x) -> x
compute(x + 1) -> x'
write(x')

如果两个客户端都从同一个旧值 x=0 开始,则:

A: read(0), write(1)
B: read(0), write(1)
最终: 1

要得到 2,必须增加互斥锁、服务端原子操作或数据库事务,使两个修改序列不能同时基于同一个旧值执行。

6.2 锁并不等于所有写入自动安全

POSIX 建议锁通常通过 NFS 锁状态机制实现。应用可以使用:

flock /mnt/data/job.lock -c 'echo running; sleep 5'

或者在程序中使用 fcntl()/flock()。但必须满足:

  • 所有参与者都遵守同一种锁协议;
  • 锁覆盖整个“读取—修改—写回”临界区;
  • 应用没有绕过锁直接写同一文件;
  • 客户端和服务端的锁服务状态恢复正常。

锁只能协调遵守协议的参与者。它不能阻止另一个程序直接执行未加锁的 write()

对配置文件或生成文件,常见安全模式是:

  1. 写入同一目录下的临时文件;
  2. fsync() 临时文件;
  3. 使用 rename() 替换目标文件;
  4. 必要时对目录执行 fsync()

同一文件系统内的 rename() 通常具有原子替换语义,但它不等于跨目录、跨文件系统或跨客户端所有缓存立即刷新。

6.3 目录操作和文件数据的不同

目录操作通常通过服务端完成,mkdir()unlink()rename() 等具有文件系统定义的原子性边界;但客户端可能缓存目录项。因此:

  • 客户端 A 创建文件后,B 的 readdir() 不一定立即出现新文件;
  • B 可能暂时得到负 dentry 缓存,认为文件不存在;
  • lookupcache=none 或缩短属性缓存可以减少窗口,但不能替代应用级协调;
  • 多个客户端对同一目录高频创建、删除和扫描,会产生大量元数据 RPC。

6.4 NFS 不适合哪些一致性模型

如果应用要求:

  • 多行记录的事务提交;
  • 严格 compare-and-swap;
  • 跨多个文件的一致提交;
  • 强一致的计数器;
  • 高并发随机写并带严格读写顺序;

直接把普通文件放在 NFS 上并不能自动满足这些要求。应使用数据库、分布式 KV 存储、专用锁服务或应用层版本校验。NFS 提供的是远程文件系统接口,而不是通用分布式事务协议。


七、权限:UID/GID、root_squash、ACL 与 Kerberos

7.1 sec=sys 的身份模型

许多 NFS 挂载默认使用:

sec=sys

此时客户端在 RPC 中发送数值 UID、GID 和辅助组。服务端根据这些数字在本地文件系统检查权限。

假设:

服务端文件:-rw-r----- 1 1001 1001 report.txt
客户端用户:UID=1001,主组 GID=1001

即使客户端和服务端没有相同的用户名数据库,只要数值 UID/GID 对得上,服务端权限检查就可能允许访问。反过来,客户端显示用户名相同但 UID 不同,也可能被拒绝。

查看身份:

id alice
stat -c '%n %u %g %A' /mnt/project/report.txt
getent passwd 1001
getent group 1001

因此传统 sec=sys 的安全前提是:客户端受信任,且 UID/GID 管理一致。它不是强身份认证。恶意客户端如果能伪造 UID,可能冒充其他用户。

7.2 服务端文件权限仍然是最终检查的一部分

导出允许写入并不表示所有用户都能写:

/srv/nfs/project 192.0.2.0/24(rw,sync,root_squash)

最终访问仍受到服务端文件系统权限、ACL、SELinux 等机制影响。客户端执行:

echo test > /mnt/project/a.txt

可能失败并显示:

Permission denied

诊断时应同时检查:

ls -ld /srv/nfs/project
getfacl /srv/nfs/project
ls -Zd /srv/nfs/project       # 使用 SELinux 的系统

服务端目录需要允许对应 UID/GID 的访问;只修改客户端挂载参数不能绕过服务端权限。

7.3 root_squash 的实际效果

若客户端 root 访问导出目录:

sudo touch /mnt/project/from-client-root

在启用 root_squash 时,服务端通常将 UID 0 映射为匿名 UID/GID,常见为 nobody:nogroup 或发行版对应的匿名账号。因此可能出现:

touch: cannot touch '/mnt/project/from-client-root': Permission denied

如果目录对匿名身份可写,则文件可能被创建,但所有者通常不是服务端 root。匿名 UID/GID 可通过 anonuidanongid 等选项定制,但这会改变安全边界,应配合目录权限和测试使用。

7.4 NFSv4 身份名称映射

NFSv4 可以使用类似:

alice@example.com

这样的身份名称,而不是只依赖数值 UID。客户端和服务端通常需要正确配置 idmapd 的域,具体配置文件位置和服务名称受发行版影响。

身份映射错误的表现可能包括:

  • ls -l 显示 nobody
  • 文件能访问但所有者显示异常;
  • 设置 ACL 失败;
  • 仅某些用户访问失败。

这类问题不能只检查 /etc/passwd,还要检查:

systemctl status nfs-idmapd 2>/dev/null || true
grep -v '^[[:space:]]*#' /etc/idmapd.conf 2>/dev/null
nfsstat -m

NFSv4 ACL 与 POSIX ACL 不是完全相同的模型。服务端文件系统、NFS 服务实现和客户端工具对 ACL 的支持存在差异,不能把 Windows 风格 NFSv4 ACL、POSIX ACL 和普通 mode bits 当成完全等价的权限系统。

7.5 Kerberos:sec=krb5krb5ikrb5p

在具备 Kerberos 基础设施的环境中,可以使用:

  • sec=krb5:认证身份;
  • sec=krb5i:认证并提供完整性保护;
  • sec=krb5p:认证、完整性保护并加密传输。

挂载示例:

sudo mount -t nfs4 \
  -o vers=4.1,sec=krb5p,hard \
  nfs.example.com:/export/secure /mnt/secure

这要求客户端、服务端、DNS、时间同步、Kerberos principal、keytab 和凭据获取都正确。krb5p 通常会增加 CPU 和网络处理开销;是否采用应根据威胁模型和性能测试决定。


八、NFS 的故障行为:为什么一个挂载点可能“卡死”

8.1 hardsoft

使用 hard 时,如果服务端暂时不可达,NFS 请求会重试。应用可能在系统调用中长时间阻塞,例如:

cat /mnt/project/large-file

服务端断网后,该命令可能不立即返回错误。对需要数据正确性的写入场景,这种行为通常比静默返回错误更安全,但会让:

  • Web 请求线程被占用;
  • 数据库进程卡在 I/O;
  • umount 等操作等待;
  • 关机过程变慢。

使用 soft 时,超时后可能向应用返回错误。错误返回看似更容易处理,但很多应用没有正确处理部分读写、重试和重连,可能留下损坏文件。softrevalsofterr 等选项在不同内核和发行版中的支持与默认行为可能不同,不能脱离目标系统测试。

8.2 intr 的误解

旧资料经常建议使用 intr 中断 NFS 阻塞。现代 Linux 内核中,intr/nointr 已经不是解决 NFS 阻塞的主要手段,相关行为和历史版本不同。应通过合理的超时、服务端高可用、应用超时和故障演练解决问题,而不是照抄旧版挂载参数。

8.3 卸载失败与恢复

查看谁正在使用挂载点:

sudo fuser -vm /mnt/project
sudo lsof +D /mnt/project

如果服务端正常、进程已退出但挂载仍存在:

sudo umount /mnt/project

如果服务端不可达,普通卸载可能阻塞。umount -f 对 NFS 的效果依赖内核和具体状态,不能保证立即消除所有引用;umount -l 是延迟卸载,会从命名空间移除挂载点,但仍可能让已有进程持有底层引用。它们都不应作为数据一致性修复手段。

恢复的基本顺序是:

  1. 确认服务端和网络是否恢复;
  2. 检查客户端是否仍有阻塞进程;
  3. 让应用停止访问;
  4. 再执行卸载或重启相关服务;
  5. 检查文件是否有部分写入或应用级恢复需求。

九、NFS 高可用:解决的到底是哪一层故障

“高可用 NFS”不是一个单独开关,而是至少包含四层:

  1. 客户端到服务端网络路径
  2. NFS 服务进程
  3. 服务端本地文件系统和后端存储
  4. 服务端身份、锁状态和故障切换过程

一个 VIP 漂移只能解决“客户端访问的 IP 从故障节点转移到存活节点”,不能自动复制数据,也不能自动恢复 NFSv4 的打开和锁状态。

9.1 常见 HA 架构

一种典型架构如下:

flowchart LR
    C1[NFS 客户端 A] --> VIP[服务虚拟 IP]
    C2[NFS 客户端 B] --> VIP
    VIP --> N1[NFS 节点 1]
    VIP --> N2[NFS 节点 2]
    N1 --> S[共享存储或可切换存储]
    N2 --> S
    CM[集群管理器/心跳] --> N1
    CM --> N2
    CM --> VIP

要使这种架构正确,至少需要:

  • 共享存储或经过验证的复制存储;
  • 同一时间只有一个节点以可写方式使用对应文件系统,除非底层文件系统明确支持并发集群访问;
  • VIP 漂移;
  • 集群管理器控制资源顺序;
  • 故障节点 fencing(隔离);
  • NFS 服务状态和锁状态的恢复设计;
  • 客户端挂载和超时策略的验证。

9.2 fencing 为什么是必要条件

假设节点 1 与节点 2 之间发生网络分区:

  • 节点 1 认为节点 2 已故障;
  • 节点 2 也认为节点 1 已故障;
  • 两者都抢占 VIP 或共享存储;
  • 两个节点同时写入同一个非集群文件系统。

这就是 split-brain(脑裂)。即使底层是 RAID 或高性能 SAN,也不能阻止两个文件系统实例破坏元数据。

fencing 的目标是先确保旧节点不能继续访问共享资源,再允许新节点接管。可使用电源管理、云平台实例隔离、存储侧 SCSI-3 PR 等机制,具体方案依赖基础设施。没有可靠 fencing 的双节点可写 HA,不能仅凭心跳和 VIP 宣称安全。

9.3 NFSv3 与 NFSv4 的故障切换差异

NFSv3 相对无状态,故障切换时通常更容易重新建立访问路径,但锁服务和挂载状态仍需设计。NFSv4 包含打开、锁、租约和委派等状态,服务端切换后客户端需要重新声明状态;服务端还需要正确处理 grace period。

因此以下现象并不罕见:

  • 文件读写恢复,但锁恢复失败;
  • 应用收到 ESTALEEIO 或超时;
  • 旧节点恢复后仍持有共享存储访问权;
  • 客户端长期卡在旧连接上;
  • 文件句柄在后端文件系统切换后失效。

ESTALE 通常表示客户端持有的文件句柄在服务端已经不再有效,例如文件系统重新导出、后端对象更换或文件被删除后重建。重新挂载可能恢复路径访问,但应用持有的旧文件描述符和状态不一定能透明恢复。

9.4 复制存储不等于文件系统一致

如果两个 NFS 节点各自拥有文件系统副本,通过块复制或文件复制同步,必须确认复制层和文件系统层的组合是否支持目标模式:

  • 同步复制还是异步复制;
  • 故障切换时最后确认写入是否存在;
  • 文件系统是否只在一个节点挂载;
  • 复制延迟期间客户端能否继续写入;
  • 切换后文件句柄和 inode 是否保持兼容;
  • 锁和租约如何恢复。

异步复制意味着故障时可能丢失最近一段已确认写入;这不是 NFS 客户端挂载选项可以弥补的。


十、性能:网络、RPC、元数据和后端存储共同决定结果

NFS 性能不能只看链路带宽。一个大文件顺序读主要受数据吞吐影响,而编译、容器镜像、源码树扫描和小文件 workload 往往受元数据延迟影响。

可以用以下命令观察:

nfsstat -c
nfsstat -s
nfsiostat 1
iostat -xz 1
sar -n DEV 1

其中:

  • nfsstat -c:客户端 RPC、读写、重传等统计;
  • nfsstat -s:服务端 NFS 统计;
  • nfsiostat 1:按挂载点观察 NFS I/O 和延迟;
  • iostat -xz 1:观察服务端或客户端本地块设备;
  • sar -n DEV 1:观察网卡流量。

常见误判包括:

  • 看到网络带宽不高,就认为 NFS 很慢;实际上可能是元数据请求多;
  • 只调大 rsizewsize,却忽略 RTT、服务端磁盘和 RPC 重传;
  • noac 解决可见性问题,结果服务端被大量 GETATTR 和查找请求压垮;
  • 把 NFS 挂载在数据库数据文件上,却没有测试 fsync、锁和故障恢复;
  • 把多个客户端共享目录当成本地高速文件系统,忽略目录锁和缓存失效成本。

rsizewsize 等参数的可用范围和默认值受 Linux 内核、NFS 版本、服务端限制和协商结果影响。现代系统通常能自动选择合理值,应先通过 nfsstat -m 确认实际值,再基于基准测试调整。


十一、诊断方法:按数据流定位,而不是只看“挂载成功”

11.1 先确认客户端实际状态

findmnt -T /mnt/project
nfsstat -m
mountpoint /mnt/project
stat /mnt/project

需要确认:

  • 是否真的挂载,而不是访问了底层空目录;
  • 实际使用的是 NFSv3 还是 NFSv4;
  • 实际使用 hard 还是其他超时策略;
  • 使用的是 sec=sys 还是 Kerberos;
  • 是否启用了特殊缓存参数。

11.2 再确认服务端导出和 RPC

服务端:

sudo exportfs -v
sudo systemctl status nfs-server 2>/dev/null || \
sudo systemctl status nfs-kernel-server
sudo ss -lntup | grep -E '(:2049|rpcbind|mountd)'

客户端:

rpcinfo -p server.example.com
showmount -e server.example.com

showmount 主要适用于 NFSv3 的挂载服务查询,对 NFSv4 不应作为唯一判断依据;NFSv4 常见问题应直接检查 2049、导出伪根、服务端日志和实际挂载结果。

11.3 权限问题的最小验证

在客户端:

id
namei -l /mnt/project/file
ls -ln /mnt/project/file

在服务端:

ls -ldn /srv/nfs/project
getfacl /srv/nfs/project

ls -ln 使用数字 UID/GID,能绕过用户名显示差异,直接判断数值身份是否一致。若客户端显示用户为 alice,但数字 UID 与服务端文件所有者不同,问题通常不是用户名字符串,而是身份数据库或映射不一致。

11.4 观察内核和服务日志

dmesg -T | grep -iE 'nfs|rpc|lock|stale'
journalctl -k -b | grep -iE 'nfs|rpc|lock'
journalctl -u nfs-server -b 2>/dev/null

常见错误含义:

  • mount.nfs: access denied by server:导出范围、版本、认证方式或服务端权限不匹配;
  • No route to host:路由或防火墙问题;
  • Connection timed out:服务端不可达、端口被过滤或路径故障;
  • Stale file handle:文件句柄对应的远端对象或文件系统状态已改变;
  • Permission denied:服务端 mode bits、ACL、UID/GID、root squash 或安全模块拒绝;
  • 进程长时间处于 D 状态:可能在等待不可中断的 NFS I/O,不代表进程逻辑死循环。

十二、安全边界和生产取舍

NFS 导出应尽可能限制客户端范围:

/srv/nfs/project 192.0.2.10(rw,sync,root_squash,sec=sys)
/srv/nfs/project 192.0.2.11(ro,sync,root_squash,sec=sys)

同一目录对不同客户端设置不同权限时,要明确验证导出规则匹配顺序和客户端地址识别,避免宽泛网段覆盖了更严格的规则。

不要把 NFS 直接暴露到不可信网络。至少应考虑:

  • 网络访问控制和防火墙;
  • 仅允许受控客户端;
  • root_squash
  • Kerberos 认证或完整性/加密保护;
  • 服务端文件系统权限和 ACL;
  • 日志、审计与凭据管理;
  • DNS 和时间同步的可靠性。

NFSv4 的 sec=sys 便于部署,但信任的是客户端提供的数值身份;krb5ikrb5p 能提高身份与传输安全,却会引入 Kerberos 运维和性能成本。安全方案应与网络边界、数据敏感性和故障恢复要求一起设计。


十三、一个可验证的端到端实验

下面实验可在一台服务端和一台客户端之间执行。假设:

  • 服务端地址:192.0.2.10
  • 客户端地址:192.0.2.20
  • 服务端目录:/srv/nfs/lab

服务端执行:

sudo install -d -m 0770 -o root -g users /srv/nfs/lab
echo 'nfs-lab' | sudo tee /srv/nfs/lab/server-file >/dev/null
echo '/srv/nfs/lab 192.0.2.20(rw,sync,root_squash,no_subtree_check)' | \
  sudo tee /etc/exports.d/lab.exports

sudo exportfs -rav
sudo exportfs -v

客户端执行:

sudo install -d /mnt/nfs-lab
sudo mount -t nfs4 -o vers=4.1,hard,proto=tcp \
  192.0.2.10:/srv/nfs/lab /mnt/nfs-lab

findmnt /mnt/nfs-lab
cat /mnt/nfs-lab/server-file

预期看到:

nfs-lab

验证权限身份:

ls -ln /mnt/nfs-lab
id

测试写入:

echo "$(hostname) $(date -Is)" | sudo tee /mnt/nfs-lab/client-file
ls -ln /mnt/nfs-lab/client-file

如果写入失败,应按顺序检查:

  1. 客户端是否确实使用了该挂载点;
  2. 服务端导出是否包含客户端地址;
  3. 客户端使用的 UID/GID 是否有服务端目录权限;
  4. root 是否被 root_squash 映射;
  5. 服务端 SELinux 或其他安全模块是否拒绝;
  6. NFS 版本、认证方式和防火墙是否匹配。

实验结束后:

sudo umount /mnt/nfs-lab
sudo rm -f /etc/exports.d/lab.exports
sudo exportfs -rav

这个实验只验证“导出、挂载、读取、写入和权限路径”是否连通,不证明生产环境下的并发一致性、断网恢复、锁恢复或高可用切换是正确的。那些问题必须通过多客户端并发测试、服务端重启、网络隔离、存储切换和应用恢复测试分别验证。


十四、如何选择 NFS 版本和部署模式

可以按问题边界选择:

  • 需要简单共享目录、环境兼容性高:优先评估 NFSv4,必要时兼容 NFSv3。
  • 需要统一认证和更完整的状态管理:使用 NFSv4 配合 Kerberos。
  • 需要高吞吐并行访问:评估 NFSv4.1/pNFS,但必须确认完整栈支持。
  • 需要严格事务和并发更新:不要只依赖 NFS 缓存和锁,使用数据库或应用级协议。
  • 需要服务端故障切换:先设计共享或复制存储、fencing、VIP、锁状态和客户端恢复,再选择 NFS 版本。
  • 需要块级访问:使用块存储协议,而不是把 NFS 当作远端磁盘。

NFS 的核心价值是把远端文件系统接入本地 VFS;它的核心限制也来自这里:客户端会缓存,权限由远端文件系统最终检查,网络故障会暴露到系统调用,多个客户端之间只有有限的文件一致性模型,而高可用必须同时处理数据、身份、锁、文件句柄和故障切换顺序。 понимание这些边界后,挂载参数和导出选项才有明确的因果意义,而不是一组需要机械复制的字符串。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。