使用 RDMA Send 操作时的一个坑

毕设还差一周就要 DDL 了。由于之前摸鱼划水过于严重,这几天花了很长时间去调毕设中的 bug,发现踩了一些奇妙的坑。这篇文章就是为了记录其中一个:使用 RDMA Send 操作时必须 poll 掉对应的 CQE,否则会出问题。

我毕设做的是一个基于 NVM 和 RDMA 的分布式存储。当我写好之后扔上服务器去测,发现它只能完成很少量的 I/O 请求;之后如果再进行操作,就会报 -ENOMEM 失败。测试场景非常简单,就是先进行若干次 4KB 写,然后再进行同样次数的 4KB 读。当然,写必须要在读之前,否则在确认元数据的阶段就会报错。

问题一开始总是出在读数据的时候。读数据的流程是这样的:客户端节点先向元数据服务器发出 RPC,拿到文件元数据;之后向几个存储文件数据的服务器发出 RDMA Read,取得一些数据块;最后从这些数据块中解码出用户需要的 4KB 原始数据。

用于铺垫的 RDMA 简介

为了方便接下来的说明,有必要先简单介绍一下 RDMA 的通信机制。RDMA,全称远程直接内存访问(Remote Direct Memory Access),是一种高性能的网络通信手段;它的一大特点就是可以在用户态进行通信,这就减少了很多内核态和用户态之间上下文切换和数据复制的开销。

但是,也正因为 RDMA 运行在用户态,应用程序在使用 RDMA 的时候,没有办法写成事件驱动的模式。虽然 RDMA 事实上提供了一些事件驱动的接口,例如 ibv_get_cq_event,对它的调用会导致当前进程睡眠并在有新的 RDMA 事件时被唤醒;不过这样一来,我们就重新引入了上下文切换开销,因此编写高性能程序的时候基本上还是不要使用。总而言之,RDMA 请求发出后,用户为了得知请求的状态,必须轮询一个消息队列,直到收到 RDMA 设备的消息。

因此 RDMA 的工作流程一般是这样:用户首先与远端建立 RDMA 连接,构造一个被称作 QP(Queue Pair)的东西,这个东西可以理解为 TCP/IP 协议栈中的 socket。一个 QP 包含一个发送队列(Send Queue,SQ)和一个接收队列(Receive Queue,RQ),SQ 和 RQ 又各自关联一个完成队列(Completion Queue,CQ)。现在,假如说我要和远端进行一次通信,那么我应该:

  1. 准备好要发送的内容
  2. 发一个 Send WR(Work Request)到 SQ 中
  3. 轮询 SQ 关联的 CQ,得到一个 CQE(Completion Queue Entry),确认 WR 被发送出去
  4. 准备好一个 buffer 用来接收回复
  5. 发一个 Receive WR 到 RQ 中
  6. 轮询 RQ 关联的 CQ,得到一个 CQE,确认 buffer 收到远端的回复内容

这样就完成一次 RDMA 的双向通信。远端要做的操作也是类似的,只不过是先做 4 ~ 6 步再做 1 ~ 3 步。

RDMA 分为单向操作(Read、Write)和双向操作(Send、Receive),其中 Read、Write 和 Send 使用同一个接口,因此以下统称为 Send。

有的时候我们希望 RDMA 默默地帮我们处理好某个请求,处理完了不要产生对应的 CQE。这是可以实现的,通过调整 QP 和 WR 的参数就可以做到。

我的毕设在一开始就是这么实现的。为了方便调试,我的读写请求都设定为不产生 CQE,这样我也就永远不用轮询 CQ。结果就出现了一开始所说的那个问题。

为了改掉这个问题,我花了很长时间,却没有取得什么成果。后来我选择先放弃,把 I/O 总量调小,测了几组数据过了中期答辩,继续写了几周代码。接下来,我意识到读(RDMA Read)的时候必须要轮询 CQ,否则可能还没读到东西就返回了;于是在 RDMA Read 的时候加上了轮询代码。

结果在轮询的时候卡死了。

我实在是百思不得其解,于是去掉轮询,试着统计了一下报 -ENOMEM 之前的 RDMA 操作数;结果这一统计就发现了问题,报错之前成功发出的 RDMA 请求数量正好等于我申请的 SQ 队列长度。

也就是说我发的请求都留在了 SQ 里面没有被清理掉。

按理说这是不该的,RDMA 设备应该会自动消化掉它们。于是我在远端输出了一下内存内容,发现一个震惊的事实:我最开始发出的那些 RDMA 写请求根本没有到达远端!甚至我把远端的内存权限改掉,改成不允许 RDMA 读写,这边发出的 RDMA 读写也不会报错。

在这之后我又做了各种无谓的尝试,都没有什么卯月……直到我试着让所有的 RDMA 读写请求都产生 CQE,并且在发出 RDMA Write 后也进行轮询,问题突然就解决了

对此我确实没有什么感想。虽然白白浪费了我一个周末的人生,但是这就是那个吧,虽然吃第六个馒头吃饱了,但是又没办法只吃第六个馒头。

总结

以上经验表明:

  • 如果设置 RDMA Send 不产生 CQE,那么它们可能会在 SQ 中堆积
  • 如果只让一部分 RDMA Send 产生 CQE,而在这之前有另外一些不产生 CQE 的 RDMA Send 请求,那么轮询 CQ 时可能永远轮询不到任何东西
  • 最好让所有 RDMA Send 都产生对应的 CQE,然后随发随轮询