Rust RDMA 编程「四、收发控制」

2024.07.31
SF-Zhou

1. 背景

RDMA 编程中,常用的通信操作有 Read/Write 和 Send/Recv。前者是单向操作,即发起端可以直接读写远端的内存,不需要远端的 CPU 参与;后者是双向操作,发起端 Send 需要搭配远端 Recv,类似 TCP 通信。Read/Write 使用上会更简单,但在发起操作前需要知道远端内存的地址,所以正常情况下无法单独使用,一般需要搭配 Send/Recv 实现的控制流,才能完成 Read/Write 实现的数据流。

所以实现高性能的 RDMA 通信仍需要先实现高性能的 Send/Recv。但与 TCP 通信不同的是,RDMA Send 一定需要远端已经发起了 Recv 操作,否则 Send 操作会直接失败。这就需要实现一套良好的收发控制策略,使得 Send 操作可以高效且安全地被提交。

2. 策略

整理 Send 相关的限制条件:

  1. 本地 Send Queue 的 max_send_wr,限制提交到该发送队列的 WR 的最大数量,WR 的类型包含 Send/Read/Write 操作
  2. 远端 Recv Queue 的 max_recv_wr,以及提交的未完成的 Recv 操作数量
  3. 本地 Completion Queue 的大小 max_cqe,如果完成的 WR 累积在完成队列中没有被消费也会引发错误

限制条件简化:

  1. 约定本地 Completion Queue 的 max_cqe = max_send_wr + max_recv_wr,这样即使当前所有提交的任务都完成了,也不会使得完成队列溢出
  2. 对 Send Queue 的 max_send_wr 进行容量上的拆分,分为 max_send_data_wrmax_send_imm_wrmax_read_wrmax_write_wrmax_send_data_wr 用于发送数据,max_send_imm_wr 用于发送立即数,并且 max_recv_wr = max_send_data_wr + max_send_imm_wr,这样即使提交所有 Send 操作,也不会使得远端的接收队列溢出
  3. 远端的一个 Recv 操作完成时,立即重新提交一个新的 Recv 操作,使得正在接收的任务数重新回到 max_recv_wr

分析一下本地 Send、远端 Recv 的场景:

  1. QP 建立之初,可以假定远端已经提交了 N 个 Recv,并且本地知道该信息
  2. 因为远端有 N 个 Recv 操作,所以本地可以直接提交 N 个 Send 操作而不会失败
  3. 但本地无法获悉远端何时完成一个 Recv 操作并重新提交一个 Recv 操作,所以本地提交完 N 个 Send 操作后,是不敢贸然再次提交的
  4. 所以远端需要在重新提交了 Recv 操作后,通过某种方式通知本地,“你可以再次安全地提交 Send 操作”
  5. 最简单的通知方式是远端提交一个发送立即数(Send Imm)操作给本地,立即数的值设定为重新提交的 Recv 的数量。本地收到这条立即数消息后,可以再次安全地提交 n 个 Send 操作。定义该操作为 Send(ack),而发送正常数据定义为 Send(data) 操作,这两种都是 Send 行为
  6. 复杂的地方在于,远端提交 Send(ack) 也是一个 Send 操作,同样需要考虑是否可以直接发送;并不能完成一个 Recv 就发送一个 Send(ack),否则对方的 Recv 收到 Send(ack) 后也得继续发送一个 Send(ack),就无限循环了

设计策略如下:

  1. 定义 send_data_local_remainsend_imm_local_remain,即本地限制下还可以提交多少个 Send(data) 和 Send(ack) 操作,初始值分别为 max_send_data_wrmax_send_imm_wr,提交 WR 前消耗计数,对应的 WR 完成时恢复计数
  2. 定义 send_data_remote_remainsend_imm_remote_remain,即远端限制下还可以提交多少个 Send(data) 和 Send(ack) 操作,初始值同样分别为 max_send_data_wrmax_send_imm_wr,提交 WR 前消耗计数,直到收到 Ack 后恢复计数
  3. 准备提交 Send 操作时,检查对应的 local_remainremote_remain 是否还有余量,如果没有余量则暂存到队列中等待,直到 WR 完成或者收到 Ack 更新计数后再进行发送
  4. 将 Send(ack) 附带的 32 位立即数的值划分为两个 u16,分别用户发送收到的 Send(data) 和 Send(ack) 的 Ack。当收到的 Send(data) 数量 a 大于阈值,或者收到的 Send(ack) 数量 b 大于阈值时,尝试向对端发送一次 Ack(a, b),对端收到该消息后,将 send_data_remote_remain += asend_imm_remote_remain += b。阈值可以设定为初试容量的一半