一线攻城狮实战经验:RDMA,好用却又很难用?

势不可挡的 RDMA

如今,服务器的网络带宽越来越高。当网络带宽迈过万兆这条线后,操作系统用于处理网络IO的开销就越来越难以忽视。在一些网络IO密集的业务中,操作系统本身成为了网络通信的瓶颈,这不仅会导致调用时延的增加(尤其是长尾),还会影响到服务的整体吞吐。

相对于网络带宽的发展速度,CPU性能的发展停滞是导致上述问题的主要原因。 因此,想从根本上解决CPU参与网络传输的低效问题,就要更多地借助专用芯片的能力,RDMA高性能网络势不可挡。 

RDMA(Remote Direct Memory Access),可以简单理解为网卡完全绕过CPU实现两个服务器之间的内存数据交换。其作为一种硬件实现的网络传输技术,可以大幅提升网络传输效率,帮助网络IO密集的业务(比如分布式存储、分布式数据库等)获得更低的时延以及更高的吞吐。

具体来说,RDMA技术的应用要借助支持RDMA功能的网卡以及相应的驱动程序。由下图所示,一旦应用程序分配好资源,其可以直接把要发送的数据所在的内存地址和长度信息交给网卡。网卡从内存中拉取数据,由硬件完成报文封装,然后发送给对应的接收端。接收端收到RDMA报文后,直接由硬件解封装,取出数据后,直接放在应用程序预先指定的内存位置。

由于整个IO过程无需CPU参与,无需操作系统内核参与,没有系统调用,没有中断,也无需内存拷贝,因此RDMA网络传输可以做到极高的性能。在极限benchmark测试中,RDMA的时延可以做到1us级别,而吞吐甚至可以达到200G。

RDMA技术说明

需要注意的是,RDMA的使用需要应用程序的代码配合(RDMA编程)。与传统TCP传输不同,RDMA并没有提供socket API封装,而是要通过verbs API来调用(使用libibverbs)。出于避免中间层额外开销的考虑,verbs API采用了贴近硬件实现的语义形态,导致使用方法与socket API差异巨大。 因此,对大多数开发者来说,无论是改造原有应用程序适配RDMA,还是写一个全新的RDMA原生的应用程序,都不容易。

RDMA 编程难在哪里?

如图所示,在socket API中,发送接收数据主要用到的接口如下:

 

Socket API

其中,write和read操作中的fd是标识一个连接的文件描述符。应用程序要发送的数据,会通过write拷贝到系统内核缓冲区中;而read实际上也是从系统内核缓冲区中将数据拷贝出来。 在绝大多数应用程序里,通常都会设置fd为非阻塞的,也就是说,如果系统内核缓冲区已满,write操作会直接返回;而如果系统内核缓冲区为空,read操作也会直接返回。 为了在第一时间获知内核缓冲区的状态变化,应用程序需要epoll机制来监听EPOLLIN和EPOLLOUT事件。如果epoll_wait函数因这些事件返回,则可以触发下一次的write和read操作。这就是socket API的基本使用方法。 作为对比,在verbs API中,发送接收数据主要用到的接口如下:

 

Verbs API

 

其中,ibv_是libibverbs库中函数和结构体的前缀。ibv_post_send近似于发送操作,ibv_post_recv近似于接收操作。发送接收操作里面的qp(queue pair)近似于socket API里面的fd,作为一个连接对应的标识。wr(work request)这个结构体里包含了要发送/接收的数据所在的内存地址(进程的虚拟地址)和长度。ibv_poll_cq则作为事件检测机制存在,类似于epoll_wait。

乍一看去,RDMA编程似乎很简单,只要把上述函数替换了就可以。但事实上,上述的对应关系都是近似、类似,而不是等价。 关键区别在于,socket API都是同步操作,而RDMA API都是异步操作(注意异步和非阻塞是两个不同的概念)

具体而言,ibv_post_send函数返回成功,仅仅意味着成功地向网卡提交了发送请求,并不保证数据真的被发送出去了。如果此时立马对发送数据所在的内存进行写操作,那么发送出去的数据就很可能是不正确的。socket API是同步操作,write函数返回成功,意味着数据已经被写入了内核缓冲区,虽然此时数据未必真的发送了,但应用程序已经可以随意处置发送数据所在的内存。

另一方面,ibv_poll_cq所获取的事件,与epoll_wait获取的事件也是不同的。前者表明,之前提交给网卡的某一发送或接收请求完成了;而后者表示,有新的报文被成功发送或是接收了。这些语义上的变化会影响到上层应用程序的内存使用模式和API调用方式。

除了同步、异步的语义区别外, RDMA编程还有一个关键要素,即所有参与发送、接收的数据,所在的内存必须经过注册

