简介:这是一份面向网络工程师、高性能计算与分布式存储开发者的RDMA技术调研PDF文档。文档从传统网络协议栈的痛点切入,系统梳理了RDMA零拷贝、内核旁路、CPU卸载三大核心优势,并对比Infiniband、RoCE、iWARP三种协议的演进脉络与适用场景,帮助读者快速建立技术选型框架。此外,文档还详细解释了WQ、QP、CQ等关键术语,结合SEND/RECV、READ/WRITE等通信操作说明了一次RDMA传输的软硬件交互流程,并介绍了verbs与rdma-core等编程接口,适合作为入门学习与内部培训资料。资源为1个PDF文件,大小约1.01MB,内容精炼、图文结合,方便离线阅读与团队分享。目前已有527人学习,对于希望理解RDMA基本原理与落地路径的读者,这是一份值得收藏的调研笔记。
1. RDMA 为什么能让 CPU 退出数据通路
一条经常被忽略的事实:RDMA 的快不来自网卡速率本身,而来自数据路径变短。同样是 100GbE,传统 TCP 路径上的数据先由 DMA 进内核缓冲区,再由 CPU 参与协议栈封包、校验、中断,最后从内核拷回用户缓冲区;RDMA 网卡直接按应用发布的工作请求,从用户缓冲区 DMA 数据,经网络直接 DMA 到对端用户预注册的缓冲区,CPU 只剩控制面。远程直接内存访问这个名字准确概括了这件事:读写远程主机内存而不中断远程 CPU。低延迟、高带宽、低 CPU 占用是它的三个核心指标。需要确定性延迟的金融与 HPC,以及把存储放在以太网上的云计算和 AI 集群,都在用 RDMA。下面按协议选型、队列模型、verbs 编程和排错四条线展开这份调研。
2. 三种RDMA协议选型:IB、RoCE与iWARP的取舍
2.1 一个名字、三套协议栈
RDMA 描述的是能力,不是某一个具体协议。InfiniBand(IB)从链路层开始就是为 RDMA 设计的全新网络,网卡和交换机都要专用;RoCE 把 IB 的报文头套在以太网帧里,让 RDMA 可以跑在已有以太网交换机上;iWARP 则把 RDMA 语义映射到 TCP 之上。三者对上层都暴露同一套 verbs 接口,所以应用层创建 QP、下发 WR 的代码可以一致,差异几乎全部集中在链路层和运维上。
2.2 IB、RoCE、iWARP 的横向对比
| 对比维度 | InfiniBand | RoCE | iWARP |
|---|---|---|---|
| 链路层 | 专属 IB 链路 | 以太网 | 以太网 |
| 报文承载 | 完整 IB 协议栈 | 以太网/IP/UDP 上承载 IB 报文头 | TCP/IP 上承载 RDMA 语义 |
| 交换机 | 必须 IB 专用交换机 | 支持无损特性的一般交换机 | 普通以太网交换机 |
| 丢包处理 | 下层保证有序不丢 | 需 PFC/DCQCN 提供无损保障 | TCP 重传兜底 |
| CPU 开销 | 最低 | 低 | 中,依赖 TCP 卸载能力 |
| 典型场景 | HPC、GPU 集群 | 数据中心存储、云计算 | 跨 TCP 网络的老旧兼容场景 |
上表的关键不在选哪个更好,而在背后的约束不同。IB 的隔离性最好,但整个网络要单独建设;RoCE 复用现有运维体系,但要求交换机支持无损队列;iWARP 看似兼容最省事,但 TCP 层重传和窗口管理一旦交给 CPU 处理,就失去 RDMA 的大部分优势,现代数据中心已经很少把它作为主力方案。如果选 iWARP 且网卡不支持 TCP 卸载,驱动栈会在软件里模拟 RDMA 语义,延迟和吞吐都会明显劣化。
2.3 为什么 RoCE 需要一个“不像以太网”的以太网
RoCE v2 的报文结构是以太网头、IP、UDP,后面再挂 IB 的基础传输头。之所以走 UDP,是为了让现有三层交换机也能路由 RoCE 报文而不需要操作系统参与,按照常见约定使用 UDP 4791 端口。麻烦也出在这:IB 原本假设链路有序不丢,而普通以太网没有这个保证,UDP 也没有重传能力,RoCE 接收端遇到乱序或丢帧就直接丢弃数据。
所以 RoCE 实际部署时要把以太网改造成“看起来像 IB”的无损网络,典型手段是交换机开启 PFC(优先级流控),再配合 ECN 和显式拥塞通知做闭环,比如 DCQCN 拥塞控制方案。PFC 会在某一优先级队列缓存耗尽时暂停上游端口,代价是同一队列上的 TCP 流也被堵住;因此启用 PFC 的队列最好只承载 RoCE 业务。这是 RoCE 选型时最容易低估的工程量,普通千兆交换机如果没有无损功能,跑起来性能会很差。
2.4 生产环境的选型判断
预算充足且准备从头建一张高性能网络时,IB 最省心,GPU 直连和并行文件系统场景里生态也最成熟。在已有万兆或百G 以太网机房里补一个存储池,RoCE 改造网络的成本远低于换交换机。iWARP 几乎只出现在需要穿越传统 TCP 网络且不能改链路的个别情况。网卡层面,Mellanox ConnectX 系列是 RDMA 生态里最常见的硬件选择,采购时注意确认网卡固件里开启了 RDMA 功能,以及驱动版本与内核匹配。
3. QP、CQ与内存注册:RDMA通信的基本链路
3.1 工作队列模型:SQ、RQ 与 QP
RDMA 应用不下“发送数据”这样的指令,而是向工作队列下发一条工作请求(WR)。发送队列 SQ 里放的是“把这段内存发给对端”这类请求,接收队列 RQ 里放的是“我准备好了一块缓冲区”。SQ 和 RQ 合起来称为队列对 QP,通信双方每建立一条逻辑连接就需要一个 QP。任务完成后,硬件往完成队列 CQ 里写一个完成事件,应用轮询 CQ 就知道这次 WR 结束了。QP、CQ 都是网卡硬件对象,用户态只是通过 verbs 库管理它们。
3.2 一次 SEND-RECV 的事件顺序
把队列放一起看一次典型的 SEND-RECV 交互,其顺序不是直觉里的“先发再收”,而是接收方先准备好:
| 参与者 | 动作 | 说明 |
|---|---|---|
| 接收端应用 | 向 RQ 发布 RECV | 把用户态缓冲区提前交给网卡 |
| 发送端应用 | 向 SQ 发布 SEND | 指定发送缓冲区地址和长度 |
| 发送端网卡 | DMA 数据并组装报文 | 不经过内核协议栈,直接上链路 |
| 接收端网卡 | DMA 写入用户缓冲区并回 ACK | 接收端 CPU 完全不碰数据 |
| 双方 CQ | 分别产生完成事件 | 发送端在收到 ACK 后得到完成通知 |
发送端应用在收到完成通知之前不能改动缓冲区数据;接收端收到完成事件才知道缓冲区里已经有数据,但还要靠自己的应用逻辑去判断数据完整性。整个数据面没有任何一次上下文切换,这就是“内核旁路”的落地形态。也正因如此,QP 的状态管理工作比 socket 复杂得多:连接建好之后,双方还要维护发送窗口、包序号和重传状态。
3.3 三个容易卡住理解的设计
第一个是“接收端为什么要提前发布 RECV”。传统 socket 有内核接收缓冲区兜底,数据先进内核再通知应用;RDMA 没有内核缓冲区这层,应用如果不提前把内存区域注册并挂到 RQ 上,对端发来的数据在网卡上没有卸载目标,只能丢弃或触发 RNR(Receiver Not Ready)错误。
第二个是“为什么要轮询 CQ 而不是靠中断”。高吞吐下中断带来的上下文切换开销极高,轮询 CQ 使延迟更稳定,代价是被轮询的那个核始终占着。RDMA 说的 CPU 占用小,是指数据面不耗 CPU,但轮询本身仍然要一个线程空转,这个区别在资源规划时经常被忽略。
第三个是“零拷贝到底免掉了哪一次拷贝”。TCP 路径上至少有内核缓冲区和用户缓冲区之间的两次拷贝,RDMA 只有用户缓冲区到网卡、网卡到对端用户缓冲区各一次 DMA,数据不落在任何中间缓冲里。代价是注册内存区域时网卡要求锁定物理页并建立地址映射,注册本身有成本,所以高频发送的小消息通常需要预注册一块内存池循环复用,而不是每条消息现注册现用。
3.4 内存注册、Protection Domain 与权限
用户空间传给网卡的地址是虚拟地址,网卡做 DMA 时必须知道对应的物理地址,因此需要注册内存区域(MR)。注册后返回两类 key:lkey 供本地 SGE 使用,rkey 连同远程虚拟地址一起告诉对端,对端才能发起 READ 或 WRITE。RDMA 不加锁、不通知对端进程,rkey 一旦泄漏,任何知道该地址的远端 QP 都有权写这块内存,这就是 RDMA 的权限边界所在。
多个资源要放进一个保护域(PD)里管理。QP 只能访问同一 PD 下的 MR,两个互不相关的应用用不同 PD 就可以隔离资源;Mellanox 网卡也支持在 MR 之上再开内存窗口进行更细粒度的权限控制,适合把大块注册内存划成多个逻辑窗口分别授权。写生产代码时,一个简单原则是:注册时只给当前业务真正需要的访问权限,不要图省事把所有权限一次打开。
4. SEND/RECV与WRITE/READ:通信操作、传输模式与verbs编程
4.1 四种通信操作的语义差异
SEND/RECV 成对出现,接收端指定接收缓冲区,数据落在哪完全由接收端控制,语义接近“把一条消息传给对方”。RDMA WRITE 则是由发送端直接写对端内存的某个 remote_addr,数据到达时接收端无感知;RDMA READ 是发送端从对端内存读数据回来,过程同样对远端进程透明。第四种 ATOMIC 只支持少量原子操作,比如原子加和比较交换,用于分布式锁和计数器。
在实际工程里,大规模数据多用 WRITE/READ 传输,因为省去了接收端 post 缓冲区的开销,也不需要接收端进程在数据到达时做事件处理;但“对端无感知”也带来同步问题。常见做法是用 RDMA WRITE 传完数据后,再补一个带即时数的 SEND 或一个原子操作作为“门铃”,告诉对端数据已就位,接收端再轮询完成队列去消费。这种方式比每次数据都走 SEND/RECV 吞吐更高,但代码复杂度也上一个台阶。
4.2 RC、UC、UD 与 DCT 的取舍
传输模式的选择直接决定支持哪些操作,这张表值得收藏:
| 传输模式 | 可靠性 | 单 QP 可通信对端 | 支持的操作 |
|---|---|---|---|
| RC 可靠连接 | 可靠、保序、重传 | 只能与一个 QP 通信 | SEND、WRITE、READ、ATOMIC |
| UC 不可靠连接 | 不重传、保序 | 只能与一个 QP 通信 | SEND、WRITE |
| UD 不可靠数据报 | 不重传、不保序 | 可与任意 QP 通信 | 仅 SEND,负载受 MTU 限制 |
| DCT 动态连接 | 可靠语义 | 一个 QP 与多个远端 QP 通信 | 近似 RC,降低连接数 |
RC 最容易写对,但每对连接都要建独立 QP,大规模场景下内存和上下文开销很高。UD 不建连接,任意 QP 之间都能互发小消息,适合控制信令,但单包长度受 MTU 限制,也没有动词层的重传。DCT(动态连接传输)把 RC 的可靠性扩展成动态连接,解决的是大量短连接场景下 QP 太多的问题,代价是支持面不如 RC 老,遇到老版本固件或驱动要先确认兼容性。
4.3 一个最小可用的 verbs 调用链
用 rdma-core 写程序,最核心的链路是打开设备、分配 PD、注册 MR、创建 CQ、创建 QP、下发 WR、轮询 CQ。下面的骨架省略了双端握手细节,只展示调用顺序:
#include <infiniband/verbs.h> /* 打开设备:RDMA 设备通常叫 mlx5_0/mlx5_1,由驱动注册 */ struct ibv_device **devs = ibv_get_device_list(NULL); struct ibv_context *ctx = ibv_open_device(devs[0]); /* PD:绑定本端资源,也决定同名 MR/QP 的隔离边界 */ struct ibv_pd *pd = ibv_alloc_pd(ctx); /* 注册内存:对网卡可见,REMOTE_WRITE 表示允许远端写这块区域 */ struct ibv_mr *mr = ibv_reg_mr(pd, buf, buf_size, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE); /* 完成队列:深度表示 CQ 最多挂多少个未消费的完成事件 */ struct ibv_cq *cq = ibv_create_cq(ctx, 64, NULL, NULL, 0); /* 创建 QP:RC 模式,SQ/RQ 深度各 16,每个 WR 最多 1 个 SGE */ struct ibv_qp_init_attr attr = { .qp_type = IBV_QPT_RC, .send_cq = cq, .recv_cq = cq, .cap.max_send_wr = 16, .cap.max_recv_wr = 16, .cap.max_send_sge = 1, .cap.max_recv_sge = 1, }; struct ibv_qp *qp = ibv_create_qp(pd, &attr); /* 之后还有三次状态迁移:RESET -> INIT -> RTR -> RTS, RTR 需要交换对端 QPN、GID、PSN 等地址信息, RTS 阶段配置超时与重试参数 */ /* 构造并发送一个 SEND WR */ struct ibv_sge sge = { .addr = (uint64_t)buf, .length = buf_size, .lkey = mr->lkey, /* 本地 key 从 MR 拿到 */ }; struct ibv_send_wr wr = { .opcode = IBV_WR_SEND, .send_flags = IBV_SEND_SIGNALED, /* 必须请求生成完成事件 */ .sg_list = &sge, .num_sge = 1, }; struct ibv_send_wr *bad_wr = NULL; ibv_post_send(qp, &wr, &bad_wr); /* 轮询 CQ 等待完成 */ struct ibv_wc wc; while (ibv_poll_cq(cq, 1, &wc) == 0); if (wc.status != IBV_WC_SUCCESS) { /* 从 wc.status 转成字符串即可定位错误类型 */ }这里几个参数的坑要注意。一是IBV_SEND_SIGNALED不能省,没有它硬件不会针对这个 WR 生成完成事件,轮询线程会一直空转;二是如果改成 WRITE/READ,将 opcode 换成IBV_WR_RDMA_WRITE或IBV_WR_RDMA_READ,同时在wr.wr.rdma.remote_addr和wr.wr.rdma.rkey中填入对端地址和 rkey;三是max_send_wr必须大于发送端同时挂起(在途)的 WR 数,否则 post_send 会返回非零错误码。opcode 每次只能指定一个,多语义的复杂请求要拆成多个 WR。
4.4 RNR 重试、队列深度与超时参数
RNR 错误发生在发送端抢先于接收端发布 RECV 时。接收端网卡收到没有接收缓冲区的 SEND 请求,会回一个 RNR NAK;发送端依据rnr_retry参数重试。设置偏小,一次延迟就会导致任务失败;设置偏大,接收端一直不 post 时延迟被无意义放大。生产上应该在接收端把max_recv_wr调大,甚至用 SRQ(共享接收队列)让多个连接共享一个接收缓冲池,而不是指望重试来兜住设计缺口。
提示:CQ 里出现
IBV_WC_RNR_RETRY_EXC_ERR时,先查接收端是不是没有及时 post_recv,再查队列深度,最后才调 rnr_retry 参数。
链路层参数同样影响行为:timeout控制每次重传的时间间隔,retry_count控制请求重传次数;RoCE 环境还要确认 GID 索引和 VLAN 优先级与交换机配置一致,否则会出现偶发性连接断开或建链失败。
5. rdma-core库分工、环境验证与高频排错
5.1 libibverbs 与 librdmacm
rdma-core 的用户态由两个库分工。libibverbs 提供ibv_*接口,负责 QP、CQ、MR 这些对象的生命周期;librdmacm 在 verbs 之上封装连接管理,提供rdma_create_qp、rdma_connect这类接口,省去手工交换 QPN、GID 和状态迁移的过程。写连接密集型程序时,用 rdmacm 比裸 verbs 快得多,也更容易控制超时和重试。
5.2 快速验证 RDMA 环境是否可用
# 列出本机有哪些 RDMA 设备 ibv_devices # 查看设备能力,确认端口 UP,并拿到 GID 索引 ibv_devinfo -d mlx5_0 -v | grep -A 6 "port 1" # 服务端先起带宽测试,监听默认端口 ib_write_bw -d mlx5_0 --report_gbits # 客户端指定对端 IP,-s 控制消息大小 ib_write_bw -d mlx5_0 -s 65536 --report_gbits 192.168.1.2用ibv_devinfo确认端口速度和实际链路一致,再看单播 GID 列表里有没有 IPv4 或 IPv6 地址。RoCE 环境一般需要用-x指定 GID 索引,常见取值为 3,但以设备实际输出为准;GID 配错会导致建链失败但不是硬件故障。
5.3 三个实际排错现场
第一个是“RNR retry exceeded”。除了把接收端队列调深之外,还要检查是否在接收路径上给每个连接预留了足够的接收 WR,有些库在初始化时按最小值分配,低负载看不出问题,高并发一上来就报 RNR。第二个是“容器里找不到 mlx5_0”。RDMA 设备不是普通网络设备,容器要看到它必须走 SR-IOV 的 VF 透传或 vfio-pci 直通,单靠网络命名空间和端口映射无法让 verbs 层枚举到设备。第三个是“RoCE 吞吐跑不满”。先执行ethtool -S看端口计数,如果 rx_discards 或 buffer overrun 持续增长,多半是交换机侧丢帧;RoCE 没有 TCP 那样的可靠重传,千分之一的丢包率就会让吞吐明显下跌,需要把对应队列的 PFC 打开,并确保链路两端和交换机处于同一套无损配置中。
5.4 RDMA 的权限边界
RDMA WRITE/READ 一旦生效,远端进程并不感知,数据面没有协议栈去拦截它。因此注册 MR 时只给必要权限,不让不可信节点共享 rkey,跨节点通信只暴露需要访问的内存区域,并用 PD 隔离不同应用。RoCE v2 报文通常走 UDP 4791,普通防火墙规则拦不住硬件直接 DMA 的转发路径,物理接入控制反而更实际。把队列深度调大、GID 对齐、PFC 打开,再分别跑一次 TCP 和 RDMA 的带宽与 CPU 占用对比,RDMA 的钱花在哪就一目了然了。
本文还有配套的精品资源,点击获取