一种安全的 RDMA Buffer 复用策略

2025.11.19
SF-Zhou

使用 RDMA Read/Write 时,首先需要交换已经注册过的 Buffer 地址,提交 Read/Write 任务,等待完成事件。为了尽可能的提高性能,一般不会每次都申请 Buffer 再去注册,而是一开始就准备好一批注册好的 Buffer,使用时从池子里取出,用完放回。但当意外发生时,例如请求超时了,此时如何处理这个 Buffer 就会成为一个麻烦的问题。

回顾一下 3FS 中的处理方式:

  1. 所有 RDMA 操作都是由 Server 端发起,读 chunk 操作会执行 Server 端到 Client 端的 RDMA Write 操作,写 chunk 操作会执行 Server 端向 Client 端的 RDMA Read 操作;
  2. 当 Client 端请求超时时,立即关闭当前通信的 RDMA 连接,也就阻止了这条连接上 Server 端后续可能的 RDMA Write/Read 操作,此时 Client 端的 Buffer 就可以安全的被复用、而不至于被远端的 RDMA 操作默默修改掉了。

这样做问题也很明显,当出现请求超时时,连接会断掉,下一轮请求会复用其他连接或者建立新连接,更容易触发下一轮的超时。有什么更好的办法吗?

仔细分析一下 RDMA Write/Read 的流程。首先 RDMA Write 操作,当本地将 Buffer 地址传播给远端后,远端如果准备执行 RDMA Write 操作,那么就有默默改变本地 Buffer 内容的能力,反注册或者断连接都是为了阻止这一步,如果这两种方法都不想使用,可以直接考虑不使用 RDMA Write。

再来看 RDMA Read,本地将 Buffer 地址传播给远端后,远端如果准备执行 RDMA Read,那么本地就必须保持 Buffer 处于安全、不被修改的状态,如果因为超时回收了 Buffer、后续其他操作修改了 Buffer 内容,远端不知道这件事,远端后续的数据正确性可能就出问题。换句话说,让远端知道这件事就行。

所以这里提出的安全复用策略就是:禁用 RDMA Write,RDMA Read 成功后向远端确认 Buffer 还未修改。下面详细介绍本地读远端的步骤:

本地 远端
1. 发起 RPC
2. 收到 RPC,准备好 Buffer,返回 Buffer 地址
3. 发起 RDMA Read,等待完成
4. 发起 RPC,确认刚才的 Buffer 没有被修改
5. 收到 RPC,检查 Buffer 是否已经被复用,返回结果,同时可以安全的释放 Buffer 了
6. 收到检查结果。如果已经被复用,则本轮通信失败,可以重试

远端读本地的步骤:

本地 远端
1. 准备好 Buffer,发起 RPC,附带 Buffer 地址
2. 收到 RPC,发起 RDMA Read,等待完成,回包
3. 收到回包,检查 Buffer 是否已经被复用,发起 RPC 通知远端检查结果
4. 收到 RPC,如果一切正常则继续后续步骤并回包,反之则放弃回复失败
5. 收到远端结果

通过每次 RDMA Read 完成后,使用第二次 RPC 向对方确认 Buffer 是否已经被复用,来避免读到脏数据的可能。代价就是会多出一次 RTT。至于处理流程,则可以通过 Server 端给 Client 发反向 RPC 来简化,Server 端的处理步骤可以放到一个协程里顺序执行,3FS RDMA Control 有类似的案例(实际上它也会增加一次 RTT😉)。

本质上,这套方案是在 One-sided RDMA + Two-sided RPC 的组合里,用 RPC 来弥补 RDMA 在 buffer 生命周期和错误语义上的不足。

进一步的讨论

笔者坚持不使用 RDMA Write 的另一个原因是,远端并发的 RDMA Write 会很容易触发网络上的拥塞。只有 RDMA Read 的话可以很方便地在应用层做 Receiver Driven 的流量控制,避免网络层拥塞。在 3FS 文件系统、KVCache 存储系统上都观察到这一现象:单靠网络层自身的拥塞控制算法是无法避免网络拥塞的。

所以本文中提到的策略实际上是在应用层上解决这两个问题:RDMA Buffer 的生命周期管理和网络拥塞控制。在应用层上做这两件事有一些好处:对 RDMA 网卡本身基本没有要求,不需要强制断掉 QP。

如果你的网络硬件环境很好,例如全 Mellanox 网卡,那么也可以试试我同事教我的另外一招:考虑使用硬件级的 Memory Window 来解决 RDMA Buffer 的生命周期管理问题。同样以本地读远端为例:

本地 远端
1. 发起 RPC
2. 收到 RPC,准备好 Buffer,Bind MW 同时返回 Response
3. 发起 RDMA Read,附带授权的 RKey,等待完成,返回结果
4. 异步地发起 RPC,通知远端 Invalidate MW
5. 收到 RPC,提交一个 Invalidate MW 的 WR。如果指定时间内没有收到 Client 发送的 Invalidate RPC 请求,Server 端也需要提交 Invalidate MW 的 WR,完成后 Buffer 就可以复用了

Type 2 的 Memory Window 可以将 Bind MW 操作和 RDMA Send 操作一并提交,这样这个请求过程只需要一次 RTT(Invalidate RPC 是异步的,不占用请求的延迟),延迟可以做的更低。极端情况下 Client 还没有来得及进行 RDMA Read,Server 已经超时并且 Invalidate MW 了,那么 Client 的 RDMA Read 操作会因为 MW 已经失效而失败,不会读到脏数据。同时 QP 会陷入错误的状态,需要重新建立连接。

如果你的网络环境不支持 Type 2 的 Memory Window(例如阿里云),本文提到的应用层的 Buffer 管理也仍然是一条可行的退路。笔者是希望将这两套策略都实现在 RPC 框架里,开箱即可用,用户可以根据不同的网络环境自行配置。当然我现在还没有时间完成它:https://github.com/SF-Zhou/ruapc