乐趣区

关于rust:asyncrdma使高性能网络应用开发更简单

王恒宇,中科院软件所根底软件实验室研究生。次要钻研方向为软硬件交融,对物联网、操作系统、Serverless 等方向感兴趣。DatenLord 社区 async-rdma 我的项目贡献者之一,曾获嵌入式芯片与零碎设计比赛一等奖等多项国家级奖项,参加编写《openEuler 操作系统》一书。

async-rdma 是由 DatenLord 社区发动的 Rust 异步 RDMA 编程库,致力于晋升高性能网络应用的开发效率。在介绍 async-rdma 之前咱们先来理解一下 RDMA 的概念及其应用形式。

RDMA 简介

RDMA(Remote Direct Memory Access)直译为近程间接内存拜访,是一种将一台主机内存中的内容间接传输到另一台主机内存中的技术。计算机中罕用的 DMA 技术防止了 CPU 忙于将数据从 I / O 设施拷贝到内存,RDMA 则将这一能力拓展到了通过网络连接的多台主机之间。

RDMA 在初始化阶段实现后能够由用户态程序通过用户态驱动间接操作 RDMA 网卡收发数据,无需陷入内核,防止了上下文切换,也无需 CPU 进行内存拷贝。凭借绕过内核 (kernel bypass) 和零拷贝 (zero copy) 等个性,RDMA 能以极低的 CPU 占用率实现高吞吐、低时延网络通信。这里简要介绍下文中用到的 RDMA 的外围概念,若想进一步理解 RDMA 的细节能够关注知乎的系列博客,或更进一步翻阅 RDMA 协定标准理解细节。

  • MR(Memory Region):RDMA 应用程序向操作系统申请和注册的一片内存,用于后续 RDMA 操作。MR 不光有地址和长度,还蕴含其余 RDMA 特有的元数据,如权限位,除本地读写权限还有远端读写权限等。
  • 两种操作类型:

1、单端操作(SEND & RECV)

  • 发送、接收数据,一端 Send 前须要对端先 Receive
  • 对端 CPU 须要感知每次收发操作
  • 用于替换元数据等小数据量操作

2、双端操作(READ & WRITE)

  • 间接对指标主机内存进行读写,申请发送前后对端无需进行操作
  • 对端 CPU 不感知单端操作,读写实现后网卡主动响应
  • RDMA 的次要劣势所在,实用于大数据量传输

现有 RDMA 开发方法

咱们应用 Socket API 编写基于 TCP/IP 的网络应用,基于 RDMA 的网络应用开发则应用由 C 语言编写的 Verbs API。Verbs API 提供了对立的编程接口,屏蔽了 RDMA 底层协定简单多样的实现,保障了应用程序的通用性。以下是两个 Verbs API 实例,暂且不关注其各个参数的具体含意,只察看其类型就能够发现其中隐含的内存平安问题。咱们无奈确定裸指针所指数据是否无效,在开发过程中很容易遇到空指针、野指针、内存泄露和屡次开释等常见内存平安问题。

// 注册 MR,应用前需先申请内存
struct ibv_mr *ibv_reg_mr(struct ibv_pd *pd, void *addr, size_t length, enum ibv_access_flags access)
// 数据申请发送接口
int ibv_post_send(struct ibv_qp *qp, struct ibv_send_wr *wr, struct ibv_send_wr **bad_wr)

Rust 提供了以上内存平安问题的高效解决方案,从语言层面帮忙咱们防止了上述问题。DatenLord 社区提供了 Verbs API 的 Rust 封装库:rdma-sys。

为什么要做 async-rdma?

然而,Verbs API 低层级形象导致的开发效率低的问题是单纯应用 Rust 封装所无奈解决的。这就是为什么有了 rdma-sys 库后还要再做一个 async-rdma。具体来说,就 ibv_post_send()接口而言, 参数所用到的数据结构是非常复杂的,间接操作这些参数须要对 RDMA 有很深刻的理解,这会给开发者带来很大认知累赘,并且应用时极易出错。以下是 Verbs 简单数据结构的一个例子供大家领会,其成员变量还蕴含其余让人头大的数据结构。

