前面我们聊了RC(可靠连接)和UD(不可靠数据报)。RC什么都好------可靠、保序、能拆长消息------但QP上下文太重,硬件成本高。UD什么都省------无连接、单包、不确认------但代价是不可靠,丢包了上层自己兜底。
那么,有没有一种"中间形态"?既要面向连接、能拆长消息,又要省掉确认和重传的硬件开销?
答案是有的,UC(Unreliable Connection,不可靠连接)就是这个中间产物。只不过它尴尬到几乎没人用。
一、UC在三大服务类型中的位置
UC的定位可以用一句话概括:连接方式像RC(一对一),可靠性像UD(不确认、不重传)。
更准确的说,UC是RC的"子集"。InfiniBand官方定义明确写道:"UC传输服务类型是RC的一个子集。在UC中,没有任何响应包被期待。"这意味着UC沿用了RC的数据包格式(支持多包消息拆分),但去掉了一整套确认与重传机制。
我们接下来从连接方式和可靠性两个维度看这三个类型:
|------|-------|------------|---------|------|-------|
| 传输类型 | 连接方式 | RMDA write | 端到端确认 | 硬件重传 | 长消息拆分 |
| RC | 一对一连接 | 支持 | ACK/NAK | 有 | 支持 |
| UC | 一对一连接 | 支持 | 无 | 无 | 支持 |
| UD | 无连接 | 不支持 | 无 | 无 | 不支持 |
在消息处理能力上,UC与RC完全一致------消息可以被拆分成first/middle/last多个数据包发送,到达后重组,最终向上层交付一个完整消息。在支持的opcode上,UC支持SEND和WRITE,不支持READ和原子操作。
二、建链信息交换
UC QP要正常工作,首先需要建链,随后在建链及开始的时候进行信息交换,双方需要互换以下几种关键信息:
(1) QPN:队列对编号,相当于ID;
(2) 起始PSN:每个数据包都有的序列号,用于排序和丢包检测;
(3) MTU:协商单个包的最大载荷,该值为二的N次幂;
这些信息交换完毕后,双方网卡便可以进行数据的收发了,这个是和RC建链完全一致的。
三、发送报文
应用程序如果需要发送数据,则产生一个WR给到驱动,驱动将其转化为一个WQE下发给SQ。
对于网卡硬件来说,他的执行过程与RC write几乎完全相同:
- DMA取数据,根据WQE里面信息将数据包拉取下来;
- 拆包,根据MTU将其拆成多个包,opcode分别打上UC write first/middle/last,PSN连续递增;
- 封装传输头,构造BTH,插入RETH;
- 计算ICRC插入到报文包尾;
- 发送;
上述步骤其实与RC write完全一致,但是不一致的是确认机制,在UC传输服务类型中,不期待任何确认。发送端软件在传输完write的最后一个数据包后,立即被告知消息传输已完成。
也就是说,网卡把最后一个RDMA WRITE Last包推上线路后,立刻在CQ里生成CQE告诉你"写完了"------它根本不关心对端收到没有。没有计时器、没有重传缓冲区、不等待ACK。整个流程是"发完即忘"的线性流水。
四、接收报文
对端网卡收到UC Write包后做什么?规范里这样描述:
- 正常验证BTH、CRC。损坏的包悄悄丢弃,不发NAK;
- PSN跳变时照收不误,不丢包也不发任何反馈;
- 所有数据包按序抵达后DMA写入远端内存,硬件产生CQE,
如果远端收到First和Last但中间缺了Middle,UC会怎么处理?答案是会因为乱序原因放弃整个消息。
因为UC write没有硬件重传,那丢包了怎么办?此时上层应用自行处理。
五、UC的尴尬地位
UC的设计目标很明确,那就是在保留RC长消息拆分能力的前提下,砍掉确认和重传以降低硬件成本,并给予"发送即可完成"的极低延迟发送端体验。
但是,他"成功"把自己做到了一个尴尬的位置。
对于不需要可靠性的场景,UD比UC更省:UC要建链、要维护连接状态,UD连这个都省了,还支持多播。
对于需要可靠性的场景,RC比UC更可靠,而且RC能提供硬件级别的丢包恢复,UC却做不到。
结果,UC成了一个几乎没人用的中间产物。在一些高性能网络框架中,UC主要用于内部本地通信或开发调试。在实际生产环境中,大部分场景的选择是"要么RC,要么UD",几乎看不到UC的身影。