数据包接收、发送原理

image.png

发送数据包:

image.png

  1. 应⽤程序的数据包,在TCP层增加TCP报⽂头,形成可传输的数据包。
  2. 在 IP 层增加 IP 报头,形成 IP 报⽂。
  3. 经过数据⽹卡驱动程序将IP包再添加14字节的MAC头,构成frame(暂⽆CRC),frame(暂⽆CRC)中含有发送端和接收端的MAC地址。
  4. 驱动程序将 frame(暂⽆CRC)拷贝到⽹卡的缓冲区,由⽹卡处理。
  5. ⽹卡为 frame(暂⽆CRC)添加头部同步信息和CRC校验,将其封装为可以发送的packet,然后再发送到⽹线上,这样说就完成了⼀个IP报⽂的发送了,所有连接到这个⽹线上的⽹卡都可以看到该 packet。

接收数据包:
image.png

  1. ⽹卡收到⽹线上的packet,⾸先检查packet的CRC校验,保证完整性,然后将packet头去掉,得到frame。(⽹卡会检查MAC包内的⽬的MAC地址是否和本⽹卡的MAC地址⼀样,不⼀样则会丢弃。)
  2. ⽹卡将frame拷贝到预分配的ring buffer缓冲。
  3. ⽹卡驱动程序通知内核处理,经过TCP/IP协议栈层层解码处理。
  4. 应⽤程序从socket buffer 中读取数据。

核心思路

了解了收发包的原理,可以了解到丢包原因主要会涉及⽹卡设备、⽹卡驱动、内核协议栈三⼤类。以下我们将遵循“从下到上分层分析(各层可能性出现的丢包场景),然后查看关键信息,最终得出分析结果”的原则展开介绍。

硬件网卡丢包

Ring Buffer 溢出

image.png

如图所示,物理介质上的数据帧到达后首先由 NIC(网络适配器)读取,写入设备内部缓冲区 Ring Buffer 中,再由中断处理程序触发 Softirq 从中消费,Ring Buffer 的大小因网卡设备而异。当网络数据包到达(生产)的速率快于内核处理(消费)的速率时,Ring Buffer 很快会被填满,新来的数据包将被丢弃;

查看网卡 ring buffer

通过 ethtool 或 /proc/net/dev 可以查看因 Ring Buffer 满而丢弃的包统计,在统计项中以 fifo 标识:


$ ethtool -S eth0|grep rx_fifo
rx_fifo_errors: 0

$ cat /proc/net/dev
Inter-|Receive | Transmitface |bytes packets errs drop fifo frame compressed 
multicast|bytes packets errs drop fifo colls carrier compressed
eth0: 17253386680731 42839525880 0 0 0 0 0 244182022 14879545018057 41657801805 0 0 0 0 0 0

查看 eth0 网卡 Ring Buffer 最大值和当前设置

$ ethtool -g eth0

解决方案

修改网卡 eth0 接收与发送硬件缓存区大小

$ ethtool -G eth0 rx 4096 tx 4096

网卡端口协商丢包

1.查看网卡丢包统计

ethtool -S eth1/eth0
image.png

2.查看网卡配置状态

ethtool eth1/eth0
image.png

主要查看网卡和上游网络设备协商速率和模式是否符合预期;

3. 解决方案

# 1. 重新自协商
ethtool -r  eth1/eth0;

# 2. 如果上游不支持自协商,可以强制设置端口速率
ethtool -s eth1 speed 1000 duplex full autoneg off

网卡流控丢包

1. 查看流控统计

ethtool -S eth1 | grep control

image.png

rx_flow_control_xon 是在网卡的 RX Buffer 满或其他网卡内部的资源受限时,给交换机端口发送的开启流控的 pause 帧计数。对应的,tx_flow_control_xoff 是在资源可用之后发送的关闭流控的 pause 帧计数。

2.查看网络流控配置:

ethtool -a eth1

image.png

3. 解决方案

关闭网卡流控

ethtool -A ethx autoneg off //自协商关闭
ethtool -A ethx tx off //发送模块关闭
ethtool -A ethx rx off //接收模块关闭

报文 mac 地址丢包

一般计算机网卡都工作在非混杂模式下,此时网卡只接受来自网络端口的目的地址指向自己的数据,如果报文的目的 mac 地址不是对端的接口的 mac 地址,一般都会丢包,一般这种情况很有可能是源端设置静态 arp 表项或者动态学习的 arp 表项没有及时更新,但目的端 mac 地址已发生变化(换了网卡),没有更新通知到源端(比如更新报文被丢失,中间交换机异常等情况);

查看

  1. 目的端抓包,tcpdump 可以开启混杂模式,可以抓到对应的报文,然后查看 mac 地址;
  2. 源端查看 arp 表或者抓包(上一跳设备),看发送的 mac 地址是否和下一跳目的端的 mac 地址一致;

解决方案

  1. 刷新 arp 表然后发包触发 arp 重新学习(可能影响其他报文,增加延时,需要小心操作);
  2. 可以在源端手动设置正确的静态的 arp 表项;

其他网卡异常丢包

这类异常比少见,但如果都不是上面哪些情况,但网卡统计里面仍然有丢包计数,可以试着排查一下:

网卡 firmware 版本

排查一下网卡 phy 芯片 firmware 是不是有 bug,安装的版本是不是符合预期

查看

ethtool -i eth1:

image.png

网线接触不良

如果网卡统计里面存在 crc error 计数增长,很可能是网线接触不良,可以通知网管排查一下:

ethtool -S eth0

image.png

解决方案:一般试着重新插拔一下网线,或者换一根网线,排查插口是否符合端口规格等;

报文长度丢包

网卡有接收正确报文长度范围,一般正常以太网报文长度范围:64-1518,发送端正常情况会填充或者分片来适配,偶尔会发生一些异常情况导致发送报文不正常丢包;

查看

ethtool -S eth1|grep length_errors

image.png

解决方案:

  1. 调整接口MTU配置,是否开启支持以太网巨帧;
  2. 发送端开启PATH MTU进行合理分片;

思维导图

未命名文件2.png

网卡驱动丢包

ifconfig eth0
ip -s addr show eth0
[root@localhost ~]# ip -s addr show enp0s3
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:3d:18:c2 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
       valid_lft 78905sec preferred_lft 78905sec
    inet6 fe80::a00:27ff:fe3d:18c2/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
    RX: bytes  packets  errors  dropped missed  mcast   
    58670      289      0       0       0       0       
    TX: bytes  packets  errors  dropped carrier collsns 
    26843      310      0       0       0       0       

