作者/机构: Feng Ren†, Mingxing Zhang†, Kang Chen†, Huaxia Xia‡, Zuoning Chen♣, Yongwei Wu† (†清华大学; ‡美团; ♣中国工程院)

A1 主要贡献

本文旨在解决IOPS密集型的内存分离应用在现代多核服务器上扩展性不佳的问题。研究发现,当核心数超过32个时,这类应用的吞吐量通常会达到瓶颈并开始下降,无法充分利用现代数据中心中拥有数十甚至数百个核心的机器。

通过对RDMA网络接口卡(RNIC)内部架构的深入分析,本文识别出三个主要的扩展性瓶颈:
1. 门铃寄存器(doorbell register)的隐式争用:当创建大量队列对(QP)时,默认的资源分配机制会导致多个线程共享同一个门铃寄存ר,从而引发锁争用,限制了并行度。
2. 过多的未完成工作请求(outstanding work requests, OWRs)导致的缓存颠簸(cache trashing):RNIC内部的缓存(如WQE缓存)大小有限,过多的并发RDMA操作会导致缓存未命中率增加,RNIC需要通过昂贵的PCIe DMA读取来获取元数据,从而降低性能。
3. 失败的CAS(比较并交换)重试造成的IOPS浪费:在应用层,为保证数据一致性而采用的无锁或乐观锁机制,在高并发下会导致大量的CAS操作失败和重试,这些重试消耗了宝贵的网络IOPS。

为解决这些底层且复杂的问题,本文提出了Smart,一个RDMA编程框架。该框架通过提供类似于原生单边RDMA verbs的接口,向应用开发者隐藏了解决上述瓶颈所需的底层技术细节。Smart框架的核心技术包括:
1. 线程感知的资源分配:为每个线程分配独立的门铃寄存器,从根本上消除隐式争用。
2. 基于信用的自适应工作请求节流策略:动态调整并发深度,避免RNIC缓存颠簸。
3. 自适应退避技术:通过指数退避和并发度控制,减少失败的CAS操作,避免IOPS浪费。

本文通过使用Smart重构了三个先进的内存分离应用来验证其效果:
- RACE(无锁哈希表):修改44行代码,吞吐量提升高达132.4倍
- FORD(持久化事务处理系统):修改16行代码,吞吐量提升高达5.2倍
- Sherman(B+树):修改48行代码并加入额外的推测性查找优化,将其内存访问模式从带宽密集型转为IOPS密集型,速度提升2.0倍

图1. 内存分离架构中的主要资源争用点。
图1. 内存分离架构中的主要资源争用点。

A3 背景知识

2.1 内存分离

传统数据中心的资源利用率问题。传统数据中心由计算和内存资源捆绑在一起的单体服务器构成。由于资源比例固定,这种架构导致了内存利用率低下【46, 51】。为了解决这个问题,内存分离架构【3, 9, 52, 54, 64】被提了出来。与传统单体服务器不同,计算和内存资源被物理分离到计算池和内存池中。计算池由计算刀片组成,每个刀片只有少量DRAM缓冲区但可能包含许多CPU核心。另一方面,内存池是内存刀片的集合,每个刀片拥有大量内存但计算能力较弱(例如1-2个CPU核心),因此无法处理大量计算【64, 68】。两种刀片都通过高速网络互连,如RDMA、Gen-Z【7】和CXL【6】。由于RDMA已在许多数据中心部署,本文我们考虑计算刀片使用单边RDMA动词访问内存刀片。

2.2 RDMA网络

RDMA技术及其关键组件。RDMA是内存分离的关键技术。最近的RNIC,如Mellanox ConnectX-6,已经实现了高达200 Gbps的带宽和低于600纳秒的延迟,这对于许多分离式应用来说是足够的【14, 17】。此外,RDMA支持单边动词,即READ、WRITE、CAS(比较并交换)和FAA(取值并加一),这些操作直接在远程内存上进行,而无需涉及内存刀片中较弱的CPU。

图2. Mellanox ConnectX-6中的门铃寄存器。
图2. Mellanox ConnectX-6中的门铃寄存器。

RDMA编程接口与内部机制。RDMA的编程接口围绕队列对(QP)的抽象展开。每个QP包含一对发送队列和接收队列,并与一个完成队列(CQ)相关联。对于每个RDMA操作,库将一个工作请求(WR)提交到QP。然后,在接收方的RNIC确认一个WR后,发送方的RNIC执行一个PCIe DMA写操作,附加一个CQ条目,并通过持续轮询CQ来通知应用程序。然而,在这个简单的抽象之下,RNIC的内部结构非常复杂,应用程序开发人员对此并不熟悉。根据我们的调查,一些最重要的机制描述如下。

