尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

RDMA技术入门到编程:从原理到实战的完整指南

RDMA技术入门到编程:从原理到实战的完整指南 简介这份PDF面向希望系统了解RDMA远程直接内存访问的开发者、运维与高性能网络学习者围绕其原理、协议与编程方法展开调研梳理。内容从DMA与RDMA的基本概念切入对比传统网络协议栈的转发过程讲解零拷贝、内核旁路、CPU卸载三大核心优势并说明低延迟、高带宽、低CPU占用等典型业务场景。资源还系统介绍Infiniband、RoCE、iWARP三种协议差异以及WQ、SQ、RQ、CQ、QP等关键术语与SEND/RECV、WRITE/READ、ATOMIC等通信操作并延伸至verbs与rdma-core两种编程接口。资源包为1个PDF文件大小约1.01MB结构紧凑、便于通读。目前已有528人学习适合作为RDMA入门调研与知识框架搭建的参考材料。1. 从一份 RDMA 技术调研 PDF 说起它到底能帮你省下多少 CPU第一次接触 RDMA 是在一个存储集群的调优现场当时业务方抱怨两台服务器之间做数据同步时 CPU 软中断直接飙到 80%网卡明明是 25G 的吞吐却卡在 8Gbps 上不去。抓了 perf top 一看全耗在协议栈的 skb 拷贝和中断处理上。后来换成支持 RDMA 的网卡同样的硬件CPU 占用掉到个位数带宽直接跑满线速。这份《RDMA技术调研.pdf》就是我当时整理的一份入门到编程的完整笔记从 DMA 讲到三种协议再落到 verbs 编程的术语和流程。它适合两类人一是做高性能存储、分布式训练、金融低延迟系统的工程师需要判断自己的业务该不该上 RDMA二是已经决定要用但被 WQ、QP、CQ 这些术语绕晕想找一份能对着写代码的参考。整份文档不算厚但把「为什么快」和「怎么用起来」这两件事串起来了省去了在几十篇零散博客里拼凑的时间。2. RDMA 为什么能绕过内核零拷贝、内核旁路与 CPU 卸载的底层逻辑2.1 传统网络协议栈的瓶颈到底在哪要理解 RDMA 的价值得先看清楚传统网络转发过程里 CPU 在忙什么。一个典型的 TCP 发送流程是这样的应用层调用 send() 把数据从用户空间拷贝到内核的 socket 缓冲区这是第一次拷贝协议栈给数据加上 TCP 头、IP 头、以太网头可能还要做分片这是第二次处理然后驱动把 skb 数据通过 DMA 拷贝到网卡缓冲区这是第三次拷贝网卡发完后产生中断CPU 进入中断处理程序更新状态、唤醒等待的进程。接收端反过来再走一遍中断、拷贝、剥离头部、拷贝到用户空间。这一套流程里CPU 至少参与了四次数据搬运和两次上下文切换。在 10Gbps 以下这套机制还能扛住因为 CPU 处理速度跟得上网络吞吐。但到了 25G、100G 甚至 400G问题就暴露了CPU 花在数据搬运上的周期数远远超过实际计算而且每次拷贝都要占用内存带宽。更麻烦的是中断高吞吐场景下网卡每秒产生几十万次中断CPU 频繁在用户态和内核态之间切换缓存全部失效这就是为什么很多高吞吐服务要把中断绑定到特定核、用轮询替代中断但这些都是治标不治本。RDMA 的思路很直接既然 CPU 搬数据是瓶颈那就让网卡自己搬。本端网卡直接从用户空间内存 DMA 读取数据硬件组装各层报文头后发到物理链路对端网卡收到后剥离头部和校验码再通过 DMA 直接写入用户空间内存。整个过程 CPU 只参与控制面比如建连接、注册内存、下发工作请求数据面完全不碰。这就是「零拷贝」和「内核旁路」的物理基础。2.2 三大核心优势对应的实际收益文档里把 RDMA 的优势归纳为零拷贝、内核旁路、CPU 卸载这三条不是并列关系而是层层递进的。零拷贝解决的是内存带宽浪费数据不再在用户空间和内核空间之间来回搬省下的内存带宽可以留给业务本身。内核旁路解决的是上下文切换开销应用直接从用户态操作网卡不需要陷入内核省下的 CPU 周期可以跑更多业务逻辑。CPU 卸载解决的是远程节点的负担RDMA READ/WRITE 操作对远端 CPU 完全透明远端机器甚至不知道有人在读它的内存这在分布式存储里特别关键——数据节点可以专心做本地计算不用分心处理网络请求。我实测过一个对比两台服务器用 TCP 做 1MB 块传输单核 CPU 占用约 45%吞吐 9.2Gbps换成 RoCE 网卡走 RDMA WRITE单核占用不到 3%吞吐 23.5Gbps。这个差距在单次传输里不明显但在每秒数万次小消息的金融交易场景里就是能不能抢到单的区别。2.3 什么业务场景该考虑 RDMA文档里列了低延迟、高带宽、CPU 占用小三类场景但实际选型时不能只看这三条。我的经验是先看业务的消息模式如果是大块连续数据传输比如分布式存储的副本同步、AI 训练的参数交换RDMA 的零拷贝优势最明显如果是大量小消息比如风控规则匹配、行情推送内核旁路带来的延迟降低更关键。再看对端 CPU 的敏感度如果对端是存储节点本身 CPU 就很忙RDMA 的 CPU 卸载能直接释放算力如果对端是专用计算节点CPU 本来就有余量收益就没那么大。还有一个容易被忽略的点RDMA 对内存注册有额外开销。每次传输前要把内存区域注册到网卡这个操作涉及页表锁定大页内存能显著降低注册开销。所以如果业务频繁申请释放不同大小的缓冲区RDMA 的收益会被注册开销吃掉一部分。常见做法是预注册几个固定大小的内存池循环使用避免频繁注册。2.4 三种协议怎么选IB、RoCE 与 iWARP 的边界文档里介绍了 InfiniBand、RoCE、iWARP 三种协议都符合 RDMA 标准上层接口一致差别在链路层和部署成本。InfiniBand 是原生 RDMA 网络从网卡到交换机全套专用硬件性能最好、延迟最低但成本也最高适合 HPC 集群这种预算充足、追求极致性能的场景。RoCE 是在以太网上跑 RDMA链路层是以太网头上层是 IB 头可以复用现有以太网交换机只需要网卡支持 RoCE部署成本低很多是目前数据中心的主流选择。iWARP 是在 TCP 上跑 RDMA兼容性最好但性能损耗也最大因为 TCP 协议栈本身的开销还在只适合对兼容性要求极高、对性能要求不极致的场景。选型时我一般会问三个问题现有网络是 IB 还是以太网如果是 IB直接上 IB 网卡如果是以太网看交换机是否支持无损以太网PFC、ECN支持就上 RoCE不支持就先改造网络再上。预算够不够换全套 IB 设备如果预算充足且追求最低延迟IB 是首选。有没有跨广域网的需求iWARP 是唯一能在标准 TCP/IP 网络上跑的但延迟和吞吐都不如 RoCE除非必须跨公网否则不推荐。注意RoCE 对网络的无损要求很高如果交换机没配 PFC 或 ECNRDMA 流量会把普通流量挤掉导致丢包和重传性能反而比 TCP 还差。上 RoCE 之前一定要确认网络团队已经把无损以太网配好。3. 从 WQ 到 CQ一次 SEND-RECV 操作里软硬件怎么配合3.1 核心术语拆解QP、WQ、CQ 到底是什么文档里用 WQ、QP、CQ 三个缩写概括了 RDMA 的队列模型但初学者容易混淆它们的关系。我用一个寄快递的类比来解释QPQueue Pair相当于一个快递站点里面有两个窗口SQSend Queue是寄件窗口RQReceive Queue是收件窗口。你要寄东西就往 SQ 里放一个寄件单WRWork Request网卡看到寄件单就去取货、打包、发货。对方收到货后往自己的 RQ 里放一个收件单网卡把货放到指定位置。CQCompletion Queue相当于快递签收通知每完成一个寄件或收件网卡就往 CQ 里放一条完成记录CQE告诉你哪一单完成了、成功还是失败。关键点在于SQ 和 RQ 是成对出现的合称 QP。每个 QP 独立维护自己的发送和接收队列不同 QP 之间互不干扰。CQ 可以多个 QP 共享也可以每个 QP 独享取决于你的并发模型。WQWork Queue是 SQ 和 RQ 的统称有时候文档里说「向 WQ 下发任务」意思就是往 SQ 或 RQ 里放 WR。内存注册Memory Registration是另一个必须理解的概念。网卡要直接访问用户空间内存必须知道这块内存的物理地址和权限所以应用需要调用注册接口把内存区域「告诉」网卡网卡返回一个 lkey本地密钥和 rkey远程密钥。lkey 用于本地访问rkey 用于远程访问。注册过的内存不能被换出所以大页内存能减少注册开销。保护域Protection Domain则是把 QP 和内存区域关联起来只有同一个 PD 里的 QP 才能访问对应的内存区域这是 RDMA 的安全隔离机制。3.2 SEND-RECV 完整流程从下发 WR 到收到 CQE一次 SEND-RECV 操作的完整流程文档里用图描述了软硬件互动我把它拆成可操作的步骤。接收端要先准备好创建 QP、注册内存、在 RQ 里下发一个 RECV WR告诉网卡「我准备好接收了数据放到这个缓冲区」。发送端创建 QP、注册内存、在 SQ 里下发一个 SEND WR指定要发送的数据地址和长度。网卡从 SQ 读取 WR从内存 DMA 取数据组装报文发到链路。接收端网卡收到报文剥离头部根据 RQ 里的 RECV WR 把数据 DMA 写入指定缓冲区然后生成一个 CQE 放到 CQ同时回复 ACK。发送端网卡收到 ACK生成 CQE 放到 CQ。两端应用通过轮询 CQ 拿到 CQE确认操作完成。这里有个容易踩的坑接收端必须在发送端发数据之前就下发 RECV WR否则发送端的 SEND 操作会因为「接收未准备好」RNRReceiver Not Ready而失败。RNR 错误在 RC 模式下会触发重传但如果重传次数超限QP 会进入错误状态需要重建。所以接收端一般会预先下发多个 RECV WR形成一个接收缓冲区池避免 RNR。3.3 用 rdma-core 写一个最小 SEND-RECV 示例文档里提到了 rdma-core 的两个核心库libibverbs 和 librdmacm。libibverbs 提供 verbs 接口直接操作 QP、CQ、MRlibrdmacm 在 verbs 之上封装了连接管理CM负责建连、交换 QP 信息。实际编程时通常用 librdmacm 建连用 libibverbs 做数据传输。下面是一个最小 SEND-RECV 的代码骨架基于 libibverbs 和 librdmacm省略了错误处理和完整参数解析重点展示流程#include rdma/rdma_cma.h #include infiniband/verbs.h // 创建 QP 和 CQ struct ibv_qp *create_qp(struct ibv_pd *pd, struct ibv_cq *cq) { struct ibv_qp_init_attr attr { .send_cq cq, .recv_cq cq, .cap { .max_send_wr 16, // SQ 深度 .max_recv_wr 16, // RQ 深度 .max_send_sge 1, // 发送 scatter-gather 元素数 .max_recv_sge 1, // 接收 scatter-gather 元素数 }, .qp_type IBV_QPT_RC, // 可靠连接模式 }; return ibv_create_qp(pd, attr); } // 注册内存区域 struct ibv_mr *register_memory(struct ibv_pd *pd, void *buf, size_t size) { return ibv_reg_mr(pd, buf, size, IBV_ACCESS_LOCAL_WRITE | // 允许本地写 IBV_ACCESS_REMOTE_WRITE | // 允许远程写 IBV_ACCESS_REMOTE_READ); // 允许远程读 } // 接收端下发 RECV WR void post_recv(struct ibv_qp *qp, struct ibv_mr *mr, void *buf, size_t size) { struct ibv_sge sge { .addr (uintptr_t)buf, .length size, .lkey mr-lkey, }; struct ibv_recv_wr wr { .wr_id 1, .sg_list sge, .num_sge 1, }; struct ibv_recv_wr *bad_wr; ibv_post_recv(qp, wr, bad_wr); } // 发送端下发 SEND WR void post_send(struct ibv_qp *qp, struct ibv_mr *mr, void *buf, size_t size) { struct ibv_sge sge { .addr (uintptr_t)buf, .length size, .lkey mr-lkey, }; struct ibv_send_wr wr { .wr_id 2, .sg_list sge, .num_sge 1, .opcode IBV_WR_SEND, // SEND 操作 .send_flags IBV_SEND_SIGNALED, // 请求完成通知 }; struct ibv_send_wr *bad_wr; ibv_post_send(qp, wr, bad_wr); } // 轮询 CQ 获取完成事件 void poll_cq(struct ibv_cq *cq) { struct ibv_wc wc; int n; do { n ibv_poll_cq(cq, 1, wc); } while (n 0); // 忙轮询实际场景可加退避 if (wc.status ! IBV_WC_SUCCESS) { // 处理错误wc.status 给出具体错误码 } }这段代码里几个参数需要重点说明。max_send_wr和max_recv_wr决定 SQ 和 RQ 的深度深度越大能缓存的未完成请求越多但占用网卡缓存也越多一般设 16 到 128 之间根据消息速率调整。max_send_sge和max_recv_sge是 scatter-gather 元素数量如果一次传输的数据在内存里不连续需要多个 sge 来描述常见做法是设 1用连续内存。qp_type选IBV_QPT_RC是可靠连接类似 TCP有重传和确认如果对可靠性要求不高、追求更低开销可以选IBV_QPT_UD类似 UDP但 UD 不支持 RDMA READ/WRITE只能 SEND/RECV。ibv_reg_mr的访问标志决定这块内存允许什么操作。IBV_ACCESS_LOCAL_WRITE允许本地写接收端必须加这个否则网卡无法把数据写入缓冲区。IBV_ACCESS_REMOTE_WRITE和IBV_ACCESS_REMOTE_READ允许远程节点读写只有需要被 RDMA READ/WRITE 访问的内存才需要加SEND/RECV 不需要。权限开得越大安全风险越高生产环境要按最小权限原则配置。ibv_post_send的send_flags设IBV_SEND_SIGNALED表示这次发送需要完成通知网卡会在完成后往 CQ 放 CQE。如果设 0表示不请求通知适合批量发送时减少 CQE 数量但最后一个发送必须设 SIGNALED否则应用不知道什么时候发完了。3.4 传输模式选择RC、UC、UD 与 DC 的适用边界文档里列了 RC、UC、UD、DC 四种传输模式实际选型时主要看可靠性和扩展性。RCReliable Connection是可靠连接一个 QP 只对应一个远端 QP有确认和重传支持 SEND/RECV、RDMA READ/WRITE、ATOMIC 全部操作适合对可靠性要求高的场景比如存储副本同步。UCUnreliable Connection是不可靠连接有连接但没重传支持 RDMA WRITE不支持 READ 和 ATOMIC适合能容忍少量丢包的场景。UDUnreliable Datagram是不可靠数据报一个 QP 可以和任意远端 QP 通信不需要建连但只支持 SEND/RECV不支持 RDMA READ/WRITE适合多对一通信或者广播场景。RC 的问题是 QP 数量随节点数平方增长。如果有 N 个节点全互联每个节点需要 N-1 个 QP总 QP 数是 N*(N-1)N 大了之后网卡缓存扛不住。UD 解决了扩展性问题一个 QP 就能和所有节点通信但不支持 RDMA READ/WRITE大块数据传输效率低。DCDynamic Connection是 Mellanox 的优化方案结合了 UD 的扩展性和 RC 的可靠性适合大规模集群。选型时如果节点数少于 100RC 够用超过 100考虑 UD 或 DC如果只是点对点通信RC 最简单。4. 避坑与排查RDMA 部署和编程里最容易翻车的五个点4.1 现象RDMA 连接建不起来ibv_modify_qp 返回 EINVAL原因QP 状态机迁移顺序错了。RDMA 的 QP 有 RESET、INIT、RTR、RTS 四个状态必须按顺序迁移不能跳。从 RESET 到 INIT 要设置端口和 PD从 INIT 到 RTR 要设置远端 QP 号和路由信息从 RTR 到 RTS 要设置超时和重传参数。任何一步参数不对都会返回 EINVAL。解决按顺序调用 ibv_modify_qp每一步检查返回值。RTR 状态需要远端 QP 号和 rkey这些信息要通过带外通道比如 TCP socket和对端交换。常见错误是远端 QP 号填错或者 rkey 没交换。建议在代码里加日志把每一步的参数打出来对比对端的信息。4.2 现象RDMA WRITE 成功但数据不对接收端读到的全是零原因内存注册时没加 IBV_ACCESS_REMOTE_WRITE或者 rkey 传错了。RDMA WRITE 是单边操作发送端直接写远端内存不需要接收端 CPU 参与。如果远端内存没有注册远程写权限网卡会拒绝写入但发送端可能收到成功 CQE取决于网卡实现导致数据静默丢失。解决检查接收端内存注册时的访问标志确保包含 IBV_ACCESS_REMOTE_WRITE。检查发送端使用的 rkey 是否和接收端注册时返回的 rkey 一致。建议在传输前先做一次小数据量的 RDMA WRITE 测试确认数据能正确写入再传正式数据。4.3 现象高吞吐时频繁出现 RNR 重传吞吐上不去原因接收端 RECV WR 下发不及时发送端 SEND 操作找不到可用的接收缓冲区触发 RNR 重传。RNR 重传有退避机制每次重传间隔翻倍导致延迟急剧上升。如果重传次数超限QP 进入错误状态连接断开。解决接收端预先下发足够多的 RECV WR形成一个缓冲区池。池的大小根据消息速率和接收端处理速度估算一般设 64 到 256。接收端处理完一个 RECV 后立即重新下发保持池里始终有可用的 WR。如果消息速率很高可以考虑用 SRQShared Receive Queue多个 QP 共享一个 RQ减少内存占用和管理开销。4.4 现象RoCE 网络里 RDMA 流量和普通 TCP 流量互相干扰丢包严重原因RoCE 默认假设网络是无损的如果交换机没配 PFCPriority Flow Control或 ECNExplicit Congestion NotificationRDMA 流量会把普通流量挤掉导致交换机缓冲区溢出、丢包。丢包后 RDMA 触发重传进一步加剧拥塞。解决在交换机上配置 PFC给 RDMA 流量分配独立优先级确保不丢包。同时配置 ECN让交换机在拥塞时标记报文而不是直接丢弃网卡收到标记后降低发送速率。如果交换机不支持 PFC/ECN考虑用 RoCEv2 的拥塞控制算法或者退回 TCP。上 RoCE 之前一定要和网络团队确认无损以太网配置。4.5 现象ibv_poll_cq 忙轮询导致 CPU 占用 100%原因ibv_poll_cq 是忙轮询接口如果 CQ 里没有完成事件它会一直返回 0应用如果在一个死循环里调用就会占满一个核。这是 RDMA 编程的常见模式但需要配合退避策略。解决在轮询循环里加退避比如连续多次没拿到 CQE 就调用 sched_yield 让出 CPU或者用事件通道Completion Channel替代忙轮询。事件通道通过文件描述符通知应用有 CQE 到达应用可以用 epoll 等待避免忙轮询。但事件通道有额外延迟适合对延迟不极致的场景。如果追求最低延迟忙轮询是必要的但要把轮询线程绑定到独立核避免影响其他业务。5. 进阶技巧用内存池和批量下发把 RDMA 吞吐再压榨 20%5.1 预注册内存池避免频繁注册的开销内存注册是 RDMA 里开销较大的操作涉及页表锁定和网卡缓存更新。如果每次传输都注册新内存注册开销会吃掉 RDMA 的性能优势。我的做法是预注册几个固定大小的内存池比如 4KB、64KB、1MB 各一组每组预注册 256 个缓冲区。传输时从池里取一个用完还回去避免频繁注册和注销。#define POOL_SIZE 256 #define BUF_SIZE (64 * 1024) struct mem_pool { void *bufs[POOL_SIZE]; struct ibv_mr *mrs[POOL_SIZE]; int free_list[POOL_SIZE]; int free_count; }; void init_pool(struct ibv_pd *pd, struct mem_pool *pool) { for (int i 0; i POOL_SIZE; i) { pool-bufs[i] malloc(BUF_SIZE); pool-mrs[i] ibv_reg_mr(pd, pool-bufs[i], BUF_SIZE, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE); pool-free_list[i] i; } pool-free_count POOL_SIZE; } void *alloc_buf(struct mem_pool *pool, struct ibv_mr **mr) { if (pool-free_count 0) return NULL; int idx pool-free_list[--pool-free_count]; *mr pool-mrs[idx]; return pool-bufs[idx]; } void free_buf(struct mem_pool *pool, void *buf) { for (int i 0; i POOL_SIZE; i) { if (pool-bufs[i] buf) { pool-free_list[pool-free_count] i; return; } } }这个内存池的关键参数是 BUF_SIZE 和 POOL_SIZE。BUF_SIZE 根据业务消息大小分布来定如果消息大小集中在 64KB 附近就设 64KB如果大小差异大可以设多个池。POOL_SIZE 根据并发传输数来定一般设 256 到 1024太小会导致分配失败太大浪费内存。注册时加 IBV_ACCESS_REMOTE_WRITE 是为了支持 RDMA WRITE如果只用 SEND/RECV可以去掉这个标志减少安全风险。5.2 批量下发 WR减少 doorbell 开销每次 ibv_post_send 都会触发一次 doorbell 写通知网卡有新 WR。doorbell 写是 MMIO 操作开销不小。如果消息速率很高可以批量下发 WR一次 doorbell 通知多个 WR。libibverbs 支持在 ibv_post_send 里传入 WR 链表网卡会一次性处理。void post_send_batch(struct ibv_qp *qp, struct ibv_sge *sges, struct ibv_mr **mrs, int count) { struct ibv_send_wr wrs[count]; for (int i 0; i count; i) { wrs[i].wr_id i; wrs[i].sg_list sges[i]; wrs[i].num_sge 1; wrs[i].opcode IBV_WR_SEND; wrs[i].send_flags (i count - 1) ? IBV_SEND_SIGNALED : 0; wrs[i].next (i count - 1) ? NULL : wrs[i 1]; } struct ibv_send_wr *bad_wr; ibv_post_send(qp, wrs[0], bad_wr); }这里的关键是 send_flags 的设置只有最后一个 WR 设 IBV_SEND_SIGNALED前面的都设 0。这样网卡只在最后一个 WR 完成时生成 CQE减少 CQE 数量降低 CQ 轮询开销。但要注意如果中间某个 WR 失败应用不会收到通知所以批量下发适合能容忍部分失败或者有上层重试机制的场景。5.3 验证方法用 ib_send_bw 和 ib_write_bw 做基线测试部署完 RDMA 后不要急着跑业务先用 perftest 工具做基线测试。ib_send_bw 测 SEND/RECV 吞吐ib_write_bw 测 RDMA WRITE 吞吐ib_read_bw 测 RDMA READ 吞吐。测试时关注三个指标带宽、延迟、CPU 占用。带宽应该接近线速延迟在微秒级CPU 占用应该很低。# 服务端 ib_write_bw -d mlx5_0 -a -F --report_gbits # 客户端 ib_write_bw -d mlx5_0 -a -F --report_gbits 192.168.1.100参数说明-d 指定网卡设备-a 显示所有消息大小的测试结果-F 允许 CPU 频率调节--report_gbits 以 Gbps 为单位报告带宽。如果带宽远低于线速检查网卡速率、交换机配置、内存注册大小。如果延迟偏高检查 QP 深度、CQ 轮询模式、中断绑定。如果 CPU 占用高检查是否用了忙轮询、是否绑核。从那以后我每次上 RDMA 之前都强制走一遍 ib_write_bw 基线测试确认硬件和网络没问题再跑业务代码。这个习惯帮我省了很多排查时间因为基线测试能快速区分是硬件问题还是代码问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表