[root@localhost ~]# ifconfig enp0s3
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.0.2.15  netmask 255.255.255.0  broadcast 10.0.2.255
        inet6 fe80::a00:27ff:fe3d:18c2  prefixlen 64  scopeid 0x20<link>
        ether 08:00:27:3d:18:c2  txqueuelen 1000  (Ethernet)
        RX packets 292  bytes 58910 (57.5 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 313  bytes 27083 (26.4 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
  • RX errors:表示总的收包的错误数量,还包括 too-long-frames 错误,Ring Buffer 溢出错误,crc 校验错误,帧同步错误,fifo overruns 以及 missed pkg 等等。
  • RX dropped:表示数据包已经进入了 Ring Buffer,但是由于内存不够等系统原因,导致在拷贝到内存的过程中被丢弃。
  • RX overruns:表示了 fifo 的 overruns,这是由于 Ring Buffer(aka Driver Queue) 传输的 IO 大于 kernel 能够处理的 IO 导致的,而 Ring Buffer 则是指在发起 IRQ 请求之前的那块 buffer。很明显,overruns 的增大意味着数据包没到 Ring Buffer 就被网卡物理层给丢弃了,而 CPU 无法即使的处理中断是造成 Ring Buffer 满的原因之一,上面那台有问题的机器就是因为 interruprs 分布的不均匀(都压在 core0),没有做 affinity 而造成的丢包。
  • RX frame:表示 misaligned 的 frames。对于 TX 的来说,出现上述 counter 增大的原因主要包括 aborted transmission,errors due to carrirer,fifo error,heartbeat erros 以及 windown error,而 collisions 则表示由于 CSMA/CD 造成的传输中断。

驱动溢出丢包

netdev_max_backlog 是内核从 NIC 收到包后,交由协议栈(如IP、TCP)处理之前的缓冲队列。每个 CPU 核都有一个 backlog 队列,与 Ring Buffer 同理,当接收包的速率大于内核协议栈处理的速率时,CPU 的 backlog 队列不断增长,当达到设定的 netdev_max_backlog 值时,数据包将被丢弃。

查看

通过查看 /proc/net/softnet_stat 可以确定是否发生了 netdev backlog 队列溢出:

cat /proc/net/softnet_stat 
073ab5b9 00000000 0000009c 00000000 00000000 00000000 00000000 00000000 00000000 0177b7e4 00000000 00000000 00000000
0750f99d 00000000 000000a6 00000000 00000000 00000000 00000000 00000000 00000000 015aa79d 00000000 00000000 00000001
075d063a 00000000 000000a3 00000000 00000000 00000000 00000000 00000000 00000000 02beab5d 00000000 00000000 00000002
07ece660 00000000 000000ac 00000000 00000000 00000000 00000000 00000000 00000000 03042ee4 00000000 00000000 00000003

其中:

  • 每一行代表每个 CPU 核的状态统计,从 CPU0 依次往下;
  • 每一列代表一个 CPU 核的各项统计:
    • 第一列代表中断处理程序收到的包总数;
    • 第二列即代表由于 netdev_max_backlog 队列溢出而被丢弃的包总数。

从上面的输出可以看出,这台服务器统计中,并没有因为 netdev_max_backlog 导致的丢包。

解决方案

netdev_max_backlog 的默认值是 1000,在高速链路上,可能会出现上述第二统计不为 0 的情况,可以通过修改内核参数 net.core.netdev_max_backlog 来解决:

sysctl -w net.core.netdev_max_backlog=2000

单核负载高导致丢包

单核 CPU 软中断占有高, 导致应用没有机会收发或者收包比较慢,即使调整 netdev_max_backlog 队列大小仍然会一段时间后丢包,处理速度跟不上网卡接收的速度;

查看

mpstat -P ALL 1

image.png

单核软中断占有 100%,导致应用没有机会收发或者收包比较慢而丢包;

解决方案

1.调整网卡 RSS 队列配置:
# 查看:
ethtool -x ethx;
# 调整:
ethtool -X ethx xxxx;
2.看一下网卡中断配置是否均衡
cat /proc/interrupts

调整

1) irqbalance 调整;
# 查看当前运行情况
service irqbalance status
# 终止服务
service irqbalance stop

2) 中断绑CPU核 
echo mask > /proc/irq/xxx/smp_affinity
3.根据 CPU 和网卡队列个数调整网卡多队列和 RPS 配置

CPU 大于网卡队列个数:查看网卡队列 ethtool -x ethx;协议栈开启 RPS 并设置 RPS;

echo $mask(CPU配置)> /sys/class/net/$eth/queues/rx-$i/rps_cpus
echo 4096(网卡buff)> /sys/class/net/$eth/queues/rx-$i/rps_flow_cnt2)

CPU小于网卡队列个数,绑中断就可以,可以试着关闭RPS看一下效果:

echo 0 > /sys/class/net/<dev>/queues/rx-<n>/rps_cpu
4.numa CPU 调整

对齐网卡位置,可以提高内核处理速度,从而给更多 CPU 给应用收包,减缓丢包概率;

查看网卡 numa 位置:

ethtool -i eth1|grep bus-info
lspci -s bus-info -vv|grep node

上面中断和 RPS 设置里面 mask 需要重新按 numa CPU 分配重新设置;

5.可以试着开启中断聚合(看网卡是否支持)

查看 :

ethtool -c ethx
Coalesce parameters for eth1:
Adaptive RX: on  TX: on
stats-block-usecs: 0
sample-interval: 0
pkt-rate-low: 0
pkt-rate-high: 0

rx-usecs: 25
rx-frames: 0
rx-usecs-irq: 0
rx-frames-irq: 256

tx-usecs: 25
tx-frames: 0
tx-usecs-irq: 0
tx-frames-irq: 256

rx-usecs-low: 0
rx-frame-low: 0
tx-usecs-low: 0
tx-frame-low: 0

rx-usecs-high: 0
rx-frame-high: 0
tx-usecs-high: 0
tx-frame-high: 0

调整:

ethtool -C ethx adaptive-rx on

思维导图

640.webp 1.jpg

内核协议栈丢包

链路层丢包

neighbor 系统 arp 丢包

arp_ignore 配置丢包

arp_ignore 参数的作用是控制系统在收到外部的 arp 请求时,是否要返回 arp 响应。arp_ignore 参数常用的取值主要有 0,1,2,3~8 较少用到;

查看:

sysctl -a|grep arp_ignore
net.ipv4.conf.all.arp_ignore = 0
net.ipv4.conf.default.arp_ignore = 0
net.ipv4.conf.docker0.arp_ignore = 0
net.ipv4.conf.enp0s3.arp_ignore = 0
net.ipv4.conf.enp0s8.arp_ignore = 0
net.ipv4.conf.lo.arp_ignore = 0

解决方案:根据实际场景设置对应值;

  1. 响应任意网卡上接收到的对本机 IP 地址的 arp 请求(包括环回网卡上的地址),而不管该目的 IP 是否在接收网卡上。
  2. 只响应目的 IP 地址为接收网卡上的本地地址的 arp 请求。
  3. 只响应目的 IP 地址为接收网卡上的本地地址的 arp 请求,并且 arp 请求的源 IP 必须和接收网卡同网段。
  4. 如果 ARP 请求数据包所请求的 IP 地址对应的本地地址其作用域(scope)为主机(host),则不回应 ARP 响应数据包,如果作用域为全局(global)或链路(link),则回应 ARP 响应数据包。
arp_filter 配置丢包

在多接口系统里面(比如腾讯云的弹性网卡场景),这些接口都可以回应arp请求,导致对端有可能学到不同的mac 地址,后续报文发送可能由于 mac 地址和接收报文接口 mac 地址不一样而导致丢包,arp_filter 主要是用来适配这种场景;

查看:

sysctl -a | grep arp_filter
net.ipv4.conf.all.arp_filter = 0
net.ipv4.conf.default.arp_filter = 0
net.ipv4.conf.docker0.arp_filter = 0
net.ipv4.conf.enp0s3.arp_filter = 0
net.ipv4.conf.enp0s8.arp_filter = 0
net.ipv4.conf.lo.arp_filter = 0

解决方案:
根据实际场景设置对应的值,一般默认是关掉此过滤规则,特殊情况可以打开;

  • 0:默认值,表示回应arp请求的时候不检查接口情况;
  • 1:表示回应arp请求时会检查接口是否和接收请求接口一致,不一致就不回应;

arp 表满导致丢包

比如下面这种情况,由于突发 arp 表项很多超过协议栈默认配置,发送报文的时候部分 arp 创建失败,导致发送失败,从而丢包:

image.png

查看:
查看 arp 状态:cat /proc/net/stat/arp_cache ,table_fulls 统计:

cat /proc/net/stat/arp_cache
entries  allocs destroys hash_grows  lookups hits  res_failed  rcv_probes_mcast rcv_probes_ucast  periodic_gc_runs forced_gc_runs unresolved_discards table_fulls
00000013  00000006 00000002 00000000  00019c57 00017c9c  00000000  00000000 00000000  0001787e 00000000 00000000 00000000
00000013  0000000f 00000000 00000002  0000afc5 00009069  00000000  00000000 00000000  00000000 00000000 00000000 00000000

