NFS LOCALIO

概述

LOCALIO 辅助 RPC 协议允许 Linux NFS 客户端和服务器可靠地握手,以确定它们是否位于同一主机上。在 menuconfig 中选择“NFS client and server support for LOCALIO auxiliary protocol”以在内核配置中启用 CONFIG_NFS_LOCALIO(CONFIG_NFS_FS 和 CONFIG_NFSD 也必须启用)。

一旦 NFS 客户端和服务器握手确定为“本地”,客户端将绕过网络 RPC 协议进行读、写和提交操作。由于这种 XDR 和 RPC 绕过,这些操作将运行得更快。

LOCALIO 辅助协议的实现使用了与 NFS 流量相同的连接,遵循了 NFS ACL 协议扩展建立的模式。

LOCALIO 辅助协议是实现客户端可靠发现其本地服务器所必需的。在 LOCALIO 协议使用之前的一个私有实现中,曾尝试基于 sockaddr 网络地址与所有本地网络接口进行脆弱的匹配。但与 LOCALIO 协议不同,基于 sockaddr 的匹配无法处理 iptables 或容器的使用。

本地客户端和服务器之间强大的握手只是开始,这种本地性实现的最终用例是客户端能够直接向服务器打开文件并发出读、写和提交操作,而无需通过网络。要求是尽可能高效地执行这些回环 NFS 操作,这对于容器用例(例如 Kubernetes)特别有用,在这些用例中,可以在服务器本地运行 IO 作业。

LOCALIO 能够绕过 XDR 和 RPC 进行读、写和提交操作所实现的性能优势可能非常显著,例如:

fio,directio,qd 为 8,16 个 libaio 线程,运行 20 秒
  • 使用 LOCALIO:4K 读取:IOPS=979k, 带宽=3825MiB/s (4011MB/s)(74.7GiB/20002msec) 4K 写入:IOPS=165k, 带宽=646MiB/s (678MB/s)(12.6GiB/20002msec) 128K 读取:IOPS=402k, 带宽=49.1GiB/s (52.7GB/s)(982GiB/20002msec) 128K 写入:IOPS=11.5k, 带宽=1433MiB/s (1503MB/s)(28.0GiB/20004msec)

  • 不使用 LOCALIO:4K 读取:IOPS=79.2k, 带宽=309MiB/s (324MB/s)(6188MiB/20003msec) 4K 写入:IOPS=59.8k, 带宽=234MiB/s (245MB/s)(4671MiB/20002msec) 128K 读取:IOPS=33.9k, 带宽=4234MiB/s (4440MB/s)(82.7GiB/20004msec) 128K 写入:IOPS=11.5k, 带宽=1434MiB/s (1504MB/s)(28.0GiB/20011msec)

fio,directio,qd 为 8,1 个 libaio 线程,运行 20 秒
  • 使用 LOCALIO:4K 读取:IOPS=230k, 带宽=898MiB/s (941MB/s)(17.5GiB/20001msec) 4K 写入:IOPS=22.6k, 带宽=88.3MiB/s (92.6MB/s)(1766MiB/20001msec) 128K 读取:IOPS=38.8k, 带宽=4855MiB/s (5091MB/s)(94.8GiB/20001msec) 128K 写入:IOPS=11.4k, 带宽=1428MiB/s (1497MB/s)(27.9GiB/20001msec)

  • 不使用 LOCALIO:4K 读取:IOPS=77.1k, 带宽=301MiB/s (316MB/s)(6022MiB/20001msec) 4K 写入:IOPS=32.8k, 带宽=128MiB/s (135MB/s)(2566MiB/20001msec) 128K 读取:IOPS=24.4k, 带宽=3050MiB/s (3198MB/s)(59.6GiB/20001msec) 128K 写入:IOPS=11.4k, 带宽=1430MiB/s (1500MB/s)(27.9GiB/20001msec)

