2. RDMA核心概念:InfiniBand与RoCE、IB协议栈、QP、CQ、MR

好,咱们进入正题。这一章要聊的,是RDMA世界里最核心的几个概念。说实话,很多新手一开始就被这些术语搞懵了——什么InfiniBand、RoCE、QP、CQ、MR……我当年刚接触时也头大。但你别急,这些东西说白了就是一套“高速搬运数据”的规矩和工具。咱们一个一个拆开讲。

2.1 InfiniBand与RoCE:两种主流物理链路

RDMA最初是InfiniBand(IB)的专属技术。IB是一套完整的网络体系,从物理层到传输层全是自己定义的。它的网卡、交换机、线缆都是专用的,性能极好,延迟能低到1微秒以内。我在做高性能计算集群时,用的就是IB——那会儿觉得这玩意儿真贵,但确实稳。

后来,大家发现以太网太普及了,能不能在以太网上跑RDMA?于是就有了RoCE(RDMA over Converged Ethernet)。RoCE分两代:RoCE v1和RoCE v2。v1只能在同一个二层网络里用,v2可以跨三层路由,更灵活。

我个人习惯是:如果预算充足、对延迟要求极其苛刻(比如金融交易、超算),选InfiniBand。如果追求性价比、想复用现有以太网基础设施,选RoCE v2。嗯,这里有个坑——RoCE对网络丢包非常敏感,一旦丢包,性能断崖式下降。我曾经在一个项目中因为交换机缓存配置不当,RoCE性能直接腰斩,排查了两天才找到原因。

特性 InfiniBand RoCE v2
物理层 专用IB线缆/交换机 标准以太网
延迟 ~1μs ~3-5μs
成本 中低
丢包容忍度 高(有流控) 低(需PFC等)
我的建议:如果刚开始学习RDMA,用RoCE v2就够了。买两张支持RoCE的网卡(比如Mellanox ConnectX系列),两台机器直连,成本低,调试也方便。

2.2 IB协议栈:分层结构

IB协议栈分好几层,但咱们不用背。你只需要知道:上层是传输层(负责可靠/不可靠传输),下层是网络层和链路层。RDMA的“零拷贝”能力,主要靠传输层和硬件配合实现。

我画个简化的图给你看:

+-------------------+
|  上层应用 (verbs) |
+-------------------+
|  传输层 (RC/UC/UD)|
+-------------------+
|  网络层 (GRH)     |
+-------------------+
|  链路层 (L2)      |
+-------------------+
|  物理层 (PHY)     |
+-------------------+

传输层有几种服务类型:RC(可靠连接)、UC(不可靠连接)、UD(不可靠数据报)。RC用得最多,因为它保证数据不丢不乱序。我刚开始做RDMA编程时,图省事用了UD,结果数据偶尔丢失,排查起来特别痛苦。后来老老实实换回RC,世界清净了。

2.3 QP(队列对):RDMA通信的“管道”

QP是RDMA通信的核心。它由两个队列组成:发送队列(SQ)和接收队列(RQ)。你想想看,就像一根水管的两头——一头往里塞数据(发送),另一头往外拿数据(接收)。

每个QP都有一个唯一的编号(QP Number)。通信双方需要知道对方的QP Number才能建立连接。这个过程叫“QP连接建立”,类似于TCP的三次握手,但更轻量。

我在项目中遇到过一个问题:QP数量太多,导致内存占用过高。每个QP都要占用一定的内存资源(比如WQE队列、CQE队列)。后来我改用“QP池”的方式,按需分配,才解决了问题。

关键点:QP是RDMA通信的最小单元。一个应用可以创建多个QP,每个QP对应一条独立的通信链路。

2.4 CQ(完成队列):通知你“活干完了”

CQ是用来存放“完成通知”的。当你通过QP发送或接收数据后,硬件会把完成状态(成功/失败、字节数等)写入CQ。你的程序需要轮询或等待CQ事件,才能知道操作是否完成。

说白了,CQ就像快递柜的取件通知——你寄了包裹(发送数据),快递柜会发个码给你(CQ条目),你凭码去取回执(确认送达)。

我刚开始写RDMA程序时,经常忘记轮询CQ,导致发送了一大堆数据,却不知道哪些已经完成。结果就是内存泄漏、程序崩溃。嗯,这里要注意:CQ的轮询频率要适中,太频繁浪费CPU,太慢则延迟高。

// 伪代码:轮询CQ
struct ibv_wc wc;
int ne = ibv_poll_cq(cq, 1, &wc);
if (ne > 0) {
    if (wc.status == IBV_WC_SUCCESS) {
        // 操作成功
    } else {
        // 操作失败,处理错误
    }
}

2.5 MR(内存注册):让网卡直接访问你的内存

MR是RDMA实现“零拷贝”的关键。普通网络通信中,数据要从用户态拷贝到内核态,再拷贝到网卡。RDMA绕过了内核,让网卡直接读写用户态内存。但前提是——这块内存必须先“注册”到网卡。

注册MR时,网卡会记录这块内存的物理地址和长度,并分配一个L_Key(本地键)和R_Key(远程键)。远程访问时,对方需要提供R_Key才能读写你的内存。这就像你给朋友一把钥匙(R_Key),他才能进你家拿东西。

我曾经犯过一个低级错误:注册MR时忘了设置权限(读/写/本地/远程),结果远程端一直报权限错误。排查了半天才发现是权限位没设对。所以,注册MR时一定要仔细检查权限标志。

避坑指南:MR注册后,这块内存就不能被换出(page out)了。如果注册了太多MR,会占用大量物理内存,甚至导致系统OOM。我曾经在一个长期运行的服务中,因为频繁注册/注销MR,导致内存碎片化严重。后来改用“MR池”复用机制,才解决了问题。
// 注册MR的示例(libibverbs)
struct ibv_mr *mr = ibv_reg_mr(
    pd,           // 保护域
    buf,          // 内存缓冲区
    size,         // 大小
    IBV_ACCESS_LOCAL_WRITE |   // 本地写权限
    IBV_ACCESS_REMOTE_WRITE    // 远程写权限
);
if (!mr) {
    // 注册失败,处理错误
}

小结

这一章咱们聊了RDMA的五个核心概念:InfiniBand和RoCE是两种物理链路选择;IB协议栈是分层架构;QP是通信管道;CQ是完成通知;MR是内存注册。这些概念是RDMA编程的基石,你理解透了,后面写代码就会顺手很多。

下一章,咱们会动手写第一个RDMA程序——实现两台机器之间的内存读写。到时候你会看到,这些概念是如何在代码中落地的。