查看 dmesg 消息(内核打印):

dmesg|grep neighbour
neighbour: arp_cache: neighbor table overflow!

查看当前 arp 表大小:ip n|wc -l

ip n|wc -l
9

查看系统配额:

sysctl -a |grep net.ipv4.neigh.default.gc_thresh
net.ipv4.neigh.default.gc_thresh1 = 128
net.ipv4.neigh.default.gc_thresh2 = 512
net.ipv4.neigh.default.gc_thresh3 = 4096
  • gc_thresh1:存在于ARP高速缓存中的最少层数,如果少于这个数,垃圾收集器将不会运行。缺省值是128。
  • gc_thresh2:保存在 ARP 高速缓存中的最多的记录软限制。垃圾收集器在开始收集前,允许记录数超过这个数字 5 秒。缺省值是 512。
  • gc_thresh3:保存在 ARP 高速缓存中的最多记录的硬限制,一旦高速缓存中的数目高于此,垃圾收集器将马上运行。缺省值是1024。

解决方案:
根据实际 arp 最大值情况(比如访问其他子机最大个数),调整 arp 表大小

$ sudo sysctl -w net.ipv4.neigh.default.gc_thresh1=1024
$ sudo sysctl -w net.ipv4.neigh.default.gc_thresh2=2048
$ sudo sysctl -w net.ipv4.neigh.default.gc_thresh3=4096
$ sudo sysctl  -p

arp 请求缓存队列溢出丢包

查看

cat /proc/net/stat/arp_cache

# unresolved_discards是否有新增计数

解决方案:根据客户需求调整缓存队列大小 unres_qlen_bytes:

sysctl -a | grep unres_qlen_bytes
net.ipv4.neigh.default.unres_qlen_bytes = 212992
net.ipv4.neigh.docker0.unres_qlen_bytes = 212992
net.ipv4.neigh.enp0s3.unres_qlen_bytes = 212992
net.ipv4.neigh.enp0s8.unres_qlen_bytes = 212992
net.ipv4.neigh.lo.unres_qlen_bytes = 212992
net.ipv6.neigh.default.unres_qlen_bytes = 212992
net.ipv6.neigh.docker0.unres_qlen_bytes = 212992
net.ipv6.neigh.enp0s3.unres_qlen_bytes = 212992
net.ipv6.neigh.enp0s8.unres_qlen_bytes = 212992
net.ipv6.neigh.lo.unres_qlen_bytes = 212992

网络 IP 层丢包

接口 IP 地址配置丢包

本机服务不通,检查lo接口有没有配置地址是 127.0.0.1;
本机接收失败, 查看 local 路由表:ip r show table local|grep 子机 IP 地址;这种丢包一般会出现在多 IP 场景,子机底层配置多 IP 失败,导致对应 IP 收不到包而丢包;

解决方案:
配置正确接口 IP 地址;比如 ip a add 1.1.1.1 dev eth0
如果发现接口有地址还丢包,可能是 local 路由表没有对应条目,紧急情况下,可以用手工补上:比如 ip r add local 本机ip地址 dev eth0 table local ;

路由丢包

路由配置丢包

查看:
1.查看配置 路由是否设置正确(是否可达),是否配置策略路由(在弹性网卡场景会出现此配置)ip rule:

ip rule
0:	from all lookup local
32766:	from all lookup main
32767:	from all lookup default

然后找到对应路由表。查看路由表:
或者直接用 ip r get x.x.x.x,让系统帮你查找是否存在可达路由,接口是否符合预期;

2.查看系统统计信息:

netstat -s|grep "dropped because of missing route"
    157 dropped because of missing route

解决方案:重新配置正确的路由;

反向路由过滤丢包

反向路由过滤机制是 Linux 通过反向路由查询,检查收到的数据包源IP是否可路由(Loose mode)、是否最佳路由(Strict mode),如果没有通过验证,则丢弃数据包,设计的目的是防范IP地址欺骗攻击。

查看
rp_filter 提供三种模式供配置:

  • 不验证
  • RFC3704定义的严格模式:对每个收到的数据包,查询反向路由,如果数据包入口和反向路由出口不一致,则不通过
  • RFC3704定义的松散模式:对每个收到的数据包,查询反向路由,如果任何接口都不可达,则不通过

查看当前 rp_filter 策略配置:

cat /proc/sys/net/ipv4/conf/eth0/rp_filter
1

如果这里设置为1,就需要查看主机的网络环境和路由策略是否可能会导致客户端的入包无法通过反向路由验证了。

从原理来看这个机制工作在网络层,因此,如果客户端能够Ping通服务器,就能够排除这个因素了。

解决方案:
根据实际网络环境将 rp_filter 设置为 0 或 2:

$ sysctl -w net.ipv4.conf.all.rp_filter=2或
$ sysctl -w net.ipv4.conf.eth0.rp_filter=2

防火墙丢包

客户设置规则导致丢包

iptables -nvL |grep DROP
Chain FORWARD (policy DROP 0 packets, 0 bytes)
    0     0 DROP       all  --  *      docker0  0.0.0.0/0            0.0.0.0/0           
    0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes firewall for dropping marked packets */ mark match 0x8000/0x8000
    0     0 DROP       all  --  *      *      !127.0.0.0/8          127.0.0.0/8          /* block incoming localnet connections */ ! ctstate RELATED,ESTABLISHED,DNAT

解决方案:修改防火墙规则;

连接跟踪导致丢包

连接跟踪表溢出丢包

kernel 用 ip_conntrack 模块来记录 iptables 网络包的状态,并把每条记录保存到 table 里(这个 table 在内存里,可以通过 /proc/net/ip_conntrack 查看当前已经记录的总数),如果网络状况繁忙,比如高连接,高并发连接等会导致逐步占用这个 table 可用空间,一般这个 table 很大不容易占满并且可以自己清理,table 的记录会一直呆在 table 里占用空间直到源 IP 发一个 RST 包,但是如果出现被攻击、错误的网络配置、有问题的路由/路由器、有问题的网卡等情况的时候,就会导致源 IP 发的这个 RST 包收不到,这样就积累在 table 里,越积累越多直到占满。无论,哪种情况导致 table 变满,满了以后就会丢包,出现外部无法连接服务器的情况。内核会报如下错误信息:kernel: ip_conntrack: table full,dropping packet;

查看当前连接跟踪数 :

cat /proc/sys/net/netfilter/nf_conntrack_max
131072

解决方案:


# 增大跟踪的最大条数
net.netfilter.nf_conntrack_max  = 3276800
# 减少跟踪连接的最大有效时间
net.netfilter.nf_conntrack_tcp_timeout_established = 1200
net.netfilter.nf_conntrack_udp_timeout_stream = 180
net.netfilter.nf_conntrack_icmp_timeout = 30
ct 创建冲突失导致丢包

查看:当前连接跟踪统计:cat /proc/net/stat/nf_conntrack,可以查各种 ct 异常丢包统计
解决方案:内核热补丁修复或者更新内核版本(合入补丁修改);

传输层 UDP/TCP 丢包

TCP连接跟踪安全检查丢包

丢包原因:由于连接没有断开,但服务端或者client之前出现过发包异常等情况(报文没有经过连接跟踪模块更新窗口计数),没有更新合法的window范围,导致后续报文安全检查被丢包;协议栈用nf_conntrack_tcp_be_liberal 来控制这个选项:

  • 1:关闭,只有不在 TCP 窗口内的rst包被标志为无效;
  • 0:开启,所有不在 TCP 窗口中的包都被标志为无效;

查看:
查看配置 :

sysctl -a|grep nf_conntrack_tcp_be_liberal 
net.netfilter.nf_conntrack_tcp_be_liberal = 1

查看 log:
一般情况下 netfiler 模块默认没有加载 log,需要手动加载;

modprobe ipt_LOG11
sysctl -w net.netfilter.nf_log.2=ipt_LOG

