关于rust:DatenLord|如何用Rust实现RDMA

32次阅读

共计 5285 个字符,预计需要花费 14 分钟才能阅读完成。

作者:王璞
前期编辑:张汉东
首发于《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 封装,有两个层面的问题须要思考:

  1. 如何做到合乎 Rust 的标准和常规;
  2. 如何实现 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 编程,同时咱们欢送有感兴趣的敌人一起参加。

正文完
 0