1、RDMA技术全景:从DMA到RDMA的演进、InfiniBand与RoCEv2对比、核心概念(MR、QP、CQ、PD)
各位同学,咱们今天聊聊RDMA的全景。说实话,很多搞网络的人一听到RDMA就觉得头大,觉得这玩意儿太底层、太复杂。其实不然,你把它拆开来看,核心就那么几块东西。我做了这么多年高性能网络,踩过的坑不少,今天就把这些经验掰开了揉碎了讲给你听。
1.1 从DMA到RDMA:一次数据传输的进化史
先说说DMA。DMA这玩意儿,说白了就是让硬件自己搬数据,不用CPU一趟趟地跑腿。你想想看,如果没有DMA,CPU得亲自把数据从网卡拷贝到内存,再从内存拷贝到应用程序的缓冲区。这中间CPU啥也干不了,就光当搬运工了。
DMA出现后,CPU只需要告诉硬件:“嘿,帮我把这块数据搬到那边去”,然后就可以去干别的事了。这效率一下子就上来了。
但DMA有个局限——它只能在单台机器内部搬数据。跨机器的数据传输呢?还是得走传统网络协议栈。TCP/IP那套东西,数据从应用程序到网卡,中间要经过内核缓冲区、Socket层、协议栈……这一圈下来,延迟就上去了。
RDMA就是来解决这个问题的。它把DMA的能力扩展到了网络上。什么意思呢?就是一台机器的应用程序可以直接读写另一台机器的内存,中间不需要经过操作系统。我当年第一次看到RDMA的延迟数据时,说实话,挺震撼的——微秒级的延迟,比传统TCP快了一个数量级。
核心区别一句话总结:DMA是单机内的内存拷贝加速器,RDMA是跨机器的内存直接访问通道。
1.2 InfiniBand vs RoCEv2:两种主流RDMA网络的较量
现在RDMA网络主要有两派:一派是InfiniBand,一派是RoCEv2。我经常被问到:“到底选哪个?”嗯,这个问题没有标准答案,得看你的场景。
| 特性 | InfiniBand | RoCEv2 |
|---|---|---|
| 网络层 | 专有协议(IB) | 基于UDP/IP(以太网) |
| 延迟 | 极低(约1μs) | 较低(约2-3μs) |
| 带宽 | 高(HDR 200Gbps+) | 高(100Gbps+) |
| 拥塞控制 | 原生支持,成熟 | 依赖DCQCN等机制 |
| 成本 | 高(专用硬件) | 相对低(复用以太网) |
| 生态 | 成熟,但封闭 | 开放,兼容性好 |
InfiniBand的优势在于它从头到尾就是为RDMA设计的。它的拥塞控制、流控机制都是原生的,你几乎不用操心。我在一个超算项目中用过InfiniBand,那性能确实稳,延迟抖动很小。
RoCEv2呢,它跑在标准以太网上。这意味着你可以用现有的交换机、网线,成本低很多。但问题也来了——以太网本来不是为RDMA设计的,所以你得额外处理PFC(优先级流控)、ECN(显式拥塞通知)这些机制。我曾经在一个RoCEv2部署中遇到过PFC死锁的问题,排查了好几天才找到根因。
我的建议:如果你预算充足,追求极致性能和稳定性,选InfiniBand。如果你希望复用现有以太网基础设施,或者需要与其他以太网应用共存,选RoCEv2。但记住,RoCEv2的调优工作比InfiniBand多不少。
1.3 核心概念:MR、QP、CQ、PD
好,接下来咱们聊聊RDMA的四个核心概念。这四个东西你搞明白了,RDMA就算入门了。
1.3.1 MR(Memory Region,内存区域)
MR是RDMA操作的基础。你想让网卡直接读写某块内存,那这块内存必须先注册成MR。注册的时候,网卡会拿到这块内存的物理地址和访问权限。
为什么要注册?因为网卡是硬件,它不认识虚拟地址。你得告诉它:“这块内存的物理地址在这里,你可以读也可以写。”注册完成后,网卡就能直接访问这块内存了,不需要CPU参与。
我遇到过一个问题:有人注册了MR之后,程序里又把这块内存释放了,结果网卡还在往里面写数据。嗯,这后果你想想看——内存损坏、程序崩溃,严重的可能把整个系统搞挂。所以,MR的生命周期管理一定要小心。
// 注册MR的示例代码(伪代码)
struct ibv_mr *mr = ibv_reg_mr(pd, buffer, size,
IBV_ACCESS_LOCAL_WRITE |
IBV_ACCESS_REMOTE_WRITE);
if (!mr) {
// 处理错误
fprintf(stderr, "注册MR失败\n");
exit(1);
}
// 使用完毕后,记得注销
ibv_dereg_mr(mr);
1.3.2 QP(Queue Pair,队列对)
QP是RDMA通信的基本单元。每个QP包含两个队列:一个发送队列(SQ)和一个接收队列(RQ)。发送方把请求放到SQ里,接收方从RQ里取数据。
QP的状态机有几种状态:RESET、INIT、RTR(Ready to Receive)、RTS(Ready to Send)。你得按顺序一步步切换状态,跳步了就会出错。我刚开始学的时候,就因为在INIT状态直接跳到RTS,结果通信一直失败,查了半天才发现是状态机没走对。
注意:QP的状态切换必须严格按照顺序来。RESET -> INIT -> RTR -> RTS,一步都不能少。特别是从INIT到RTR,需要先建立连接(交换QP号等信息),否则接收端不知道数据该发给谁。
1.3.3 CQ(Completion Queue,完成队列)
CQ用来通知应用程序:你之前发起的RDMA操作完成了。每次你往QP里提交一个请求,完成后就会有一个完成事件(Completion)被放到CQ里。
你可以用轮询(Polling)或者事件通知(Event Notification)两种方式来获取完成事件。轮询的方式延迟更低,但会占用CPU。事件通知的方式CPU占用少,但延迟会高一些。
我个人习惯用轮询,尤其是在延迟敏感的场景下。但要注意,轮询不能太频繁,否则CPU就全耗在轮询上了。我一般会设置一个超时机制,比如轮询1000次还没结果,就切到事件通知。
// 轮询CQ的示例
struct ibv_wc wc;
int ret = ibv_poll_cq(cq, 1, &wc);
if (ret > 0) {
if (wc.status == IBV_WC_SUCCESS) {
// 操作成功,处理完成事件
printf("操作完成,opcode: %d\n", wc.opcode);
} else {
// 操作失败,打印错误
fprintf(stderr, "操作失败,状态: %d\n", wc.status);
}
}
1.3.4 PD(Protection Domain,保护域)
PD是一个逻辑上的隔离域。它把MR、QP、CQ这些资源绑定在一起。只有属于同一个PD的资源才能互相通信。这就像是一个安全沙箱,防止不同应用程序之间的资源互相干扰。
举个例子:你有一个应用程序,里面有两个不同的模块。你希望模块A和模块B各自使用自己的MR和QP,互不干扰。那你就给模块A创建一个PD,给模块B创建另一个PD。这样,模块A的QP只能访问模块A的MR,模块B的QP只能访问模块B的MR。
小技巧:在多租户场景下,每个租户使用独立的PD,可以有效隔离资源。我曾经在一个云平台上这么做过,效果很好,不同租户之间的RDMA操作完全隔离,不会互相影响。
1.4 避坑指南:我踩过的那些坑
最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路。
- MR注册后不要随意移动内存:MR注册时绑定的是物理地址。如果你在程序里用realloc或者移动了指针,MR就失效了。我曾经因为这个导致数据写到了错误的内存地址,排查了很久才发现是内存移动的问题。
- QP状态机不要跳步:前面说了,QP的状态切换必须按顺序来。我见过有人为了省事,直接从RESET跳到RTS,结果通信一直失败。老老实实按步骤来,别偷懒。
- CQ的深度要设置合理:CQ太浅,会导致完成事件丢失;太深,浪费内存。一般建议设置为QP数量的2-4倍。我习惯设置为QP数量的3倍,既不会丢失事件,也不会浪费太多内存。
- PD不要跨进程共享:PD是进程级别的隔离。如果你在多个进程之间共享PD,可能会导致资源冲突。每个进程使用独立的PD,安全又省心。
好了,这一章的内容就到这里。RDMA的全景图你应该有个大概的了解了。下一章咱们会深入聊聊RDMA的通信模型,包括Send/Recv和Read/Write两种操作模式。到时候见。