// Verbs 简单数据结构示意,无需关注细节
struct ibv_send_wr 
{
    uint64_t wr_id;
    struct ibv_send_wr *next;
    struct ibv_sge *sg_list;
    int num_sge;
    enum ibv_wr_opcode opcode;
    enum ibv_send_flags send_flags;
    uint32_t imm_data;/* network byte order */
    union
    {
        struct 
        {
            uint64_t remote_addr;
            uint32_t rkey;
        } rdma;
        struct 
        {
            uint64_t remote_addr;
            uint64_t compare_add;
            uint64_t swap;
            uint32_t rkey;
        } atomic;
        struct 
        {
            struct ibv_ah *ah;
            uint32_t remote_qpn;
            uint32_t remote_qkey;
        } ud;
    } wr;
    uint32_t xrc_remote_srq_num;
};

简化 API

每次发送数据申请都要解决这些简单的数据结构显然是十分苦楚的。因而 async-rdma 的首项工作就是对这些数据结构之间的关系进行整顿,简化操作逻辑。例如库中的 Rdma 数据结构记录了双端建设稳固连贯所需的各种参数,后续通信过程中用到相干数据可间接复用而无需用户填充。通过高层次形象的数据结构封装,使得每次数据操作只须要用户填入必要的信息。

如下代码所示,申请注册 MR 时用户只须要指定寄存的数据类型或大小即可。SEND 操作也只须要指定要发送哪个 MR 的数据,底层 Verbs 接口所用到的简单参数由库填充。当然,为了实现操作的便捷性,库中除了提供类型间转换的逻辑外,还应用了一些默认参数。这些对于这些参数将提供批改接口供进阶开发者配置。


// 在本地申请一片寄存 i32 类型数据的用于 RDMA 操作的内存空间。let mut lmr = rdma.alloc_local_mr(Layout::new::<i32>())?;
// 将上述申请的内存区域中的数据发送到对端。rdma.send(&lmr).await?;

辅助资源管理

只将这些参数的处理过程进行封装也还是不够的,因为这解决不了资源管理艰难的问题。简单的数据结构背地是各种须要治理的资源,如本地注册的内存和从远端申请的内存,以及它们的状态、权限等。

要达到在远端主机 CPU 不感知的状况下对其内存进行读写,须要双端当时和预先替换元数据用以申请和开释远端资源。即不光要治理本地资源,还要与远端主机协同治理远端资源,相比传统的网络应用开发复杂性更高。async-rdma 提供了 Verbs API 之外面向应用层语义的下层接口,使得建设连贯、远端资源协同治理等操作变得简略。

在建设连贯时,后盾会主动运行 Agent 服务线程,其接管到下层接口收回的申请后,通过与远端 Agent 交互进行元数据交换,实现建设连贯、远端资源申请开释等操作。借助于 Rust 的生命周期机制,当本地或远端资源被 Drop 时,Agent 会清理本地资源或主动向远端发送资源开释的申请,开发者无需感知。以下代码展现了建设连贯、申请本地和远端内存,通过 RDMA WRITE 操作通信和最终资源开释的过程。

/// 模仿客户端链接服务端
async fn client(addr: SocketAddrV4) -> io::Result<()> {
    // 通过 TCP 连贯远端,与远端替换用于建设稳固连贯的元数据并启动 Agent
    // 连贯建设后,后续元数据交换通过 RDMA SEND RECV 操作进行
    let rdma = Rdma::connect(addr, 1, 1, 512).await?;
    // 申请一块本地用于 RDMA 操作的内存(MR)
    let mut lmr = rdma.alloc_local_mr(Layout::new::<i32>())?;
    // 申请一块远端的内存(MR)
    let mut rmr = rdma.request_remote_mr(Layout::new::<i32>()).await?;
    // 将要发送的数据填入本地 MR
    unsafe {*(lmr.as_ptr() as *mut i32) = 666 };
    // 通过 RDMA WRITE 操作将本地内存所存数据写入远端内存,远端不感知也无需解决本次申请
    rdma.write(&lmr, &mut rmr).await?;
    // rmr 会在生命周期完结时向远端发送开释申请, lmr 开释逻辑只需在本地执行。Ok(())
}