常见问题

  1. LOCALIO 的用例有哪些?

    1. 当 NFS 客户端和服务器位于同一主机上的工作负载能够实现改进的 IO 性能。特别是在运行容器化工作负载时,作业通常会发现自己与用于存储的 knfsd 服务器运行在同一主机上。

  2. LOCALIO 的要求是什么?

    1. 尽可能绕过网络 RPC 协议。这包括绕过 XDR 和 RPC 进行打开、读取、写入和提交操作。

    2. 允许客户端和服务器自主发现它们是否在彼此的本地运行,而无需对本地网络拓扑做出任何假设。

    3. 通过与相关命名空间(例如网络、用户、挂载)兼容来支持容器的使用。

    4. 支持所有 NFS 版本。NFSv3 尤其重要,因为它在企业中广泛使用,并且 pNFS flexfiles 利用它作为数据路径。

  3. 为什么 LOCALIO 在决定 NFS 客户端和服务器是否位于同一主机上时不直接比较 IP 地址或主机名?

    由于其中一个主要用例是容器化工作负载,我们不能假设客户端和服务器之间会共享 IP 地址。这就提出了对握手协议的要求,该协议需要通过与 NFS 流量相同的连接进行,以便识别客户端和服务器是否确实在同一主机上运行。握手使用一个通过网络发送的秘密,如果客户端和服务器确实位于同一位置,双方可以通过与共享内核内存中存储的值进行比较来验证该秘密。

  4. LOCALIO 能否改进 pNFS flexfiles?

    是的,LOCALIO 通过允许 pNFS flexfiles 利用 NFS 客户端和服务器的本地性来对其进行补充。将客户端 IO 尽可能地发起在数据存储的服务器附近,这样的策略自然会受益于 LOCALIO 提供的数据路径优化。

  5. 为什么不开发一个新的 pNFS 布局来启用 LOCALIO?

    可以开发一个新的 pNFS 布局,但这样做会将发现客户端是否共置的责任放在服务器上,以便决定分发布局。更简单的方法(如 LOCALIO 提供的那样)更有价值,它允许 NFS 客户端协商和利用本地性,而无需以更集中的方式进行更复杂的建模和发现。

  6. 让客户端在不使用 RPC 的情况下执行服务器端文件 OPEN 有何益处?这种益处是 pNFS 特有的吗?

    无论是否使用 pNFS,避免在文件打开时使用 XDR 和 RPC 都有助于提高性能。特别是处理小文件时,最好尽可能避免网络传输,否则可能会减少甚至抵消避免网络传输本身带来的小文件 I/O 优势。鉴于 LOCALIO 的要求,目前让客户端在不使用 RPC 的情况下执行服务器端文件打开的方法是理想的。如果将来需求发生变化,我们可以相应地进行调整。

  7. 为什么 LOCALIO 仅支持 UNIX 认证 (AUTH_UNIX)?

    强认证通常与连接本身绑定。它通过建立一个由服务器缓存的上下文来工作,该上下文作为发现授权令牌的密钥,然后该令牌可以传递给 rpc.mountd 来完成认证过程。另一方面,在 AUTH_UNIX 的情况下,通过网络传递的凭证直接用作向上调用 rpc.mountd 的密钥。这简化了认证过程,从而使 AUTH_UNIX 更易于支持。

  8. 将 RPC 用户 ID 转换的导出选项(例如 root_squash, all_squash)在 LOCALIO 操作中如何表现?

    翻译用户 ID 的导出选项由 nfsd_setuser() 管理,该函数由 nfsd_setuser_and_check_port() 调用,而 nfsd_setuser_and_check_port() 又由 __fh_verify() 调用。因此,它们对 LOCALIO 的处理方式与非 LOCALIO 完全相同。

  9. 鉴于 NFSD 和 NFS 在不同的上下文中运行,LOCALIO 如何确保对象生命周期得到妥善管理?

    请参阅下面的详细“NFS 客户端和服务器互锁”部分。

RPC

LOCALIO 辅助 RPC 协议包含一个单独的“UUID_IS_LOCAL”RPC 方法,该方法允许 Linux NFS 客户端验证本地 Linux NFS 服务器是否能够看到客户端生成并放入 nfs_common 中的 nonce(一次性 UUID)。此协议并非 IETF 标准的一部分,也无需是,因为它是一个 Linux 到 Linux 的辅助 RPC 协议,属于实现细节。

UUID_IS_LOCAL 方法根据固定的 UUID_SIZE(16 字节)编码客户端生成的 uuid_t。使用固定大小的 opaque 编码和解码 XDR 方法,而不是效率较低的变长方法。

