2.RDMA基本概念
究竟是如何进行通信的?
TCP是流式协议(就是字节流),但是RDMA是面向消息(就是element)通信的.
放弃用双边操作(SEND/RECV)来传输业务数据,而是用双边操作来“交换信令”,用单边操作(WRITE)来“传输数据”。
[Valkey Client] [Valkey Server (Listen RDMA Port)]
| |
| ======================== 1. 建立 RDMA 连接 =========================> |
| <======================= (RC QP 初始化) ======================== |
| |
| ================== 2. 交换 Feature 支持 [@IBV_WR_SEND] =============> |
| <================= 3. 交换 Feature 支持 [@IBV_WR_SEND] ============== |
| |
| [阶段 A:初始化远端内存写入权限] 这里都是双边操作 |
| <---- 4. Server 发送其 RX Buffer 的地址/长度/R_Key [@IBV_WR_SEND] --- | (Server 执行 ibv_post_recv 准备接收命令)
| ---- 5. Client 发送其 RX Buffer 的地址/长度/R_Key [@IBV_WR_SEND] ---> | (Client 执行 ibv_post_recv 准备接收响应) 两边都通过send来发送R_key和内存地址,两边收到之后都可以绕过CPU直接进行读写的操作.
| |
| [阶段 B:使用 WRITE 进行命令与数据交互] |
| --- 6. Valkey Command (如 PING) [@IBV_WR_RDMA_WRITE_WITH_IMM] ------> | (直接写入 Server 内存,触发 IMM 通知 Server CPU)
| <--- 7. Valkey Response (如 PONG) [@IBV_WR_RDMA_WRITE_WITH_IMM] ----- | (直接写入 Client 内存,触发 IMM 通知 Client CPU)
| --- 8. Valkey Command (如 SET) [@IBV_WR_RDMA_WRITE_WITH_IMM] -------> |
| <--- 9. Valkey Response (如 OK) [@IBV_WR_RDMA_WRITE_WITH_IMM] ------- |
| |
| [阶段 C:处理缓冲区写满 (RX is full)] |
| (假设 Client 发现 Server 之前给的 RX Buffer 空间不足了) |
| |
| <--- 10. Server 重新注册内存并发送新 RX Buffer 信息 [@IBV_WR_SEND] -- | (Server 发现自己消费完了,主动发新 Buffer,并 ibv_post_recv)
| --- 11. Valkey Command (恢复发送) [@IBV_WR_RDMA_WRITE_WITH_IMM] ----> |
| |
| (假设 Server 发现 Client 的 RX Buffer 不足了) |
| ---- 12. Client 重新注册内存并发送新 RX Buffer 信息 [@IBV_WR_SEND]-> | (Client 主动发新 Buffer,并 ibv_post_recv)
| <--- 13. Valkey Response (恢复发送) [@IBV_WR_RDMA_WRITE_WITH_IMM] --- |
| |
| ======================== 14. 断开 RDMA 连接 ========================> |
1.什么是IMM消息通知?
就是一个立即数,下面4会讲解到.
2.为什么数据传输不使用双边操作?
在传统的双边操作(SEND/RECV)中,接收端必须先猜到发送端会发多大的数据。 接收端必须调用
ibv_post_recv挂载一个 RECV WQE(工作请求)。 如果 Redis 客户端发来一个 10 字节的PING,但服务器挂载了一个 10MB 的接收缓冲区,这是极大的内存浪费;如果服务器挂载了 10 字节的缓冲区,但客户端发来一个 1MB 的SET KEY VALUE,网卡会直接报 “Length Error” 并断开 QP 连接。 对于 Redis 这种每次请求长度完全不可预测的流式协议,纯粹的 SEND/RECV 就是一场灾难。 但是如果只是握手的话,提前通信的内容大小就是可以预测的,所以没有关系。
3.使用单边的WRITE操作来模拟TCP流式协议?
这里就可以看RDMA的操作类型生动的理解: 3.RDMA操作类型 既然无法预测消息大小,作者的设计思路是:退回 TCP 的本质,自己在应用层维护一个类似 TCP 滑动窗口的虚拟接收缓冲区。 建立大内存池:服务器先注册一块相对较大的内存(比如 1MB),将其视为一个大木桶(Receive buffer)。 信令交换(SEND 操作的作用):服务器用底层的 SEND 报文,把这个大木桶的“钥匙和地址”(R_Key、虚拟地址、总长度)交还给客户端。 客户端自由写入:客户端手里有了服务器木桶的钥匙,想发多少数据,就直接调用单边操作
RDMA_WRITE往服务器木桶里的当前偏移量写数据。写 10 字节还是 10 万字节都可以,只要不超过木桶的剩余容量。
4.IBV_WR_RDMA_WRITE_WITH_IMM 为什么使用这个?
如果你熟悉 RDMA 单边操作,你会知道纯粹的
RDMA_WRITE是一次“静默操作”——数据直接写入远端内存,远端网卡不会产生任何完成事件(CQE),远端 CPU 根本不知道数据来了。 如果 Valkey Server 连命令到了都不知道,怎么执行GET/SET呢? 这就是IBV_WR_RDMA_WRITE_WITH_IMM发挥作用的地方。它在静默写入数据的同时,允许发送端附加一个 32 位的立即数(Immediate Data)。 当这条指令到达远端网卡并完成内存写入后,远端网卡会被强制唤醒,在 Server 端的完成队列(CQ)里产生一个带有这个立即数的完成事件(CQE)。 (所以我只知道什么时候可读,但是不知道什么时候可写.) 在 Redis 的工程实现中,客户端通常会把写入数据的字节长度放在这个 32 位的 IMM 字段中。 Server 端的 epoll 检测到网卡事件,读取 CQE,一看 IMM 是 25,就知道:“哦,有人往我的木桶里刚刚写入了 25 字节的数据,我赶紧去处理”。 所以数据的传输是单边的操作,作为server,大多数情况下,我知道什么时候能读取,但是不知道什么时候能写入对方的内存.(这部分内存就是我们注册的内存.)
5.如果写满,还需要进行内存的刷新机制.
既然是用一段固定大小的内存做流式接收,总有写满的时候。 在这个协议中,Client 会自己记录“Server 的木桶还剩多少空间”。当 Client 发现即将写满时,它必须停下来。 与此同时,Server 在处理完堆积的命令后,发现这块内存用完了,它会重新初始化(或重置)这块内存区域,然后再次发起一次 SEND 操作,告诉 Client:“我的木桶清空了(或者我换了一个新木桶),这是新的起始地址和长度,你继续写吧”。 上述这个RTT的过程会不会带来抖动?是啊,我就是好奇这个问题。