# RDMA 基本服务类型
本文欢迎非商业转载,转载请注明出处。
声明:仅用于收藏,便于阅读
― Savir, 知乎专栏:5. RDMA 基本服务类型
我们在 【“3. RDMA 基本元素”】 一文中提到过,RDMA 的基本通信单元是 QP,而基于 QP 的通信模型有很多种,我们在 RDMA 领域称其为“服务类型”。IB 协议中通过“可靠”和“连接”两个维度来描述一种服务类型。
# 可靠
通信中的可靠性指的是通过一些机制保证发出去的数据包都能够被正常接收。IB 协议中是这样描述可靠服务的:
Reliable Service provides a guarantee that messages are delivered from a requester to a responder at most once, in order and without corruption.
即“可靠服务在发送和接受者之间保证了信息最多只会传递一次,并且能够保证其按照发送顺序完整的被接收”。
IB 通过以下三个机制来保证可靠性:
# 应答机制
假设 A 给 B 发了一个数据包,A 怎样才能知道 B 收到了呢,自然是 B 回复一个“我收到了”消息给 A。在通信领域我们一般称这个回复为应答包或者 ACK(Acknowledge)。在 IB 协议的可靠服务类型中,使用了应答机制来保证数据包被对方收到。IB 的可靠服务类型中,接收方不是每一个包都必须回复,也可以一次回复多个包的 ACK,以后我们再展开讨论。
# 数据校验机制
这个比较好理解,发端会对 Header 和 Payload(有效载荷,也就是真正要收发的数据)通过一定的算法得到一个校验值放到数据包的末尾。对端收到数据包后,也会用相同的算法计算出校验值,然后与数据包中的校验值比对,如果不一致,说明数据中包含错误(一般是链路问题导致的),那么接收端就会丢弃这个数据包。IB 协议使用的 CRC 校验,本文对 CRC 不做展开介绍。
# 保序机制
保序指的是,保证先被发送到物理链路上的数据包一定要先于后发送的数据包被接收方收到。有一些业务对数据包的先后顺序是有严格要求的,比如语音或者视频。IB 协议中有 PSN(Packet Sequence Number,包序号)的概念,即每个包都有一个递增的编号。PSN 可以用来检测是否丢包,比如收端收到了 1,但是在没收到 2 的情况下就收到了 3,那么其就会认为传输过程中发生了错误,之后会回复一个 NAK 给发端,让其重发丢失的包。
不可靠服务,没有上述这些机制来保证数据包被正确的接收,属于“发出去就行,我不关心有没有被收到”的服务类型。
# 连接与数据报
连接(Connection) 在这里指的是一个抽象的逻辑概念,需要区别于物理连接,熟悉 Socket 的读者一定对这个其不陌生。连接是一条通信的“管道”,一旦管道建立好了,管道这端发出的数据一定会沿着这条管道到达另一端。
对于“连接”或者说“面向连接”的定义有很多种,有的侧重于保证消息顺序,有的侧重于消息的传递路径唯一,有的强调需要软硬件开销来维护连接,有的还和可靠性的概念有交集。本专栏既然是介绍 RDMA 技术,那么我们就看一下 IB 协议 3.2.2 节中对其的描述:
IBA supports both connection oriented and datagram service. For connected service, each QP is associated with exactly one remote consumer. In this case the QP context is configured with the identity of the remote consumer’s queue pair. … During the communication establishment process, this and other information is exchanged between the two nodes.
即“IBA 支持基于连接和数据报的服务。对于基于连接的服务来说,每个 QP 都和另一个远端节点相关联。在这种情况下,QP Context 中包含有远端节点的 QP 信息。在建立通信的过程中,两个节点会交换包括稍后用于通信的 QP 在内的对端信息"。
上面这端描述中的 Context 一般被翻译成上下文,QP Context(简称 QPC)可以简单理解为是记录一个 QP 相关信息的表格。我们知道 QP 是两个队列,除了这两个队列之外,我们还需要把关于 QP 的信息记录到一张表里面,这些信息可能包括队列的深度,队列的编号等等,后面我们会展开讲。
可能还是有点抽象,我们用图说话:
A、B 和 A、C 节点的网卡在物理上是连接在一起的,A 上面的 QP2 和 B 上面的 QP7、A 上面的 QP4 和 B 上面的 QP2 建立了逻辑上的连接,或者说“绑定到了一起”。在连接服务类型中的每个 QP,都和唯一的另一个 QP 建立了连接,也就是说 QP 下发的每个 WQE 的目的地都是唯一的。拿上图来说,对于 A 的 QP2 下发的每个 WQE,硬件都可以通过 QPC 得知其目的为 B 的 QP7,就会把组装好的数据包发送给 B,然后 B 会根据 QP7 下发的 RQ WQE 来存放数据;同理,对于 A 的 QP4 下发的每个 WQE,A 的硬件都知道应该把数据发给 Node C 的 QP2。
“连接”是如何维护的呢?其实就是在 QPC 里面的一个记录而已。如果 A 的 QP2 想断开与 B 的 QP7 的“连接”然后与其他 QP 相“连接”,只需要修改 QPC 就可以了。两个节点在建立连接的过程中,会交换稍后用于数据交互的 QP Number,然后分别记录在 QPC 中。
数据报(Datagram) 与连接相反,发端和收端间不需要“建立管道”的步骤,只要发端到收端物理上是可以到达的,那么我就可能从任何路径发给任意的收端节点。IB 协议对其的定义是这样的:
For datagram service, a QP is not tied to a single remote consumer, but rather information in the WQE identifies the destination. A communication setup process similar to the connection setup process needs to occur with each destination to exchange that information.
即“对于数据报服务来说,QP 不会跟一个唯一的远端节点绑定,而是通过 WQE 来指定目的节点。和连接类型的服务一样,建立通信的过程也需要两端交换对端信息,但是数据报服务对于每个目的节点都需要执行一次这个交换过程。”
我们举个例子:
在数据报类型的 QP 的 Context 中,不包含对端信息,即每个 QP 不跟另一个 QP 绑定。QP 下发给硬件的每个 WQE 都可能指向不同的目的地。比如节点 A 的 QP2 下发的第一个 WQE,指示给节点 C 的 QP3 发数据;而下一个 WQE,可以指示硬件发给节点 B 的 QP7。
与连接服务类型一样,本端 QP 可以和哪个对端 QP 发送数据,是在准备阶段提前通过某些方式相互告知的。这也是上文“数据报服务对于每个目的节点都需要执行一次这个交换过程”的含义。
# 服务类型
上面介绍的两个维度两两组合就形成了 IB 的四种基本服务类型:
可靠(Reliable) | 不可靠(Unreliable) | |
---|---|---|
连接(Connection) | RC(Reliable Connection) | UC(Unreliable Connection) |
数据报(Datagram) | RD(Reliable Datagram) | UD(Unreliable Datagram) |
RC 和 UD 是应用最多也是最基础的两种服务类型,我们可以将他们分别类比成 TCP/IP 协议栈传输层的 TCP 和 UDP。
RC 用于对数据完整性和可靠性要求较高的场景,跟 TCP 一样,因为需要各种机制来保证可靠,所以开销自然会大一些。另外由于 RC 服务类型和每个节点间需要各自维护一个 QP,假设有 N 个节点要相互通信,那么至少需要 N * (N - 1) 个 QP,而 QP 和 QPC 本身是需要占用网卡资源或者内存的,当节点数很多时,存储资源消耗将会非常大。
UD 硬件开销小并且节省存储资源,比如 N 个节点需要相互通信,只需要创建 N 个 QP 就可以了,但是可靠性跟 UDP 一样没法保证。用户如果想基于 UD 服务类型实现可靠性,那么需要自己基于 IB 传输层实现应用层的可靠传输机制。
除此之外,还有 RD 和 UC 类型,以及 XRC(Extended Reliable Connection),SRD(Scalable Reliable Datagram)等更复杂的服务类型,我们将在协议解析部分对其进行详细的描述。
更多关于 QP 类型选择的信息可以参考 RDMAmojo 上的Which Queue Pair type to use? 这篇文章,感谢 @sinkinben 同学在评论区指路。
# 代码示例
在 RDMA 编程中,我们可以通过 ibv_create_qp
函数来创建 QP,其中的 struct ibv_qp_init_attr
结构体中的 qp_type
字段就是用来指定 QP 的服务类型的。下面是一个简单的示例代码:
struct ibv_qp_init_attr qp_init_attr;
qp_init_attr.qp_type = IBV_QPT_RC; // RC 类型
qp_init_attr.sq_sig_all = 1; // 1 表示 SQ 中的每个 WQE 都需要对应的接收一个 CQE
qp_init_attr.send_cq = cq; // 发送 CQ
qp_init_attr.recv_cq = cq; // 接收 CQ
qp_init_attr.cap.max_send_wr = 1024; // SQ 的深度
struct ibv_qp *qp = ibv_create_qp(pd, &qp_init_attr);