然后发包后在查看 syslog;
解决方案:根据实际抓包分析情况判断是不是此机制导致的丢包,可以试着关闭试一下;

分片重组丢包

情况总结:超时

查看:

netstat -s|grep timeout
601 fragments dropped after timeout

解决方法:调整超时时间

net.ipv4.ipfrag_time = 30
sysctl -w net.ipv4.ipfrag_time=60

frag_high_thresh,分片的内存超过一定阈值会导致系统安全检查丢包
查看:

netstat -s|grep reassembles
8094 packet reassembles failed

解决方案:调整大小

net.ipv4.ipfrag_high_thresh 
net.ipv4.ipfrag_low_thresh

分片安全距检查离丢包

查看:

netstat -s|grep reassembles
8094 packet reassembles failed

解决方案:把 ipfrag_max_dist 设置为 0,就关掉此安全检查

image.png

系统内存不足,创建新分片队列失败

查看方法:

netstat -s|grep reassembles
8094 packet reassembles failed

dropwatch 查看丢包位置 :

dnf install dropwatch
dropwatch -l kas
dropwatch> start
ctrl + c

解决方案:
a.增大系统网络内存:

net.core.rmem_default 
net.core.rmem_max 
net.core.wmem_default

b.系统回收内存:
紧急情况下,可以用 /proc/sys/vm/drop_caches,去释放一下虚拟内存;

To free pagecache:
# echo 1 > /proc/sys/vm/drop_caches
To free dentries and inodes:
# echo 2 > /proc/sys/vm/drop_caches
To free pagecache, dentries and inodes:
echo 3 > /proc/sys/vm/drop_caches

MTU 丢包

image.png

查看:
检查接口 MTU 配置,ifconfig eth1/eth0,默认是1500;
进行 MTU 探测,然后设置接口对应的MTU值;

解决方案:
根据实际情况,设置正确 MTU 值;
设置合理的 tcp mss,启用 TCP MTU Probe:

cat /proc/sys/net/ipv4/tcp_mtu_probing:
tcp_mtu_probing - INTEGER Controls TCP Packetization-Layer Path MTU Discovery.
Takes three values:
0 - Disabled 
1 - Disabled by default, enabled when an ICMP black hole detected
2 - Always enabled, use initial MSS of tcp_base_mss.

TCP 层丢包

TIME_WAIT 过多丢包

大量 TIMEWAIT 出现,并且需要解决的场景,在高并发短连接的 TCP 服务器上,当服务器处理完请求后立刻按照主动正常关闭连接。。。这个场景下,会出现大量 socket 处于 TIMEWAIT 状态。如果客户端的并发量持续很高,此时部分客户端就会显示连接不上;

查看:
查看系统 log :

dmsg
TCP: time wait bucket table overflow;

查看系统配置:

sysctl -a|grep tcp_max_tw_buckets
net.ipv4.tcp_max_tw_buckets = 16384

解决方案:

  1. tw_reuse,tw_recycle 必须在客户端和服务端 timestamps 开启时才管用(默认打开)
  2. tw_reuse 只对客户端起作用,开启后客户端在 1s 内回收;
  3. tw_recycle 对客户端和服务器同时起作用,开启后在 3.5*RTO 内回收,RTO 200ms~120s具体时间视网络状况。内网状况比 tw_reuse 稍快,公网尤其移动网络大多要比 tw_reuse 慢,优点就是能够回收服务端的 TIME_WAIT 数量;在服务端,如果网络路径会经过 NAT 节点,不要启用 net.ipv4.tcp_tw_recycle,会导致时间戳混乱,引起其他丢包问题;
  4. 调整 tcp_max_tw_buckets 大小,如果内存足够:
sysctl -w net.ipv4.tcp_max_tw_buckets=163840;
时间戳异常丢包

当多个客户端处于同一个 NAT 环境时,同时访问服务器,不同客户端的时间可能不一致,此时服务端接收到同一个 NAT 发送的请求,就会出现时间戳错乱的现象,于是后面的数据包就被丢弃了,具体的表现通常是是客户端明明发送的 SYN,但服务端就是不响应 ACK。在服务器借助下面的命令可以来确认数据包是否有不断被丢弃的现象。

检查:
netstat -s | grep rejects
解决方案:
如果网络路径会经过 NAT 节点,不要启用 net.ipv4.tcp_tw_recycle;

TCP队列问题导致丢包

原理:
TCP 状态机(三次握手)
查看:
抓包分析一下网络 RTT:

用其他工具测试一下当前端到端网络质量(hping等);

HPING 9.199.10.104 (bond1 9.199.10.104): SA set, 40 headers + 0 data bytes
len=46 ip=9.199.10.104 ttl=53 DF id=47617 sport=0 flags=R seq=0 win=0 rtt=38.3 ms
len=46 ip=9.199.10.104 ttl=53 DF id=47658 sport=0 flags=R seq=1 win=0 rtt=38.3 ms
len=46 ip=9.199.10.104 ttl=53 DF id=47739 sport=0 flags=R seq=2 win=0 rtt=30.4 ms
len=46 ip=9.199.10.104 ttl=53 DF id=47842 sport=0 flags=R seq=3 win=0 rtt=30.4 ms
len=46 ip=9.199.10.104 ttl=53 DF id=48485 sport=0 flags=R seq=4 win=0 rtt=38.7 ms
len=46 ip=9.199.10.104 ttl=53 DF id=49274 sport=0 flags=R seq=5 win=0 rtt=34.1 ms
len=46 ip=9.199.10.104 ttl=53 DF id=49491 sport=0 flags=R seq=6 win=0 rtt=30.3 ms

解决方案:
关闭 Nagle 算法,减少小包延迟;
关闭延迟 ack:

sysctl -w net.ipv4.tcp_no_delay_ack=1
TCP 乱序丢包

此时TCP会无法判断是数据包丢失还是乱序,因为丢包和乱序都会导致接收端收到次序混乱的数据包,造成接收端的数据空洞。TCP会将这种情况暂定为数据包的乱序,因为乱序是时间问题(可能是数据包的迟到),而丢包则意味着重传。

当TCP意识到包出现乱序的情况时,会立即ACK,该ACK的TSER部分包含的TSEV值会记录当前接收端收到有序报文段的时刻。这会使得数据包的RTT样本值增大,进一步导致RTO时间延长。这对TCP来说无疑是有益的,因为TCP有充分的时间判断数据包到底是失序还是丢了来防止不必要的数据重传。当然严重的乱序则会让发送端以为是丢包一旦重复的ACK超过TCP的阈值,便会触发超时重传机制,以及时解决这种问题;详细请参考博客:

https://blog.csdn.net/dog250/article/details/78692585

查看:抓包分析是否存在很多乱序报文:
image.png

解决方案:如果在多径传输场景或者网络质量不好,可以通过修改下面值来提供系统对TCP无序传送的容错率:

sysctl -a | grep tcp_reordering 
net.ipv4.tcp_reordering = 3
拥塞控制丢包

在互联网发展的过程当中,TCP算法也做出了一定改变,先后演进了
Reno、NewReno、Cubic和Vegas,这些改进算法大体可以分为基于丢包和基于延时的拥塞控制算法。基于丢包的拥塞控制算法以Reno、NewReno为代表,它的主要问题有Buffer bloat和长肥管道两种,基于丢包的协议拥塞控制机制是被动式的,其依据网络中的丢包事件来做网络拥塞判断。即使网络中的负载很高,只要没有产生拥塞丢包,协议就不会主动降低自己的发送速度。最初路由器转发出口的Buffer 是比较小的,TCP在利用时容易造成全局同步,降低带宽利用率,随后路由器厂家由于硬件成本下降不断地增加Buffer,基于丢包反馈的协议在不丢包的情况下持续占用路由器buffer,虽然提高了网络带宽的利用率,但同时也意味着发生拥塞丢包后,网络抖动性加大。

