文章路径:
https://zhuanlan.zhihu.com/p/645489431
分布式系统利用卸载来减少 CPU 负载变得越来越流行。远程直接内存访问 (RDMA) 卸载尤其变得流行。然而,RDMA 仍然需要 CPU 干预来处理超出简单远程内存访问范围的复杂卸载。因此,卸载潜力是有限的,基于 RDMA 的系统通常必须解决这些限制。 我们提出了 RedN,这是一种原则性的、实用的方法,可以实现复杂的 RDMA 卸载,无需任何硬件修改。使用自修改 RDMA 链,我们将现有的 RDMA 动词接口提升为图灵完备的编程抽象集。我们探索使用商用 RDMA NIC 在卸载复杂性和性能方面的可能性。我们展示了如何将这些 RDMA 链集成到应用程序中,例如 Memcached 键值存储,从而使我们能够卸载复杂的任务,例如键查找。与使用单侧 RDMA 原语(例如 FaRM-KV)的最先进的 KV 设计以及传统的 RPC-over-RDMA 方法相比,RedN 可以将键值获取操作的延迟减少高达 2.6 倍。此外,与这些基准相比,RedN 提供性能隔离,并且在存在争用的情况下,可以将延迟减少高达 35 倍,同时为应用程序提供针对操作系统和进程崩溃的故障恢复能力。
随着服务器 CPU 周期成为越来越稀缺的资源,卸载越来越受欢迎 [23,28,30–32,36]。系统操作员希望保留 CPU 周期用于应用程序执行,同时可以卸载常见的、经常重复的操作。特别是 NIC 卸载的优点是它们驻留在网络数据路径中,并且 NIC 可以低延迟地对传输中的数据执行操作 [31]。
因此,远程直接内存访问(RDMA)[15]已经变得无处不在[20]。Mellanox ConnectX NIC [4] 率先推出了普遍存在的 RDMA 支持,并且 Intel 已将 RDMA 支持添加到其 800 系列以太网网络适配器 [7]。RDMA 专注于简单消息传递(通过 SEND/RECV 动词)和远程内存访问(通过 READ/WRITE 动词)的卸载 [15]。这两种原语都广泛用于网络应用程序中,并且它们的卸载非常有用。然而,RDMA 并不是为网络应用程序中常见的更复杂的卸载而设计的。例如,远程数据结构遍历和哈希表访问通常不被认为可以通过 RDMA 实现[39]。这导致许多基于 RDMA 的系统需要多次网络往返或重新引入服务器 CPU 的参与来执行此类请求 [18,22,26,27,35,37,41]。
为了支持复杂的卸载,网络社区开发了许多 SmartNIC 架构 [2,3,11,14,17]。SmartNIC 通过 CPU 或 FPGA 整合了更强大的计算功能。他们可以在 NIC 上执行任意程序,包括复杂的卸载。然而,这些智能网卡并不普遍,而且其较小的体积意味着较高的成本。在相同的链路速度下,SmartNIC 的成本比商用 RDMA NIC (RNIC) 高出 5.7 倍(§2.1)。由于其定制架构,它们也是系统运营商的管理负担,系统运营商必须支持 SmartNIC,而不是其他设备。
我们询问是否可以避免这种权衡,并尝试使用无处不在的 RNIC 来实现复杂的卸载。为此,我们必须解决许多挑战。首先,我们必须回答是否以及如何使用 RNIC 接口来实现复杂的卸载,该接口仅由简单的数据移动动词(READ、WRITE、SEND、RECV 等)组成,没有条件或循环。我们的解决方案必须是通用的,以便卸载开发人员可以使用它来构建可以执行广泛功能的复杂 RDMA 程序。其次,我们必须确保我们的解决方案高效,并且我们了解使用 RNIC 进行复杂卸载的性能和性能可变性属性。最后,我们必须回答复杂的 RNIC 卸载如何与现有应用程序集成。
在本文中,我们证明 RDMA 是图灵完备的,使得使用 RNIC 实现复杂的卸载成为可能。为此,我们通过自修改 RDMA 动词实现条件分支。巧妙地使用现有的比较和交换 (CAS) 动词使我们能够通过使用 CAS 操作数作为verbs编辑 RDMA 程序中的后续verbs来动态修改 RNIC 执行路径。就像在 CPU 上执行的自修改代码一样,自修改verbs需要仔细控制执行路径,以避免由于 RNIC verbs预取而导致的一致性问题。为此,我们依赖提供执行依赖性的 WAIT 和 ENABLE RDMA verbs [28,34]。WAIT 允许我们停止执行新verbs,直到过去的verbs完成,从而在 RDMA verbs之间提供严格的排序。通过控制verbs预取,ENABLE 可以强制由前面的verbs修改的verbs的一致性。ENABLE 还允许我们通过重新触发 RDMA 工作队列中较早的、已执行的verbs来创建循环,从而允许 NIC 无需 CPU 干预即可自主运行。
基于这些原语,我们提出了 RedN,这是一种实现复杂 RNIC 卸载的原则性实用方法。使用自修改 RDMA 程序,我们开发了许多构建块,将现有的 RDMA verbs接口提升为图灵完整的编程抽象集。使用这些抽象,我们探索仅使用商用 RNIC 在卸载复杂性和性能方面的可能性。我们展示了如何将根据 RedN 原理开发的复杂 RNIC 卸载集成到现有的网络应用程序中。RedN 为卸载开发人员提供了一种在商用 RNIC 上实现复杂 NIC 卸载的实用方法,而无需承担获取和维护 SmartNIC 的负担。
我们做出以下贡献:
RDMA 是为高性能计算 (HPC) 集群而设计的,但它已经脱离了这个领域 [20]。由于网络带宽的增长,CPU 性能的增长停滞,使得 CPU 周期成为越来越稀缺的资源,最好保留用于运行应用程序代码,因此它变得越来越流行。随着 RNIC 现在被视为商品,探索其硬件可以产生效益的用例是机会主义的。然而,这些努力受到 RDMA API 的限制,它限制了许多复杂卸载的表达。因此,网络社区使用 FPGA 和 CPU 构建了 SmartNIC,以研究新的复杂卸载。
为了实现复杂的网络卸载,人们开发了 SmartNIC [1,2,10,11]。SmartNIC 包括专用计算单元或 FPGA、内存以及多个专用加速器,例如加密引擎。例如,Mellanox BlueField [11] 具有 8 个 ARMv8 内核、16GB 内存和 2 个 25GbE 端口。这些 SmartNIC 能够运行成熟的操作系统,而且还附带轻量级运行时系统,可以提供对 NIC IO 引擎的内核绕过访问。
SmartNIC 卸载的相关工作。SmartNIC 已用于减轻服务器 CPU 的复杂任务。例如,StRoM [39] 使用 FPGA NIC 来实现 RDMA verbs,并创建执行各种功能(例如遍历链表)的通用内核(或构建块)。KV-Direct [30] 使用 FPGA NIC 来加速键值访问。iPipe [31] 和 Floem [36] 是编程框架,可简化主要基于 CPU 的 SmartNIC 的复杂卸载开发。E3 [32] 透明地将微服务卸载到 SmartNIC。
SmartNIC 的成本。虽然 SmartNIC 提供了复杂卸载的功能,但它们是有代价的。例如,售价 2,340 美元的双端口 25GbE BlueField SmartNIC 的成本比售价 410 美元的同速 ConnectX-5 RNIC 贵 5.7 倍(参见[13])。另一个成本是 SmartNIC 所需的额外管理。SmartNIC 是一种特殊的复杂设备,系统管理员需要了解和维护。SmartNIC 操作系统和运行时可能会崩溃、存在安全缺陷,并且需要保持最新的供应商补丁。这对运营商来说是额外的维护负担,而 RNIC 不会产生这种负担。
每一代 RDMA NIC (RNIC) 的处理能力都增加了一倍。这使得 RNIC 能够应对更高的数据包速率和更复杂的硬编码卸载(例如,缩减操作、加密、纠删码)。
我们使用 Mellanox ib_write_bw 基准测试来测量几代 Mellanox ConnectX NIC 的动词处理带宽。该基准测试执行 64B RDMA 写入,因此,它不会因 RDMA 写入大小较小而受到网络带宽限制。我们发现,verbs处理带宽随着每一代的增加而增加一倍,如表 1 所示。这主要是由于每一代的处理单元 (PU) 都增加了一倍。1 因此,ConnectX-6 NIC 使用单个 NIC 端口每秒可以执行多达 1.1 亿个 RDMA verbs。硬件性能的提高进一步激发了利用这些设备的计算能力的需求。
RDMA 卸载的相关工作。RDMA 已在许多不同的环境中使用,包括加速键值存储和文件系统 [19,22,26,35,44],共识 [18,27,37,41],分布式锁定 [45],甚至微妙的用例,例如基于分布式树的索引结构中的高效访问 [46]。这些系统在 RDMA 作为数据移动卸载(通过远程内存访问和消息传递)的预期用途范围内运行。当需要复杂的功能时,这些系统涉及多个 RDMA 往返和/或依赖主机 CPU 来执行复杂的操作。
在存储环境中,Hyperloop [28] 证明了推动 RNIC 卸载功能是可能的。超级循环结合 RDMA 动词来实现复杂的存储操作,例如链复制,而无需 CPU 参与。但是,它不提供用于卸载任意处理的蓝图,并且无法卸载使用任何类型的条件逻辑的功能(例如,遍历远程数据结构)。此外,Hyperloop 协议可能与下一代 RNIC 不兼容,因为其实现依赖于更改工作请求所有权——ConnectX-4 和更新的卡已弃用该功能。
与之前的工作不同,我们的目标是通过使用现有 RDMA verbs的新颖组合(第 3 节),释放 RNIC 的通用处理能力,并为复杂卸载提供前所未有的可编程性。
为了实现上述目标,我们开发了一个能够实现复杂卸载的框架,称为 RedN。RedN 的关键思想是结合广泛可用的 RNIC 功能来实现自修改 RDMA 程序。这些程序(RDMA 操作链)能够执行带有条件和循环的动态控制流。图 1 说明了 RedN 的用法。设置阶段涉及(1)准备/编译服务所需的RDMA代码以及(2)将RDMA WR的输出链发布到RNIC。然后,客户端可以通过调用触发器 (3) 来使用卸载,该触发器使服务器的 RNIC (4) 执行发布的 RDMA 程序,该程序在完成后向客户端返回响应 (5)。
为了进一步理解这个提议的框架,我们首先研究 RNIC 提供的执行模型,以及它们为 RDMA verbs提供的排序保证。然后,我们研究传统 RDMA verbs的表达能力,并探索与 CPU 指令集的相似之处。我们利用这些见解来描述使用传统 RDMA verbs表达复杂逻辑的策略,而不需要任何硬件修改。
RDMA 接口指定了许多数据移动verbs(READ、WRITE、SEND、RECV 等),这些verbs由卸载开发人员作为工作请求 (WR) 发布到主机内存中的工作队列 (WQ) 中。一旦卸载开发人员触发门铃(RNIC 内存中的一个特殊寄存器,通知 RNIC WQ 已更新并且应该执行),RNIC 就会开始执行 WQ 中的一系列 WR。工作请求订购。RDMA WR 的排序规则区分写入 WR 和返回值的非写入 WR。在每一类操作中,RDMA 保证单个 WQ 内 WR 的按顺序执行。特别是,写入 WR(即,SEND、WRITE、WRITEIMM)相对于彼此完全排序,但写入可以在先前的非写入 WR 之前重新排序。
我们将默认的 RDMA 排序模式称为工作队列(WQ)排序。复杂的卸载逻辑通常需要更强的排序约束,我们在两个 RDMA verbs的帮助下构建它。图 2 显示了我们引入的两种更严格的排序模式以及如何实现它们。 WAIT verbs停止 WR 执行,直到另一个 WQ 或同一 WQ 中的前一个 WR 完成指定的 WR。我们称之为完成排序(图 2a)。它实现了 WR 沿执行链的全排序(可能涉及多个 WQ)。它可用于强制数据一致性,类似于 CPU 指令集中的数据内存屏障 — 在执行对数据操作的 WR 之前等待数据可用。此外,WAIT 允许开发人员将 RDMA verbs链预先发布到 RNIC,而无需立即执行它们。
在所有上述排序模式中,RNIC 可以自由地将 WQ 内的 WR 预取到其高速缓存中。因此,执行结果反映了获取它们时的 WR,这可能与驻留在主机内存中的版本不一致,以防这些版本后来被修改。为了避免此问题,RNIC 允许将 WQ 置于托管模式,在该模式下禁用 WR 预取。然后使用 ENABLE verbs显式启动 WR 的预取。这允许在 WQ 内修改现有的 WR,只要这是在发布的 ENABLE 完成之前完成的,类似于指令屏障。通过按顺序使用 WAIT 和 ENABLE,我们实现了完整的(数据和指令)屏障。我们称之为门铃订购(图 2b)。Doorbell ordering 允许开发人员就地修改 WR 链。特别是,它允许依赖于数据的、自我修改的 WR。
因此,我们已经证明我们可以通过特殊verbs来控制 WR 获取和执行,我们将在下一节中利用这些verbs来开发成熟的 RDMA 程序。这些动词在商品 RNIC 中广泛使用(例如,Mellanox 将其称为跨渠道通信 [34])。
虽然 RDMA WR 的静态序列已经是一个基本的 RDMA 程序,但复杂的卸载需要数据相关的执行,其中卸载的逻辑依赖于输入参数。为了实现数据相关的执行,我们构建了自修改 RDMA 代码。
自修改 RDMA 代码。门铃排序支持受限形式的自修改代码,能够进行数据相关的执行。为了说明这个概念,我们使用服务器主机的示例,该服务器主机将 RPC 处理程序卸载到其 RNIC,如图 3 所示。RPC 响应取决于客户端设置的参数,因此 RDMA 卸载是数据相关的。服务器发布由一组跨越两个 WQ 的 WR 组成的 RDMA 程序。客户端通过发出 SEND 操作来调用卸载。在 RNIC,SEND 触发发布的 RECV 操作。请注意 RECV 指定了 SEND 数据的放置位置。我们配置RECV将接收到的数据注入到WQ2中发布的WR链中以修改其属性。我们通过利用门铃排序来实现这一点,以确保发布的 WR 不会被 RNIC 预取,并且可以被之前的 WR 更改。
这是自修改代码的一个实例。因此,客户端可以将参数传递给卸载的 RPC 处理程序,RNIC 将相应地动态更改执行的代码。然而,这本身并不足以提供图灵完整的卸载框架.
RDMA 的图灵完备性。图灵完备性意味着数据操作规则系统(例如 RDMA)在计算上是通用的。为了使 RDMA 是图灵完备的,我们需要满足两个要求 [25]:
T1:能够读/写任意数量的内存
T2:条件分支(例如 if/else 语句)
使用常规 RDMA verbs可以满足 T1 的有限内存量,而 T2 尚未通过 RDMA NIC 进行演示。然而,为了真正能够访问任意数量的内存,我们需要一种实现循环的方法。循环开辟了一系列复杂的用例,并减少了程序员在卸载时必须考虑的约束数量。为了强调它们的重要性,我们将它们添加为第三个要求,这是满足第一个要求所必需的:
T3:重复执行代码(循环)的能力。
在接下来的小节中,我们将展示如何使用动态执行来满足所有上述要求。附录 A 给出了图灵完备性的证明草图。
条件执行——根据运行时条件选择要执行的计算——通常使用条件分支来实现,这在 RDMA 中不容易实现。为此,我们引入了一种使用自修饰 CAS 动词的新颖方法。主要见解是该verbs可用于检查条件(即 x 和 y 相等),然后执行交换以修改 WR 的属性。我们在图 4 中描述了这是如何完成的。我们插入一个 CAS,将 R2 的操作码属性(最初为 NOOP)地址处的 64 位值与其旧参数(最初也是 NOOP)进行比较。然后我们将 R2 的 id 字段设置为 x。该字段可以自由操作,无需更改 WR 的行为,从而允许我们使用它来存储 x。操作数y存储在R1旧字段的相应位置。这意味着如果 x 和 y 相等,CAS 操作将成功,并且 R1 的新字段(我们设置为 WRITE)中的值将替换 R2 的操作码。因此,在 x = y 的情况下,R2 将从 NOOP 变为 WRITE 操作。该WRITE设置为将返回操作(R3)的数据值修改为1。如果x和y不相等,则返回默认值0。
现在我们已经确定了这种技术对于基本条件的实用性,接下来我们将研究如何用于支持循环结构。
为了有效地支持循环构造,我们需要(1)条件分支来测试循环条件并在必要时中断,以及(2)WR重新执行以重复循环体。我们将在下文中逐一展开。
考虑图 5 中的 while 循环示例。此卸载在数组 A 中搜索 x 并发送相应的索引。该循环是静态的,因为 A 的大小是有限的(在本例中,大小 =2),这是先验已知的。为了简化表示,请考虑 A[i] = i, ∀i 的情况。如果没有这种简化,该示例将包含一个额外的 WRITE 来获取 A[i] 处的值。
循环体使用 CAS verbs来实现 if 条件(第 3 行),后跟一个 ADD 动词来递增 i(第 6 行)。鉴于循环大小是先验已知的(大小 = 2),RedN 可以提前展开 while 循环并发布所有迭代的 WR。因此,无需检查第 2 行的条件。对于每次迭代,如果 CAS 成功,WQ1 中的 NOOP verbs将更改为 WRITE,这会将响应发送回客户端。然而,很明显,无论比较结果如何,所有后续迭代都将被执行。这是低效的,因为如果发送(第 4 行)发生在循环完成之前,NIC 将浪费地执行大量 WR。对于较大的循环尺寸或者迭代次数事先未知的情况,这是不切实际的。
无界循环和终止。图 6 修改了前面的示例,使其循环无界。为了提高效率,我们添加了一个break,如果找到元素就退出循环。break的作用是防止执行额外的迭代。我们使用额外的 NOOP,其格式一旦被 CAS 操作转换为 WRITE,就会阻止循环中后续迭代的执行。这是通过修改循环中的最后一个 WR 使其不会触发完成事件来完成的。因此,循环中等待此类事件(通过完成排序)的下一次迭代将不会被执行。此外,WRITE还会修改WR的操作码,用于将响应从NOOP发送回WRITE。
因此,break 允许高效且无限制的循环执行。然而,CPU 仍然需要在所有 WR 执行完毕后发布 WR 以继续循环。这会消耗 CPU 周期,如果 CPU 无法跟上 WR 执行的速度,甚至会增加延迟。
通过 WQ 回收实现无限循环。为了允许 NIC 在无需 CPU 干预的情况下回收 WR,我们使用了一种称为 WQ 回收的新技术。RNIC 迭代作为循环缓冲区的 WQ,并执行其中的 WR。根据设计,每个 WR 只执行一次。然而,WR 不能被重用并没有根本原因,因为 RNIC 实际上并没有从 WQ 中删除它们。为了启用 WR 链的回收,我们在 WQ 的尾部插入一个 WAIT 和 ENABLE 序列。这指示 RNIC 环绕尾部并根据需要多次重新执行 WR 链。
需要注意的是,WQ回收并不是万能的。为了允许 WQ 的尾部环绕,循环中所有发布的 WAIT 和 ENABLE WR 都需要更新其 wqe_count 属性。该属性用于确定这些排序verbs影响的 WR 的索引。在 ConnectX NIC 中,这些索引由 RNIC 内部维护,并且它们的值单调递增(而不是在 WQ 环绕后重置)。因此,wqe_count 值需要递增才能匹配。这会产生开销(如表 2 所示),并且需要与其他verbs结合进行额外的 ADD 操作。因此,循环展开(每次迭代均由 CPU 手动发布)总体上对 RNIC 的负担较小。然而,WQ 回收避免了 CPU 干预,即使在主机软件故障的情况下也允许卸载保持可用(正如我们将在后面的第 5.6 节中看到的)。
通过条件分支,我们可以动态改变 RNIC 上任何函数的控制流。循环允许我们遍历任意数据结构。我们共同将 RNIC 转变为通用处理单元。在本节中,我们从开销、安全性、可编程性和表达性的角度讨论可用性方面。
构建blocks。我们将条件分支和循环所需的 RDMA 链抽象并参数化为 if 和 while 结构。我们构造的 RDMA WR 链的开销如表 2 所示。我们可以看到每个所需的最小操作数的细分。不等式谓词(例如 < 或 >)也可以通过将等式检查与 MAX 或 MIN 相结合来支持,如后面的表 3 所示。但是,它们的可用性是特定于供应商的,目前仅受 ConnectX NIC 支持。
操作数限制。RedN 的限制基于 CAS verbs支持的大小,即 64 位。操作数以 48 位值的形式提供,在其 id 和其他相邻字段中进行编码(也可以自由修改而不影响执行)。其余位用于根据比较结果修改 WR 的操作码。我们注意到,我们公布的限制仅表示我们为构造分配的操作数量是可能的。例如,尽管我们的构造有 48 位操作数限制,但我们可以将多个 CAS 操作链接在一起来处理更大操作数的不同段(我们不依赖 CAS 的原子性属性)。因此,没有根本性的限制,只有性能损失。
卸载设置。要卸载 RDMA 程序,客户端首先创建到目标服务器的 RDMA 连接并发送 RPC 以启动卸载。我们设想服务器已经有卸载代码;然而,部署卸载的其他方式也是可能的。收到连接请求后,服务器会创建一个或多个托管本地 WQ 来发布卸载的代码。接下来,它注册用于 RDMA 访问的两种主要类型的内存区域:(a) 代码区域和 (b) 数据区域。代码区域是在服务器上创建的一组远程 RDMA WQ,它们对于每个客户端来说都是唯一的,并且需要通过 RDMA 进行访问以允许自修改代码。代码区域在注册时(在连接时)受到内存密钥(RDMA 访问所需的特殊令牌)的保护,禁止未经授权的访问。数据区域保存卸载使用的任何数据元素(例如哈希表)。数据区域可以是共享的或私有的,具体取决于用例。安全。RedN 无法解决现有 RDMA 或 Infiniband 实施中的安全挑战 [40]。然而,RedN 可以帮助 RDMA 系统变得更加安全。对于此类系统,经常使用单侧 RDMA 操作(例如,RDMA READ 和 WRITE)[22,28,33,35,42,43],因为它们避免了响应方的 CPU 开销。然而,这样做需要客户端具有直接读和/或写内存访问权限。如果客户端存在错误和/或恶意,这可能会损害安全性。举个例子,FaRM 允许客户端直接将消息写入共享 RPC 缓冲区。这要求客户端行为正确,否则它们可能会覆盖或修改其他客户端的 RPC。RedN 允许应用程序使用双边 RDMA 操作(例如,SEND 和 RECV),这些操作不需要直接内存访问,同时仍然完全绕过服务器 CPU。正如我们在第 5 节中的用例中所演示的,SEND 操作可用于触发卸载程序,而无需任何 CPU 参与。
隔离。鉴于 RedN 实现了动态循环,客户端可以滥用此类结构来消耗超出其公平份额的资源。幸运的是,流行的 RNIC(例如 ConnectX)提供了 WQ 速率限制器 [6] 以实现性能隔离。因此,即使客户端触发非终止卸载代码,他们仍然必须遵守指定的速率。此外,服务器可以将卸载的代码配置为可通过执行 WR 后自动创建的完成事件进行审核。可以监视这些事件,并且服务器可以终止与运行行为不当代码的客户端的连接。
并行性。与 CPU 指令相比,RDMA WR 获取和执行延迟的成本更高,因为 WR 是通过 PCIe 获取/执行的(微秒与纳秒)。因此,为了隐藏 WR 延迟,并行化逻辑上不相关的操作非常重要。与 CPU 中的执行线程一样,每个 WQ 都分配有一个 RNIC PU,以确保按顺序执行,而无需 PU 间同步。因此,我们仔细调整卸载的代码,以允许不相关的动词在独立队列上执行,以便能够尽可能并行执行。并行性的好处在第 5.2 节中进行了评估。
我们的卸载框架是用 C 语言实现的,有约 2,300 行代码,其中包括我们的用例 (约 1400) 和 RDMA verbs (libibverbs) API 的便捷包装器 (约 900)。 我们的方法不需要修改任何 RDMA 库或驱动程序。RedN 使用 Mellanox 的 ConnectX 驱动程序 (libmlx5) 提供的低级函数来公开内存中的 WQ 缓冲区并将其注册到 RNIC,从而允许通过 RDMA verbs操作 WR。我们配置 ConnectX-5 固件以允许自由操作 WR id 字段,这是条件操作和 WR 回收所必需的。这是通过修改 NIC 上的特定配置寄存器来完成的[12]。 RedN 与任何支持 WAIT 和 ENABLE 的 ConnectX NIC(例如 ConnectX-3 及更高版本)兼容。
我们首先描述底层 RNIC 性能(第 5.1 节),以了解它如何影响我们实现的编程结构。然后,在针对最先进的 RNIC 和 SmartNIC 卸载的评估中,我们表明 RedN:
1. 与普通 RDMA 卸载相比,加速远程数据结构遍历,例如哈希表(第 5.2 节)和链表(第 5.3 节); 2. 为 Memcached 键值存储加速(第 5.4 节)并提供性能隔离(第 5.5 节); 3. 提高应用程序的可用性(§5.6)——允许它们在操作系统和进程崩溃的情况下运行; 4. 公开足够通用的编程结构,以支持各种用例(§5.2–§5.6);
试验台。我们的实验测试台由 3 台双路 Haswell 服务器组成,运行频率为 3.2 GHz,共有 16 个内核、128 GB DRAM 和 100 Gbps 双端口 Mellanox ConnectX-5 Infiniband RNIC。所有节点都运行带有 Linux 内核版本 4.15 的 Ubuntu 18.04,并通过背对背 Infiniband 链路进行连接。
网卡设置。对于我们所有的实验,我们使用可靠连接(RC)RDMA 传输,它支持我们使用的 RDMA 同步功能。所有执行门铃命令的 WQ 均使用特殊的“托管”标志进行初始化,以禁止驱动程序在 WR 发布后发出门铃。WQ 大小设置为与卸载程序的大小相匹配。
我们运行微基准测试来分解 RNIC verbs执行延迟,了解不同排序模式的开销,并确定不同 RDMA 动词和我们的构造的处理带宽。
5.1.1 RDMA 延迟
我们通过测量执行 100K 次后的平均延迟来分解配置为执行 64B IO 的 RDMA verbs的性能。除非另有说明,所有动词均远程执行。如图 7 所示,WRITE 的延迟为 1.6 μs。它使用发布的 PCIe 事务,这是单向的。相比之下,非发布verbs(例如 READ)或原子(例如获取和添加(ADD)和比较和交换(CAS))需要等待 PCIe 完成并需要 ∼1.8 μs.2 总体而言,verbs之间的执行时间差异很小,即使对于执行逻辑和算术计算(例如 MAX)的更高级、特定于供应商的 Calc verbs也是如此。
为了分解 RDMA verbs执行的不同延迟组件,我们首先估计发出门铃和将 WR 复制到 RNIC 的延迟。这可以通过测量 NOOP WR 的执行时间来完成。一旦 WR 在 RNIC 的缓存中可用,就可以从其他 WR 的延迟中减去该时间,以估计其执行时间。我们还通过执行远程和本地环回 NOOP WR(如右侧所示)并测量差异(对于背靠背连接的节点而言大约为 0.25 μs)来量化网络成本。总体而言,这些结果显示verbs执行延迟较低,证明在其之上构建更复杂的函数是合理的。接下来我们衡量卸载排序的影响。
5.1.2 订购管理花销 我们展示了使用不同排序模式执行 RDMA verbs链的延迟。链中所有发布的 WR 都是 NOOP,以简化隔离排序对性能的影响。我们首先测量执行发布到同一队列但没有任何约束(WQ 顺序)的verbs链的延迟,并将其与我们在图 2 中引入的排序模式(完成顺序和门铃顺序)进行比较。WQ order 仅要求按顺序更新内存,这可以提高并发性。不修改同一内存地址的操作可以同时执行,并且 RNIC 可以使用单个 DMA3 自由地预取多个 WR。我们可以在图 8 中看到,单个 NOOP 的延迟为 1.21 μs,添加后续verbs的开销约为每个动词 0.17 μs。第一个动词较慢,因为它需要初始门铃来告诉 NIC 有未完成的工作。对于完成排序,可能会出现较低的并发性,因为 WR 等待其前任的完成,并且每个额外 WR 的开销略有增加至 0.19 μs。对于门铃命令,不可能隐藏延迟,因为 NIC 必须从内存中逐一获取 WR,这会导致每个额外 WR 的开销为 0.54 μs。这些结果表明,门铃订购应谨慎使用,因为与更宽松的订购模式相比,每次使用门铃订购时都会增加超过 0.5 μs 的延迟。
5.1.3 RDMA verbs吞吐量
我们在表 3 中显示了单个 ConnectX-5 端口的常见 RDMA verbs的吞吐量。ConnectX 卡按端口分配计算资源。对于 ConnectX-5,每个端口有 8 个 PU。由于跨 PCIe 的内存同步,原子verbs(例如 CAS)提供的吞吐量相对有限(比常规动词低 8 倍)。
此外,我们还测量了 RedN 的 if 和 while 结构的性能。使用 48 位操作数,ConnectX-5 NIC 每秒可以执行 700K if 构造。这是因为 CAS 需要确保 CAS 和它修改的后续 WR 之间的门铃排序。这会导致吞吐量受到 NIC 处理限制的限制。展开的 while 循环每次迭代需要与 if 语句相同数量的verbs,并且它们的吞吐量是相同的。由于每次迭代必须执行更多的 WR,因此具有 WQ 回收的循环会降低性能。
在评估了 RedN 排序模式和构造的开销之后,我们接下来研究 RedN 在卸载对流行数据结构的远程访问方面的性能。我们首先研究哈希表,因为它们在键值存储中广泛用于索引存储对象。要执行简单的获取操作,客户端首先必须在哈希表中查找所需的键值条目。该条目可以直接内联值或指向其内存地址的指针。然后获取该值并将其返回给客户端。Hopscotch散列是一种流行的散列方案,它通过对每个条目使用 H 散列并将它们存储在 H 个桶中的 1 个中来解决冲突。每个桶都有一个可以概率地保存给定密钥的邻域。在找到匹配的键值条目之前,查找可能需要搜索多个存储桶。为了支持动态值大小,我们假设该值没有内联在存储桶中,而是通过指针引用。
对于使用 RDMA 构建的分布式键值存储,获取操作通常通过以下两种方式之一实现:
单边方法首先使用单边 RDMA READ 操作检索键的位置,然后发出第二次 READ 来获取值。这些方法通常至少需要两次网络往返。这会大大增加延迟,但不需要服务器 CPU 的参与。许多系统利用这种方法来实现查找,包括 FaRM [22] 和 Pilaf [35]。 双边方法要求客户端使用 RDMA SEND 或 WRITE 发送请求。服务器拦截请求,找到值,然后使用上述动词之一返回它。这种广泛使用的 [19, 26] 方法遵循传统的 RPC 实现,并且避免了多次往返的需要。然而,这是以服务器 CPU 周期为代价的。
5.2.1 RedN 的方法
为了卸载键值获取操作,我们利用第 3.3 节和第 3.4 节中介绍的卸载方案。
图 9 描述了单哈希查找所涉及的 RDMA 操作。为了获取与键对应的值,客户端首先计算其键的哈希值。对于这个用例,我们将哈希数设置为两个,这在实践中很常见[24]。然后,客户端使用键 x 的值和第一个存储桶 H1(x) 的地址执行 SEND,然后通过服务器上发布的 RECV WR 捕获这些值。RECV WR(R1)将x插入CAS WR(R3)的旧字段中,并将桶地址H1(x)插入READ WR(R2)中。READ WR 检索桶并将响应 WR (R4) 的源地址 (src) 设置为值 (ptr) 的地址。它还将存储桶的密钥插入到 id 字段中,为条件检查做好准备。最后,CAS(R3)检查设置为键 x 的预期值 old 是否与(R4)中设置为存储桶键的 id 字段匹配。如果相等,(R4) 的操作码从 NOOP 更改为 WRITE,然后返回存储桶中的值。鉴于每个密钥可能存储在多个存储桶中(我们的设置中有两个),这些查找可以顺序或并行执行,具体取决于卸载配置。
5.2.2 结果
我们针对键值获取操作的单向和双向实现来评估我们的方法。我们使用 FaRM 的方法 [22] 来执行单向查找。FaRM 使用Hopscotch 散列法来定位密钥,使用大约两次 RDMA 读取——一次用于获取保存键值对的邻域中的存储桶,另一个用于读取实际值。邻域大小默认设置为 6,这意味着 RDMA 元数据操作的开销为 6 倍。对于双向查找,我们到主机的 RPC 涉及客户端发起的 RDMA SEND 以传输获取请求,以及服务器发起的 RDMA WRITE 以在执行查找后返回值。 潜在因素。图 10 显示了 RedN 的 KV get 操作与一侧和两侧基线的延迟比较。我们评估了双面的两种不同变体。基于事件的方法会阻止完成事件,以避免浪费 CPU 周期,而基于轮询的方法则专用一个 CPU 核心来轮询完成队列。我们使用 48 位密钥并改变值大小。值的大小在 x 轴上给出。在这种情况下,我们假设没有哈希冲突,并且所有键都在第一个存储桶中找到。RedN 能够超越所有基线——在 16.22 μs 内获取 64 KB 键值对,这在单次网络往返 READ 的 5% 之内(理想)。RedN 能够提供接近理想的性能,因为它绕过服务器的 CPU 并在单个网络 RTT 中获取值。与 RedN 相比,单边操作会导致高达 2 倍的延迟,因为它们需要两个 RTT 来获取值。两侧实施不会产生任何额外的 RTT;但是,它们需要服务器 CPU 干预。基于轮询的变体会消耗整个 CPU 核心,但提供有竞争力的延迟。基于事件的方法会阻止完成事件,以避免浪费 CPU 周期并因此导致更高的延迟。RedN 的性能分别比基于轮询和基于事件的方法高出 2 倍和 3.8 倍。鉴于基于事件的方法的延迟要高得多,在本次评估的其余部分中,我们将仅关注基于轮询的方法,并在下文中将它们简单地称为双边方法。
图 11 显示了存在哈希冲突时的延迟。在这种情况下,我们假设最坏的情况,即键值对总是在第二个桶中找到。在这种情况下,我们引入了 RedN 的两种卸载变体 — RedN-Seq 和 RedN-Parallel。前者在单个 WQ 内顺序执行存储桶查找。后者通过在两个不同的 WQ 上执行查找来并行存储桶查找,以允许在不同的 NIC PU 上执行。我们可以看到,RedN-Parallel 与没有哈希冲突的查找(即图 10 中的 RedN)保持了相似的延迟,因为存储桶查找几乎完全并行。值得注意的是,这种情况下的并行性不会导致不必要的数据移动,因为只有找到相应的键时才会返回该值。对于另一个存储桶,WRITE 操作(图 9 中的 R4)是 NOOP。另一方面,RedN-Seq 会产生至少 3 μs 的额外延迟,因为它需要逐个搜索存储桶。因此,只要有可能,没有依赖关系的操作就应该并行执行。权衡是必须为每个并行级别分配额外的 WQ。
吞吐量。我们在表 4 中描述了我们的吞吐量。在较低的 IO 下,由于使用门铃排序,RedN 受到 NIC 处理能力的瓶颈 — 在单个端口上达到 500K ops/s(双端口为 1M ops/s)。在 64 KB 时,RedN 达到单端口 IB 带宽限制(~ 92 Gbps)。双端口配置受到 ConnectX-5 的 16× PCIe 3.0 通道的限制。
SmartNIC比较。我们将哈希表获取的性能与 StRoM [39](一种基于 FPGA 的可编程 SmartNIC)进行比较。由于我们无法访问可编程 FPGA,因此我们从[39]中提取结果进行比较,并将其报告在表 5 中。RedN 使用与之前相同的实验设置。我们的哈希表配置在功能上与 StRoM 相同,并且我们的客户端和服务器节点也通过背靠背链接连接。我们可以看到 RedN 提供比 StRoM 更低的查找延迟。StRoM 使用 Xilinx Virtex 7 FPGA,运行频率为 156.25 MHz,并且至少需要两次 PCIe 往返来检索密钥和值。我们的评估表明,RedN 可以提供与更昂贵的 SmartNIC 一致的延迟。
接下来,我们探讨存储系统中也广泛使用的另一种数据结构。我们专注于存储键值对的链表,并评估使用卸载远程遍历它们的开销。与之前的用例类似,我们关注 FaRM 和 Pilaf [22, 35] 使用的片面方法。 链表处理可以分解为用于遍历链表的 while 循环和用于查找并返回键的 if 条件。我们在图 12 中描述了卸载的实现。客户端提供密钥 x 和列表 N0 中第一个节点的地址。然后执行读取操作(R2)以读取第一节点的内容并更新返回操作(R5)的值。我们还使用 WRITE 操作 (R3) 通过在其旧字段中插入键 x 来准备 CAS 操作 (R4)。作为优化,可以删除此 WRITE,而可以通过 RECV 操作直接插入 x。然而,这需要对每个要执行的 CAS 进行,因此,这种方法仅限于较小的列表大小,因为 RECV 只能执行 16 次分散。 对于此用例,我们引入了两种卸载变体。第一个,简称为 RedN,使用图 12 中的实现。第二个在 R4 和 R5 之间使用附加的 break 语句来退出循环,以避免执行任何附加操作。
5.3.1 结果
图 13 显示了在不同链表范围内针对 RedN 的单边和双边变体的延迟 - 其中范围表示密钥可以随机放置的最高列表元素。列表本身的大小设置为常数值 8。我们将链表设置为分别使用 48 位和 64 字节的键和值大小,并为每个系统执行 100k 列表遍历。为每个 RPC 随机选择请求的密钥。在标记为“RedN”的变体中,我们不使用中断并假设需要搜索列表的所有 8 个元素。在 8 之前,RedN 的所有列表范围都优于所有基线——提供高达 2 倍的改进。RedN (+break) 在每次迭代时执行一个break语句,由于检查break条件的额外开销,其性能比RedN差。然而,使用break语句可以提高卸载的整体效率,因为在找到密钥后不会执行不必要的迭代——在所有实验中平均使用30个WR。如果没有中断,即使在找到/返回键值对之后,RedN 也需要执行所有后续迭代,并且它使用了超过 65% 的 WR。因此,虽然 RedN 能够提供更好的延迟,但对于较长的列表,使用 break 语句更为明智。
根据我们早期卸载远程数据结构遍历的经验,我们着手了解:1)我们的上述技术在实际系统中的有效性如何,2)在此类设置中部署它会遇到哪些挑战。Memcached 是一种键值存储,通常用作大规模存储服务的缓存服务。我们使用采用布谷鸟散列法的 Memcached 版本 [24]。由于Memcached本身并不支持RDMA,因此我们对其进行了~700 LoC的修改以集成RDMA功能,允许RNIC注册哈希表和存储对象内存区域。我们还修改存储桶,以便值的地址以大端存储 - 以匹配 WR 属性使用的格式。然后,我们使用 RedN 卸载 Memcached 的获取请求,以允许 RNIC 直接为它们提供服务,而无需 CPU 参与。我们将结果与 Memcached 的各种配置进行比较。
为了对 Memcached 进行基准测试,我们使用 Memtier 基准测试,将其配置为使用 UDP(以减少基准的 TCP 开销),并使用不同的键值大小发出 100 万次获取操作。为了为双向方法创建有竞争力的基线,我们使用 Mellanox 的 VMA [9]——一种内核绕过用户空间 TCP/IP 堆栈,它通过拦截基于套接字的应用程序的套接字调用并使用内核绕过来发送/接收数据来提高基于套接字的应用程序的性能。我们将 VMA 配置为轮询模式以优化延迟。此外,我们还实现了一种片面的方法,类似于 5.2 节中介绍的方法。
图 14 显示了获取的延迟。正如我们所看到的,RedN 的哈希获取卸载速度比单侧快 1.7 倍,比两侧快 2.6 倍。尽管后者配置为轮询模式,但 VMA 会产生额外的开销,因为它依赖网络堆栈来处理数据包。此外,为了遵守套接字 API,VMA 必须从发送和接收缓冲区中 memcpy 数据,这进一步增加了延迟,这就是为什么它在较高值大小时性能相对较差的原因。
暴露 RNIC 潜在图灵能力的好处之一是加强应用程序之间的隔离。多租户和云设置中的 CPU 争用可能会导致任意上下文切换,进而导致平均延迟和尾部延迟增加。我们通过使用一个或多个编写器(客户端)将后台流量发送到 Memcached 来探索这样的场景。这些编写器在闭环中生成一组 RPC 以加载 Memcached 服务。同时,我们使用单个读取器客户端来生成获取操作。为了在最大程度地减少锁争用的同时对 CPU 资源施加压力,每个读取器/写入器都被分配了一组不同的 10K 密钥,它们用这些密钥来生成查询。客户端按顺序访问每组中的密钥。
我们可以在图 15 中看到,随着编写器数量的增加,两侧的平均延迟和第 99 个百分位延迟都急剧增加。对于 RedN,CPU 争用对 RNIC 的性能没有影响,平均百分位数和第 99 个百分位数都低于 7 μs。在 16 个写入器中,RedN 的第 99 个百分位数延迟比基线低 35 倍。 这表明 RNIC 卸载还可以具有其他有用的效果。服务提供商可能会选择卸载高优先级流量以获得更可预测的性能,或者将服务器资源分配给租户以减少争用。
我们现在考虑服务器故障以及 RNIC 对故障的影响。表 6 显示了服务器软件和硬件组件的故障率。与软件组件相比,NIC 发生故障的可能性要小得多 — NIC 年故障率 (AFR) 低一个数量级。更重要的是,NIC 与其主机部分解耦,并且在操作系统出现故障时仍然可以访问内存(或 NVM)。这意味着 RNIC 能够卸载关键系统功能,使服务器能够在操作系统出现故障(尽管处于降级状态)的情况下继续运行。为了对此进行测试,我们进行了故障转移实验,以探索 RedN 如何增强服务的故障恢复能力。
进程崩溃。我们研究如何允许 RNIC 在 Memcached 实例崩溃后继续提供 RPC 服务。我们发现这在实践中并不简单。RNIC 访问应用程序内存中的许多功能所需的资源(例如队列、门铃记录等)。如果托管这些资源的进程崩溃,操作系统将自动释放属于这些组件的内存,从而导致 RDMA 程序终止。为了解决这个问题,我们使用 [38] forks 创建一个空的 hull 父进程来托管 RDMA 资源,然后允许 Memcached 作为子进程运行。Linux 系统不会释放崩溃的子进程的资源,直到父进程也终止为止。因此,将 RDMA 资源绑定到空进程使我们能够在应用程序发生故障时继续运行。我们运行一个实验(如图 16 所示的时间线),将 get 查询发送到 Memcached 的单个实例,然后在运行期间简单地终止 Memcached。操作系统检测到应用程序终止并立即重新启动它。尽管如此,我们可以看到一个普通的 Memcached 实例至少需要 1 秒的时间来引导,并且需要额外 1.25 秒的时间来构建其元数据和哈希表。使用 RedN,不会遇到服务中断,并且可以继续发出查询而无需恢复时间。
操作系统故障。我们还使用 sysctl 以编程方式引发内核恐慌,冻结系统。这是比进程崩溃更简单的情况,因为我们不再需要担心操作系统释放 RDMA 资源。为简洁起见,我们没有显示这些结果,但我们通过实验验证了 RedN 卸载在操作系统崩溃的情况下继续运行。
客户端可扩展性。RedN要求服务器为每个客户端管理至少两个WQ,这并不高于其他RDMA系统。由于 RNIC 缓存有限,RedN 仍然会给数千个客户端带来可扩展性挑战。然而,Mellanox 的动态连接 (DC) 传输服务 [5] 允许回收未使用的连接,可以规避许多此类可扩展性限制。
基于套接字的应用程序的卸载。rsocket [16] 等协议可用于透明地将基于套接字的应用程序转换为使用 RDMA,使它们成为 RedN 的可能目标。尽管 rsocket 不支持流行的系统调用(例如 epoll),但已经提出了其他扩展[29],它们支持更全面的系统调用列表,并且被证明可以与 Memcached 和 Redis 等应用程序一起使用。 英特尔 RNIC。下一代英特尔 RNIC 预计将支持原子verbs,例如 CAS——RedN 使用它来实现条件。为了控制 NIC 何时可以获取 WR,Intel 在每个 WR 标头中使用了一个有效性位。该位可以通过 RDMA 操作动态修改以模拟 ENABLE。但是,WAIT 原语没有等效项,这意味着客户端无法触发预发布链。一种可能的解决方法是使用服务器上的另一个 PCIe 设备向 RNIC 发出门铃,从而允许触发 WR 链。我们将对此类技术的探索留作未来的工作。 对下一代 RNIC 的见解。我们在 RedN 方面的经验表明,将 WR 保留在服务器内存中(以允许其他 RDMA 动词修改它们)是一个关键瓶颈。如果通过 RDMA 直接访问 NIC 的缓存,则可以提前预取 WR,并可以避免关键路径上不必要的 PCIe 往返。我们希望未来的 RNIC 将支持此类功能。
我们证明,不管表面如何,商品 RDMA NIC 都是图灵完备的,并且能够执行复杂的卸载,而无需任何硬件修改。我们利用这一见解并探索这些卸载的可行性和性能。我们发现,使用商品 RNIC,与最先进的 RDMA 方法相比,对于无竞争和竞争设置下的键值获取操作,我们可以分别实现高达 2.6 倍和 35 倍的加速,同时允许应用程序获得针对操作系统和进程崩溃的故障恢复能力。我们相信这项工作为 RNIC 卸载方面的各种创新打开了大门,这反过来又可以帮助指导 RDMA 标准的发展