异步 I /O

如本我的项目名中的 async,本库提供的波及 I / O 的接口都是异步的 。应用 Verbs API 时,每一次 RDMA SEND、WRITE 等申请后,都须要应用 poll() 办法轮询接管对端应答,能力确定申请是否正确实现。首先忙等必定是低效的,当然也能够由开发者实现异步逻辑,但轮询到的响应是后面哪次申请的也不确定,须要额定的逻辑去判断。

为了防止所有开发者都实现一遍上述逻辑,咱们借助 tokio 实现了异步的 RDMA 申请解决逻辑。以后 task 收回 RDMA 申请后即让出,当对端响应事件返回本机后,由后盾服务程序唤醒与响应信息对应的 task。这防止了忙等开销,也无需开发者解决申请与响应间的对应关系,具体实现可见仓库中的文档。当然,事件驱动的异步逻辑在进步机器利用效率的同时可能会带来肯定时延。对低时延有极致要求的场景能够就义效率换取低时延,有相干需要的敌人能够参加奉献忙等接口。

结语

总结一下,async-rdma 次要提供了以下接口和性能:

  • 用于主机之间建设 RDMA 稳固连贯的异步接口,如 connect()。
  • 高形象层级的异步 RDMA 数据收发操作接口,如 write()。
  • 高形象层级的异步远端 RDMA 内存治理接口和本地 RDMA 内存治理接口,如 alloc_remote_mr()。
  • 用于反对上述异步接口,以及辅助治理远端内存的后盾服务,如 Agent。

除此之外,async-rdma 还在不断完善谬误的主动解决和提供尽可能具体的谬误提示信息。Verbs API 在进行 MR 的治理或数据传输操作时遇到谬误只返回零碎错误代码,蕴含的信息很少。有的错误处理逻辑会呈现下层错误代码笼罩底层错误代码,返回对开发者具备误导性的错误代码。一些谬误在本人的应用程序中很难发现,要到用户态驱动甚至内核代码中去找。这显然是极为低效和不敌对的,也是 async-rdma 想要解决的问题。

以后 async-rdma 尚处于晚期开发阶段,提供了罕用性能接口,但我的项目在稳定性等方面仍有待增强。DatenLord 的指标是将其打造成产品级软件,使之可能广泛应用于生产环境中。

咱们近期的次要开发指标如下:

  • 进步内存管理效率,革新通用内存分配器 jemalloc 使其可能调配 MR,缩小 MR 注册等慢速门路操作数量。
  • 欠缺错误处理,使后盾服务尝试主动处理错误,并在尝试失败后返回具体错误信息和操作倡议。
  • 欠缺测试,包含单元测试,压力测试,性能测试等,以发现潜在问题和辅助性能调优。
  • 提供更多配置接口,以后一些配置参数为默认值且尚未给出用户自定义接口,不足灵活性。

其中指标 3、4 不须要对 RDMA 或者本我的项目有十分深刻的理解,只需理解对外裸露的下层接口,或相熟 Rust 语言即可。因而非常适合想要入门的敌人参加奉献,社区也会提供疏导和帮忙。咱们为每一个接口都提供了阐明文档和应用样例,同时提供了在虚拟机中搭建 RDMA 利用运行环境的配置指南(无需非凡硬件反对),此前未接触过 RDMA 的敌人也能够尝试。欢送大家提出问题和倡议,欢送对本我的项目感兴趣的敌人退出 DatenLord 社区一起参加奉献。

DatenLord 社区的主我的项目为高性能分布式存储系统:datenlord,async-rdma 是其子我的项目之一,更多我的项目见社区 github 仓库。

相干链接

  • async-rdma
  • Github:https://github.com/datenlord/…
  • Crate:https://crates.io/crates/asyn…
  • Docs:https://docs.rs/async-rdma/0….
  • rdma-sys:https://github.com/datenlord/…
  • DatenLord:https://github.com/datenlord
  • RDMA 专栏文章: https://zhuanlan.zhihu.com/p/…
  • RDMA 协定标准:https://cw.infinibandta.org/d…
退出移动版