另外对于带宽和RTT都很高的长肥管道问题来说,管道中随机丢包的可能性很大,TCP的默认buffer设置比较小加上随机丢包造成的cwnd经常下折,导致带宽利用率依旧很低;BBR(Bottleneck Bandwidth and Round-trip propagation time)是一种基于带宽和延迟反馈的拥塞控制算法。目前已经演化到第二版,是一个典型的封闭反馈系统,发送多少报文和用多快的速度发送这些报文都是在每次反馈中不断调节。在BBR提出之前,拥塞控制都是基于事件的算法,需要通过丢包或延时事件驱动;BBR提出之后,拥塞控制是基于反馈的自主自动控制算法,对于速率的控制是由算法决定,而不由网络事件决定,BBR算法的核心是找到最大带宽(Max BW)和最小延时(Min RTT)这两个参数,最大带宽和最小延时的乘积可以得到BDP(Bandwidth Delay Product), 而BDP就是网络链路中可以存放数据的最大容量。BDP驱动Probing State Machine得到Rate quantum和cwnd,分别设置到发送引擎中就可以解决发送速度和数据量的问题。

Linux 4.9内核首次采用BBR拥塞控制算法第一个版本,BBR抗丢包能力比其他算法要强,但这个版本在某些场景下面有问题(缺点),BBR在实时音视频领域存在的问题,深队列竞争不过Cubic。
问题现象就是:在深队列场景,BBR 的 ProbeRTT 阶段只发4个包,发送速率下降太多会引发延迟加大和卡顿问题。

ss -sti //在源端 ss -sti|grep 10.125.42.49:47699 -A 3 ( 10.125.42.49:47699 是目的端地址和端口号)

image.png

image.png

解决方案:

  • ProbeRTT 并不适用实时音视频领域,因此可以选择直接去除,或者像BBRV2把probe RTT 缩短到 2.5s 一次,使用 0.5xBDP 发送;
  • 如果没有特殊需求,切换成稳定的cubic算法;

UDP 层丢包

收发包失败丢包

查看:netstat 统计
如果有持续的 receive buffer errors/send buffer errors 计数;

netstat -s |grep errors
    0 packet receive errors
    0 receive buffer errors
    0 send buffer errors

解决方案:

  • CPU 负载(多核绑核配置),网络负载(软中断优化,调整驱动队列netdev_max_backlog),内存配置(协议栈内存);
  • 按峰值在来,增大buffer缓存区大小:
net.ipv4.udp_mem = xxx
net.ipv4.udp_rmem_min = xxx
net.ipv4.udp_wmem_min = xxx

调整应用设计:

  • UDP 本身就是无连接不可靠的协议,适用于报文偶尔丢失也不影响程序状态的场景,比如视频、音频、游戏、监控等。
    对报文可靠性要求比较高的应用不要使用 UDP,推荐直接使用 TCP。
    当然,也可以在应用层做重试、去重保证可靠性
  • 如果发现服务器丢包,首先通过监控查看系统负载是否过高,先想办法把负载降低再看丢包问题是否消失
  • 如果系统负载过高,UDP丢包是没有有效解决方案的。
    如果是应用异常导致 CPU、memory、IO 过高,请及时定位异常应用并修复;
    如果是资源不够,监控应该能及时发现并快速扩容
  • 对于系统大量接收或者发送 UDP 报文的,可以通过调节系统和程序的 socket buffer size 来降低丢包的概率
  • 应用程序在处理UDP报文时,要采用异步方式,在两次接收报文之间不要有太多的处理逻辑

应用层 socket 丢包

socket 缓存区接收丢包

查看:

  • 抓包分析是否存在丢包情况;
  • 查看统计
netstat -s|grep "packet receive errors"

解决方案:
调整 socket 缓冲区大小:

socket配置(所有协议socket):

# Default Socket Receive Buffer
net.core.rmem_default = 31457280
# Maximum Socket Receive Buffer
net.core.rmem_max = 67108864

具体大小调整原理:

缓冲区大小没有任何设置值是最佳的,因为最佳大小随具体情况而不同
缓冲区估算原理:在数据通信中,带宽时延乘积(英语:bandwidth-delay product;或称带宽延时乘积、带宽延时积等)指的是一个数据链路的能力(每秒比特)与来回通信延迟(单位秒)的乘积。[1][2]其结果是以比特(或字节)为单位的一个数据总量,等同在任何特定时间该网络线路上的最大数据量——已发送但尚未确认的数据。

BDP = 带宽 * RTT

可以通过计算当面节点带宽和统计平均时延来估算 BDP,即缓冲区的大小,可以参考下面常见场景估计:

image.png

参考:https://docs.oracle.com/cd/E56344_01/html/E53803/gnkor.html

应用设置 tcp 连接数大小丢包

查看:
请参考上面TCP连接队列分析;

解决方案:
设置合理的连接队列大小,当第三次握手时,当 server 接收到 ACK 报之后, 会进入一个新的叫 accept 的队列,该队列的长度为 min(backlog, somaxconn),默认情况下,somaxconn 的值为 128,表示最多有 129 的 ESTAB 的连接等待 accept(),而 backlog 的值则应该是由 int listen(int sockfd, int backlog) 中的第二个参数指定,listen 里面的 backlog 可以有我们的应用程序去定义的;

应用发送太快导致丢包

查看统计:

netstat -s|grep "send buffer errors

解决方案:
ICMP/UDP 没有流控机制,需要应用设计合理发送方式和速度,照顾到底层 buff 大小和 CPU 负载以及网络带宽质量;

设置合理的 sock 缓冲区大小:

setsockopt(s,SOL_SOCKET,SO_SNDBUF,  i(const char*)&nSendBuf,sizeof(int));

调整系统 socket 缓冲区大小:

# Default Socket Send Buffer
net.core.wmem_default = 31457280
# Maximum Socket Send Buffer
net.core.wmem_max = 33554432

思维导图

640.webp 2.jpg

相关工具介绍

1. dropwatch 工具

原理:监听 kfree_skb(把网络报文丢弃时会调用该函数)函数或者事件吗,然后打印对应调用堆栈;想要详细了解 linux 系统在执行哪个函数时丢包的话,可以使用 dropwatch 工具,它监听系统丢包信息,并打印出丢包发生的函数:

2.Tcpdump工具

原理:tcpdump 是一个 Unix 下一个功能强大的网络抓包工具,它允许用户拦截和显示发送或收到过网络连接到该计算机的 TCP/IP 和其他数据包
image.png

Linux 丢包那些事

最近一直在排查一些网络的问题,比如 connect timeout 、read timeout 以及一些丢包的问题,刚好想整理一些东西,方便和团队内及开发分享。

我们先看下 Linux 系统接收数据包的过程:

image.png

  1. 网卡收到数据包。
  2. 将数据包从网卡硬件缓存转移到服务器内存中。
  3. 通知内核处理。
  4. 经过 TCP/IP 协议逐层处理。
  5. 应用程序通过 read() 从 socket buffer 读取数据。

网卡丢包

我们先看下ifconfig的输出:

$ ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.5.224.27  netmask 255.255.255.0  broadcast 10.5.224.255
        inet6 fe80::5054:ff:fea4:44ae  prefixlen 64  scopeid 0x20<link>
        ether 52:54:00:a4:44:ae  txqueuelen 1000  (Ethernet)
        RX packets 9525661556  bytes 10963926751740 (9.9 TiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8801210220  bytes 12331600148587 (11.2 TiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

$ ip -s addr show eth0
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 52:54:00:a4:32:6f brd ff:ff:ff:ff:ff:ff
    inet 10.0.8.6/22 brd 10.0.11.255 scope global noprefixroute eth0
       valid_lft forever preferred_lft forever
    inet 10.0.8.100/32 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fea4:326f/64 scope link 
       valid_lft forever preferred_lft forever
    RX: bytes  packets  errors  dropped overrun mcast   
    42800747904 183630464 0       0       0       0       
    TX: bytes  packets  errors  dropped carrier collsns 
    32887492062 213574143 0       0       0       0       

RX(receive) 代表接收报文, TX(transmit) 表示发送报文。

  • RX errors: 表示总的收包的错误数量,这包括 too-long-frames 错误,Ring Buffer 溢出错误,crc 校验错误,帧同步错误,fifo overruns 以及 missed pkg 等等。
  • RX dropped: 表示数据包已经进入了 Ring Buffer,但是由于内存不够等系统原因,导致在拷贝到内存的过程中被丢弃。
  • RX overruns: 表示了 fifo 的 overruns,这是由于 Ring Buffer(aka Driver Queue) 传输的 IO 大于 kernel 能够处理的 IO 导致的,而 Ring Buffer 则是指在发起 IRQ 请求之前的那块 buffer。很明显,overruns 的增大意味着数据包没到 Ring Buffer 就被网卡物理层给丢弃了,而 CPU 无法及时的处理中断是造成 Ring Buffer 满的原因之一,上面那台有问题的机器就是因为 interruprs 分布的不均匀(都压在 core0),没有做 affinity 而造成的丢包。
  • RX frame: 表示 misaligned 的 frames。

对于 TX 的来说,出现上述 counter 增大的原因主要包括 aborted transmission, errors due to carrirer, fifo error, heartbeat erros 以及 windown error,而 collisions 则表示由于 CSMA/CD 造成的传输中断。

dropped 与 overruns 的区别:

  • dropped,表示这个数据包已经进入到网卡的接收缓存 fifo 队列,并且开始被系统中断处理准备进行数据包拷贝(从网卡缓存 fifo 队列拷贝到系统内存),但由于此时的系统原因(比如内存不够等)导致这个数据包被丢掉,即这个数据包被 Linux 系统丢掉。
  • overruns,表示这个数据包还没有被进入到网卡的接收缓存 fifo 队列就被丢掉,因此此时网卡的 fifo 是满的。为什么 fifo 会是满的?因为系统繁忙,来不及响应网卡中断,导致网卡里的数据包没有及时的拷贝到系统内存, fifo 是满的就导致后面的数据包进不来,即这个数据包被网卡硬件丢掉。所以,个人觉得遇到 overruns 非0,需要检测cpu负载与cpu中断情况。

netstat -i也会提供每个网卡的接发报文以及丢包的情况:

netstat -i
Kernel Interface table
Iface             MTU    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
cni0             1450 30323570      0      0 0      34356925      0      0      0 BMRU
docker0          1500        0      0      0 0             0      0      0      0 BMU
eth0             1500 183767060     0      0 0      213744723     0      0      0 BMRU
flannel.1        1450     2407      0      0 0          2407      0    548      0 BMRU
lo              65536 125753157     0      0 0      125753157     0      0      0 LRU
veth438a2717     1450    20532      0      0 0         18209      0      0      0 BMRU
vethbaa10764     1450   109255      0      0 0        109286      0      0      0 BMRU
vethcd8e1791     1450   279804      0      0 0        245335      0      0      0 BMRU

Ring Buffer 溢出

如果硬件或者驱动没有问题,一般网卡丢包是因为设置的缓存区(ring buffer)太小。当网络数据包到达(生产)的速率快于内核处理(消费)的速率时, Ring Buffer 很快会被填满,新来的数据包将被丢弃。

image.png

通过 ethtool 或 /proc/net/dev 可以查看因Ring Buffer满而丢弃的包统计,在统计项中以fifo标识:

# ethtool -S eth0|grep rx_fifo
rx_fifo_errors: 0
# cat /proc/net/dev
Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
  eth0: 10967216557060 9528860597    0    0    0     0          0         0 12336087749362 8804108661    0    0    0     0       0          0

如果发现服务器上某个网卡的 fifo 数持续增大,可以去确认 CPU 中断是否分配均匀,也可以尝试增加 Ring Buffer 的大小,通过 ethtool 可以查看网卡设备 Ring Buffer 最大值,修改 Ring Buffer 当前设置:

# 查看eth0网卡Ring Buffer最大值和当前设置
$ ethtool -g eth0
Ring parameters for eth0:

Pre-set maximums:
RX:     4096   
RX Mini:    0
RX Jumbo:   0
TX:     4096   
Current hardware settings:
RX:     1024   
RX Mini:    0
RX Jumbo:   0
TX:     1024   
# 修改网卡eth0接收与发送硬件缓存区大小
$ ethtool -G eth0 rx 4096 tx 4096
Pre-set maximums:
RX:     4096   
RX Mini:    0
RX Jumbo:   0
TX:     4096   
Current hardware settings:
RX:     4096   
RX Mini:    0
RX Jumbo:   0
TX:     4096

netdev_max_backlog 溢出

netdev_max_backlog 是内核从 NIC 收到包后,交由协议栈(如 IP、TCP )处理之前的缓冲队列。每个 CPU 核都有一个 backlog 队列,与 Ring Buffer 同理,当接收包的速率大于内核协议栈处理的速率时, CPU 的 backlog 队列不断增长,当达到设定的 netdev_max_backlog 值时,数据包将被丢弃。

# cat /proc/net/softnet_stat 
2e8f1058 00000000 000000ef 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0db6297e 00000000 00000035 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
09d4a634 00000000 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0773e4f1 00000000 00000005 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

其中: 每一行代表每个 CPU 核的状态统计,从 CPU0 依次往下; 每一列代表一个 CPU 核的各项统计:第一列代表中断处理程序收到的包总数;第二列即代表由于 netdev_max_backlog 队列溢出而被丢弃的包总数。 从上面的输出可以看出,这台服务器统计中,确实有因为 netdev_max_backlog 导致的丢包。

netdev_max_backlog 的默认值是 1000,在高速链路上,可能会出现上述第二列统计不为 0 的情况,可以通过修改内核参数 net.core.netdev_max_backlog 来解决:

sysctl -w net.core.netdev_max_backlog=2000

Socket Buffer 溢出

Socket 可以屏蔽 linux 内核不同协议的差异,为应用程序提供统一的访问接口。每个 Socket 都有一个读写缓存区。

  • 读缓冲区,缓存远端发来的数据。如果读缓存区已满,就不能再接收新的数据。
  • 写缓冲区,缓存了要发出去的数据。如果写缓冲区已满,应用程序的写操作就会阻塞。

image.png

半连接队列和全连接队列溢出

之前有个 connect timeout 的 case ,这篇博客里,我也详细介绍了如何去查看半连接队列和全连接队列,包括如何去优化,这里我不展开写。
但是补充一点,在半连接满的情况下,若启用syncookie机制,并不会直接丢弃 SYN 包,而是回复带有 syncookie 的 SYN+ACK 包,设计的目的是防范 SYN Flood 造成正常请求服务不可用。 syncookie 之前也有一篇博客分享,参考 谈谈 syn-cookie 的问题.

PAWS

PAWS 全名 Protect Againest Wrapped Sequence numbers ,目的是解决在高带宽下, TCP 序列号在一次会话中可能被重复使用而带来的问题。

image.png

如上图所示,客户端发送的序列号为 A 的数据包 A1 因某些原因在网络中“迷路”,在一定时间没有到达服务端,客户端超时重传序列号为 A 的数据包 A2 ,接下来假设带宽足够,传输用尽序列号空间,重新使用 A ,此时服务端等待的是序列号为 A 的数据包 A3 ,而恰巧此时前面“迷路”的 A1 到达服务端,如果服务端仅靠序列号A就判断数据包合法,就会将错误的数据传递到用户态程序,造成程序异常。

PAWS 要解决的就是上述问题,它依赖于 timestamp 机制,理论依据是:在一条正常的 TCP 流中,按序接收到的所有 TCP 数据包中的 timestamp 都应该是单调非递减的,这样就能判断那些 timestamp 小于当前 TCP 流已处理的最大 timestamp 值的报文是延迟到达的重复报文,可以予以丢弃。在上文的例子中,服务器已经处理数据包 Z,而后到来的 A1 包的 timestamp 必然小于 Z 包的 timestamp ,因此服务端会丢弃迟到的 A1 包,等待正确的报文到来。

PAWS 机制的实现关键是内核保存了 Per-Connection 的最近接收时间戳,如果加以改进,就可以用来优化服务器TIME_WAIT状态的快速回收。

TIME_WAIT 状态是TCP四次挥手中主动关闭连接的一方需要进入的最后一个状态,并且通常需要在该状态保持 2*MSL (报文最大生存时间),它存在的意义有两个:

  • 可靠地实现 TCP 全双工连接的关闭:关闭连接的四次挥手过程中,最终的 ACK 由主动关闭连接的一方(称为 A )发出,如果这个 ACK 丢失,对端(称为 B )将重发 FIN ,如果 A 不维持连接的 TIME_WAIT 状态,而是直接进入 CLOSED ,则无法重传 ACK , B 端的连接因此不能及时可靠释放。
  • 等待“迷路”的重复数据包在网络中因生存时间到期消失:通信双方 A 与 B , A 的数据包因“迷路”没有及时到达 B , A 会重发数据包,当 A 与 B 完成传输并断开连接后,如果 A 不维持 TIME_WAIT 状态 2MSL 时间,便有可能与 B 再次建立相同源端口和目的端口的“新连接”,而前一次连接中“迷路”的报文有可能在这时到达,并被 B 接收处理,造成异常,维持 2MSL 的目的就是等待前一次连接的数据包在网络中消失。

TIME_WAIT 状态的连接需要占用服务器内存资源维持, Linux 内核提供了一个参数来控制 TIME_WAIT 状态的快速回收:tcp_tw_recycle,它的理论依据是:

  • 在 PAWS 的理论基础上,如果内核保存 Per-Host 的最近接收时间戳,接收数据包时进行时间戳比对,就能避免 TIME_WAIT 意图解决的第二个问题:前一个连接的数据包在新连接中被当做有效数据包处理的情况。这样就没有必要维持

TIME_WAIT 状态 2*MSL 的时间来等待数据包消失,仅需要等待足够的 RTO (超时重传),解决 ACK 丢失需要重传的情况,来达到快速回收 TIME_WAIT 状态连接的目的。

在 PAWS 的理论基础上,如果内核保存 Per-Host 的最近接收时间戳,接收数据包时进行时间戳比对,就能避免 TIME_WAIT 意图解决的第二个问题:前一个连接的数据包在新连接中被当做有效数据包处理的情况。这样就没有必要维持 TIME_WAIT 状态 2*MSL 的时间来等待数据包消失,仅需要等待足够的 RTO (超时重传),解决 ACK 丢失需要重传的情况,来达到快速回收 TIME_WAIT 状态连接的目的。

但上述理论在多个客户端使用 NAT 访问服务器时会产生新的问题:同一个 NAT 背后的多个客户端时间戳是很难保持一致的( timestamp 机制使用的是系统启动相对时间),对于服务器来说,两台客户端主机各自建立的 TCP 连接表现为同一个对端IP的两个连接,按照 Per-Host 记录的最近接收时间戳会更新为两台客户端主机中时间戳较大的那个,而时间戳相对较小的客户端发出的所有数据包对服务器来说都是这台主机已过期的重复数据,因此会直接丢弃。

通过netstat可以得到因PAWS机制timestamp验证被丢弃的数据包统计:

# netstat -s |grep -e "passive connections rejected because of time stamp" -e "packets rejects in established connections because of timestamp”
387158 passive connections rejected because of time stamp
825313 packets rejects in established connections because of timestamp

通过sysctl查看是否启用了 tcp_tw_recycle 及 tcp_timestamp :

$ sysctl net.ipv4.tcp_tw_recycle
net.ipv4.tcp_tw_recycle = 1
$ sysctl net.ipv4.tcp_timestamps
net.ipv4.tcp_timestamps = 1

如果服务器作为服务端提供服务,且明确客户端会通过 NAT 网络访问,或服务器之前有7层转发设备会替换客户端源IP时,是不应该开启 tcp_tw_recycle 的,而 timestamps 除了支持 tcp_tw_recycle 外还被其他机制依赖,推荐继续开启:

sysctl -w net.ipv4.tcp_tw_recycle=0
sysctl -w net.ipv4.tcp_timestamps=1

总结

image.png

linux 网络协议栈太深,每一层都有可能出现各种各样的问题,我们需要了解这些原理,同时利用好工具去排查这些问题。同时我们在优化的时候,不要盲目的看别人的优化结果,更重要的是体系化的去了解 linux 协议栈的实现,只有知其所以然,才能结合实际业务特点,得出最合理的优化配置。

最后我按照倪鹏飞之前的优化,整理了一个表格,方便参考(数值仅供参考,具体配置还需要结合实际场景来调整):

image.png

关于网卡中断不均衡问题及其解决方案

前不久生产碰到一个故障,一台宿主机上出现了大量的丢包,对业务造成了比较大的影响,遇到的问题还是蛮值得记录下来,所以简单的整理了下。

什么是中断

CPU 工作的模式有两种,一种是中断,由各种设备发起;一种是轮询,由 CPU 主动发起。

我们主要看下中断。
中断又分为两种:一种硬中断;一种软中断。硬中断是由硬件产生的,比如,像磁盘,网卡,键盘;软中断是由当前正在运行的进程所产生的。

中断,是一种由硬件产生的电信号直接发送到中断控制器上,然后由中断控制器向 CPU 发送信号,CPU 检测到该信号后,会中断当前的工作转而去处理中断。然后,处理器会通知内核已经产生中断,这样内核就会对这个中断进行适当的处理。

举个例子:当网卡收到数据包时会产生中断请求通知到 CPU,CPU 会中断当前正在运行的任务,然后通知内核有新数据包,内核调用中断处理程序进行响应,把数据包从网卡缓存及时拷贝到内存,否则会因为缓存溢出被丢弃。剩下的处理和操作数据包的工作就会交给软中断。

什么是多队列网卡

我们已经理解了中断,可是当网卡不断的接收数据包,就会产生很多中断,CPU 又如何能满足需求呢?
答案就是多队列网卡。

RSS(Receive Side Scaling)是网卡的硬件特性,实现了多队列。通过多队列网卡驱动加载,获取网卡型号,得到网卡的硬件 queue 的数量,并结合 CPU 核的数量,最终通过 Sum=Min(网卡 queue,CPU core)得出所要激活的网卡 queue 数量。

然后将各个 queue 中断分布到 CPU 多个核上,实现负载均衡,避免了单个核被占用到 100% 而其他核还处于空闲的情况。同一数据流会始终在同一 CPU 上,避免 TCP 的顺序性和 CPU 的并行性的冲突。基于流的负载均衡,解决了顺序协议和 CPU 并行的冲突以及 cache 热度问题。

多队列需要网卡硬件的支持。如果服务器的网卡支持 RSS,会在系统中看到网卡对应多个发送和接收队列:

root@SVR:~  # ls /sys/class/net/eth0/queues/
rx-0   rx-13  rx-18  rx-22  rx-27  rx-31  rx-36  rx-5  tx-0   tx-13  tx-18  tx-22  tx-27  tx-31  tx-36  tx-5
rx-1   rx-14  rx-19  rx-23  rx-28  rx-32  rx-37  rx-6  tx-1   tx-14  tx-19  tx-23  tx-28  tx-32  tx-37  tx-6
rx-10  rx-15  rx-2   rx-24  rx-29  rx-33  rx-38  rx-7  tx-10  tx-15  tx-2   tx-24  tx-29  tx-33  tx-38  tx-7
rx-11  rx-16  rx-20  rx-25  rx-3   rx-34  rx-39  rx-8  tx-11  tx-16  tx-20  tx-25  tx-3   tx-34  tx-39  tx-8
rx-12  rx-17  rx-21  rx-26  rx-30  rx-35  rx-4   rx-9  tx-12  tx-17  tx-21  tx-26  tx-30  tx-35  tx-4   tx-9

irq 亲缘绑定

/proc/interrupts 文件中可以看到各个 CPU 上的中断情况。

/proc/irq/[irq_num]/smp_affinity_list 可以查看指定中断当前绑定的 CPU。

我们主要关注网卡中断的 CPU 情况:

cat /proc/interrupts | grep mlx5_comp | cut -d: -f1 | while read i; do echo -ne irq":$i\t bind_cpu: "; cat /proc/irq/$i/smp_affinity_list; done | sort -n -t' ' -k3

可以看到绑定的是一组 CPU。

我们再查看下具体的中断情况:

# cat /proc/interrupts | grep mlx5_comp | tr -s ' ' '\t'|cut -f 1-15
	88:	0	0	0	0	0	0	0	0	0	0	1577579474	0	0
	89:	0	0	0	0	0	0	0	0	0	0	3677007833	1	0
	90:	0	0	0	0	0	0	0	0	0	0	1084594339	0	1
	91:	0	0	0	0	0	0	0	0	0	0	2274784560	0	0
	92:	0	0	0	0	0	0	0	0	0	0	1141602063	0	0
	93:	0	0	0	0	0	0	0	0	0	0	2788053592	0	0
	94:	0	0	0	0	0	0	0	0	0	0	2929294228	0	0
	95:	0	0	0	0	0	0	0	0	0	0	3437304297	0	0
	96:	0	0	0	0	0	0	0	0	0	0	4209729499	0	0
	97:	0	0	0	0	0	0	0	0	0	0	296747534	0	0
	98:	0	0	0	0	0	0	0	0	0	0	2753415640	0	0
	99:	0	0	0	0	0	0	0	0	0	0	2542583067	0	0
	100:	0	0	0	0	0	0	0	0	0	0	3961165575	0	0
	101:	0	0	0	0	0	0	0	0	0	0	1548441749	0	0
	102:	0	0	0	0	0	0	0	0	0	0	2205752712	0	0
	103:	0	0	0	0	0	0	0	0	0	0	3285051802	0	0
	112:	0	0	0	0	0	0	0	0	0	0	2039544026	0	0
	113:	0	0	0	0	0	0	0	0	0	0	994776868	0	0
	114:	0	0	0	0	0	0	0	0	0	0	3212436506	0	0
	115:	0	0	0	0	0	0	0	0	0	0	615608559	0	0
	116:	2	0	0	0	0	0	0	0	0	0	1387799451	0	0
	117:	0	1	0	0	0	0	0	0	0	0	1704941483	0	0
	118:	0	0	1	0	0	0	0	0	0	0	1605432559	0	0
	119:	0	0	0	1	0	0	0	0	0	0	3932507321	0	0
	120:	0	0	0	0	1	0	0	0	0	0	1610073075	0	0
	121:	0	0	0	0	0	1	0	0	0	0	1114634191	0	0
	122:	0	0	0	0	0	0	1	0	0	0	3338484401	0	0
	123:	0	0	0	0	0	0	0	1	0	0	1982853886	0	0

可以看到中断都是被 CPU10 处理的。查看 mellanox 的资料 what-is-irq-affinity-x

也就是说这里配置了 10-19,30-39,但是只有 CPU10 在处理中断。

丢包的原因

上面已经分析了中断都是被 CPU10 处理了,我们再查看下 softnet_stat 的情况:

# cat /proc/net/softnet_stat
dcc6cc07 00006e12 000001ef 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
199c3900 00000000 0000001b 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
e576cfcb 00000000 00000016 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
c3c17454 00000000 00000013 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
abb700af 00000000 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
98ceb017 00000000 0000000c 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
89c0460b 00000000 0000000c 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
7d3d0e6a 00000000 0000000e 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
724680ee 00000000 00000008 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
68f63700 00000000 00000013 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
b4f004b0 30b8190e 00180514 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
bdfe8473 00037a32 0000022a 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
8b1c5f73 0001a1ee 000001b5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
f107894f 0000e7f9 00000181 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
7663ef78 0000a29f 00000120 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
76275161 00008367 000000e2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
......

其中: 每一行代表每个 CPU 核的状态统计,从 CPU0 依次往下; 每一列代表一个 CPU 核的各项统计:第一列代表中断处理程序收到的包总数;第二列即代表由于 netdev_max_backlog 队列溢出而被丢弃的包总数。

可以看到 CPU10 有大量的丢包现象(第二列数据)。

对 softnet_stat 数据我们做了一些监控,可以看到的定期会有丢包的现象。

image.png

如何处理

netdev_max_backlog 调优

netdev_max_backlog 是内核从 NIC 收到包后,交由协议栈(如 IP、TCP )处理之前的缓冲队列。

从这次现象看,因为超越了设定的 netdev_max_backlog 值,导致数据包被丢弃。netdev_max_backlog 的默认值是 1000,我们可以修改内核参数来调优:

sysctl -w net.core.netdev_max_backlog=2000

网卡中断均衡

调优 netdev_max_backlog 只是一个 workaround 方案,本质还是因为网卡中断分配不均导致,所以我们需要讲中断均衡,方案如下:

网卡绑定

网卡绑定就是将网卡中断手动绑定到不同 CPU,前面简单介绍了下 smp_affinity_list,我们再来看下。

/proc/irq/[irq_num]/smp_affinity:

该文件存放的是 CPU 位掩码(十六进制)。修改该文件中的值可以改变 CPU 和某中断的亲和性。

/proc/irq/[irq_num]/smp_affinity_list:

该文件存放的是 CPU 列表(十进制)。注意,CPU 核心个数用表示编号从 0 开始,如 CPU0, CPU1 等。

所以我们手动的对网卡的各个队列进行 CPU 绑定,如下:

echo 0 > /proc/irq/101/smp_affinity_list
echo 0 > /proc/irq/102/smp_affinity_list
echo 1 > /proc/irq/103/smp_affinity_list
echo 1 > /proc/irq/104/smp_affinity_list
echo 2 > /proc/irq/105/smp_affinity_list
echo 2 > /proc/irq/106/smp_affinity_list
......

中断绑定后, 我们查看 /proc/interrupts 就可以看到中断会分布在 CPU 多个核上。

35:	492825	0	0	0	0	0	0	0	0	0	0	0	0
36:	0	421113	0	0	0	0	0	0	0	0	0	0	0
37:	0	0	384418	0	0	0	0	0	0	0	0	0	0
38:	0	0	0	371091	0	0	0	0	0	0	0	0	0
39:	0	0	0	0	362977	0	0	0	0	0	0	0	0
40:	0	0	0	0	0	352131	0	0	0	0	0	0	0
41:	0	0	0	0	0	0	326605	0	0	0	0	0	0
42:	0	0	0	0	0	0	0	366171	0	0	0	0	0
43:	0	0	0	0	0	0	0	0	353674	0	0	0	0
44:	0	0	0	0	0	0	0	0	0	321370	0	0	0
45:	0	0	0	0	0	0	0	0	0	0	342732	0	0
46:	0	0	0	0	0	0	0	0	0	0	0	321248	0
47:	0	0	0	0	0	0	0	0	0	0	0	0	324321

irqbalance 和 minx_tune

这两块我想作为扩展阅读,后面提供一些资料,对于 irqbalance 补充一点,irqbalance 是根据系统中断负载的情况,自动迁移中断保持中断的平衡,同时会考虑到省电因素等等。但是在实时系统中会导致中断自动漂移,对性能造成不稳定因素,在高性能的场合建议关闭。