门铃寄存器(DB)及其争用问题。门铃寄存器(DB)是RNIC用来接收新入队工作请求通知的机制。图2展示了ConnectX-6中的门铃寄存器【12】。默认情况下,每个RDMA设备上下文最多分配16个DB,包括4个低延迟DB和12个中等延迟DB。每个低延迟DB只与一个QP关联,但多个QP可以与同一个中等延迟DB关联。这些门铃寄存器映射在用户访问区域(UAR)中,驱动程序库执行内存映射I/O(MMIO)写操作来更新它们。对于从Connect-IB到ConnectX-7使用的驱动程序,对同一DB的更新都由一个自旋锁保护。因此,当一个DB被不同线程使用的多个QP共享时,会发生隐式的线程争用,并成为限制分离式应用程序并行性的隐式瓶颈。例如,如图2(b)所示,使用QP16的线程T1可能会被使用QP28的线程T2争用,因为两个QP都与DB16相关联。显然,这种默认映射没有为大规模并行应用进行优化。

内存转换与保护表及其缓存。许多现有工作担心过多的QP会耗尽RNIC中小的片上SRAM缓存,并导致缓存颠簸问题【5, 16, 25, 41】。但是,RNIC内部的缓存实际上用于缓存许多其他重要对象,其大小与QP数量不成比例【26】。例如,一旦RNIC通过门铃响铃被通知,它必须通过查找内存转换表(MTT)和内存保护表(MPT)来执行虚拟到物理地址的转换和安全检查。这些映射表被缓存在MTT/MPT缓存中以实现更好的性能,其大小不取决于QP的数量,而是取决于设备上下文、内存区域(MR)和内存窗口(MW)的数量。根据Mellanox Neo-Host【13】收集的内部性能计数器,在一个典型环境中,应用程序的所有线程共享同一个设备上下文,因此也共享相同的MTT/MPT,MTT/MPT缓存的命中率在大多数情况下高于95%。相比之下,当创建多个设备上下文时,MTT/MPT缓存的命中率可能下降到70%以下,因为每个设备上下文都会单独分配内存区域,即使每个内存区域的大小只有几MB。因此,共享设备上下文以减少全局数据结构的冗余不仅有利于管理,也有利于性能。其他工作【5, 30】也证实了类似的现象。

未完成工作请求(OWRs)与缓存颠簸。未完成工作请求(OWRs)是已入队但其完成条目尚未从CQ中轮询的工作请求。在从远程QP接收消息后,RNIC必须检索相应工作请求的元数据,这些元数据可能驻留在片上WQE缓存中。缓存未命中会导致PCIe DMA读取,这要昂贵得多【5, 41】。因此,当未完成工作请求的数量超过某个阈值时,会发生缓存颠簸。然而,较少的OWRs也可能由于缺乏并行性而导致性能下降。正如我们稍后在§3.2中将看到的,正确的配置取决于系统当前的整体并行性,因此应该动态调整。

A3 扩展性瓶颈

本节使用微观基准测试和PCIe/RNIC性能计数器来分析影响内存分离应用扩展性的因素,这些因素也激发了Smart的设计。本节所有评估的设置与§6一致。

3.1 隐式门铃争用

假设与验证方法。经过我们的调查,我们推测门铃寄存器中的隐式争用是分离式应用程序中并行性的主要限制。为了验证这个假设,我们比较了现有工作使用的三种不同类型的分配机制以及一种新颖的、能够感知门铃寄存器结构的方法:
1. 共享QP 【20】:所有线程共享一个单一的QP。
2. 多路复用QP 【16, 52, 53】:每个QP由k个线程以NUMA感知的方式共享。
3. 每线程QP 【1, 54, 57, 64】:每个线程拥有并使用一个专用的QP。
4. 每线程门铃(§4.1):每个线程不仅关联一个独立的QP,还关联一个独立的门铃寄存器。

图3. 使用不同QP分配策略的吞吐量。
图3. 使用不同QP分配策略的吞吐量。

实验结果与分析。我们构建了一个基准测试工具,用于测量在任意线程数和任意并发深度d(即每个线程的OWR数)下的READ/WRITE操作吞吐量。每个线程重复提交d个工作请求,敲响门铃,然后等待确认。每个工作请求的远程地址在1GB内存区域内均匀随机选择。图3显示了在不同线程数和相同并发深度8下,8字节READ/WRITE操作的结果。当线程数少于32时,QP的数量是主要瓶颈。“每线程QP”和“每线程门铃”策略在这些情况下比其他连接多路复用策略的性能高出2.4倍至130.1倍。然而,当线程数超过32时,“每线程QP”策略的吞吐量急剧下降。例如,当线程数增加到96时,“每线程QP”的吞吐量减少了一半。相比之下,“每线程门铃”能够释放更高并行性的潜力。对于8字节READ请求,“每线程门铃”的吞吐量比“每线程QP”高出最多5.6倍/3.2倍。“每线程门铃”的最大吞吐量可以达到110 MOPS。我们使用Linux perf工具和Intel VTune Profiler【11】分析了“每线程QP”策略(图3中线程数为96)的执行开销。高达74.05%的总执行时间被门铃相关自旋锁的pthread_spin_lock()消耗。这表明门铃相关的自旋锁在“每线程QP”的高并行环境下会产生显著的开销。读者可能也注意到曲线末尾“每线程门铃”的吞吐量下降,这与下一节将讨论的缓存颠簸问题有关。关于“每线程门铃”的具体实现细节在§4.1中解释。我们还用更大的有效负载大小(最大64字节)运行了相同的基准测试,观察结果仍然成立。有效负载大小超过64字节的工作负载是带宽密集型,而不是IOPS密集型。

3.2 缓存颠簸

现象观察与实验设计。在消除了隐式门铃争用后,我们仍然观察到随着未完成工作请求数量的增加,吞吐量会下降。我们高度怀疑这种现象与上面讨论的缓存颠簸问题有关。由于缓存大小和替换策略对产品供应商来说是保密的,我们使用§3.1中的基准测试工具来描述这种现象。具体来说,我们使用“每线程门铃”策略来避免门铃争用,然后调整线程数(即并行度)和OWR数量(即并发深度)。然后我们为每个设置测量随机8字节READ和WRITE请求的吞吐量。每个线程的特定并发深度是通过每提交d个工作请求后轮询完成队列来实现的,结果如图4a所示。

图4. 不同线程数和未完成工作请求(OWRs)下的性能指标。
图4. 不同线程数和未完成工作请求(OWRs)下的性能指标。

并发度与并行度的权衡。从图中可以看出,更多的OWRs可以带来更好的吞吐量,因为它们可以隐藏网络往返延迟。最大读取IOPS是在96个线程×每个线程8个OWRs = 768个并发工作请求时实现的。相比之下,较少的并行性需要更多的并发性来达到类似的吞吐量。例如,36个线程需要32个未完成工作请求才能达到101 MOPS,这需要32 × 36 = 1152个并发工作请求(即多50%的并发性,但吞吐量仍低5%)。然而,随着并发深度的增加,吞吐量会下降。对于96个读取线程,每个线程32个OWRs的吞吐量仅为每个线程8个OWRs的49.5%。通过我们的基于控制变量的实验,我们表明不同级别的并行性需要相应适当的并发深度才能实现最佳吞吐量。

缓存未命中的证据。我们通过测量PCIe入站带宽,即RNIC访问主机DRAM的流量,证实了RNIC中缓存未命中的增加。图4b显示了不同线程数和未完成工作请求下每个工作请求的平均DRAM访问流量。在96个读取线程和每个线程32个OWRs的情况下,它比每个线程8个OWRs的情况高1.9倍(180字节对93字节)。这些额外的PCIe读取主要是由WQE缓存未命中引起的。比较图4a和4b,我们发现吞吐量随着每个工作请求的DRAM访问流量的增加而降低。其他特征研究也观察到类似现象【24, 26】,但现有解决方案通常假设固定大小的并发深度足以避免此问题。然而,在我们的扩展场景中,即使是少量的并发深度也可能成为问题,因为可能有一百多个线程。

3.3 失败的重试

接收端争用问题。除了上述观察到的发送方争用之外,接收方的争用也可能导致分离式应用程序的扩展性瓶颈。根据我们的调查,更新争用(导致不成功的RDMA CAS重试)是接收方争用中最常见和最重要的扩展性瓶颈。具体来说,在没有服务器端协调的情况下,分离式内存应用程序必须使用单边RDMA操作来实现应用侧的协调,这是接收方争用的主要来源。例如,Sherman【57】指出,使用RDMA CAS的基本自旋锁存在扩展性问题:如果客户端线程未能获取锁,它会立即重试,这导致额外的远程网络访问,从而浪费了RDMA网络有限的IOPS。Sherman通过使用分层片上锁(HOPL)技术解决了这个问题。

图5. 在不同(a)线程数(并发深度=8,Zipfian参数=0.99)和(b)Zipfian分布参数(16个线程)下哈希表更新的性能。
图5. 在不同(a)线程数(并发深度=8,Zipfian参数=0.99)和(b)Zipfian分布参数(16个线程)下哈希表更新的性能。

问题普遍性与案例分析。然而,除了基于锁的方法,这种由不成功重试引起的扩展性问题存在于更广泛的应用中,例如无锁数据结构【52, 68, 69】和乐观锁【61】。图5展示了RACE【68, 69】(一个分离式无锁哈希表)在不同并行度和不同Zipfian分布【19】下的吞吐量。从图中可以看出,尽管RDMA操作的并行性可以扩展到更多的线程,但RACE的峰值吞吐量仅在8个线程时达到。对于更多的线程,吞吐量下降,99%的延迟增加了高达17.1倍。我们还研究了数据倾斜如何影响应用操作的扩展性。随着Zipfian参数α从0增加到0.99,中位延迟增加了1.9倍,99%的延迟增加了78.4倍。根据我们的扩展性分析,这种扩展性问题的一个主要原因是高数据争用导致的大量不成功重试。在RACE中,一次失败的试图更新桶槽的RDMA CAS后,会触发一次重试,该重试会发起三个额外的RDMA请求(即,重新读取此桶,写入键/值条目,并再次尝试原子地更新此槽)。这种重试浪费了RNIC有限的IOPS资源。相比之下,最初的RACE论文【68, 69】仅评估了分布在四台机器上的最多128个并发任务,因此上述扩展性问题不显著。正如我们稍后在§6.3中讨论的,67.9%的更新操作导致一次或多次不成功的重试。通知机制在我们的环境中不适用,因为它需要远程CPU或专用硬件(如SmartNIC【57】)的参与。

A2 方法细节

受扩展性瓶颈分析的启发,我们设计了Smart,一个用于构建可扩展分离式应用的库。我们在Smart中引入了以下技术:
1. 线程感知的资源分配 (§4.1)。Smart避免了来自队列对和门铃寄存器共享的隐式线程间争用。
2. 自适应工作请求节流 (§4.2)。Smart动态控制并发深度以避免RNIC缓存颠簸。阈值是动态设置的。
3. 冲突避免 (§4.3)。Smart使用截断指数退避来降低失败重试的速率。它还根据重试率进一步控制并发深度。

更重要的是,这些技术的细节被隐藏在Smart易于使用的接口之下,该接口提供了一套基于协程的异步API,类似于原始的单边RDMA动词。

4.1 线程感知的资源分配

设计目标与错误方法。为了避免资源共享导致的线程争用(如图6a所示),除了在每线程基础上分配QP【1, 54, 57, 64】,Smart提出了线程感知的资源分配。在Smart中,同一线程使用的多个QP与一个共享的门铃寄存器相关联。一个简单的方法是为每个线程创建独立的RDMA设备上下文【36, 57】。这实质上避免了隐式线程争用,因为不同的上下文使用不同的门铃寄存器集。然而,如§2.2所讨论的,共享设备上下文不仅有利于管理,也有助于提高性能。当使用多个设备上下文时,本地内存必须为每个上下文注册为MR和MW。MR/MW的数量会高出数十甚至数百倍,这增加了MPT/MTT的大小,并因MPT/MTT缓存颠簸【41】而导致性能下降。此外,ibv_open_device()的共享语义取决于设备供应商的具体实现。它可能不会为每次调用返回一个不同的上下文【2】。

图6. (a) 两种潜在的RDMA资源争用:1. 多个线程共享同一个QP(T1和T2),2. 多个线程使用不同的QP,但在内部共享同一个门铃寄存器(T3和T4)。
图6. (a) 两种潜在的RDMA资源争用:1. 多个线程共享同一个QP(T1和T2),2. 多个线程使用不同的QP,但在内部共享同一个门铃寄存器(T3和T4)。

图6. (b) 以线程感知的方式分配RDMA资源以避免隐式争用。
图6. (b) 以线程感知的方式分配RDMA资源以避免隐式争用。

图6. 线程感知的资源分配。计算刀片中有四个线程(T1到T4)连接到内存刀片(MB1和MB2)。

Smart的分配机制。相比之下,Smart提出了共享同一设备上下文的线程感知分配机制,这是应用程序开发人员的常见做法。如图6b所示,Smart以每线程方式分配QP、CQ和DB,这些资源不被多个线程共享。其他资源,包括保护域(PD)和内存区域(MR),在所有线程之间共享,因此本地内存只需注册一次。为每个本地线程和远程内存刀片的配对创建一个QP。因此,每个线程包含多个QP(用于不同的远程刀片),但它们与同一个CQ和DB相关联。Smart使用单个ibv_poll_cq()调用来确定当前线程是否有任何已完成的工作请求。QP也与属于同一线程的DB相关联,因此避免了共享DB引起的线程争用。在同一线程中运行的协程共享相同的QP和CQ。更具体地说,Smart为每个线程维护一个QP池,其中同一池中的所有QP都与同一个CQ和DB相关联。一些QP是活动的(例如QP池1中的QP1和QP5),而其他QP是空闲的(例如QP9)。每个线程仅从其自己的QP池中分配QP,并在使用后将其释放回自己的QP池。

实现细节与限制。为了在应用程序启动期间构建足够的QP池,可用的中等延迟门铃寄存器的数量不应少于线程数,但默认情况下只有12个。RNIC驱动程序支持调整每个RDMA上下文的门铃寄存器数量,例如Mellanox产品的环境变量MLX5_TOTAL_UUARS。然而,驱动程序也限制了中等延迟门铃寄存器的数量,因此需要对驱动程序代码进行少量修改。在Mellanox ConnectX-6 RNIC中,DB的最大有效数量是512【12】,在Intel E810 RNIC中是256【22】,这通常大于每台机器的CPU核心数。这一限制也表明DB和QP之间的一对一映射是不切实际的,因为QP的数量可能远大于512。另一方面,由于门铃寄存器的结构没有向开发者公开,没有外部API可用于将QP与特定的门铃寄存器绑定。然而,在深入研究RNIC用户模式驱动程序的源代码后,我们发现每个新创建的QP都以循环方式与门铃关联(如图2b所示)。因此,在创建QP之前,我们可以知道它将与哪个门铃寄存器关联。这可以用来确定关联的QP池。不同的提供商可能有不同的DB分配策略,但只要它是确定性的,我们的机制就是可行的。我们也提倡提供商提供明确的DB绑定API,因为它们对性能至关重要。

内存刀片的资源分配。因为RC连接需要QP到QP的通信,内存刀片需要建立与计算刀片总线程数一样多的QP。我们确认出站吞吐量不会随着QP数量的增加而下降【5】。因此,只要内存刀片从不发布RDMA请求或轮询CQ条目,就无需按线程分配RDMA对象。

4.2 自适应工作请求节流

节流算法设计。为防止因RNIC缓存颠簸导致的性能下降,Smart使用基于信用的节流【41】来限制QP的未完成工作请求数量。我们缺乏关于特定RNIC使用的缓存替换策略的信息,因此Smart提出了一种启发式机制。如§3.2所讨论的,一旦确定了线程数,通过限制OWR的上限就可以避免性能下降。我们还假设应用程序工作负载在短时间内(约500毫秒)保持稳定,以便可以定期更新最佳阈值。基于这些观察,我们设计了一种自适应节流算法(算法1,第1-13行)。线程局部变量credit表示可用信用额度。它最初设置为C_max,即最大信用额度。我们使用工作请求ID字段(wr_id)来指示要提交的工作请求数量(第4行)。在发出ibv_post_send()之前,信用额度减去大小(第7行)。然而,如果信用额度耗尽,发布工作请求将被节流,直到信用额度得到补充(第6行)。如果CQ轮询成功,信用额度将增加已完成工作请求的数量(第13行)。

算法1 工作请求节流

1: thread_local C_max, credit ← C_max
2: procedure SmartPostSend(qp, wr, bad_wr)
3:   size = Length(wr) ▷ wr链表的长度
4:   wr[size − 1].wr_id ← size, ... ▷ 将元数据填充到wr_id中
5:   while credit − size < 0 do
6:     Wait ▷ 延迟提交直到信用足够
7:   credit ← credit − size ▷ 扣除信用
8:   ibv_post_send(qp, wr, bad_wr) ▷ bad_wr返回失败的WR
9: procedure SmartPollCQ(cq, num_entries, wc)
10:  ibv_poll_cq(cq, num_entries, wc)
11:  for each w in wc do
12:    size, ... = w.wr_id ▷ 从wr_id中提取元数据
13:    credit ← credit + size ▷ 补充信用
14: procedure UpdateCMax(target)
15:  credit ← credit + (target − C_max), C_max ← target
16: procedure Update
17:  candidates = (4, 6, 8, 10, 12) ▷ C_max的候选值
18:  best_n = −1, best_target = −1
19:  for each target in candidates do
20:    UpdateCMax(target)
21:    n = Δ毫秒内完成的RDMA工作请求数
22:    if n > best_n then
23:      best_n ← n, best_target ← target
24:  UpdateCMax(best_target)

可用信用的动态更新。根据我们的观察(§3),最佳最大信用额度C_max取决于线程数和工作负载。Smart在应用程序执行期间定期更新C_max。我们遵循基于纪元(epoch-based)的模型,即每个纪元由一个更新阶段和一个稳定阶段组成。应用程序在任何阶段都正常运行。为了增加或减少可用信用,Smart使用UpdateCMax函数,其中targetC_max的新值(第15行)。在更新阶段,Smart调用Update(算法1中的第14-24行)来找到C_max的最佳值。对于C_max的每个候选值,它更改可用信用(第20行),然后计算接下来Δ = 8毫秒内完成的RDMA操作数量。这样的间隔允许应用程序生成足够数量的RDMA请求,以准确估计RDMA请求的吞吐量。在更新阶段结束时,Smart确定实现最高吞吐量的最佳target,并更新可用信用(第24行)。C_max在稳定阶段不改变。在Smart中,稳定阶段持续时间更长,即T = 60 × Δ = 480毫秒。

4.3 冲突避免

基于指数退避的解决方案。在处理了发送端的扩展性瓶颈后,接收端的争用,特别是由不成功的CAS操作引起的IOPS浪费,导致应用程序性能下降。我们对此问题的解决方案基于带有自适应限制的指数退避【39】。截断指数退避的基本思想很简单:一旦检测到不成功的重试(例如,RDMA CAS失败),我们将下一次重试延迟d个CPU周期。因此,每次检测到不成功的重试时,延迟时间乘以2,即第k次重试时d = C_0 * 2^k。为了避免极长的延迟,这会使性能变得更糟【32】,Smart通过限制最大退避周期C_max来使用截断变体的指数退避。为避免冲突,退避周期也是随机化的。总而言之,对于第k次尝试,延迟为:
$d = random(0, \min(C_{max}, C_0 \times 2^k))$
其中random(x)是0和x之间的随机值。在Smart中,退避单位是C_0 = 4096个周期,这接近于一次RDMA往返的时间。C_max是使用以下算法动态确定的。

动态退避限制。最大退避周期,即C_max,对性能有显著影响。较小的C_max会增加冲突的概率,从而降低可扩展性。然而,较大的C_max也会导致性能下降,特别是如果某些操作遭受更长的延迟。最佳的C_max取决于并发操作的数量。我们使用一种算法来自动找到最佳的C_max。每毫秒,Smart收集并统计所有操作的重试百分比。我们称之为重试率,或p。如果p大于高水位标记p_highC_max将乘以2。如果p小于低水位标记p_lowC_max将除以2。我们确保C_maxC_0(退避单位)和C_limit(最长退避周期,默认为C_limit = 2^10 * C_0)之间。在我们的原型中,我们选择p_high = 0.5p_low = 0.1,这有助于C_max根据工作负载的倾斜度收敛到不同的值。例如,对于倾斜(高争用)的更新,C_max = C_limit = 1.6μs,而对于均匀更新(即重试很少),C_max = C_0

并发深度节流。为了进一步减少由同一线程执行的并发操作引起的冲突,Smart通过基于信用的协程节流来减少并发操作的数量。这实质上控制了并发任务的数量,因为Smart是作为一个基于协程的异步框架实现的。例如,在均匀工作负载下,当协程等待RDMA ACK时,它会暂停执行(yield)。然而,在高争用工作负载下,协程直到当前操作完成才暂停。为实现这一点,调度程序必须为每个线程保持最多n_max个可以并发执行的协程。其他协程必须被阻塞,直到一些正在运行的协程完成了它们当前的操作并补充了它们的信用。与C_max类似,n_max的值根据重试率动态确定。当p高于p_high或低于p_low时,我们收缩或扩展n_max。请注意,只有当C_max达到下限或上限时(例如,中止率p > p_highn_max = 1),n_max才会被更新。

A4 实验环境

  • 硬件配置:
    • 服务器: 8台机器,每台配备2颗Intel Xeon Gold 6240R CPU(共96核),384 GB DRAM。
    • 持久内存: 每台机器1.5 TB Intel Optane DC Persistent Memory。
    • 网络: 每台机器配备一张200 Gbps Mellanox ConnectX-6 InfiniBand RNIC,通过200 Gbps Mellanox InfiniBand交换机连接。测试平台的RNIC硬件IOPS上限为110.0 MOP/s。
  • 软件配置:
    • 操作系统: Ubuntu 20.04 LTS (Linux kernel 5.4.0)。
    • RDMA库: Mellanox OpenFabrics Enterprise Distribution for Linux (MLNX_OFED) v5.3-1.0.0.1。
  • 实验设置:
    • 内存页面: 使用2MB巨页以减少RNIC的页转换缓存未命中。
    • 内存策略: 启用内存交错(interleave)策略。
    • 并发深度: 除非另有说明,每个线程使用8个协程以最大化吞吐量,即并发深度为8。
    • 应用与数据集:
      • 哈希表 (Smart-HT vs. RACE): 使用YCSB基准测试,包含写重(50%更新/50%查找)、读重(5%更新/95%查找)和只读(100%查找)三种负载。键分布遵循Zipfian分布(偏斜度α=0.99)。预加载1亿个8字节键值对。
      • 分布式事务 (Smart-DTX vs. FORD): 使用SmallBank(85%读写事务)和TATP(80%只读事务)两个OLTP基准。数据库记录存储在NVM上。
      • B+树 (Smart-BT vs. Sherman): 使用与哈希表相同的YCSB负载。

A4 实验结果

6.2 端到端性能

6.2.1 哈希表 (Smart-HT vs. RACE)

扩展性:如图7所示,Smart-HT在单计算节点的扩展性上远超RACE。在写重负载下,RACE在8线程时达到2.8 MOP/s的峰值,而Smart-HT在48线程时达到5.7 MOP/s。在读重和只读负载下,Smart-HT的吞吐量分别达到21.2 MOP/s和23.7 MOP/s,而RACE均低于11.4 MOP/s。在多计算节点(横向扩展)场景下(图7(d)-(f)),由于并发度更高导致争用加剧,Smart-HT的优势更加明显,在写重和读重负载下分别比RACE高出132.4倍和77.3倍。

图7. 不同计算线程数下哈希表的吞吐量。
图7. 不同计算线程数下哈希表的吞吐量。

