目录

Linux 系统收包流程以及内核参数优化

简介

高并发的系统架构中,任何细微调整,稍有不注意便会引起连锁反应,只有系统地了解整个网络栈,在处理疑难杂症或者系统优化工作中,才能做到手中有粮心中不慌。在本节,我们概览一个 Linux 系统收包的流程,以便了解高并发系统所面临的性能瓶颈问题以及相关的优化策略。

收包过程

/images/networking-dc52a683.svg

  1. 网卡 eth0 收到数据包。
  2. 网卡通过 DMA 将数据包拷贝到内存的环形缓冲区(Ring Buffer,在网卡中有 RX Ring 和 TX Ring 两种缓冲)。
  3. 数据从网卡拷贝到内存后, 网卡产生 IRQ(Interupt ReQuest,硬件中断)告知内核有新的数据包达到。
  4. 内核收到中断后, 调用相应中断处理函数,开始唤醒 ksoftirqd 内核线程处理软中断。
  5. 内核进行软中断处理,调用 NAPI poll 接口来获取内存环形缓冲区(ring buffer)的数据包,送至更上层处理。
  6. 内核中网络协议栈:L2 处理。
  7. 内核中网络协议栈:L3 处理。
  8. 内核中网络协议栈:L4 处理。
  9. 网络协议栈处理数据后,并将其发送到对应应用的 socket 接收缓冲区。

高并发瓶颈

  • 用户进程调用系统调用陷入内核态的开销。
  • CPU 响应包的硬中断 CPU 开销
  • ksoftirqd 内核线程的软中断上下文开销。

RX/TX Ring 优化

处理一个数据包会有各类的中断、softirq 等处理,因为分配给 Ring Buffer 的空间是有限的,当收到的数据包速率大于单个 CPU 处理速度的时,Ring Buffer 可能被占满并导致新数据包被自动丢弃。一个 CPU 去处理 Ring Buffer 数据会很低效,这个时候就产生 RSS、RPS 等多核并发机制来提升内核网络包的处理能力。

但是注意,开启多核并发特性,会挤压业务代码的执行时间,如果业务属于 CPU 密集型,会导致业务性能下降。是否开启多核处理,需要根据业务场景考虑,根据笔者的经验来看,例如此类负载均衡服务器网关集群核心转发节点网络I/O 密集型场景可以尝试优化 RSS、RPS 等配置

1. 判断是否需进行优化

当类似 LVS、集群核心交换节点、负载均衡服务器的场景 PPS(Packet Per Second,包每秒)指标存在一定的优化空间且这些核心节点影响了集群业务,那我们可以查看系统状态以决定是否进行内核优化。
首先我们确定是否存在丢包状况,如果存在则进行相应的调整。查询网卡收包情况 (RX 为收到的数据、TX 为发送的数据)。

1
ifconfig eth0 | grep -E 'RX|TX'

eth0 是你具体服务器的网卡地址

出现以下结果说明RX dropped 表示数据包已经进入了 Ring Buffer,但是由于内存不够等系统原因,导致在拷贝到内存的过程中被丢弃,RX overruns 为 Ring Buffer 传输的 IO 大于 kernel 能够处理的 IO 导致,overruns 的错误意味着 CPU 无法即使的处理中断是造成 Ring Buffer 溢出

1
2
3
4
RX packets 490423734  bytes 193802774970 (180.4 GiB)
RX errors 12732344  dropped 9008921  overruns 3723423  frame 0
TX packets 515280693  bytes 140609362555 (130.9 GiB)
TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

2. RSS 下的多队列调整

RSS(receive side steering)利用网卡多队列特性,将每个核分别跟网卡的一个首发队列绑定,以达到网卡硬中断和软中断均衡的负载在各个 CPU 中,RSS 要求网卡必须要支持多队列特性。(注意:对于大部分驱动,修改以下配置会使网卡先 down 再 up,因此会造成丢包!)
查询 RX/TX 队列配置和使用情况。

1
ethtool -l eth0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Channel parameters for eth0:
Pre-set maximums:
RX:		0
TX:		0
Other:		0
Combined:	8
Current hardware settings:
RX:		0
TX:		0
Other:		0
Combined:	4

可以看到硬件最多支持 6 个,当前使用了 4 个。将 RX 和 TX queue 数量都设为 8。

1
ethtool -L eth0 combined 8

3. 队列大小调整

增大 ring buffer 可以在 PPS(packets per second)很大时缓解丢包问题
查看队列大小。

