简介:rdma_bench 是一个面向高性能网络开发者的 RDMA 性能分析与理解框架,源自 USENIX ATC 顶会论文的开源实现,适用于具备 Linux 内核驱动开发基础、熟悉 InfiniBand 架构及 RDMA 编程(verbs API)的中高级工程师与科研人员。资源聚焦 RDMA 协议栈性能建模、HCA 硬件协同基准测试与底层驱动接口验证,可支撑 RDMA 应用调优、网卡驱动适配及分布式存储/高性能计算场景的通信瓶颈分析。压缩包共226个文件,含36个C源码(如qp.c、verbs.c、cq.c等核心RDMA对象实现)、22个头文件(h)、81个Shell脚本(用于自动化编译、部署与多场景压测)、15个Markdown文档(含设计说明与实验指南),以及Makefile、配置模板与多组预设测试用例(如swarm-32b_read、allsig-varsize_reads等),整体仅325KB,轻量但结构完整。目前已有375人学习下载,提供从环境搭建、驱动集成(兼容Mellanox OFED及上游内核)、到多维度RDMA操作延迟/吞吐基准测试的一站式参考实现。
1. rdma_bench:一个能让你亲手摸到 RDMA 内存语义的轻量框架,不是 demo,是可调试、可打断点、可改参数的真实 bench
你有没有试过在 Linux 上跑ib_write_bw,看着吞吐数字跳得飞起,却完全不知道底层到底发生了什么?——数据从用户态哪块内存出发?QP 是怎么建的?WR 是怎么发的?Completion Queue 怎么被轮询?更别说修改 MTU、调整 WR 数量、模拟乱序完成这些关键控制点了。rdma_bench 不是另一个封装好的黑匣子 benchmark 工具,它是一套带完整注释、分层清晰、每行代码都暴露 RDMA 原语调用路径的 C 框架。它把 libibverbs 的裸 API 调用组织成可读、可断、可改的模块:init → setup_qp → post_send → poll_cq → cleanup。某高校实验室用它给研究生讲 RDMA 编程模型,学生第一次在 gdb 里单步进ibv_post_send()看 WR 结构体填充过程时,当场画出了完整的 send queue 生命周期图。它适合两类人:刚学完 RoCE/InfiniBand 协议栈想落地验证的工程师,以及需要快速构建定制化 RDMA 测试逻辑(比如测特定大小包的 latency 分布、压测特定 QP 配置下的重传行为)的系统开发者。开源不等于好懂,但这个项目把“开源”二字落到了每一行#include <infiniband/verbs.h>后面。
2. 从零编译运行:环境准备、依赖解析与最小可执行路径
2.1 环境前提:确认 RDMA 子系统已就绪,不是“装了驱动就等于能跑”
RDMA 不是插上网卡就能用的即插即用设备。必须确认内核已加载对应驱动,并且用户态能通过 sysfs 和 ibstat 看到有效端口。常见翻车点在于:
- 使用 Mellanox 网卡但未安装
mlnx-ofed或仅安装了通用rdma-core; - 系统启用了 IOMMU(如 Intel VT-d),但未在 GRUB 中添加
intel_iommu=off(某些老版本 OFED 与 IOMMU 冲突); - 网卡工作在 Ethernet 模式而非 IB/RoCE 模式(需通过
mlxfwmanager或厂商工具切换)。
验证命令链必须连贯执行,缺一不可:
# 检查内核模块是否加载(重点看 mlx5_core, ib_uverbs, rdma_cm) lsmod | grep -E "(mlx|ib|rdma)" # 检查是否有可用的 RDMA 设备(输出应含 port GUID 和状态 active) ibstat # 检查用户态 verbs 接口是否可访问(返回非空 JSON 表示正常) ibv_devinfo -v | head -20 # 检查 RoCE 模式下 IP 地址是否绑定到 RDMA 端口(RoCE v2 必须有 IPv4 地址) ibdev2netdev提示:
ibv_devinfo -v输出中若出现No devices found,90% 是ib_uverbs模块未加载或/dev/infiniband/uverbs0权限不足(需加入rdma用户组或chmod 666)。不要跳过这一步直接编译——后面所有报错都会指向“找不到 device”,而真实原因藏在这儿。
2.2 依赖安装:只装真正需要的头文件与库,拒绝全量 ofed
rdma_bench 仅依赖libibverbs和librdmacm的开发包,不需要安装整套 MLNX_OFED(那会覆盖系统原有驱动,引发兼容性问题)。根据发行版选择精简安装:
# Ubuntu/Debian(注意:不要 apt install ibverbs-utils,那是命令行工具,不是 dev 包) sudo apt update && sudo apt install -y libibverbs-dev librdmacm-dev # CentOS/RHEL 8+ sudo dnf install -y libibverbs-devel librdmacm-devel # 验证头文件存在(关键!编译失败常因找不到 verbs.h) ls /usr/include/infiniband/verbs.h /usr/include/infiniband/rdma_cma.hlibibverbs-dev提供ibv_*系列函数声明与静态链接符号;librdmacm-dev提供rdma_*连接管理接口(本项目虽以 verbs mode 为主,但部分初始化逻辑复用 rdma_cm)。若系统中存在多个版本(如/usr/local/lib64下有自编译旧版),务必用pkg-config --modversion libibverbs确认编译时实际链接的是哪个版本——版本错位会导致ibv_create_qp()返回EINVAL这类玄学错误。
2.3 编译与运行:Makefile 解析与第一个成功日志解读
项目根目录下Makefile极简,仅三行核心逻辑:
CC = gcc CFLAGS = -O2 -Wall -Wextra -I/usr/include/infiniband LIBS = -libverbs -lrdmacm rdma_bench: rdma_bench.c $(CC) $(CFLAGS) -o $@ $< $(LIBS)执行make后生成可执行文件rdma_bench。首次运行必须指定服务端/客户端角色及目标地址:
# 启动服务端(监听默认端口 18515,绑定本机第一个 RDMA 端口) ./rdma_bench -s # 启动客户端(连接服务端 IP,注意:此处 IP 是 RoCE 网络的 IPv4 地址,非 IB LID) ./rdma_bench -c 192.168.10.10成功运行后,客户端输出类似:
[INFO] Connected to server 192.168.10.10:18515 [INFO] QP created: qpn=0x1a32, lid=0x0003, port=1 [INFO] Memory registered: addr=0x7f8b3c000000, length=1048576, rkey=0x123456 [INFO] Sent 1000 messages of size 4096 bytes [INFO] Avg latency: 1.24 us (min=0.87, max=3.52)这段日志不是装饰——qpn是 Queue Pair Number,rkey是远程内存访问密钥,Avg latency是端到端微秒级测量值。所有这些字段都在源码rdma_bench.c的print_stats()函数中硬编码打印,你可以随时加一行printf("WR id=%d\n", wr.wr_id)查看每个工作请求的 ID 分配顺序。这才是“可调试”的起点。
3. 核心模块拆解:init → qp → send → poll → cleanup 的五段式控制流
3.1 初始化阶段:context、pd、mr 的创建逻辑与资源生命周期绑定
rdma_bench的init_resources()函数是 RDMA 编程的“宪法级”入口。它不抽象、不封装,逐行调用 libibverbs 原语:
// 1. 获取设备列表并选择第一个可用设备 struct ibv_device **dev_list = ibv_get_device_list(&num_devices); ctx = ibv_open_device(dev_list[0]); // 打开设备上下文 // 2. 创建保护域(Protection Domain),所有后续资源归属于此 pd = ibv_alloc_pd(ctx); // 3. 分配内存并注册为 MR(Memory Region) buf = memalign(page_size, BUFFER_SIZE); // 对齐到页边界! mr = ibv_reg_mr(pd, buf, BUFFER_SIZE, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ);关键参数说明:
IBV_ACCESS_REMOTE_WRITE:允许远端节点通过 RDMA WRITE 操作写入该内存;IBV_ACCESS_REMOTE_READ:允许远端通过 RDMA READ 读取;IBV_ACCESS_LOCAL_WRITE:本地 CPU 可写(否则 malloc 内存无法初始化内容);memalign(page_size, ...):必须页对齐,否则ibv_reg_mr()返回ENOMEM(即使内存足够);page_size 通常为 4096,可通过getpagesize()获取。
注意:
ibv_reg_mr()返回的mr->lkey用于本地操作(如ibv_post_send()中的wr.wr.ud.ah),mr->rkey则通过交换机制发给对端,用于远端构造rdma_write请求。二者不可混用——这是新手最常踩的“密钥错位”坑。
3.2 QP 创建与状态迁移:从 RESET 到 RTS 的三次握手本质
QP(Queue Pair)是 RDMA 数据通路的载体。rdma_bench中setup_qp()函数完整呈现了状态机迁移:
// 创建 QP(初始状态为 RESET) struct ibv_qp_init_attr attr = { .send_cq = cq, .recv_cq = cq, .cap = { .max_send_wr = 128, .max_recv_wr = 128 }, .qp_type = IBV_QPT_RC // 可靠连接模式 }; qp = ibv_create_qp(pd, &attr); // 迁移至 INIT 状态(需设置端口号和 PKey 索引) struct ibv_qp_attr init_attr = { .qp_state = IBV_QPS_INIT, .port_num = port, .pkey_index = 0 }; ibv_modify_qp(qp, &init_attr, IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT); // 迁移至 RTR(Ready to Receive),此时需填对端 LID/QPN struct ibv_qp_attr rtr_attr = { .qp_state = IBV_QPS_RTR, .path_mtu = IBV_MTU_4096, .dest_qp_num = remote_qpn, .rq_psn = 0, .max_dest_rd_atomic = 16, .min_rnr_timer = 12 }; ibv_modify_qp(qp, &rtr_attr, IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN | IBV_QP_RQ_PSN | IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER); // 最后迁至 RTS(Ready to Send) struct ibv_qp_attr rts_attr = { .qp_state = IBV_QPS_RTS, .timeout = 14, // 2^(timeout-1) * 4.096us = ~33ms .retry_cnt = 7, // 重试次数 .rnr_retry = 7, // RNR 重试次数 .sq_psn = 0, .max_rd_atomic = 16 }; ibv_modify_qp(qp, &rts_attr, IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC);这里没有 magic:IBV_QPS_RTR要求填写dest_qp_num(对端 QP 号),IBV_QPS_RTS要求设置timeout(超时重传基值)。timeout=14不是随便写的——它对应 2^(14-1) × 4.096μs ≈ 33ms,是 RoCE 网络中避免因交换机 buffer 满导致丢包的典型值。若网络延迟高(如跨机柜),需调大此值,否则大量RETRY EXC ERR错误。
3.3 发送与轮询:WR 封装、CQE 解析与 latency 测量精度控制
post_send_batch()函数是性能核心。它不使用ibv_post_send()单发,而是批量构造 WR(Work Request)并一次提交:
struct ibv_send_wr wrs[MAX_WR]; struct ibv_send_wr *bad_wr; for (int i = 0; i < batch_size; i++) { struct ibv_sge sge = { .addr = (uint64_t)buf + i * MSG_SIZE, .length = MSG_SIZE, .lkey = mr->lkey }; wrs[i] = (struct ibv_send_wr){ .wr_id = i, .sg_list = &sge, .num_sge = 1, .opcode = IBV_WR_SEND, // 或 IBV_WR_RDMA_WRITE .send_flags = IBV_SEND_SIGNALED // 关键!触发 CQE }; } ibv_post_send(qp, &wrs[0], &bad_wr); // 一次提交 batch_size 个 WRIBV_SEND_SIGNALED是精度命脉:它确保每个 WR 完成后生成一个 CQE(Completion Queue Entry),从而可精确测量每个消息的往返时间。若省略此 flag,CQE 只在队列满或显式ibv_req_notify_cq()时产生,latency 统计将严重失真。
轮询 CQE 的poll_cq()函数则直击 RDMA 实时性本质:
while (completed < expected) { int n = ibv_poll_cq(cq, MAX_CQE, wc); for (int i = 0; i < n; i++) { if (wc[i].status != IBV_WC_SUCCESS) { fprintf(stderr, "WC error %d for wr_id %lu\n", wc[i].status, wc[i].wr_id); return -1; } // 计算 latency:发送时刻戳 vs 完成时刻戳(高精度 clock_gettime(CLOCK_MONOTONIC)) uint64_t end_ts = get_cycles(); // 或 rdtsc() latencies[wc[i].wr_id] = end_ts - send_ts[wc[i].wr_id]; } completed += n; }get_cycles()若用rdtsc指令,需注意多核间 TSC 不同步问题——生产环境建议统一用clock_gettime(CLOCK_MONOTONIC, &ts)并校准时钟偏移。
4. 避坑指南:五个血泪换来的 RDMA 运行时故障排查清单
4.1 现象:ibv_create_qp()返回errno=22 (EINVAL)
原因:max_send_wr或max_recv_wr超过网卡硬件能力。Mellanox ConnectX-5 默认最大max_send_wr=32768,但若cap.max_inline_data > 0且申请过大 WR 数,部分固件会拒绝。
解决:先调用ibv_query_port()获取port_attr.max_msg_sz和port_attr.active_mtu,再按max_send_wr = min(1024, port_attr.max_qp_wr)安全设值;或直接设为128起步,逐步上调。
4.2 现象:客户端ibv_post_send()成功,但服务端ibv_poll_cq()永远收不到 CQE
原因:服务端未正确调用ibv_post_recv()预 posting 接收缓冲区。RC 模式下,接收方必须提前准备好recv_wr,否则发送方 WR 会被静默丢弃(无错误提示!)。
解决:检查服务端init_qp()是否包含ibv_post_recv(qp, &rr, &bad_rr)循环;确认rr.num_sge == 1且rr.sg_list->length≥ 发送方消息长度。
4.3 现象:ibv_reg_mr()失败,errno=12 (ENOMEM)
原因:内存未页对齐,或系统vm.max_map_count过低(尤其容器环境)。
解决:强制buf = memalign(getpagesize(), size);检查cat /proc/sys/vm/max_map_count,若 < 65536 则echo 262144 > /proc/sys/vm/max_map_count(临时)或写入/etc/sysctl.conf(永久)。
4.4 现象:latency 波动极大(从 1μs 到 1000μs),且伴随RETRY EXC ERR
原因:RoCE 网络中 PFC(Priority Flow Control)未启用,导致交换机 buffer 溢出丢包,触发重传。
解决:在交换机侧配置 PFC for priority 3(RoCE 默认优先级),并在服务器侧启用 DCB:dcbtool set eth0 pfc e:1 a:1 w:1 i:1(e=enable, a=autogroup, w=wdt, i=init)。
4.5 现象:ibv_modify_qp()迁移至 RTS 后,ibv_poll_cq()收到WC_RETRY_EXC_ERR
原因:timeout参数过小,或网络实际 RTT >timeout设置的理论上限。
解决:用ib_send_lat测基线 RTT,设timeout = ceil(log2(RTT_us / 4.096)) + 1;例如实测 RTT=50μs,则timeout = ceil(log2(50/4.096)) + 1 = ceil(log2(12.2)) + 1 = 4 + 1 = 5(对应 2^4×4.096≈65μs)。
5. 进阶技巧:定制化测试场景的三类实战改造法
5.1 场景一:测不同消息大小的吞吐拐点(非固定 4KB)
rdma_bench默认所有消息固定MSG_SIZE=4096。要测 64B~1MB 的吞吐变化,需动态分配 buffer 并修改 WR 构造逻辑。关键改动在main()中:
// 支持命令行传参 -m 64,256,1024,4096,16384 char *msg_sizes_str = getenv("MSG_SIZES"); if (msg_sizes_str) { char *tok = strtok(msg_sizes_str, ","); while (tok && num_sizes < MAX_SIZES) { msg_sizes[num_sizes++] = atoi(tok); tok = strtok(NULL, ","); } } // 在循环中为每个 size 构造独立测试批次 for (int s = 0; s < num_sizes; s++) { int cur_size = msg_sizes[s]; // 重新分配对齐内存 free(buf); buf = memalign(getpagesize(), cur_size * BATCH_SIZE); // 重注册 MR(注意:rkey 会变,需重新交换) ibv_dereg_mr(mr); mr = ibv_reg_mr(pd, buf, cur_size * BATCH_SIZE, IBV_ACCESS_FLAGS); // 重设 WR 的 sge.length for (int i = 0; i < BATCH_SIZE; i++) { sge[i].length = cur_size; } // 执行测试... }提示:每次
ibv_reg_mr()会生成新rkey,必须通过 socket 重新发给对端。rdma_bench原生不支持多 size,但上述结构只需 20 行代码即可扩展——我一般会在server.c中加一个send_rkey_to_client(int sockfd, uint32_t rkey)辅助函数。
5.2 场景二:注入可控丢包,验证重传恢复能力
RDMA 协议栈本身不提供丢包注入接口,但可在内核 TC 层实现。在服务端执行:
# 创建 qdisc root,添加 netem 丢包规则(仅对 RoCE UDP 流生效) tc qdisc add dev ib0 root handle 1: htb default 10 tc class add dev ib0 parent 1: classid 1:1 htb rate 100gbit tc qdisc add dev ib0 parent 1:1 handle 10: netem loss 0.1% 25% # 验证:此时 rdma_bench 客户端将看到 WC_RETRY_EXC_ERR 显著增加 # 通过监控 /sys/class/infiniband/*/ports/*/counters/port_xmit_data 可见重传字节数上升此法无需修改rdma_bench代码,纯外部干扰。注意:netem 丢包率需 ≤0.5%,否则 RC 模式可能因 RNR 超时进入 ERROR 状态,需重启 QP。
5.3 场景三:对接 perf 工具,采集硬件级事件(如rdma_read_req)
rdma_bench可与 Linuxperf深度集成,捕获网卡硬件事件。先确认网卡支持:
# 查询支持的 RDMA PMU 事件 perf list | grep rdma # 典型事件:rdma_read_req(发出的 RDMA READ 请求数)、rdma_write_comp(WRITE 完成数) perf stat -e 'rdma::rdma_read_req,rdma::rdma_write_comp' \ -a -C 0-3 -- ./rdma_bench -c 192.168.10.10输出示例:
12,345,678 rdma::rdma_read_req 98,765,432 rdma::rdma_write_comp这比用户态gettimeofday()测 latency 更底层——它告诉你网卡 MAC 层实际处理了多少 RDMA 操作。当发现rdma_write_comp远小于发送 WR 数,说明存在硬件级 backpressure,需检查 PCIe 带宽或网卡 firmware 版本。
从那以后我每次部署 RDMA 测试,都强制走一遍ibstat→ibv_devinfo→perf list | grep rdma三连检;哪怕只是跑个 hello world,也要先strace -e trace=ibv_* ./rdma_bench -s 2>&1 | head -20看一眼系统调用链是否完整。因为 RDMA 的“快”是建立在每层零误差之上的,漏掉一个ibv_post_recv(),整个流水线就静默瘫痪。希望帮到你。
本文还有配套的精品资源,点击获取