2、RDMA核心概念:QP、CQ、WR、WC、MR

好,咱们正式开始聊RDMA的核心概念。说实话,这几个概念是RDMA的基石,你搞懂了它们,后面写代码就是搭积木。我当年刚接触RDMA时,被这些缩写搞得一头雾水,后来在项目中踩了几个坑,才算真正理解它们之间的关系。

咱们一个一个来拆解。你想想看,RDMA的本质是什么?说白了就是让网卡直接读写远端内存,绕过CPU。那怎么实现这个“直接读写”呢?就需要一套通信机制。这套机制的核心就是下面这五个东西。

2.1 Queue Pair(QP)——通信的“管道”

QP是RDMA通信的基本单元。你可以把它想象成一根双向管道。一端是发送端,一端是接收端。每个QP包含两个队列:

  • Send Queue(SQ):发送队列,存放你要发出去的数据请求。
  • Receive Queue(RQ):接收队列,存放你准备接收数据的缓冲区。

嗯,这里要注意:QP是一对的。你创建一个QP,必须和远端的另一个QP建立连接,才能通信。就像打电话,你得拨号到对方,才能通话。

关键点:每个QP都有一个唯一的QP编号(QP Number)。通信双方通过这个编号来识别对方。我在项目中遇到过一个问题,就是QP编号冲突导致连接失败,后来排查了半天才发现是编号分配逻辑写错了。

QP的状态机也很重要。它有几个关键状态:

状态 说明
RESET 初始状态,啥也不能干
INIT 初始化完成,可以修改属性
RTR 准备好接收数据
RTS 准备好发送数据,可以正常通信了
SQD 发送队列被清空,但接收还能用
SQE 发送队列出错,需要重置
ERR 出错状态,所有操作都停了

我个人习惯是,在代码里把QP的状态转换写成一个状态机,这样调试起来特别清晰。你想想看,如果QP状态不对,你发再多的数据也是白搭。

2.2 Completion Queue(CQ)——操作的“回执”

CQ是用来通知你操作完成的地方。你提交了一个Work Request(WR)到QP,网卡处理完后,会把结果放到CQ里。你从CQ里取出Work Completion(WC),就知道操作成功了还是失败了。

说白了,CQ就是一个完成通知队列。它和QP是解耦的。一个CQ可以服务于多个QP,一个QP也可以关联多个CQ(比如发送完成用一个CQ,接收完成用另一个CQ)。

我的建议:刚开始做项目时,我习惯把发送和接收的完成事件放到同一个CQ里。后来发现这样调试起来有点乱,尤其是高并发场景下。现在我更倾向于为每个QP分配独立的CQ,或者至少把发送和接收分开。这样出问题时,一眼就能看出是哪个方向出了问题。

CQ的深度(即能容纳多少个WC)需要提前设置好。设置得太小,网卡处理完操作后没地方放完成通知,就会丢事件。设置得太大,又浪费内存。我一般根据QP的并发深度来估算,再留个20%的余量。

2.3 Work Request(WR)——你要干啥

WR就是你告诉网卡“我要干什么”的指令。它包含了你想要执行的操作类型、数据地址、长度等信息。常见的WR类型有:

  • IBV_WR_SEND:发送数据
  • IBV_WR_RDMA_WRITE:直接写远端内存
  • IBV_WR_RDMA_READ:直接读远端内存
  • IBV_WR_ATOMIC_CMP_AND_SWP:原子比较并交换
  • IBV_WR_ATOMIC_FETCH_AND_ADD:原子取并加

你提交WR到QP的Send Queue或Receive Queue。网卡从队列里取出WR,然后执行。执行完后,生成一个WC放到CQ里。

我记得有一次,我提交了一个RDMA WRITE请求,但忘了设置正确的内存地址偏移,结果写到了远端进程的堆栈里,导致对方进程直接崩溃。嗯,从那以后,我对WR里的地址参数检查得特别仔细。

2.4 Work Completion(WC)——干完了,结果在这

WC是网卡给你的反馈。它告诉你:你之前提交的那个WR,执行得怎么样了。WC里包含的信息有:

  • 操作状态(成功、失败、超时等)
  • 操作的字节数
  • 对应的QP编号
  • 用户自定义的上下文(比如你传进去的指针)

你从CQ里取出WC,检查状态。如果成功,就继续下一步。如果失败,就得根据错误码去排查问题。

避坑指南:我曾经犯过一个错误,就是只检查WC的状态,没检查它对应的QP是不是我期望的那个。在高并发场景下,多个QP共享一个CQ,你取出来的WC可能来自不同的QP。如果不做区分,就会把A QP的成功当成B QP的成功,导致逻辑混乱。所以,一定要检查WC里的QP编号。

2.5 Memory Region(MR)——内存的“通行证”

MR是RDMA里最核心的概念之一。你想让网卡直接读写你的内存,必须先注册这块内存。注册后,网卡会得到这块内存的物理地址访问权限。同时,你会得到一个lkey(本地key)和rkey(远端key)。

lkey用于本地操作(比如本地DMA),rkey用于远端操作(比如远端RDMA READ/WRITE)。你想想看,如果没有这个机制,网卡随便读写内存,那系统早就崩溃了。

注册MR时,你需要指定:

  • 内存起始地址
  • 内存长度
  • 访问权限(本地读/写、远端读/写、原子操作等)

重要提示:注册MR时,内存必须是固定的(即不能被换出到磁盘)。所以,你通常需要调用mlock()mlockall()来锁定内存页。我在项目中遇到过一个问题,就是忘了锁定内存,结果在高负载下,内存被换出,网卡访问时直接报错。排查了好久才发现是这个问题。

另外,MR的注册和注销是有开销的。每次注册都需要内核参与,做页表映射、权限检查等操作。所以,不要频繁注册和注销MR。我一般会在程序启动时,预先注册好一大块内存,然后自己管理这块内存的分配和释放。这样能显著提升性能。

2.6 它们之间的关系

好,咱们把这五个概念串起来。整个流程是这样的:

  1. 你注册一块内存,得到MR(拿到lkey和rkey)。
  2. 你创建一个QP(拿到一个通信管道)。
  3. 你创建一个CQ(拿到一个完成通知队列)。
  4. 你把QP和CQ关联起来。
  5. 你构造一个WR,告诉网卡:用MR里的数据,通过QP,发给远端。
  6. 你把WR提交到QP的Send Queue。
  7. 网卡从SQ里取出WR,执行操作。
  8. 操作完成后,网卡生成一个WC,放到CQ里。
  9. 你从CQ里取出WC,检查操作结果。

说白了,就是:你告诉网卡干啥(WR),网卡通过管道(QP)干活,干完了通知你(WC),通知放在信箱里(CQ),干活用的数据放在注册过的内存里(MR)

这个流程,你写代码时天天都会用到。我刚开始写RDMA程序时,总是搞混WR和WC的关系。后来我给自己编了个口诀:“WR是命令,WC是回执,QP是通道,CQ是信箱,MR是地盘”。你试试看,是不是好记多了?

下一章,咱们会深入聊QP的创建和配置,包括那些让人头疼的QP属性。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。