1
ethtool -g eth0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Ring parameters for eth0:
Pre-set maximums:
RX:		1024
RX Mini:	0
RX Jumbo:	0
TX:		1024
Current hardware settings:
RX:		512
RX Mini:	0
RX Jumbo:	0
TX:		512

以上输出显示网卡最多支持 1024 个 RX/TX 数据包大小,但是现在只用到了 512 个。 ethtool -G 修改 queue 大小。

1
ethtool -G eth0 rx 1024

内核协议栈优化

一个传输少量数据的 TCP 短连接的生命周期中,握手、挥手阶段将近占用了 70% 的资源消耗,在一个高并发的服务场景,如负载均衡、数据库等,针对性的去优化较为保守内核参数提提升服务性能的必要手段。在本文中,内核协议栈参数优化方向为 TCP 握手流程中队列大小、挥手 TIME_WAITE、keepalive 保活机制以及拥塞控制。
/images/TCP-ad017126.svg

TCP 握手流程参数优化

握手流程中有两个队列较为关键,当队列满时,多余的连接将会被丢弃。

  • SYN Queue 也被称为半连接队列,是内核保持的未被 ACK 的 SYN 包最大队列长度,通过内核参数 net.ipv4.tcp_max_syn_backlog 配置,高并发的环境下建议设置为 1024 或更高
  • Accept Queue 也被称为全连接队列, 是一个 socket 上等待应用程序 accept 的最大队列长度。取值为 min(backlog,net.core.somaxconn)。

TCP 连接保活参数优化

当 TCP 建立连接后,会有个发送一个空的 ACK 的探测行为来保持连接(keepalive)。keepalive 受以下参数影响:

  • net.ipv4.tcp_keepalive_time 最大闲置时间
  • net.ipv4.tcp_keepalive_intvl 发送探测包的时间间隔
  • net.ipv4.tcp_keepalive_probes 最大失败次数,超过此值后将通知应用层连接失效 在大规模的集群内部,如果 keepalive_time 设置较短且发送较为频繁,会产生大量的空 ACK 报文,存在塞满 RingBuffer 造成 TCP 丢包甚至连接断开问题。

TCP 连接断开参数优化

由于 TCP 双全工的特性,安全关闭一个连接需要四次挥手。但在一个复杂的网络环境下,会存在很多异常情况,异常断开连接会导致产生孤儿连(半连接)。 这种连接既不能发送数据,也无法接收数据,累计过多,会消耗大量系统资源。在高并发的场景下,孤儿连过多会引起资源不足,产生 Address already in use: connect 类似的错误。
/images/tcp_disconnect-4051b998.svg
挥手流程中的主要优化为 TIME_WAIT 的参数调整。TIME_WAIT 是 TCP 挥手的最后一个状态。当收到被动方发来的 FIN 报文后,主动方回复 ACK,表示确认对方的发送通道已经关闭,继而进入TIME_WAIT 状态 ,等待 2MSL 时间后关闭连接。
如果发起连接一方的 TIME_WAIT 状态过多,会占满了所有端口资源,则会导致无法创建新连接。TIME_WAIT 的问题在反向代理中比较明显,例如 nginx 默认行为下会对于 client 传来的每一个 request 都向 upstream server 打开一个新连接,高 QPS 的反向代理将会快速积累 TIME_WAIT 状态的 socket,直到没有可用的本地端口,无法继续向 upstream 打开连接,此时服务将不可用。

  • net.ipv4.tcp_max_tw_buckets,此数值定义系统在同一时间最多能有多少 TIME_WAIT 状态,当超过这个值时,系统会直接删掉这个 socket 而不会留下 TIME_WAIT 的状态
  • net.ipv4.ip_local_port_range,TCP 建立连接时 client 会随机从该参数中定义的端口范围中选择一个作为源端口。可以调整该参数范围增大可选择的端口范围

相关配置参考

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
net.ipv4.tcp_tw_recycle = 0
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_rmem = 16384 262144 8388608
net.ipv4.tcp_wmem = 32768 524288 16777216
net.core.somaxconn = 8192
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.wmem_default = 2097152
net.ipv4.tcp_max_tw_buckets = 5000
net.ipv4.tcp_max_syn_backlog = 10240
net.core.netdev_max_backlog = 10240
net.netfilter.nf_conntrack_max = 1000000
net.ipv4.netfilter.ip_conntrack_tcp_timeout_established = 7200
net.core.default_qdisc = fq_codel
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_slow_start_after_idle = 0