一种安全的 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 生命周期和错误语义上的不足。