作者:王璞
前期编辑:张汉东
首发于《Rust中文精选》
RDMA是罕用于高性能计算(HPC)畛域的高速网络,在存储网络等专用场景也有宽泛的用处。RDMA最大的特点是通过软硬件配合,在网络传输数据的时候,齐全不须要CPU/内核参加,从而实现高性能的传输网络。最早RDMA要求应用InfiniBand (IB)网络,采纳专门的IB网卡和IB交换机。当初RDMA也能够采纳以太网交换机,然而还须要专用的IB网卡。尽管也有基于以太网卡用软件实现RDMA的计划,然而这种计划没有性能劣势。
RDMA在理论应用的时候,须要采纳特定的接口来编程,而且因为RDMA在传输数据的过程中,CPU/内核不参加,因而很多底层的工作须要在RDMA编程的时候自行实现。比方RDMA传输时波及的各种内存管理工作,都要开发者调用RDMA的接口来实现,甚至自行实现,而不像在socket编程的时候,有内核帮忙做各种缓存等等。也正是因为RDMA编程的复杂度很高,再加上先前RDMA硬件价格昂扬,使得RDMA不像TCP/IP失去宽泛应用。
本文次要介绍咱们用Rust对RDMA的C接口封装时碰到的各种问题,并探讨下如何用Rust对RDMA实现safe封装。上面首先简略介绍RDMA的根本编程形式,而后介绍下采纳Rust对RDMA的C接口封装时碰到的各种技术问题,最初介绍下后续工作。咱们用Rust实现的RDMA封装曾经开源,包含rdma-sys和async-rdma,前者是对RDMA接口的unsafe封装,后者是safe封装(尚未实现)。
RDMA编程理念
先首先简要介绍下RDMA编程,因为本文重点不是如何用RDMA编程,所以次要介绍下RDMA的编程理念。RDMA的全称是Remote Direct Memory Access,从字面意思能够看出,RDMA要实现间接拜访近程内存,RDMA的很多操作就是对于如何在本地节点和近程节点之间实现内存拜访。
RDMA的数据操作分为“单边”和“双边”,双边为send/receive,单边是read/write,实质都是在本地和近程节点之间共享内存。对于双边来说,须要单方节点的CPU独特参加,而单边则仅仅须要一方CPU参加即可,对于另一方的CPU是齐全通明的,不会触发中断。根据上述解释,大家能够看出“单边”传输才是被用来传输大量数据的次要办法。然而“单边”传输也面临这下列挑战:
因为RDMA在数据传输过程中不须要内核参加,所以内核也无奈帮忙RDMA缓存数据,因而RDMA要求在写入数据的时候,数据的大小不能超过接管方筹备好的共享内存大小,否则出错。所以发送方和接管方在写数据前必须约定好每次写数据的大小。
此外,因为RDMA在数据传输过程中不须要内核参加,因而有可能内核会把本地节点要通过RDMA共享给近程节点的内存给替换进来,所以RDMA必须要跟内核申请把共享的内存空间常驻内存,这样保障近程节点通过RDMA平安拜访本地节点的共享内存。
再者,尽管RDMA须要把本地节点跟近程节点共享的内存空间注册到内核,以防内核把共享内存空间替换进来,然而内核并不保障该共享内存的拜访平安。即本地节点的程序在更新共享内存数据时,有可能近程节点正在拜访该共享内存,导致近程节点读到不统一的数据;反之亦然,近程节点在写入共享内存时,有可能本地节点的程序也正在读写该共享内存,导致数据抵触或不统一。应用RDMA编程的开发者必须自行保障共享内存的数据一致性,这也是RDMA编程最简单的关键点。
总之,RDMA在数据传输过程中绕开了内核,极大晋升性能的同时,也带来很多复杂度,特地是对于内存治理的问题,都须要开发者自行解决。
RDMA的unsafe封装
RDMA的编程接口次要是C实现的rdma-core,最开始咱们感觉用Rust的bingen能够很容易生成对rdma-core的Rust封装,但理论中却碰到了很多问题。
首先,rdma-core有大量的接口函数是inline形式定义,至多上百个inline函数接口,bindgen在生成Rust封装时间接疏忽所有的inline函数,导致咱们必须手动实现。Rust社区有另外几个开源我的项目也实现了对rdma-core的Rust封装,然而都没有很好解决inline函数的问题。此外,咱们在自行实现rdma-core的inline函数Rust封装时,放弃了原有的函数名和参数名不变。
其次,rdma-core有不少宏定义,bindgen在生成Rust封装时也间接疏忽所有的宏定义,于是咱们也必须手动实现一些要害的宏定义,特地是要手动实现rdma-core里用宏定义实现的接口函数和一些要害常量。
再有,rdma-core有很多数据结构的定义用到了union,然而bindgen对C的union解决得不好,并不是间接转换成Rust里的union。更重大的是rdma-core的数据结构里还用到匿名union,如下所示:
struct ibv_wc { ... union { __be32 imm_data; uint32_t invalidated_rkey; }; ...};
因为Rust不反对匿名union,针对这些rdma-core的匿名union,bindgen在生成的Rust binding里会主动生成union类型的名字,然而bindgen主动生成的名字对开发者很不敌对,诸如ibv_flow_spec__bindgen_ty_1__bindgen_ty_1这种名字,所以咱们都是手动从新定义匿名union,如下所示:
#[repr(C)]pub union imm_data_invalidated_rkey_union_t { pub imm_data: __be32, pub invalidated_rkey: u32,}#[repr(C)]pub struct ibv_wc { ... pub imm_data_invalidated_rkey_union: imm_data_invalidated_rkey_union_t, ...}
再次,rdma-core里援用了很多C的数据结构,诸如pthread_mutex_t和sockaddr_in之类,这些数据结构应该应用Rust libc里定义好的,而不是由bindgen再从新定义一遍。所以咱们须要配置bindgen不反复生成libc里曾经定义好的数据结构的Rust binding。
简略一句话总结下,bindgen对生成rdma-core的unsafe封装只能起到一半作用,剩下很多工作还须要手动实现,十分细碎。不过益处是,RDMA接口曾经稳固,此类工作只须要一次操作即可,后续简直不会须要大量更新。
RDMA的safe封装
对于RDMA的safe封装,有两个层面的问题须要思考:
- 如何做到合乎Rust的标准和常规;
- 如何实现RDMA操作的内存平安。
首先,对于RDMA的各种数据结构类型,怎样才能封装成对Rust敌对的类型。rdma-core里充斥着大量的指针,绝大多数指针被bindgen定义为mut类型,少部分定义为const类型。在Rust里,这些裸指针类型不是Sync也不是Send,因而不能多线程拜访。如果把这些裸指针转化为援用,又波及到生命周期问题,而这些指针指向的数据结构都是rdma-core生成的,大都须要显式的开释,比方struct ibv_wq这个数据结构由ibv_create_wq()函数创立,并由ibv_destroy_wq()函数开释:
struct ibv_wq *ibv_create_wq(...);int ibv_destroy_wq(struct ibv_wq *wq);
然而用Rust开发RDMA利用的时候,Rust代码并不间接治理struct ibv_wq这个数据结构的生命周期。进一步,在Rust代码中并不会间接批改rdma-core创立的各种数据结构,Rust代码都是通过调用rdma-core的接口函数来操作各种RDMA的数据结构/指针。所以对Rust代码来说,rdma-core生成的各种数据结构的指针,实质是一个句柄/handler,这个handler的类型是不是裸指针类型并不重要。于是,为了在Rust代码中便于多线程拜访,咱们把rdma-core返回的裸指针类型都转换成usize类型,当须要调用rdma-core的接口函数时,再从usize转换成相应的裸指针类型。这么做听下来很hack,但背地的起因还是很不言而喻的。进一步,对于在rdma-core中须要手动开释的资源,能够通过实现Rust的Drop trait,在drop()函数中调用rdma-core相应的接口实现资源主动开释。
其次,对于RDMA的内存平安问题,这部分工作尚未实现。目前RDMA的共享内存拜访平安问题在学术界也是个热门研究课题,并没有完满的解决方案。实质上讲,RDMA的共享内存拜访平安问题是因为为了实现高性能网络传输、绕过内核做内存共享带来的,内核在内存治理方面做了大量的工作,RDMA的数据传输绕过内核,因而RDMA无奈利用内核的内存管理机制保障内存平安。如果要把内核在内存治理方面的工作都搬到用户态来实现RDMA共享内存拜访平安,这么做的话一方面复杂度太高,另一方面也不肯定有很好的性能。
在理论应用中,人们会对RDMA的应用形式进行规约,比方不容许近程节点写本地节点的共享内存,只容许近程节点读。但即使是只容许近程读取,也有可能有数据不统一的问题。比方近程节点读取了共享内存的前半段数据,本地节点开始更新共享内存。假设本地节点更新的数据很少而近程节点读取的数据很多,因而本地节点更新的速度比近程节点读取的速度快,导致有可能本地节点在近程节点读后半段数据前更新结束,这样近程节点读取的是不统一的数据,前半段数据不包含更新数据然而后半段包含更新数据。近程节点读到的这个不统一的数据,既不是先前实在存在的某个版本的数据,也不是全新版本的数据,毁坏了数据一致性的保障。
针对RDMA内存平安问题,一个常见的解决方案是采纳无锁(Lock-free)数据结构。无锁数据结构实质上就是解决并发拜访下保障内存平安问题,当多个线程并发批改时,无锁数据结构保障后果的一致性。针对下面提到的近程读、本地写的形式,能够采纳Seqlock来实现。即每块RDMA的共享内存空间关联一个序列号(sequence number),本地节点每次批改共享内存前就把序列号加一,近程节点在读取开始和完结后查看序列号是否有变动,没有变动阐明读取过程中共享内存没有被批改,序列号有变动阐明读取过程中共享内存被批改,读到了有可能不统一的数据,则近程节点从新读取共享内存。
如果要放宽对RDMA的应用规约,即近程节点和本地节点都能够读写共享内存的场景,那么就须要采纳更加简单的算法或无锁数据结构,诸如Copy-on-Write和Read-Copy-Update等。内核中大量应用Copy-on-Write和Read-Copy-Update这两种技术来实现高效内存治理。这方面的工作有不少技术难度。
后续工作
下一步在实现对RDMA的safe封装之后,咱们布局用Rust实现对RDMA接口函数的异步调用。因为RDMA都是IO操作,非常适合异步形式来实现。
对RDMA接口函数的异步解决,最次要的工作是对于RDMA的实现队列的音讯解决。RDMA采纳了多个工作队列,包含接管队列(RQ),发送队列(SQ)以及实现队列(CQ),这些队列个别是RDMA的硬件来实现。其中发送队列和接管队列的性能很好了解,如字面意思,别离是寄存待发送和待接管的音讯,音讯是指向内存中的一块区域,在发送时该内存区域蕴含要发送的数据,在接管时该内存区域用于寄存接收数据。在发送和接管实现后,RDMA会在实现队列里放入实现音讯,用于批示相应的发送音讯或接管音讯是否胜利。用户态RDMA程序能够定期不定期查问实现队列里的实现音讯,也能够通过中断的形式在CPU收到中断后由内核告诉利用程序处理。
异步IO实质上都是利用Linux的epoll机制,由内核来告诉用户态程序某个IO曾经就绪。对RDMA操作的异步解决,办法也一样。RDMA是通过创立设施文件来实现用户态RDMA程序跟内核里的RDMA模块交互。在装置RDMA设施和驱动后,RDMA会创立一个或多个字符设施文件,/dev/infiniband/uverbsN,N从0开始,有几个RDMA设施就有几个uverbsN设施文件。如果只有一个那就是/dev/infiniband/uverbs0。用户态RDMA程序要实现针对RDMA实现队列的异步音讯解决,就是采纳Linux提供的epoll机制,对RDMA的uverbsN设施文件进行异步查问,在实现队列有新音讯时告诉用户态RDMA程序来解决音讯。
对于RDMA的封装,这块工作咱们还没有实现,咱们打算把RDMA的safe封装以及对RDMA的共享内存治理都实现,这样能力不便地应用Rust进行RDMA编程,同时咱们欢送有感兴趣的敌人一起参加。