内核网络优化

在内核旁路文章内,笔者表述了一个观点:内核是高负载的瓶颈所在,在本篇文章内,我们以网卡收发数据流程为例来继续阐述这个问题,并给到相关的内核优化策略。

什么是中断?

首先,我们要对中断有个基本的认识。

中断在本质上是软件或者硬件发生了某种情形而通知处理器的行为,处理器进而停止正在运行的指令流,去转去执行预定义的中断处理程序。中断一般分为 IRQ(Interupt ReQuest) 和 softIRQ。

  • 硬中断主要是负责耗时短的工作,特点是快速执行
  • 软中断由内核处理,通常都是耗时比较长的事情,是一种推后执行的机制。

软件中断也就是通知内核的机制的泛化名称,目的是促使系统切换到内核态去执行异常处理程序。

以网卡收发数据为例,硬中断把网卡的数据读到内存中,然后更新一下硬件寄存器的状态,比如把状态更新为表示数据已经读到内存中的状态值。接着,软中断调用软中断处理程序处理一些比较耗时且复杂的事情,如从内存中找到网络数据,再按照网络协议栈,对网络数据进行逐层解析和处理,最后把数据送给应用程序。

中断是高负载的瓶颈

中断的方式存在一个问题,当大流量数据到来时候,网卡会产生大量的中断,内核在处理中断上下文中,会浪费大量资源来处理中断本身。

所以,NAPI 技术被提出来,NAPI 技术先将内核屏蔽,然后每隔一段时间去轮询网卡是否有数据。不过相应的,如果数据量少,轮询本身也会占用大量不必要的 CPU 资源,所以需要进行抉择判断。

  • 首先,内核在主内存中为收发数据建立一个环形的缓冲队列(通常也叫 DMA 环形缓冲区)
  • 内核将这个缓冲区通过 DMA 映射,把这个队列交给网卡
  • 网卡收到数据,将会直接放到这个环形缓冲区,也就是直接放进主内存中了,然后向系统产生一个中断
  • 内核收到这个中断,将会取消 DMA 映射,这样内核就直接从主内存中读取了数据

这就形成了我们文章开头的结论:高负载的网卡是软中断产生的大户,很容易形成瓶颈

启用网卡多队列

NAPI 技术可以很好地与现在常见的 1 Gbps 网卡配合使用。但是,对于 10Gbps、20Gbps 甚至 40Gbps 的网卡,NAPI 还是不够。

我们要换一个思路,不拘泥于单个 CPU 进行队列处理呢,要知道一直以来都是 CPU0 进行绑定到网卡队列,导致这个核心的负载会异常高。

网卡多队列技术就是适用于高流量的情况。网卡多队列是一种技术手段,可以解决网络 I/O 带宽 QoS(Quality of Service)问题。

网卡多队列驱动将各个队列通过中断绑定到不同的核上,从而解决网络 I/O 带宽升高时单核 CPU 的处理瓶颈,提升网络 PPS 和带宽性能。经测试,在相同的网络 PPS 和网络带宽的条件下,与 1 个队列相比,2 个队列最多可提升性能达 50% 到 100%,4 个队列的性能提升更大。

查看与开启多队列

我们找到主网卡,查询配置信息

$ ethtool -l eth0  
Channel parameters for eth0:
Pre-set maximums:
RX:		0
TX:		0
Other:		0
Combined:	2  // 表示最多支持设置2个队列
Current hardware settings:
RX:		0
TX:		0
Other:		0
Combined:	1 // 当前生效队列

但注意,不是所有网卡驱动都支持这个操作。如果你的网卡不支持,会看到如下类似的错误

$ ethtool -l eth0
Channel parameters for eth0:
Cannot get device channel parameters
: Operation not supported

设置多队列均匀分布流量。

$ ethtool -L eth0 combined 2

路由、交换机缓冲队列

路由器或者交换机的数据发送依赖于队列(queue),它首先是将数据存储在内存中,如果当前的发送接口不繁忙,那么它将转发数据包,如果当前的接口繁忙,那么网络设备会将数据包暂存于内存中,直到接口空闲才发送数据包,基本的发送原则是 先进先出(FIFO)。

如果使用单一的 FIFO 队列,那么将会缺失一项很重要的功能 - Qos,不能获得优先调度哪些数据转发的能力,FIFO 队列的长度,也是影响 数据的延迟、抖动、丢弃等问题的因素。

  • 较长的队列相较于较短的队列,数据被 尾部丢弃 可能性降低,但是延迟和抖动会加大
  • 较短的队列相较于较长的队列,数据的 尾部丢弃 可能性会增加,但是延迟和抖动会下降
  • 如果产生了持续的拥塞,无论队列是长或者短,数据都会被丢弃

Ring

要发送数据并不是由 发送队列直接输出到 一个输出接口进行转发,而是将数据包从一个输出队列(通常指示为软件队列,RP Ring)传送到另一个更小的输出队列(通常指示为硬件队列,TX Ring)。 然后再从这个更小的输出队列进行转发。

通常硬件队列的长度很小,而且硬件队列的转发不依赖于通过 CPU 而被关联到每一个物理接口。所以即便是路由器的 CPU 工作负荷很重,硬件队列也可以快速的发送数据,而不需要去等待 CPU 做中断处理的时间延迟。