性能分解:如图8所示,各项技术贡献如下:
1. ThdResAlloc (线程感知资源分配): 在读密集型负载中效果最显著,解决了门铃争用这一主要性能瓶颈。
2. WorkReqThrot (工作请求节流): 在写重负载下、线程数在8到32之间时有效,通过避免缓存颠簸提升了RDMA IOPS。
3. ConflictAvoid (冲突避免): 对于高争用(写重、数据倾斜)的负载,此技术通过指数退避减少失败重试,对提升吞吐量和扩展性起决定性作用。

图8. 哈希表性能分解。
图8. 哈希表性能分解。

吞吐量 vs. 延迟:如图9所示,在只读负载下,Smart-HT相比RACE,中位延迟降低了69.6%,尾延迟(99%)降低了80.6%。这主要归功于线程感知资源分配避免了线程间的阻塞。

图9. 哈希表中的吞吐量与延迟关系(只读负载)。
图9. 哈希表中的吞吐量与延迟关系(只读负载)。

6.2.2 持久化分布式事务 (Smart-DTX vs. FORD+)

扩展性:如图10所示,FORD+的吞吐量在24-32线程后因隐式门铃争用而严重下降。Smart-DTX通过线程感知资源分配解决了此问题,在SmallBank和TATP基准上吞吐量分别比FORD+高出5.2倍和2.6倍。

图10. 不同执行线程数下分布式事务的吞吐量。
图10. 不同执行线程数下分布式事务的吞吐量。

吞吐量 vs. 延迟:如图11所示,Smart-DTX不仅提升了最大吞吐量,还显著降低了延迟,SmallBank和TATP的中位延迟分别降低了45.8%和77.0%。

图11. 分布式事务中的吞吐量与延迟关系。
图11. 分布式事务中的吞吐量与延迟关系。

6.2.3 B+树 (Smart-BT vs. Sherman+)

扩展性:如图12所示,Smart-BT在读重和只读负载下,性能分别比Sherman+高出1.8倍和2.0倍。性能提升主要来自两方面:
1. 推测性查找 (Speculative Lookup): 该优化将原本需要读取整个叶节点的查找操作,变为只读取单个键值对,大大减少了读放大,使应用从带宽密集型转为IOPS密集型。仅此项优化就带来了高达1.6倍的吞吐量提升。
2. Smart技术: 在应用变为IOPS密集型后,Smart的线程感知资源分配技术解决了其在多核下的扩展性瓶颈。

图12. 不同计算线程数下B+树的吞吐量。
图12. 不同计算线程数下B+树的吞吐量。

6.3 微观基准测试

线程感知资源分配:如图13(a)所示,该技术使得RDMA吞吐量能够达到RNIC的硬件上限(110.0 MOP/s),性能比每线程QP策略高出1.0至4.3倍。

自适应工作请求节流:如图13(a)和(b)所示,该技术在高并发线程(>56)或大批量工作请求(>8)时能有效防止吞吐量下降,避免了缓存颠簸。在动态变化的负载下(表1),该技术依然能保持接近最优的吞吐量。

图13. 不同QP分配和节流技术的性能:(a)不同线程数,工作请求批处理大小=16;(b)不同批处理大小,线程数=96。
图13. 不同QP分配和节流技术的性能:(a)不同线程数,工作请求批处理大小=16;(b)不同批处理大小,线程数=96。

表1. 动态变化工作负载下8字节RDMA读操作的吞吐量(MOP/s)。
表1. 动态变化工作负载下8字节RDMA读操作的吞吐量(MOP/s)。

冲突避免:如图14所示,在高争用(100%更新,Zipfian分布)场景下,不使用冲突避免技术的应用性能会随着线程数增加而急剧下降,平均每次操作重试次数高达11.5次。Smart通过指数退避、动态限制和协程节流,将平均重试次数降至1.1次,并显著提升了吞吐量。在Smart-HT中,93.3%的更新操作无需任何额外的网络往返。

图14. 冲突避免的性能指标:(a)吞吐量;(b)每次操作的平均重试次数;(c)96个线程下每次操作的重试次数分布。
图14. 冲突避免的性能指标:(a)吞吐量;(b)每次操作的平均重试次数;(c)96个线程下每次操作的重试次数分布。

A5 结论

本文揭示了限制IOPS密集型内存分离应用在多核服务器上扩展的三个主要瓶颈:门铃寄存器的隐式争用、RNIC缓存颠簸以及失败CAS操作导致的IOPS浪费。针对这些问题,本文提出了相应的通用解决方案,并将其集成到一个易于使用的RDMA编程框架——Smart中,从而向应用开发者隐藏了底层的技术细节。评估结果表明,通过修改少量代码(通常少于50行),使用Smart重构的现有先进内存分离系统,在操作吞吐量和延迟方面都取得了显著的性能提升。

