RDMA初探(以太网网卡数据)

在多机训练场景下, 如何加速多机间的数据交换是绕不开的话题, 作者之前负责的集群是以IB网络为载体, 但由于其昂贵的成本及超高的专业性往往让人退而却步,基于以太网的RoCE则具有很高的性价比。
作者经过一段时间的学习及落地,也算是对RoCE(英文读音类Rocky)技术有些了解,简单记录一下。

说到RoCE, 其实是个很大的范畴, 在作者学习的过程中会出现一千个疑问, 无奈作者是个细节控, 必须说服自己。
作者尽力从最基础的底层网卡 -> DMA -> RDMA -> RoCE来铺开。
作者也不是网络工程专业出身, 整体还是非常复杂的,作者将更加聚焦于列出困惑,
比如,有哪些点作者花了比较长的时间去佐证、哪些点跟以往的认识有很大出入等等问题。
可细可简。

网络设备/网卡/NIC 这里都代表同一种配件设备,就是我们常见的网卡。以下可能会混合使用,不再单独区分

这里还是有必要对网卡的发送/接收数据的过程进行展开。站在应用程序的角度看一个数据是如何通过网卡发送/接收到应用中的。

这篇文章网卡适配器收发数据帧流程非常具体的阐述了网卡是如何进行发送/接收数据的流程, 插图也是非常形象,值得好好沉下心好好地看一看。

之所以说需要沉下心来,是因为确实如果不是对网络专业知识有很好掌握的话,其实是看不下去的,里面涉及到太多的专业流程:
协议栈、网络模块、软硬中断、NAPI等等。作者看了几遍也还是消化不了, 有些都是理论性的知识,其实在实践上并没有那么重要,因此只关注我们感兴趣的点即可。

网络设备工作在物理层和数据链路层。有一些比较重要的参数可以拿出来说一说。

Tx/Rx

1
2
3
4
# 执行ifconfig 网卡名, 其中有以下几个字段:
Tx errors 0 drops 0 overruns 0 frame 0
Rx errors 0 drops 0 overruns 0 frame 0
# 这些参数分别代表什么, 从这些参数中可以得出什么结论?

Tx/Rx FIFO: Tx 表示发送(Transport), Rx 是接收(Receive)

  • errors: CRC错误、硬件故障、超长帧
  • drops: 内存不足、应用层延迟、过滤规则
  • overruns: 中断延迟、缓冲区过小、流量突发
  • frame: 帧格式错误、MTU不匹配、信号干扰

从这个局部图可以看出, drops与overruns的发生的时机有所不同。

总结一下:
overruns vs dropped
overruns: 数据包未进入接收队列(硬件层), 但网卡硬件缓存(FIFO)满,通常因 CPU 处理中断不及时或队列配置过小。
dropped: 数据包已进入接收队列(内核层), 但内核内存不足、协议栈处理能力不足,或应用程序未及时从Socket Buffer 读取数据。

RingBuffer

网卡是一种硬件, 本身也有一些片上缓存(NIC On-Chip Memory), 这些缓存也就几M的级别, 位于网卡芯片内部,作为指针指向主机内存中的实际数据区域。记录数据包在主机内存中的地址、长度和状态(如ready或used)

另外一个比较重要的是RingBuffer, 可以通过以下命令查看网卡的RingBuffer设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ethtool -g eth0: 查询ethx网口基本设置
# -g: 显示网卡的接收/发送环形参数(ringbuffer)。

Ring parameters for eth0:
Pre-set maximums:
RX: 4096
RX Mini: 0
RX Jumbo: 0
TX: 4096
Current hardware settings:
RX: 512
RX Mini: 0
RX Jumbo: 0
TX: 512

Pre-set maximums指的是RingBuffer的最大值。这个是由硬件的上限决定的,不能更改。
Current hardware settings指的是当前的设置。

要特别注意的是,这里的512/4096指的是BD(Buffer descriptor)的数量, 单位不是字节数, BD指向的是实际存放数据的skb的地址指针

简单来说,如果内核处理得不及时导致RingBuffer满了(以当前设置的值为准,上述是RX=512, TX=512),那后面新来的数据包就会被丢弃,生成overruns,
因此当看到网卡在某个方面产生了overruns时,可以通过将RingBuffer适当调大来解决。

但要注意,TX/RX也不能一直调大。因为RingBuffer指向的是内存地址,位于主机内存,系统启动初始化网卡驱动时时,内核为网卡分配 Rx Ring Buffer 的主机内存(可以理解为: 网卡驱动和操作系统合作,预留(reserve)出一段内存来给网卡使用),并与网卡硬件描述符建立映射关系, 申请的是一段连续的物理内存块,确保网卡DMA控制器可直接访问(接下来收到的包就会放到这里,进而被 操作系统取走)

当观察到RingBuffer过大时, 可能会引起排队的包过多,也会增加网络包的延时,因此如何加快内核处理网络包的速度,而不是让网络包在RingBuffer中排队是更好的选择。

所以通过这篇文章网卡适配器收发数据帧流程中的一张图来总结一下:

  1. 网卡中可以配置ringbuffer的数量, ringbuffer数量的多少关系到是否会产生overruns还是drops
  2. 网卡驱动申请Rx/Tx descriptor并为其分配socket buffer, 本质上是在主内存中分配的一片缓冲区,用来接收数据帧。将数据缓存区的总线地址保存到 descriptor

sk_buff(Socket Buffer)

当然还有一个概念会经常听到: sk_buffer(skb)
Linux 内核中,用sk_buff来描述一个缓存,所谓分配缓存空间,就是建立一定数量的 sk_buff.
sk_buff 是 Linux 内核网络协议栈实现中最重要的结构体,它是网络数据报文在内核中的表现形式以及在内核态和用户态间传递。

三者的协作流程如下:

  • 接收数据:
  1. 网卡根据Buffer Descriptor找到空闲内存块(可能是Rx Ring Buffer中的一个槽), 通过DMA写入数据。
  2. 驱动从Rx Ring Buffer中读取数据,封装为sk_buff,递交给协议栈。
  3. 协议栈处理完成后,sk_buff被释放或转发。

网卡使用Buffer Descriptor将数据DMA到内存→驱动从Rx Ring取出数据→封装为sk_buff→协议栈处理→用户态读取

  • 发送数据:
  1. 应用程序数据通过系统调用进入内核,构建sk_buff。
  2. 协议栈处理后的sk_buff被放入Tx Ring Buffer。
  3. 网卡通过Buffer Descriptor找到这段Tx Ring Buffer并读取其中的数据, 然后通过DMA发送。

用户态数据构建sk_buff→协议栈处理→放入Tx Ring→网卡通过Buffer Descriptor经DMA读取后发送

最后以一张网卡内部结构图来结束

以太网 RDMA 网卡模块架构图(以发送接收为例)

参考文章: