资讯动态

RDMA技术调研:从零拷贝原理到SEND-RECV编程实战

发布时间:2026/9/30 1:27:09 来源:尧图企业网站定制
简介这是一份面向网络与高性能计算方向工程师、运维人员及在校学生的RDMA技术调研文档系统梳理了远程直接内存访问的原理、协议与编程方法适合希望从零建立RDMA知识体系或为数据中心选型做技术储备的读者。资源包内含1个PDF文件大小约1.01MB内容涵盖RDMA相较传统网络在零拷贝、内核旁路、CPU卸载三方面的核心优势Infiniband、RoCE、iWARP三种协议对比以及WQ、SQ、RQ、CQ、QP等关键术语与SEND/RECV、WRITE/READ、ATOMIC等通信操作流程并延伸至verbs与rdma-core两种编程接口。目前已有528人学习文档结构清晰、概念讲解与流程图配合可作为入门速查与团队内部分享的参考材料。1. 从一次存储集群调优说起这份 RDMA 技术调研 PDF 到底能解决什么去年帮一个做分布式存储的团队排查故障现象很怪存储节点之间做数据同步时万兆网卡跑满但 CPU 的sys态占用常年飘在 30% 以上业务进程一多就抖。抓了perf top一看全耗在 TCP/IP 协议栈的软中断和内存拷贝上。当时有人提了一句“要不试试 RDMA”结果团队里没人能说清 RDMA 到底怎么落地、QP 怎么建、内存怎么注册。后来翻到这份《RDMA技术调研 .pdf》它把 RDMA 的三大核心优势、三种协议选型、通信流程和编程接口串成了一条线正好补上了从“知道概念”到“能动手写”之间的那段空白。这份资料适合两类人一是被高速网络性能瓶颈卡住的存储、HPC、金融低延迟场景的工程师二是准备在 rdma-core 上做二次开发但被 WQ、QP、CQ 这些术语绕晕的开发者。它不厚但把 RDMA 的骨架讲清楚了剩下的血肉得靠你自己在代码里填。2. 拆开 RDMA 的三大核心优势零拷贝、内核旁路与 CPU 卸载到底省在哪2.1 传统协议栈的“黑匣子”开销一次 send 背后发生了什么要理解 RDMA 为什么快得先看传统网络慢在哪。一次普通的send()调用数据从用户态缓冲区出发先被拷贝到内核态的 socket 缓冲区这是第一次拷贝然后协议栈层层加头TCP 层、IP 层、链路层各封装一遍接着网卡驱动通过 DMA 把数据从内核缓冲区搬到网卡硬件队列这是第二次拷贝对端收到后网卡 DMA 到内核再层层剥头最后拷贝到用户态缓冲区又是两次拷贝。整个过程 CPU 要处理中断、执行协议栈代码、管理缓冲区上下文切换频繁。这份调研 PDF 里画的那张对比图很直观传统路径上 CPU 像个忙碌的调度员每个包都要经手RDMA 路径上 CPU 只负责建连接和注册内存数据搬运全交给网卡硬件。2.2 零拷贝与内核旁路数据怎么绕过 CPU 直达对端内存RDMA 的零拷贝不是魔法它靠的是“内存注册”这个前置动作。应用程序调用ibv_reg_mr把一块虚拟内存区域注册给网卡网卡驱动会锁定这块内存的物理页防止被换出同时生成一个lkey本地密钥和rkey远程密钥。之后发送数据时应用程序直接把包含虚拟地址、长度和lkey的 Work Request 交给 QP网卡硬件根据这些信息从用户态内存直接 DMA 读取数据封装成 RDMA 报文发走。对端网卡收到后根据报文里的rkey和远程虚拟地址直接 DMA 写入对端用户态内存。全程没有内核协议栈参与没有 CPU 拷贝这就是“内核旁路”和“零拷贝”的物理基础。PDF 里强调的“CPU 卸载”则体现在对端远程机器上的 CPU 完全不知道有数据写进来了缓存也不会被这些数据污染对于内存带宽敏感的业务这点很关键。2.3 三种协议怎么选IB、RoCE 与 iWARP 的适用边界PDF 把 InfiniBand、RoCE、iWARP 放在一起对比这个结构很实用。InfiniBand 是从头为 RDMA 设计的网络需要专用网卡和交换机性能最好但成本最高适合对延迟极度敏感的 HPC 和金融交易场景。RoCE 允许在标准以太网上跑 RDMA下层是以太网头上层是 IB 头只需要网卡支持 RoCE交换机用普通的就行但要求网络是无损的得配 PFC 和 ECN否则丢包会导致性能断崖。iWARP 则是把 RDMA 跑在 TCP 上兼容性最好普通网卡加软件栈也能跑但性能优势会打折扣适合对成本敏感、网络环境复杂的场景。选型时先看你的网络基础设施已有 IB 交换机就上 IB只有以太网但能控制整网配置RoCE v2 是主流如果网络不可控、跨广域网iWARP 更稳妥。3. 从 WQ 到 CQ把 RDMA 通信流程拆成可复现的步骤3.1 核心术语的物理含义QP、SQ、RQ、CQ 不是抽象概念PDF 里用一张图把 WQ、QP、CQ 的关系画得很清楚但初学者容易把它们当成纯软件结构。实际上QPQueue Pair是网卡硬件里的一对队列SQ 用于发送RQ 用于接收。你往 SQ 里放一个 Send Request网卡就会从里面取出来执行对端网卡收到数据后如果是一个 SEND 操作会把它放到对端 QP 的 RQ 里等待对端应用程序用 Receive Request 来认领。CQCompletion Queue则是任务完成的通知机制无论是发送完成还是接收完成网卡都会往 CQ 里放一个 Completion Queue Entry应用程序通过轮询 CQ 来获知结果。这里有个容易翻车的点CQ 是共享的多个 QP 可以绑定同一个 CQ轮询时得根据wr_id区分是哪个任务完成了。3.2 一次 SEND-RECV 的完整交互代码之外的时序逻辑PDF 描述的 SEND-RECV 流程可以拆成五步。第一步接收端应用程序提前在 RQ 里发布一个 Receive Request告诉网卡“我准备好接收数据了缓冲区地址是 X长度是 Y”。第二步发送端应用程序在 SQ 里发布一个 Send Request指定要发送的数据地址、长度和目标 QP。第三步发送端网卡从用户态内存读取数据封装成 RDMA 报文发到物理链路。第四步接收端网卡收到报文根据 RQ 里的 Receive Request 把数据 DMA 写入指定缓冲区然后自动生成一个 ACK 回给发送端。第五步接收端网卡往 CQ 里写一个完成条目发送端网卡收到 ACK 后也往自己的 CQ 里写一个完成条目。两端的应用程序各自轮询 CQ就知道任务完成了。注意接收端必须在发送端发出 SEND 之前就发布 Receive Request否则会触发 RNRReceiver Not Ready错误这是新手最常见的翻车点之一。3.3 用 rdma-core 写一个最小可跑的 SEND-RECV 示例下面这段代码基于 rdma-core 的 libibverbs 接口展示了一个简化的 SEND-RECV 流程。为了聚焦核心逻辑省略了连接建立和错误处理的完整代码但关键步骤都保留了。#include infiniband/verbs.h #include stdio.h #include stdlib.h #include string.h #define BUFFER_SIZE 4096 int main() { // 1. 获取设备列表打开第一个 RDMA 设备 struct ibv_device **dev_list ibv_get_device_list(NULL); struct ibv_context *ctx ibv_open_device(dev_list[0]); // 2. 查询设备属性获取最大 QP 数、CQ 数等 struct ibv_device_attr dev_attr; ibv_query_device(ctx, dev_attr); // 3. 分配保护域 PD用于关联 QP 和 MR struct ibv_pd *pd ibv_alloc_pd(ctx); // 4. 注册内存区域 MR得到 lkey 和 rkey char *buf malloc(BUFFER_SIZE); struct ibv_mr *mr ibv_reg_mr(pd, buf, BUFFER_SIZE, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ); // 5. 创建 CQ容量为 10 个完成条目 struct ibv_cq *cq ibv_create_cq(ctx, 10, NULL, NULL, 0); // 6. 创建 QP类型为 RC可靠连接 struct ibv_qp_init_attr qp_init_attr { .send_cq cq, .recv_cq cq, .qp_type IBV_QPT_RC, .cap { .max_send_wr 10, .max_recv_wr 10, .max_send_sge 1, .max_recv_sge 1 } }; struct ibv_qp *qp ibv_create_qp(pd, qp_init_attr); // 7. 将 QP 转为 INIT 状态再转为 RTR最后转为 RTS // 这里省略了 modify_qp 的详细参数实际使用需填写对端 QP 信息 // ... // 8. 在 RQ 中发布 Receive Request struct ibv_recv_wr recv_wr { .wr_id 1, .sg_list (struct ibv_sge){ .addr (uintptr_t)buf, .length BUFFER_SIZE, .lkey mr-lkey }, .num_sge 1, .next NULL }; struct ibv_recv_wr *bad_recv_wr; ibv_post_recv(qp, recv_wr, bad_recv_wr); // 9. 在 SQ 中发布 Send Request struct ibv_send_wr send_wr { .wr_id 2, .sg_list (struct ibv_sge){ .addr (uintptr_t)buf, .length BUFFER_SIZE, .lkey mr-lkey }, .num_sge 1, .opcode IBV_WR_SEND, .send_flags IBV_SEND_SIGNALED, .next NULL }; struct ibv_send_wr *bad_send_wr; ibv_post_send(qp, send_wr, bad_send_wr); // 10. 轮询 CQ 获取完成事件 struct ibv_wc wc; while (ibv_poll_cq(cq, 1, wc) 0) { // 忙等待实际应用中可结合事件通道 } if (wc.status IBV_WC_SUCCESS) { printf(任务完成opcode%d\n, wc.opcode); } // 11. 清理资源 ibv_destroy_qp(qp); ibv_destroy_cq(cq); ibv_dereg_mr(mr); ibv_dealloc_pd(pd); ibv_close_device(ctx); ibv_free_device_list(dev_list); free(buf); return 0; }这段代码里ibv_reg_mr的IBV_ACCESS_REMOTE_WRITE和IBV_ACCESS_REMOTE_READ标志决定了这块内存是否允许远端直接读写。如果只做 SEND-RECV其实不需要这两个标志但加上也无妨。ibv_create_qp里的max_send_wr和max_recv_wr决定了队列深度设太小会导致ibv_post_send返回 ENOMEM。ibv_modify_qp的状态迁移是必须的RC 类型的 QP 必须经过 INIT、RTR、RTS 三个状态每个状态要填的参数不同比如 RTR 需要填对端的 QPN 和路由信息RTS 需要填超时和重传次数。轮询 CQ 时ibv_poll_cq返回的是本次取到的完成条目数返回 0 表示还没有完成事件返回负数表示出错。4. 避坑指南RDMA 编程与部署中的五个血泪教训4.1 内存注册失败为什么ibv_reg_mr返回 NULL现象是调用ibv_reg_mr后返回 NULLerrno显示ENOMEM或EPERM。常见原因有三个一是内存区域太大超过了网卡支持的单个 MR 最大长度查ibv_query_device返回的max_mr_size可以确认二是权限标志冲突比如同时设置了IBV_ACCESS_REMOTE_WRITE和IBV_ACCESS_REMOTE_READ但网卡不支持三是系统ulimit -l限制太小注册内存会被锁定超过限制就失败。解决办法是先查设备属性再调整ulimit -l到足够大或者用ibv_reg_mr的IBV_ACCESS_ON_DEMAND标志按需分页。4.2 RNR 错误接收端没准备好发送端直接超时现象是发送端ibv_poll_cq返回的wc.status是IBV_WC_RNR_RETRY_EXC_ERR意思是接收端没有可用的 Receive Request。原因很简单发送端发 SEND 太快接收端还没来得及ibv_post_recv。RC 连接默认的 RNR 重试次数是 6 次间隔是 1 毫秒左右如果接收端在这段时间内还是没发布 Receive Request发送端就会报错。解决办法有两个一是接收端提前批量发布多个 Receive Request保证 RQ 里始终有存货二是调整 QP 的min_rnr_timer和rnr_retry参数在ibv_modify_qp转 RTS 时设置但治本的办法还是让接收端及时补充 RQ。4.3 RoCE 网络丢包PFC 没配好性能直接崩现象是 RoCE v2 环境下小包测试正常一跑大流量就出现吞吐量骤降、延迟飙升。抓包会发现大量重传但以太网层没有丢包计数。原因是 RoCE 对丢包极度敏感一旦交换机或网卡队列溢出PFCPriority Flow Control没有正确配置报文被丢弃后 RDMA 的重传机制会导致性能断崖。解决办法是在交换机和网卡两端都配置 PFC把 RDMA 流量映射到无损队列同时开启 ECN显式拥塞通知让端侧能感知拥塞并降速。具体命令取决于交换机型号网卡侧可以用mlnx_qos工具配置。4.4 QP 状态迁移失败RTR 和 RTS 的参数填错现象是ibv_modify_qp返回EINVALQP 卡在 INIT 状态。RTR 状态需要填对端的 QPN、LIDIB 网络或 GIDRoCE 网络、MTU、路径 MTU 等RTS 状态需要填超时、重传次数、RNR 重试次数。最常见的错误是 RoCE 环境下 GID 索引填错或者 MTU 和对端不一致。解决办法是先用ibv_query_gid确认 GID 索引再确保两端 MTU 一致RC 连接的 QPN 必须和对端实际 QPN 匹配。4.5 轮询 CQ 占满 CPU忙等待不是唯一选择现象是应用程序轮询 CQ 的线程 CPU 占用率 100%即使没有数据传输。原因是ibv_poll_cq是忙等待没有事件时也会一直空转。解决办法是结合 Completion Channel 使用通过ibv_create_comp_channel创建事件通道用ibv_get_cq_event阻塞等待有事件时再轮询 CQ。另一种做法是混合策略先轮询几次没有完成事件就切换到阻塞等待兼顾低延迟和 CPU 占用。5. 进阶技巧用 RDMA READ/WRITE 替代 SEND-RECV 降低对端 CPU 开销SEND-RECV 虽然简单但有个天然缺陷接收端必须提前发布 Receive Request而且发送端无法控制数据落在对端内存的哪个位置。对于存储场景比如把数据写入远端内存的指定偏移SEND-RECV 就不够用了。RDMA READ/WRITE 才是真正的“远程直接内存访问”。WRITE 操作由发送端指定对端虚拟地址和rkey网卡直接把数据写入对端内存对端 CPU 完全无感。READ 操作则是从对端内存读取数据到本地同样不需要对端 CPU 参与。PDF 里提到“对于 RDMA 的读写操作远程端都是不知道正在执行此操作的”这句话点出了关键对端只需要在初始化阶段注册好内存并交换rkey之后所有读写都是网卡硬件完成。用 WRITE 替代 SEND 的改造点在于发送端的ibv_send_wr里opcode改成IBV_WR_RDMA_WRITE并且要填wr.rdma.remote_addr和wr.rdma.rkey。接收端不需要发布 Receive Request但必须提前把内存注册好并把rkey和虚拟地址告诉发送端。这里有个容易忽略的细节rkey是和 MR 绑定的如果接收端重新注册了 MRrkey会变发送端必须同步更新。另外WRITE 操作默认不产生完成事件除非设置IBV_SEND_SIGNALED对于大批量小数据写入可以攒一批再发一个带信号标志的 WR减少 CQ 轮询次数。验证 RDMA READ/WRITE 是否生效可以用ibv_rc_pingpong和ibv_ud_pingpong做基础连通性测试再用perftest套件里的ib_write_bw和ib_read_bw测带宽。我一般会先跑ib_write_bw -d mlx5_0 -a看两端能否正常握手如果报错就查 GID 索引和防火墙。测延迟用ib_write_lat注意加-n 10000增加迭代次数否则数据波动大。从那以后我每次部署 RDMA 环境都强制先跑一遍ibv_devinfo确认网卡状态是 PORT_ACTIVE再跑ib_write_bw验证链路最后才上业务代码。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