所谓内存注册,简单理解,就是把一段内存的虚拟地址和物理地址间的映射关系绑定好以后注册给网卡硬件。这么做的原因在于,发送请求和接收请求所提交的内存地址,都是虚拟地址。只有完成内存注册,网卡才能把请求中的虚拟地址翻译成物理地址,才能跳过CPU做直接内存访问。内存注册(以及解注册)是一个很慢的操作,实际应用中通常需要构建内存池,通过一次性注册、重复使用来避免频繁调用注册函数。

关于RDMA编程,还有很多细节是普通网络编程所不关心的(比如流控、TCP回退、非中断模式等等),这里就不一一展开介绍了。 总而言之,RDMA编程并不是一件容易的事情。那么,如何才能让开发者快速用上RDMA这样的高性能网络技术呢?

见招拆招,在brpc中使用RDMA

上面说了很多socket API和verbs API的对比,主要还是为了衬托RDMA编程本身的复杂度。事实上,在实际生产环境中,直接调用socket API进行网络传输的业务并不多,绝大多数都通过rpc框架间接使用socket API。一个完整的rpc框架,需要提供一整套网络传输的解决方案,包括数据序列化、错误处理、多线程处理等等。 brpc是百度开源的一个基于C++的rpc框架,其相对于grpc更适用于有高性能需求的场景。在传统TCP传输以外,brpc也提供了RDMA的使用方式,以进一步突破操作系统本身的性能限制。有关具体的实现细节,感兴趣的朋友可以参见github上的源码(https://github.com/apache/incubator-brpc/tree/rdma)。

 

brpc client端使用RDMA

 

brpc server端使用RDMA

上面分别列出了brpc中client和server使用RDMA的方法,即在channel和server创建时,把use_rdma这个option设置为true即可(默认是false,也就是使用TCP)。

是的,只需这两行代码。如果你的应用程序本身基于brpc构建,那么从TCP迁移到RDMA,几分钟就够了。当然,在上述quick start之后,如果有更高级的调优需求,brpc也提供了一些运行时的flag参数可调整,比如内存池大小、qp/cq的大小、轮询替代中断等等。

下面通过echo benchmark说明brpc使用RDMA的性能收益(可以在github代码中的rdma_performance目录下找到该benchmark)。在25G网络的测试环境中,对于2KB以下的消息,使用RDMA后,server端的最大QPS提高了50%以上,200k的QPS下平均时延下降了50%以上。 

 

Echo benchmark下server端最大QPS(25G网络)

 

Echo benchmark在200k QPS下的平均时延(25G网络)

RDMA对echo benchmark的性能收益仅作为参考。实际应用程序的workload与echo相差巨大。对于某些业务,使用RDMA的收益可能会小于上述值,因为网络部分的开销仅占到其业务开销的一部分。但对于另一些业务,由于避免了内核操作对业务逻辑的干扰,使用RDMA的收益甚至会高于上述值。这里举两个应用brpc的例子:

  • 在百度分布式块存储业务中,使用RDMA相比于使用TCP,4KB fio时延测试的平均值下降了约30%(RDMA仅优化了网络IO,存储IO则不受RDMA影响)。

  • 在百度分布式内存KV业务中,使用RDMA相比于使用TCP,200k QPS下,单次查询30key的平均时延下降了89%,而99分位时延下降了96%。

RDMA 需要基础设施支持

RDMA是一种新兴的高性能网络技术,对于通信两端均可控的数据中心内网络IO密集业务,如HPC、机器学习、存储、数据库等,意义重大。我们鼓励相关业务的开发者关注RDMA技术,尝试利用brpc构建自己的应用以平滑迁移至RDMA。 但是,需要指出的是,RDMA当前还无法像TCP那样通用,有一些基础设施层面的限制需要注意:

  • RDMA需要网卡硬件支持。常见的万兆网卡一般不支持这项技术。

  • RDMA的正常使用有赖于物理网络支持。

百度智能云在RDMA基础设施方面积累了深厚的经验。得益于先进的硬件配置与强大的工程技术,用户可以在25G甚至是100G网络环境中,通过物理机或容器,充分获取RDMA技术带来的性能收益,而将与业务无关的复杂的物理网络配置工作(比如无损网络PFC以及显示拥塞通告ECN),交给百度智能云技术支持人员。欢迎对高性能计算、高性能存储等业务有需求的开发者向百度智能云咨询相关情况。 

 

本文作者李兆耕,百度系统部资深研发工程师,长期关注高性能网络技术,负责百度RDMA研发工作,覆盖从上层业务调用到底层硬件支持的全技术栈。