网卡优化
Ring Buffer和网卡队列
内核处理一个数据包会有各类的中断处理、协议栈封装转换等繁琐流程,当收到的数据包速率大于单个 CPU 处理速度的时,Ring Buffer 可能被占满并导致新数据包被自动丢弃。
在多核 CPU 的服务器上,网卡内部会有多个 Ring Buffer,网卡负责将传进来的数据分配给不同的 Ring Buffer,同时触发的中断也可以分配到多个 CPU 上处理,这样存在多个 Ring Buffer 的情况下 Ring Buffer 缓存的数据也同时被多个 CPU 处理,就能提高数据的并行处理能力。
网卡收发包
TX(Transmit 发送端口),RX(Receive 接收端口)
- 略去物理层和链路层处理的细节,网卡在收到包后会将帧存放在硬件的 frame buffer 上,并通过 DMA 同步到内核的一块内存(称为 ring buffer,对 ingress 称 rx_ring),
ethtool -g [nic]可以查看 ringbuffer 大小 - 在最传统的中断模式下,每个帧将产生一次硬中断,CPU 0 收到硬中断后产生一个软中断,内核切换上下文进行协议栈的处理,理论上这是延迟最低的方案,但大量的软中断会消耗 CPU 资源,导致其他外设来不及正常响应,因此可以启用中断聚合(Interrupt Coalesce),多帧产生一个中断,
ethtool -c [nic]可以查看中断聚合状态,ethtool -C [nic] adaptive-rx on开启自适应中断聚合。 - NAPI 是一种更先进的处理方式,NAPI 模式下网卡收到帧后会进入 polling mode 此时网卡不再产生更多的硬中断,内核的 ksoftirqd 在软中断的上下文中调用 NAPI 的 poll 函数从 ring buffer 收包,直到 rx_ring 为空或执行超过一定时间(如 ixgbe 驱动中定义超时为2个 CPU 时钟)
- 内核将收到的包复制到一块新的内存空间,组织成内核中定义的
skb数据结构,交上层处理
通过对Ring Buffer调整可以缓解大流量网卡丢包的情况
$ cat /proc/net/softnet_stat
# 每行代表一个cpu core接收数据的情况
# 第一列收到的包总数
# 第二列是丢弃的包计数,net.core.netdev_max_backlog
# 第三列,net.core.netdev_budget
# 4-8列没有意义因此全是0
# 第九列是 CPU 为了发送包而获取锁的时候有冲突的次数
# 第十列是 CPU 被其他 CPU 唤醒来处理 backlog 数据的次数
# 第十一列是触发 flow_limit 限制的次数
000006ab 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
3749406b 00000000 00001f0b 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
3bcdebc6 00000000 00002070 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
3a948321 00000000 00001ee2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
$ cat /proc/net/softnet_stat|awk '{print $2}'|grep -v 00000000
第二列丢包计数是netdev backlog,指包已进行网卡的ring buffer,但在从网卡ring buffer 运输到内核缓存队列时,由于内核缓存队列满了,从而数据包被丢弃。如果第2列数值在增长,那么我们需要将内核网络缓存队列的长度调大一点,netdev backlog就是上图中的 recv_backlog
echo "net.core.netdev_max_backlog = 600" >> /etc/sysctl.conf
第三列在增长,说明是在 从ring buffer 运输到内核网络缓存队列时, 由于”运输小车”没能装下,而导致丢包。这个运输过程是由中断处理程序的下半部分完成的(即软中断),但软中断会被调度且有一定的执行时间。此时我们需要修改 “运输小车”的大小, 内核参数为net.core.netdev_budget , 可先将其修改为原来的2倍大小。此值不能改的太大,可能会造成软中断占用太长cpu时间,而引起其他性能问题。
Ring Buffer优化
查看丢包情况
$ sudo ethtool -S eth0 | grep -iE "error|drop"
rx_errors: 0
tx_errors: 0
rx_dropped: 144
tx_dropped: 0
rx_length_errors: 0
rx_crc_errors: 0
port.tx_errors: 0
port.rx_dropped: 0
port.tx_dropped_link_down: 0
port.rx_crc_errors: 0
port.rx_length_errors: 0
查看 Ring Buffer 队列大小
队列越大丢包的可能越小,但数据延迟会增加。
$ sudo ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX: 4096 # RX最大值 (接收)
RX Mini: 0
RX Jumbo: 0
TX: 4096 # Tx最大值 (发送)
Current hardware settings:
RX: 512 # RX当前值
RX Mini: 0
RX Jumbo: 0
TX: 512 # TX当前值
调整Ring Buffer 队列大小
注意:修改可能会引起网卡异常,例如:网卡流量掉底
# rx_dropped rx接收有丢包
$ sudo ip link set ethX down
$ sudo ethtool -G ethX rx 4096
$ sudo ip link set ethX up
pre-up /sbin/ethtool -G eth0 rx 4096
pre-up /sbin/ethtool -G eth1 rx 4096
查看Ring Buffer 队列数量
$ sudo ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX: 0
TX: 0
Other: 1
Combined: 64 # 最大队列
Current hardware settings:
RX: 0
TX: 0
Other: 1
Combined: 56 # 当前队列
# 实际队列数量
$ sudo ethtool -x eth0
RX flow hash indirection table for eth0 with 56 RX ring(s):
0: 0 1 2 3 4 5 6 7
8: 8 9 10 11 12 13 14 15 # 0 - 15
16: 0 1 2 3 4 5 6 7
24: 8 9 10 11 12 13 14 15
32: 0 1 2 3 4 5 6 7
40: 8 9 10 11 12 13 14 15
48: 0 1 2 3 4 5 6 7
56: 8 9 10 11 12 13 14 15
64: 0 1 2 3 4 5 6 7
72: 8 9 10 11 12 13 14 15
80: 0 1 2 3 4 5 6 7
88: 8 9 10 11 12 13 14 15
96: 0 1 2 3 4 5 6 7
104: 8 9 10 11 12 13 14 15
112: 0 1 2 3 4 5 6 7
120: 8 9 10 11 12 13 14 15
RSS hash key:
6e:31:97:53:46:68:15:4a:3a:83:12:92:af:17:2a:48:81:84:81:e4:4b:2f:c2:45:d0:87:8d:0c:e0:fc:57:f2:57:6b:4d:8d:ce:9a:d0:1e
RSS hash function:
toeplitz: on
xor: off
crc32: off
调整Ring Buffer 队列数量
# ethtool 的设置操作可能都要重启一下才能生效
$ sudo ethtool -L ethX combined 64
# 如果支持对特定类型 RX 或 TX 设置队列数量的话可以执行:
sudo ethtool -L ethX rx 8
多CPU多队列网卡的 Ring Buffer 处理
绑定网卡中断到各个cpu上,参考irq文档
NIC 收到数据的时候产生的 IRQ 只可能被一个 CPU 处理,从而只有一个 CPU 会执行 napi_schedule 来触发 softirq,触发的这个 softirq 的 handler 也还是会在这个产生 softIRQ 的 CPU 上执行。
硬中断 /proc/interrupts
在多核 CPU 的服务器上,NIC 会为每个队列分配一个 IRQ,通过 /proc/interrupts 能进行查看。你可以通过配置 IRQ affinity 指定 IRQ 由哪个 CPU 来处理中断。
#6 表示的是 CPU2 和 CPU1
echo 6 > /proc/irq/41/smp_affinity
查看网卡中断/proc/interrupts,我们常常对网卡硬中断进行cpu绑定
$ cat /proc/interrupts | grep eth[0-9]- | awk '{print $1}'|cut -d':' -f1|wc -l
224
# 4个up的网卡,每个网卡Combined: 56,56*4=224
查看eth2的中断绑定在哪些cpu上
# i就是对应的中断号,去/proc/irq/$i/smp_affinity_list里看cpu号
for i in $(cat /proc/interrupts | grep eth[0-4]- | awk '{print $1}' | sed 's/://g'); do sudo cat /proc/irq/$i/smp_affinity_list; done
softirq /proc/softirqs
软中断是由操作系统内核发起的一种中断方式,用于在内核空间和用户空间之间进行通信和执行特定的操作。例如
- 系统调用中断:用户空间的程序通过系统调用请求内核执行特定的操作,例如打开文件、读取数据等。
- 定时器中断:操作系统内核使用定时器软中断来触发周期性的操作,如更新系统时间、调度任务等。
- 网络中断:当网络数据包到达时,网络驱动程序可以触发软中断来通知内核进行网络数据包的处理和分发。
- 异常中断:当发生异常或错误情况时,例如内存访问错误或除零错误,内核可以触发软中断来处理这些异常情况。
- 调度中断:操作系统内核使用调度器软中断来进行任务调度,决定哪个任务应该在给定的时间片内执行。
RPS软中断
RPS(receive packet steering),实现了多队列网卡所提供的功能,分散了在多CPU系统上数据接收时的负载, 把软中断分到各个CPU处理,而不需要硬件支持,大大提高了网络性能。
RFS(Receive Flow Steering),是RPS补丁的扩展补丁,它把接收的数据包送达应用所在的CPU上,提高cache的命中率。
# rx-*为sudo ethtool -l eth0的最大队列
$ cat /sys/class/net/eth4/queues/rx-*/rps_cpus # 每个核处理一个rx或者全部绑定
$ cat /sys/class/net/eth4/queues/rx-*/rps_flow_cnt # 改为4096
echo "obase=2;$(grep -c processor /proc/cpuinfo)" | bc #十进制转成二进制
#!/bin/bash
# Enable RPS (Receive Packet Steering)
rfc=4096
cc=$(grep -c processor /proc/cpuinfo)
rsfe=$(echo $cc*$rfc | bc)
sysctl -w net.core.rps_sock_flow_entries=$rsfe
for fileRps in $(ls /sys/class/net/eth*/queues/rx-*/rps_cpus)
do
echo fff > $fileRps
done
for fileRfc in $(ls /sys/class/net/eth*/queues/rx-*/rps_flow_cnt)
do
echo $rfc > $fileRfc
done
tail /sys/class/net/eth*/queues/rx-*/{rps_cpus,rps_flow_cnt}
多队列udp hash
UDP流量只用Src IP和dst IP做哈希
可以决定针对不同的流量(IPv4-tcp, IPv4-udp, IPv6-tcp, Ethernet…)采用报文的哪些字段进行RSS Hash。一般对udp流量做hash。
# 查看
sudo ethtool -n eth0 rx-flow-hash udp4
# 设置
sudo ethtool -N eth0 rx-flow-hash udp4 sdfn
sdfn含义如下
m Hash on the Layer 2 destination address of the rx packet.
v Hash on the VLAN tag of the rx packet.
t Hash on the Layer 3 protocol field of the rx packet.
s Hash on the IP source address of the rx packet. (ip)
d Hash on the IP destination address of the rx packet.(ip)
f Hash on bytes 0 and 1 of the Layer 4 header of the rx packet. (port)
n Hash on bytes 2 and 3 of the Layer 4 header of the rx packet. (port)
r Discard all packets of this flow type. When this option is set, all other options are ignored.
内核优化
TCP 握手优化
握手流程中有两个队列比较关键,当队列满时多余的连接将会被丢弃。
- SYN Queue(半连接队列)是内核保持未被 ACK 的 SYN 包最大队列长度,通过内核参数
net.ipv4.tcp_max_syn_backlog设置。 - Accept Queue(全连接队列) 是 socket 上等待应用程序 accept 的最大队列长度,取值为 min(
net.ipv4.tcp_max_syn_backlog,net.core.somaxconn)。
TCP 连接保活优化
TCP 建立连接后有个发送一个空 ACK 的探测行为来保持连接(keepalive),保活机制受以下参数影响:
net.ipv4.tcp_keepalive_time最大闲置时间net.ipv4.tcp_keepalive_intvl发送探测包的时间间隔net.ipv4.tcp_keepalive_probes最大失败次数,超过此值后将通知应用层连接失效
大规模的集群内部,如果 keepalive_time 设置较短且发送较为频繁,会产生大量的空 ACK 报文,存在塞满 RingBuffer 造成 TCP 丢包甚至连接断开风险,可以适当调整 keepalive 范围减小空报文 burst 风险。
TCP 连接断开优化
由于 TCP 双全工的特性,安全关闭一个连接需要四次挥手。但复杂的网络环境中存在很多异常情况,异常断开连接会导致产生“孤儿连”,这种连接既不能发送数据,也无法接收数据,累计过多,会消耗大量系统资源,资源不足时产生Address already in use: connect类似的错误。
“孤儿连”的问题和 TIME_WAIT 紧密相关。TIME_WAIT 是 TCP 挥手的最后一个状态,当收到被动方发来的 FIN 报文后,主动方回复 ACK,表示确认对方的发送通道已经关闭,继而进入 TIME_WAIT 状态,等待 2MSL 时间后关闭连接。如果发起连接一方的 TIME_WAIT 状态过多,会占满了所有端口资源,则会导致无法创建新连接。
可以尝试调整以下参数减小 TIME_WAIT 影响:
- net.ipv4.tcp_max_tw_buckets,此数值定义系统在同一时间最多能有多少 TIME_WAIT 状态,当超过这个值时,系统会直接删掉这个 socket 而不会留下 TIME_WAIT 的状态
- net.ipv4.ip_local_port_range,TCP 建立连接时 client 会随机从该参数中定义的端口范围中选择一个作为源端口。可以调整该参数增大可选择的端口范围。
TIME_WAIT 问题在反向代理节点中出现概率较高,例如 client 传来的每一个 request,Nginx 都会向 upstream server 创建一个新连接,如果请求过多, Nginx 节点会快速积累大量 TIME_WAIT 状态的 socket,直到没有可用的本地端口,Nginx 服务就会出现不可用。
参考配置
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
内核旁路(dpdk)
高并发下网络协议栈的冗长流程是最主要的性能负担,也就是说内核才是高并发的瓶颈所在。既然内核是瓶颈所在,那很明显解决方案就是想办法绕过内核。经很多前辈先驱的研究,目前业内已经出现了很多优秀内核旁路(kernel bypass)思想的高性能网络数据处理框架,如 6WIND、Wind River、Netmap、DPDK 等。其中,Intel 的 DPDK 在众多方案脱颖而出,一骑绝尘。
DPDK(Data Plane Development Kit,数据平面开发套件) 为 Intel 处理器架构下用户空间高效的数据包处理提供了库函数和驱动的支持,它不同于 Linux 系统以通用性设计为目的,而是专注于网络应用中数据包的高性能处理。也就是 DPDK 绕过了 Linux 内核协议栈对数据包的处理过程,在用户空间实现了一套数据平面来进行数据包的收发与处理。
在内核看来,DPDK 就是一个普通的用户态进程,它的编译、连接和加载方式和普通程序没有什么两样。
- 左边是原来的方式:数据从网卡 -> 驱动 -> 协议栈 -> Socket 接口 -> 业务。
- 右边是 DPDK 方式:基于 UIO(Userspace I/O)旁路数据。数据从网卡 -> DPDK 轮询模式-> DPDK 基础库 -> 业务。