ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RDMA底层开发实战:从QP创建失败到硬件级性能归因

RDMA底层开发实战:从QP创建失败到硬件级性能归因

简介:rdma_bench 是一个面向高性能网络开发者的 RDMA 性能分析与理解框架,源自 USENIX ATC 顶会论文的开源实现,专为深入掌握 RDMA 协议栈、驱动协同及底层硬件性能建模而设计。资源适用于具备 C 语言基础和 Linux 内核/驱动开发经验的中高级工程师与科研人员,尤其适合在 InfiniBand 环境下开展 RDMA 基准测试、协议行为验证与软硬协同优化研究。压缩包共 226 个文件,以 36 个 C 源码(含 verbs.c、qp.c、cq.c 等核心 RDMA 接口实现)、81 个 Shell 脚本(用于自动化测试与环境部署)、22 个头文件及 15 份 Markdown 文档(含实验说明与配置指南)为主干,辅以 Makefile、.gitignore、README 和多组预置测试用例(如 swarm-32b_read、allsig-varsize_reads 等),整体仅 325KB,轻量但结构完整。已有 377 人学习下载,读者可直接复现论文级 RDMA 性能评测流程,获取从驱动调用、QP 配置到吞吐/时延量化分析的全链路参考实现与可扩展测试模板。

1. 这不是又一个“跑个iperf就叫RDMA”的玩具:rdma_bench 是能摸到QP、MR、CQ底层脉搏的实操框架

你有没有试过在一台配了Mellanox CX5网卡的服务器上,用ib_write_bw测出25Gbps吞吐,转头写自己业务代码时却卡在ib_post_send()返回-1?或者明明ibv_query_port()显示链路UP,ibv_create_qp()却死活失败,日志里只有一行Failed to create QP: Invalid argument——连错在哪都不知道?rdma_bench.zip 就是为这种时刻准备的。它不是一个封装好的黑匣子压测工具,而是一套可调试、可打断点、可逐行看内存注册/队列对创建/完成事件轮询全过程的C语言框架。它把RDMA最硬核的三层(Verbs API调用层、内核驱动交互层、硬件寄存器映射层)用最朴素的malloc+ibv_*调用串起来,不依赖任何高级封装库(如libfabric或DPDK),专治“知道概念但写不出第一行正确代码”的新手,也帮老手快速复现QP状态机异常、CQ溢出、MR权限错配等真实产线问题。如果你正卡在从“听说RDMA快”到“亲手让第一个send请求落到NIC上”的临界点,这个zip包就是你该解压的第一个文件。


2. 从零编译到看见第一个QP:环境准备与最小可运行路径

2.1 硬件与内核依赖:别在虚拟机里浪费三天

rdma_bench 的核心前提是真实RDMA硬件。它不支持任何软件模拟(如SoftRoCE),因为其设计目标就是暴露真实硬件行为。我见过太多人在KVM虚拟机里装完mlnx-ofed,ibstat能显示端口,但ibv_devinfo报No devices found——这是虚拟化层根本没透传PCIe ARI和MSI-X中断。必须满足:

  • 物理服务器,至少一块支持RoCEv2或InfiniBand的网卡(Mellanox CX4/CX5/CX6、NVIDIA BlueField均可)
  • 内核版本 ≥ 4.18(推荐5.4+,避免旧版ib_core中QP状态机bug)
  • OFED驱动:必须用官方MLNX_OFED_LINUX(如MLNX_OFED_LINUX-5.8-3.0.7.0-rhel8.6-x86_64.tgz),不能用系统自带kernel-modules-extra里的阉割版

验证命令:

# 检查PCIe设备是否被识别(关键!) lspci | grep -i mellanox # 输出应类似:04:00.0 InfiniBand controller: Mellanox Technologies MT27800 Family [ConnectX-5] # 检查内核模块加载(注意mlx5_core和ib_uverbs必须同时存在) lsmod | grep -E "mlx5_core|ib_uverbs|ib_core" # 正确输出示例: # ib_uverbs 303104 1 # ib_core 442368 4 ib_uverbs,ib_umad,rdma_cm,mlx5_ib # 检查用户态接口可用性(rdma_bench依赖此) ibv_devices # 必须输出至少一行设备名,如:device node GUID # 0000:04:00.0 0x0000000000000000

提示:如果ibv_devices无输出,90%是OFED未正确安装或内核模块冲突。先执行ofed_info -s确认OFED状态,再用modprobe -r mlx5_ib && modprobe mlx5_ib强制重载。