NFS_LOCALIO_PROGRAM 的 RPC 程序号是 400122(由 IANA 分配,参见 https://www.iana.org/assignments/rpc-program-numbers/):Linux Kernel Organization 400122 nfslocalio

rpcgen 语法中的 LOCALIO 协议规范是

/* raw RFC 9562 UUID */
#define UUID_SIZE 16
typedef u8 uuid_t<UUID_SIZE>;

program NFS_LOCALIO_PROGRAM {
    version LOCALIO_V1 {
        void
            NULL(void) = 0;

        void
            UUID_IS_LOCAL(uuid_t) = 1;
    } = 1;
} = 400122;

LOCALIO 使用与 NFS 流量相同的传输连接。因此,LOCALIO 未在 rpcbind 中注册。

NFS Common 和客户端/服务器握手

fs/nfs_common/nfslocalio.c 提供了接口,使 NFS 客户端能够生成一个 nonce(一次性 UUID)及相关的短生命周期 nfs_uuid_t 结构体,并将其注册到 nfs_common 中,以便 NFS 服务器后续查找和验证,如果匹配,NFS 服务器会填充 nfs_uuid_t 结构体中的成员。然后 NFS 客户端使用 nfs_common 将 nfs_uuid_t 从其 nfs_uuids 列表传输到 nfs_common 的 uuids_list 中的 nn->nfsd_serv clients_list。参见:fs/nfs/localio.c:nfs_local_probe()

nfs_common 的 nfs_uuids 列表是启用 LOCALIO 的基础,因此它具有指向 nfsd 内存的成员,供客户端直接使用(例如,'net' 是服务器的网络命名空间,通过它客户端可以以适当的 RCU 读取访问权限访问 nn->nfsd_serv)。正是这种客户端和服务器的同步,使得高级用法和对象生命周期能够从宿主内核的 nfsd 扩展到连接到在同一本地主机上运行的 nfs 客户端的每个容器 knfsd 实例。

NFS 客户端和服务器互锁

LOCALIO 提供 nfs_uuid_t 对象和相关接口,以实现正确的网络命名空间 (net-ns) 和 NFSD 对象引用计数。

LOCALIO 需要引入和使用 NFSD 的 percpu nfsd_net_ref 来互锁 nfsd_shutdown_net() 和 nfsd_open_local_fh(),以确保每个 net-ns 在被 nfsd_open_local_fh() 使用时不会被销毁,这需要更详细的解释。

nfsd_open_local_fh() 在打开其 nfsd_file 句柄之前使用 nfsd_net_try_get(),然后调用方(NFS 客户端)在完成 IO 后必须使用 nfsd_file_put_local() 释放 nfsd_file 和相关 net-ns 的引用。

这种互锁工作在很大程度上依赖于 nfsd_open_local_fh() 能够安全地处理 NFSD 的 net-ns(以及关联的 nfsd_net)可能已被 nfsd_destroy_serv() 通过 nfsd_shutdown_net() 销毁的情况。

这种 NFS 客户端和服务器的互锁已被验证可以修复一个容易发生的崩溃问题,即当容器中运行的 NFSD 实例在挂载 LOCALIO 客户端的情况下关闭时会发生崩溃。在容器和相关 NFSD 重启后,客户端会因为 LOCALIO 客户端尝试调用 nfsd_open_local_fh() 而没有正确引用 NFSD 的 net-ns,导致空指针解引用而崩溃。

NFS 客户端而非服务器发出 IO

由于 LOCALIO 专注于协议绕过以实现改进的 IO 性能,因此必须提供替代传统 NFS 网络协议(带 XDR 的 SUNRPC)的方式来访问后端文件系统。

请参阅 fs/nfs/localio.c:nfs_local_open_fh() 和 fs/nfsd/localio.c:nfsd_open_local_fh(),了解该接口如何集中使用选定的 nfs 服务器对象,以允许位于服务器本地的客户端打开文件指针而无需通过网络。

客户端的 fs/nfs/localio.c:nfs_local_open_fh() 将调用服务器的 fs/nfsd/localio.c:nfsd_open_local_fh(),并谨慎地通过 RCU 访问相关的 nfsd 网络命名空间和 nn->nfsd_serv。如果 nfsd_open_local_fh() 发现客户端不再看到有效的 nfsd 对象(无论是 struct net 还是 nn->nfsd_serv),它将向 nfs_local_open_fh() 返回 -ENXIO,客户端将尝试通过再次调用 nfs_local_probe() 来重新建立所需的 LOCALIO 资源。当容器中运行的 nfsd 实例在 LOCALIO 客户端连接到它时重启,就需要这种恢复。

一旦客户端获得一个打开的 nfsd_file 指针,它将直接对底层本地文件系统(通常由 nfs 服务器完成)发出读、写和提交操作。因此,对于这些操作,NFS 客户端正在对其与 NFS 服务器共享的底层本地文件系统发出 IO。参见:fs/nfs/localio.c:nfs_local_doio() 和 fs/nfs/localio.c:nfs_local_commit()。

在传统使用 RPC 向服务器发出 IO 的 NFS 中,如果应用程序使用 O_DIRECT,NFS 客户端将绕过页缓存,但 NFS 服务器不会。NFS 服务器使用缓冲 IO,使得应用程序在向 NFS 客户端发出 IO 时对齐要求不那么严格。但如果所有应用程序都能正确对齐其 IO,LOCALIO 可以通过将 'localio_O_DIRECT_semantics' nfs 模块参数设置为 Y 来配置,以使用从 NFS 客户端到它与 NFS 服务器共享的底层本地文件系统的端到端 O_DIRECT 语义,例如:

echo Y > /sys/module/nfs/parameters/localio_O_DIRECT_semantics

启用后,它将使 LOCALIO 使用端到端 O_DIRECT 语义(但请注意,如果应用程序未能正确对齐其 IO,这可能会导致 IO 失败)。

安全性

LOCALIO 仅在使用 UNIX 风格认证(AUTH_UNIX,也称 AUTH_SYS)时受支持。

无论使用 LOCALIO 还是常规 NFS 访问,都会注意确保使用相同的 NFS 安全机制(认证等)。作为传统 NFS 客户端访问 NFS 服务器的一部分建立的 auth_domain 也用于 LOCALIO。

相对于容器,LOCALIO 允许客户端访问服务器的网络命名空间。这是允许客户端访问服务器的每个命名空间 nfsd_net 结构所必需的。对于传统 NFS,客户端也享有同等访问权限(尽管是通过 SUNRPC 的 NFS 协议)。没有其他命名空间(用户、挂载等)从服务器到客户端被更改或特意扩展。

模块参数

/sys/module/nfs/parameters/localio_enabled (布尔值) 控制 LOCALIO 是否启用,默认为 Y。如果客户端和服务器是本地的,但‘localio_enabled’设置为 N,则 LOCALIO 将不会被使用。

/sys/module/nfs/parameters/localio_O_DIRECT_semantics (布尔值) 控制 O_DIRECT 是否扩展到底层文件系统,默认为 N。应用程序 IO 必须与逻辑块大小对齐,否则 O_DIRECT 将失败。

/sys/module/nfsv3/parameters/nfs3_localio_probe_throttle (无符号整型) 控制 NFSv3 读写 IO 是否每 N (nfs3_localio_probe_throttle) 次 IO 触发 LOCALIO 的(重新)启用,默认为 0(禁用)。必须是 2 的幂次方,如果配置错误(值过低或非 2 的幂次方),管理员将承担所有后果。

测试

LOCALIO 辅助协议及相关的 NFS LOCALIO 读、写和提交访问已在各种测试场景中证明稳定

  • 客户端和服务器都在同一主机上。

  • 本地和远程客户端和服务器的所有客户端和服务器支持启用组合。

  • 还对不支持 LOCALIO 协议的 NFS 存储产品进行了测试。

  • 主机上的客户端,容器内的服务器(适用于 v3 和 v4.2)。容器测试是针对 podman 管理的容器进行的,并包括成功的容器停止/重启场景。

  • 将这些测试场景在现有测试基础设施中正式化正在进行中。初步的定期覆盖是通过 ktest 针对启用 LOCALIO 的 NFS 环回挂载配置运行 xfstests 提供,并包括 lockdep 和 KASAN 覆盖,参见:https://evilpiepirate.org/~testdashboard/ci?user=snitzer&branch=snitm-nfs-next https://github.com/koverstreet/ktest

  • 已进行了各种 kdevops 测试(通过“Chuck's BuildBot”)以定期验证 LOCALIO 更改未对非 LOCALIO NFS 用例造成任何回归。

  • 启用 LOCALIO 后,Hammerspace 的各种健全性测试均通过(这包括大量的 pNFS 和 flexfiles 测试)。