NFS LOCALIO¶
概述¶
LOCALIO 辅助 RPC 协议允许 Linux NFS 客户端和服务器可靠地握手,以确定它们是否在同一主机上。在 menuconfig 中选择“NFS client and server support for LOCALIO auxiliary protocol”(NFS 客户端和服务器对 LOCALIO 辅助协议的支持)来启用内核配置中的 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 测试 20 秒,使用 directio、qd 为 8、16 个 libaio 线程
使用 LOCALIO: 4K 读取:IOPS=979k,BW=3825MiB/s (4011MB/s)(74.7GiB/20002msec) 4K 写入:IOPS=165k,BW=646MiB/s (678MB/s)(12.6GiB/20002msec) 128K 读取:IOPS=402k,BW=49.1GiB/s (52.7GB/s)(982GiB/20002msec) 128K 写入:IOPS=11.5k,BW=1433MiB/s (1503MB/s)(28.0GiB/20004msec)
不使用 LOCALIO: 4K 读取:IOPS=79.2k,BW=309MiB/s (324MB/s)(6188MiB/20003msec) 4K 写入:IOPS=59.8k,BW=234MiB/s (245MB/s)(4671MiB/20002msec) 128K 读取:IOPS=33.9k,BW=4234MiB/s (4440MB/s)(82.7GiB/20004msec) 128K 写入:IOPS=11.5k,BW=1434MiB/s (1504MB/s)(28.0GiB/20011msec)
- fio 测试 20 秒,使用 directio、qd 为 8、1 个 libaio 线程
使用 LOCALIO: 4K 读取:IOPS=230k,BW=898MiB/s (941MB/s)(17.5GiB/20001msec) 4K 写入:IOPS=22.6k,BW=88.3MiB/s (92.6MB/s)(1766MiB/20001msec) 128K 读取:IOPS=38.8k,BW=4855MiB/s (5091MB/s)(94.8GiB/20001msec) 128K 写入:IOPS=11.4k,BW=1428MiB/s (1497MB/s)(27.9GiB/20001msec)
不使用 LOCALIO: 4K 读取:IOPS=77.1k,BW=301MiB/s (316MB/s)(6022MiB/20001msec) 4K 写入:IOPS=32.8k,BW=128MiB/s (135MB/s)(2566MiB/20001msec) 128K 读取:IOPS=24.4k,BW=3050MiB/s (3198MB/s)(59.6GiB/20001msec) 128K 写入:IOPS=11.4k,BW=1430MiB/s (1500MB/s)(27.9GiB/20001msec)
常见问题解答¶
LOCALIO 的用例是什么?
NFS 客户端和服务器在同一主机上的工作负载可提高 IO 性能。 特别是在运行容器化工作负载时,作业通常会发现自己与用于存储的 knfsd 服务器在同一主机上运行。
LOCALIO 的要求是什么?
尽可能绕过网络 RPC 协议的使用。 这包括绕过用于打开、读取、写入和提交操作的 XDR 和 RPC。
允许客户端和服务器自主发现它们是否在本地相互运行,而不对本地网络拓扑结构进行任何假设。
通过与相关的命名空间(例如网络、用户、挂载)兼容来支持容器的使用。
支持所有版本的 NFS。NFSv3 尤其重要,因为它具有广泛的企业用途,并且 pNFS flexfiles 将其用于数据路径。
当决定 NFS 客户端和服务器是否位于同一主机上时,为什么 LOCALIO 不只是比较 IP 地址或主机名?
由于主要用例之一是容器化工作负载,因此我们不能假设 IP 地址将在客户端和服务器之间共享。 这就设定了对握手协议的要求,该协议需要在与 NFS 流量相同的连接上进行,以便识别客户端和服务器是否真的在同一主机上运行。 握手使用通过线路发送的密钥,并且如果它们确实位于同一位置,则可以通过与存储在共享内核内存中的值进行比较来由双方验证。
LOCALIO 是否改进了 pNFS flexfiles?
是的,LOCALIO 通过允许它利用 NFS 客户端和服务器的本地性来补充 pNFS flexfiles。 将客户端 IO 发起得尽可能靠近存储数据的服务器的策略自然会受益于 LOCALIO 提供的数据路径优化。
为什么不开发新的 pNFS 布局来启用 LOCALIO?
可以开发新的 pNFS 布局,但是这样做会将责任推给服务器,以某种方式发现在决定分配布局时客户端是否位于同一位置。 简单方法(如 LOCALIO 提供的方法)的价值在于,它允许 NFS 客户端在不需要以更集中的方式对这种本地性进行更精细的建模和发现的情况下,协商并利用本地性。
为什么让客户端执行服务器端文件打开(不使用 RPC)是有益的? 该好处是否是 pNFS 特有的?
无论是否使用 pNFS,避免将 XDR 和 RPC 用于文件打开都有利于性能。 特别是在处理小文件时,最好尽可能避免通过线路传输,否则可能会减少甚至抵消避免通过线路进行小文件 I/O 本身的好处。 考虑到 LOCALIO 的要求,当前让客户端执行服务器端文件打开(不使用 RPC)的方法是理想的。 如果未来需求发生变化,我们可以相应地进行调整。
为什么 LOCALIO 仅支持 UNIX 身份验证 (AUTH_UNIX)?
强大的身份验证通常与连接本身相关联。 它的工作原理是建立一个由服务器缓存的上下文,该上下文充当发现授权令牌的密钥,然后可以将该令牌传递到 rpc.mountd 以完成身份验证过程。 另一方面,对于 AUTH_UNIX,在线路上传递的凭据直接用作 rpc.mountd 的上调中的密钥。 这简化了身份验证过程,因此使 AUTH_UNIX 更容易支持。
转换 RPC 用户 ID 的导出选项如何表现 LOCALIO 操作(例如 root_squash,all_squash)?
转换用户 ID 的导出选项由 nfsd_setuser() 管理,nfsd_setuser() 由 nfsd_setuser_and_check_port() 调用,nfsd_setuser_and_check_port() 又由 __fh_verify() 调用。 因此,它们对于 LOCALIO 的处理方式与对非 LOCALIO 的处理方式完全相同。
鉴于 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 进行编码。 使用固定大小的不透明编码和解码 XDR 方法,而不是效率较低的可变大小的方法。
NFS_LOCALIO_PROGRAM 的 RPC 程序号为 400122(由 IANA 分配,请参见 https://www.iana.org/assignments/rpc-program-numbers/):Linux 内核组织 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 通用和客户端/服务器握手¶
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 传输到 nn->nfsd_serv clients_list(从 nfs_common 的 uuids_list)。 请参见:fs/nfs/localio.c:nfs_local_probe()
nfs_common 的 nfs_uuids 列表是启用 LOCALIO 的基础,因此它具有指向 nfsd 内存的成员,供客户端直接使用(例如,“net”是服务器的网络命名空间,通过它,客户端可以访问 nn->nfsd_serv 并具有适当的 rcu 读取访问权限)。 正是这种客户端和服务器同步使得对象的高级用法和生命周期能够从主机内核的 nfsd 跨越到连接到在同一本地主机上运行的 nfs 客户端的每个容器 knfsd 实例。
NFS 客户端和服务器互锁¶
LOCALIO 提供 nfs_uuid_t 对象和相关接口,以允许正确的网络命名空间 (net-ns) 和 NFSD 对象引用计数
我们不想在客户端的每个 NFSD 的 net-ns 上保留长期计数的引用,因为这会阻止服务器容器完全关闭。
因此,我们根本不采用引用,而是依靠每个 CPU 对服务器的引用(在下面详述)足以保持 net-ns 处于活动状态。 这涉及允许 NFSD 的 net-ns 退出代码迭代所有活动客户端并清除其 ->net 指针(需要这些指针来查找 nfsd_serv 的每个 CPU 引用计数)。
详细信息
将 nfs_uuid_t 嵌入 nfs_client 中。 nfs_uuid_t 提供了一个 list_head,可以用于查找客户端。 它确实将 16 字节的 uuid_t 添加到了 nfs_client 中,因此它比需要的更大(因为 uuid_t 仅在初始 NFS 客户端和服务器 LOCALIO 握手期间使用,以确定它们是否彼此本地)。 如果这确实是一个问题,我们可以找到解决方案。
当 nfs 服务器确认 uuid_t 是本地的时,它会将 nfs_uuid_t 移动到 NFSD 的 nfsd_net 中的每个 net-ns 列表上。
当每个服务器的 net-ns 关闭时 - 在“pre_exit”处理程序中,所有这些 nfs_uuid_t 的 ->net 都会被清除。 在 pre_exit() 处理程序和 exit() 处理程序之间有一个 rcu_synchronize() 调用,因此任何将 nfs_uuid_t ->net 视为非 NULL 的调用者都可以安全地管理 nfsd_serv 的每个 CPU 引用计数。
客户端的 nfs_uuid_t 会传递给 nfsd_open_local_fh(),以便它可以在私有
rcu_read_lock()
部分中安全地取消引用 ->net,从而允许安全访问相关的 nfsd_net 和 nfsd_serv。
因此,LOCALIO需要引入和使用 NFSD 的 percpu_ref 来互锁 nfsd_destroy_serv() 和 nfsd_open_local_fh(),以确保每个 nn->nfsd_serv 在被 nfsd_open_local_fh() 使用时不会被销毁,这需要更详细的解释。
nfsd_open_local_fh() 在打开其 nfsd_file 句柄之前使用 nfsd_serv_try_get(),然后调用者(NFS 客户端)必须在使用完 IO 后使用 nfs_file_put_local() 释放 nfsd_file 和关联的 nn->nfsd_serv 的引用。
这种互锁机制的运作很大程度上依赖于 nfsd_open_local_fh() 能够安全地处理以下可能性:NFSD 的 net-ns(以及相关的 nfsd_net)可能已被 nfsd_destroy_serv() 通过 nfsd_shutdown_net() 销毁——这只有在上面详细介绍的 nfs_uuid_t -> net 指针管理的情况下才有可能发生。
总而言之,这种精巧的 NFS 客户端和服务器互锁机制已验证可以修复一个容易触发的崩溃,该崩溃会在容器中运行的 NFSD 实例与本地 IO 客户端挂载时关闭时发生。在重新启动容器和相关的 NFSD 后,由于 LOCALIO 客户端尝试在没有正确引用 nn->nfsd_serv 的情况下使用 nn->nfsd_serv 调用 nfsd_open_local_fh(),导致出现 NULL 指针解引用,客户端将继续崩溃。
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(),并仔细访问相关的 nfsd 网络命名空间和 nn->nfsd_serv,以 RCU 的形式。如果 nfsd_open_local_fh() 发现客户端不再看到有效的 nfsd 对象(无论是 struct net 还是 nn->nfsd_serv),它将返回 -ENXIO 给 nfs_local_open_fh(),并且客户端将尝试通过再次调用 nfs_local_probe() 来重新建立所需的 LOCALIO 资源。如果/当在 LOCALIO 客户端连接到它的情况下重新启动容器中运行的 nfsd 实例时,则需要此恢复。
一旦客户端有了打开的 nfsd_file 指针,它将直接向底层本地文件系统(通常由 nfs 服务器完成)发出读取、写入和提交操作。因此,对于这些操作,NFS 客户端正在向它与 NFS 服务器共享的底层本地文件系统发出 IO。请参阅:fs/nfs/localio.c:nfs_local_doio() 和 fs/nfs/localio.c:nfs_local_commit()。
安全性¶
仅当使用 UNIX 风格的身份验证(AUTH_UNIX,又名 AUTH_SYS)时才支持 Localio。
我们已采取措施确保使用相同的 NFS 安全机制(身份验证等),无论使用 LOCALIO 还是常规 NFS 访问。作为传统 NFS 客户端访问 NFS 服务器的一部分而建立的 auth_domain 也用于 LOCALIO。
相对于容器而言,LOCALIO 允许客户端访问服务器的网络命名空间。这是必要的,以允许客户端访问服务器的每个命名空间的 nfsd_net 结构。使用传统的 NFS,客户端可以获得相同级别的访问权限(尽管是以通过 SUNRPC 的 NFS 协议的形式)。没有其他命名空间(用户、挂载等)从服务器更改或故意扩展到客户端。
测试¶
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 用例造成任何回归。
Hammerspace 的所有各种健全性测试在启用 LOCALIO 的情况下都通过了(这包括许多 pNFS 和 flexfiles 测试)。