方法细节中的引用汇总

  • 【2, ibv_open_device() - rdmamojo, 2022】: 在§4.1中引用,说明ibv_open_device()的共享语义取决于设备供应商的具体实现,它可能不会为每次调用返回一个不同的上下文。
  • 【5, Scalable rdma rpc on reliable connection with efficient resource sharing, 2019, EuroSys】: 在§2.2、§4.1中引用。§2.2中指出,现有工作担心过多QP会耗尽RNIC缓存,且WQE缓存未命中代价高昂。§4.1中确认,内存刀片上的出站吞吐量不会随QP数量增加而下降。
  • 【12, Mellanox adapters programmer’s reference manual (prm), 2022】: 在§2.2、§4.1中引用。§2.2中用于说明ConnectX-6中的门铃寄存器结构。§4.1中指出ConnectX-6 RNIC中DB的最大有效数量是512。
  • 【13, Neo-host, 2022】: 在§2.2中引用,其内部性能计数器数据显示,共享设备上下文时MTT/MPT缓存命中率很高。
  • 【16, Farm: Fast remote memory, 2014, NSDI】: 在§1、§2.2中引用。§1中指出其提出了连接多路复用,但也显示了QP共享因锁而性能不佳。§2.2中指出其担心过多QP会耗尽SRAM缓存。
  • 【19, Quickly generating billion-record synthetic databases, 1994, SIGMOD】: 在§3.3中引用,作为实验中使用的Zipfian分布的来源。
  • 【22, Intel® ethernet controller e810, 2022】: 在§4.1中引用,指出Intel E810 RNIC中DB的最大数量是256。
  • 【24, Datacenter rpcs can be general and fast, 2019, NSDI】: 在§3.2中引用,指出其他特征研究也观察到类似的缓存颠簸现象。
  • 【25, Using rdma efficiently for key-value services, 2014, SIGCOMM】: 在§2.2中引用,指出其担心过多QP会耗尽SRAM缓存。
  • 【26, Design guidelines for high performance rdma systems, 2016, USENIX ATC】: 在§2.2、§3.2中引用。§2.2中指出RNIC缓存中缓存了许多与QP数量不成比例的重要对象。§3.2中指出其他特征研究也观察到类似的缓存颠簸现象。
  • 【27, Fasst: Fast, scalable and simple distributed transactions with two-sided (rdma) datagram rpcs, 2016, OSDI】: 在§1中引用,指出其显示了QP共享因锁而性能不佳。
  • 【30, Understanding rdma microarchitecture resources for performance isolation, 2023, NSDI】: 在§2.2中引用,其工作也证实了共享设备上下文对性能有益。
  • 【32, Performance analysis of exponential backoff, 2005, IEEE/ACM ToN】: 在§4.3中引用,说明极长的退避延迟会使性能变得更糟。
  • 【36, X-rdma: Effective rdma middleware in large-scale production environments, 2019, IEEE CLUSTER】: 在§4.1中引用,作为为每个线程创建独立RDMA设备上下文方法的例子。
  • 【39, Ethernet: Distributed packet switching for local computer networks, 1976, Communications of the ACM】: 在§4.3中引用,作为指数退避思想的来源。
  • 【41, Birds of a feather flock together: Scaling rdma rpcs with flock, 2021, SOSP】: 在§1、§2.2、§4.1、§4.2中引用。§1中指出有工作观察到RDMA吞吐量不佳。§2.2中指出其担心QP过多导致缓存颠簸,且WQE缓存未命中昂贵。§4.1中指出MPT/MTT缓存颠簸问题。§4.2中作为基于信用的节流方法的参考。
  • 【52, Disaggregating persistent memory and controlling them remotely: An exploration of passive disaggregated key-value stores, 2020, USENIX ATC】: 在§1、§3.3中引用。§1中作为连接多路复用和无锁数据结构的例子。§3.3中作为无锁数据结构的例子。
  • 【53, Lite kernel rdma support for datacenter applications, 2017, SOSP】: 在§1中引用,作为连接多路复用优化的例子。
  • 【57, Sherman: A write-optimized distributed b+ tree index on disaggregated memory, 2022, SIGMOD】: 在§1、§3.3、§4.1中引用。§1中作为使用HOPL锁解决冲突的例子。§3.3中指出其讨论了RDMA CAS自旋锁的扩展性问题,并提到SmartNIC可用于通知机制。§4.1中作为每线程分配QP和每线程创建设备上下文的例子。
  • 【61, Fast in-memory transaction processing using rdma and htm, 2015, SOSP】: 在§1、§3.3中引用,作为使用乐观锁的例子。
  • 【68, One-sided rdma-conscious extendible hashing for disaggregated memory, 2021, USENIX ATC】: 在§1、§3.3中引用,作为无锁数据结构RACE的参考。
  • 【69, Race: A one-sided rdma-conscious extendible hashing for disaggregated memory, 2022, TOS】: 在§1、§3.3中引用,作为无锁数据结构RACE的参考。