首先我们介绍一下RDMA的一些核心概念,当然了,我并不打算写他的API以及调用方式,我们更多关注这些基础概念背后的硬件执行方式和原理,对于这些原理的理解是能够写出高性能RDMA程序的关键。
RDMA的网卡(下文以RNIC指代)通过DMA来读写系统内存,由于DMA只能根据物理地址访问,所以RNIC需要保存一份目标内存区域的虚拟内存到物理内存的映射表,这个映射表被存储在RNIC的Memory Translation Table(MTT)中。同时,由于目前RDMA的访问大都基于Direct Cache Access,不支持page-fault,所以我们还需要保证目标内存区域是被pagelock住以防止操作系统将这部分内存页换出。
总结一下就是,当我们使用RDMA来访问一块内存的时候,这部分内存首先要被pagelock,接着我们还需要把这块内存虚拟地址到逻辑地址的映射表发送给RNIC用于后续的访问查找,这个过程就叫Memory Registeration,这块被注册的内存就是Memory Region。同时我们注册内存的时候需要指定这块内存的访问权限,RNIC将这个访问权限信息存储在Memory Protection Tables(MPT)中用于用户请求时的权限验证。
MTT和MPT被存储在内存中,但是RNIC的SRAM中会进行缓存。当RNIC接收到来自用户的READ/WRITE请求的时候,首先在SRAM中的缓存中查找用户请求的目标地址对应的物理地址以及这块地址对应的访问权限,如果缓存命中了,就直接基于DMA进行操作,如果没有命中,就得通过PCIe发送请求,在内存的MTT和MPT中进行查找,这带来了相当的额外开销,尤其是当你的应用场景需要大量的、细粒度的内存访问的时候,此时RNIC SRAM中的MTT/MPT命中缺失带来的影响可能是致命的。
Memory Region的注册是一个耗时的操作,但大部分情况下,我们都只需要在最开始的时候做一次或者多次。现在也有不需要注册MR基于on-demand paging的方式来访问的,比如AWS的EFA协议。但今天先不展开这块的内容,因为这块更多是Unified Memory这个话题下的,之后我可能会把这个和GPU的UVM放在一起介绍下,因为他们的核心原理其实是一样的。
用户通过RDMA的Verbs API向RNIC发送指令,Verbs分为Memory Verbs和Message Verbs,Memory Verbs主要就是READ、WRITE以及一些ATOMIC的操作,Message Verbs主要包含SEND、RECV。Memory verbs是真正的CPU Bypass以及Kernel Bypass,所以总归是性能比较好的。Message Verbs需要Responder的CPU的参与,相对而言更灵活,但是性能相比Memory Verbs而言一般不太行。
RDMA的hosts之间是通过Queue Pair(QP)来通信的,一个QP包含一个Send Queue(SQ),一个Receive Queue(RQ)以及对应的Send Completion Queue(SCQ)和Receive Completion Queue(RCQ)。用户发送请求的时候,把请求封装为一个Work Queue Element(WQE)发送到SQ里面,然后RDMA网卡会把这个WQE发送出去,当这个WQE完成的时候,对应的SCQ里面会被放一个Completion Queue Element(CQE),然后用户可以从SCQ里面Poll这个CQE并通过检查状态来确认对应的WQE是否成功完成。需要指出的是,不同的QP可以共用CQ来减少SRAM的存储消耗。
接下来,我们重点介绍一下QP背后的知识。
首先,当我们创建了QP之后,系统是需要保存状态数据的,比如QP的metadata,拥塞控制状态等等,除去QP中的WQE、MTT、MPT,一个QP大约对应375B的状态数据。这在以前RNIC的SRAM比较小的时候会是一个比较重的存储负担,所以以前的RDMA工作会有QP Sharing的研究,就是不同的处理线程去共用QP来减少meta data的存储压力,但是这会带来一定的性能的损失[1]。现在新的RNIC的SRAM已经比较大了,Mellanox的CX4、CX5系列的网卡的SRAM大约2MB,所以现在新网卡上,大家还是比较少去关注QP带来的存储开销,除非你要创建几千个,几万个QP。
其次,RNIC是包含多个Processing Unit(PU)的[2],同时由于QP内的请求处理是具有顺序的,且为了避免cross-PU的同步,一般而言我们认为一个QP对应一个PU来处理。所以,我们可以在一个线程内建立多个QP来加速你数据处理,避免RDMA程序性能瓶颈卡在PU的处理上[3]。
RDMA性能优化这个东西说复杂也复杂,说简单也简单。简单的点在于,从性能优化角度而言,其实软件层面我们可以做的设计和选择不会太多,因为性能上限是被硬件卡住的,所以我们为了追求尽可能逼近硬件上限的性能表现,其核心就在于按照硬件最友好的方式去做数据访问即可,没有特别多复杂的算法在这里面,当你想要高性能的时候,多多了解硬件就对了。对照着我们在上面介绍的三个核心概念,我们一一介绍性能优化的经验。
前面我们提到,当待请求的数据地址在RNIC SRAM中的MTT/MPT没有命中的时候,RNIC需要通过PCIe去在内存中的MTT和MPT进行查找,这是一个耗时的操作。尤其是当我们需要 high fan-out、fine-grained的数据访问时,这个开销会尤为的明显。现有针对这个问题的优化方式主要有两种:
当然,其实还有一些别的优化手段,在最近我们的工作中提出一种新的方式来提升地址翻译的性能,具体等工作开源出来之后我再来介绍介绍。
一个QP对应一个PU,这是我们对RNIC执行方式的一个简单建模。这个模型下,我们需要通过多QP来充分发挥多PU并行处理的能力,同时也要关注我们的操作减少PU之间的同步,PU之间同步对于性能有着较大的伤害。
对于RDMA的Verbs的使用,以我个人的经验来看,就是优先使用READ/WRITE,在一些需要CPU介入且需要Batch处理逻辑的,可以尝试使用SEND/RECV。过往的工作有很多基于READ/WRITE去构建Message Passing处理语义的工作[1, 6, 7],可以着重参考。
同时,一个READ/WRITE的WQE可以通过设置对应的FLAG来设置其是否需要在完成时需要被SIGNALED,如果不需要则该WQE完成时不会产生一个CQE。此时一个常见的优化技巧是,当你需要连续在一个QP中发送K个READ/WRITE请求时,只把最后一个请求设置为SIGNALED,其他均为UNSIGNALED,由于QP的执行本身具备顺序关系,所以最后一个执行完了后一定意味着之前的WQE都已经执行完了。当然,是否执行成功需要Application-Specific的方法来确认。
最经典的RNIC的使用方式自然是RNIC + System Memory,即直接通过RNIC来访问内存。但是随着GP-GPU、NVM的发展,通过RNIC来直接访问GPU或者通过RNIC来直接访问NVM都是目前比较成熟和热门的技术。RDMA + GPU可以大幅度加速GPU和GPU之间的通信,RDMA + NVM则可以大幅度的扩大内存容量,减少网络通信的需求。这块内容既涉及到硬件又涉及到操作系统的虚拟内存机制,要讲清楚需要不少篇幅,我们放在下一篇进行介绍。
本篇文章主要是介绍一些RDMA的基础概念以及背后的原理,基于这些概念和原理我们介绍了RDMA的常见性能优化技巧,下一篇我们将会介绍RNIC + X,包括RNIC + GPU以及RNIC + NVM的内容介绍,感兴趣的读者朋友可以保持关注~。