但是硬件队列永远都是遵守 FIFO 原则,它不像软件队列一样可以使用 Qos 的队列工具来进行管理。

$ ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX:		256
RX Mini:	0
RX Jumbo:	0
TX:		256
Current hardware settings:
RX:		256
RX Mini:	0
RX Jumbo:	0
TX:		256

接收和发送的大小都是 256 字节,这个明显偏小。在遇到 burst 流量时,可能导致网卡的接收 ring buffer 满而丢包。 在高性能大流量的服务器或者转发设备上,一般都要配置为 2048 甚至更高。

RPS / RFS

对于多队列网卡,针对网卡硬件接收队列与 CPU 核数在数量上不匹配导致报文在 CPU 之间分配不均这个问题,Google 的工程师提供的 RPS / RFS 两个补丁,它们运行在单队列网卡,可以在软件层面将报文平均分配到多个 CPU 上。

RPS (Receive Package Steering)帮助单队列网卡将其产生的 SoftIRQ 分派到多个 CPU 内核进行处理,网卡驱动通过四元组(源 ip、源端口、目的 ip 和目的端口)生成一个 hash 值,然后根据这个 hash 值分配到对应的 CPU 上处理,从而发挥多核的能力,有效的避免处理瓶颈。

在使用 RPS 接收数据包之后,会在指定的 CPU 进行软中断处理,之后就会在用户态进行处理;如果用户态处理的 CPU 不在软中断处理的 CPU,则会造成 CPU cache miss,造成很大的性能影响。RFS 能够保证处理软中断和处理应用程序是同一个 CPU,这样会保证 local cache hit,提升处理效率。RFS 需要和 RPS 一起配合使用。

不支持网卡多队列技术

$ ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX:		0
TX:		0
Other:		0
Combined:	1
Current hardware settings:
RX:		0
TX:		0
Other:		0
Combined:	1

对于一个多队列系统,如果配置了 RSS,则硬件接收队列会映射到每个 CPU 上,此时 RPS 可能会冗余。但如果硬件队列的数目少于 CPU,设置 rps_cpus 为每个队列指定的 CPU 与中断该队列的 CPU 共享相同的内存域时,则 RPS 可能是有用的

$ cat /sys/class/net/eth0/queues/rx-0/rps_cpus 
0  // 当上述值为0时(默认为0),不会启用RPS。

如果要使用 CPU 1~2,则位图为 0 0 0 0 0 0 1 1,即 0x3,将 3 写入 rps_cpus 即可,后续 rx-0 将会使用 CPU 1~2 来接收报文。

echo 3 > /sys/class/net/eth0/queues/rx-0/rps_cpus

期间,我们开启另外一个终端进行测试,注意观察 idle 字段,越低代表 CPU 负载越低(空闲)

先开启接收客户端

$ netserver
Starting netserver with host 'IN(6)ADDR_ANY' port '3030' and family AF_UNSPEC
$ netperf -H 192.168.28.152 -l 60 -- -m 1024 
MIGRATED TCP STREAM TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.28.152 () port 0 AF_INET
Recv   Send    Send                          
Socket Socket  Message  Elapsed              
Size   Size    Size     Time     Throughput  
bytes  bytes   bytes    secs.    10^6bits/sec  

 87380  16384   1024    60.00     272.80 

$ mpstat  -P ALL 5
Linux 3.10.0-1160.el7.x86_64 (MiWiFi-RB03-srv) 	2023年05月14日 	_x86_64_	(2 CPU)

16时22分04秒  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
16时22分09秒  all    1.38    0.00    2.34    0.00    0.00    1.38   28.37    0.00    0.00   66.52
16时22分09秒    0    1.10    0.00    2.42    0.00    0.00    0.22   22.47    0.00    0.00   73.79
16时22分09秒    1    1.44    0.00    2.46    0.00    0.00    2.67   33.88    0.00    0.00   59.55

开启 RPS 后,负载压到了 0-1 号核心上

这里吞吐没有提升的原因,是因为单队列已经到达内存读写的极限了(本地压测),我们只需要关注 CPU 负载被分摊的现象即可

$ netperf -H 192.168.28.152 -l 60 -- -m 1024 
MIGRATED TCP STREAM TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.28.152 () port 0 AF_INET
Recv   Send    Send                          
Socket Socket  Message  Elapsed              
Size   Size    Size     Time     Throughput  
bytes  bytes   bytes    secs.    10^6bits/sec  

 87380  16384   1024    60.00     273.81 

$ mpstat  -P ALL 5
Linux 3.10.0-1160.el7.x86_64 (MiWiFi-RB03-srv) 	2023年05月14日 	_x86_64_	(2 CPU)

16时32分29秒  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
16时32分34秒  all    3.15    0.00    2.93    0.00    0.00    1.30   26.38    0.00    0.00   66.23
16时32分34秒    0    3.54    0.00    3.98    0.00    0.00    0.00   24.12    0.00    0.00   68.36
16时32分34秒    1    2.78    0.00    1.93    0.00    0.00    2.57   28.48    0.00    0.00   64.24
Last Updated:
Contributors: isno