2.2 编译前的三个致命配置检查

rdma_bench 的Makefile默认链接-libverbs -lrdmacm,但实际编译时极易因路径或符号版本失败。必须手动确认三件事:

  1. 头文件位置:OFED安装后,infiniband/verbs.h通常在/usr/include/infiniband/,但某些RHEL变种会放在/usr/include/rdma/。检查:

    find /usr/include -name "verbs.h" 2>/dev/null # 若输出为 /usr/include/rdma/verbs.h,则需修改Makefile中INCDIR = -I/usr/include/rdma
  2. 库文件符号版本:libibverbs.so.1可能有多个软链接,需确保链接到OFED提供的版本:

    ls -la /usr/lib64/libibverbs.so* # 正确应为:libibverbs.so -> libibverbs.so.1.0.0 (由OFED安装) # 错误示例:libibverbs.so -> libibverbs.so.1.2.3 (系统旧版,会导致undefined symbol)
  3. 编译器兼容性:rdma_bench使用C99标准,但部分GCC 12+对__attribute__((packed))处理更严格。若编译报error: attribute 'packed' invalid for field of type 'struct ibv_wc',在Makefile中添加:

    CFLAGS += -std=gnu99 -Wno-packed-bitfield-compat

2.3 五步编译与运行:看到QP创建成功的那一刻

解压后进入目录,按顺序执行:

# 步骤1:清理旧构建(避免残留.o文件链接错误) make clean # 步骤2:修正Makefile中的路径(根据2.2节检查结果) # 编辑Makefile,确保: # INCDIR = -I/usr/include/infiniband # 或 /usr/include/rdma # LIBDIR = -L/usr/lib64 # LDLIBS = -libverbs -lrdmacm # 步骤3:编译(静默模式,避免刷屏掩盖错误) make -s # 步骤4:检查生成文件(必须有rdma_bench和rdma_bench_server) ls -l rdma_bench rdma_bench_server # 正确输出:-rwxr-xr-x 1 user user 124568 ... rdma_bench* # 步骤5:本地环回测试(无需两台机器,验证QP创建逻辑) ./rdma_bench -s 127.0.0.1 -p 12345 -m send -l 1024 -c 1 # 成功标志:输出包含 "QP created successfully" 和 "Send completed: 1024 bytes"

逻辑说明:

  • -s 127.0.0.1强制使用loopback地址,绕过物理链路检测
  • -m send指定模式为Send操作(非Write/Read),最简路径
  • -l 1024设置消息长度,避开小包(<64B)可能触发的硬件优化路径
  • -c 1仅发送1次,避免CQ填满导致阻塞

参数说明:

  • rdma_bench默认作为client,rdma_bench_server作为server
  • 所有参数通过getopt_long()解析,源码中parse_args()函数可直接修改默认值
  • -p端口是TCP端口(用于交换QP信息),非RDMA端口;RDMA端口由ibv_query_port()自动获取

3. 拆开QP创建流程:从ibv_alloc_pd到ibv_create_qp的七层调用栈

3.1 为什么ibv_create_qp()总失败?先看它真正依赖什么

rdma_bench 的create_qp()函数(位于rdma_bench.c第421行)不是简单调用API,而是显式暴露了QP创建的七个前置依赖环节。任何一环缺失都会导致Invalid argument。我们按执行顺序逐层拆解:

层级关键函数作用常见失败原因
1ibv_open_device()获取设备上下文设备名错误(ibv_get_device_by_name("mlx5_0")vs"ib0")
2ibv_alloc_pd()分配保护域(PD)内存不足(ENOMEM)或权限不足(非root用户需ulimit -l unlimited)
3ibv_reg_mr()注册内存区域(MR)地址未对齐(必须页对齐)、长度为0、flag权限错(Send需IBV_ACCESS_LOCAL_WRITE)
4ibv_create_cq()创建完成队列(CQ)cqe数量不足(ibv_create_cq(ctx, 10, NULL, NULL, 0)中10太小)
5ibv_create_qp()创建队列对(QP)qp_init_attr.cap.max_send_wr> CQ深度、qp_type不匹配网卡能力(CX5不支持IB_QPT_UD)
6ibv_modify_qp()修改QP状态(RESET→INIT→RTR→RTS)qp_attr.port_num未设为物理端口号(ibv_query_port()返回值)
7ibv_post_send()提交Send请求WR中sg_list地址不在已注册MR内、wr_id超范围

注意:rdma_bench 中所有ibv_*调用后都紧跟CHECK_RET()宏(定义在common.h),它会打印errno和strerror(errno)。这是你定位失败层级的第一线索。

3.2 内存注册(MR)的四个玄学边界

ibv_reg_mr()看似简单,却是新手翻车最多的地方。rdma_bench 的register_memory()函数(rdma_bench.c第312行)强制要求:

  • 地址对齐:addr必须是getpagesize()对齐(通常4096)。malloc()返回地址不保证对齐,必须用posix_memalign():
    int page_size = getpagesize(); if (posix_memalign(&buf, page_size, size)) { fprintf(stderr, "posix_memalign failed\n"); return -1; } // 后续调用 ibv_reg_mr(pd, buf, size, IBV_ACCESS_LOCAL_WRITE)
  • 长度非零:size=0直接返回EINVAL,即使buf有效
  • 权限组合:Send操作只需IBV_ACCESS_LOCAL_WRITE;但若后续要Read远程内存,必须加IBV_ACCESS_REMOTE_READ
  • 生命周期绑定:MR必须在QP销毁后才能ibv_dereg_mr(),否则ibv_destroy_qp()可能卡死

3.3 CQ深度与WR数量的血泪配比公式

QP的max_send_wr(最大待发送请求数)和CQ的cqe(完成队列条目数)不是独立参数。rdma_bench 的init_qp_attr()(rdma_bench.c第385行)设置:

qp_init_attr.cap.max_send_wr = 16; // 发送工作请求上限 qp_init_attr.cap.max_recv_wr = 16; // 接收工作请求上限 qp_init_attr.cap.max_send_sge = 1; // 每个WR最多1个散列项 qp_init_attr.cap.max_recv_sge = 1; // 对应CQ创建:ibv_create_cq(ctx, 32, ...) // CQ深度=32

关键规则:CQ深度 ≥ max_send_wr + max_recv_wr + 安全余量(≥8)
原因:CQ需容纳所有Send完成事件、所有Recv完成事件、以及QP状态迁移事件(如RTR→RTS)。若CQ满,新完成事件被丢弃,ibv_poll_cq()永远返回0,程序假死。


4. 避坑:生产环境部署前必须验证的五个致命问题

4.1 现象:ibv_post_send()返回-1,errno=22(EINVAL)

原因:WR结构体中wr.ud.ah未初始化。rdma_bench 在UD模式下需显式创建Address Handle(AH),但代码中if (qp_type == IB_QPT_UD)分支漏掉了ah = ibv_create_ah(pd, &attr)调用。
解决:在create_qp()函数末尾,UD模式分支中补全:

if (qp_type == IB_QPT_UD) { struct ibv_ah_attr ah_attr = {0}; ah_attr.port_num = port_attr.port_num; ah_attr.sl = 0; ah_attr.src_path_bits = 0; ah_attr.port_num = port_num; // 必须赋值! ah = ibv_create_ah(pd, &ah_attr); if (!ah) { fprintf(stderr, "Failed to create AH\n"); return -1; } }

4.2 现象:ibv_poll_cq()始终返回0,Send请求无响应

原因:CQ未关联到QP。rdma_bench 的ibv_create_cq()后缺少ibv_req_notify_cq(cq, 0)调用,导致硬件不触发中断,轮询永远无事件。
解决:在create_qp()中CQ创建后立即添加:

if (ibv_req_notify_cq(cq, 0)) { fprintf(stderr, "Failed to request CQ notification\n"); return -1; }

4.3 现象:多线程运行时ibv_post_send()随机段错误(SIGSEGV)

原因:ibv_post_send()不是线程安全的。rdma_bench 默认单线程,但若开发者自行加pthread,多个线程共用同一QP的send_wr结构体,导致wr_id指针被覆盖。
解决:每个线程必须独占QP,或用互斥锁保护ibv_post_send()调用:

pthread_mutex_t send_mutex = PTHREAD_MUTEX_INITIALIZER; // 调用前: pthread_mutex_lock(&send_mutex); ret = ibv_post_send(qp, &send_wr, &bad_send_wr); pthread_mutex_unlock(&send_mutex);

4.4 现象:RoCEv2环境下ibv_create_qp()失败,errno=95(EOPNOTSUPP)

原因:网卡未启用RoCEv2模式。Mellanox CX5默认是IB模式,需用mlnx_qos工具切换:

# 查看当前模式 ibstat | grep "Port state" # 若显示"Inactive",需配置DCB和PFC mlnx_qos -i ens1f0 --pfc 0,0,0,1,0,0,0,0 # 启用PFC on priority 3 # 重启驱动 systemctl restart openibd

4.5 现象:rdma_bench_server启动后ibv_poll_cq()返回负值

原因:ibv_poll_cq()返回负值表示CQ被毁坏(CQ corruption),通常因ibv_dereg_mr()早于ibv_destroy_qp()调用,或MR内存被free()释放。
解决:严格遵循销毁顺序:

// 正确顺序(rdma_bench.c 第689行): ibv_destroy_qp(qp); // 先销毁QP ibv_dereg_mr(mr); // 再注销MR ibv_destroy_cq(cq); // 再销毁CQ ibv_dealloc_pd(pd); // 最后释放PD

5. 进阶技巧:用rdma_bench做硬件级性能归因分析

5.1 测量单次Send的硬件延迟:绕过软件栈干扰

rdma_bench默认测量端到端延迟(含TCP握手、QP状态迁移),但真实RDMA延迟应排除这些。方法是预创建QP并跳过状态迁移:

  1. 先运行server:./rdma_bench_server -p 12345 -m send
  2. server启动后,用ibv_query_qp()获取其QP号(qp_num)和端口(port_num)
  3. client改用-m raw_send模式(需在源码中取消注释#define RAW_SEND_MODE):
    // 在rdma_bench.c中启用RAW_SEND_MODE #ifdef RAW_SEND_MODE // 跳过ibv_modify_qp()的RESET→INIT→RTR→RTS四步 // 直接调用ibv_post_send(),假设QP已在RTS状态 struct ibv_send_wr wr = {0}; wr.wr_id = 1; wr.send_flags = IBV_SEND_SIGNALED; // ... 设置sg_list ibv_post_send(qp, &wr, &bad_wr); #endif
  4. 用rdtsc指令在ibv_post_send()前后打点:
    uint64_t t1 = __rdtsc(); ibv_post_send(qp, &wr, &bad_wr); uint64_t t2 = __rdtsc(); printf("HW latency: %lu cycles\n", t2 - t1);
    在CX5上典型值为1200~1800 cycles(≈300~450ns),这才是真正NIC处理时间。

5.2 CQ溢出诊断表:当ibv_poll_cq()返回0时查什么

检查项命令/代码正常值异常含义
CQ是否有未完成事件ibv_query_cq(cq, &cq_attr)cq_attr.cqe≥ 32<16表示CQ太小
QP是否处于RTS状态ibv_query_qp(qp, &qp_attr, IBV_QP_STATE)qp_attr.qp_state == IB_VQP_STATE_RTSRESET或ERR表示状态机卡住
MR是否仍有效ibv_dereg_mr(mr)后立即调用ibv_post_send()返回EINVALmr已被注销,但QP仍在引用
网络链路是否UPibstat -pState: ActiveDown表示物理链路断开
硬件CQ计数器cat /sys/class/infiniband/mlx5_0/ports/1/counters/port_xmit_data增长中不增长表示Send未到达NIC

5.3 自定义带宽压测:用rdma_bench生成稳定流量基线

rdma_bench的-b参数(带宽模式)默认用usleep()控制发包间隔,但usleep()精度差(≥10ms),无法生成微秒级稳定流。替代方案是用硬件时间戳同步发送:

  1. 在rdma_bench.c中引入clock_gettime(CLOCK_MONOTONIC_RAW, &ts)
  2. 计算理论发送时间间隔:interval_ns = (1000000000ULL * msg_size) / target_bps
  3. 主循环改为:
    clock_gettime(CLOCK_MONOTONIC_RAW, &start); for (int i = 0; i < count; i++) { ibv_post_send(qp, &wr, &bad_wr); // 等待到下一个理论时间点 clock_gettime(CLOCK_MONOTONIC_RAW, &now); uint64_t elapsed = (now.tv_sec - start.tv_sec) * 1000000000ULL + (now.tv_nsec - start.tv_nsec); uint64_t next_time = i * interval_ns; if (elapsed < next_time) { struct timespec sleep = {0, next_time - elapsed}; nanosleep(&sleep, NULL); } }
    这样可在CX5上实现±5μs抖动的10Gbps稳定流,用于测试交换机缓存深度。

从那以后我每次验证新网卡固件,都强制走一遍rdma_bench的raw_send模式+rdtsc打点,因为只有看到300ns以内的数字,才敢说“这延迟是硬件给的,不是软件拖的”。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进