plantegg

java tcp mysql performance network docker Linux


  • 首页

  • 分类

  • 归档

  • 标签

  • 站点地图

  • 关于

网络硬件相关知识

发表于 2021-01-01 | 分类于 network

网络硬件相关知识

程序员很难有机会接触到底层的一些东西,尤其是偏硬件部分,所以记录下

光纤和普通网线的性能差异

以下都是在4.19内核的UOS,光纤交换机为锐捷,服务器是华为鲲鹏920的环境测试所得数据:

image.png

光纤稳定性好很多,平均rt是网线的三分之一,最大值则是网线的十分之一. 上述场景下光纤的带宽大约是网线的1.5倍. 实际光纤理论带宽一般都是万M, 网线是千M.

光纤接口:

image.png

单模光纤和多模光纤

下图绿色是多模光纤(Multi Mode Fiber),黄色是单模光纤(Single Mode Fiber), 因为光纤最好能和光模块匹配, 我们测试用的光模块都是多模的, 单模光纤线便宜,但是对应的光模块贵多了。

image-20230227152302800

多模光模块工作波长为850nm,单模光模块工作波长为1310nm或1550nm, 从成本上来看,单模光模块所使用的设备多出多模光模块两倍,总体成本远高于多模光模块,但单模光模块的传输距离也要长于多模光模块,单模光模块最远传输距离为100km,多模光模块最远传输距离为2km。因单模光纤的传输原理为使光纤直射到中心,所以主要用作远距离数据传输,而多模光纤则为多通路传播模式,所以主要用于短距离数据传输。单模光模块适用于对距离和传输速率要求较高的大型网络中,多模光模块主要用于短途网路。

image-20210831211315077

ping结果比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
[aliyun@uos15 11:00 /home/aliyun]  以下88都是光口、89都是电口。
$ping -c 10 10.88.88.16 //光纤
PING 10.88.88.16 (10.88.88.16) 56(84) bytes of data.
64 bytes from 10.88.88.16: icmp_seq=1 ttl=64 time=0.058 ms
64 bytes from 10.88.88.16: icmp_seq=2 ttl=64 time=0.049 ms
64 bytes from 10.88.88.16: icmp_seq=3 ttl=64 time=0.053 ms
64 bytes from 10.88.88.16: icmp_seq=4 ttl=64 time=0.040 ms
64 bytes from 10.88.88.16: icmp_seq=5 ttl=64 time=0.053 ms
64 bytes from 10.88.88.16: icmp_seq=6 ttl=64 time=0.043 ms
64 bytes from 10.88.88.16: icmp_seq=7 ttl=64 time=0.038 ms
64 bytes from 10.88.88.16: icmp_seq=8 ttl=64 time=0.050 ms
64 bytes from 10.88.88.16: icmp_seq=9 ttl=64 time=0.043 ms
64 bytes from 10.88.88.16: icmp_seq=10 ttl=64 time=0.064 ms

--- 10.88.88.16 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 159ms
rtt min/avg/max/mdev = 0.038/0.049/0.064/0.008 ms

[aliyun@uos15 11:01 /home/aliyun]
$ping -c 10 10.88.89.16 //电口
PING 10.88.89.16 (10.88.89.16) 56(84) bytes of data.
64 bytes from 10.88.89.16: icmp_seq=1 ttl=64 time=0.087 ms
64 bytes from 10.88.89.16: icmp_seq=2 ttl=64 time=0.053 ms
64 bytes from 10.88.89.16: icmp_seq=3 ttl=64 time=0.095 ms
64 bytes from 10.88.89.16: icmp_seq=4 ttl=64 time=0.391 ms
64 bytes from 10.88.89.16: icmp_seq=5 ttl=64 time=0.051 ms
64 bytes from 10.88.89.16: icmp_seq=6 ttl=64 time=0.343 ms
64 bytes from 10.88.89.16: icmp_seq=7 ttl=64 time=0.045 ms
64 bytes from 10.88.89.16: icmp_seq=8 ttl=64 time=0.341 ms
64 bytes from 10.88.89.16: icmp_seq=9 ttl=64 time=0.054 ms
64 bytes from 10.88.89.16: icmp_seq=10 ttl=64 time=0.066 ms

--- 10.88.89.16 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 149ms
rtt min/avg/max/mdev = 0.045/0.152/0.391/0.136 ms

[aliyun@uos15 11:02 /u01]
$scp uos.tar aliyun@10.88.89.16:/tmp/
uos.tar 100% 3743MB 111.8MB/s 00:33

[aliyun@uos15 11:03 /u01]
$scp uos.tar aliyun@10.88.88.16:/tmp/
uos.tar 100% 3743MB 178.7MB/s 00:20

[aliyun@uos15 11:07 /u01]
$sudo ping -f 10.88.89.16
PING 10.88.89.16 (10.88.89.16) 56(84) bytes of data.
--- 10.88.89.16 ping statistics ---
284504 packets transmitted, 284504 received, 0% packet loss, time 702ms
rtt min/avg/max/mdev = 0.019/0.040/1.014/0.013 ms, ipg/ewma 0.048/0.042 ms

[aliyun@uos15 11:07 /u01]
$sudo ping -f 10.88.88.16
PING 10.88.88.16 (10.88.88.16) 56(84) bytes of data.
--- 10.88.88.16 ping statistics ---
299748 packets transmitted, 299748 received, 0% packet loss, time 242ms
rtt min/avg/max/mdev = 0.012/0.016/0.406/0.006 ms, pipe 2, ipg/ewma 0.034/0.014 ms

另外还要考虑网卡和光模块的带宽匹配,一般万兆网卡插上2.5万兆的光模块是无法联通的

多网卡bonding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#cat ifcfg-bond0
DEVICE=bond0
TYPE=Bond
ONBOOT=yes
BOOTPROTO=static
IPADDR=10.176.7.11
NETMASK=255.255.255.0

#cat /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0
TYPE=Ethernet
ONBOOT=yes
BOOTPROTO=none
MASTER=bond0
SLAVE=yes

#cat /etc/sysconfig/network-scripts/ifcfg-eth1
DEVICE=eth1
TYPE=Ethernet
ONBOOT=yes
BOOTPROTO=none
MASTER=bond0
SLAVE=yes

#cat /proc/net/bonding/bond0

----加载内核bonding模块, mode=0 是RR负载均衡模式
#cat /etc/modprobe.d/bonding.conf
# modprobe bonding
alias bond0 bonding
options bond0 mode=0 miimon=100 //这一行也可以放到bond0配置文件中,比如:BONDING_OPTS="miimon=100 mode=4 xmit_hash_policy=layer3+4" 用iperf 多连接测试bonding后的带宽发现,发送端能用上两张网卡,但是接收队列只能使用一张物理网卡

网卡绑定mode共有七种(0~6) bond0、bond1、bond2、bond3、bond4、bond5、bond6

常用的有三种

  • mode=0:平衡负载模式 **(balance-rr)**,有自动备援,两块物理网卡和bond网卡使用同一个mac地址,但需要”Switch”支援及设定。

  • mode=1:自动备援模式 **(balance-backup)**,其中一条线若断线,其他线路将会自动备援。

  • mode=6:平衡负载模式**(balance-alb)**,有自动备援,不必”Switch”支援及设定,两块网卡是使用不同的MAC地址

  • Mode 4 (802.3ad): This mode creates aggregation groups that share the same speed and duplex settings, and it requires a switch that supports an IEEE 802.3ad dynamic link. Mode 4 uses all interfaces in the active aggregation group. For example, you can aggregate three 1 GB per second (GBPS) ports into a 3 GBPS trunk port. This is equivalent to having one interface with 3 GBPS speed. It provides fault tolerance and load balancing.

需要说明的是如果想做成mode 0的负载均衡,仅仅设置这里options bond0 miimon=100 mode=0是不够的,与网卡相连的交换机必须做特殊配置(这两个端口应该采取聚合方式),因为做bonding的这两块网卡是使用同一个MAC地址.从原理分析一下(bond运行在mode 0下):

mode 0下bond所绑定的网卡的IP都被修改成相同的mac地址,如果这些网卡都被接在同一个交换机,那么交换机的arp表里这个mac地址对应的端口就有多 个,那么交换机接受到发往这个mac地址的包应该往哪个端口转发呢?正常情况下mac地址是全球唯一的,一个mac地址对应多个端口肯定使交换机迷惑了。所以 mode0下的bond如果连接到交换机,交换机这几个端口应该采取聚合方式(cisco称为 ethernetchannel,foundry称为portgroup),因为交换机做了聚合后,聚合下的几个端口也被捆绑成一个mac地址.我们的解决办法是,两个网卡接入不同的交换机即可。

mode6模式下无需配置交换机,因为做bonding的这两块网卡是使用不同的MAC地址。

mod=5,即:(balance-tlb) Adaptive transmit load balancing(适配器传输负载均衡)

特点:不需要任何特别的switch(交换机)支持的通道bonding。在每个slave上根据当前的负载(根据速度计算)分配外出流量。如果正在接受数据的slave出故障了,另一个slave接管失败的slave的MAC地址。

该模式的必要条件:ethtool支持获取每个slave的速率.

案例,两块万兆bonding后带宽翻倍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#ethtool bond0
Settings for bond0:
Supported ports: [ ]
Supported link modes: Not reported
Supported pause frame use: No
Supports auto-negotiation: No
Advertised link modes: Not reported
Advertised pause frame use: No
Advertised auto-negotiation: No
Speed: 20000Mb/s
Duplex: Full
Port: Other
PHYAD: 0
Transceiver: internal
Auto-negotiation: off
Link detected: yes

[root@phy 16:55 /root]
#cat /etc/sysconfig/network-scripts/ifcfg-bond0
DEVICE=bond0
BOOTPROTO=static
TYPE="ethernet"
IPADDR=100.1.1.2
NETMASK=255.255.255.192
ONBOOT=yes
USERCTL=no
PEERDNS=no
BONDING_OPTS="miimon=100 mode=4 xmit_hash_policy=layer3+4"

#cat /etc/modprobe.d/bonding.conf
alias netdev-bond0 bonding

#lsmod |grep bond
bonding 137339 0

#cat ifcfg-bond0
DEVICE=bond0
BOOTPROTO=static
TYPE="ethernet"
IPADDR=100.81.131.221
NETMASK=255.255.255.192
ONBOOT=yes
USERCTL=no
PEERDNS=no
BONDING_OPTS="miimon=100 mode=4 xmit_hash_policy=layer3+4"

#cat ifcfg-eth1
DEVICE=eth1
TYPE="Ethernet"
HWADDR=7C:D3:0A:E0:F7:81
BOOTPROTO=none
ONBOOT=yes
MASTER=bond0
SLAVE=yes
PEERDNS=no
RX_MAX=`ethtool -g "$DEVICE" | grep 'Pre-set' -A1 | awk '/RX/{print $2}'`
RX_CURRENT=`ethtool -g "$DEVICE" | grep "Current" -A1 | awk '/RX/{print $2}'`
[[ "$RX_CURRENT" -lt "$RX_MAX" ]] && ethtool -G "$DEVICE" rx "$RX_MAX"

网络中断和绑核

网络包的描述符的内存(RingBuffer)跟着设备走(设备在哪个Die/Node上,就近分配内存), 数据缓冲区(Data Buffer–存放网络包)内存跟着队列(中断)走, 如果队列绑定到DIE0, 而设备在die1上,这样在做DMA通信时, 会产生跨die的交织访问。

不管设备插在哪一个die上, 只要描述符申请的内存和数据缓冲区的内存都在同一个die上(需要修改驱动源代码–非常规),就能避免跨die内存交织, 性能能保持一致。

irqbalance服务不会将中断进行跨node迁移,只会在同一numa node中进行优化。

ethtool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ethtool -i p1p1   //查询网卡bus-info
driver: mlx5_core
version: 5.0-0
firmware-version: 14.27.1016 (MT_2420110004)
expansion-rom-version:
bus-info: 0000:21:00.0
supports-statistics: yes
supports-test: yes
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: yes

//根据bus-info找到中断id
#cat /proc/interrupts | grep 0000:21:00.0 | awk -F: '{print $1}' | wc -l

//修改网卡队列数
sudo ethtool -L eth0 combined 2 (不能超过网卡最大队列数)

然后检查是否生效了(不需要重启应用和机器,实时生效):
sudo ethtool -l eth0

根据网卡bus-info可以找到对应的irq id

手工绑核脚本:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
#irq_list=(`cat /proc/interrupts | grep enp131s0 | awk -F: '{print $1}'`)
intf=$1
irq_list=(cat /proc/interrupts | grep `ethtool -i $intf |grep bus-info | awk '{ print $2 }'` | awk -F: '{print $1}')
cpunum=48 # 修改为所在node的第一个Core
for irq in ${irq_list[@]}
do
echo $cpunum > /proc/irq/$irq/smp_affinity_list
echo `cat /proc/irq/$irq/smp_affinity_list`
(( cpunum+=1 ))
done

检查绑定结果: sh irqCheck.sh enp131s0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 网卡名
intf=$1
irqID=`ethtool -i $intf |grep bus-info | awk '{ print $2 }'`
log=irqSet-`date "+%Y%m%d-%H%M%S"`.log
# 可用的CPU数
cpuNum=$(cat /proc/cpuinfo |grep processor -c)
# RX TX中断列表
irqListRx=$(cat /proc/interrupts | grep ${irqID} | awk -F':' '{print $1}')
irqListTx=$(cat /proc/interrupts | grep ${irqID} | awk -F':' '{print $1}')
# 绑定接收中断rx irq
for irqRX in ${irqListRx[@]}
do
cat /proc/irq/${irqRX}/smp_affinity_list
done
# 绑定发送中断tx irq
for irqTX in ${irqListTx[@]}
do
cat /proc/irq/${irqTX}/smp_affinity_list
done

中断联合(Coalescing)

中断联合可供我们推迟向内核通告新事件的操作,将多个事件汇总在一个中断中通知内核。该功能的当前设置可通过ethtool -c查看:

1
2
3
4
5
$ ethtool -c eth0
Coalesce parameters for eth0:
...
rx-usecs: 50
tx-usecs: 50

此处可以设置固定上限,对每内核每秒处理中断数量的最大值进行硬性限制,或针对特定硬件根据吞吐率自动调整中断速率。

启用联合(使用 -C)会增大延迟并可能导致丢包,因此对延迟敏感的工作可能需要避免这样做。另外,彻底禁用该功能可能导致中断受到节流限制,进而影响性能。

多次在nginx场景下测试未发现这个值对TPS有什么明显的改善

How to achieve low latency with 10Gbps Ethernet 中有提到 Linux 3.11 added support for the SO_BUSY_POLL socket option. 也有类似的作用

irqbalance

irqbalance 是一个命令行工具,在处理器中分配硬件中断以提高系统性能。默认设置下在后台程序运行,但只可通过 --oneshot 选项运行一次。

以下参数可用于提高性能。

  • –powerthresh

    CPU 进入节能模式之前,设定可空闲的 CPU 数量。如果有大于阀值数量的 CPU 是大于一个标准的偏差,该差值低于平均软中断工作负载,以及没有 CPU 是大于一个标准偏差,且该偏差高出平均,并有多于一个的 irq 分配给它们,一个 CPU 将处于节能模式。在节能模式中,CPU 不是 irqbalance 的一部分,所以它在有必要时才会被唤醒。

  • –hintpolicy

    决定如何解决 irq 内核关联提示。有效值为 exact(总是应用 irq 关联提示)、subset (irq 是平衡的,但分配的对象是关联提示的子集)、或者 ignore(irq 完全被忽略)。

  • –policyscript

    通过设备路径、当作参数的irq号码以及 irqbalance 预期的零退出代码,定义脚本位置以执行每个中断请求。定义的脚本能指定零或多键值对来指导管理传递的 irq 中 irqbalance。下列是为效键值对:ban有效值为 true(从平衡中排除传递的 irq)或 false(该 irq 表现平衡)。balance_level允许用户重写传递的 irq 平衡度。默认设置下,平衡度基于拥有 irq 设备的 PCI 设备种类。有效值为 none、package、cache、或 core。numa_node允许用户重写视作为本地传送 irq 的 NUMA 节点。如果本地节点的信息没有限定于 ACPI ,则设备被视作与所有节点距离相等。有效值为识别特定 NUMA 节点的整数(从0开始)和 -1,规定 irq 应被视作与所有节点距离相等。

  • –banirq

    将带有指定中断请求号码的中断添加至禁止中断的列表。

也可以使用 IRQBALANCE_BANNED_CPUS 环境变量来指定被 irqbalance 忽略的 CPU 掩码。

1
2
3
4
5
//默认irqbalance绑定一个numa, -1指定多个numa
echo -1 >/sys/bus/pci/devices/`ethtool -i p1p1 |grep bus-info | awk '{ print $2 }'`/numa_node ;
// 目录 /sys/class/net/p1p1/ link到了 /sys/bus/pci/devices/`ethtool -i p1p1 |grep bus-info | awk '{ print $2 }'`

执行 irqbalance --debug 进行调试

irqbalance指定core

1
2
3
4
5
6
7
8
9
10
11
12
13
cat /etc/sysconfig/irqbalance
# IRQBALANCE_BANNED_CPUS
# 64 bit bitmask which allows you to indicate which cpu's should
# be skipped when reblancing irqs. Cpu numbers which have their
# corresponding bits set to one in this mask will not have any
# irq's assigned to them on rebalance
#绑定软中断到8-15core, 每位表示4core
#IRQBALANCE_BANNED_CPUS=ffffffff,ffff00ff
#绑定软中断到8-15core和第65core
IRQBALANCE_BANNED_CPUS=ffffffff,fffffdff,ffffffff,ffff00ff

#96core 鲲鹏920下绑前16core
IRQBALANCE_BANNED_CPUS=ffffffff,ffffffff,ffff0000

irqbalance的流程

初始化的过程只是建立链表的过程,暂不描述,只考虑正常运行状态时的流程
-处理间隔是10s
-清除所有中断的负载值
-/proc/interrupts读取中断,并记录中断数
-/proc/stat读取每个cpu的负载,并依次计算每个层次每个节点的负载以及每个中断的负载
-通过平衡算法找出需要重新分配的中断
-把需要重新分配的中断加入到新的节点中
-配置smp_affinity使处理生效

irqbalance服务不会将中断进行跨node迁移,只会在同一numa node中进行优化。

网卡软中断以及内存远近的测试结论

一般网卡中断会占用一些CPU,如果把网卡中断挪到其它node的core上,在鲲鹏920上测试(网卡插在node0上),业务跑在node3,网卡中断分别在node0和node3,QPS分别是:179000 VS 175000

如果将业务跑在node0上,网卡中断分别在node0和node1上得到的QPS分别是:204000 VS 212000

以上测试的时候业务进程分配的内存全限制在node0上

1
2
3
4
5
6
7
8
9
10
#/root/numa-maps-summary.pl </proc/123853/numa_maps
N0 : 5085548 ( 19.40 GB)
N1 : 4479 ( 0.02 GB)
N2 : 1 ( 0.00 GB)
active : 0 ( 0.00 GB)
anon : 5085455 ( 19.40 GB)
dirty : 5085455 ( 19.40 GB)
kernelpagesize_kB: 2176 ( 0.01 GB)
mapmax : 348 ( 0.00 GB)
mapped : 4626 ( 0.02 GB)

从以上测试数据可以看到在这个内存分布场景下,如果就近访问内存性能有20%以上的提升

一般默认申请的data buffer也都在设备所在的numa节点上, 如果将队列的中断绑定到其他cpu上, 那么队列申请的data buffer的节点也会跟着中断迁移。

阿里云绑核脚本

通常情况下,Linux的网卡中断是由一个CPU核心来处理的,当承担高流量的场景下,会出现一些诡异的情况(网卡尚未达到瓶颈,但是却出现丢包的情况)

这种时候,我们最好看下网卡中断是不是缺少调优。

优化3要点:网卡多队列+irq affinity亲缘性设置+关闭irqbalance (systemctl stop irqbalance)

目前阿里云官方提供的centos和ubuntu镜像里面,已经自带了优化脚本,内容如下:

centos7的脚本路径在 /usr/sbin/ecs_mq_rps_rfs 具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/bin/bash
# This is the default setting of networking multiqueue and irq affinity
# 1. enable multiqueue if available
# 2. irq affinity optimization
# 3. stop irqbalance service
# set and check multiqueue

function set_check_multiqueue()
{
eth=$1
log_file=$2
queue_num=$(ethtool -l $eth | grep -ia5 'pre-set' | grep -i combined | awk {'print $2'})
if [ $queue_num -gt 1 ]; then
# set multiqueue
ethtool -L $eth combined $queue_num
# check multiqueue setting
cur_q_num=$(ethtool -l $eth | grep -iA5 current | grep -i combined | awk {'print $2'})
if [ "X$queue_num" != "X$cur_q_num" ]; then
echo "Failed to set $eth queue size to $queue_num" >> $log_file
echo "after setting, pre-set queue num: $queue_num , current: $cur_q_num" >> $log_file
return 1
else
echo "OK. set $eth queue size to $queue_num" >> $log_file
fi
else
echo "only support $queue_num queue; no need to enable multiqueue on $eth" >> $log_file
fi
}

#set irq affinity
function set_irq_smpaffinity()
{
log_file=$1
node_dir=/sys/devices/system/node
for i in $(ls -d $node_dir/node*); do
i=${i/*node/}
done

echo "max node :$i" >> $log_file
node_cpumax=$(cat /sys/devices/system/node/node${i}/cpulist |awk -F- '{print $NF}')
irqs=($(cat /proc/interrupts |grep virtio |grep put | awk -F: '{print $1}'))
core=0
for irq in ${irqs[@]};do
VEC=$core
if [ $VEC -ge 32 ];then
let "IDX = $VEC / 32"
MASK_FILL=""
MASK_ZERO="00000000"
for ((i=1; i<=$IDX;i++))
do
MASK_FILL="${MASK_FILL},${MASK_ZERO}"
done
let "VEC -= 32 * $IDX"
MASK_TMP=$((1<<$VEC))
MASK=$(printf "%X%s" $MASK_TMP $MASK_FILL)
else
MASK_TMP=$((1<<$VEC))
MASK=$(printf "%X" $MASK_TMP)
fi
echo $MASK > /proc/irq/$irq/smp_affinity
echo "mask:$MASK, irq:$irq" >> $log_file
core=$(((core+1)%(node_cpumax+1)))
done
}

# stop irqbalance service
function stop_irqblance()
{
log_file=$1
ret=0
if [ "X" != "X$(ps -ef | grep irqbalance | grep -v grep)" ]; then
if which systemctl;then
systemctl stop irqbalance
else
service irqbalance stop
fi
if [ $? -ne 0 ]; then
echo "Failed to stop irqbalance" >> $log_file
ret=1
fi
else
echo "OK. irqbalance stoped." >> $log_file
fi
return $ret
}
# main logic
function main()
{
ecs_network_log=/var/log/ecs_network_optimization.log
ret_value=0
echo "running $0" > $ecs_network_log
echo "======== ECS network setting starts $(date +'%Y-%m-%d %H:%M:%S') ========" >> $ecs_network_log
# we assume your NIC interface(s) is/are like eth*
eth_dirs=$(ls -d /sys/class/net/eth*)
if [ "X$eth_dirs" = "X" ]; then
echo "ERROR! can not find any ethX in /sys/class/net/ dir." >> $ecs_network_log
ret_value=1
fi
for i in $eth_dirs
do
cur_eth=$(basename $i)
echo "optimize network performance: current device $cur_eth" >> $ecs_network_log
# only optimize virtio_net device
driver=$(basename $(readlink $i/device/driver))
if ! echo $driver | grep -q virtio; then
echo "ignore device $cur_eth with driver $driver" >> $ecs_network_log
continue
fi
echo "set and check multiqueue on $cur_eth" >> $ecs_network_log
set_check_multiqueue $cur_eth $ecs_network_log
if [ $? -ne 0 ]; then
echo "Failed to set multiqueue on $cur_eth" >> $ecs_network_log
ret_value=1
fi
done
stop_irqblance $ecs_network_log
set_irq_smpaffinity $ecs_network_log
echo "======== ECS network setting END $(date +'%Y-%m-%d %H:%M:%S') ========" >> $ecs_network_log
return $ret_value
}


# program starts here
main
exit $?

查询的rps绑定情况的脚本 get_rps.sh

1
2
3
4
5
6
#!/bin/bash
# 获取当前rps情况
for i in $(ls /sys/class/net/eth0/queues/rx-*/rps_cpus); do
echo $i
cat $i
done

RSS 和 RPS

  • RSS:即receive side steering,利用网卡的多队列特性,将每个核分别跟网卡的一个首发队列绑定,以达到网卡硬中断和软中断均衡的负载在各个CPU上。他要求网卡必须要支持多队列特性。
  • RPS:receive packet steering,他把收到的packet依据一定的hash规则给hash到不同的CPU上去,以达到各个CPU负载均衡的目的。他只是把软中断做负载均衡,不去改变硬中断。因而对网卡没有任何要求。
  • RFS:receive flow steering,RFS需要依赖于RPS,他跟RPS不同的是不再简单的依据packet来做hash,而是根据flow的特性,即application在哪个核上来运行去做hash,从而使得有更好的数据局部性。

RSS

image-20221125190002856

设置 RPS,首先内核要开启CONFIG_RPS编译选项,然后设置需要将中断分配到哪些CPU:

1
2
#cat /sys/class/net/eth3/queues/rx-[0-7]/rps_cpus
#cat /sys/class/net/eth3/queues/tx-[0-7]/xps_cpus

我们可以看到很多案例,使用这些特性后提醒了网络包的处理能力,从而提升QPS,降低RT。

Image

如上图所示,数据包在进入内核IP/TCP协议栈之前,经历了这些步骤:

  1. 网口(NIC)收到packets
  2. 网口通过DMA(Direct memeory access)将数据写入到内存(RAM)中。
  3. 网口通过RSS(网卡多队列)将收到的数据包分发给某个rx队列,并触发该队列所绑定核上的CPU中断。
  4. 收到中断的核,调用该核所在的内核软中断线程(softirqd)进行后续处理。
  5. softirqd负责将数据包从RAM中取到内核中。
  6. 如果开启了RPS,RPS会选择一个目标cpu核来处理该包,如果目标核非当前正在运行的核,则会触发目标核的IPI(处理器之间中断),并将数据包放在目标核的backlog队列中。
  7. 软中断线程将数据包(数据包可能来源于第5步、或第6步),通过gro(generic receive offload,如果开启的话)等处理后,送往IP协议栈,及之后的TCP/UDP等协议栈。

查看网卡和numa的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#yum install lshw -y
#lshw -C network -short
H/W path Device Class Description
=============================================================
/0/100/0/9/0 eth0 network MT27710 Family [ConnectX-4 Lx]
/0/100/0/9/0.1 eth1 network MT27710 Family [ConnectX-4 Lx]
/1 e41358fae4ee_h network Ethernet interface
/2 86b0637ef1e1_h network Ethernet interface
/3 a6706e785f53_h network Ethernet interface
/4 d351290e50a0_h network Ethernet interface
/5 1a9e5df98dd1_h network Ethernet interface
/6 766ec0dab599_h network Ethernet interface
/7 bond0.11 network Ethernet interface
/8 ea004888c217_h network Ethernet interface

以及:

1
2
3
lscpu | grep -i numa
numactl --hardware
cat /proc/interrupts | egrep -i "CPU|rx"

Check if the network interfaces are tied to Numa (if -1 means not tied, if 0, then to numa0):

1
cat /sys/class/net/eth0/device/numa_node

You can see which NAMA the network card belongs to, for example, using lstopo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
yum install hwloc -y
lstopo
lstopo --logical
lstopo --logical --output-format png > lstopo.png

--
[root@hygon3 10:58 /root] //hygon 7280 CPU
#lstopo --logical
Machine (503GB total) //总内存大小
NUMANode L#0 (P#0 252GB) //socket0、numa0 的内存大小
Package L#0
L3 L#0 (8192KB) //L3 cache,对应4个物理core,8个HT
L2 L#0 (512KB) + L1d L#0 (32KB) + L1i L#0 (64KB) + Core L#0 // L1/L2
PU L#0 (P#0)
PU L#1 (P#64)
L2 L#1 (512KB) + L1d L#1 (32KB) + L1i L#1 (64KB) + Core L#1
PU L#2 (P#1)
PU L#3 (P#65)
L2 L#2 (512KB) + L1d L#2 (32KB) + L1i L#2 (64KB) + Core L#2
PU L#4 (P#2)
PU L#5 (P#66)
L2 L#3 (512KB) + L1d L#3 (32KB) + L1i L#3 (64KB) + Core L#3
PU L#6 (P#3)
PU L#7 (P#67)
L3 L#1 (8192KB)
L3 L#2 (8192KB)
L3 L#3 (8192KB)
L3 L#4 (8192KB)
L3 L#5 (8192KB)
L3 L#6 (8192KB)
L3 L#7 (8192KB)
HostBridge L#0
PCIBridge
PCIBridge
PCI 1a03:2000
GPU L#0 "controlD64"
GPU L#1 "card0"
PCIBridge
PCI 1d94:7901
Block(Disk) L#2 "sdm" //ssd系统盘,接在Node0上,绑核有优势
HostBridge L#4
PCIBridge
PCI 1000:0097
PCIBridge
PCI 1c5f:000d
PCIBridge
PCI 1c5f:000d
HostBridge L#8
PCIBridge
PCI 15b3:1015
Net L#3 "p1p1" //万兆网卡接在Node0上
PCI 15b3:1015
Net L#4 "p1p2"
HostBridge L#10
PCIBridge
PCI 8086:1521
Net L#5 "em1" //千兆网卡接在Node0上
PCI 8086:1521
Net L#6 "em2"
NUMANode L#1 (P#1 251GB) //另外一个socket
Package L#1
L3 L#8 (8192KB)
L2 L#32 (512KB) + L1d L#32 (32KB) + L1i L#32 (64KB) + Core L#32

----------- FT2500 两路共128core
#lstopo-no-graphics --logical
Machine (503GB total)
Package L#0 + L3 L#0 (64MB)
NUMANode L#0 (P#0 31GB)
L2 L#0 (2048KB) //4个物理core共享2M
L1d L#0 (32KB) + L1i L#0 (32KB) + Core L#0 + PU L#0 (P#0)
L1d L#1 (32KB) + L1i L#1 (32KB) + Core L#1 + PU L#1 (P#1)
L1d L#2 (32KB) + L1i L#2 (32KB) + Core L#2 + PU L#2 (P#2)
L1d L#3 (32KB) + L1i L#3 (32KB) + Core L#3 + PU L#3 (P#3)
L2 L#1 (2048KB)
L1d L#4 (32KB) + L1i L#4 (32KB) + Core L#4 + PU L#4 (P#4)
L1d L#5 (32KB) + L1i L#5 (32KB) + Core L#5 + PU L#5 (P#5)
L1d L#6 (32KB) + L1i L#6 (32KB) + Core L#6 + PU L#6 (P#6)
L1d L#7 (32KB) + L1i L#7 (32KB) + Core L#7 + PU L#7 (P#7)
HostBridge L#0
PCIBridge
PCIBridge
PCIBridge
PCI 1000:00ac
Block(Disk) L#0 "sdh"
Block(Disk) L#1 "sdf" // 磁盘挂在Node0上
PCIBridge
PCI 8086:1521
Net L#13 "eth0"
PCI 8086:1521
Net L#14 "eth1" //网卡挂在node0上
PCIBridge
PCIBridge
PCI 1a03:2000
GPU L#15 "controlD64"
GPU L#16 "card0"
NUMANode L#1 (P#1 31GB)
NUMANode L#2 (P#2 31GB)
NUMANode L#3 (P#3 31GB)
NUMANode L#4 (P#4 31GB)
NUMANode L#5 (P#5 31GB)
NUMANode L#6 (P#6 31GB)
NUMANode L#7 (P#7 31GB)
L2 L#14 (2048KB)
L1d L#56 (32KB) + L1i L#56 (32KB) + Core L#56 + PU L#56 (P#56)
L1d L#57 (32KB) + L1i L#57 (32KB) + Core L#57 + PU L#57 (P#57)
L1d L#58 (32KB) + L1i L#58 (32KB) + Core L#58 + PU L#58 (P#58)
L1d L#59 (32KB) + L1i L#59 (32KB) + Core L#59 + PU L#59 (P#59)
L2 L#15 (2048KB)
L1d L#60 (32KB) + L1i L#60 (32KB) + Core L#60 + PU L#60 (P#60)
L1d L#61 (32KB) + L1i L#61 (32KB) + Core L#61 + PU L#61 (P#61)
L1d L#62 (32KB) + L1i L#62 (32KB) + Core L#62 + PU L#62 (P#62)
L1d L#63 (32KB) + L1i L#63 (32KB) + Core L#63 + PU L#63 (P#63)
Package L#1 + L3 L#1 (64MB) //socket2
NUMANode L#8 (P#8 31GB)
L2 L#16 (2048KB)
L1d L#64 (32KB) + L1i L#64 (32KB) + Core L#64 + PU L#64 (P#64)
L1d L#65 (32KB) + L1i L#65 (32KB) + Core L#65 + PU L#65 (P#65)
L1d L#66 (32KB) + L1i L#66 (32KB) + Core L#66 + PU L#66 (P#66)
L1d L#67 (32KB) + L1i L#67 (32KB) + Core L#67 + PU L#67 (P#67)
L2 L#17 (2048KB)
L1d L#68 (32KB) + L1i L#68 (32KB) + Core L#68 + PU L#68 (P#68)
L1d L#69 (32KB) + L1i L#69 (32KB) + Core L#69 + PU L#69 (P#69)
L1d L#70 (32KB) + L1i L#70 (32KB) + Core L#70 + PU L#70 (P#70)
L1d L#71 (32KB) + L1i L#71 (32KB) + Core L#71 + PU L#71 (P#71)
HostBridge L#7
PCIBridge
PCIBridge
PCIBridge
PCI 15b3:1015
Net L#17 "eth2" //node8 上的网卡,eth2、eth3做了bonding
PCI 15b3:1015
Net L#18 "eth3"
PCIBridge
PCI 144d:a808
PCIBridge
PCI 144d:a808

---鲲鹏920 每路48core 2路共4node,网卡插在node0,磁盘插在node2
#lstopo-no-graphics
Machine (755GB total)
Package L#0
NUMANode L#0 (P#0 188GB)
L3 L#0 (24MB)
L2 L#0 (512KB) + L1d L#0 (64KB) + L1i L#0 (64KB) + Core L#0 + PU L#0 (P#0)
L2 L#1 (512KB) + L1d L#1 (64KB) + L1i L#1 (64KB) + Core L#1 + PU L#1 (P#1)
L2 L#22 (512KB) + L1d L#22 (64KB) + L1i L#22 (64KB) + Core L#22 + PU L#22 (P#22)
L2 L#23 (512KB) + L1d L#23 (64KB) + L1i L#23 (64KB) + Core L#23 + PU L#23 (P#23)
HostBridge L#0
PCIBridge
PCI 15b3:1017
Net L#0 "enp2s0f0"
PCI 15b3:1017
Net L#1 "eth1"
PCIBridge
PCI 19e5:1711
GPU L#2 "controlD64"
GPU L#3 "card0"
HostBridge L#3
2 x { PCI 19e5:a230 }
PCI 19e5:a235
Block(Disk) L#4 "sda"
HostBridge L#4
PCIBridge
PCI 19e5:a222
Net L#5 "enp125s0f0"
PCI 19e5:a221
Net L#6 "enp125s0f1"
PCI 19e5:a222
Net L#7 "enp125s0f2"
PCI 19e5:a221
Net L#8 "enp125s0f3"
NUMANode L#1 (P#1 189GB) + L3 L#1 (24MB)
L2 L#24 (512KB) + L1d L#24 (64KB) + L1i L#24 (64KB) + Core L#24 + PU L#24 (P#24)
Package L#1
NUMANode L#2 (P#2 189GB)
L3 L#2 (24MB)
L2 L#48 (512KB) + L1d L#48 (64KB) + L1i L#48 (64KB) + Core L#48 + PU L#48 (P#48)
HostBridge L#6
PCIBridge
PCI 19e5:3714
PCIBridge
PCI 19e5:3714
PCIBridge
PCI 19e5:3714
PCIBridge
PCI 19e5:3714
HostBridge L#11
PCI 19e5:a230
PCI 19e5:a235
PCI 19e5:a230
NUMANode L#3 (P#3 189GB) + L3 L#3 (24MB)
L2 L#72 (512KB) + L1d L#72 (64KB) + L1i L#72 (64KB) + Core L#72 + PU L#72 (P#72)
Misc(MemoryModule)

如果cpu core太多, interrupts 没法看的话,通过cut只看其中一部分core

1
cat /proc/interrupts | grep -i 'eth4\|CPU' | cut -c -8,865-995,1425-

lspci

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#lspci -s 21:00.0 -vvv
21:00.0 Ethernet controller: Mellanox Technologies MT27710 Family [ConnectX-4 Lx]
Subsystem: Mellanox Technologies ConnectX-4 Lx Stand-up dual-port 10GbE MCX4121A-XCAT
Control: I/O- Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr+ Stepping- SERR+ FastB2B- DisINTx+
Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
Latency: 0, Cache Line Size: 64 bytes
Interrupt: pin A routed to IRQ 105
Region 0: Memory at 3249c000000 (64-bit, prefetchable) [size=32M]
Expansion ROM at db300000 [disabled] [size=1M]
Capabilities: [60] Express (v2) Endpoint, MSI 00
DevCap: MaxPayload 512 bytes, PhantFunc 0, Latency L0s unlimited, L1 unlimited
ExtTag+ AttnBtn- AttnInd- PwrInd- RBE+ FLReset+ SlotPowerLimit 0.000W
DevCtl: CorrErr+ NonFatalErr+ FatalErr+ UnsupReq-
RlxdOrd+ ExtTag+ PhantFunc- AuxPwr- NoSnoop+ FLReset-
MaxPayload 512 bytes, MaxReadReq 512 bytes
DevSta: CorrErr+ NonFatalErr- FatalErr- UnsupReq+ AuxPwr- TransPend-
LnkCap: Port #0, Speed 8GT/s, Width x8, ASPM not supported
ClockPM- Surprise- LLActRep- BwNot- ASPMOptComp+
LnkCtl: ASPM Disabled; RCB 64 bytes Disabled- CommClk+
ExtSynch- ClockPM- AutWidDis- BWInt- AutBWInt-
LnkSta: Speed 8GT/s (ok), Width x8 (ok)
TrErr- Train- SlotClk+ DLActive- BWMgmt- ABWMgmt-
DevCap2: Completion Timeout: Range ABC, TimeoutDis+, LTR-, OBFF Not Supported
AtomicOpsCap: 32bit- 64bit- 128bitCAS-
DevCtl2: Completion Timeout: 50us to 50ms, TimeoutDis-, LTR-, OBFF Disabled
AtomicOpsCtl: ReqEn-
LnkCtl2: Target Link Speed: 8GT/s, EnterCompliance- SpeedDis-
Transmit Margin: Normal Operating Range, EnterModifiedCompliance- ComplianceSOS-
Compliance De-emphasis: -6dB
LnkSta2: Current De-emphasis Level: -6dB, EqualizationComplete+, EqualizationPhase1+
EqualizationPhase2+, EqualizationPhase3+, LinkEqualizationRequest-
Capabilities: [48] Vital Product Data
Product Name: CX4121A - ConnectX-4 LX SFP28
Read-only fields:
[PN] Part number: MCX4121A-XCAT
[EC] Engineering changes: AJ
[SN] Serial number: MT2031J09199
[V0] Vendor specific: PCIeGen3 x8
[RV] Reserved: checksum good, 0 byte(s) reserved
End
Capabilities: [9c] MSI-X: Enable+ Count=64 Masked-
Vector table: BAR=0 offset=00002000
PBA: BAR=0 offset=00003000
Capabilities: [c0] Vendor Specific Information: Len=18 <?>
Capabilities: [40] Power Management version 3
Flags: PMEClk- DSI- D1- D2- AuxCurrent=375mA PME(D0-,D1-,D2-,D3hot-,D3cold+)
Status: D0 NoSoftRst+ PME-Enable- DSel=0 DScale=0 PME-
Capabilities: [100 v1] Advanced Error Reporting
UESta: DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- UnxCmplt- RxOF- MalfTLP- ECRC- UnsupReq- ACSViol-
UEMsk: DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- UnxCmplt- RxOF- MalfTLP- ECRC- UnsupReq- ACSViol-
UESvrt: DLP+ SDES- TLP- FCP+ CmpltTO- CmpltAbrt- UnxCmplt- RxOF+ MalfTLP+ ECRC+ UnsupReq- ACSViol-
CESta: RxErr- BadTLP- BadDLLP- Rollover- Timeout- AdvNonFatalErr-
CEMsk: RxErr- BadTLP- BadDLLP- Rollover- Timeout- AdvNonFatalErr+
AERCap: First Error Pointer: 04, ECRCGenCap+ ECRCGenEn+ ECRCChkCap+ ECRCChkEn+
MultHdrRecCap- MultHdrRecEn- TLPPfxPres- HdrLogCap-
HeaderLog: 00000000 00000000 00000000 00000000
Capabilities: [150 v1] Alternative Routing-ID Interpretation (ARI)
ARICap: MFVC- ACS-, Next Function: 1
ARICtl: MFVC- ACS-, Function Group: 0
Capabilities: [180 v1] Single Root I/O Virtualization (SR-IOV)
IOVCap: Migration-, Interrupt Message Number: 000
IOVCtl: Enable- Migration- Interrupt- MSE- ARIHierarchy+
IOVSta: Migration-
Initial VFs: 8, Total VFs: 8, Number of VFs: 0, Function Dependency Link: 00
VF offset: 2, stride: 1, Device ID: 1016
Supported Page Size: 000007ff, System Page Size: 00000001
Region 0: Memory at 000003249e800000 (64-bit, prefetchable)
VF Migration: offset: 00000000, BIR: 0
Capabilities: [1c0 v1] Secondary PCI Express <?>
Capabilities: [230 v1] Access Control Services
ACSCap: SrcValid- TransBlk- ReqRedir- CmpltRedir- UpstreamFwd- EgressCtrl- DirectTrans-
ACSCtl: SrcValid- TransBlk- ReqRedir- CmpltRedir- UpstreamFwd- EgressCtrl- DirectTrans-
Kernel driver in use: mlx5_core
Kernel modules: mlx5_core

如果有多个高速设备争夺带宽(例如将高速网络连接到高速存储),那么 PCIe 也可能成为瓶颈,因此可能需要从物理上将 PCIe 设备划分给不同 CPU,以获得最高吞吐率。

img

数据来源: https://en.wikipedia.org/wiki/PCI_Express#History_and_revisions

Intel 认为,有时候 PCIe 电源管理(ASPM)可能导致延迟提高,因进而导致丢包率增高。因此也可以为内核命令行参数添加pcie_aspm=off将其禁用。

Default 路由持久化

通过 ip route 可以添加默认路由,但是reboot就丢失了

1
route add default dev bond0

如果要持久化,在centos下可以创建 /etc/sysconfig/network-scripts/route-bond0 文件,内容如下

1
2
3
4
5
6
7
8
default dev bond0    ---默认路由,后面的可以省略
10.0.0.0/8 via 11.158.239.247 dev bond0
11.0.0.0/8 via 11.158.239.247 dev bond0
30.0.0.0/8 via 11.158.239.247 dev bond0
172.16.0.0/12 via 11.158.239.247 dev bond0
192.168.0.0/16 via 11.158.239.247 dev bond0
100.64.0.0/10 via 11.158.239.247 dev bond0
33.0.0.0/8 via 11.158.239.247 dev bond0

或者用sed在文件第一行添加

1
2
sed -i '/default /d'  /etc/sysconfig/network-scripts/route-bond0   //先删除默认路由(如果有)
sed -i '1 i\default dev bond0' /etc/sysconfig/network-scripts/route-bond0 //添加

Centos 7的话需要在 /etc/sysconfig/network 中添加创建默认路由的命令

1
2
3
# cat /etc/sysconfig/network
# Created by anaconda
ip route add default dev eth0

内核态启动并加载网卡的逻辑

  1. 运行Linux的机器在BIOS阶段之后,机器的boot loader根据我们预先定义好的配置文件,将intrd和linux kernel加载到内存。这个包含initrd和linux kernel的配置文件通常在/boot分区(从grub.conf中读取参数)

  2. 内核启动,运行当前根目录下面的init进程,init进程再运行其他必要的进程,其中跟网卡PCI设备相关的一个进程,就是udevd进程,udevd负责根据内核pci scan的pci设备,从initrd这个临时的根文件系统中加载内核模块,对于网卡来说,就是网卡驱动。(对应systemd-udevd 服务)

  3. udevd,根据内核pci device scan出来的pci device,通过netlink消息机制通知udevd加载相应的内核驱动,其中,网卡驱动就是在这个阶段加载,如果initrd临时文件系统里面有这个网卡的驱动文件。通常upstream到linux内核的驱动,比如ixgbe,或者和内核一起编译的网卡驱动,会默认包含在initrd文件系统中。这些跟内核一起ship的网卡驱动会在这个阶段加载

  4. udevd除了负责网卡驱动加载之外,还要负责为网卡命名。udevd在为网卡命名的时候,会首先check “/etc/udev/rules.d/“下的rule,如果hit到相应的rule,就会通过rule里面指定的binary为网卡命名。如果/etc/udev/rules.d/没有命名成功网卡,那么udevd会使用/usr/lib/udev/rule.d下面的rule,为网卡重命名。其中rule的文件经常以数字开头,数字越小,表示改rule的优先级越高。intrd init不会初始化network服务,所以/etc/sysconfig/network-scripts下面的诸如bond0,route的配置都不会生效。(内核启动先是 intrd init,然后执行一次真正的init)

  5. 在完成网卡driver load和name命名之后,initrd里面的init进程,会重启其他用户态进程,如udevd等,并且重新mount真正的根文件系统,启动network service。

  6. 重启udevd,会触发一次kernel的rescan device。这样第三方安装的网卡driver,由于其driver模块没有在initrd里面,会在这个阶段由udevd触发加载。同时,也会根据“/etc/udev/rules.d/”和“/usr/lib/udev/rule.d”的rule,重命名网卡设备。–用户态修改网卡名字的机会

    1
    2
    kernel: ixgbe 0000:3b:00.1 eth1: renamed from enp59s0f1
    kernel: i40e 0000:88:00.0 eth7: renamed from enp136s0
  7. 同时network service 会启动,进而遍历etc/sysconfig/network-scripts下面的脚本,我们配置的bond0, 默认路由,通常会在这个阶段运行,创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    kernel: bond0: Enslaving eth0 as a backup interface with a down link
    kernel: ixgbe 0000:3b:00.0 eth0: detected SFP+: 5
    kernel: power_meter ACPI000D:00: Found ACPI power meter.
    kernel: power_meter ACPI000D:00: Ignoring unsafe software power cap!
    kernel: ixgbe 0000:3b:00.1: registered PHC device on eth1
    kernel: ixgbe 0000:3b:00.0 eth0: NIC Link is Up 10 Gbps, Flow Control: RX/TX
    kernel: bond0: Enslaving eth1 as a backup interface with a down link
    kernel: bond0: Warning: No 802.3ad response from the link partner for any adapters in the bond
    kernel: bond0: link status definitely up for interface eth0, 10000 Mbps full duplex
    kernel: bond0: first active interface up!

由于我们系统的初始化有两个阶段,udevd会运行两次,所以内核态网卡driver的加载,网卡命名也有两次机会。

第一次网卡driver的加载和命名是在initrd运行阶段,这个阶段由于initrd文件系统比较小,只包括和kernel一起ship的内核module,所以这个阶段只能加载initrd里面有的内核模块。网卡的重命名也只能重命名加载了驱动的网卡。

第二个网卡driver的加载和命名,是在真正根文件系统加载后,内核再一次pci scan,这个时候,由于真的根文件系统包含了所有的driver,第一个阶段无法probe的网卡会在这个阶段probe,重命名也会在这个阶段进行。

内核默认命名规则有一定的局限性,往往不一定准确对应网卡接口的物理顺序,而且每次启动只根据内核发现网卡的顺序进行命名,因此并不固定;所以目前一般情况下会在用户态启用其他的方式去更改网卡名称,原则就是在内核命名ethx后将其在根据用户态的规则rename为其他的名字,这种规则往往是根据网卡的Mac地址以及其他能够唯一代表一块网卡的参数去命名,因此会一一对应;

内核自带的网卡驱动在initrd中的内核模块中。对于第三方网卡,我们通常通过rpm包的方式安装。这种第三方安装的rpm,通常不会在initrd里面,只存在disk上。这样这种内核模块就只会在第二次udevd启动的时候被加载。

不论第一次重命名还是第二次重命名,其都遵循一样的逻辑,也就是先check /etc/udev/rules.d/的rule,然后check /usr/lib/udev/rule.d中的rule,其中rule的优先级etc下最高,然后是usr下面。并且,rule的文件名中的数字表示该rule在同一文件夹中的优先级,数字越低,优先级越高。

network.service 根据network-script里面的脚本创建bond0,下发路由。这个过程和网卡重命名是同步进行,一般网卡重命名会超级快,单极端情况下重命名可能在network.service后会导致创建bond0失败(依赖网卡名来bonding),这里会依赖network.service retry机制来反复尝试确保network服务能启动成功

要想解决网卡加载慢的问题,可以考虑把安装后的网卡集成到initrd中。Linux系统提供的dracut可以做到这一点,我们只需要在安装完第三方网卡驱动后,执行:

1
2
3
4
dracut --forace

查看
udevadm info -q all -a /dev/nvme0

就可以解决这个问题,该命令会根据最新的内存中的module,重新下刷initrd。

其实在多数第三方网卡的rpm spec或者makefile里面通常也会加入这种强制重刷的逻辑,确保内核驱动在initrd里面,从而加快网卡驱动的加载。

用户态命名网卡流程

CentOS 7提供了在网络接口中使用一致且可预期的网络设备命名方法, 目前默认使用的是net.ifnames规则。The device name procedure in detail is as follows:

  1. A rule in /usr/lib/udev/rules.d/60-net.rules instructs the udev helper utility, /lib/udev/rename_device, to look into all /etc/sysconfig/network-scripts/ifcfg-*suffix* files. If it finds an ifcfg file with a HWADDR entry matching the MAC address of an interface it renames the interface to the name given in the ifcfg file by the DEVICE directive.(根据提前定义好的ifcfg-网卡名来命名网卡–依赖mac匹配,如果网卡的ifconfig文件中未加入HWADDR,则rename脚本并不会根据配置文件去重命名网卡)
  2. A rule in /usr/lib/udev/rules.d/71-biosdevname.rules instructs biosdevname to rename the interface according to its naming policy, provided that it was not renamed in a previous step, biosdevname is installed, and biosdevname=0 was not given as a kernel command on the boot command line.
  3. A rule in /lib/udev/rules.d/75-net-description.rules instructs udev to fill in the internal udev device property values ID_NET_NAME_ONBOARD, ID_NET_NAME_SLOT, ID_NET_NAME_PATH, ID_NET_NAME_MAC by examining the network interface device. Note, that some device properties might be undefined.
  4. A rule in /usr/lib/udev/rules.d/80-net-name-slot.rules instructs udev to rename the interface, provided that it was not renamed in step 1 or 2, and the kernel parameter net.ifnames=0 was not given, according to the following priority: ID_NET_NAME_ONBOARD, ID_NET_NAME_SLOT, ID_NET_NAME_PATH. It falls through to the next in the list, if one is unset. If none of these are set, then the interface will not be renamed.

Steps 3 and 4 are implementing the naming schemes 1, 2, 3, and optionally 4, described in Section 11.1, “Naming Schemes Hierarchy”. Step 2 is explained in more detail in Section 11.6, “Consistent Network Device Naming Using biosdevname”.

以上重命名简要概述就是对于CentOS系统,一般有下面几个rule在/usr/lib/udev/rule.d来重命名网卡:

  1. /usr/lib/udev/rules.d/60-net.rules 文件中的规则会让 udev 帮助工具/lib/udev/rename_device 查看所有 /etc/sysconfig/network-scripts/ifcfg-* 文件。如果发现包含 HWADDR 条目的 ifcfg 文件与某个接口的 MAC 地址匹配,它会将该接口重命名为ifcfg 文件中由 DEVICE 指令给出的名称。rename条件:如果网卡的ifconfig文件中未加入HWADDR,则rename脚本并不会根据配置文件去重命名网卡;
  2. /usr/lib/udev/rules.d/71-biosdevname.rules 中的规则让 biosdevname 根据其命名策略重命名该接口,即在上一步中没有重命名该接口、安装biosdevname、且在 boot 命令行中将biosdevname=0 作为内核命令给出。(bisodevname规则,从CentOS 7 开始默认不使用,所以该条规则在不配置的情况下失效,直接去执行3;默认在cmdline中bisodevname=0,如果需要启用,则需要设置bisodevname=1)
  3. /lib/udev/rules.d/75-net-description.rules 中的规则让 udev 通过检查网络接口设备,填写内部 udev 设备属性值 ID_NET_NAME_ONBOARD、ID_NET_NAME_SLOT、ID_NET_NAME_PATH、ID_NET_NAME_MAC。注:有些设备属性可能处于未定义状态。 –没有修改网卡名,只是取到了命名需要的一些属性值。查看:udevadm info -p /sys/class/net/enp125s0f0
  4. /usr/lib/udev/rules.d/80-net-name-slot.rules 中的规则让 udev 重命名该接口,优先顺序如下:ID_NET_NAME_ONBOARD、ID_NET_NAME_SLOT、ID_NET_NAME_PATH。并提供如下信息:没有在步骤 1 或 2 中重命名该接口,同时未给出内核参数 net.ifnames=0。如果一个参数未设定,则会按列表的顺序设定下一个。如果没有设定任何参数,则不会重命名该接口 —- 目前主流CentOS流都是这个命名方式
  5. network service起来后会遍历/etc/sysconfig/network-scripts下的脚本,配置bond0、默认路由、其它网卡等

其中60 rule会调用rename_device根据ifcfg-xxx脚本来命名,rule 71调用biosdevname来命名网卡。以上规则数字越小优先级越高,高优先级生效后跳过低优先级

总的来说网卡命名规则:grub启动参数 -> /etc/udev/rules.d/的rule -> /usr/lib/udev/rule.d

参考:

The following is an excerpt from Chapter 11 of the RHEL 7 “Networking Guide”:

  • Scheme 1: Names incorporating Firmware or BIOS provided index numbers for on-board devices (example: eno1), are applied if that information from the firmware or BIOS is applicable and available, else falling back to scheme 2.
  • Scheme 2: Names incorporating Firmware or BIOS provided PCI Express hotplug slot index numbers (example: ens1) are applied if that information from the firmware or BIOS is applicable and available, else falling back to scheme 3.
  • Scheme 3: Names incorporating physical location of the connector of the hardware (example: enp2s0), are applied if applicable, else falling directly back to scheme 5 in all other cases.
  • Scheme 4: Names incorporating interface’s MAC address (example: enx78e7d1ea46da), is not used by default, but is available if the user chooses.
  • Scheme 5: The traditional unpredictable kernel naming scheme, is used if all other methods fail (example: eth0).

网卡命名

最开始Linux对网卡的命名规范是 eth* , 后来随着PCIe插槽的普及开始有 eno/enp等命名

  1. eno1: 代表由主板bios内置的网卡
  2. Ens: 代表有主板bios内置的PCI-E网卡
  3. Enp2s0: PCI-E独立网卡
  4. Eth0: 如果以上都不使用回到默认的网卡名

En 代笔:ethernet

第3个字符根据设备类型选择

1
2
3
4
5
o<index>           on-board device index number
s<slot> hotplug slot index number
x<MAC> MAC address
p<bus>s<slot> PCI geographical location
p<bus>s<slot> USB port number chain

默认安装网卡所在位置来命名(enp131s0 等),按位置命名实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
//name example  ---默认方式,按照 /usr/lib/udev/rules.d/80-net-name-slot.rules 来命名
enp4s10f1 pci 0000:04:0a.1
| | | | | | | |
| | | | domain <- 0000 | | |
| | | | | | |
en| | | --> ethernet | | |
| | | | | |
p4| | --> prefix/bus number (4) <-- 04 | |
| | | |
s10| --> slot/device number (10) <-- 10 |
| |
f1 --> function number (1) <-- 1

可以关掉这种按位置命名的方式,在grub参数中添加: net.ifnames=0 biosdevname=0,关闭后默认命名方式是eth**,开启biosdevname=1后,默认网卡命名方式是p1p1/p1p2(麒麟默认开启;alios默认关闭,然后以eth来命名)

You have two options (as described in the new RHEL 7 Networking Guide) to disable the new naming scheme:

  • Run once: ln -s /dev/null /etc/udev/rules.d/80-net-name-slot.rules

or

  • Run once: echo 'GRUB_CMDLINE_LINUX="net.ifnames=0"' >>/etc/default/grub

Note that the biosdevname package is not installed by default, so unless it gets installed, you don’t need to add biosdevname=0 as a kernel argument.

也可以添加命名规则在 /etc/udev/rules.d/ 下(这种优先级挺高),比如

1
2
3
4
5
cat /etc/udev/rules.d/70-persistent-net.rules
# PCI device 21:00.0 (ixgbe)
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="d4:5d:64:bb:06:32", PROGRAM="/lib/udev/rename_device", ATTR{type}=="1", KERNEL=="eth*", NAME="eth0"
# PCI device 0x8086:0x105e (e1000e)
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="b8:59:9f:2d:48:2b", PROGRAM="/lib/udev/rename_device", ATTR{type}=="1", KERNEL=="eth*", NAME="eth1"

但是以上规则在麒麟下没有生效

网卡重命名方式:

1
/sbin/ip link set eth1 name eth123

校验

比如如下结构下因为通过xdp redirect来联通veth0、veth1,两边能ping通,但是TCP、UDP 都不通

image-20220614171759153

正常走bridge ping/tcp/udp是不会有问题的, 这也是docker下常见用法

image-20220614173416775

当前主流的网卡(包括虚拟网卡,如veth/tap)都支持一个叫做RX/TX Checksum Offload(RX和TX对应接收和发送两个方向)的特性,用于将传输层协议的校验和计算卸载到网卡硬件中(IP头的检验和会被操作系统用软件方式正确计算)。对于经过启用该功能的网卡的报文,操作系统不会对该报文进行校验和的计算,从而减少对系统CPU资源的占用。

1613803162582-ce09e9cc-36e4-4805-b968-98d8dd601f52

对于没有挂载XDP程序的且开启Checksum Offload功能的Veth设备,在接收到数据包时,会将ip_summed置为CHECKSUM_UNNECESSARY,因此上层L4协议栈在收到该数据包的时候不会再检查校验和,即使是数据包的校验和不正确也会正常被处理。但是若我们在veth设备上挂载了XDP程序,XDP程序运行时将网卡接收队列中的数据转换为结构struct xdp_buff时会丢失掉ip_summed信息,这就导致数据包被L4协议栈接收后由于校验和错误而被丢弃。

如上图因为veth挂载了XDP程序,导致包没有校验信息而丢掉,如果在同样环境下ping是可以通的,因为ping包提前计算好了正确的校验和

img

这种丢包可以通过 /proc/net/snmp 看到

img

通过命令ethtool -K <nic-name> tx off工具关闭Checksum Offload特性,强行让操作系统用软件方式计算校验和。

日志

网卡日志打开:

1
2
3
4
sysctl -w net.ipv4.conf.all.log_martians=1 //所有网卡
sysctl -w net.ipv4.conf.p1p1.log_martians=1 //特定网卡

/proc/sys/net/ipv4/conf/eth0.9/log_martians

/var/log/messages中:

messages-20120101:Dec 31 09:25:45 nixcraft-router kernel: martian source 74.xx.47.yy from 10.13.106.25, on dev eth1

修改mac地址

1
2
3
sudo ip link set dev eth1 down
sudo ip link set dev eth1 address e8:61:1f:33:c5:fd
sudo ip link set dev eth1 up

参考资料

高斯在鲲鹏下跑TPCC的优化

https://www.cyberciti.biz/faq/linux-log-suspicious-martian-packets-un-routable-source-addresses/

鲲鹏性能优化十板斧

如何用1分钱建站速度秒杀三大门户网站站

发表于 2021-01-01 | 分类于 技巧

如何用1分钱建站速度秒杀三大门户网站

如何快速又便宜地建立一个高质量的网站呢(高质量指的是访问速度快),还能够双站热备(国内国外热备两份内容),整个开支大概一分钱吧

核心就是用阿里云的OSS来提供高速的访问。

先看访问速度

同样是访问下面三个网站首页:

OSS托管,页面大小 96.6MB、242个GET,耗时2.21秒加载,价格不到1分钱

搜狐首页,页面大小16.6MB、555个GET,耗时3.6秒

新浪首页, 页面大小17.8MB、404个GET,耗时9.63秒

OSS托管的网站

用OSS托管的网站加载速度,96MB页面(很大了)2.21秒加载完毕

image-20210702140950863

访问搜狐首页

image-20210702141336301

访问新浪首页

image-20210702142610162

原因分析

OSS快的原因是:小网站并发不高,服务器、带宽资源充足,还不用花钱,没有机器、带宽维护成本以及人员成本

有专业的阿里云工程师负责运维,给的是最好的服务器、最大的带宽(你用的少就不用花钱,带宽资源费用超级便宜)

到底有多便宜呢?1.6万次GET请求才1分钱,0.52GB流量才0.25元,计价金额单位震惊我了

image-20210702163910295

OSS托管网站方案

将所有内容静态化,然后上传到OSS就可以了

发布操作步骤:

  • markdown编辑器中编写要发布的页面
  • 用hexo静态化全站(将markdown转换成html页面)
  • git commit到github或者ossutil 同步到aliyun oss中

比如下面就是我的网站发布脚本:

1
2
3
4
5
#静态化网站,并同步到github,多活;-d 表示 deploy
hexo g -d

#sync all pages to oss
ossutil --config-file=~/src/script/mac/.ossutilconfig sync ./public/ oss://mysite/ -u --output-dir=/tmp/

实际我的网站通过github和OSS都能访问到,内容完全一样,github免费,但是多图页面速度太慢, 比如我一个页面几十个图,github加载偶尔失败, 但是我把图片放到了OSS,因为OSS超级快这样github加载也变得超级快了。

hexo是一个node实现的网站生成工具

oss 托管网站介绍

感叹一下,个人建站现在真的是又便宜又方便,只是域名实名制恶心了点,那就干脆不要域名了。

update 20241120: 阿里云 OSS关闭了默认域名的静态托管,也就是会强制下载而不是在浏览器里展示静态页面:https://help.aliyun.com/zh/oss/user-guide/how-to-ensure-an-object-is-previewed-when-you-access-the-object,除非你绑定自己的域名

一些问题

themes/next

github page 当个人网站,main 分支对应源码,master 分支对应静态化后的页面,如果 main 分支 generate 静态页面都是空的,请注意 themes/next 文件夹内容要完整

1
2
3
4
5
6
#ls themes/next
LICENSE _config.yml.bak package.json
README.en.md bower.json scripts
README.md gulpfile.coffee source
_config-20200519.yml languages test
_config.yml layout

image-20240924143604980

版本搭配

当前测试如下版本搭配能正常 generate 静态页面,如何这个 3.9 的 hexo 搭配 node 14/20 都会 generate 空页面,需要进一步升级 hexo 版本测试搭配更高的 node 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
✗ hexo -v
hexo: 3.9.0
hexo-cli: 4.3.2
os: darwin 23.4.0 14.4.1

node: 12.22.12
v8: 7.8.279.23-node.57
uv: 1.40.0
zlib: 1.2.11
brotli: 1.0.9
ares: 1.18.1
modules: 72
nghttp2: 1.41.0
napi: 8
llhttp: 2.1.4
http_parser: 2.9.4
openssl: 1.1.1n
cldr: 37.0
icu: 67.1
tz: 2021a4
unicode: 13.0

//保持如下 link 才会使用 hexo 3.9,以及 3.9 依赖的 node 版本
/opt/homebrew/bin/hexo -> /opt/homebrew/lib/node_modules/hexo-cli/bin/hexo

升级 hexo 到 7.3 失败

hugo

更快,一来更少,所以问题更少

theme:DoIt

配置:https://niceram.xyz/2021/03/04/20210304_1125/

theme 配置:https://blog.csdiy.org/2023/02/23/depoly-hugo-theme-doit/

一个有意思的问题

发表于 2020-12-25 | 分类于 Linux

一个有意思的问题

问题描述

1
2
3
4
5
6
7
8
9
10
11
12
$mysql -N -h127.0.0.1 -e "select id from sbtest1 limit 1"
+--------+
| 100024 |
+--------+

$mysql -N -h127.0.0.1 -e "select id from sbtest1 limit 1" | cat
100024

$mysql -t -N -h127.0.0.1 -e "select id from sbtest1 limit 1" | cat
+--------+
| 100024 |
+--------+

如上第一和第二个语句,为什么mysql client的输出重定向后就没有ascii制表符了呢?

语句三加上 -t后再经过管道,也有制表符了。

stackoverflow上也有很多人有同样的疑问,不过不但没有给出第三行的解法,更没有人讲清楚这个里面的原理。所以接下来我们来分析下这是为什么

-N 去掉表头

-B batch 模式,用tab键替换分隔符

分析

strace看看第一个语句:

image.png

再对比下第二个语句的strace:

image.png

从上面两个strace比较来看,似乎mysql client能检测到要输出到命名管道(S_IFIFO )还是character device(S_IFCHR),如果是命名管道的话就不要输出制表符了,如果是character device那么就输出ascii制表符。

fstats里面对不同输出目标的说明:

1
2
3
4
5
6
7
8
9
10
11
printf("File type:                ");
switch (sb.st_mode & S_IFMT) {
case S_IFBLK: printf("block device\n"); break;
case S_IFCHR: printf("character device\n"); break;
case S_IFDIR: printf("directory\n"); break;
case S_IFIFO: printf("FIFO/pipe\n"); break;
case S_IFLNK: printf("symlink\n"); break;
case S_IFREG: printf("regular file\n"); break;
case S_IFSOCK: printf("socket\n"); break;
default: printf("unknown?\n"); break;
}

第4行和第6行两个类型就是导致mysql client选择了不同的输出内容

误解

所以这个问题不是:

为什么mysql client的输出重定向后就没有ascii制表符了呢?

而是:

mysql client 可以检测到不同的输出目标然后输出不同的内容吗? 管道或者重定向是一个应用能感知的输出目标吗?

误解:觉得管道写在后面,mysql client不应该知道后面是管道,mysql client输出内容到stdout,然后os将stdout的内容重定向给管道。

实际上mysql是可以检测(detect)输出目标的,如果是管道类的非交互输出那么没必要徒增一些制表符;如果是交互式界面那么就输出一些制表符好看一些。

要是想想在Unix下一切皆文件就更好理解了,输出到管道这个管道也是个文件,所以mysql client是可以感知各种输出文件的属性的。

背后的实现大概是这样:

1
2
3
4
5
6
7
#include <stdio.h>
#include <io.h>
...
if (isatty(fileno(stdout)))
printf( "stdout is a terminal\n" ); // 输出制表符
else
printf( "stdout is a file or a pipe\n"); // 不输出制表符

isatty的解释

结论就是 mysql client根据输出目标的不同(stdout、重定向)输出不同的内容,不过这种做法对用户体感上不是太好。

其它

Linux管道居然不是按顺序,而是并发执行的:https://unix.stackexchange.com/questions/37508/in-what-order-do-piped-commands-run 掉坑里了,并发问题就多了,实际测试也发现跑几千次 ps |grep 会出现,ps看不到后面的grep进程

参考资料

https://www.pyrosoft.co.uk/blog/2014/09/08/how-to-stop-mysql-ascii-tables-column-separators-from-being-lost-when-redirecting-bash-output/

https://www.oreilly.com/library/view/mysql-cookbook/0596001452/ch01s22.html

到底一台服务器上最多能创建多少个TCP连接

发表于 2020-11-30 | 分类于 TCP

到底一台服务器上最多能创建多少个TCP连接

经常听到有同学说一台机器最多能创建65535个TCP连接,这其实是错误的理解,为什么会有这个错误的理解呢?

port range

我们都知道linux下本地随机端口范围由参数控制,也就是listen、connect时候如果没有指定本地端口,那么就从下面的port range 中随机取一个可用的

1
2
# cat /proc/sys/net/ipv4/ip_local_port_range 
2000 65535

port range的上限是65535,所以也经常看到这个误解:一台机器上最多能创建65535个TCP连接

到底一台机器上最多能创建多少个TCP连接

先说结论:在内存、文件句柄足够的话可以创建的连接是没有限制的(每个TCP连接至少要消耗一个文件句柄)。

那么/proc/sys/net/ipv4/ip_local_port_range指定的端口范围到底是什么意思呢?

核心规则:一个TCP连接只要保证四元组(src-ip src-port dest-ip dest-port)唯一就可以了,而不是要求src port唯一

后面所讲都遵循这个规则,所以在心里反复默念:四元组唯一 五个大字,就能分析出来到底能创建多少TCP连接了。

比如如下这个机器上的TCP连接实际状态:

1
2
3
4
5
6
# netstat -ant |grep 18089
tcp 0 0 192.168.1.79:18089 192.168.1.79:22 ESTABLISHED
tcp 0 0 192.168.1.79:18089 192.168.1.79:18080 ESTABLISHED
tcp 0 0 192.168.0.79:18089 192.168.0.79:22 TIME_WAIT
tcp 0 0 192.168.1.79:22 192.168.1.79:18089 ESTABLISHED
tcp 0 0 192.168.1.79:18080 192.168.1.79:18089 ESTABLISHED

从前三行可以清楚地看到18089被用了三次,第一第二行src-ip、dest-ip也是重复的,但是dest port不一样,第三行的src-port还是18089,但是src-ip变了。他们的四元组均不相同。

所以一台机器能创建的TCP连接是没有限制的,而ip_local_port_range是指没有bind的时候OS随机分配端口的范围,但是分配到的端口要同时满足五元组唯一,这样 ip_local_port_range 限制的是连同一个目标(dest-ip和dest-port一样)的port的数量(请忽略本地多网卡的情况,因为dest-ip为以后route只会选用一个本地ip)。

那么为什么大家有这样的误解呢?我总结了下,大概是以下两个原因让大家误解了:

  • 如果是listen服务,那么肯定端口不能重复使用,这样就跟我们的误解对应上了,一个服务器上最多能监听65535个端口。比如nginx监听了80端口,那么tomcat就没法再监听80端口了,这里的80端口只能监听一次。
  • 另外如果我们要连的server只有一个,比如:1.1.1.1:80 ,同时本机只有一个ip的话,那么这个时候即使直接调connect 也只能创建出65535个连接,因为四元组中的三个是固定的了。

我们在创建连接前,经常会先调bind,bind后可以调 listen当做服务端监听,也可以直接调connect当做client来连服务端。

bind(ip,port=0) 的时候是让系统绑定到某个网卡和自动分配的端口,此时系统没有办法确定接这个socket 是要去connect还是listen. 如果是listen的话,那么肯定是不能出现端口冲突的(得local port 唯一),如果是connect的话,只要满足4元组唯一即可。在这种情况下,系统只能尽可能满足更强的要求,就是先要求端口不能冲突,即使之后去connect的时候四元组是唯一的。

比如 Nginx HaProxy envoy这些软件在创建到upstream的连接时,都会用 bind(0) 的方式, 导致到不同目的的连接无法复用同一个src port,这样后端的最大连接数受限于local_port_range。 nginx的修改 http://hg.nginx.org/nginx/rev/2c7b488a61fb

Linux 4.2后的内核增加了IP_BIND_ADDRESS_NO_PORT 这个socket option来解决这个问题,将src port的选择延后到connect的时候

IP_BIND_ADDRESS_NO_PORT (since Linux 4.2)
Inform the kernel to not reserve an ephemeral port when using bind(2) with a port number of 0. The port will later be automatically chosen at connect(2) time, in a way that allows sharing a source port as long as the 4-tuple is unique.

但如果我只是个client端,只需要连接server建立连接,也就不需要bind,直接调connect就可以了,这个时候只要保证四元组唯一就行。

bind()的时候内核是还不知道四元组的,只知道src_ip、src_port,所以这个时候单网卡下src_port是没法重复的,但是connect()的时候已经知道了四元组的全部信息,所以只要保证四元组唯一就可以了,那么这里的src_port完全是可以重复使用的。

Image

是不是加上了 SO_REUSEADDR、SO_REUSEPORT 就能重用端口了呢?

TCP SO_REUSEADDR

文档描述:

SO_REUSEADDR Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses. For AF_INET sockets this means that a socket may bind, except when there is an active listening socket bound to the address. When the listening socket is bound to INADDR_ANY with a specific port then it is not possible to bind to this port for any local address. Argument is an integer boolean flag.

从这段文档中我们可以知道三个事:

  1. 使用这个参数后,bind操作是可以重复使用local address的,注意,这里说的是local address,即ip加端口组成的本地地址,如果机器有两个本地ip,那么任意ip或端口部分不一样,它们本身就是可以共存的,不需要使用这个参数。
  2. 当local address被一个处于listen状态的socket使用时,加上该参数也不能重用这个地址。
  3. 当处于listen状态的socket监听的本地地址的ip部分是INADDR_ANY,即表示监听本地的所有ip,即使使用这个参数,也不能再bind包含这个端口的任意本地地址,这个和 2 中描述的其实是一样的。

==SO_REUSEADDR 可以用本地相同的(sip, sport) 去连connect 远程的不同的(dip、dport)//而 SO_REUSEPORT主要是解决Server端的port重用==

SO_REUSEADDR 还可以重用TIME_WAIT状态的port, 在程序崩溃后之前的TCP连接会进入到TIME_WAIT状态,需要一段时间才能释放,如果立即重启就会抛出Address Already in use的错误导致启动失败。这时候可以通过在调用bind函数之前设置SO_REUSEADDR来解决。

What exactly does SO_REUSEADDR do?

This socket option tells the kernel that even if this port is busy (in the TIME_WAIT state), go ahead and reuse it anyway. If it is busy, but with another state, you will still get an address already in use error. It is useful if your server has been shut down, and then restarted right away while sockets are still active on its port. You should be aware that if any unexpected data comes in, it may confuse your server, but while this is possible, it is not likely.

It has been pointed out that “A socket is a 5 tuple (proto, local addr, local port, remote addr, remote port). SO_REUSEADDR just says that you can reuse local addresses. The 5 tuple still must be unique!” This is true, and this is why it is very unlikely that unexpected data will ever be seen by your server. The danger is that such a 5 tuple is still floating around on the net, and while it is bouncing around, a new connection from the same client, on the same system, happens to get the same remote port.

By setting SO_REUSEADDR user informs the kernel of an intention to share the bound port with anyone else, but only if it doesn’t cause a conflict on the protocol layer. There are at least three situations when this flag is useful:

  1. Normally after binding to a port and stopping a server it’s neccesary to wait for a socket to time out before another server can bind to the same port. With SO_REUSEADDR set it’s possible to rebind immediately, even if the socket is in a TIME_WAIT state.
  2. When one server binds to INADDR_ANY, say 0.0.0.0:1234, it’s impossible to have another server binding to a specific address like 192.168.1.21:1234. With SO_REUSEADDR flag this behaviour is allowed.
  3. When using the bind before connect trick only a single connection can use a single outgoing source port. With this flag, it’s possible for many connections to reuse the same source port, given that they connect to different destination addresses.

TCP SO_REUSEPORT

SO_REUSEPORT主要用来解决惊群、性能等问题。通过多个进程、线程来监听同一端口,进来的连接通过内核来hash分发做到负载均衡,避免惊群。

SO_REUSEPORT is also useful for eliminating the try-10-times-to-bind hack in ftpd’s data connection setup routine. Without SO_REUSEPORT, only one ftpd thread can bind to TCP (lhost, lport, INADDR_ANY, 0) in preparation for connecting back to the client. Under conditions of heavy load, there are more threads colliding here than the try-10-times hack can accomodate. With SO_REUSEPORT, things work nicely and the hack becomes unnecessary.

SO_REUSEPORT使用场景:linux kernel 3.9 引入了最新的SO_REUSEPORT选项,使得多进程或者多线程创建多个绑定同一个ip:port的监听socket,提高服务器的接收链接的并发能力,程序的扩展性更好;此时需要设置SO_REUSEPORT(注意所有进程都要设置才生效)。

setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT,(const void *)&reuse , sizeof(int));

目的:每一个进程有一个独立的监听socket,并且bind相同的ip:port,独立的listen()和accept();提高接收连接的能力。(例如nginx多进程同时监听同一个ip:port)

(a) on Linux SO_REUSEPORT is meant to be used purely for load balancing multiple incoming UDP packets or incoming TCP connection requests across multiple sockets belonging to the same app. ie. it’s a work around for machines with a lot of cpus, handling heavy load, where a single listening socket becomes a bottleneck because of cross-thread contention on the in-kernel socket lock (and state).

(b) set IP_BIND_ADDRESS_NO_PORT socket option for tcp sockets before binding to a specific source ip
with port 0 if you’re going to use the socket for connect() rather then listen() this allows the kernel
to delay allocating the source port until connect() time at which point it is much cheaper

The Ephemeral Port Range

Ephemeral Port Range就是我们前面所说的Port Range(/proc/sys/net/ipv4/ip_local_port_range)

A TCP/IPv4 connection consists of two endpoints, and each endpoint consists of an IP address and a port number. Therefore, when a client user connects to a server computer, an established connection can be thought of as the 4-tuple of (server IP, server port, client IP, client port).

Usually three of the four are readily known – client machine uses its own IP address and when connecting to a remote service, the server machine’s IP address and service port number are required.

What is not immediately evident is that when a connection is established that the client side of the connection uses a port number. Unless a client program explicitly requests a specific port number, the port number used is an ephemeral port number.

Ephemeral ports are temporary ports assigned by a machine’s IP stack, and are assigned from a designated range of ports for this purpose. When the connection terminates, the ephemeral port is available for reuse, although most IP stacks won’t reuse that port number until the entire pool of ephemeral ports have been used.

So, if the client program reconnects, it will be assigned a different ephemeral port number for its side of the new connection.

linux 如何选择Ephemeral Port

有资料说是随机从Port Range选择port,有的说是顺序选择,那么实际验证一下。

如下测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>      // printf
#include <stdlib.h> // atoi
#include <unistd.h> // close
#include <arpa/inet.h> // ntohs
#include <sys/socket.h> // connect, socket

void sample() {
// Create socket
int sockfd;
if (sockfd = socket(AF_INET, SOCK_STREAM, 0), -1 == sockfd) {
perror("socket");
}

// Connect to remote. This does NOT actually send a packet.
const struct sockaddr_in raddr = {
.sin_family = AF_INET,
.sin_port = htons(8080), // arbitrary remote port
.sin_addr = htonl(INADDR_ANY) // arbitrary remote host
};
if (-1 == connect(sockfd, (const struct sockaddr *)&raddr, sizeof(raddr))) {
perror("connect");
}

// Display selected ephemeral port
const struct sockaddr_in laddr;
socklen_t laddr_len = sizeof(laddr);
if (-1 == getsockname(sockfd, (struct sockaddr *)&laddr, &laddr_len)) {
perror("getsockname");
}
printf("local port: %i\n", ntohs(laddr.sin_port));

// Close socket
close(sockfd);
}

int main() {
for (int i = 0; i < 5; i++) {
sample();
}

return 0;
}

bind逻辑测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>

void test_bind(){
int listenfd = 0, connfd = 0;
struct sockaddr_in serv_addr;
char sendBuff[1025];
time_t ticks;
socklen_t len;

listenfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_addr, '0', sizeof(serv_addr));
memset(sendBuff, '0', sizeof(sendBuff));

serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(0);

bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

len = sizeof(serv_addr);
if (getsockname(listenfd, (struct sockaddr *)&serv_addr, &len) == -1) {
perror("getsockname");
return;
}
printf("port number %d\n", ntohs(serv_addr.sin_port)); //只是挑选到了port,在系统层面保留,tcp连接还没有,netstat是看不到的
}

int main(int argc, char *argv[])
{
for (int i = 0; i < 5; i++) {
test_bind();
}
return 0;
}

3.10.0-327.ali2017.alios7.x86_64

编译后,执行(3.10.0-327.ali2017.alios7.x86_64):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#date; ./client && echo "+++++++" ; ./client && sleep 0.1 ; echo "-------" && ./client && sleep 10; date; ./client && echo "+++++++" ; ./client && sleep 0.1 && echo "******"; ./client;
Fri Nov 27 10:52:52 CST 2020
local port: 17448
local port: 17449
local port: 17451
local port: 17452
local port: 17453
+++++++
local port: 17455
local port: 17456
local port: 17457
local port: 17458
local port: 17460
-------
local port: 17475
local port: 17476
local port: 17477
local port: 17478
local port: 17479
Fri Nov 27 10:53:02 CST 2020
local port: 17997
local port: 17998
local port: 17999
local port: 18000
local port: 18001
+++++++
local port: 18002
local port: 18003
local port: 18004
local port: 18005
local port: 18006
******
local port: 18010
local port: 18011
local port: 18012
local port: 18013
local port: 18014

从测试看起来linux下端口选择跟时间有关系,起始端口肯定是顺序增加,起始端口应该是在Ephemeral Port范围内并且和时间戳绑定的某个值(也是递增的),即使没有使用任何端口,起始端口也会随时间增加而增加。

4.19.91-19.1.al7.x86_64

换个内核版本编译后,执行(4.19.91-19.1.al7.x86_64):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$date; ./client && echo "+++++++" ; ./client && sleep 0.1 ; echo "-------" && ./client && sleep 10; date; ./client && echo "+++++++" ; ./client && sleep 0.1 && echo "******"; ./client;
Fri Nov 27 14:10:47 CST 2020
local port: 7890
local port: 7892
local port: 7894
local port: 7896
local port: 7898
+++++++
local port: 7900
local port: 7902
local port: 7904
local port: 7906
local port: 7908
-------
local port: 7910
local port: 7912
local port: 7914
local port: 7916
local port: 7918
Fri Nov 27 14:10:57 CST 2020
local port: 7966
local port: 7968
local port: 7970
local port: 7972
local port: 7974
+++++++
local port: 7976
local port: 7978
local port: 7980
local port: 7982
local port: 7984
******
local port: 7988
local port: 7990
local port: 7992
local port: 7994
local port: 7996

以上测试时的参数

1
2
$cat /proc/sys/net/ipv4/ip_local_port_range
1024 65535

将1024改成1025后,分配出来的都是奇数端口了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$cat /proc/sys/net/ipv4/ip_local_port_range
1025 1034

$./client
local port: 1033
local port: 1025
local port: 1027
local port: 1029
local port: 1031
local port: 1033
local port: 1025
local port: 1027
local port: 1029
local port: 1031
local port: 1033
local port: 1025

之所以都是偶数端口,是因为port_range 从偶数开始, 每次从++变到+2的原因,connect挑选随机端口时都是在起始端口的基础上+2,而bind挑选随机端口的起始端口是系统port_range起始端口+1(这样和connect错开),然后每次仍然尝试+2,这样connect和bind基本一个用偶数另外一个就用奇数,一旦不够了再尝试使用另外一组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$cat /proc/sys/net/ipv4/ip_local_port_range
1024 1047

$./bind & ---bind程序随机挑选5个端口
port number 1039
port number 1043
port number 1045
port number 1041
port number 1047 --用完所有奇数端口

$./bind & --继续挑选偶数端口
[8] 4170
port number 1044
port number 1042
port number 1046
port number 0 --实在没有了
port number 0

可见4.19内核下每次port是+2,在3.10内核版本中是+1. 并且都是递增的,同时即使port不使用,也会随着时间的变化这个起始port增大。

Port Range有点像雷达转盘数字,时间就像是雷达上的扫描指针,这个指针不停地旋转,如果这个时候刚好有应用要申请Port,那么就从指针正好指向的Port开始向后搜索可用port

tcp_max_tw_buckets

tcp_max_tw_buckets: 在 TIME_WAIT 数量等于 tcp_max_tw_buckets 时,新的连接断开不再进入TIME_WAIT阶段,而是直接断开,并打印warnning.

实际测试发现 在 TIME_WAIT 数量等于 tcp_max_tw_buckets 时 新的连接仍然可以不断地创建和断开,这个参数大小不会影响性能,只是影响TIME_WAIT 数量的展示(当然 TIME_WAIT 太多导致local port不够除外), 这个值设置小一点会避免出现端口不够的情况

tcp_max_tw_buckets - INTEGER
Maximal number of timewait sockets held by system simultaneously.If this number is exceeded time-wait socket is immediately destroyed and warning is printed. This limit exists only to prevent simple DoS attacks, you must not lower the limit artificially, but rather increase it (probably, after increasing installed memory), if network conditions require more than default value.

监控指标:

1
netstat -s | grep TCPTimeWaitOverflow

SO_LINGER

SO_LINGER选项用来设置延迟关闭的时间,等待套接字发送缓冲区中的数据发送完成。 没有设置该选项时,在调用close() 后,在发送完FIN后会立即进行一些清理工作并返回。 如果设置了SO_LINGER选项,并且等待时间为正值,则在清理之前会等待一段时间。

如果把延时设置为 0 时,Socket就丢弃数据,并向对方发送一个 RST 来终止连接,因为走的是 RST 包,所以就不会有 TIME_WAIT 了。

This option specifies how the close function operates for a connection-oriented protocol (for TCP, but not for UDP). By default, close returns immediately, but ==if there is any data still remaining in the socket send buffer, the system will try to deliver the data to the peer==.

SO_LINGER 有三种情况

  1. l_onoff 为false(0), 那么 l_linger 的值没有意义,socket主动调用close时会立即返回,操作系统会将残留在缓冲区中的数据发送到对端,并按照正常流程关闭(交换FIN-ACK),最后连接进入TIME_WAIT状态。这是默认情况
  2. l_onoff 为true(非0), l_linger 为0,主动调用close的一方也是立刻返回,但是这时TCP会丢弃发送缓冲中的数据,而且不是按照正常流程关闭连接(不发送FIN包),直接发送RST,连接不会进入 time_wait 状态,对端会收到 java.net.SocketException: Connection reset异常
  3. l_onoff 为true(非0), l_linger 也为非 0,这表示 SO_LINGER选项生效,并且超时时间大于零,这时调用close的线程被阻塞,TCP会发送缓冲区中的残留数据,这时有两种可能的情况:
    • 数据发送完毕,收到对方的ACK,然后进行连接的正常关闭(交换FIN-ACK)
    • 超时,未发送完(指没收到对端的 ACK)的数据被丢弃,发送RST进行非正常关闭
1
2
3
4
struct linger {
int l_onoff; /* 0=off, nonzero=on */
int l_linger; /* linger time, POSIX specifies units as seconds */
};

NIO下设置 SO_LINGER 的错误案例

在使用NIO时,最好不设置SO_LINGER。比如Tomcat服务端接收到请求创建新连接时,做了这样的设置:

1
SocketChannel.setOption(SocketOption.SO_LINGER, 1000)

SO_LINGER的单位为秒!在网络环境比较好的时候,例如客户端、服务器都部署在同一个机房,close虽然会被阻塞,但时间极短可以忽略。但当网络环境不那么好时,例如存在丢包、较长的网络延迟,buffer中的数据一直无法发送成功,那么问题就出现了:close会被阻塞较长的时间,从而直接或间接引起NIO的IO线程被阻塞,服务器会不响应,不能处理accept、read、write等任何IO事件。也就是应用频繁出现挂起现象。解决方法就是删掉这个设置,close时立即返回,由操作系统接手后面的工作。

被阻塞时会看到如下连接状态:

image-20220721100246598

以及对应的堆栈

image-20220721100421130

查看其中一个IO线程等待的锁,发现锁是被HTTP线程持有。这个线程正在执行preClose0,就是在这里等待连接的关闭image-20220721100446521

每次HTTP线程在关闭连接被阻塞时,同时持有了SocketChannelImpl的对象锁,而 IO线程在把这个连接移除出它的 selector管理队列时,也要获得同一个SocketChannelImpl的对象锁。IO 线程就这么一次次的被阻塞,悲剧的无以复加。有些 NIO框架会让 IO线程去做close,这时候就更加悲剧了。

总之这里的错误原因有两点:1)网络状态不好;2)错误理解了l_linger 的单位,是秒,不是毫秒。 在这两个原因的共同作用下导致了数据迟迟不能发送完毕,l_linger 超时又需要很久,所以服务会出现一直阻塞的状态。

为什么要有 time_wait 状态

TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.

alt text

由Nginx SY CPU高负载引发内核探索之旅

这个案例来自腾讯7层网关团队,网关用的Nginx,请求转发给后面的被代理机器(RS:real server),发现 sys CPU异常高,CPU都用在搜索可用端口.

Image

Image

Figure 4: This is a flame graph of the connect syscall in Linux.

local port 不够的时候inet_hash_connect 中的spin_lock 会消耗过高的 sys(特别注意4.6内核后 local port 分奇偶数,每次loop+2,所以更容易触发port不够的场景)

核心原因总结: 4.6后内核把本地端口分成奇偶数,奇数给connect, 偶数给listen,本来端口有6万,这样connect只剩下3万,当这3万用完后也不会报找不到本地可用端口的错误(这里报错可能更好),而是在奇数里找不到就找偶数里的,每次都这样。 没改以前,总共6万端口,用掉3万,不分奇偶的话那么每找两个端口就有一个能用,也就是50%的概率。但是改了新的实现方案后,每次先要找奇数的3万个,全部在用,然后到偶数里继续找到第30001个才是可用的,也就是找到的概率变成了3万分之一,一下子复杂度高了15000倍,不慢才怪

对这个把端口分成奇偶数我的看法:这个做法就是坑爹货,在内核里胡乱搞,为了一个小场景搞崩大多数正常场景,真没必要,当然我这是事后诸葛亮,如果当时这种feature拿给我看我也会认为很不错,想不到这个坑点!

从STGW流量下降探秘内核收包机制

listen port search消耗CPU异常高

图片

在正常的情况下,服务器的listen port数量,大概就是几w个这样的量级。这种量级下,一个port对应一个socket,哈希桶大小为32是可以接受的。

然而在内核支持了reuseport并且被广泛使用后,情况就不一样了,在多进程架构里,listen port对应的socket数量,是会被几十倍的放大的。以应用层监听了5000个端口,reuseport 使用了50个cpu核心为例,5000*50/32约等于7812,意味着每次握手包到来时,光是查找listen socket,就需要遍历7800多次。随着机器硬件性能越来越强,应用层使用的cpu数量增多,这个问题还会继续加剧。

正因为上述原因,并且我们现网机器开启了reuseport,在端口数量较多的机器里,inet_lookup_listener的哈希桶大小太小,遍历过程消耗了cpu,导致出现了函数热点。

短连接的开销

用ab通过短连接走 lo 网卡压本机 nginx,CPU0是 ab 进程,CPU3/4 是 Nginx 服务,可以看到 si 非常高,QPS 2.2万

image-20220627154822263

再将 ab 改用长连接来压,可以看到si、sy都有下降,并且 si 下降到短连接的20%,QPS 还能提升到 5.2万

image-20220627154931495

一条连接的开销

主要是内存开销(如图,来源见水印),另外就是每个连接都会占用一个文件句柄,可以通过参数来设置:fs.nr_open、nofile(其实 nofile 还分 soft 和 hard) 和 fs.file-max

Image

从上图可以看到:

  • 没有收发数据的时候收发buffer不用提前分配,3K多点的内存是指一个连接的元信息数据空间,不包含传输数据的内存buffer

  • 客户端发送数据后,会根据数据大小分配send buffer(一般不超过wmem,默认kernel会根据系统内存压力来调整send buffer大小)

  • server端kernel收到数据后存放在rmem中,应用读走后就会释放对应的rmem

  • rmem和wmem都不会重用,用时分配用完释放

可见,内核在 socket 内存开销优化上采取了不少方法:

  • 内核会尽量及时回收发送缓存区、接收缓存区,但高版本做的更好
  • 发送接收缓存区最小并一定不是 rmem 内核参数里的最小值,实际大部分时间都是0
  • 其它状态下,例如对于TIME_WAIT还会回收非必要的 socket_alloc 等对象

或者看这篇分析:https://zhuanlan.zhihu.com/p/25241630

不同进程使用相同端口,设置SO_REUSEADDR后被bind 导致可用 local port 不够

A进程选择某个端口当local port 来connect,并设置了 reuseaddr opt(表示其它进程还能继续用这个端口),这时B进程选了这个端口,并且bind了,B进程用完后把这个bind的端口释放了,但是如果 A 进程一直不释放这个端口对应的连接,那么这个端口会一直在内核中记录被bind用掉了(能bind的端口 是65535个,四元组不重复的连接你理解可以无限多),这样的端口越来越多后,剩下可供 A 进程发起连接的本地随机端口就越来越少了(也就是本来A进程选择端口是按四元组的,但因为前面所说的原因,导致不按四元组了,只按端口本身这个一元组来排重),这时会造成新建连接的时候这个四元组高概率重复,一般这个时候对端大概率还在 time_wait 状态,会忽略掉握手 syn 包并回复 ack ,进而造成建连接卡顿的现象;超频繁的端口复用在LVS 场景下会产生问题,导致建连异常;或者syn包被 RST 触发1秒钟重传 syn

这个A、B进程共同跑在一台宿主机上很多年了,只因为之前是3.10内核,这次升级到了4.19后因为奇偶数放大了问题

当A进程已经开启了 SO_REUSEADDR 对外建联,此时 B 进程同样开启 SO_REUSEADDR 可以bind 此端口成功,当前端口就被设置为bind 状态,其他非 SO_REUSEADDR 的建联无法选到此端口

验证端口被connect 和 listen 同时使用

尝试先用 connect 把18181 端口用掉,然后在18181端口上起一个listen 服务,再从其他地方连这个listen的 18181端口

image-20240522103927360

抓包,本机 ip 是 172.17.151.5 :

image-20240522103407831

抓包里的 stream 1 对应上图的connect to baidu.com:80

抓包里的 stream 2 对应其它客户端连listen 18181上的服务,对应的netstat 信息:

1
2
3
4
5
#netstat -anpo |grep 18181
0.0.0.0:18181 0.0.0.0:* LISTEN 2732449/nc off (0.00/0/0)
172.17.151.5:18181 19.12.59.7:56166 ESTABLISHED 2732449/nc off (0.00/0/0) (stream2)
172.17.151.5:18181 110.242.68.66:80 ESTABLISHED 2732445/python keepalive (4.96/0/0)(stream1)
172.17.151.5:18181 10.143.33.49:123 ESTABLISHED 624/chronyd off (0.00/0/0)

可以得出如下结论:

  • 两个TCP 连接四元组不一样,互相不干涉

  • 先connect(SO_REUSEADDR) 用掉A端口后,还可以在上面继续使用A 端口来 listen(nc -l 18181)

  • 先 listen 再connect 是不行的,报:Cannot assign requested address

结论

  • 在内存、文件句柄足够的话一台服务器上可以创建的TCP连接数量是没有限制的
  • SO_REUSEADDR 主要用于快速重用 TIME_WAIT状态的TCP端口,避免服务重启就会抛出Address Already in use的错误
  • 先起一个listen 的端口设置了 SO_REUSEADDR,在其它进程 connect 的时候也不会从 port range 里再被选出来重用
  • SO_REUSEPORT主要用来解决惊群、性能等问题
  • 全局范围可以用 net.ipv4.tcp_max_tw_buckets = 50000 来限制总 time_wait 数量,但是会掩盖问题
  • local port的选择是递增搜索的,搜索起始port随时间增加也变大

参考资料

https://segmentfault.com/a/1190000002396411

linux中TCP的socket、bind、listen、connect和accept的实现

How Linux allows TCP introspection The inner workings of bind and listen on Linux.

https://idea.popcount.org/2014-04-03-bind-before-connect/

TCP连接中客户端的端口号是如何确定的?

对应4.19内核代码解析

How to stop running out of ephemeral ports and start to love long-lived connections

https://blog.cloudflare.com/how-to-stop-running-out-of-ephemeral-ports-and-start-to-love-long-lived-connections/

connect() why you so slow?https://blog.cloudflare.com/linux-transport-protocol-port-selection-performance https://lpc.events/event/17/contributions/1593/attachments/1208/2472/lpc-2023-connect-why-you-so-slow.pdf?file=lpc-2023-connect-why-you-so-slow.pdf

一次春节大促性能压测不达标的瓶颈推演

发表于 2020-11-23 | 分类于 performance

一次春节大促性能压测不达标的瓶颈推演

本文示范了教科书式的在分布式应用场景下如何通过一个节点的状态来推演分析瓶颈出在上下游的哪个环节上。

场景描述

某客户通过PTS(一个打压力工具)来压选号业务(HTTP服务在9108端口上),一个HTTP请求对应一次select seq-id 和 一次insert

PTS端看到RT900ms+,QPS大概5万(期望20万), 数据库代理服务 rt 5ms,QPS 10万+

链路:

pts发起压力 -> 5个eip -> slb -> app(300个容器运行tomcat监听9108端口上) -> slb -> 数据库代理服务集群 -> RDS集群

性能不达标,怀疑数据库代理服务或者RDS性能不行,作为数据库需要自证清白,所以从RDS和数据库代理服务开始分析问题在哪里。

略过一系列在数据库代理服务、RDS上分析数据和监控图表都证明数据库代理服务和RDS没问题的过程。

在明确给出证据数据库代理服务和RDS都没问题后还是要解决问题,所以只能进一步帮助前面的app来分析为什么性能不达标。

在其中一个app应用上抓包(00:18秒到1:04秒),到数据库代理服务的一个连接分析:

image.png

数据库代理服务每个HTTP请求的响应时间都控制在15ms(一个前端HTTP请求对应一个select seq-id,一个 select readonly, 一个insert, 这个响应时间符合预期)。一个连接每秒才收到20 tps(因为压力不够,压力加大的话这个单连接tps还可以增加), 20*3000 = 6万 , 跟压测看到基本一致

300个容器,每个容器 10个连接到数据库代理服务

如果300个容器上的并发压力不够的话就没法将3000个连接跑满,所以看到的QPS是5万。

从300个容器可以计算得到这个集群能支持的tps: 300*10(10个连接)* 1000/15(每秒钟每个连接能处理的请求数)=20万个tps (关键分析能力)

也就是说通过单QPS 15ms,我们计算可得整个后端的吞吐能力在20万QPS。所以目前问题不在后端,而是压力没有打到后端就出现瓶颈了。

9108的HTTP服务端口上的抓包分析

image.png

9108服务的每个HTTP response差不多都是15ms(这个响应时间基本符合预期),一个HTTP连接上在45秒的抓包时间范围只收到23个HTTP Request。

或者下图:

image-20220627164250973 image-20220630101036341

统计9108端口在45秒总共收到的HTTP请求数量是6745(如下图),也就是每个app每秒钟收到的请求是150个,300*150=4.5万(理论值,300个app可能压力分布不一样?),从这里看app收到的压力还不够,所以压力还没有打到应用容器中的app,还在更前面

image.png

后来从容器app监控也确认了这个响应时间和抓包看到的一致,所以从抓包分析http响应时间也基本得到15ms的rt关键结论

从wireshark IO Graphs 也能看到RT 和 QPS

image-20220623003026351

从应用容器上的netstat统计来看,也是压力端回复太慢

image.png

send-q表示回复从9108发走了,没收到对方的ack

ARMS监控分析9108端口上的RT

后来PTS的同学说ARMS可以捞到监控数据,如下是对rt时间降序排

image.png

中的rt平均时间,可以看到http的rt确实14.4ms,表现非常平稳,从这个监控也发现实际app是330个而不是用户自己描述的300个,这也就是为什么实际是tps是5万,但是按300个去算的话tps是4.5万(不要纠结客户为什么告诉你是300个容器而不是330个,有时候他们也搞不清楚,业务封装得太好了)

image.png

5分钟时间,QPS是5万+,HTTP的平均rt是15ms, HTTP的最大rt才79ms,和前面抓包分析一致。

从后端分析的总结

从9108端口响应时间15ms来看是符合预期的,为什么PTS看到的RT是900ms+,所以压力还没有打到APP上(也就是9108端口)

结论

最后发现是 eip 带宽不足,只有200M,调整到1G后 tps 也翻了5倍到了25万。

pts -> 5个eip(总带宽200M) -> slb -> app(330个HTTP容器) -> slb -> 数据库代理服务 -> RDS

这个案例有意思的地方是可以通过抓包就能分析出集群能扛的QPS20万(实际只有5万),那么可以把这个分析原则在每个角色上挨个分析一下,来看瓶颈出在了哪个环节。

应用端看到的rt是900ms,从后段开始往前面应用端来撸,看看每个环节的rt数据。

教训

  • 搞清楚 请求 从发起端到DB的链路路径,比如 pts -> 5个eip(总带宽200M) -> slb -> app(330个HTTP容器) -> slb -> 数据库代理服务 -> RDS
  • 压不上去得从发压力端开始往后端撸,撸每个产品的rt,每个产品给出自己的rt来自证清白
  • 应用有arms的话学会看arms对平均rt和QPS的统计,不要纠结个别请求的rt抖动,看平均rt
  • 通过抓包完全可以分析出来系统能扛多少并发,以及可能的瓶颈位置

一包在手 万事无忧

活久见,TCP连接互串了

发表于 2020-11-18 | 分类于 TCP

活久见,TCP连接互串了

背景

应用每过一段时间总是会抛出几个连接异常的错误,需要查明原因。

排查后发现是TCP连接互串了,这个案例实在是很珍惜,所以记录一下。

抓包

业务结构: 应用->MySQL(10.112.61.163)

在 应用 机器上抓包这个异常连接如下(3269为MySQL服务端口):

image.png

粗一看没啥奇怪的,就是应用发查询给3269,但是一直没收到3269的ack,所以一直重传。这里唯一的解释就是网络不通。最后MySQL的3269还回复了一个rst,这个rst的id是42889,引起了我的好奇,跟前面的16439不连贯,正常应该是16440才对。(请记住上图中的绿框中的数字)

于是我过滤了一下端口61902上的所有包:

image.png

可以看到绿框中的查询从61902端口发给3269后,很奇怪居然收到了一个来自别的IP+3306端口的reset,这个包对这个连接来说自然是不认识(这个连接只接受3269的回包),就扔掉了。但是也没收到3269的ack,所以只能不停地重传,然后每次都收到3306的reset,reset包的seq、id都能和上图的绿框对应上。

明明他们应该是两个连接:

61902->10.141.16.0:3306

61902->10.112.61.163:3269

他们虽然用的本地ip端口(61902)是一样的, 但是根据四元组不一样,还是不同的TCP连接,所以应该是不会互相干扰的。但是实际看起来seq、id都重复了,不会有这么巧,非常像是TCP互串了。

分析原因

10.141.16.0 这个ip看起来像是lvs的ip,查了一下系统,果然是lvs,然后这个lvs 后面的rs就是10.112.61.163

那么这个连结构就是10.141.16.0:3306:

应用 -> lvs(10.141.16.0:3306)-> 10.112.61.163:3269 跟应用直接连MySQL是一回事了

所以这里的疑问就变成了:10.141.16.0 这个IP的3306端口为啥能知道 10.112.61.163:3269端口的seq和id,也许是TCP连接串了

接着往下排查

先打个岔,分析下这里的LVS的原理

这里使用的是 full NAT模型(full NetWork Address Translation-全部网络地址转换)

基本流程(类似NAT):

  1. client发出请求(sip 200.200.200.2 dip 200.200.200.1)
  2. 请求包到达lvs,lvs修改请求包为(sip 200.200.200.1, dip rip) 注意这里sip/dip都被修改了
  3. 请求包到达rs, rs回复(sip rip,dip 200.200.200.1)
  4. 这个回复包的目的IP是VIP(不像NAT中是 cip),所以LVS和RS不在一个vlan通过IP路由也能到达lvs
  5. lvs修改sip为vip, dip为cip,修改后的回复包(sip 200.200.200.1,dip 200.200.200.2)发给client

image.png

注意上图中绿色的进包和红色的出包他们的地址变化

本来这个模型下都是正常的,但是为了Real Server能拿到client ip,也就是Real Server记录来源ip的时候希望记录的是client ip而不是LVS ip。这个时候LVS会将client ip放在tcp的options里面,然后在RealServer机器的内核里面将options中的client ip取出替换掉 lvs ip。所以Real Server上感知到的对端ip就是client ip。

回包的时候RealServer上的内核模块同样将目标地址从client ip改成lvs ip,同时将client ip放入options中。

回到问题

看完理论,再来分析这两个连接的行为

fulnat模式下连接经过lvs到达mysql后,mysql上看到的连接信息是,cip+port,也就是在MySQL上的连接

**lvs-ip:port -> 10.112.61.163:3269 被修改成了 **client-ip:61902 **-> 10.112.61.163:3269

那么跟不走LVS的连接:

client-ip:61902 -> 10.112.61.163:3269 (直连) 完全重复了。

MySQL端看到的两个连接四元组一模一样了:

10.112.61.163:3269 -> client-ip:61902 (走LVS,本来应该是lvs ip的,但是被替换成了client ip)

10.112.61.163:3269 -> client-ip:61902 (直连)

这个时候应用端看到的还是两个连接:

client-ip:61902 -> 10.141.16.0:3306 (走LVS)

client-ip:61902 -> 10.112.61.163:3269 (直连)

总结下,也就是这个连接经过LVS转换后在服务端(MYSQL)跟直连MySQL的连接四元组完全重复了,也就是MySQL会认为这两个连接就是同一个连接,所以必然出问题了

这个时候用 netstat 看到的应该是两个连接(vtoa 没有替换), 一个是client->rs, 一个是lvs->rs, 内核层面看到的还是两个连接, 只是get_peername接口被toa hook修改后, 两个连接返回的srcip是同一个

实际两个连接建立的情况:

和mysqlserver的61902是04:22建起来的,和lvs的61902端口 是42:10建起来的,和lvs的61902建起来之后马上就出问题了

问题出现的条件

  • fulnat模式的LVS,RS上装有ip转换模块(RS上会将LVS ip还原成client ip)
  • client端正好重用一个相同的本地端口分别和RS以及LVS建立了两个连接

这个时候这两个连接在MySQL端就会变成一个,然后两个连接的内容互串,必然导致rst

这个问题还挺有意思的,估计没几个程序员一辈子能碰上一次。推荐另外一个好玩的连接:如何创建一个自己连自己的TCP连接

在很多容器场景也容易出现同样的问题,比如同时暴露 Nodeport 和 Loadbalance IP

其他场景

比如在 HA 场景下,需要通过直连节点去做心跳检查(B链路);同时又要走A链路去跨机房检测,这两个链路下连接的目标IP一直、端口不一样,但是经过转换后都是MySQL-Server+3306端口,容易出现两条连接转换后变成一条连接

image-20240723203828093

参考资料

就是要你懂负载均衡–lvs和转发模式

https://idea.popcount.org/2014-04-03-bind-before-connect/

no route to host

另一种形式的tcp连接互串,新连接重用了time_wait的port,导致命中lvs内核表中的维护的旧连接发给了老的realserver

MySQL针对秒杀场景的优化

发表于 2020-11-18 | 分类于 MySQL

MySQL针对秒杀场景的优化

背景

对于秒杀热点场景,MySQL官方版本500 TPS每秒,在对MySQL优化前只能用redis来扛,redis没有事务能力,比如一个item下有多个sku就搞不定了。同时在前端搞限流、答题等让秒杀流量控制在可以承受的范围内。

过程

对于秒杀热点场景,MySQL官方版本扣减只能做到 500 TPS每秒,扛不住大促的流量,需要优化。从控制并发量将500优化到1400,再通过新语法来消除网络rtt对加锁时间的控制这样达到了 4000 TPS。最后合并多个扣减到一个,累积比如10ms提交,能将TPS 能升到4万以上这个能力。

排队控制并发

拍减模式在整个交易过程中只有一次扣减交互,所以是不需要付款减库存那样的判重逻辑,就是说,拍减的减库存sql只有一条update语句就搞定了。而付减有两条,一条insert判重+一条update减库存(双十一拍减接口在高峰的rt约为8ms,而付减接口在高峰的rt约为15ms);

其次,当大量请求(线程)落到mysql的同一条记录上进行减库存时,线程之间会存在竞争关系,因为要争夺InnoDB的行锁,当一个线程获得了行锁,其他并发线程就只能等待(InnoDB内部还有死锁检测等机制会严重影响性能),当并发度越高时,等待的线程就越多,此时tps会急剧下降,rt会飙升,性能就不能满足要求了。那如何减少锁竞争?答案是:排队!库存中心从几个层面做了排队策略。

首先,在应用端进行排队,因为很多商品都是有sku的,当sku库存变化时item的库存也要做相应变化,所以需要根据itemId来进行排队,相同itemId的减库存操作会进入串行化排队处理逻辑,不过应用端的排队只能做到单机内存排队,当应用服务器数量过多时,落到db的并发请求仍然很多,所以最好的办法是在db端也加上排队策略,今年库存中心db部署了两个的排队patch,一个叫“并发控制”,是做在InnoDB层的,另一个叫“queue on pk”,是做在mysql的server层的,两个patch各有优缺点,前者不需要应用修改代码,db自动判断,后者需要应用程序写特殊的sql hint,前者控制的全局的sql,后者是根据hint来控制指定sql,两个patch的本质和应用端的排队逻辑是一致的,具体实现不同。双十一库存中心使用的是“并发控制”的patch。

2013年的单减库存TPS最高记录是1381次每秒。

对于秒杀热点场景,官方版本500tps每秒,问题在于同时涌入的请求太多,每次取锁都要检查其它等锁的线程(防止死锁),这个线程队列太长的话导致这个检查时间太长; 继续在前面增加能够进入到后面的并发数的控制,通过增加线程池、控制并发能到1400(no deadlock list check);

热点更新下的死锁检测(no deadlock list check)

由于热点更新是分布式的客户端并发的向单点的数据库进行了并行更新一条记录,到数据库最后是把并行的线程转行成串行的操作。但在串行操作的时候,由于对同一记录的锁申请列表过大,死锁检测的机制在检测锁队列的时候,反而拖慢了每一个更新。

原生版本的MySQL对于正常业务链接没有拒绝机制(除了TDDL的链接池或者MySQL的user_connnection不够用),对于同一行记录到innodb层修改的时候,凡是到innodb层的任务都必须拿到innodb_thread_concurrency的槽位才能执行(当然这里也有很多细节,这里就说最主要的代码改动点),举例来说:开启一个事务,对于id=1的行记录更新,进到innodb层,占着1个innodb_thread_concurrency,等到id=1的事务结束,会释放innodb_thread_concurrency,从而达到innodb_thread_concurrency的平衡;

再进一步,开启一个事务,对id=1的行记录更新进到innodb层,占着1个innodb_thread_concurrency,事务不提交(假设innodb_thread_concurrency=32),如果有下一个对id=1记录来更新的话,进到innodb层,又占着1个innodb_thread_concurrency,检测发现是对id=1的更新,排到第1个对id=1的队列的后面,同时释放innodb_thread_concurrency;以此类推这个链表有可能会很长比如1024;执行的时候又需要做死锁检测等一系列工作,都需要用到一个叫做kernel_mutex的mutex(这是一个全局互斥量用来管理锁系统,事务系统,MVCC多版本控制),对于大并发,整个链表非常长的时候,可想而知kernel_metex的竞争多么激烈,从而在链表检测的时间变长。

缩短锁时间

接下来的问题在于一个事务中有多条语句(最少也有一个update+一个commit),这样update(减库存,开始锁表),走网络,查询结果(走网络),commit,两次跨网络调用导致update锁行比较久,于是可以新造一个语法 select update一次搞定,继续优化 select update commit_on_success_or_fail_rollback,将所有操作一次网络操作全部搞定,能到4000;

比如库存扣减的业务逻辑可以简化为下面这个事务:

(1)begin;

(2)insert 交易流水表; – 交易流水对账

(3)update 库存明细表 where id in (sku_id,item_id);

(4)select 库存明细表;

(5)commit

Snip20161116_88.png

SQL case:

1
2
3
4
4059550 Query   SET autocommit=0
4059550 Query update ROLLBACK_ON_FAIL TARGET_AFFECT_ROW 1 trade set version = version+3 ,gmt_modified = now() , optype = 2 , feature = ';abc;' where sub_biz_order_id = '15' and biz_order_type = 1 and id = 5 and ti_id = 1 and optype = 3 and root_id = 11
4059550 Query select id,* from update COMMIT_ON_SUCCESS ROLLBACK_ON_FAIL TARGET_AFFECT_ROW 1 invetory set withholding_quantity = withholding_quantity + -1, flag=flag &~ (1<<10) &~ (1<<11) , version=version+3,gmt_modified = now() WHERE root_id = 11 and status = 1 and id in ( 1 ) and (withholding_quantity + -1) >= 0
4059550 Query commit

批量提交

其主要的核心思想是:针对应用层SQL做轻量化改造,带上”热点行SQL”的hint,当这种SQL进入内核后,在内存中维护一个hash表,将主键或唯一键相同的请求(一般也就是同一商品id)hash到同一个地方做请求的合并,经过一段时间后(默认100us)统一提交,从而实现了将串行处理变成了批处理,让每个热点行更新请求并不需要都去扫描和更新btree。

  1. 热点的自动识别:前面已经讲过了,库存的扣减SQL都会有commit on success标记。mysql内部分为普通通道和热点扣减通道。普通通道里是正常的事务。热点通道里收集带有commit on success标记的事务。在一定的时间区间段内(0.1ms),将收集到的热点按照主键或者唯一键进行hash; hash到同一个桶中为相同的sku; 分批组提交这0.1ms收集到的热点商品。
  2. 轮询处理: 第一批进行提交时,第二批进行收集; 当第一批完成了提交开始收集时,第二批就可以进行提交了。不断轮询,提高效率

通过内存合并库存减操作,干到100000(每个减库存操作生成一条独立的update binlog,不影响其他业务2016年双11),实际这里还可以调整批提交时间间隔来进一步提升扣减QPS

Snip20161116_87.png

超卖:付款减库存会超卖,拍减库存要防止恶意拍不付款。拍减的话可以通过增加SQL新语法来进一步优化DB响应(select update)

innodb_buffer_pool_instance: 将buffer pool 分成几个(hash),避免高并发修改的时候一个大锁mutex导致性能不高

批量提交的压测效果:

image-20230814104356084

业务优化

延迟扣减item,一般一个item下会有多个sku(比如 iPhone14 不同的颜色、配置就是一个不同的sku),而库存会有总库存(item),也有sku 库存,sku库存加起来就是item库存

导致扣减的时候 item库存更热

MySQL线程池导致的延时卡顿排查

发表于 2020-11-17 | 分类于 MySQL

MySQL 线程池导致的卡顿

问题描述

简单小表的主键点查SQL,单条执行很快,但是放在业务端,有时快有时慢,取了一条慢sql,在MySQL侧查看,执行时间很短。

通过Tomcat业务端监控有显示慢SQL,取slow.log里显示有12秒执行时间的SQL,但是这次12秒的执行在MySQL上记录下来的执行时间都不到1ms。

所在节点的tsar监控没有异常,Tomcat manager监控上没有fgc,Tomcat实例规格 16C32g*8, MySQL 32c128g *32 。

5-28号现象复现,从监控图上CPU、内存、网络都没发现异常,MySQL侧查到的SQL依然执行很快,Tomcat侧记录12S执行时间,当时Tomcat节点的网络流量、CPU压力都很小。

所以客户怀疑Tomcat有问题或者Tomcat上的代码写得有问题导致了这个问题,需要排查和解决掉。

接下来我们会先分析这个问题出现的原因,然后会分析这类问题的共性同时拓展到其它场景下的类似问题。

Tomcat上抓包分析

慢的连接

经过抓包分析发现在慢的连接上,所有操作都很慢,包括set 命令,慢的时间主要分布在3秒以上,1-3秒的慢查询比较少,这明显不太符合分布规律。并且目前看慢查询基本都发生在MySQL的0库的部分连接上(后端有一堆MySQL组成的集群),下面抓包的4637端口是MySQL的服务端口:

image.png

以上两个连接都很慢,对应的慢查询在MySQL里面记录很快。

慢的SQL的response按时间排序基本都在3秒以上:

image.png

或者只看response time 排序,中间几个1秒多的都是 Insert语句。也就是1秒到3秒之间的没有,主要是3秒以上的查询

image.png

快的连接

同样一个查询SQL,发到同一个MySQL上(4637端口),下面的连接上的所有操作都很快,下面是两个快的连接上的执行截图

image.png

别的MySQL上都比较快,比如5556分片上的所有response RT排序,只有偶尔极个别的慢SQL

image.png

MySQL相关参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
mysql> show variables like '%thread%';
+--------------------------------------------+-----------------+
| Variable_name | Value |
+--------------------------------------------+-----------------+
| innodb_purge_threads | 1 |
| innodb_MySQL_thread_extra_concurrency | 0 |
| innodb_read_io_threads | 16 |
| innodb_thread_concurrency | 0 |
| innodb_thread_sleep_delay | 10000 |
| innodb_write_io_threads | 16 |
| max_delayed_threads | 20 |
| max_insert_delayed_threads | 20 |
| myisam_repair_threads | 1 |
| performance_schema_max_thread_classes | 50 |
| performance_schema_max_thread_instances | -1 |
| pseudo_thread_id | 12882624 |
| MySQL_is_dump_thread | OFF |
| MySQL_threads_running_ctl_mode | SELECTS |
| MySQL_threads_running_high_watermark | 50000 |
| rocksdb_enable_thread_tracking | OFF |
| rocksdb_enable_write_thread_adaptive_yield | OFF |
| rocksdb_signal_drop_index_thread | OFF |
| thread_cache_size | 100 |
| thread_concurrency | 10 |
| thread_handling | pool-of-threads |
| thread_pool_high_prio_mode | transactions |
| thread_pool_high_prio_tickets | 4294967295 |
| thread_pool_idle_timeout | 60 |
| thread_pool_max_threads | 100000 |
| thread_pool_oversubscribe | 10 |
| thread_pool_size | 96 |
| thread_pool_stall_limit | 30 |
| thread_stack | 262144 |
| threadpool_workaround_epoll_bug | OFF |
| tokudb_cachetable_pool_threads | 0 |
| tokudb_checkpoint_pool_threads | 0 |
| tokudb_client_pool_threads | 0 |
+--------------------------------------------+-----------------+
33 rows in set (0.00 sec)

综上结论

问题原因跟MySQL线程池比较相关,慢的连接总是慢,快的连接总是快。需要到MySQL Server下排查线程池相关参数。

同一个慢的连接上的回包,所有 ack 就很快(OS直接回,不需要进到MySQL),但是set就很慢,基本理解只要进到MySQL的就慢了,所以排除了网络原因(流量本身也很小,也没看到乱序、丢包之类的)

问题解决

18点的时候将4637端口上的MySQL thread_pool_oversubscribe 从10调整到20后,基本没有慢查询了:

image.png

当时从MySQL的观察来看,并发压力很小,很难抓到running thread比较高的情况(update: 可能是任务积压在队列中,只是96个thread pool中的一个thread全部running,导致整体running不高)

MySQL记录的执行时间是指SQL语句开始解析后统计,中间的等锁、等Worker都不会记录在执行时间中,所以当时对应的SQL在MySQL日志记录中很快。

thread_pool_stall_limit 会控制一个SQL过长时间(默认60ms)占用线程,如果出现stall_limit就放更多的SQL进入到thread pool中直到达到thread_pool_oversubscribe个

The thread_pool_stall_limit affects executing statements. The value is the amount of time a statement has to finish after starting to execute before it becomes defined as stalled, at which point the thread pool permits the thread group to begin executing another statement. The value is measured in 10 millisecond units, so the default of 6 means 60ms. Short wait values permit threads to start more quickly. Short values are also better for avoiding deadlock situations. Long wait values are useful for workloads that include long-running statements, to avoid starting too many new statements while the current ones execute.

类似案例

一个其它客户同样的问题的解决过程,最终发现是因为thread pool group中的active thread count 计数有泄漏,导致达到thread_pool_oversubscribe 的上限(实际没有任何线程运行)

问题:1)为什么调整到20后 active_thread_count 没变,但是不卡了?那以前卡着的10个 thread在干嘛?

​ 2)卡住的根本原因是,升级能解决?

image-20230308214801877

调整前的 thread pool 配置:

image-20230308222538102

出问题时候的线程池 32个 group状态,有两个group queue count、active thread都明显到了瓶颈:select * from information_schema.THREAD_POOL_STATUS;

image-20230308222416238

  • id 线程组id
  • thread_count // 当前线程组中的线程数量
  • active_thread_count //当前线程组中活跃线程数量,这个不包含dump线程
  • waiting_thread_count // 当前线程组中处于waiting状态的线程数量
  • dump_thread_count // dump类线程数量
  • slow_thread_timeout_count // 目前仅对DDL生效
  • connection_count // 当前线程组中的连接数量
  • low_queue_count // 低优先级队列中的请求数量
  • high_queue_count // 高优先级队列中的请求数量
  • waiting_thread_timeout_count // 处于waiting状态并且超时的线程数量

or: select sum(LOW_QUEUE_COUNT) from information_schema.thread_pool_status;

调整 thread_pool_oversubscribe由10到20后不卡了,这时的 pool status(重点注意 ACTIVE_THREAD_COUNT 数字没有任何变化):

image-20230308223126774

看起来像是 ACTIVE_THREAD 全假死了,没有跑任务也不去take新任务一直卡在那里,类似线程泄露。查看了一下所有MySQLD 线程都是 S 的正常状态,并无异常。

继续分析统计了一下mysqld线程数量(157),远小于 thread pool 中的active线程数量(202),看起来像是计数器在线程异常(磁盘故障时)忘记 减减 了,导致计数器虚高进而无法新创建新线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#top -H -b -n 1 -p 130650 |grep mysqld | wc -l
157

# mysql -e "select sum(THREAD_COUNT), sum(ACTIVE_THREAD_COUNT) from information_schema.THREAD_POOL_STATUS"
+-------------------+--------------------------+
| sum(THREAD_COUNT) | sum(ACTIVE_THREAD_COUNT) |
+-------------------+--------------------------+
| 71 | 206 |
+-------------------+--------------------------+
# mysql -e "select sum(THREAD_COUNT), sum(ACTIVE_THREAD_COUNT) from information_schema.THREAD_POOL_STATUS"
+-------------------+--------------------------+
| sum(THREAD_COUNT) | sum(ACTIVE_THREAD_COUNT) |
+-------------------+--------------------------+
| 75 | 202 |
+-------------------+--------------------------+
# mysql_secbox --exe-path=mysql -A -uroot -h127.0.0.1 -P3054 -e "select ID,THREAD_COUNT,ACTIVE_THREAD_COUNT AS ATC,CONNECTION_COUNT AS CC,LOW_QUEUE_COUNT,HIGH_QUEUE_COUNT from information_schema.THREAD_POOL_STATUS"
+----+--------------+-----+----+-----------------+------------------+
| ID | THREAD_COUNT | ATC | CC | LOW_QUEUE_COUNT | HIGH_QUEUE_COUNT |
+----+--------------+-----+----+-----------------+------------------+
| 0 | 3 | 7 | 13 | 0 | 0 |
| 19 | 3 | 10 | 14 | 0 | 0 |
| 20 | 3 | 8 | 13 | 0 | 0 |
| 21 | 2 | 5 | 9 | 0 | 0 |
| 28 | 2 | 6 | 6 | 0 | 0 |
//增加了一个active count,执行完毕后active thread count会减下去
| 29 | 2 | 12 | 14 | 0 | 0 |
| 30 | 2 | 4 | 11 | 0 | 0 |
| 31 | 2 | 8 | 10 | 0 | 0 |
+----+--------------+-----+----+-----------------+------------------+

正常的thread_pool状态, ACTIVE_THREAD_COUNT极小且小于 THREAD_COUNT:

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> select ID,THREAD_COUNT,ACTIVE_THREAD_COUNT AS ATC,CONNECTION_COUNT AS CC,LOW_QUEUE_COUNT,HIGH_QUEUE_COUNT from information_schema.THREAD_POOL_STATUS;
+----+--------------+-----+----+-----------------+------------------+
| ID | THREAD_COUNT | ATC | CC | LOW_QUEUE_COUNT | HIGH_QUEUE_COUNT |
+----+--------------+-----+----+-----------------+------------------+
| 0 | 2 | 0 | 3 | 0 | 0 |
| 1 | 2 | 0 | 2 | 0 | 0 |
| 2 | 2 | 0 | 2 | 0 | 0 |
| 3 | 2 | 0 | 2 | 0 | 0 |
| 4 | 2 | 0 | 4 | 0 | 0 |
| 5 | 2 | 0 | 3 | 0 | 0 |
| 6 | 2 | 0 | 4 | 0 | 0 |
| 7 | 2 | 0 | 4 | 0 | 0 |
+----+--------------+-----+----+-----------------+------------------+

Thread Pool原理

image.png

MySQL 原有线程调度方式有每个连接一个线程(one-thread-per-connection)和所有连接一个线程(no-threads)。

no-threads一般用于调试,生产环境一般用one-thread-per-connection方式。one-thread-per-connection 适合于低并发长连接的环境,而在高并发或大量短连接环境下,大量创建和销毁线程,以及线程上下文切换,会严重影响性能。另外 one-thread-per-connection 对于大量连接数扩展也会影响性能。

为了解决上述问题,MariaDB、Percona、Aliyun RDS、Oracle MySQL 都推出了线程池方案,它们的实现方式大体相似,这里以 Percona 为例来简略介绍实现原理,同时会介绍我们在其基础上的一些改进。

线程池由一系列 worker 线程组成,这些worker线程被分为thread_pool_size个group。用户的连接按 round-robin 的方式映射到相应的group 中,一个连接可以由一个group中的一个或多个worker线程来处理。

thread_pool_oversubscribe 一个group中活跃线程和等待中的线程超过thread_pool_oversubscribe时,不会创建新的线程。 此参数可以控制系统的并发数,同时可以防止调度上的死锁,考虑如下情况,A、B、C三个事务,A、B 需等待C提交。A、B先得到调度,同时活跃线程数达到了thread_pool_max_threads上限,随后C继续执行提交,此时已经没有线程来处理C提交,从而导致A、B一直等待。thread_pool_oversubscribe控制group中活跃线程和等待中的线程总数,从而防止了上述情况。

MySQL Thread Pool之所以分成多个小的Thread Group Pool而不是一个大的Pool,是为了分解锁(每个group中都有队列,队列需要加锁。类似ConcurrentHashMap提高并发的原理),提高并发效率。另外如果对每个Pool的 Worker做CPU 亲和性绑定也会对cache更友好、效果更高

group中又有多个队列,用来区分优先级的,事务中的语句会放到高优先队列(非事务语句和autocommit 都会在低优先队列);等待太久的SQL也会挪到高优先队列,防止饿死。

比如启用Thread Pool后,如果出现多个慢查询,容易导致拨测类请求超时,进而出现Server异常的判断(类似Nginx 边缘触发问题);或者某个group满后导致慢查询和拨测失败之类的问题

thread_pool_size 过小的案例

应用出现大量1秒超时报错:

image.png

image-20211104130625676

分析代码,这个Druid报错堆栈是数据库连接池在创建到MySQL的连接后或者从连接池取一个连接给业务使用前会发送一个ping来验证下连接是否有效,有效后才给应用使用。说明TCP连接创建成功,但是MySQL 超过一秒钟都没有响应这个 ping,说明 MySQL处理指令缓慢。

继续分析MySQL的参数:

image.png

可以看到thread_pool_size是1,太小了,将所有MySQL线程都放到一个buffer里面来抢锁,锁冲突的概率太高。调整到16后可以明显看到MySQL的RT从原来的12ms下降到了3ms不到,整个QPS大概有8%左右的提升。这是因为pool size为1的话所有sql都在一个队列里面,多个worker thread加锁等待比较严重,导致rt延迟增加。

image.png

这个问题发现是因为压力一上来的时候要创建大量新的连接,这些连结创建后会去验证连接的有效性,也就是Druid给MySQL发一个ping指令,一般都很快,同时Druid对这个valid操作设置了1秒的超时时间,从实际看到大量超时异常堆栈,从而发现MySQL内部响应有问题。

MySQL ping 和 MySQL 协议相关知识

Ping use the JDBC method Connection.isValid(int timeoutInSecs). Digging into the MySQL Connector/J source, the actual implementation uses com.mysql.jdbc.ConnectionImpl.pingInternal() to send a simple ping packet to the DB and returns true as long as a valid response is returned.

MySQL ping protocol是发送了一个 0e 的byte标识给Server,整个包加上2byte的Packet Length(内容为:1),2byte的Packet Number(内容为:0),总长度为5 byte。Druid、DRDS默认都会 testOnBorrow,所以每个连接使用前都会先做ping。

1
2
3
4
5
6
7
8
9
10
11
12
public class MySQLPingPacket implements CommandPacket {
private final WriteBuffer buffer = new WriteBuffer();
public MySQLPingPacket() {
buffer.writeByte((byte) 0x0e);
}
public int send(final OutputStream os) throws IOException {
os.write(buffer.getLengthWithPacketSeq((byte) 0)); // Packet Number
os.write(buffer.getBuffer(),0,buffer.getLength()); // Packet Length 固定为1
os.flush();
return 0;
}
}

image.png

也就是一个TCP包中的Payload为 MySQL协议中的内容长度 + 4(Packet Length+Packet Number):https://dev.mysql.com/doc/dev/mysql-server/8.4.2/page_protocol_com_ping.html

线程池卡死案例:show stats导致集群3406监控卡死

现象

应用用于获取监控信息的端口 3406卡死,监控脚本无法连接上3406,监控没有数据(需要从3406采集)、DDL操作、show processlist、show stats操作卡死(需要跟整个集群的3406端口同步)。

通过jstack看到应用进程的3406端口线程池都是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
"ManagerExecutor-1-thread-1" #47 daemon prio=5 os_prio=0 tid=0x00007fe924004000 nid=0x15c runnable [0x00007fe9034f4000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
- locked <0x0000000722538b60> (a com.mysql.jdbc.util.ReadAheadInputStream)
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3005)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3466)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3456)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3897)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2524)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2677)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2545)
- locked <0x00000007432e19c8> (a com.mysql.jdbc.JDBC4Connection)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2503)
at com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1369)
- locked <0x00000007432e19c8> (a com.mysql.jdbc.JDBC4Connection)
at com.alibaba.druid.pool.ValidConnectionCheckerAdapter.isValidConnection(ValidConnectionCheckerAdapter.java:44)
at com.alibaba.druid.pool.DruidAbstractDataSource.testConnectionInternal(DruidAbstractDataSource.java:1298)
at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:1057)
at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:997)
at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:987)
at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:103)
at com.taobao.tddl.atom.AbstractTAtomDataSource.getConnection(AbstractTAtomDataSource.java:32)
at com.alibaba.cobar.ClusterSyncManager$1.run(ClusterSyncManager.java:60)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:627)
at java.lang.Thread.run(Thread.java:882)

原因

  1. 用户监控采集数据通过访问3306端口上的show stats,这个show stats命令要访问集群下所有节点的3406端口来执行show stats,3406端口上是一个大小为8个的Manager 线程池在执行这些show stats命令,导致占满了manager线程池的8个线程,每个3306的show stats线程都在wait 所有节点3406上的子任务的返回
  2. 每个子任务的线程,都在等待向集群所有节点3406端口的manager建立连接,建连接后会先执行testValidatation操作验证连接的有效性,这个验证操作会执行SQL Query:select 1,这个query请求又要申请一个manager线程才能执行成功
  3. 默认isValidConnection操作没有超时时间,如果Manager线程池已满后需要等待至socketTimeout后才会返回,导致这里出现卡死,还不如快速返回错误,可以增加超时来改进

从线程栈来说,就是出现了活锁

解决方案

  • 增加manager线程池大小
  • 代码逻辑上优化3406 jdbc连接池参数,修改jdbc默认的socketTimeout超时时间以及替换默认checker(一般增加一个1秒超时的checker)

对于checker,参考druid的实现,com/alibaba/druid/pool/vendor/MySqlValidConnectionChecker.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//druid的MySqlValidConnectionChecker设定了valid超时时间为1秒
public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
if (conn.isClosed()) {
return false;
}

if (usePingMethod) {
if (conn instanceof DruidPooledConnection) {
conn = ((DruidPooledConnection) conn).getConnection();
}

if (conn instanceof ConnectionProxy) {
conn = ((ConnectionProxy) conn).getRawObject();
}

if (clazz.isAssignableFrom(conn.getClass())) {
if (validationQueryTimeout <= 0) {
validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT;// 默认值1ms
}

try {
ping.invoke(conn, true, validationQueryTimeout * 1000); //1秒
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof SQLException) {
throw (SQLException) cause;
}
throw e;
}
return true;
}
}

String query = validateQuery;
if (validateQuery == null || validateQuery.isEmpty()) {
query = DEFAULT_VALIDATION_QUERY;
}

Statement stmt = null;
ResultSet rs = null;
try {
stmt = conn.createStatement();
if (validationQueryTimeout > 0) {
stmt.setQueryTimeout(validationQueryTimeout);
}
rs = stmt.executeQuery(query);
return true;
} finally {
JdbcUtils.close(rs);
JdbcUtils.close(stmt);
}

}

//使用如上validation
public final static String DEFAULT_DRUID_MYSQL_VALID_CONNECTION_CHECKERCLASS =
"com.alibaba.druid.pool.vendor.MySqlValidConnectionChecker";

String validConnnectionCheckerClassName =
TAtomConstants.DEFAULT_DRUID_MYSQL_VALID_CONNECTION_CHECKERCLASS;
try {
Class.forName(validConnnectionCheckerClassName);
localDruidDataSource.setValidConnectionCheckerClassName(validConnnectionCheckerClassName);

这种线程池打满特别容易在分布式环境下出现,除了以上案例比如还有:

分库分表业务线程池,接收一个逻辑 SQL,如果需要查询1024分片的sort merge join,相当于派生了一批子任务,每个子任务占用一个线程,父任务等待子任务执行后返回数据。如果这样的逻辑SQL同时来一批并发,就会出现父任务都在等子任务,子任务又因为父任务占用了线程,导致子任务也在等着从线程池中取线程,这样父子任务就进入了死锁

比如并行执行的SQL MPP线程池也有这个问题,多个查询节点收到SQL,拆分出子任务做并行,互相等待资源

X应用对分布式任务打挂线程池的优化

对如下这种案例:

X 应用通过线程池来接收一个逻辑SQL并处理,如果需要查询1024分片的sort merge join,相当于派生了1024个子任务,每个子任务占用一个线程,父任务等待子任务执行后返回数据。如果这样的逻辑SQL同时来一批并发,就会出现父任务都在等子任务,子任务又因为父任务占用了线程,导致子任务也在等着从线程池中取线程,这样父子任务就进入了死锁

首先X对执行SQL 的线程池分成了多个bucket,每个SQL只跑在一个bucket里面的线程上,同时通过滑动窗口向线程池提交任务数,来控制并发量,进而避免线程池的死锁、活锁问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static final ServerThreadPool create(String name, int poolSize, int deadLockCheckPeriod, int bucketSize) {
return new ServerThreadPool(name, poolSize, deadLockCheckPeriod, bucketSize); //bucketSize可以设置
}

public ServerThreadPool(String poolName, int poolSize, int deadLockCheckPeriod, int bucketSize) {
this.poolName = poolName;
this.deadLockCheckPeriod = deadLockCheckPeriod;

this.numBuckets = bucketSize;
this.executorBuckets = new ThreadPoolExecutor[bucketSize];
int bucketPoolSize = poolSize / bucketSize; //将整个pool分成多个bucket
this.poolSize = bucketPoolSize;
for (int bucketIndex = 0; bucketIndex < bucketSize; bucketIndex++) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(bucketPoolSize,
bucketPoolSize,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
new NamedThreadFactory(poolName + "-bucket-" + bucketIndex, true));

executorBuckets[bucketIndex] = executor;
}

this.lastCompletedTaskCountBuckets = new long[bucketSize];
// for check thread
if (deadLockCheckPeriod > 0) {
this.timer = new Timer(SERVER_THREAD_POOL_TIME_CHECK, true);
buildCheckTask();
this.timer.scheduleAtFixedRate(checkTask, deadLockCheckPeriod, deadLockCheckPeriod);
}
}

通过bucketSize将一个大的线程池分成多个小的线程池,每个SQL 控制跑在一个小的线程池中,这里和MySQL的thread_pool是同样的设计思路,当然MySQL 的thread_pool主要是为了改进大锁的问题。

另外DRDS上线程池拆分后性能也有提升:

image-20211104163732499

测试结果说明:(以全局线程池为基准,分别关注:关日志、分桶线程池、协程)

  1. 关日志,整体性能提升在20%左右 (8core最好成绩在6.4w qps)
  2. 协程,整体性能15%左右
  3. 关日志+协程,整体提升在35%左右 (8core最好成绩在7w qps)
  4. 分桶,整体性能提升在18%左右
  5. 分桶+关日志,整体提升在39%左右 (8core最好成绩在7.4w qps)
  6. 分桶+协程,整体提升在36%左右
  7. 分桶+关日志+协程,整体提升在60%左右 (8core最好成绩在8.3w qps)

线程池拆成多个bucket优化分析

拆分前锁主要是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
Started [lock] profiling
--- Execution profile ---
Total samples: 496

Frame buffer usage: 0.0052%

--- 352227700 ns (53.09%), 248 samples
[ 0] java.util.Properties
[ 1] java.util.Hashtable.get
[ 2] java.util.Properties.getProperty
[ 3] com.taobao.tddl.common.properties.SystemPropertiesHelper.getPropertyValue
[ 4] com.taobao.tddl.executor.MatrixExecutor.configMppExecutionContext
[ 5] com.taobao.tddl.executor.MatrixExecutor.optimize
[ 6] com.taobao.tddl.matrix.jdbc.TConnection.optimizeThenExecute
[ 7] com.taobao.tddl.matrix.jdbc.TConnection.executeSQL
[ 8] com.taobao.tddl.matrix.jdbc.TPreparedStatement.executeSQL
[ 9] com.taobao.tddl.matrix.jdbc.TStatement.executeInternal
[10] com.taobao.tddl.matrix.jdbc.TPreparedStatement.execute
[11] com.alibaba.cobar.server.ServerConnection.innerExecute
[12] com.alibaba.cobar.server.ServerConnection.innerExecute
[13] com.alibaba.cobar.server.ServerConnection$1.run
[14] com.taobao.tddl.common.utils.thread.FlowControlThreadPool$RunnableAdapter.run
[15] java.util.concurrent.Executors$RunnableAdapter.call
[16] java.util.concurrent.FutureTask.run
[17] java.util.concurrent.ThreadPoolExecutor.runWorker
[18] java.util.concurrent.ThreadPoolExecutor$Worker.run
[19] java.lang.Thread.run

--- 307781689 ns (46.39%), 243 samples
[ 0] java.util.Properties
[ 1] java.util.Hashtable.get
[ 2] java.util.Properties.getProperty
[ 3] com.taobao.tddl.common.properties.SystemPropertiesHelper.getPropertyValue
[ 4] com.taobao.tddl.config.ConfigDataMode.isDrdsMasterMode
[ 5] com.taobao.tddl.matrix.jdbc.TConnection.updatePlanManagementInfo
[ 6] com.alibaba.cobar.server.ServerConnection.innerExecute
[ 7] com.alibaba.cobar.server.ServerConnection.innerExecute
[ 8] com.alibaba.cobar.server.ServerConnection$1.run
[ 9] com.taobao.tddl.common.utils.thread.FlowControlThreadPool$RunnableAdapter.run
[10] java.util.concurrent.Executors$RunnableAdapter.call
[11] java.util.concurrent.FutureTask.run
[12] java.util.concurrent.ThreadPoolExecutor.runWorker
[13] java.util.concurrent.ThreadPoolExecutor$Worker.run
[14] java.lang.Thread.run

--- 3451038 ns (0.52%), 4 samples
[ 0] java.lang.Object
[ 1] sun.nio.ch.SocketChannelImpl.ensureReadOpen
[ 2] sun.nio.ch.SocketChannelImpl.read
[ 3] com.alibaba.cobar.net.AbstractConnection.read
[ 4] com.alibaba.cobar.net.NIOReactor$R.read
[ 5] com.alibaba.cobar.net.NIOReactor$R.run
[ 6] java.lang.Thread.run

--- 4143 ns (0.00%), 1 sample
[ 0] com.taobao.tddl.common.IdGenerator
[ 1] com.taobao.tddl.common.IdGenerator.nextId
[ 2] com.alibaba.cobar.server.ServerConnection.genTraceId
[ 3] com.alibaba.cobar.server.ServerQueryHandler.query
[ 4] com.alibaba.cobar.net.FrontendConnection.query
[ 5] com.alibaba.cobar.net.handler.FrontendCommandHandler.handle
[ 6] com.alibaba.cobar.net.FrontendConnection$1.run
[ 7] java.util.concurrent.ThreadPoolExecutor.runWorker
[ 8] java.util.concurrent.ThreadPoolExecutor$Worker.run
[ 9] java.lang.Thread.run

ns percent samples top
---------- ------- ------- ---
660009389 99.48% 491 java.util.Properties
3451038 0.52% 4 java.lang.Object
4143 0.00% 1 com.taobao.tddl.common.IdGenerator

com.taobao.tddl.matrix.jdbc.TConnection.optimizeThenExecute调用对应代码逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (InsertSplitter.needSplit(sql, policy, extraCmd)) {
executionContext.setDoingBatchInsertBySpliter(true);
InsertSplitter insertSplitter = new InsertSplitter(executor);
// In batch insert, update transaction policy in writing
// broadcast table is also needed.
resultCursor = insertSplitter.execute(sql,executionContext,policy,
(String insertSql) -> optimizeThenExecute(insertSql, executionContext,trxPolicyModified));
} else {
resultCursor = optimizeThenExecute(sql, executionContext,trxPolicyModified);
}

最终会访问到:
protected void configMppExecutionContext(ExecutionContext executionContext) {

String instRole = (String) SystemPropertiesHelper.getPropertyValue(SystemPropertiesHelper.INST_ROLE);
SqlType sqlType = executionContext.getSqlType();

相当于执行每个SQL都要加锁访问HashMap(SystemPropertiesHelper.getPropertyValue),这里排队比较厉害

实际以上测试结果显示bucket对性能有提升这么大是不对的,刚好这个版本把对HashMap的访问去掉了,这才是提升的主要原因,当然如果线程池入队出队有等锁的话改成多个肯定是有帮助的,但是从等锁观察是没有这个问题的。

在这个代码基础上将bucket改成1,在4core机器下经过反复对比测试性能基本没有明显的差异,可能core越多这个问题会更明显些。总结

回到最开始部分查询卡顿这个问题,本质在于 MySQL线程池开启后,因为会将多个连接分配在一个池子中共享这个池子中的几个线程。导致一个池子中的线程特别慢的时候会影响这个池子中所有的查询都会卡顿。即使别的池子很空闲也不会将任务调度过去。

MySQL线程池设计成多个池子(Group)的原因是为了将任务队列拆成多个,这样每个池子中的线程只是内部竞争锁,跟其他池子不冲突,类似ConcurrentHashmap的实现,当然这个设计带来的问题就是多个池子中的任务不能均衡了。

同时从案例我们也可以清楚地看到这个池子太小会造成锁冲突严重的卡顿,池子太大(每个池子中的线程数量就少)容易造成等线程的卡顿。

类似地这个问题也会出现在Nginx的多worker中,一旦一个连接分发到了某个worker,就会一直在这个worker上处理,如果这个worker上的某个连接有一些慢操作,会导致这个worker上的其它连接的所有操作都受到影响,特别是会影响一些探活任务的误判。Nginx的worker这么设计也是为了将单worker绑定到固定的cpu,然后避免多核之间的上下文切换。

如果池子卡顿后,调用方有快速fail,比如druid的MySqlValidConnectionChecker,那么调用方从堆栈很快能发现这个问题,如果没有异常一直死等的话对问题的排查不是很友好。

另外可以看到分布式环境下死锁、活锁还是很容易产生的,想要一次性提前设计好比较难,需要不断踩坑爬坑。

参考文章

记一次诡异的数据库故障的排查过程

http://mysql.taobao.org/monthly/2016/02/09/

https://dbaplus.cn/news-11-1989-1.html

慢查询触发kill后导致集群卡死 把queryTimeout换成socketTimeout,这个不会发送kill,只会断开连接

青海湖、天津医保 RDS线程池过小导致DRDS查询卡顿问题排查

Linux内存--PageCache

发表于 2020-11-15 | 分类于 Memory

Linux内存–PageCache

本系列有如下几篇

[Linux 内存问题汇总](/2020/01/15/Linux 内存问题汇总/)

Linux内存–PageCache

Linux内存–管理和碎片

Linux内存–HugePage

Linux内存–零拷贝

read/write

read(2)/write(2) 是 Linux 系统中最基本的 I/O 读写系统调用,我们开发操作 I/O 的程序时必定会接触到它们,而在这两个系统调用和真实的磁盘读写之间存在一层称为 Kernel buffer cache 的缓冲区缓存。在 Linux 中 I/O 缓存其实可以细分为两个:Page Cache 和 Buffer Cache,这两个其实是一体两面,共同组成了 Linux 的内核缓冲区(Kernel Buffer Cache),Page Cache 是在应用程序读写文件的过程中产生的:

  • 读磁盘:内核会先检查 Page Cache 里是不是已经缓存了这个数据,若是,直接从这个内存缓冲区里读取返回,若否,则穿透到磁盘去读取,然后再缓存在 Page Cache 里,以备下次缓存命中;
  • 写磁盘:内核直接把数据写入 Page Cache,并把对应的页标记为 dirty,添加到 dirty list 里,然后就直接返回,内核会定期把 dirty list 的页缓存 flush 到磁盘,保证页缓存和磁盘的最终一致性。

在 Linux 还不支持虚拟内存技术之前,还没有页的概念,因此 Buffer Cache 是基于操作系统读写磁盘的最小单位 – 块(block)来进行的,所有的磁盘块操作都是通过 Buffer Cache 来加速,Linux 引入虚拟内存的机制来管理内存后,页成为虚拟内存管理的最小单位,因此也引入了 Page Cache 来缓存 Linux 文件内容,主要用来作为文件系统上的文件数据的缓存,提升读写性能,常见的是针对文件的 read()/write() 操作,另外也包括了通过 mmap() 映射之后的块设备,也就是说,事实上 Page Cache 负责了大部分的块设备文件的缓存工作。而 Buffer Cache 用来在系统对块设备进行读写的时候,对块进行数据缓存的系统来使用。

在 Linux 2.4 版本之后,kernel 就将两者进行了统一,Buffer Cache 不再以独立的形式存在,而是以融合的方式存在于 Page Cache 中

融合之后就可以统一操作 Page Cache 和 Buffer Cache:处理文件 I/O 缓存交给 Page Cache,而当底层 RAW device 刷新数据时以 Buffer Cache 的块单位来实际处理。

pagecache 的产生和释放

  • 标准 I/O 是写的 (write(2)) 用户缓冲区 (Userpace Page 对应的内存),**然后再将用户缓冲区里的数据拷贝到内核缓冲区 (Pagecache Page 对应的内存)**;如果是读的 (read(2)) 话则是先从内核缓冲区拷贝到用户缓冲区,再从用户缓冲区读数据,也就是 buffer 和文件内容不存在任何映射关系。
  • 对于存储映射 I/O(Memory-Mapped I/O) 而言,则是直接将 Pagecache Page 给映射到用户地址空间,用户直接读写 Pagecache Page 中内容,效率相对标准IO更高一些
image.png

当 将用户缓冲区里的数据拷贝到内核缓冲区 (Pagecache Page 对应的内存) 最容易发生缺页中断,OS需要先分配Page(应用感知到的就是卡顿了)

img.png
  • Page Cache 是在应用程序读写文件的过程中产生的,所以在读写文件之前你需要留意是否还有足够的内存来分配 Page Cache;
  • Page Cache 中的脏页很容易引起问题,你要重点注意这一块;
  • 在系统可用内存不足的时候就会回收 Page Cache 来释放出来内存,可以通过 sar 或者 /proc/vmstat 来观察这个行为从而更好的判断问题是否跟回收有关

缺页后kswapd在短时间内回收不了足够多的 free 内存,或kswapd 还没有触发执行,操作系统就会进行内存页直接回收。这个过程中,应用会进行自旋等待直到回收的完成,从而产生巨大的延迟。

如果page被swapped,那么恢复进内存的过程也对延迟有影响,当匿名内存页被回收后,如果下次再访问就会产生IO的延迟。

min 和 low的区别

  1. min下的内存是保留给内核使用的;当到达min,会触发内存的direct reclaim (vm.min_free_kbytes)
  2. low水位比min高一些,当内存可用量小于low的时候,会触发 kswapd回收内存,当kswapd慢慢的将内存 回收到high水位,就开始继续睡眠

内存回收方式

内存回收方式有两种,主要对应low ,min

  1. kswapd reclaim : 达到low水位线时执行 – 异步(实际还有,只是比较危险了,后台kswapd会回收,不会卡顿应用)
  2. direct reclaim : 达到min水位线时执行 – 同步

为了减少缺页中断,首先就要保证我们有足够的内存可以使用。由于Linux会尽可能多的使用free的内存,运行很久的应用free的内存是很少的。下面的图中,紫色表示已经使用的内存,白色表示尚未分配的内存。当我们的内存使用达到水位的low值的时候,kswapd就会开始回收工作,而一旦内存分配超过了min,就会进行内存的直接回收。

针对这种情况,需要采用预留内存的手段,系统参数vm.extra_free_kbytes就是用来做这个事情的。这个参数设置了系统预留给应用的内存,可以避免紧急需要内存时发生内存回收不及时导致的高延迟。从下面图中可以看到,通过vm.extra_free_kbytes的设置,预留内存可以让内存的申请处在一个安全的水位。需要注意的是,因为内核的优化,在3.10以上的内核版本这个参数已经被取消。

三个watermark的计算方法:

watermark[min] = vm.min_free_kbytes换算为page单位即可,假设为vm.min_free_kbytes。

watermark[low] = watermark[min] * 5 / 4

watermark[high] = watermark[min] * 3 / 2

比如默认 vm.min_free_kbytes = 65536是64K,很容易导致应用的毛刺,可以适当改大

或者禁止: vm.swappiness 来避免swapped来减少延迟

direct IO

绕过page cache,直接读写硬盘

cache回收

系统内存大体可分为三块,应用程序使用内存、系统Cache 使用内存(包括page cache、buffer,内核slab 等)和Free 内存。

  • 应用程序使用内存:应用使用都是虚拟内存,应用申请内存时只是分配了地址空间,并未真正分配出物理内存,等到应用真正访问内存时会触发内核的缺页中断,这时候才真正的分配出物理内存,映射到用户的地址空间,因此应用使用内存是不需要连续的,内核有机制将非连续的物理映射到连续的进程地址空间中(mmu),缺页中断申请的物理内存,内核优先给低阶碎内存。

  • 系统Cache 使用内存:使用的也是虚拟内存,申请机制与应用程序相同。

  • Free 内存,未被使用的物理内存,这部分内存以4k 页的形式被管理在内核伙伴算法结构中,相邻的2^n 个物理页会被伙伴算法组织到一起,形成一块连续物理内存,所谓的阶内存就是这里的n (0<= n <=10),高阶内存指的就是一块连续的物理内存,在OSS 的场景中,如果3阶内存个数比较小的情况下,如果系统有吞吐burst 就会触发Drop cache 情况。

echo 1/2/3 >/proc/sys/vm/drop_caches

查看回收后:

cat /proc/meminfo
image.png

当我们执行 echo 2 来 drop slab 的时候,它也会把 Page Cache(inode可能会有对应的pagecache,inode释放后对应的pagecache也释放了)给 drop 掉

在系统内存紧张的时候,运维人员或者开发人员会想要通过 drop_caches 的方式来释放一些内存,但是由于他们清楚 Page Cache 被释放掉会影响业务性能,所以就期望只去 drop slab 而不去 drop pagecache。于是很多人这个时候就运行 echo 2 > /proc/sys/vm/drop_caches,但是结果却出乎了他们的意料:Page Cache 也被释放掉了,业务性能产生了明显的下降。

查看 drop_caches 是否执行过释放:

1
2
3
4
5
6
7
$ grep drop /proc/vmstat
drop_pagecache 1
drop_slab 0

$ grep inodesteal /proc/vmstat
pginodesteal 114341
kswapd_inodesteal 1291853

在内存紧张的时候会触发内存回收,内存回收会尝试去回收 reclaimable(可以被回收的)内存,这部分内存既包含 Page Cache 又包含 reclaimable kernel memory(比如 slab)。inode被回收后可以通过 grep inodesteal /proc/vmstat 观察到

kswapd_inodesteal 是指在 kswapd 回收的过程中,因为回收 inode 而释放的 pagecache page 个数;

pginodesteal 是指 kswapd 之外其他线程在回收过程中,因为回收 inode 而释放的 pagecache page 个数;

Page回收–缺页中断

image.png

从图里你可以看到,在开始内存回收后,首先进行后台异步回收(上图中蓝色标记的地方),这不会引起进程的延迟;如果后台异步回收跟不上进程内存申请的速度,就会开始同步阻塞回收,导致延迟(上图中红色和粉色标记的地方,这就是引起 load 高的地址 – Sys CPU 使用率飙升/Sys load 飙升)。

那么,针对直接内存回收引起 load 飙高或者业务 RT 抖动的问题,一个解决方案就是及早地触发后台回收来避免应用程序进行直接内存回收,那具体要怎么做呢?

image.png

它的意思是:当内存水位低于 watermark low 时,就会唤醒 kswapd 进行后台回收,然后 kswapd 会一直回收到 watermark high。

那么,我们可以增大 min_free_kbytes 这个配置选项来及早地触发后台回收,该选项最终控制的是内存回收水位,不过,内存回收水位是内核里面非常细节性的知识点,我们可以先不去讨论。

对于大于等于 128G 的系统而言,将 min_free_kbytes 设置为 4G 比较合理,这是我们在处理很多这种问题时总结出来的一个经验值,既不造成较多的内存浪费,又能避免掉绝大多数的直接内存回收。

该值的设置和总的物理内存并没有一个严格对应的关系,我们在前面也说过,如果配置不当会引起一些副作用,所以在调整该值之前,我的建议是:你可以渐进式地增大该值,比如先调整为 1G,观察 sar -B 中 pgscand 是否还有不为 0 的情况;如果存在不为 0 的情况,继续增加到 2G,再次观察是否还有不为 0 的情况来决定是否增大,以此类推。

sar -B : Report paging statistics.

pgscand/s Number of pages scanned directly per second.

系统中脏页过多引起 load 飙高

直接回收过程中,如果存在较多脏页就可能涉及在回收过程中进行回写,这可能会造成非常大的延迟,而且因为这个过程本身是阻塞式的,所以又可能进一步导致系统中处于 D 状态的进程数增多,最终的表现就是系统的 load 值很高。

image.png

可以通过 sar -r 来观察系统中的脏页个数:

1
2
3
4
5
6
$ sar -r 1
07:30:01 PM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
09:20:01 PM 5681588 2137312 27.34 0 1807432 193016 2.47 534416 1310876 4
09:30:01 PM 5677564 2141336 27.39 0 1807500 204084 2.61 539192 1310884 20
09:40:01 PM 5679516 2139384 27.36 0 1807508 196696 2.52 536528 1310888 20
09:50:01 PM 5679548 2139352 27.36 0 1807516 196624 2.51 536152 1310892 24

kbdirty 就是系统中的脏页大小,它同样也是对 /proc/vmstat 中 nr_dirty 的解析。你可以通过调小如下设置来将系统脏页个数控制在一个合理范围:

vm.dirty_background_bytes = 0

vm.dirty_background_ratio = 10

vm.dirty_bytes = 0

vm.dirty_expire_centisecs = 3000

vm.dirty_ratio = 20

至于这些值调整大多少比较合适,也是因系统和业务的不同而异,我的建议也是一边调整一边观察,将这些值调整到业务可以容忍的程度就可以了,即在调整后需要观察业务的服务质量 (SLA),要确保 SLA 在可接受范围内。调整的效果可以通过 /proc/vmstat 来查看:

1
2
3
#grep "nr_dirty_" /proc/vmstat
nr_dirty_threshold 3071708
nr_dirty_background_threshold 1023902

在4.20的内核并且sar 的版本为12.3.3可以看到PSI(Pressure-Stall Information)

1
2
some avg10=45.49 avg60=10.23 avg300=5.41 total=76464318
full avg10=40.87 avg60=9.05 avg300=4.29 total=58141082

重点关注 avg10 这一列,它表示最近 10s 内存的平均压力情况,如果它很大(比如大于 40)那 load 飙高大概率是由于内存压力,尤其是 Page Cache 的压力引起的。

image.png

通过tracepoint分析内存卡顿问题

image.png

我们继续以内存规整 (memory compaction) 为例,来看下如何利用 tracepoint 来对它进行观察:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#首先来使能compcation相关的一些tracepoing
$ echo 1 >
/sys/kernel/debug/tracing/events/compaction/mm_compaction_begin/enable
$ echo 1 >
/sys/kernel/debug/tracing/events/compaction/mm_compaction_end/enable

#然后来读取信息,当compaction事件触发后就会有信息输出
$ cat /sys/kernel/debug/tracing/trace_pipe
<...>-49355 [037] .... 1578020.975159: mm_compaction_begin:
zone_start=0x2080000 migrate_pfn=0x2080000 free_pfn=0x3fe5800
zone_end=0x4080000, mode=async
<...>-49355 [037] .N.. 1578020.992136: mm_compaction_end:
zone_start=0x2080000 migrate_pfn=0x208f420 free_pfn=0x3f4b720
zone_end=0x4080000, mode=async status=contended

从这个例子中的信息里,我们可以看到是 49355 这个进程触发了 compaction,begin 和 end 这两个 tracepoint 触发的时间戳相减,就可以得到 compaction 给业务带来的延迟,我们可以计算出这一次的延迟为 17ms。

或者用 perf script 脚本来分析, 基于 bcc(eBPF) 写的direct reclaim snoop来观察进程因为 direct reclaim 而导致的延迟。

参考资料

https://www.atatech.org/articles/66885

https://cloud.tencent.com/developer/article/1087455

https://www.cnblogs.com/xiaolincoding/p/13719610.html

Linux内存--零拷贝

发表于 2020-11-15 | 分类于 Memory

Linux内存–零拷贝

本系列有如下几篇

[Linux 内存问题汇总](/2020/01/15/Linux 内存问题汇总/)

Linux内存–PageCache

Linux内存–管理和碎片

Linux内存–HugePage

Linux内存–零拷贝

零拷贝

“Zero-copy“ describes computer operations in which the CPU does not perform the task of copying data from one memory area to another. This is frequently used to save CPU cycles and memory bandwidth when transmitting a file over a network.

零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU 和内存带宽。

零拷贝可以做到用户空间和内核空间共用同一块内存(Java中的DirectBuffer),这样少做一次拷贝。普通Buffer是在JVM堆上分配的内存,而DirectBuffer是堆外分配的(内核和JVM可以同时读写),这样不需要再多一次内核到用户Buffer的拷贝

比如通过网络下载文件,普通拷贝的流程会复制4次并有4次上下文切换,上下文切换是因为读写慢导致了IO的阻塞,进而线程被内核挂起,所以发生了上下文切换。在极端情况下如果read/write没有导致阻塞是不会发生上下文切换的:

NoOptimization

改成零拷贝后,也就是将read和write合并成一次,直接在内核中完成磁盘到网卡的数据复制

image.png

零拷贝就是操作系统提供的新函数(sendfile),同时接收文件描述符和 TCP socket 作为输入参数,这样执行时就可以完全在内核态完成内存拷贝,既减少了内存拷贝次数,也降低了上下文切换次数。

而且,零拷贝取消了用户缓冲区后,不只降低了用户内存的消耗,还通过最大化利用 socket 缓冲区中的内存,间接地再一次减少了系统调用的次数,从而带来了大幅减少上下文切换次数的机会!

应用读取磁盘写入网络的时候还得考虑缓存的大小,一般会设置的比较小,这样一个大文件导致多次小批量的读取,每次读取伴随着多次上下文切换。

零拷贝使我们不必关心 socket 缓冲区的大小(socket缓冲区大小本身默认就是动态调整、或者应用代码指定大小)。比如,调用零拷贝发送方法时,尽可以把发送字节数设为文件的所有未发送字节数,例如 320MB,也许此时 socket 缓冲区大小为 1.4MB,那么一次性就会发送 1.4MB 到客户端,而不是只有 32KB。这意味着对于 1.4MB 的 1 次零拷贝,仅带来 2 次上下文切换,而不使用零拷贝且用户缓冲区为 32KB 时,经历了 176 次(4 * 1.4MB/32KB)上下文切换。

read+write 和零拷贝优化

1
2
read(file, tmp_buf, len);
write(socket, tmp_buf, len);

image-20201104175056589

通过mmap替换read优化一下

用 mmap() 替换原先的 read(),mmap() 也即是内存映射(memory map):把用户进程空间的一段内存缓冲区(user buffer)映射到文件所在的内核缓冲区(kernel buffer)上。

image.png

通过使用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程。

但这还不是最理想的零拷贝,因为首先仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次;另外内存映射技术是一个开销很大的虚拟存储操作:这种操作需要修改页表以及用内核缓冲区里的文件数据汰换掉当前 TLB 里的缓存以维持虚拟内存映射的一致性。

sendfile

在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),函数形式如下:

1
2
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。

首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。

其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。如下图:

image.png

当然这里还是有一次CPU来拷贝内存的过程,仍然有文件截断的问题。sendfile() 依然是一个适用性很窄的技术,最适合的场景基本也就是一个静态文件服务器了。

然而 sendfile() 本身是有很大问题的,从不同的角度来看的话主要是:

  1. 首先一个是这个接口并没有进行标准化,导致 sendfile() 在 Linux 上的接口实现和其他类 Unix 系统的实现并不相同;
  2. 其次由于网络传输的异步性,很难在接收端实现和 sendfile() 对接的技术,因此接收端一直没有实现对应的这种技术;
  3. 最后从性能方面考量,因为 sendfile() 在把磁盘文件从内核缓冲区(page cache)传输到到套接字缓冲区的过程中依然需要 CPU 参与,这就很难避免 CPU 的高速缓存被传输的数据所污染。

SG-DMA(The Scatter-Gather Direct Memory Access)技术

上一小节介绍的 sendfile() 技术已经把一次数据读写过程中的 CPU 拷贝的降低至只有 1 次了,但是人永远是贪心和不知足的,现在如果想要把这仅有的一次 CPU 拷贝也去除掉,有没有办法呢?

当然有!通过引入一个新硬件上的支持,我们可以把这个仅剩的一次 CPU 拷贝也给抹掉:Linux 在内核 2.4 版本里引入了 DMA 的 scatter/gather – 分散/收集功能,并修改了 sendfile() 的代码使之和 DMA 适配。scatter 使得 DMA 拷贝可以不再需要把数据存储在一片连续的内存空间上,而是允许离散存储,gather 则能够让 DMA 控制器根据少量的元信息:一个包含了内存地址和数据大小的缓冲区描述符,收集存储在各处的数据,最终还原成一个完整的网络包,直接拷贝到网卡而非套接字缓冲区,避免了最后一次的 CPU 拷贝:

image.png

如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。

这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。 数据传输过程就再也没有 CPU 的参与了,也因此 CPU 的高速缓存再不会被污染了,也不再需要 CPU 来计算数据校验和了,CPU 可以去执行其他的业务计算任务,同时和 DMA 的 I/O 任务并行,此举能极大地提升系统性能。

零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。

所以,总体来看,零拷贝技术可以把文件传输的性能提高至少一倍以上。

splice()

sendfile() + DMA Scatter/Gather 的零拷贝方案虽然高效,但是也有两个缺点:

  1. 这种方案需要引入新的硬件支持;
  2. 虽然 sendfile() 的输出文件描述符在 Linux kernel 2.6.33 版本之后已经可以支持任意类型的文件描述符,但是输入文件描述符依然只能指向文件。

这两个缺点限制了 sendfile() + DMA Scatter/Gather 方案的适用场景。为此,Linux 在 2.6.17 版本引入了一个新的系统调用 splice(),它在功能上和 sendfile() 非常相似,但是能够实现在任意类型的两个文件描述符时之间传输数据;而在底层实现上,splice()又比 sendfile() 少了一次 CPU 拷贝,也就是等同于 sendfile() + DMA Scatter/Gather,完全去除了数据传输过程中的 CPU 拷贝。

splice() 所谓的写入数据到管道其实并没有真正地拷贝数据,而是玩了个 tricky 的操作:只进行内存地址指针的拷贝而不真正去拷贝数据。所以,数据 splice() 在内核中并没有进行真正的数据拷贝,因此 splice() 系统调用也是零拷贝。

还有一点需要注意,前面说过管道的容量是 16 个内存页,也就是 16 * 4KB = 64 KB,也就是说一次往管道里写数据的时候最好不要超过 64 KB,否则的话会 splice() 会阻塞住,除非在创建管道的时候使用的是 pipe2() 并通过传入 O_NONBLOCK 属性将管道设置为非阻塞。

send() with MSG_ZEROCOPY

Linux 内核在 2017 年的 v4.14 版本接受了来自 Google 工程师 Willem de Bruijn 在 TCP 网络报文的通用发送接口 send() 中实现的 zero-copy 功能 (MSG_ZEROCOPY) 的 patch,通过这个新功能,用户进程就能够把用户缓冲区的数据通过零拷贝的方式经过内核空间发送到网络套接字中去,这个新技术和前文介绍的几种零拷贝方式相比更加先进,因为前面几种零拷贝技术都是要求用户进程不能处理加工数据而是直接转发到目标文件描述符中去的。Willem de Bruijn 在他的论文里给出的压测数据是:采用 netperf 大包发送测试,性能提升 39%,而线上环境的数据发送性能则提升了 5%~8%,官方文档陈述说这个特性通常只在发送 10KB 左右大包的场景下才会有显著的性能提升。一开始这个特性只支持 TCP,到内核 v5.0 版本之后才支持 UDP。

这个技术是基于 redhat 红帽在 2010 年给 Linux 内核提交的 virtio-net zero-copy 技术之上实现的,至于底层原理,简单来说就是通过 send() 把数据在用户缓冲区中的分段指针发送到 socket 中去,利用 page pinning 页锁定机制锁住用户缓冲区的内存页,然后利用 DMA 直接在用户缓冲区通过内存地址指针进行数据读取,实现零拷贝

目前来说,这种技术的主要缺陷有:

  1. 只适用于大文件 (10KB 左右) 的场景,小文件场景因为 page pinning 页锁定和等待缓冲区释放的通知消息这些机制,甚至可能比直接 CPU 拷贝更耗时;
  2. 因为可能异步发送数据,需要额外调用 poll() 和 recvmsg() 系统调用等待 buffer 被释放的通知消息,增加代码复杂度,以及会导致多次用户态和内核态的上下文切换;
  3. MSG_ZEROCOPY 目前只支持发送端,接收端暂不支持。

零拷贝应用

kafka就利用了「零拷贝」技术,从而大幅提升了 I/O 的吞吐率,这也是 Kafka 在处理海量数据为什么这么快的原因之一(利用磁盘顺序写;PageCache)。

非零拷贝代码:

1
2
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

Traditional data copying approach:

Traditional data copying approach

Traditional context switches:

Traditional context switches

如果你追溯 Kafka 文件传输的代码,你会发现,最终它调用了 Java NIO 库里的 transferTo 方法:

1
2
3
4
@Overridepublic 
long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel.transferTo(position, count, socketChannel);
}

Data copy with transferTo()

Data copy with transferTo()

Context switching with transferTo():

Context switching when using transferTo()

Data copies when transferTo() and gather operations are used

Data copies when transferTo() and gather operations are used

如果 Linux 系统支持 sendfile() 系统调用,那么 transferTo() 实际上最后就会使用到 sendfile() 系统调用函数。

Nginx 也支持零拷贝技术,一般默认是开启零拷贝技术,这样有利于提高文件传输的效率,是否开启零拷贝技术的配置如下:

1
2
3
4
5
http {
...
sendfile on
...
}

sendfile 配置的具体意思:

  • 设置为 on 表示,使用零拷贝技术来传输文件:sendfile ,这样只需要 2 次上下文切换,和 2 次数据拷贝。
  • 设置为 off 表示,使用传统的文件传输技术:read + write,这时就需要 4 次上下文切换,和 4 次数据拷贝。

如果是大文件很容易消耗非常多的PageCache,不推荐使用PageCache(或者说零拷贝),建议使用异步IO+直接IO。

在 nginx 中,我们可以用如下配置,来根据文件的大小来使用不同的方式:

1
2
3
4
5
location /video/ { 
sendfile on;
aio on;
directio 1024m;
}

当文件大小大于 directio 值后,使用「异步 I/O + 直接 I/O」,否则使用「零拷贝技术」。

零拷贝性能

如果用零拷贝和不用零拷贝来做一个文件服务器,来对比下他们的性能

Performance comparison: Traditional approach vs. zero copy

File size Normal file transfer (ms) transferTo (ms)
7MB 156 45
21MB 337 128
63MB 843 387
98MB 1320 617
200MB 2124 1150
350MB 3631 1762
700MB 13498 4422
1GB 18399 8537

DMA

什么是 DMA 技术?简单理解就是,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务

RDMA

remote DMA

参考资料

https://www.atatech.org/articles/66885

https://cloud.tencent.com/developer/article/1087455

https://www.cnblogs.com/xiaolincoding/p/13719610.html

https://mp.weixin.qq.com/s/dZNjq05q9jMFYhJrjae_LA 从Linux内存管理到零拷贝

Efficient data transfer through zero copy

CPU:一个故事看懂DMA

Linux内存--管理和碎片

发表于 2020-11-15 | 分类于 Memory

Linux内存–管理和碎片

本系列有如下几篇

[Linux 内存问题汇总](/2020/01/15/Linux 内存问题汇总/)

Linux内存–PageCache

Linux内存–管理和碎片

Linux内存–HugePage

Linux内存–零拷贝

物理结构分析

内存从物理结构上面分为:Channel > DIMM(对应物理上售卖的内存条) >Rank > Chip > Bank > Row/Column。

Chip就是DRAM芯片,一个chip里面会有很多bank。每个bank就是数据存储的实体,相当于一个二维矩阵,只要声明了column和row就可以从每个bank中取出8bit的数据。

具体可以看如下图,一个通道Channel可以是一个DIMM也可以是两个DIMM,甚至3个DIMM,图中是2个DIMM。

image-20211222135852796

虚拟内存和物理内存

进程所操作的内存是一个虚拟内存,由OS来将这块虚拟内存映射到实际的物理内存上,这样做的好处是每个进程可以独占 128T 内存,任意地使用,系统上还运行了哪些进程已经与我们完全没有关系了(不需要考虑和其它进程之间的地址会冲突)。为变量和函数分配地址的活,我们交给链接器去自动安排就可以了。这一切都是因为虚拟内存能够提供内存地址空间的隔离,极大地扩展了可用空间。

操作系统管理着这种映射关系,所以你在写代码的时候,就不用再操心物理内存的使用情况了,你看到的内存就是虚拟内存。无论一个进程占用的内存资源有多大,在任一时刻,它需要的物理内存都是很少的。在这个推论的基础上,CPU 为每个进程只需要保留很少的物理内存就可以保证进程的正常执行了。

当程序中使用 malloc 等分配内存的接口时会将内存从待分配状态变成已分配状态,此时这块分配好的内存还没有真正映射到对应的物理内存上,这块内存就是未映射状态,因为它并没有被映射到相应的物理内存,直到对该块内存进行读写时,操作系统才会真正地为它分配物理内存。然后这个页面才能成为正常页面。

i7 处理器的页表也是存储在内存页里的,每个页表项都是 4 字节。所以,人们就将 1024 个页表项组成一张页表。这样一张页表的大小就刚好是 4K,占据一个内存页,这样就更加方便管理。而且,当前市场上主流的处理器也都选择将页大小定为 4K。

虚拟地址在计算机体系结构里可以评为特优的一项技术;超线程、流水线、多发射只是优;cache则只是良好(成本高)

CPU 如何找到真实地址

image-20211107175201297

  • 第一步是确定页目录基址。每个 CPU 都有一个页目录基址寄存器,最高级页表的基地址就存在这个寄存器里。在 X86 上,这个寄存器是 CR3。每一次计算物理地址时,MMU 都会从 CR3 寄存器中取出页目录所在的物理地址。
  • 第二步是定位页目录项(PDE)。一个 32 位的虚拟地址可以拆成 10 位,10 位和 12 位三段,上一步找到的页目录表基址加上高 10 位的值乘以 4,就是页目录项的位置。这是因为,一个页目录项正好是 4 字节,所以 1024 个页目录项共占据 4096 字节,刚好组成一页,而 1024 个页目录项需要 10 位进行编码。这样,我们就可以通过最高 10 位找到该地址所对应的 PDE 了。
  • 第三步是定位页表项(PTE)。页目录项里记录着页表的位置,CPU 通过页目录项找到页表的位置以后,再用中间 10 位计算页表中的偏移,可以找到该虚拟地址所对应的页表项了。页表项也是 4 字节的,所以一页之内刚好也是 1024 项,用 10 位进行编码。所以计算公式与上一步相似,用页表基址加上中间 10 位乘以 4,可以得到页表项的地址。
  • 最后一步是确定真实的物理地址。上一步 CPU 已经找到页表项了,这里存储着物理地址,这才真正找到该虚拟地址所对应的物理页。虚拟地址的低 12 位,刚好可以对一页内的所有字节进行编码,所以我们用低 12 位来代表页内偏移。计算的公式是物理页的地址直接加上低 12 位。

前面我们分析的是 32 位操作系统,那对于 64 位机器是不是有点不同呢?在 64 位的机器上,使用了 48 位的虚拟地址,所以它需要使用 4 级页表。它的结构与 32 位的 3 级页表是相似的,只是多了一级页目录,定位的过程也从 32 位的 4 步变成了 5 步。

image-20211107182305732

8086最开始是按不同的作用将内存分为代码段、数据段等,386开始按页开始管理内存(混合有按段管理)。 现代的操作系统都是采用段式管理来做基本的权限管理,而对于内存的分配、回收、调度都是依赖页式管理。

tlab miss

tlb:从各级cache里分配的一块专用空间,用来存放页表(虚拟地址和物理地址的对应关系)–存放在CPU cache里的索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#x86info -c
Monitor/Mwait: min/max line size 64/64, ecx bit 0 support, enumeration extension
SVM: revision 1, 32768 ASIDs
Address Size: 48 bits virtual, 48 bits physical
The physical package has 96 of 32768 possible cores implemented.
L1 Data TLB (1G): Fully associative. 64 entries.
L1 Instruction TLB (1G): Fully associative. 64 entries.
L1 Data TLB (2M/4M): Fully associative. 64 entries.
L1 Instruction TLB (2M/4M): Fully associative. 64 entries.
L1 Data TLB (4K): Fully associative. 64 entries.
L1 Instruction TLB (4K): Fully associative. 64 entries.
L1 Data cache:
Size: 32Kb 8-way associative.
lines per tag=1 line size=64 bytes.
L1 Instruction cache:
Size: 32Kb 8-way associative.
lines per tag=1 line size=64 bytes.
L2 Data TLB (1G): Fully associative. 64 entries.
L2 Instruction TLB (1G): Disabled. 0 entries.
L2 Data TLB (2M/4M): 4-way associative. 2048 entries.
L2 Instruction TLB (2M/4M): 2-way associative. 512 entries.
L2 Data TLB (4K): 8-way associative. 2048 entries.
L2 Instruction TLB (4K): 4-way associative. 512 entries.
L2 cache:
Size: 512Kb 8-way associative.
lines per tag=1 line size=64 bytes.

running at an estimated 2.55GHz

image-20220928160318893

TLB(Translation Lookaside Buffer) Cache用于缓存少量热点内存地址的mapping关系。TLB和L1一样每个core独享,由于制造成本和工艺的限制,响应时间需要控制在CPU Cycle级别的Cache容量只能存储几十个对象。那么TLB Cache在应对大量热点数据Virual Address转换的时候就显得捉襟见肘了。我们来算下按照标准的Linux页大小(page size) 4K,一个能缓存64元素的TLB Cache只能涵盖4K*64 = 256K的热点数据的内存地址,显然离理想非常遥远的。于是Huge Page就产生了。

These are typical performance levels of a TLB:

  • Size: 12 bits – 4,096 entries
  • Hit time: 0.5 – 1 clock cycle
  • Miss penalty: 10 – 100 clock cycles
  • Miss rate: 0.01 – 1% (20–40% for sparse/graph applications)

TLB也分为iTLB和dTLB, 分别顶在L1i和L1d前面(比L1更小更快,每个core独享tlb)

img

以intel x86为例,一个cpu也就32到64个tlb, 超出这个范畴,就得去查页表。 每个型号的cpu都不一样,需要查看spec

进程分配到的不是内存的实际物理地址,而是一个经过映射后的虚拟地址,这么做的原因是为了让每个应用可以独享完整的虚拟地址,而不需要每个进程互相考虑使用内存的协调。

但是虚拟地址到物理地址的映射需要巨大的映射空间,如何用更少的内存消耗来管理庞大的内存(如果没有分级,4G内存对应着4MB的索引空间,一级比如使用4K就够了,多个二级使总共用4M,但是这4M大部分时候不用提前分配),Linux通过四级表项来做虚拟地址到物理地址的映射,这样4Kb就能管理256T内存,4级映射是时间换空间的典型案例。不过一般而言一个进程是远远用不了256T内存的,那么这四级映射大部分时候都是没必要的,所以实际用不了那么大的PageTable。

虚拟内存的核心原理是:为每个程序设置一段”连续”的虚拟地址空间,把这个地址空间分割成多个具有连续地址范围的页 (page),并把这些页和物理内存做映射,在程序运行期间动态映射到物理内存。当程序引用到一段在物理内存的地址空间时,由硬件立刻执行必要的映射;而当程序引用到一段不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令:

img

在 内存管理单元(Memory Management Unit,MMU)进行地址转换时,如果页表项的 “在/不在” 位是 0,则表示该页面并没有映射到真实的物理页框,则会引发一个缺页中断,CPU 陷入操作系统内核,接着操作系统就会通过页面置换算法选择一个页面将其换出 (swap),以便为即将调入的新页面腾出位置,如果要换出的页面的页表项里的修改位已经被设置过,也就是被更新过,则这是一个脏页 (dirty page),需要写回磁盘更新改页面在磁盘上的副本,如果该页面是”干净”的,也就是没有被修改过,则直接用调入的新页面覆盖掉被换出的旧页面即可。

还需要了解的一个概念是转换检测缓冲器(Translation Lookaside Buffer,TLB,每个core一个TLB,类似L1 cache),也叫快表,是用来加速虚拟地址映射的,因为虚拟内存的分页机制,页表一般是保存内存中的一块固定的存储区,导致进程通过 MMU 访问内存比直接访问内存多了一次内存访问,性能至少下降一半,因此需要引入加速机制,即 TLB 快表,TLB 可以简单地理解成页表的高速缓存,保存了最高频被访问的页表项,由于一般是硬件实现的,因此速度极快,MMU 收到虚拟地址时一般会先通过硬件 TLB 查询对应的页表号,若命中且该页表项的访问操作合法,则直接从 TLB 取出对应的物理页框号返回,若不命中则穿透到内存页表里查询,并且会用这个从内存页表里查询到最新页表项替换到现有 TLB 里的其中一个,以备下次缓存命中。

如果没有TLB那么每一次内存映射都需要查表四次然后才是一次真正的内存访问,代价比较高。

有了TLB之后,CPU访问某个虚拟内存地址的过程如下

  1. CPU产生一个虚拟地址
  2. MMU从TLB中获取页表,翻译成物理地址
  3. MMU把物理地址发送给L1/L2/L3/内存
  4. L1/L2/L3/内存将地址对应数据返回给CPU

由于第2步是类似于寄存器的访问速度,所以如果TLB能命中,则虚拟地址到物理地址的时间开销几乎可以忽略。tlab miss比较高的话开启内存大页对性能是有提升的,但是会有一定的内存浪费。

内存布局

  • 代码段:CPU 运行一个程序,实质就是在顺序执行该程序的机器码。一个程序的机器码会被组织到同一个地方。
  • 数据段:程序在运行过程中必然要操作数据。这其中,对于有初值的变量,它的初始值会存放在程序的二进制文件中,而且,这些数据部分也会被装载到内存中,即程序的数据段。数据段存放的是程序中已经初始化且不为 0 的全局变量和静态变量。
  • BSS 段: 对于未初始化的全局变量和静态变量,因为编译器知道它们的初始值都是 0,因此便不需要再在程序的二进制映像中存放这么多 0 了,只需要记录他们的大小即可,这便是BSS段 。BSS 段这个缩写名字是 Block Started by Symbol,但很多人可能更喜欢把它记作 Better Save Space 的缩写。
  • 堆是程序员可以自由申请的空间,当我们在写程序时要保存数据,优先会选择堆;
  • 栈是函数执行时的活跃记录,这将是我们下一节课要重点分析的内容。

数据段和 BSS 段里存放的数据也只能是部分数据,主要是全局变量和静态变量,但程序在运行过程中,仍然需要记录大量的临时变量,以及运行时生成的变量,这里就需要新的内存区域了,即程序的堆空间跟栈空间。与代码段以及数据段不同的是,堆和栈并不是从磁盘中加载,它们都是由程序在运行的过程中申请,在程序运行结束后释放。

总的来说,一个程序想要运行起来所需要的几块基本内存区域:代码段、数据段、BSS 段、堆空间和栈空间。下面就是内存布局的示意图:

image-20211108115717113

其它内存形态:

  • 存放加载的共享库的内存空间:如果一个进程依赖共享库,那对应的,该共享库的代码段、数据段、BSS 段也需要被加载到这个进程的地址空间中。
  • 共享内存段:我们可以通过系统调用映射一块匿名区域作为共享内存,用来进行进程间通信。
  • 内存映射文件:我们也可以将磁盘的文件映射到内存中,用来进行文件编辑或者是类似共享内存的方式进行进程通信。

32位 x86机器下,通过 cat /proc/pid/maps 看到的进程所使用的内存分配空间:

image-20211108120532370

64位 x86机器下,通过 cat /proc/pid/maps 看到的进程所使用的内存分配空间:

image-20211108120718732

目前的 64 系统下的寻址空间是 2^48(太多用不完,高16位为Canonical空间),即 256TB。而且根据 canonical address 的划分,地址空间天然地被分割成两个区间,分别是 0x0 - 0x00007fffffffffff 和 0xffff800000000000 - 0xffffffffffffffff。这样就直接将低 128T 的空间划分为用户空间,高 128T 划分为内核空间。

brk:内核维护指向堆的顶部

Java程序对应的maps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#cat /proc/14011/maps
00400000-00401000 r-xp 00000000 08:03 3935494 /opt/taobao/install/ajdk-8.3.6_fp9-b30/bin/java
00600000-00601000 rw-p 00000000 08:03 3935494 /opt/taobao/install/ajdk-8.3.6_fp9-b30/bin/java
ed400000-1001e0000 rw-p 00000000 00:00 0
1001e0000-140000000 ---p 00000000 00:00 0
7f86e8000000-7f8a7fc00000 rw-p 00000000 00:00 0
..
7f8aaecfa000-7f8aaeff8000 rw-p 00000000 00:00 0
7f8aaeff8000-7f8aaf000000 r-xp 00000000 08:03 3935973 /opt/taobao/install/ajdk-8.3.6_fp9-b30/jre/lib/amd64/libmanagement.so
7f8aaf000000-7f8aaf1ff000 ---p 00008000 08:03 3935973 /opt/taobao/install/ajdk-8.3.6_fp9-b30/jre/lib/amd64/libmanagement.so
7f8aaf1ff000-7f8aaf200000 rw-p 00007000 08:03 3935973 /opt/taobao/install/ajdk-8.3.6_fp9-b30/jre/lib/amd64/libmanagement.so
..
7f8ad8cea000-7f8ad8cec000 r--s 00004000 08:05 7078938 /home/admin/drds-worker/lib/netty-handler-proxy-4.1.17.Final.jar
7f8ad8cec000-7f8ad8cf5000 r--s 0006f000 08:05 7078952 /home/admin/drds-worker/lib/log4j-1.2.17.jar
7f8ad8cf5000-7f8ad8cf7000 r--s 00005000 08:05 7078960 /home/admin/drds-worker/lib/objenesis-1.0.jar
7f8ad8cf7000-7f8ad8cff000 r--s 0004b000 08:05 7078929 /home/admin/drds-worker/lib/spring-aop-3.2.18.RELEASE.jar
7f8ad8cff000-7f8ad8d00000 ---p 00000000 00:00 0
7f8ad8d00000-7f8ad9000000 rw-p 00000000 00:00 0
7f8ad90e3000-7f8ad90ef000 r--s 000b6000 08:05 7079066 /home/admin/drds-worker/lib/transmittable-thread-local-2.5.1.jar
7f8ad9dd8000-7f8ad9dfe000 r--s 0026f000 08:05 7078997 /home/admin/drds-worker/lib/druid-1.1.7-preview_12.jar
7f8ad9dfe000-7f8ad9dff000 ---p 00000000 00:00 0
7f8ad9dff000-7f8ad9eff000 rw-p 00000000 00:00 0
7f8ad9eff000-7f8ad9f00000 ---p 00000000 00:00 0
7f8ad9f00000-7f8ada200000 rw-p 00000000 00:00 0
7f8ada200000-7f8ada202000 r--s 00003000 08:05 7078944 /home/admin/drds-worker/lib/liberate-rest-1.0.2.jar
7f8ada202000-7f8ada206000 r--s 00036000 08:05 7078912 /home/admin/drds-worker/lib/jackson-core-lgpl-1.9.6.jar
7f8ada289000-7f8ada28b000 r--s 00001000 08:05 7078998 /home/admin/drds-worker/lib/opencensus-contrib-grpc-metrics-0.10.0.jar
7f8ada2b5000-7f8ada2b9000 r--s 0003a000 08:05 7079099 /home/admin/drds-worker/lib/tddl-repo-mysql-5.2.7-2-EXTEND-HOTMAPPING-SNAPSHOT.jar
7f8ada2b9000-7f8ada2c6000 r--s 0007d000 08:05 7078982 /home/admin/drds-worker/lib/grpc-core-1.9.0.jar
7f8ada2c6000-7f8ada2d6000 r--s 00149000 08:05 7079000 /home/admin/drds-worker/lib/protobuf-java-3.5.1.jar
7f8ada2d6000-7f8ada2db000 r--s 0002b000 08:05 7078927 /home/admin/drds-worker/lib/tddl-net-5.2.7-2-EXTEND-HOTMAPPING-SNAPSHOT.jar
7f8ada2db000-7f8ada2e0000 r--s 0002a000 08:05 7078939 /home/admin/drds-worker/lib/grpc-netty-1.9.0.jar
7f8ada2e0000-7f8ada2ff000 r--s 00150000 08:05 7078965 /home/admin/drds-worker/lib/mockito-core-1.9.5.jar
7f8ada2ff000-7f8ada300000 ---p 00000000 00:00 0
7f8ada300000-7f8ada600000 rw-p 00000000 00:00 0
7f8ada600000-7f8ada601000 r--s 00003000 08:05 7079089 /home/admin/drds-worker/lib/ushura-1.0.jar
7f8ae9ba2000-7f8ae9baa000 r-xp 00000000 08:03 3935984 /opt/taobao/install/ajdk-8.3.6_fp9-b30/jre/lib/amd64/libzip.so
7f8ae9baa000-7f8ae9da9000 ---p 00008000 08:03 3935984 /opt/taobao/install/ajdk-8.3.6_fp9-b30/jre/lib/amd64/libzip.so
7f8ae9da9000-7f8ae9daa000 rw-p 00007000 08:03 3935984 /opt/taobao/install/ajdk-8.3.6_fp9-b30/jre/lib/amd64/libzip.so
7f8ae9daa000-7f8ae9db6000 r-xp 00000000 08:03 1837851 /usr/lib64/libnss_files-2.17.so;614d5f07 (deleted)
7f8ae9db6000-7f8ae9fb5000 ---p 0000c000 08:03 1837851 /usr/lib64/libnss_files-2.17.so;614d5f07 (deleted)
7f8ae9fb5000-7f8ae9fb6000 r--p 0000b000 08:03 1837851 /usr/lib64/libnss_files-2.17.so;614d5f07 (deleted)
7f8ae9fb6000-7f8ae9fb7000 rw-p 0000c000 08:03 1837851 /usr/lib64/libnss_files-2.17.so;614d5f07 (deleted)
7f8ae9fb7000-7f8ae9fbd000 rw-p 00000000 00:00 0
7f8ae9fbd000-7f8ae9fe7000 r-xp 00000000 08:03 3935961 /opt/taobao/install/ajdk-8.3.6_fp9-b30/jre/lib/amd64/libjava.so
7f8aebc03000-7f8aebc05000 r--s 00008000 08:05 7079098 /home/admin/drds-worker/lib/grpc-stub-1.9.0.jar
7f8aebc05000-7f8aebc07000 r--s 00020000 08:05 7078930 /home/admin/drds-worker/lib/tddl-group-5.2.7-2-EXTEND-HOTMAPPING-SNAPSHOT.jar
7f8aebc07000-7f8aebc1f000 r--s 001af000 08:05 7079085 /home/admin/drds-worker/lib/aspectjweaver-1.8.5.jar
7f8aebc1f000-7f8aebc20000 ---p 00000000 00:00 0
7f8aebc20000-7f8aebd20000 rw-p 00000000 00:00 0
7f8aebd20000-7f8aebd35000 r-xp 00000000 08:03 1837234 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f8aebd35000-7f8aebf34000 ---p 00015000 08:03 1837234 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f8aebf34000-7f8aebf35000 r--p 00014000 08:03 1837234 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f8aebf35000-7f8aebf36000 rw-p 00015000 08:03 1837234 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f8aebf36000-7f8aec037000 r-xp 00000000 08:03 1837579 /usr/lib64/libm-2.17.so;614d5f07 (deleted)
7f8aec037000-7f8aec236000 ---p 00101000 08:03 1837579 /usr/lib64/libm-2.17.so;614d5f07 (deleted)
7f8aeca17000-7f8aeca18000 rw-p 0000d000 08:03 3936057 /opt/taobao/install/ajdk-8.3.6_fp9-b30/lib/amd64/jli/libjli.so
7f8aeca18000-7f8aeca2d000 r-xp 00000000 08:03 1836998 /usr/lib64/libz.so.1.2.7
7f8aeca2d000-7f8aecc2c000 ---p 00015000 08:03 1836998 /usr/lib64/libz.so.1.2.7
7f8aecc2c000-7f8aecc2d000 r--p 00014000 08:03 1836998 /usr/lib64/libz.so.1.2.7
7f8aecc2d000-7f8aecc2e000 rw-p 00015000 08:03 1836998 /usr/lib64/libz.so.1.2.7
7f8aecc2e000-7f8aecc45000 r-xp 00000000 08:03 1836993 /usr/lib64/libpthread-2.17.so;614d5f07 (deleted)
7f8aecc45000-7f8aece44000 ---p 00017000 08:03 1836993 /usr/lib64/libpthread-2.17.so;614d5f07 (deleted)
7f8aece44000-7f8aece45000 r--p 00016000 08:03 1836993 /usr/lib64/libpthread-2.17.so;614d5f07 (deleted)
7f8aece45000-7f8aece46000 rw-p 00017000 08:03 1836993 /usr/lib64/libpthread-2.17.so;614d5f07 (deleted)
7f8aece46000-7f8aece4a000 rw-p 00000000 00:00 0
7f8aece4a000-7f8aecea1000 r-xp 00000000 08:03 3936059 /opt/taobao/install/ajdk-8.3.6_fp9-b30/lib/amd64/libjemalloc.so.2
7f8aecea1000-7f8aed0a0000 ---p 00057000 08:03 3936059 /opt/taobao/install/ajdk-8.3.6_fp9-b30/lib/amd64/libjemalloc.so.2
7f8aed0a0000-7f8aed0a3000 rw-p 00056000 08:03 3936059 /opt/taobao/install/ajdk-8.3.6_fp9-b30/lib/amd64/libjemalloc.so.2
7f8aed0a3000-7f8aed0b5000 rw-p 00000000 00:00 0
7f8aed0b5000-7f8aed0d7000 r-xp 00000000 08:03 1837788 /usr/lib64/ld-2.17.so;614d5f07 (deleted)
7f8aed0d7000-7f8aed0dc000 r--s 00038000 08:05 7079012 /home/admin/drds-worker/lib/org.osgi.core-4.2.0.jar
7f8aed0dc000-7f8aed0e1000 r--s 00038000 08:05 7079018 /home/admin/drds-worker/lib/commons-beanutils-1.9.3.jar
7f8aed0e1000-7f8aed0e3000 r--s 00001000 08:05 7079033 /home/admin/drds-worker/lib/j2objc-annotations-1.1.jar
7f8aed0e3000-7f8aed0e8000 r--s 00017000 08:05 7079056 /home/admin/drds-worker/lib/hibernate-jpa-2.1-api-1.0.0.Final.jar
7f8aed1be000-7f8aed1c6000 rw-s 00000000 08:04 393222 /tmp/hsperfdata_admin/14011
7f8aed1c6000-7f8aed1ca000 ---p 00000000 00:00 0
7f8aed1ca000-7f8aed2cd000 rw-p 00000000 00:00 0
7f8aed2cd000-7f8aed2ce000 r--s 00005000 08:05 7079029 /home/admin/drds-worker/lib/jersey-apache-connector-2.26.jar
7f8aed2ce000-7f8aed2d1000 r--s 0000a000 08:05 7079027 /home/admin/drds-worker/lib/metrics-jvm-1.7.4.jar
7f8aed2d1000-7f8aed2d3000 r--s 00006000 08:05 7078961 /home/admin/drds-worker/lib/tddl-client-5.2.7-2-EXTEND-HOTMAPPING-SNAPSHOT.jar
7f8aed2d3000-7f8aed2d4000 rw-p 00000000 00:00 0
7f8aed2d4000-7f8aed2d5000 r--p 00000000 00:00 0
7f8aed2d5000-7f8aed2d6000 rw-p 00000000 00:00 0
7f8aed2d6000-7f8aed2d7000 r--p 00021000 08:03 1837788 /usr/lib64/ld-2.17.so;614d5f07 (deleted)
7f8aed2d7000-7f8aed2d8000 rw-p 00022000 08:03 1837788 /usr/lib64/ld-2.17.so;614d5f07 (deleted)
7f8aed2d8000-7f8aed2d9000 rw-p 00000000 00:00 0
7fff087e0000-7fff08801000 rw-p 00000000 00:00 0 [stack]
7fff089c2000-7fff089c4000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

内存管理和使用

malloc

malloc()分配内存时:

  • 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存–在堆顶分配;
  • 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存–从文件映射区域分配;

图片

图片

对于 「malloc 申请的内存,free 释放内存会归还给操作系统吗?」:

  • malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用–小内存分配避免反复调用系统操作导致上下文切换,缺点是没回收容易导致内存碎片进而浪费内存。brk分配出来的内存在maps中显示有heap字样;
  • malloc 通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放。并且mmap分配的虚拟内存都是缺页状态的。

malloc和mmap

glibc中的malloc/free 负责向内核批发内存(不需要每次分配都真正地去调用内核态来分配),分配好的内存按大小分成不同的桶,每次malloc的时候实际到对应的桶上摘取对应的块(slab)就好,用完free的时候挂回去。

image-20211118121500859

mmap映射内存

image-20211108122432263

私有匿名映射常用于分配内存,也就是申请堆内存

分桶式内存管理比简单算法无论是在算法效率方面,还是在碎片控制方面都有很大的提升。但它的缺陷也很明显:区域内部的使用率不够高和动态扩展能力不够好。例如,4 字节的区域提前消耗完了,但 8 字节的空闲区域还有很多,此时就会面临两难选择,如果直接分配 8 字节的区域,则区域内部浪费就比较多,如果不分配,则明明还有空闲区域,却无法成功分配。

为了解决以上问题所以搞了buddy

node->zone->buddy->slab

img

假如需要分配一块 4 字节大小的空间,但是在 4 字节的 free list 上找不到空闲区域,系统就会往上找,假如 8 字节和 16 字节的 free list 中也没有空闲区域,就会一直向上找到 32 字节的 free list。

image-20211118120823595

伙伴系统不会直接把 32 的空闲区域分配出去,因为这样做的话,会带来巨大的浪费。它会先把 32 字节分成两个 16 字节,把后边一个挂入到 16 字节的 free list 中。然后继续拆分前一半。前一半继续拆成两个 8 字节,再把后一半挂入到 8 字节的 free list,最后,把前一半 8 字节拿去分配,当然这里也要继续拆分成两个 4 字节的空闲区域,其中一个用于本次 malloc 分配,另一个则挂入到 4 字节的 free list。分配后的内存的状态如下所示:

image-20211118120851731

查看zone

The zones are:

  • DMA is the low 16 MBytes of memory. At this point it exists for historical reasons; once upon what is now a long time ago, there was hardware that could only do DMA into this area of physical memory.
  • DMA32 exists only in 64-bit Linux; it is the low 4 GBytes of memory, more or less. It exists because the transition to large memory 64-bit machines has created a class of hardware that can only do DMA to the low 4 GBytes of memory.(This is where people mutter about everything old being new again.)
  • Normal is different on 32-bit and 64-bit machines. On 64-bit machines, it is all RAM from 4GB or so on upwards. On 32-bit machines it is all RAM from 16 MB to 896 MB for complex and somewhat historical reasons. Note that this implies that machines with a 64-bit kernel can have very small amounts of Normal memory unless they have significantly more than 4GB of RAM. For example, a 2 GB machine running a 64-bit kernel will have no Normal memory at all while a 4 GB machine will have only a tiny amount of it.
  • HighMem exists only on 32-bit Linux; it is all RAM above 896 MB, including RAM above 4 GB on sufficiently large machines.

每个zone下很多pages(大小为4K),buddy就是这些Pages的组织管理者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# cat /proc/zoneinfo |grep Node -A10
Node 0, zone DMA
pages free 3972
min 0
low 0
high 0
scanned 0
spanned 4095
present 3993
managed 3972
nr_free_pages 3972
nr_alloc_batch 0
--
Node 0, zone DMA32
pages free 361132
min 30
low 37
high 45
scanned 0
spanned 1044480
present 430773
managed 361133
nr_free_pages 361132
nr_alloc_batch 8
--
Node 0, zone Normal
pages free 96017308
min 16864
low 21080
high 25296
scanned 0
spanned 200736768
present 200736768
managed 197571780
nr_free_pages 96017308
nr_alloc_batch 3807

# free -g
total used free shared buff/cache available
Mem: 755 150 367 3 236 589
Swap: 0 0 0

每个页面大小是4K,很容易可以计算出每个 zone 的大小。比如对于上面 Node0 的 Normal, 197571780 * 4K/(1024*1024) = 753 GB。

dmidecode 可以查看到服务器上插着的所有内存条,也可以看到它是和哪个CPU直接连接的。每一个CPU以及和他直连的内存条组成了一个 node(节点)

/proc/buddyinfo

/proc/buddyinfo记录了可用内存的情况。

Normal那行之后的第二列表示: 643847*2^1*Page_Size(4K) ; 第三列表示: 357451*2^2*Page_Size(4K) ,高阶内存指的是2^3及更大的内存块。

应用申请大块连续内存(高阶内存,一般之4阶及以上, 也就是64K以上–2^4*4K)时,容易导致卡顿。这是因为大块连续内存确实系统需要触发回收或者碎片整理,需要一定的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#cat /proc/buddyinfo 
Node 0, zone DMA 1 1 1 0 2 1 1 0 1 1 3
Node 0, zone DMA32 2 5 3 6 2 0 4 4 2 2 404
Node 0, zone Normal 243430 643847 357451 32531 9508 6159 3917 2960 17172 2633 22854

如果是多node机器:
#cat /proc/buddyinfo
Node 0, zone DMA 4 6 3 2 3 3 1 1 2 3 1
Node 0, zone DMA32 1607 1619 1552 1520 1370 1065 827 576 284 105 13
Node 0, zone Normal 38337 145731 222145 199776 151452 91969 38086 10037 1762 104 1
Node 1, zone Normal 21521 147637 299185 245533 172451 81459 19451 7198 579 3 0
Node 2, zone Normal 68427 538670 446906 229138 123555 62539 21161 4407 1122 166 274
Node 3, zone Normal 27353 54601 114355 123568 101892 79098 48610 21036 5021 475 6
Node 4, zone Normal 45802 42758 8573 184548 148397 70540 20772 4147 381 148 109
Node 5, zone Normal 19514 39583 140493 167901 134774 61888 22998 6326 457 32 0
Node 6, zone Normal 104493 378362 355158 93138 12928 2248 1019 663 172 40 121
Node 7, zone Normal 34185 256886 249560 95547 54526 51022 28180 9757 2038 1351 280

[root@hygon8 15:50 /root]
#numactl -H
available: 8 nodes (0-7)
node 0 cpus: 0 1 2 3 4 5 6 7 64 65 66 67 68 69 70 71
node 0 size: 64083 MB
node 0 free: 49838 MB
node 1 cpus: 8 9 10 11 12 13 14 15 72 73 74 75 76 77 78 79
node 1 size: 64480 MB
node 1 free: 43596 MB
node 2 cpus: 16 17 18 19 20 21 22 23 80 81 82 83 84 85 86 87
node 2 size: 64507 MB
node 2 free: 44216 MB
node 3 cpus: 24 25 26 27 28 29 30 31 88 89 90 91 92 93 94 95
node 3 size: 64507 MB
node 3 free: 51095 MB
node 4 cpus: 32 33 34 35 36 37 38 39 96 97 98 99 100 101 102 103
node 4 size: 64507 MB
node 4 free: 32877 MB
node 5 cpus: 40 41 42 43 44 45 46 47 104 105 106 107 108 109 110 111
node 5 size: 64507 MB
node 5 free: 33430 MB
node 6 cpus: 48 49 50 51 52 53 54 55 112 113 114 115 116 117 118 119
node 6 size: 64507 MB
node 6 free: 14233 MB
node 7 cpus: 56 57 58 59 60 61 62 63 120 121 122 123 124 125 126 127
node 7 size: 63483 MB
node 7 free: 36577 MB
node distances:
node 0 1 2 3 4 5 6 7
0: 10 16 16 16 28 28 22 28
1: 16 10 16 16 28 28 28 22
2: 16 16 10 16 22 28 28 28
3: 16 16 16 10 28 22 28 28
4: 28 28 22 28 10 16 16 16
5: 28 28 28 22 16 10 16 16
6: 22 28 28 28 16 16 10 16
7: 28 22 28 28 16 16 16 10

[root@hygon8 15:51 /root]
#cat /proc/pagetypeinfo
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 0, zone DMA, type Unmovable 1 2 1 1 3 2 0 0 1 0 0
Node 0, zone DMA, type Movable 0 0 0 0 0 0 0 0 0 3 1
Node 0, zone DMA, type Reclaimable 3 4 2 1 0 1 1 1 1 0 0
Node 0, zone DMA, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Unmovable 151 164 162 165 140 78 19 8 0 0 0
Node 0, zone DMA32, type Movable 1435 1430 1374 1335 1214 974 798 563 281 98 12
Node 0, zone DMA32, type Reclaimable 21 25 16 20 16 13 10 5 3 7 1
Node 0, zone DMA32, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Unmovable 4849 6607 4133 1629 654 121 15 3 0 0 0
Node 0, zone Normal, type Movable 21088 >100000 >100000 >100000 >100000 90231 37197 9379 1552 83 1
Node 0, zone Normal, type Reclaimable 153 139 3012 3113 2437 1617 874 655 210 21 0
Node 0, zone Normal, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Movable Reclaimable HighAtomic Isolate
Node 0, zone DMA 1 6 1 0 0
Node 0, zone DMA32 27 974 15 0 0
Node 0, zone Normal 856 30173 709 0 0
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 1, zone Normal, type Unmovable 842 2898 2495 1316 490 102 23 1 2 0 0
Node 1, zone Normal, type Movable 22484 >100000 >100000 >100000 >100000 80084 18922 6889 48 4 0
Node 1, zone Normal, type Reclaimable 1 2022 3850 3534 2582 1273 506 308 529 0 0
Node 1, zone Normal, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 1, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Movable Reclaimable HighAtomic Isolate
Node 1, zone Normal 810 31221 737 0 0
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 2, zone Normal, type Unmovable 2833 6802 3888 1636 329 3 1 2 0 0 0
Node 2, zone Normal, type Movable 72017 >100000 >100000 >100000 >100000 61710 20764 4242 841 55 239
Node 2, zone Normal, type Reclaimable 114 8 2056 2221 1544 826 396 163 281 111 35
Node 2, zone Normal, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 2, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Movable Reclaimable HighAtomic Isolate
Node 2, zone Normal 1066 31063 639 0 0
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 3, zone Normal, type Unmovable 2508 6171 3802 1502 365 93 30 1 2 0 0
Node 3, zone Normal, type Movable 23396 48450 >100000 >100000 99802 77850 47910 20587 4796 428 5
Node 3, zone Normal, type Reclaimable 10 0 609 2111 1726 1155 670 448 223 46 1
Node 3, zone Normal, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 3, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Movable Reclaimable HighAtomic Isolate
Node 3, zone Normal 768 31425 575 0 0
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 4, zone Normal, type Unmovable 3817 3739 1716 992 261 39 4 1 0 0 1
Node 4, zone Normal, type Movable 27857 39138 6875 >100000 >100000 70501 20752 4115 362 49 104
Node 4, zone Normal, type Reclaimable 1 8 3 5 0 0 16 31 19 97 4
Node 4, zone Normal, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 4, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Movable Reclaimable HighAtomic Isolate
Node 4, zone Normal 712 31706 350 0 0
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 5, zone Normal, type Unmovable 4875 4728 3165 1202 464 67 3 0 0 0 0
Node 5, zone Normal, type Movable 18382 34874 >100000 >100000 >100000 61296 22711 6235 348 32 0
Node 5, zone Normal, type Reclaimable 16 0 1 7 2 525 284 91 109 0 0
Node 5, zone Normal, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 5, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Movable Reclaimable HighAtomic Isolate
Node 5, zone Normal 736 31716 316 0 0
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 6, zone Normal, type Unmovable 10489 6842 2821 434 257 22 1 1 1 3 0
Node 6, zone Normal, type Movable 90841 >100000 >100000 92129 11336 1526 704 552 141 34 118
Node 6, zone Normal, type Reclaimable 434 41 0 576 1338 700 314 110 30 5 3
Node 6, zone Normal, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 6, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Movable Reclaimable HighAtomic Isolate
Node 6, zone Normal 807 31686 275 0 0
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 7, zone Normal, type Unmovable 1516 1894 2285 908 633 121 16 7 4 2 0
Node 7, zone Normal, type Movable 18209 >100000 >100000 93283 52811 50349 27973 9703 2026 1341 248
Node 7, zone Normal, type Reclaimable 0 1 0 1341 1082 552 191 47 8 8 32
Node 7, zone Normal, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 7, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Movable Reclaimable HighAtomic Isolate
Node 7, zone Normal 1262 31265 241 0 0

/proc/pagetypeinfo

cat /proc/pagetypeinfo, 你可以看到当前系统里伙伴系统里各个尺寸的可用连续内存块数量。unmovable pages是不可以被迁移的,比如slab等kmem都不可以被迁移,因为内核里面对这些内存很多情况下是通过指针来访问的,而不是通过页表,如果迁移的话,就会导致原来的指针访问出错。

img

当迁移类型为 Unmovable 的页面都聚集在 order < 3 时,说明内核 slab 碎片化严重

alloc_pages分配内存的时候就到上面对应大小的free_area的链表上寻找可用连续页面。alloc_pages是怎么工作的呢?我们举个简单的小例子。假如要申请8K-连续两个页框的内存。为了描述方便,我们先暂时忽略UNMOVEABLE、RELCLAIMABLE等不同类型

img

基于伙伴系统的内存分配中,有可能需要将大块内存拆分成两个小伙伴。在释放中,可能会将两个小伙伴合并再次组成更大块的连续内存。

伙伴系统中的伙伴指的是两个内存块,大小相同,地址连续,同属于一个大块区域。

对于应用来说基本分配单位是4K(开启大页后一般是2M),对于内核来说4K有点浪费。所以内核又专门给自己定制了一个更精细的内存管理系统slab。

slab

对于内核运行中实际使用的对象来说,多大的对象都有。有的对象有1K多,但有的对象只有几百、甚至几十个字节。如果都直接分配一个 4K的页面 来存储的话也太浪费了,所以伙伴系统并不能直接使用。

在伙伴系统之上,内核又给自己搞了一个专用的内存分配器, 叫slab。

这个分配器最大的特点就是,一个slab内只分配特定大小、甚至是特定的对象。这样当一个对象释放内存后,另一个同类对象可以直接使用这块内存。通过这种办法极大地降低了碎片发生的几率。

1
2
3
4
#cat /proc/meminfo
Slab: 102076 kB
SReclaimable: 70816 kB
SUnreclaim: 31260 kB
  • Slab 内核通过slab分配管理的内存总数。
  • SReclaimable 内核通过slab分配的可回收的内存(例如dentry),通过echo 2 > /proc/sys/vm/drop_caches回收。
  • SUnreclaim 内核通过slab分配的不可回收的内存。

kmem cache

slabtop和/proc/slabinfo 查看cached使用情况 主要是:pagecache(页面缓存), dentries(目录缓存), inodes

通过查看 /proc/slabinfo 我们可以查看到所有的 kmem cache。

img

slabtop 按占用内存从大往小进行排列。用来分析 slab 内存开销。

0d8a26db-3663-40af-b215-f8601ef23676.png (1388×1506)

无论是 /proc/slabinfo,还是 slabtop 命令的输出。里面都包含了每个 cache 中 slab的如下几个关键属性。

  • objsize:每个对象的大小

  • objperslab:一个 slab 里存放的对象的数量

  • pagesperslab:一个slab 占用的页面的数量,每个页面4K,这样也就能算出每个 slab 占用的内存大小。

比如如下TCP slabinfo中可以看到每个slab占用8(pagesperslab)个Page(8*4096=32768),每个对象的大小是1984(objsize),每个slab存放了16(objperslab)个对象. 那么1984 *16=31744,现在的空间基本用完,剩下接近1K,又放不下一个1984大小的对象,算是额外开销了。

1
2
3
4
#cat /proc/slabinfo |grep -E "active_objs|TCP"
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
tw_sock_TCP 5372 5728 256 32 2 : tunables 0 0 0 : slabdata 179 179 0
TCP 6090 6144 1984 16 8 : tunables 0 0 0 : slabdata 384 384 0

内存分配和延迟

内存不够、脏页太多、碎片太多,都会导致分配失败,从而触发回收,导致卡顿。

系统中脏页过多引起 load 飙高

直接回收过程中,如果存在较多脏页就可能涉及在回收过程中进行回写,这可能会造成非常大的延迟,而且因为这个过程本身是阻塞式的,所以又可能进一步导致系统中处于 D 状态的进程数增多,最终的表现就是系统的 load 值很高。

image.png

可以通过 sar -r 来观察系统中的脏页个数:

1
2
3
4
5
6
$ sar -r 1
07:30:01 PM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
09:20:01 PM 5681588 2137312 27.34 0 1807432 193016 2.47 534416 1310876 4
09:30:01 PM 5677564 2141336 27.39 0 1807500 204084 2.61 539192 1310884 20
09:40:01 PM 5679516 2139384 27.36 0 1807508 196696 2.52 536528 1310888 20
09:50:01 PM 5679548 2139352 27.36 0 1807516 196624 2.51 536152 1310892 24

kbdirty 就是系统中的脏页大小,它同样也是对 /proc/vmstat 中 nr_dirty 的解析。你可以通过调小如下设置来将系统脏页个数控制在一个合理范围:

vm.dirty_background_bytes = 0

vm.dirty_background_ratio = 10

vm.dirty_bytes = 0

vm.dirty_expire_centisecs = 3000

vm.dirty_ratio = 20

至于这些值调整大多少比较合适,也是因系统和业务的不同而异,我的建议也是一边调整一边观察,将这些值调整到业务可以容忍的程度就可以了,即在调整后需要观察业务的服务质量 (SLA),要确保 SLA 在可接受范围内。调整的效果你可以通过 /proc/vmstat 来查看:

1
2
3
#grep "nr_dirty_" /proc/vmstat
nr_dirty_threshold 3071708
nr_dirty_background_threshold 1023902

在4.20的内核并且sar 的版本为12.3.3可以看到PSI(Pressure-Stall Information)

1
2
some avg10=45.49 avg60=10.23 avg300=5.41 total=76464318
full avg10=40.87 avg60=9.05 avg300=4.29 total=58141082

你需要重点关注 avg10 这一列,它表示最近 10s 内存的平均压力情况,如果它很大(比如大于 40)那 load 飙高大概率是由于内存压力,尤其是 Page Cache 的压力引起的。

image.png

容器中的内存回收

kswapd线程(每个node一个kswapd进程,负责本node)回收内存时,可以先对脏页进行回写(writeback)再进行回收,而直接内存回收只回收干净页。也叫同步回收.

直接内存回收是在当前进程的上下文中进行的,要等内存回收完成才能继续尝试进行分配,所以是阻塞了当前进程的执行,会导致响应延迟增加

如果是在容器里,也就是在某个子memory cgroup 中,那么在分配内存后,还有一个记账(charge)的步骤,就是要把这次分配的内存页记在某个memory cgroup的账上,这样才能控制这个容器里的进程所能使用的内存数量。

在开源社区的linux代码中,如果charge 失败,也就是说,当新分配的内存加上原先的usage超过了limit,就会触发内存回收,try_to_free_mem_cgroup_pages,这个也是同步回收,等同于直接内存回收(发生在当前进程的上下文忠),所以会对应用的响应造成影响(表现为卡顿)。

碎片化

内存碎片严重的话会导致系统hang很久(回收、压缩内存)

尽量让系统的free多一点(比例高一点)可以调整 vm.min_free_kbytes(128G 以内 2G,256G以内 4G/8G), 线上机器直接修改vm.min_free_kbytes会触发回收,导致系统hang住 https://www.atatech.org/articles/163233 https://www.atatech.org/articles/97130

每个zone都有自己的min low high,如下,但是单位是page, 计算案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#cat /proc/zoneinfo  |grep "Node"
Node 0, zone DMA
Node 0, zone DMA32
Node 0, zone Normal
Node 1, zone Normal

#cat /proc/zoneinfo |grep "Node 0, zone" -A10
Node 0, zone DMA
pages free 3975
min 20
low 25
high 30
scanned 0
spanned 4095
present 3996
managed 3975
nr_free_pages 3975
nr_alloc_batch 5
--
Node 0, zone DMA32
pages free 382873
min 2335
low 2918
high 3502
scanned 0
spanned 1044480
present 513024
managed 450639
nr_free_pages 382873
nr_alloc_batch 584
--
Node 0, zone Normal
pages free 11105097
min 61463
low 76828
high 92194
scanned 0
spanned 12058624
present 12058624
managed 11859912
nr_free_pages 11105097
nr_alloc_batch 12344

low = 5/4 * min
high = 3/2 * min


#T=min;sum=0;for i in `cat /proc/zoneinfo |grep $T | awk '{print $NF}'`;do sum=`echo "$sum+$i" |bc`;done;sum=`echo "$sum*4/1024" |bc`;echo "sum=${sum} MB"
sum=499 MB

#T=low;sum=0;for i in `cat /proc/zoneinfo |grep $T | awk '{print $NF}'`;do sum=`echo "$sum+$i" |bc`;done;sum=`echo "$sum*4/1024" |bc`;echo "sum=${sum} MB"
sum=624 MB

#T=high;sum=0;for i in `cat /proc/zoneinfo |grep $T | awk '{print $NF}'`;do sum=`echo "$sum+$i" |bc`;done;sum=`echo "$sum*4/1024" |bc`;echo "sum=${sum} MB"
sum=802 MB

内存碎片化导致rt升高的诊断

判定方法如下:

  1. 运行 sar -B 观察 pgscand/s,其含义为每秒发生的直接内存回收次数,当在一段时间内持续大于 0 时,则应继续执行后续步骤进行排查;
  2. 运行 cat /sys/kernel/debug/extfrag/extfrag_index 观察内存碎片指数,重点关注 order >= 3 的碎片指数,当接近 1.000 时,表示碎片化严重,当接近 0 时表示内存不足;
  3. 运行 cat /proc/buddyinfo, cat /proc/pagetypeinfo 查看内存碎片情况, 指标含义参考 ,同样关注 order >= 3 的剩余页面数量,pagetypeinfo 相比 buddyinfo 展示的信息更详细一些,根据迁移类型 (伙伴系统通过迁移类型实现反碎片化)进行分组,需要注意的是,当迁移类型为 Unmovable 的页面都聚集在 order < 3 时,说明内核 slab 碎片化严重,我们需要结合其他工具来排查具体原因,在本文就不做过多介绍了;
  4. 对于 CentOS 7.6 等支持 BPF 的 kernel 也可以运行我们研发的 drsnoop,compactsnoop 工具对延迟进行定量分析,使用方法和解读方式请参考对应文档;
  5. (Opt) 使用 ftrace 抓取 mm_page_alloc_extfrag 事件,观察因内存碎片从备用迁移类型“盗取”页面的信息。

一个阿里云ECS 因为宿主机碎片导致性能衰退的案例

LVS后面三个RS在同样压力流量下,其中一个节点CPU非常高,通过top看起来是所有操作都很慢,像是CPU被降频了一样,但是直接跑CPU Prime性能又没有问题

image.png

原因:ECS所在的宿主机内存碎片比较严重,导致分配到的内存主要是4K Page,在ECS中大页场景下会慢很多

通过 openssl speed aes-256-ige 能稳定重现 在大块的加密上慢很多

image.png

小块上性能一致,这也就是为什么算Prime性能没问题。导致慢只涉及到大块内存分配的场景,这里需要映射到宿主机,但是碎片多分配慢导致了问题。

如果reboot ECS的话实际只是就地重启ECS,仍然使用的reboot前分配好的宿主机内存,不会解决问题。重启ECS中的进程也不会解决问题,只有将ECS迁移到别的物理机(也就是通过控制台重启,会重新选择物理机)才有可能解决这个问题。

或者购买新的ECS机型(比如第6代之后ECS)能避免这个问题。

ECS内部没法查看到这个碎片,只能在宿主机上通过命令查看大页情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
有问题NC上buddyinfo信息
$cat /proc/buddyinfo
Node 0, zone DMA 1 1 0 0 2 1 1 0 1 1 3
Node 0, zone DMA32 23 23 17 15 13 9 8 8 4 3 367
Node 0, zone Normal 295291 298652 286048 266597 218191 156837 93156 45930 25856 0 0

最新建的vm,大页不多
$sudo cat /proc/9550/smaps |grep AnonHuge |awk '{sum+=$2}END{print sum}'
210944
------------------------
第一台正常ECS所在的NC
$cat /proc/buddyinfo
Node 0, zone DMA 1 1 0 0 2 1 1 0 1 1 3
Node 0, zone DMA32 7 5 5 9 8 4 6 10 5 5 366
Node 0, zone Normal 203242 217888 184465 176280 148612 102122 55787 26642 24824 0 0

早期的vm,大页充足
$sudo cat /proc/87369/smaps |grep AnonHuge |awk '{sum+=$2}END{print sum}'
8275968

近期的vm,大页不够
$sudo cat /proc/22081/smaps |grep AnonHuge |awk '{sum+=$2}END{print sum}'
251904

$sudo cat /proc/44073/smaps |grep AnonHuge |awk '{sum+=$2}END{print sum}'
10240

内存使用分析

pmap

1
2
3
4
5
6
7
8
9
10
11
12
13
pmap -x 24282 | less
24282: /usr/sbin/rsyslogd -n
Address Kbytes RSS Dirty Mode Mapping
000055ce1a99f000 596 580 0 r-x-- rsyslogd
000055ce1ac34000 12 12 12 r---- rsyslogd
000055ce1ac37000 28 28 28 rw--- rsyslogd
000055ce1ac3e000 4 4 4 rw--- [ anon ]
000055ce1c1f1000 364 204 204 rw--- [ anon ]
00007fff8b5a4000 132 20 20 rw--- [ stack ]
00007fff8b5e6000 12 0 0 r---- [ anon ]
00007fff8b5e9000 8 4 0 r-x-- [ anon ]
---------------- ------- ------- -------
total kB 620060 17252 3304
  • Address:占用内存的文件的内存起始地址。
  • Kbytes:占用内存的字节数。
  • RSS:实际占用内存大小。
  • Dirty:脏页大小。
  • Mapping:占用内存的文件,[anon] 为已分配的内存,[stack] 为程序堆栈

/proc/pid/

/proc/[pid]/ 下面与进程内存相关的文件主要有maps , smaps, status。
maps: 文件可以查看某个进程的代码段、栈区、堆区、动态库、内核区对应的虚拟地址
smaps: 显示每个分区更详细的内存占用数据,能看到一个动态库被共享了几次
status: 包含了所有CPU活跃的信息,该文件中的所有值都是从系统启动开始累计到当前时刻

Java内存使用分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
创建1000个线程,ss为2M
java -XX:NativeMemoryTracking=detail -Xms10g -Xmx10g -Xmn5g -XX:ReservedCodeCacheSize=512m -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:MaxDirectMemorySize=1g -Xss2048K ThreadPoolExample

分析结果:
#jcmd 81849 VM.native_memory summary
81849:

Native Memory Tracking:

Total: reserved=14737064KB, committed=13157168KB
- Java Heap (reserved=10485760KB, committed=10485760KB)
(mmap: reserved=10485760KB, committed=10485760KB)

- Class (reserved=1102016KB, committed=50112KB)
(classes #416)
(malloc=45248KB #1420)
(mmap: reserved=1056768KB, committed=4864KB)

//committed 已向OS提请分配,实际要到使用时page fault才会实际分配物理内存并在RSS中反应出来
- Thread (reserved=2134883KB, committed=2134883KB)//reserved还没分配,不能访问
(thread #1070) //1000个应用线程,加70个JVM native线程
(stack: reserved=2128820KB, committed=2128820KB) //需要2G多点
(malloc=3500KB #5390)
(arena=2563KB #2138)

- Code (reserved=532612KB, committed=4620KB)
(malloc=132KB #528)
(mmap: reserved=532480KB, committed=4488KB)

- GC (reserved=430421KB, committed=430421KB)
(malloc=50737KB #235)
(mmap: reserved=379684KB, committed=379684KB)

- Compiler (reserved=137KB, committed=137KB)
(malloc=6KB #53)
(arena=131KB #3)

- Internal (reserved=48901KB, committed=48901KB)
(malloc=48869KB #14030)
(mmap: reserved=32KB, committed=32KB)

- Symbol (reserved=1479KB, committed=1479KB)
(malloc=959KB #110)
(arena=520KB #1)

- Native Memory Tracking (reserved=608KB, committed=608KB)
(malloc=193KB #2556)
(tracking overhead=415KB)

- Arena Chunk (reserved=248KB, committed=248KB)
(malloc=248KB)

We can see two types of memory:

  • Reserved — the size which is guaranteed to be available by a host’s OS (but still not allocated and cannot be accessed by JVM) — it’s just a promise
  • Committed — already taken, accessible, and allocated by JVM

page fault

内核给用户态申请的内存,默认都只是一段虚拟地址空间而已,并没有分配真正的物理内存。在第一次读写的时候才触发物理内存的分配,这个过程叫做page fault。那么,为了访问到真正的物理内存,page fault的时候,就需要更新对应的page table了。

参考资料

https://www.atatech.org/articles/66885

https://cloud.tencent.com/developer/article/1087455

https://www.cnblogs.com/xiaolincoding/p/13719610.html

rsyslog占用内存高

https://sunsea.im/rsyslogd-systemd-journald-high-memory-solution.html

鸟哥 journald 介绍

说出来你可能不信,内核这家伙在内存的使用上给自己开了个小灶!

socket 与 slab dentry

Linux内存--HugePage

发表于 2020-11-15 | 分类于 Memory

Linux内存–HugePage

本系列有如下几篇

[Linux 内存问题汇总](/2020/01/15/Linux 内存问题汇总/)

Linux内存–PageCache

Linux内存–管理和碎片

Linux内存–HugePage

Linux内存–零拷贝

/proc/buddyinfo

/proc/buddyinfo记录了内存的详细碎片情况。

1
2
3
4
#cat /proc/buddyinfo 
Node 0, zone DMA 1 1 1 0 2 1 1 0 1 1 3
Node 0, zone DMA32 2 5 3 6 2 0 4 4 2 2 404
Node 0, zone Normal 243430 643847 357451 32531 9508 6159 3917 2960 17172 2633 22854

Normal行的第二列表示: 643847*2^1*Page_Size(4K) ; 第三列表示: 357451*2^2*Page_Size(4K) ,高阶内存指的是2^3及更大的内存块。

应用申请大块连续内存(高阶内存,一般之4阶及以上, 也就是64K以上–2^4*4K)时,容易导致卡顿。这是因为大块连续内存确实系统需要触发回收或者碎片整理,需要一定的时间。

slabtop和/proc/slabinfo

slabtop和/proc/slabinfo 查看cached使用情况 主要是:pagecache(页面缓存), dentries(目录缓存), inodes

关于hugetlb

This is an entry in the TLB that points to a HugePage (a large/big page larger than regular 4K and predefined in size). HugePages are implemented via hugetlb entries, i.e. we can say that a HugePage is handled by a “hugetlb page entry”. The ‘hugetlb” term is also (and mostly) used synonymously with a HugePage.

hugetlb 是TLB中指向HugePage的一个entry(通常大于4k或预定义页面大小)。 HugePage 通过hugetlb entries来实现,也可以理解为HugePage 是hugetlb page entry的一个句柄。

Linux下的大页分为两种类型:标准大页(Huge Pages)和透明大页(Transparent Huge Pages)

标准大页管理是预分配的方式,而透明大页管理则是动态分配的方式

目前透明大页与传统HugePages联用会出现一些问题,导致性能问题和系统重启。Oracle 建议禁用透明大页(Transparent Huge Pages)

hugetlbfs比THP要好,开thp的机器碎片化严重(不开THP会有更严重的碎片化问题),最后和没开THP一样 https://www.atatech.org/articles/152660

Linux 中的 HugePages 都被锁定在内存中,所以哪怕是在系统内存不足时,它们也不会被 Swap 到磁盘上,这也就能从根源上杜绝了重要内存被频繁换入和换出的可能。

Transparent Hugepages are similar to standard HugePages. However, while standard HugePages allocate memory at startup, Transparent Hugepages memory uses the khugepaged thread in the kernel to allocate memory dynamically during runtime, using swappable HugePages.

HugePage要求OS启动的时候提前分配出来,管理难度比较大,所以Enterprise Linux 6增加了一层抽象层来动态创建管理HugePage,这就是THP,而这个THP对应用透明,由khugepaged thread在后台动态将小页组成大页给应用使用,这里会遇上碎片问题导致需要compact才能得到大页,应用感知到的就是SYS CPU飙高,应用卡顿了。

虽然 HugePages 的开启大都需要开发或者运维工程师的额外配置,但是在应用程序中启用 HugePages 却可以在以下几个方面降低内存页面的管理开销:

  • 更大的内存页能够减少内存中的页表层级,这不仅可以降低页表的内存占用,也能降低从虚拟内存到物理内存转换的性能损耗;
  • 更大的内存页意味着更高的缓存命中率,CPU 有更高的几率可以直接在 TLB(Translation lookaside buffer)中获取对应的物理地址;
  • 更大的内存页可以减少获取大内存的次数,使用 HugePages 每次可以获取 2MB 的内存,是 4KB 的默认页效率的 512 倍;

HugePage

为什么需要Huge Page 了解CPU Cache大致架构的话,一定听过TLB Cache。Linux系统中,对程序可见的,可使用的内存地址是Virtual Address。每个程序的内存地址都是从0开始的。而实际的数据访问是要通过Physical Address进行的。因此,每次内存操作,CPU都需要从page table中把Virtual Address翻译成对应的Physical Address,那么对于大量内存密集型程序来说page table的查找就会成为程序的瓶颈。

所以现代CPU中就出现了TLB(Translation Lookaside Buffer) Cache用于缓存少量热点内存地址的mapping关系。然而由于制造成本和工艺的限制,响应时间需要控制在CPU Cycle级别的Cache容量只能存储几十个对象。那么TLB Cache在应对大量热点数据Virual Address转换的时候就显得捉襟见肘了。我们来算下按照标准的Linux页大小(page size) 4K,一个能缓存64元素的TLB Cache只能涵盖4K*64 = 256K的热点数据的内存地址,显然离理想非常遥远的。于是Huge Page就产生了。

Huge pages require contiguous areas of memory, so allocating them at boot is the most reliable method since memory has not yet become fragmented. To do so, add the following parameters to the kernel boot command line:

Huge pages kernel options

  • hugepages

    Defines the number of persistent huge pages configured in the kernel at boot time. The default value is 0. It is only possible to allocate (or deallocate) huge pages if there are sufficient physically contiguous free pages in the system. Pages reserved by this parameter cannot be used for other purposes.

    Default size huge pages can be dynamically allocated or deallocated by changing the value of the /proc/sys/vm/nr_hugepages file.

    In a NUMA system, huge pages assigned with this parameter are divided equally between nodes. You can assign huge pages to specific nodes at runtime by changing the value of the node’s /sys/devices/system/node/node_id/hugepages/hugepages-1048576kB/nr_hugepages file.

    For more information, read the relevant kernel documentation, which is installed in /usr/share/doc/kernel-doc-kernel_version/Documentation/vm/hugetlbpage.txt by default. This documentation is available only if the kernel-doc package is installed.

  • hugepagesz

    Defines the size of persistent huge pages configured in the kernel at boot time. Valid values are 2 MB and 1 GB. The default value is 2 MB.

  • default_hugepagesz

    Defines the default size of persistent huge pages configured in the kernel at boot time. Valid values are 2 MB and 1 GB. The default value is 2 MB.

应用程序想要利用大页优势,需要通过hugetlb文件系统来使用标准大页。操作步骤

1.预留大页

echo 20 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

2.挂载hugetlb文件系统

mount hugetlbfs /mnt/huge -t hugetlbfs

3.映射hugetbl文件

fd = open(“/mnt/huge/test.txt”, O_CREAT|O_RDWR);

addr = mmap(0, MAP_LENGTH, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

4.hugepage统计信息

通过hugepage提供的sysfs接口,可以了解大页使用情况

HugePages_Total: 预先分配的大页数量

HugePages_Free:空闲大页数量

HugePages_Rsvd: mmap申请大页数量(还没有产生缺页)

HugePages_Surp: 多分配的大页数量(由nr_overcommit_hugepages决定)

5 hugpage优缺点

缺点:

1.需要提前预估大页使用量,并且预留的大页不能被其他内存分配接口使用。

2.兼容性不好,应用使用标准大页,需要对代码进行重构才能有效的使用标准大页。

优点:因为内存是预留的,缺页延时非常小

针对Hugepage的不足,内核又衍生出了THP大页(Transparent Huge pages)

工具

1
2
3
4
5
6
7
8
9
10
11
yum install libhugetlbfs-utils -y

//列出
hugeadm --pool-list
Size Minimum Current Maximum Default
2097152 12850 12850 12850 *
1073741824 0 0 0

hugeadm --list-all-mounts
Mount Point Options
/dev/hugepages rw,relatime,pagesize=2M

大页和 MySQL 性能 case

MySQL的页都是16K, 当查询的行不在内存中时需要按照16K为单位从磁盘读取页,而文件系统中的页是4k,也就是一次数据库请求需要有4次磁盘IO,如过查询比较随机,每次只需要一个页中的几行数据,存在很大的读放大。

那么我们是否可以把MySQL的页设置为4K来减少读放大呢?

在5.7里收益不大,因为每次IO存在 fil_system 的锁,导致IO的并发上不去

8.0中总算优化了这个场景,测试细节可以参考这篇

16K VS 4K 性能对比(4K接近翻倍)

img

4K会带来的问题:顺序insert慢了10%(因为fsync更多了);DDL更慢;二级索引更多的场景下4K性能较差;大BP下,刷脏代价大。

HugePage 带来的问题

CPU对同一个Page抢占增多

对于写操作密集型的应用,Huge Page会大大增加Cache写冲突的发生概率。由于CPU独立Cache部分的写一致性用的是MESI协议,写冲突就意味:

  • 通过CPU间的总线进行通讯,造成总线繁忙
  • 同时也降低了CPU执行效率。
  • CPU本地Cache频繁失效

类比到数据库就相当于,原来一把用来保护10行数据的锁,现在用来锁1000行数据了。必然这把锁在线程之间的争抢概率要大大增加。

连续数据需要跨CPU读取

Page太大,更容易造成Page跨Numa/CPU 分布。

从下图我们可以看到,原本在4K小页上可以连续分配,并因为较高命中率而在同一个CPU上实现locality的数据。到了Huge Page的情况下,就有一部分数据为了填充统一程序中上次内存分配留下的空间,而被迫分布在了两个页上。而在所在Huge Page中占比较小的那部分数据,由于在计算CPU亲和力的时候权重小,自然就被附着到了其他CPU上。那么就会造成:本该以热点形式存在于CPU2 L1或者L2 Cache上的数据,不得不通过CPU inter-connect去remote CPU获取数据。 假设我们连续申明两个数组,Array A和Array B大小都是1536K。内存分配时由于第一个Page的2M没有用满,因此Array B就被拆成了两份,分割在了两个Page里。而由于内存的亲和配置,一个分配在Zone 0,而另一个在Zone 1。那么当某个线程需要访问Array B时就不得不通过代价较大的Inter-Connect去获取另外一部分数据。

img

Java进程开启HugePage

从perf数据来看压满后tlab miss比较高,得想办法降低这个值

修改JVM启动参数

JVM启动参数增加如下三个(-XX:LargePageSizeInBytes=2m, 这个一定要,有些资料没提这个,在我的JDK8.0环境必须要):

-XX:+UseLargePages -XX:LargePageSizeInBytes=2m -XX:+UseHugeTLBFS

修改机器系统配置

设置HugePage的大小

cat /proc/sys/vm/nr_hugepages

nr_hugepages设置多大参考如下计算方法:

If you are using the option -XX:+UseSHM or -XX:+UseHugeTLBFS, then specify the number of large pages. In the following example, 3 GB of a 4 GB system are reserved for large pages (assuming a large page size of 2048kB, then 3 GB = 3 * 1024 MB = 3072 MB = 3072 * 1024 kB = 3145728 kB and 3145728 kB / 2048 kB = 1536):

echo 1536 > /proc/sys/vm/nr_hugepages

透明大页是没有办法减少系统tlab,tlab是对应于进程的,系统分给进程的透明大页还是由物理上的4K page组成。

对于c++来说,他malloc经常会散落得全地址都是,因为会触发各种mmap,冷热区域。所以THP和hugepage都可能导致大量内存被浪费了,进而导致内存紧张,性能下滑。jvm的连续内存布局,加上gc会使得内存密度很紧凑。THP的问题是,他是逻辑页,不是物理页,tlb依旧要N份,所以他的收益来自page fault减少,是一次性的收益。

hugepage的在减少page_fault上和thp效果一样第二个作用是,他只需要一份TLB了,hugepage是真正的大页内存,thp是逻辑上的,物理上还是需要很多小的page。

如果TLB miss,则可能需要额外三次内存读取操作才能将线性地址翻译为物理地址。

code_hugepage 代码大页

代码大页特性主要为大代码段业务服务,可以降低程序的iTLB miss,从而提升程序性能。针对倚天这一类跨numa访存开销大的芯片有比较好的性能提升效果

1
2
// 1 表示仅打开二进制和动态库大页  2 仅打开可执行匿名大页 3 相当于1+2,0 表示关闭
echo 1 > /sys/kernel/mm/transparent_hugepage/hugetext_enabled //1 可以改成2/3

是否启用代码大页,可以查看/proc//smaps中FilePmdMapped字段可确定是否使用了代码大页。 扫描进程代码大页使用数量(单位KB):

1
cat /proc/<pid>/smaps | grep FilePmdMapped | awk '{sum+=$2}END{print"Sum= ",sum}'

THP

Linux kernel在2.6.38内核增加了Transparent Huge Pages (THP)特性 ,支持大内存页(2MB)分配,默认开启。当开启时可以降低fork子进程的速度,但fork之后,每个内存页从原来4KB变为2MB,会大幅增加重写期间父进程内存消耗。同时每次写命令引起的复制内存页单位放大了512倍,会拖慢写操作的执行时间,导致大量写操作慢查询。例如简单的incr命令也会出现在慢查询中。因此Redis日志中建议将此特性进行禁用。

THP 的目的是用一个页表项来映射更大的内存(大页),这样可以减少 Page Fault,因为需要的页数少了。当然,这也会提升 TLB(Translation Lookaside Buffer,由存储器管理单元用于改进虚拟地址到物理地址的转译速度) 命中率,因为需要的页表项也少了。如果进程要访问的数据都在这个大页中,那么这个大页就会很热,会被缓存在 Cache 中。而大页对应的页表项也会出现在 TLB 中,从上一讲的存储层次我们可以知道,这有助于性能提升。但是反过来,假设应用程序的数据局部性比较差,它在短时间内要访问的数据很随机地位于不同的大页上,那么大页的优势就会消失。

大页在使用的时候需要清理512个4K页面,再返回给用户,这里的清理动作可能会导致卡顿。另外碎片化严重的时候触发内存整理造成卡顿

大页分配: 在缺页处理函数__handle_mm_fault中判断是否使用大页 大页生成: 主要通过在分配大页内存时是否带__GFP_DIRECT_RECLAIM 标志来控制大页的生成.

1.异步生成大页: 在缺页处理中,把需要异步生成大页的VMA注册到链表,内核态线程khugepaged 动态为vma分配大页(内存回收,内存归整)

2.madvise系统调用只是给VMA加了VM_HUGEPAGE,用来标记这段虚拟地址需要使用大页

image-20240116134744487

THP 原理

大页分配: 在缺页处理函数__handle_mm_fault中判断是否使用大页 大页生成: 主要通过在分配大页内存时是否带__GFP_DIRECT_RECLAIM 标志来控制大页的生成.

1.异步生成大页: 在缺页处理中,把需要异步生成大页的VMA注册到链表,内核态线程khugepaged 动态为vma分配大页(内存回收,内存归整)

2.madvise系统调用只是给VMA加了VM_HUGEPAGE,用来标记这段虚拟地址需要使用大页

THP 对redis、mongodb 这种cache类推荐关闭,对drds这种java应用最好打开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never

grep "Huge" /proc/meminfo
AnonHugePages: 1286144 kB
ShmemHugePages: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB

$grep -e AnonHugePages /proc/*/smaps | awk '{ if($2>4) print $0} ' | awk -F "/" '{print $0; system("ps -fp " $3)} '

$grep -e AnonHugePages /proc/*/smaps | awk '{ if($2>4) print $0} ' | awk -F "/" '{print $0; system("ps -fp " $3)} '

//查看pagesize(默认4K)
$getconf PAGESIZE

在透明大页功能打开时,造成系统性能下降的主要原因可能是 khugepaged 守护进程。该进程会在(它认为)系统空闲时启动,扫描系统中剩余的空闲内存,并将普通 4k 页转换为大页。该操作会在内存路径中加锁,而该守护进程可能会在错误的时间启动扫描和转换大页的操作,从而影响应用性能。

此外,当缺页异常(page faults)增多时,透明大页会和普通 4k 页一样,产生同步内存压缩(direct compaction)操作,以节省内存。该操作是一个同步的内存整理操作,如果应用程序会短时间分配大量内存,内存压缩操作很可能会被触发,从而会对系统性能造成风险。https://yq.aliyun.com/articles/712830

1
2
3
4
5
6
7
#查看系统级别的 THP 使用情况,执行下列命令:
cat /proc/meminfo | grep AnonHugePages
#类似地,查看进程级别的 THP 使用情况,执行下列命令:
cat /proc/1730/smaps | grep AnonHugePages |grep -v "0 kB"
#是否开启了hugepage
$cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never

/proc/sys/vm/nr_hugepages 中存储的数据就是大页面的数量,虽然在默认情况下它的值都是 0,不过我们可以通过更改该文件的内容申请或者释放操作系统中的大页:

1
2
3
4
$ echo 1 > /proc/sys/vm/nr_hugepages
$ cat /proc/meminfo | grep HugePages_
HugePages_Total: 1
HugePages_Free: 1

THP和perf

thp on后比off性能稳定好 10-15%,开启THP最显著的指标是 iTLB命中率显著提升了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//on 419+E5-2682, thp never
9,145,128,732 branch-instructions # 229.068 M/sec (10.65%)
555,518,878 branch-misses # 6.07% of all branches (14.24%)
3,951,535,475 bus-cycles # 98.979 M/sec (14.29%)
372,477,068 cache-misses # 7.733 % of all cache refs (14.34%)
4,816,702,013 cache-references # 120.649 M/sec (14.36%)
114,521,174,305 cpu-cycles # 2.869 GHz (14.36%)
48,969,565,344 instructions # 0.43 insn per cycle (17.93%)
98,728,666,922 ref-cycles # 2472.967 M/sec (21.52%)
39,922.47 msec cpu-clock # 7.898 CPUs utilized
1,848,336,574 L1-dcache-load-misses # 13.31% of all L1-dcache hits (21.51%)
13,889,399,043 L1-dcache-loads # 347.903 M/sec (21.51%)
7,055,617,648 L1-dcache-stores # 176.730 M/sec (21.50%)
2,017,950,458 L1-icache-load-misses (21.50%)
88,802,885 LLC-load-misses # 9.86% of all LL-cache hits (14.35%)
900,379,398 LLC-loads # 22.553 M/sec (14.33%)
162,711,813 LLC-store-misses # 4.076 M/sec (7.13%)
419,869,955 LLC-stores # 10.517 M/sec (7.14%)
553,257,955 branch-load-misses # 13.858 M/sec (10.71%)
9,195,874,519 branch-loads # 230.339 M/sec (14.29%)
176,112,524 dTLB-load-misses # 1.28% of all dTLB cache hits (14.29%)
13,739,965,115 dTLB-loads # 344.160 M/sec (14.28%)
33,087,849 dTLB-store-misses # 0.829 M/sec (14.28%)
6,992,863,588 dTLB-stores # 175.158 M/sec (14.26%)
170,555,902 iTLB-load-misses # 107.90% of all iTLB cache hits (14.24%)
158,070,998 iTLB-loads # 3.959 M/sec (14.24%)

//on 419+E5-2682, thp always
12,958,974,094 branch-instructions # 227.392 M/sec (10.68%)
850,468,837 branch-misses # 6.56% of all branches (14.27%)
5,639,495,284 bus-cycles # 98.957 M/sec (14.29%)
526,744,798 cache-misses # 7.324 % of all cache refs (14.32%)
7,192,328,925 cache-references # 126.204 M/sec (14.34%)
163,419,436,811 cpu-cycles # 2.868 GHz (14.33%)
68,638,583,038 instructions # 0.42 insn per cycle (17.90%)
140,882,455,768 ref-cycles # 2472.076 M/sec (21.48%)
56,987.52 msec cpu-clock # 7.932 CPUs utilized
2,471,392,118 L1-dcache-load-misses # 12.69% of all L1-dcache hits (21.47%)
19,480,914,771 L1-dcache-loads # 341.833 M/sec (21.48%)
10,059,893,871 L1-dcache-stores # 176.522 M/sec (21.46%)
3,184,073,065 L1-icache-load-misses (21.46%)
128,467,945 LLC-load-misses # 10.83% of all LL-cache hits (14.31%)
1,186,653,892 LLC-loads # 20.822 M/sec (14.30%)
224,877,539 LLC-store-misses # 3.946 M/sec (7.15%)
628,574,746 LLC-stores # 11.030 M/sec (7.15%)
848,830,289 branch-load-misses # 14.894 M/sec (10.71%)
13,074,297,582 branch-loads # 229.416 M/sec (14.28%)
109,223,171 dTLB-load-misses # 0.56% of all dTLB cache hits (14.27%)
19,418,657,165 dTLB-loads # 340.741 M/sec (14.29%)
13,930,402 dTLB-store-misses # 0.244 M/sec (14.28%)
10,047,511,003 dTLB-stores # 176.305 M/sec (14.28%)
194,902,860 iTLB-load-misses # 61.23% of all iTLB cache hits (14.27%)
318,292,771 iTLB-loads # 5.585 M/sec (14.26%)

//on 310+8269 thp never
90,790,778 dTLB-load-misses # 0.67% of all dTLB cache hits (16.66%)
13,639,069,352 dTLB-loads (16.66%)
6,553,693 dTLB-store-misses (16.63%)
6,494,274,815 dTLB-stores (20.28%)
76,175,883 iTLB-load-misses # 40.53% of all iTLB cache hits (20.80%)
187,932,292 iTLB-loads (20.76%)

//on 310+8269 thp always
7,199,483,512 branch-instructions # 338.269 M/sec (11.46%)
81,893,729 branch-misses # 1.14% of all branches (14.95%)
532,919,206 bus-cycles # 25.039 M/sec (14.85%)
253,267,167 cache-misses # 11.507 % of all cache refs (14.81%)
2,201,001,946 cache-references # 103.414 M/sec (14.15%)
63,971,073,336 cpu-cycles # 3.006 GHz (14.55%)
37,214,341,673 instructions # 0.58 insns per cycle (18.09%)
52,209,823,072 ref-cycles # 2453.086 M/sec (17.23%)
1,098,964,315 L1-dcache-load-misses # 10.17% of all L1-dcache hits (14.22%)
10,808,109,191 L1-dcache-loads # 507.820 M/sec (14.31%)
5,092,652,478 L1-dcache-stores # 239.279 M/sec (14.38%)
4,338,580,209 L1-icache-load-misses # 203.849 M/sec (14.40%)
60,262,584 LLC-load-misses # 21.81% of all LL-cache hits (14.35%)
276,321,779 LLC-loads # 12.983 M/sec (14.31%)
62,982,184 LLC-store-misses # 2.959 M/sec (10.76%)
105,448,227 LLC-stores # 4.954 M/sec (8.08%)
81,163,187 branch-load-misses # 3.813 M/sec (11.67%)
7,111,481,940 branch-loads # 334.134 M/sec (14.37%)
4,527,406 dTLB-load-misses # 0.04% of all dTLB cache hits (14.30%)
10,726,725,791 dTLB-loads # 503.997 M/sec (17.33%)
1,066,097 dTLB-store-misses # 0.050 M/sec (17.37%)
5,090,008,144 dTLB-stores # 239.155 M/sec (17.34%)
18,715,797 iTLB-load-misses # 18.97% of all iTLB cache hits (17.33%)
98,684,189 iTLB-loads # 4.637 M/sec (14.29%)

MySQL 场景下代码大页对性能的影响

不只是数据可以用HugePage,代码段也可以开启HugePage, 无论在x86还是arm(arm下提升更明显)下,都可以得到大页优于透明大页,透明大页优于正常的4K page

收益:代码大页 > anon THP > 4k

arm下,对32core机器用32并发的sysbench来对比,代码大页带来的性能提升大概有11%,iTLB miss下降了10倍左右。

x86下,性能提升只有大概3-5%之间,iTLB miss下降了1.5-3倍左右。

TLAB miss高的案例

程序运行久了之后会变慢大概10%

刚开始运行的时候perf各项数据:

img

长时间运行后:

img

内存的利用以页为单位,当时分析认为,在此4k连续的基础上,页的碎片不应该对64 byte align的cache有什么影响。当时guest和host都没有开THP。

既然无法理解这个结果,那就只有按部就班的查看内核执行路径上各个函数的差别了,祭出ftrace:

1
2
3
4
5
6
echo kerel_func_name1 > /sys/kernel/debug/tracing/set_ftrace_filter

echo kerel_func_name2 > /sys/kernel/debug/tracing/set_ftrace_filter

echo kerel_func_name3 > /sys/kernel/debug/tracing/set_ftrace_filter
echo 1 > /sys/kernel/debug/tracing/function_profile_enabled

在CPU#20上执行代码:

taskset -c 20 ./b

代码执行完后:

1
2
echo 0 > /sys/kernel/debug/tracing/function_profile_enabled
cat /sys/kernel/debug/tracing/trace_stat/function20

这个时候就会打印出在各个函数上花费的时间,比如:

img

经过调试后,逐步定位到主要时间差距在 __mem_cgroup_commit_charge() (58%).

在阅读代码的过程中,注意到当前内核使能了CONFIG_SPARSEMEM_VMEMMAP=y

原因就是机器运行久了之后内存碎片化严重,导致TLAB Miss严重。

解决:开启THP后,性能稳定

碎片化

内存碎片严重的话会导致系统hang很久(回收、压缩内存)

尽量让系统的free多一点(比例高一点)可以调整 vm.min_free_kbytes(128G 以内 2G,256G以内 4G/8G), 线上机器直接修改vm.min_free_kbytes会触发回收,导致系统hang住 https://www.atatech.org/articles/163233 https://www.atatech.org/articles/97130

compact: 在进行 compcation 时,线程会从前往后扫描已使用的 movable page,然后从后往前扫描 free page,扫描结束后会把这些 movable page 给迁移到 free page 里,最终规整出一个 2M 的连续物理内存,这样 THP 就可以成功申请内存了。

image-20210628144121108

一次THP compact堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java          R  running task        0 144305 144271 0x00000080
ffff88096393d788 0000000000000086 ffff88096393d7b8 ffffffff81060b13
ffff88096393d738 ffffea003968ce50 000000000000000e ffff880caa713040
ffff8801688b0638 ffff88096393dfd8 000000000000fbc8 ffff8801688b0640

Call Trace:
[<ffffffff81060b13>] ? perf_event_task_sched_out+0x33/0x70
[<ffffffff8100bb8e>] ? apic_timer_interrupt+0xe/0x20
[<ffffffff810686da>] __cond_resched+0x2a/0x40
[<ffffffff81528300>] _cond_resched+0x30/0x40
[<ffffffff81169505>] compact_checklock_irqsave+0x65/0xd0
[<ffffffff81169862>] compaction_alloc+0x202/0x460
[<ffffffff811748d8>] ? buffer_migrate_page+0xe8/0x130
[<ffffffff81174b4a>] migrate_pages+0xaa/0x480
[<ffffffff81169660>] ? compaction_alloc+0x0/0x460 //compact and migrate
[<ffffffff8116a1a1>] compact_zone+0x581/0x950
[<ffffffff8116a81c>] compact_zone_order+0xac/0x100
[<ffffffff8116a951>] try_to_compact_pages+0xe1/0x120
[<ffffffff8112f1ba>] __alloc_pages_direct_compact+0xda/0x1b0
[<ffffffff8112f80b>] __alloc_pages_nodemask+0x57b/0x8d0
[<ffffffff81167b9a>] alloc_pages_vma+0x9a/0x150
[<ffffffff8118337d>] do_huge_pmd_anonymous_page+0x14d/0x3b0 //huge page
[<ffffffff8152a116>] ? rwsem_down_read_failed+0x26/0x30
[<ffffffff8114b350>] handle_mm_fault+0x2f0/0x300
[<ffffffff810ae950>] ? wake_futex+0x40/0x60
[<ffffffff8104a8d8>] __do_page_fault+0x138/0x480
[<ffffffff810097cc>] ? __switch_to+0x1ac/0x320
[<ffffffff81527910>] ? thread_return+0x4e/0x76e
[<ffffffff8152d45e>] do_page_fault+0x3e/0xa0 //page fault
[<ffffffff8152a815>] page_fault+0x25/0x30

查看pagetypeinfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#cat /proc/pagetypeinfo
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 0, zone DMA, type Unmovable 1 1 1 0 2 1 1 0 1 0 0
Node 0, zone DMA, type Reclaimable 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type Movable 0 0 0 0 0 0 0 0 0 1 3
Node 0, zone DMA, type Reserve 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Unmovable 89 144 98 42 21 14 5 2 1 0 1
Node 0, zone DMA32, type Reclaimable 28 22 9 8 0 0 0 0 0 1 7
Node 0, zone DMA32, type Movable 402 50 21 8 880 924 321 51 4 1 227
Node 0, zone DMA32, type Reserve 0 0 0 0 0 0 0 0 0 0 1
Node 0, zone DMA32, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Unmovable 13709 15231 6637 2646 816 181 46 4 4 1 0
Node 0, zone Normal, type Reclaimable 1 5 6 3293 1295 128 29 7 5 0 0
Node 0, zone Normal, type Movable 6396 1383350 1301956 1007627 670102 366248 160232 54894 13126 1482 37
Node 0, zone Normal, type Reserve 0 0 0 2 1 1 0 0 0 0 0
Node 0, zone Normal, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Reclaimable Movable Reserve CMA Isolate
Node 0, zone DMA 1 0 7 0 0 0
Node 0, zone DMA32 24 38 889 1 0 0
Node 0, zone Normal 1568 795 127683 2 0 0
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 1, zone Normal, type Unmovable 3938 8735 5469 3221 2097 989 202 6 0 0 0
Node 1, zone Normal, type Reclaimable 1 7 7 8 7 2 2 2 1 0 0
Node 1, zone Normal, type Movable 18623 1001037 2084894 1261484 631159 276096 87272 17169 1389 797 0
Node 1, zone Normal, type Reserve 0 0 0 8 0 0 0 0 0 0 0
Node 1, zone Normal, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 1, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Reclaimable Movable Reserve CMA Isolate
Node 1, zone Normal 1530 637 128903 2 0 0

每个zone都有自己的min low high,如下,但是单位是page, 计算案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
[root@jiangyi01.sqa.zmf /home/ahao.mah]
#cat /proc/zoneinfo |grep "Node"
Node 0, zone DMA
Node 0, zone DMA32
Node 0, zone Normal
Node 1, zone Normal

[root@jiangyi01.sqa.zmf /home/ahao.mah]
#cat /proc/zoneinfo |grep "Node 0, zone" -A10
Node 0, zone DMA
pages free 3975
min 20
low 25
high 30
scanned 0
spanned 4095
present 3996
managed 3975
nr_free_pages 3975
nr_alloc_batch 5
--
Node 0, zone DMA32
pages free 382873
min 2335
low 2918
high 3502
scanned 0
spanned 1044480
present 513024
managed 450639
nr_free_pages 382873
nr_alloc_batch 584
--
Node 0, zone Normal
pages free 11105097
min 61463
low 76828
high 92194
scanned 0
spanned 12058624
present 12058624
managed 11859912
nr_free_pages 11105097
nr_alloc_batch 12344

low = 5/4 * min
high = 3/2 * min

[root@jiangyi01.sqa.zmf /home/ahao.mah]
#T=min;sum=0;for i in `cat /proc/zoneinfo |grep $T | awk '{print $NF}'`;do sum=`echo "$sum+$i" |bc`;done;sum=`echo "$sum*4/1024" |bc`;echo "sum=${sum} MB"
sum=499 MB

[root@jiangyi01.sqa.zmf /home/ahao.mah]
#T=low;sum=0;for i in `cat /proc/zoneinfo |grep $T | awk '{print $NF}'`;do sum=`echo "$sum+$i" |bc`;done;sum=`echo "$sum*4/1024" |bc`;echo "sum=${sum} MB"
sum=624 MB

[root@jiangyi01.sqa.zmf /home/ahao.mah]
#T=high;sum=0;for i in `cat /proc/zoneinfo |grep $T | awk '{print $NF}'`;do sum=`echo "$sum+$i" |bc`;done;sum=`echo "$sum*4/1024" |bc`;echo "sum=${sum} MB"
sum=802 MB

内存碎片化导致rt升高的诊断

判定方法如下:

  1. 运行 sar -B 观察 pgscand/s,其含义为每秒发生的直接内存回收次数,当在一段时间内持续大于 0 时,则应继续执行后续步骤进行排查;
  2. 运行 cat /sys/kernel/debug/extfrag/extfrag_index 观察内存碎片指数,重点关注 order >= 3 的碎片指数,当接近 1.000 时,表示碎片化严重,当接近 0 时表示内存不足;
  3. 运行 cat /proc/buddyinfo, cat /proc/pagetypeinfo 查看内存碎片情况, 指标含义参考 (https://man7.org/linux/man-pages/man5/proc.5.html),同样关注 order >= 3 的剩余页面数量,pagetypeinfo 相比 buddyinfo 展示的信息更详细一些,根据迁移类型 (伙伴系统通过迁移类型实现反碎片化)进行分组,需要注意的是,当迁移类型为 Unmovable 的页面都聚集在 order < 3 时,说明内核 slab 碎片化严重,我们需要结合其他工具来排查具体原因,在本文就不做过多介绍了;
  4. 对于 CentOS 7.6 等支持 BPF 的 kernel 也可以运行我们研发的 drsnoop,compactsnoop 工具对延迟进行定量分析,使用方法和解读方式请参考对应文档;
  5. (Opt) 使用 ftrace 抓取 mm_page_alloc_extfrag 事件,观察因内存碎片从备用迁移类型“盗取”页面的信息。

​

参考资料

https://www.atatech.org/articles/66885

https://cloud.tencent.com/developer/article/1087455

https://www.cnblogs.com/xiaolincoding/p/13719610.html

如何在工作中学习--V2.0

发表于 2020-11-11 | 分类于 技巧

如何在工作中学习–V2.0

本文被网友翻译的英文版 (medium 需要梯子)

先说一件值得思考的事情:高考的时候大家都是一样的教科书,同一个教室,同样的老师辅导,时间精力基本差不多,可是最后别人考的是清华北大或者一本,而你的实力只能考个三本,为什么? 当然这里主要是智商的影响,那么其他因素呢?智商解决的问题能不能后天用其他方式来补位一下?

大家平时都看过很多方法论的文章,看的时候很爽觉得非常有用,但是一两周后基本还是老样子了。其中有很大一部分原因那些方法对脑力有要求、或者方法论比较空缺少落地的步骤。 下文中描述的方式方法是不需要智商也能学会的,非常具体的。

关键问题点

为什么你的知识积累不了?

有些知识看过就忘、忘了再看,实际碰到问题还是联系不上这个知识,这其实是知识的积累出了问题,没有深入的理解自然就不能灵活运用,也就谈不上解决问题了。这跟大家一起看相同的高考教科书但是高考结果不一样是一个原因。问题出在了理解上,每个人的理解能力不一样(智商),绝大多数人对知识的理解要靠不断地实践(做题)来巩固。

同样实践效果不一样?

同样工作一年碰到了10个问题(或者说做了10套高考模拟试卷),但是结果不一样,那是因为在实践过程中方法不够好。或者说你对你为什么做对了、为什么做错了没有去复盘

假如碰到一个问题,身边的同事解决了,而我解决不了。那么我就去想这个问题他是怎么解决的,他看到这个问题后的逻辑和思考是怎么样的,有哪些知识指导了他这么逻辑推理,这些知识哪些我也知道但是我没有想到这么去运用推理(说明我对这个知识理解的不到位导致灵活运用缺乏);这些知识中又有哪些是我不知道的(知识缺乏,没什么好说的快去Google什么学习下–有场景案例和目的加持,学习理解起来更快)。

等你把这个问题基本按照你同事掌握的知识和逻辑推理想明白后,需要再去琢磨一下他的逻辑推理解题思路中有没有不对的,有没有啰嗦的地方,有没有更直接的方式(对知识更好地运用)。

我相信每个问题都这么去实践的话就不应该再抱怨灵活运用、举一反三,同时知识也积累下来了,这种场景下积累到的知识是不会那么容易忘记的。

这就是向身边的牛人学习,同时很快超过他的办法。这就是为什么高考前你做了10套模拟题还不如其他人做一套的效果好

知识+逻辑 基本等于你的能力,知识让你知道那个东西,逻辑让你把东西和问题联系起来

这里的问题你可以理解成方案、架构、设计等

系统化的知识哪里来?

知识之间是可以联系起来的并且像一颗大树一样自我生长,但是当你都没理解透彻,自然没法产生联系,也就不能够自我生长了。

真正掌握好的知识点会慢慢生长连接最终组成一张大网

但是我们最容易陷入的就是掌握的深度、系统化(工作中碎片时间过多,学校里缺少时间)不够,所以一个知识点每次碰到花半个小时学习下来觉得掌握了,但是3个月后就又没印象了。总是感觉自己在懵懵懂懂中,或者一个领域学起来总是不得要领,根本的原因还是在于:宏观整体大图了解不够(缺乏体系,每次都是盲人摸象);关键知识点深度不够,理解不透彻,这些关键点就是这个领域的骨架、支点、抓手。缺了抓手自然不能生长,缺了宏观大图容易误入歧途。

我们有时候发现自己在某个领域学起来特别快,但是换个领域就总是不得要领,问题出在了上面,即使花再多时间也是徒然。这也就是为什么学霸看两个小时的课本比你看两天效果还好,感受下来还觉得别人好聪明,是不是智商比我高啊。

所以新进入一个领域的时候要去找他的大图和抓手。

好的同事总是能很轻易地把这个大图交给你,再顺便给你几个抓手,你就基本入门了,这就是培训的魅力,这种情况肯定比自学效率高多了。但是目前绝大部分的培训都做不到这点

好的逻辑又怎么来?

实践、复盘

img

讲个前同事的故事

有一个前同事是5Q过来的,负责技术(所有解决不了的问题都找他),这位同学从chinaren出道,跟着王兴一块创业5Q,5Q在学校靠鸡腿打下大片市场,最后被陈一舟的校内收购(据说被收购后5Q的好多技术都走了,最后王兴硬是呆在校内网把合约上的所有钱都拿到了)。这位同学让我最佩服的解决问题的能力,好多问题其实他也不一定就擅长,但是他就是有本事通过Help、Google不停地验证尝试就把一个不熟悉的问题给解决了,这是我最羡慕的能力,在后面的职业生涯中一直不停地往这个方面尝试。

应用刚启动连接到数据库的时候比较慢,但又不是慢查询

  1. 这位同学的解决办法是通过tcpdump来分析网络包,看网络包的时间戳和网络包的内容,然后找到了具体卡在了哪里。

  2. 如果是专业的DBA可能会通过show processlist 看具体连接在做什么,比如看到这些连接状态是 authentication 状态,然后再通过Google或者对这个状态的理解知道创建连接的时候MySQL需要反查IP、域名这里比较耗时,通过配置参数 skip-name-resolve 跳过去就好了。

  3. 如果是MySQL的老司机,一上来就知道连接慢的话跟 skip-name-resolve 关系最大。

    在我眼里这三种方式都解决了问题,最后一种最快但是纯靠积累和经验,换个问题也许就不灵了;第一种方式是最牛逼和通用的,只需要最少的知识就把问题解决了。

我当时跟着他从sudo、ls等linux命令开始学起。当然我不会轻易去打搅他问他,每次碰到问题我尽量让他在我的电脑上来操作,解决后我再自己复盘,通过history调出他的所有操作记录,看他在我的电脑上用Google搜啥了,然后一个个去学习分析他每个动作,去想他为什么搜这个关键字,复盘完还有不懂的再到他面前跟他面对面的讨论他为什么要这么做,指导他这么做的知识和逻辑又是什么。

有哪些好的行为帮你更好地掌握知识

笔记+写博客

看东西的时候要做笔记,要不当时看得再爽也很容易忘记,我们需要反复复习来加深印象和理解,复习的根据就是笔记(不可能再完整又看一次),笔记整理出里面的要点和你的盲点。

一段时间后把相关的笔记整理成一篇体系性的博客文章,这样既加深了理解又系统化了相关知识。以后再看到跟这篇博客相关的案例、知识点时不断地更新博客(完善你的知识点)

场景式学习、体感的来源、面对问题学习

前面提到的对知识的深入理解这有点空,如何才能做到深入理解?

举个学习TCP三次握手例子

经历稍微丰富点的工程师都觉得TCP三次握手看过很多次、很多篇文章了,但是文章写得再好似乎当时理解了,但是总是过几个月就忘了或者一看就懂,过一阵子被人一问就模模糊糊了,或者两个为什么就答不上了,自己都觉得自己的回答是在猜或者不确定

为什么会这样呢?而学其它知识就好通畅多了,我觉得这里最主要的是我们对TCP缺乏体感,比如没有几个工程师去看过TCP握手的代码,也没法想象真正的TCP握手是如何在电脑里运作的(打电话能给你一些类似的体感,但是细节覆盖面不够)。

如果这个时候你一边学习的时候一边再用wireshark抓包看看三次握手具体在干什么,比抽象的描述实在多了,你能看到具体握手的一来一回,并且看到一来一回带了哪些内容,这些内容又是用来做什么、为什么要带,这个时候你再去看别人讲解的理论顿时会觉得好理解多了,以后也很难忘记。

但是这里很多人执行能力不强,想去抓包,但是觉得要下载安装wireshark,要学习wireshark就放弃了。只看不动手当然是最舒适的,但是这个最舒适给了你在学习的假象,没有结果。

这是不是跟你要解决一个难题非常像,这个难题需要你去做很多事,比如下载源代码(翻不了墙,放弃);比如要编译(还要去学习那些编译参数,放弃);比如要搭建环境(太琐屑,放弃)。你看这中间九九八十一难你放弃了一难都取不了真经。这也是为什么同样学习、同样的问题,他能学会,他能解决,你不可以。

167211888bc4f2a368df3d16c68e6d51.png

再来看一个解决问题的例子

会员系统双11优化这个问题对我来说,我是个外来者,完全不懂这里面的部署架构、业务逻辑。但是在问题的关键地方(会员认为自己没问题–压力测试正常的;淘宝API更是认为自己没问题,alimonitor监控显示正常),结果就是会员的同学说我们没有问题,淘宝API肯定有问题,然后就不去思考自己这边可能出问题的环节了。思想上已经甩包了,那么即使再去review流程、环节也就不会那么仔细,自然更是发现不了问题了。

但是我的经验告诉我要有证据地甩包,或者说拿着证据优雅地甩包,这迫使我去找更多的细节证据(证据要给力哦,不能让人家拍回来)。如果我是这么说的,这个问题在淘宝API这里,你看理由是…………,我做了这些实验,看到了这些东东。那么淘宝API那边想要证明我的理由错了就会更积极地去找一些数据。

事实上我就是做这些实验找证据过程中发现了会员的问题,这就是态度、执行力、知识、逻辑能力综合下来拿到的一个结果。我最不喜欢的一句话就是我的程序没问题,因为我的逻辑是这样的,不会错的。你当然不会写你知道的错误逻辑,程序之所以有错误都是在你的逻辑、意料之外的东西。有很多次一堆人电话会议中扯皮的时候,我一般把电话静音了,直接上去人肉一个个过对方的逻辑,一般来说电话会议还没有结束我就给出来对方逻辑之外的东西。

钉子式学习方法和系统性学习方法

系统性学习方法就是想掌握MySQL,那么搞几本MySQL专著和MySQL 官方DOC看下来,一般课程设计的好的话还是比较容易掌握下来,绝大部分时候都是这种学习方法,可是在种学习方法的问题在于学完后当时看着似乎理解了,但是很容易忘记,一片一片地系统性的忘记,并且缺少应用能力(理解不深)。这是因为一般人对知识的理解没那么容易真正理解(掌握或者说应用)。

钉子式的学习方式,就是在一大片知识中打入几个桩,反复演练将这个桩不停地夯实,夯稳,做到在这个知识点上用通俗的语言跟小白都能讲明白,然后再这几个桩中间发散像星星之火燎原一样把整个一片知识都掌握下来。这种学习方法的缺点就是很难找到一片知识点的这个点,然后没有很好整合的话知识过于零散。

钉子式学习方法看着慢但是因为这样掌握的更透彻和牢固实际最终反而快。

我们常说的一个人很聪明,就是指系统性的看看书就都理解了,是真的理解那种,还能灵活运用,但是大多数普通人就不是这样的,看完书似乎理解了,实际几周后基本都忘记了,真正实践需要用的时候还是用不好。

举个Open-SSH的例子

为了做通 SSH 的免密登陆,大家都需要用到 ssh-keygen/ssh-copy-id, 如果我们把这两个命令当一个小的钉子的话,会去了解ssh-keygen做了啥(生成了密钥对),或者ssh-copy-id 的时候报错了(原来是需要秘钥对),然后将 ssh-keygen 生成的pub key复制到server的~/.ssh/authorized_keys 中。

然后你应该会对这个原理要有一些理解(更大的钉子),于是理解了密钥对,和ssh验证的流程,顺便学会怎么看ssh debug信息,那么接下来网络上各种ssh攻略、各种ssh卡顿的解决都是很简单的事情了。

比如你通过SSH可以解决这些问题:

  • 免密登陆
  • ssh卡顿
  • 怎么去掉ssh的时候需要手工多输入yes
  • 我的ssh怎么很快就断掉了
  • 我怎么样才能一次通过跳板机ssh到目标机器
  • 我怎么样通过ssh科学上网
  • 我的ansible(底层批量命令都是基于ssh)怎么这么多问题,到底是为什么
  • 我的git怎么报网络错误了
  • X11 forward我怎么配置不好
  • https为什么需要随机数加密,还需要签名
  • …………

这些问题都是一步步在扩大ssh的外延,让这个钉子变成一个巨大的桩。

然后就会学习到一些高级一些的ssh配置,比如干掉经常ssh的时候要yes一下(StrictHostKeyChecking=no), 或者怎么配置一下ssh就不会断线了(ServerAliveInterval=15),或者将 ssh跳板机->ssh server的过程做成 ssh server一步就可以了(ProxyCommand),进而发现用 ssh的ProxyCommand很容易科学上网了,或者git有问题的时候轻而易举地把ssh debug打开,对git进行debug了……

这基本都还是ssh的本质范围,像ansible、git在底层都是依赖ssh来通讯的,你会发现学、调试X11、ansible和git简直太容易了。

另外理解了ssh的秘钥对,也就理解了非对称加密,同时也很容易理解https流程(SSL),同时知道对称和非对称加密各自的优缺点,SSL为什么需要用到这两种加密算法了。

你看一个简单日常的知识我们只要沿着它用钉子精神,深挖细挖你就会发现知识之间的连接,这个小小的知识点成为你知识体系的一根结实的柱子。

我见过太多的老的工程师、年轻的工程师,天天在那里ssh 密码,ssh 跳板机,ssh 目标机,一小会ssh断了,重来一遍;或者ssh后卡住了,等吧……

在这个问题上表现得没有求知欲、没有探索精神、没有一次把问题搞定的魄力,所以就习惯了

空洞的口号

很多老师和文章都会教大家:举一反三、灵活运用、活学活用、多做多练。但是只有这些口号是没法落地的,落地的基本步骤就是前面提到的,却总是被忽视了。

什么是工程效率,什么是知识效率

有些人纯看理论就能掌握好一门技能,还能举一反三,这是知识效率,这种人非常少;

大多数普通人都是看点知识然后结合实践来强化理论,要经过反反复复才能比较好地掌握一个知识,这就是工程效率,讲究技巧、工具来达到目的。

肯定知识效率最牛逼,但是拥有这种技能的人毕竟非常少(天生的高智商吧)。从小我们周边那种不怎么学的学霸型基本都是这类,这种学霸都还能触类旁通非常快的掌握一个新知识,非常气人。剩下的绝大部分只能拼时间+方法+总结等也能掌握一些知识

非常遗憾我就是工程效率型,只能羡慕那些知识效率型的学霸。但是这事又不能独立看待有些人在某些方向上是工程效率型,有些方向就又是知识效率型(有一种知识效率型是你掌握的实在太多也就比较容易触类旁通了,这算灰色知识效率型)

使劲挖掘自己在知识效率型方面的能力吧,两者之间当然没有明显的界限,知识积累多了逻辑训练好了在别人看来你的智商就高了

知识分两种

一种是通用知识(不是说对所有人通用,而是说在一个专业领域去到哪个公司都能通用);另外一种是跟业务公司绑定的特定知识

通用知识没有任何疑问碰到后要非常饥渴地扑上去掌握他们(受益终生,这还有什么疑问吗?)。对于特定知识就要看你对业务需要掌握的深度了,肯定也是需要掌握一些的,特定知识掌握好的一般在公司里混的也会比较好。

一个具体知识体系里面又有一些核心知识点(抓手、essential knowledge),也就是掌握可以快速帮你膨胀、延伸到其他相关知识的知识点。

还有一些知识、工具一旦掌握就能帮你贯穿、具象、理解别的知识点,比如网络知识体系中的wireshark;理工科中的数学;知识体系中的学习方法、行为方式。我们要多去发现这些知识、工具(how?)

哪些品质能够决定一个人学习好坏

排在第一名的品质是复盘、总结能力

简单的说,这个能力就是这个孩子心里是否有个“小教练”,能够每次跳脱出当前任务,帮助自己分析,失败在哪里,成功在哪里,如何进阶,如何训练等等。

举几个例子:

  1. 如果写不出作文,这个“小教练”能告诉孩子,是没有素材,还是文字能力不强。

    如果是文字能力不强,应该如何训练(是造句,还是拆段落)

  2. 如果数学题做不出来,这个“小教练”能告诉孩子,我的弱点在哪里,哪个类型题我有重大问题,是因为哪里没有理解和打通。

有内化的“自我教练”,这个能力系数是1.67。也就是其他能力相当,学习效果可以翻1.67倍。

排在第二名的是建构能力。简单的说是逻辑推理,做事顺序等等

在内心“小教练”能把问题进行拆解之后,建构能力能把这些问题进行排序,应该怎么做更合理。

最终怎么把训练步骤整合。遇见一个问题,先干什么,再干什么等等。

有很强的顺序能力,系数为1.44。也就是其他能力相当,学习效果可以翻1.44倍。

排在第三名的能力是智商和过去成绩

这个毋庸置疑,聪明做事就会简单一些。平均效应系数为0.67。

也就是说,其他能力相当,智商高和过去成绩好,学习效果提升67%

智商重要程度应该比这里更高,但是实际高智商的太少,大多都是因为基础知识好给人产生了智商高的误解!

排在第四名的能力是自我驱动力

简单的说,知道自己为什么学习,能够自我鼓励,遇见失败能抗挫,有很强的心理驱动力。

平均效应系数为0.48。也就是说,其他能力相当,有自我驱动力的人,学习效果提升48%。

排在第五名的才是集中注意力

也就是说,注意力强。注意力对学习影响,并没有很多家长想象的那么大。

注意力的平均效应系数为0.44

也就是说,其他能力相当,注意力好的孩子,效果能提升44%

———总结一下——-

学习提升的个人因素:

自我分析,自我教练的元认知能力 》 逻辑排序与制定计划的建构能力》 智商和过去成绩 》自我驱动力》 集中注意力。

很多家长痴迷于“专注力”。当然专注力是一个效应量很强的学习力,但是从整体数据看,对学习的提升效果,仅仅排到第五名。

帮助孩子建立元认知能力和建构能力的培训,才能给他们对终身学习有帮助的技能包

以上缺少了对方法的落地执行能力的评估,实际这是影响最大的

为什么一直努力却没有结果

人生不是走斜坡,你持续走就可以走到巅峰;人生像走阶梯(有时候需要跳上台阶),每一阶有每一阶的难点,学物理有物理的难点,学漫画有漫画的难点,你没有克服难点,再怎么努力都是原地跳。所以当你克服难点,你跳上去就不会下来了。

这里的克服难点可以理解成真正掌握知识点,大多时候只是似是而非,所以一直在假忙碌、假学习,只有真正掌握后才像是上了个台阶。

这句话本身就是对知识的具象化理解,本来要描述的是有些人很容易达到目的、有些人则很难,我们很容易归结到天赋等原因,但是还是不能完全理解这个事情,知道天赋差异也不会帮助到我们跳楼梯,而这句话给出了一个形象的描述,一下子就会记住并在日常中尽量尝试跳上去和脱离假忙碌的状态。

image.png

在上述截图中,温伯格拿他自己玩弹子球游戏的经验,来描述”跃迁模式”。
  为啥在跃迁之前会出现一个【低谷】捏?温伯格认为:要想提升到一个新的 level,你需要放弃某些你擅长的技能,然后尝试你所【不】擅长的技能。
温伯格以他的弹子球游戏举例说:
为了得到更高的分数,他必须放弃以前熟悉的【低】难度技巧,转而尝试某种【高】难度技巧。在练习高难度技巧的过程中,他的分数会跌得比原先更低(也就是截图中下凹的低谷)。经过了一段时间的练习,当他掌握了高难度技巧,他的游戏得分就突然飞跃到一个新的 level。

举三反一--从理论知识到实际问题的推导

发表于 2020-11-02 | 分类于 TCP

举三反一–从理论知识到实际问题的推导

怎么样才能获取举三反一的秘籍, 普通人为什么要案例来深化对理论知识的理解。

什么是工程效率,什么是知识效率

有些人纯看理论就能掌握好一门技能,还能举三反一,这是知识效率,这种人非常少;

大多数普通人都是看点知识然后结合实践来强化理解理论,要经过反反复复才能比较好地掌握一个知识,这就是工程效率,讲究技巧、工具来达到目的。

对于费曼(参考费曼学习法)这样的聪明人就是很容易看到一个理论知识就能理解这个理论知识背后的本质。

肯定知识效率最牛逼,但是拥有这种能力的人毕竟非常少。从小我们周边那种不怎么学的学霸型基本都是这类,这种学霸都还能触类旁通非常快地掌握一个新知识。剩下的绝大部分只能拼时间(刷题)+方法+总结等也能掌握一些知识

但是这事又不能独立看待有些人在某些方向上是工程效率型,有些方向就又是知识效率型(有一种知识效率型是你掌握的实在太多也就比较容易触类旁通了,这算灰色知识效率型)

使劲挖掘自己在知识效率型方面的能力吧,即使灰色地带也行啊。

接下来看看TCP状态中的CLOSE_WAIT状态的含义

先看TCP连接状态图

这是网络、书本上凡是描述TCP状态一定会出现的状态图,理论上看这个图能解决任何TCP状态问题。

image.png

反复看这个图的右下部分的CLOSE_WAIT ,从这个图里可以得到如下结论:

CLOSE_WAIT是被动关闭端在等待应用进程的关闭

基本上这一结论要能帮助解决所有CLOSE_WAIT相关的问题,如果不能说明对这个知识点理解的不够。

server端大量close_wait案例

用实际案例来检查自己对CLOSE_WAIT 理论(CLOSE_WAIT是被动关闭端在等待应用进程的关闭)的掌握 – 能不能用这个结论来解决实际问题。同时也可以看看自己从知识到问题的推理能力(跟前面的知识效率呼应一下)。

问题描述:

服务端出现大量CLOSE_WAIT 个数正好 等于somaxconn(调整somaxconn大小后 CLOSE_WAIT 也会跟着变成一样的值)

根据这个描述先不要往下看,自己推理分析下可能的原因。

我的推理如下:

从这里看起来,client跟server成功建立了somaxconn个连接(somaxconn小于backlog,所以accept queue只有这么大),但是应用没有accept这个连接,导致这些连接一直在accept queue中。但是这些连接的状态已经是ESTABLISHED了,也就是client可以发送数据了,数据发送到server后OS ack了,并放在os的tcp buffer中,应用一直没有accept也就没法读取数据。client于是发送fin(可能是超时、也可能是简单发送数据任务完成了得结束连接),这时Server上这个连接变成了CLOSE_WAIT .

也就是从开始到结束这些连接都在accept queue中,没有被应用accept,很快他们又因为client 发送 fin 包变成了CLOSE_WAIT ,所以始终看到的是服务端出现大量CLOSE_WAIT 并且个数正好等于somaxconn(调整somaxconn后 CLOSE_WAIT 也会跟着变成一样的值)。

如下图所示,在连接进入accept queue后状态就是ESTABLISED了,也就是可以正常收发数据和fin了。client是感知不到server是否accept()了,只是发了数据后server的os代为保存在OS的TCP buffer中,因为应用没来取自然在CLOSE_WAIT 后应用也没有close(),所以一直维持CLOSE_WAIT 。

得检查server 应用为什么没有accept。

Recv-Q和Send-Q

如上是老司机的思路靠经验缺省了一些理论推理,缺省还是对理论理解不够, 这个分析抓住了 大量CLOSE_WAIT 个数正好 等于somaxconn(调整somaxconn后 CLOSE_WAIT 也会跟着变成一样的值)但是没有抓住 CLOSE_WAIT 背后的核心原因

更简单的推理

如果没有任何实战经验,只看上面的状态图的学霸应该是这样推理的:

看到server上有大量的CLOSE_WAIT说明client主动断开了连接,server的OS收到client 发的fin,并回复了ack,这个过程不需要应用感知,进而连接从ESTABLISHED进入CLOSE_WAIT,此时在等待server上的应用调用close连关闭连接(处理完所有收发数据后才会调close()) —- 结论:server上的应用一直卡着没有调close().

同时这里很奇怪的现象: 服务端出现大量CLOSE_WAIT 个数正好 等于somaxconn,进而可以猜测是不是连接建立后很快accept队列满了(应用也没有accept() ), 导致 大量CLOSE_WAIT 个数正好 等于somaxconn —- 结论: server 上的应用不但没有调close(), 连close() 前面必须调用 accept() 都一直卡着没调 (这个结论需要有accept()队列的理论知识)

从上面两个结论可以清楚地看到 server的应用卡住了

实际结论:

这个case的最终原因是因为OS的open files设置的是1024,达到了上限,进而导致server不能accept,但这个时候的tcp连接状态已经是ESTABLISHED了(这个状态变换是取决于内核收发包,跟应用是否accept()无关)。

同时从这里可以推断 netstat 即使看到一个tcp连接状态是ESTABLISHED也不能代表占用了 open files句柄。此时client可以正常发送数据了,只是应用服务在accept之前没法receive数据和close连接。

这个结论的图解如下:

image.png

假如全连接队列满了,握手第三步后对于client端来说是无法感知的,client端只需要回复ack后这个连接对于client端就是ESTABLISHED了,这时client是可以发送数据的。但是Server会扔掉收到的ack,回复syn+ack给client。

如果全连接队列没满,但是fd不够,那么在Server端这个Socket也是ESTABLISHED,但是只是暂存在全连接队列中,等待应用来accept,这个时候client端同样无法感知这个连接没有被accept,client是可以发送数据的,这个数据会保存在tcp receive memory buffer中,等到accept后再给应用。

如果自己无法得到上面的分析,那么再来看看如果把 CLOSE_WAIT 状态更细化地分析下(类似有老师帮你把知识点揉开跟实际案例联系下—-未必是上面的案例),看完后再来分析下上面的案例。

CLOSE_WAIT 状态拆解

通常,CLOSE_WAIT 状态在服务器停留时间很短,如果你发现大量的 CLOSE_WAIT 状态,那么就意味着被动关闭的一方没有及时发出 FIN 包,一般有如下几种可能:

  • 程序问题:如果代码层面忘记了 close 相应的 socket 连接,那么自然不会发出 FIN 包,从而导致 CLOSE_WAIT 累积;或者代码不严谨,出现死循环之类的问题,导致即便后面写了 close 也永远执行不到。
  • 响应太慢或者超时设置过小:如果连接双方不和谐,一方不耐烦直接 timeout,另一方却还在忙于耗时逻辑,就会导致 close 被延后。响应太慢是首要问题,不过换个角度看,也可能是 timeout 设置过小。
  • BACKLOG 太大:此处的 backlog 不是 syn backlog,而是 accept 的 backlog,如果 backlog 太大的话,设想突然遭遇大访问量的话,即便响应速度不慢,也可能出现来不及消费的情况,导致多余的请求还在队列里就被对方关闭了。

如果你通过「netstat -ant」或者「ss -ant」命令发现了很多 CLOSE_WAIT 连接,请注意结果中的「Recv-Q」和「Local Address」字段,通常「Recv-Q」会不为空,它表示应用还没来得及接收数据,而「Local Address」表示哪个地址和端口有问题,我们可以通过「lsof -i:」来确认端口对应运行的是什么程序以及它的进程号是多少。

如果是我们自己写的一些程序,比如用 HttpClient 自定义的蜘蛛,那么八九不离十是程序问题,如果是一些使用广泛的程序,比如 Tomcat 之类的,那么更可能是响应速度太慢或者 timeout 设置太小或者 BACKLOG 设置过大导致的故障。

看完这段 CLOSE_WAIT 更具体深入点的分析后再来分析上面的案例看看,能否推导得到正确的结论。

一些疑问

连接都没有被accept(), client端就能发送数据了?

答:是的。只要这个连接在OS看来是ESTABLISHED的了就可以,因为握手、接收数据都是由内核完成的,内核收到数据后会先将数据放在内核的tcp buffer中,然后os回复ack。另外三次握手之后client端是没法知道server端是否accept()了。

CLOSE_WAIT与accept queue有关系吗?

答:没有关系。只是本案例中因为open files不够了,影响了应用accept(), 导致accept queue满了,同时因为即使应用不accept(三次握手后,server端是否accept client端无法感知),client也能发送数据和发 fin断连接,这些响应都是os来负责,跟上层应用没关系,连接从握手到ESTABLISHED再到CLOSE_WAIT都不需要fd,也不需要应用参与。CLOSE_WAIT只跟应用不调 close() 有关系。

CLOSE_WAIT与accept queue为什么刚好一致并且联动了?

答:这里他们的数量刚好一致是因为所有新建连接都没有accept,堵在queue中。同时client发现问题后把所有连接都fin了,也就是所有queue中的连接从来没有被accept过,但是他们都是ESTABLISHED,过一阵子之后client端发了fin所以所有accept queue中的连接又变成了 CLOSE_WAIT, 所以二者刚好一致并且联动了

openfiles和accept()的关系是?

答:accept()的时候才会创建文件句柄,消耗openfiles

一个连接如果在accept queue中了,但是还没有被应用 accept,那么这个时候在server上看这个连接的状态他是ESTABLISHED的吗?

答:是

如果server的os参数 open files到了上限(就是os没法打开新的文件句柄了)会导致这个accept queue中的连接一直没法被accept对吗?

答:对

如果通过gdb attach 应用进程,故意让进程accept,这个时候client还能连上应用吗?

答: 能,这个时候在client和server两边看到的连接状态都是 ESTABLISHED,只是Server上的全连接队列占用加1。连接握手并切换到ESTABLISHED状态都是由OS来负责的,应用不参与,ESTABLISHED后应用才能accept,进而收发数据。也就是能放入到全连接队列里面的连接肯定都是 ESTABLISHED 状态的了

接着上面的问题,如果新连接继续连接进而全连接队列满了呢?

答:那就连不上了,server端的OS因为全连接队列满了直接扔掉第一个syn握手包,这个时候连接在client端是SYN_SENT,Server端没有这个连接,这是因为syn到server端就直接被OS drop 了。

1
2
3
//如下图,本机测试,只有一个client端发起的syn_send, 3306的server端没有任何连接
$netstat -antp |grep -i 127.0.0.1:3306
tcp 0 1 127.0.0.1:61106 127.0.0.1:3306 SYN_SENT 21352/telnet

能进入到accept queue中的连接都是 ESTABLISHED,不管用户态有没有accept,用户态accept后队列大小减1

如果一个连接握手成功进入到accept queue但是应用accept前被对方RESET了呢?

答: 如果此时收到对方的RESET了,那么OS会释放这个连接。但是内核认为所有 listen 到的连接, 必须要 accept 走, 因为用户有权利知道有过这么一个连接存在过。所以OS不会到全连接队列拿掉这个连接,全连接队列数量也不会减1,直到应用accept这个连接,然后read/write才发现这个连接断开了,报communication failure异常

什么时候连接状态变成 ESTABLISHED

三次握手成功就变成 ESTABLISHED 了,不需要用户态来accept,如果握手第三步的时候OS发现全连接队列满了,这时OS会扔掉这个第三次握手ack,并重传握手第二步的syn+ack, 在OS端这个连接还是 SYN_RECV 状态的,但是client端是 ESTABLISHED状态的了。

这是在4000(tearbase)端口上全连接队列没满,但是应用不再accept了,nc用12346端口去连4000(tearbase)端口的结果

1
2
3
4
5
6
# netstat -at |grep ":12346 "
tcp 0 0 dcep-blockchain-1:12346 dcep-blockchai:terabase ESTABLISHED //server
tcp 0 0 dcep-blockchai:terabase dcep-blockchain-1:12346 ESTABLISHED //client
[root@dcep-blockchain-1 cfl-sm2-sm3]# ss -lt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 73 1024 *:terabase *:*

这是在4000(tearbase)端口上全连接队列满掉后,nc用12346端口去连4000(tearbase)端口的结果

1
2
3
4
5
6
# netstat -at |grep ":12346 "  
tcp 0 0 dcep-blockchai:terabase dcep-blockchain-1:12346 SYN_RECV //server
tcp 0 0 dcep-blockchain-1:12346 dcep-blockchai:terabase ESTABLISHED //client
# ss -lt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 1025 1024 *:terabase *:*

kubernetes service 和 kube-proxy详解

发表于 2020-09-22 | 分类于 docker

kubernetes service 和 kube-proxy详解

service 是Kubernetes里面非常重要的一个功能,用以解决负载均衡、弹性伸缩、升级灰度等等

本文先从概念介绍到实际负载均衡运转过程中追踪每个环节都做哪些处理,同时这些包会相应地怎么流转最终到达目标POD,以阐明service工作原理以及kube-proxy又在这个过程中充当了什么角色。

service 模式

根据创建Service的type类型不同,可分成4种模式:

  • ClusterIP: 默认方式。根据是否生成ClusterIP又可分为普通Service和Headless Service两类:
    • 普通Service:通过为Kubernetes的Service分配一个集群内部可访问的固定虚拟IP(Cluster IP),实现集群内的访问。为最常见的方式。
    • Headless Service:该服务不会分配Cluster IP,也不通过kube-proxy做反向代理和负载均衡。而是通过DNS提供稳定的网络ID来访问,DNS会将headless service的后端直接解析为podIP列表。主要供StatefulSet中对应POD的序列用。
  • NodePort:除了使用Cluster IP之外,还通过将service的port映射到集群内每个节点的相同一个端口,实现通过nodeIP:nodePort从集群外访问服务。NodePort会RR转发给后端的任意一个POD,跟ClusterIP类似
  • LoadBalancer:和nodePort类似,不过除了使用一个Cluster IP和nodePort之外,还会向所使用的公有云申请一个负载均衡器,实现从集群外通过LB访问服务。在公有云提供的 Kubernetes 服务里,都使用了一个叫作 CloudProvider 的转接层,来跟公有云本身的 API 进行对接。所以,在上述 LoadBalancer 类型的 Service 被提交后,Kubernetes 就会调用 CloudProvider 在公有云上为你创建一个负载均衡服务,并且把被代理的 Pod 的 IP 地址配置给负载均衡服务做后端。
  • ExternalName:是 Service 的特例。此模式主要面向运行在集群外部的服务,通过它可以将外部服务映射进k8s集群,且具备k8s内服务的一些特征(如具备namespace等属性),来为集群内部提供服务。此模式要求kube-dns的版本为1.7或以上。这种模式和前三种模式(除headless service)最大的不同是重定向依赖的是dns层次,而不是通过kube-proxy。

service yaml案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
name: nginx-ren
labels:
app: web
spec:
type: NodePort
# clusterIP: None
ports:
- port: 8080
targetPort: 80
nodePort: 30080
selector:
app: ren

ports 字段指定服务的端口信息:

  • port:虚拟 ip 要绑定的 port,每个 service 会创建出来一个虚拟 ip,通过访问 vip:port 就能获取服务的内容。这个 port 可以用户随机选取,因为每个服务都有自己的 vip,也不用担心冲突的情况
  • targetPort:pod 中暴露出来的 port,这是运行的容器中具体暴露出来的端口,一定不能写错–一般用name来代替具体的port
  • protocol:提供服务的协议类型,可以是 TCP 或者 UDP
  • nodePort: 仅在type为nodePort模式下有用,宿主机暴露端口

nodePort和loadbalancer可以被外部访问,loadbalancer需要一个外部ip,流量走外部ip进出

NodePort向外部暴露了多个宿主机的端口,外部可以部署负载均衡将这些地址配置进去。

默认情况下,服务会rr转发到可用的后端。如果希望保持会话(同一个 client 永远都转发到相同的 pod),可以把 service.spec.sessionAffinity 设置为 ClientIP。

Service和kube-proxy的工作原理

kube-proxy有两种主要的实现(userspace基本没有使用了):

  • iptables来做NAT以及负载均衡(默认方案)
  • ipvs来做NAT以及负载均衡

Service 是由 kube-proxy 组件通过监听 Pod 的变化事件,在宿主机上维护iptables规则或者ipvs规则。

Kube-proxy 主要监听两个对象,一个是 Service,一个是 Endpoint,监听他们启停。以及通过selector将他们绑定。

IPVS 是专门为LB设计的。它用hash table管理service,对service的增删查找都是*O(1)*的时间复杂度。不过IPVS内核模块没有SNAT功能,因此借用了iptables的SNAT功能。IPVS 针对报文做DNAT后,将连接信息保存在nf_conntrack中,iptables据此接力做SNAT。该模式是目前Kubernetes网络性能最好的选择。但是由于nf_conntrack的复杂性,带来了很大的性能损耗。

iptables 实现负载均衡的工作流程

如果kube-proxy不是用的ipvs模式,那么主要靠iptables来做DNAT和SNAT以及负载均衡

iptables+clusterIP工作流程:

  1. 集群内访问svc 10.10.35.224:3306 命中 kube-services iptables(两条规则,宿主机、以及pod内)
  2. iptables 规则:KUBE-SEP-F4QDAAVSZYZMFXZQ 对应到 KUBE-SEP-F4QDAAVSZYZMFXZQ
  3. KUBE-SEP-F4QDAAVSZYZMFXZQ 指示 DNAT到 宿主机:192.168.0.83:10379(在内核中将包改写了ip port)
  4. 从svc description中可以看到这个endpoint的地址 192.168.0.83:10379(pod使用Host network)

image.png

iptables规则解析如下(case不一样,所以看到的端口、ip都不一样):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-t nat -A {PREROUTING, OUTPUT} -m conntrack --ctstate NEW -j KUBE-SERVICES

# 宿主机访问 nginx Service 的流量,同时满足 4 个条件:
# 1. src_ip 不是 Pod 网段
# 2. dst_ip=3.3.3.3/32 (ClusterIP)
# 3. proto=TCP
# 4. dport=80
# 如果匹配成功,直接跳转到 KUBE-MARK-MASQ;否则,继续匹配下面一条(iptables 是链式规则,高优先级在前)
# 跳转到 KUBE-MARK-MASQ 是为了保证这些包出宿主机时,src_ip 用的是宿主机 IP。
-A KUBE-SERVICES ! -s 1.1.0.0/16 -d 3.3.3.3/32 -p tcp -m tcp --dport 80 -j KUBE-MARK-MASQ
# Pod 访问 nginx Service 的流量:同时满足 4 个条件:
# 1. 没有匹配到前一条的,(说明 src_ip 是 Pod 网段)
# 2. dst_ip=3.3.3.3/32 (ClusterIP)
# 3. proto=TCP
# 4. dport=80
-A KUBE-SERVICES -d 3.3.3.3/32 -p tcp -m tcp --dport 80 -j KUBE-SVC-NGINX

# 以 50% 的概率跳转到 KUBE-SEP-NGINX1
-A KUBE-SVC-NGINX -m statistic --mode random --probability 0.50 -j KUBE-SEP-NGINX1
# 如果没有命中上面一条,则以 100% 的概率跳转到 KUBE-SEP-NGINX2
-A KUBE-SVC-NGINX -j KUBE-SEP-NGINX2

# 如果 src_ip=1.1.1.1/32,说明是 Service->client 流量,则
# 需要做 SNAT(MASQ 是动态版的 SNAT),替换 src_ip -> svc_ip,这样客户端收到包时,
# 看到就是从 svc_ip 回的包,跟它期望的是一致的。
-A KUBE-SEP-NGINX1 -s 1.1.1.1/32 -j KUBE-MARK-MASQ
# 如果没有命令上面一条,说明 src_ip != 1.1.1.1/32,则说明是 client-> Service 流量,
# 需要做 DNAT,将 svc_ip -> pod1_ip,
-A KUBE-SEP-NGINX1 -p tcp -m tcp -j DNAT --to-destination 1.1.1.1:80
# 同理,见上面两条的注释
-A KUBE-SEP-NGINX2 -s 1.1.1.2/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-NGINX2 -p tcp -m tcp -j DNAT --to-destination 1.1.1.2:80

image.png

在对应的宿主机上可以清楚地看到容器中的mysqld进程正好监听着 10379端口

1
2
3
4
5
6
7
8
[root@az1-drds-83 ~]# ss -lntp |grep 10379
LISTEN 0 128 :::10379 :::* users:(("mysqld",pid=17707,fd=18))
[root@az1-drds-83 ~]# ps auxff | grep 17707 -B2
root 13606 0.0 0.0 10720 3764 ? Sl 17:09 0:00 \_ containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/ead57b52b11902b9b5004db0b72abb060b56a1af7ee7ad7066bd09c946abcb97 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc

root 13624 0.0 0.0 103044 10424 ? Ss 17:09 0:00 | \_ python /entrypoint.py
root 14835 0.0 0.0 11768 1636 ? S 17:10 0:00 | \_ /bin/sh /u01/xcluster/bin/mysqld_safe --defaults-file=/home/mysql/my10379.cnf
alidb 17707 0.6 0.0 1269128 67452 ? Sl 17:10 0:25 | \_ /u01/xcluster_20200303/bin/mysqld --defaults-file=/home/mysql/my10379.cnf --basedir=/u01/xcluster_20200303 --datadir=/home/mysql/data10379/dbs10379 --plugin-dir=/u01/xcluster_20200303/lib/plugin --user=mysql --log-error=/home/mysql/data10379/mysql/master-error.log --open-files-limit=8192 --pid-file=/home/mysql/data10379/dbs10379/az1-drds-83.pid --socket=/home/mysql/data10379/tmp/mysql.sock --port=10379

对应的这个pod的description:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#kubectl describe pod apsaradbcluster010-cv6w
Name: apsaradbcluster010-cv6w
Namespace: default
Priority: 0
Node: az1-drds-83/192.168.0.83
Start Time: Thu, 10 Sep 2020 17:09:33 +0800
Labels: alisql.clusterName=apsaradbcluster010
alisql.pod_name=apsaradbcluster010-cv6w
alisql.pod_role=leader
Annotations: apsara.metric.pod_name: apsaradbcluster010-cv6w
Status: Running
IP: 192.168.0.83
IPs:
IP: 192.168.0.83
Controlled By: ApsaradbCluster/apsaradbcluster010
Containers:
engine:
Container ID: docker://ead57b52b11902b9b5004db0b72abb060b56a1af7ee7ad7066bd09c946abcb97
Image: reg.docker.alibaba-inc.com/apsaradb/alisqlcluster-engine:develop-20200910140415
Image ID: docker://sha256:7ad5cc53c87b34806eefec829d70f5f0192f4127c7ee4e867cb3da3bb6c2d709
Ports: 10379/TCP, 20383/TCP, 46846/TCP
Host Ports: 10379/TCP, 20383/TCP, 46846/TCP
State: Running
Started: Thu, 10 Sep 2020 17:09:35 +0800
Ready: True
Restart Count: 0
Environment:
ALISQL_POD_NAME: apsaradbcluster010-cv6w (v1:metadata.name)
ALISQL_POD_PORT: 10379
Mounts:
/dev/shm from devshm (rw)
/etc/localtime from etclocaltime (rw)
/home/mysql/data from data-dir (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-n2bmn (ro)
exporter:
Container ID: docker://b49865b7798f9036b431203d54994ac8fdfcadacb01a2ab4494b13b2681c482d
Image: reg.docker.alibaba-inc.com/apsaradb/alisqlcluster-exporter:latest
Image ID: docker://sha256:432cdd0a0e7c74c6eb66551b6f6af9e4013f60fb07a871445755f6577b44da19
Port: 47272/TCP
Host Port: 47272/TCP
Args:
--web.listen-address=:47272
--collect.binlog_size
--collect.engine_innodb_status
--collect.info_schema.innodb_metrics
--collect.info_schema.processlist
--collect.info_schema.tables
--collect.info_schema.tablestats
--collect.slave_hosts
State: Running
Started: Thu, 10 Sep 2020 17:09:35 +0800
Ready: True
Restart Count: 0
Environment:
ALISQL_POD_NAME: apsaradbcluster010-cv6w (v1:metadata.name)
DATA_SOURCE_NAME: root:@(127.0.0.1:10379)/
Mounts:
/dev/shm from devshm (rw)
/etc/localtime from etclocaltime (rw)
/home/mysql/data from data-dir (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-n2bmn (ro)

DNAT 规则的作用,就是在 PREROUTING 检查点之前,也就是在路由之前,将流入 IP 包的目的地址和端口,改成–to-destination 所指定的新的目的地址和端口。可以看到,这个目的地址和端口,正是被代理 Pod 的 IP 地址和端口。

如下是一个iptables来实现service的案例中的iptables流量分配规则:

三个pod,每个pod承担三分之一的流量

1
2
3
4
5
6
7
8
9
10
iptables-save | grep 3306

iptables-save | grep KUBE-SERVICES

#iptables-save |grep KUBE-SVC-RVEVH2XMONK6VC5O
:KUBE-SVC-RVEVH2XMONK6VC5O - [0:0]
-A KUBE-SERVICES -d 10.10.70.95/32 -p tcp -m comment --comment "drds/mysql-read:mysql cluster IP" -m tcp --dport 3306 -j KUBE-SVC-RVEVH2XMONK6VC5O
-A KUBE-SVC-RVEVH2XMONK6VC5O -m comment --comment "drds/mysql-read:mysql" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-XC4TZYIZFYB653VI
-A KUBE-SVC-RVEVH2XMONK6VC5O -m comment --comment "drds/mysql-read:mysql" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-MK4XPBZUIJGFXKED
-A KUBE-SVC-RVEVH2XMONK6VC5O -m comment --comment "drds/mysql-read:mysql" -j KUBE-SEP-AAYXWGQJBDHUJUQ3

到这里我们基本可以看到,利用iptables规则,宿主机内核把发到宿主机上的流量按照iptables规则做dnat后发给service后端的pod,同时iptables规则可以配置每个pod的流量大小。再辅助kube-proxy监听pod的起停和健康状态并相应地更新iptables规则,这样整个service实现逻辑就很清晰了。

看起来 service 是个完美的方案,可以解决服务访问的所有问题,但是 service 这个方案(iptables 模式)也有自己的缺点。

首先,如果转发的 pod 不能正常提供服务,它不会自动尝试另一个 pod,当然这个可以通过 readiness probes 来解决。每个 pod 都有一个健康检查的机制,当有 pod 健康状况有问题时,kube-proxy 会删除对应的转发规则。

另外,nodePort 类型的服务也无法添加 TLS 或者更复杂的报文路由机制。因为只做了NAT

ipvs 实现负载均衡的原理

ipvs模式下,kube-proxy会先创建虚拟网卡,kube-ipvs0下面的每个ip都对应着svc的一个clusterIP:

1
2
3
4
5
6
# ip addr
...
5: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether de:29:17:2a:8d:79 brd ff:ff:ff:ff:ff:ff
inet 10.68.70.130/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever

kube-ipvs0下面绑的这些ip就是在发包的时候让内核知道如果目标ip是这些地址的话,这些地址是自身的所以包不会发出去,而是给INPUT链,这样ipvs内核模块有机会改写包做完NAT后再发走。

ipvs会放置DNAT钩子在INPUT链上,因此必须要让内核识别 VIP 是本机的 IP。这样才会过INPUT 链,要不然就通过OUTPUT链出去了。k8s 通过kube-proxy将service cluster ip 绑定到虚拟网卡kube-ipvs0。

同时在路由表中增加一些ipvs 的路由条目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ip route show table local  //等于ip route list table local
local 10.68.0.1 dev kube-ipvs0 proto kernel scope host src 10.68.0.1
local 10.68.0.2 dev kube-ipvs0 proto kernel scope host src 10.68.0.2
local 10.68.70.130 dev kube-ipvs0 proto kernel scope host src 10.68.70.130 -- ipvs
broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1
broadcast 172.17.0.0 dev docker0 proto kernel scope link src 172.17.0.1
local 172.17.0.1 dev docker0 proto kernel scope host src 172.17.0.1
broadcast 172.17.255.255 dev docker0 proto kernel scope link src 172.17.0.1
local 172.20.185.192 dev tunl0 proto kernel scope host src 172.20.185.192
broadcast 172.20.185.192 dev tunl0 proto kernel scope link src 172.20.185.192
broadcast 172.26.128.0 dev eth0 proto kernel scope link src 172.26.137.117
local 172.26.137.117 dev eth0 proto kernel scope host src 172.26.137.117
broadcast 172.26.143.255 dev eth0 proto kernel scope link src 172.26.137.117

//访问本机IP(不是 127.0.0.1),内核在路由项查找的时候判断类型是 RTN_LOCAL,仍然会使用 net->loopback_dev。也就是 lo 虚拟网卡。

而接下来,kube-proxy 就会通过 Linux 的 IPVS 模块,为这个 IP 地址设置三个 IPVS 虚拟主机,并设置这三个虚拟主机之间使用轮询模式 (rr) 来作为负载均衡策略。我们可以通过 ipvsadm 查看到这个设置,如下所示:

1
2
3
4
5
ipvsadm -ln |grep 10.68.114.131 -A5
TCP 10.68.114.131:3306 rr
-> 172.20.120.143:3306 Masq 1 0 0
-> 172.20.185.209:3306 Masq 1 0 0
-> 172.20.248.143:3306 Masq 1 0 0

172.20.. 是后端真正pod的ip, 10.68.114.131 是cluster-ip.

完整的工作流程如下:

  1. 因为service cluster ip 绑定到虚拟网卡kube-ipvs0上,内核可以识别访问的 VIP 是本机的 IP.
  2. 数据包到达INPUT链.
  3. ipvs监听到达input链的数据包,比对数据包请求的服务是为集群服务,修改数据包的目标IP地址为对应pod的IP,然后将数据包发至POSTROUTING链.
  4. 数据包经过POSTROUTING链选路由后,将数据包通过tunl0网卡(calico网络模型)发送出去。从tunl0虚拟网卡获得源IP.
  5. 经过tunl0后进行ipip封包,丢到物理网络,路由到目标node(目标pod所在的node)
  6. 目标node进行ipip解包后给pod对应的网卡
  7. pod接收到请求之后,构建响应报文,改变源地址和目的地址,返回给客户端。

image.png

ipvs实际案例

ipvs负载均衡下一次完整的syn握手抓包。

宿主机上访问 curl clusterip+port 后因为这个ip绑定在kube-ipvs0上,本来是应该发出去的包(prerouting)但是内核认为这个包是访问自己,于是给INPUT链,接着被ipvs放置在INPUT中的DNAT钩子勾住,将dest ip根据负载均衡逻辑改成pod-ip,然后将数据包再发至POSTROUTING链。这时因为目标ip是POD-IP了,根据ip route 选择到出口网卡是tunl0。

可以看下内核中的路由规则:

1
2
3
4
5
# ip route get 10.68.70.130
local 10.68.70.130 dev lo src 10.68.70.130 //这条规则指示了clusterIP是发给自己的
cache <local>
# ip route get 172.20.185.217
172.20.185.217 via 172.26.137.117 dev tunl0 src 172.20.22.192 //这条规则指示clusterIP替换成POD IP后发给本地tunl0做ipip封包

于是cip变成了tunl0的IP,这个tunl0是ipip模式,于是将这个包打包成ipip,也就是外层sip、dip都是宿主机ip,再将这个包丢入到物理网络

网络收包到达内核后的处理流程如下,核心都是查路由表,出包也会查路由表(判断是否本机内部通信,或者外部通信的话需要选用哪个网卡)

ipvs的一些分析

ipvs是一个内核态的四层负载均衡,支持NAT以及IPIP隧道模式,但LB和RS不能跨子网,IPIP性能次之,通过ipip隧道解决跨网段传输问题,因此能够支持跨子网。而NAT模式没有限制,这也是唯一一种支持端口映射的模式。

但是ipvs只有NAT(也就是DNAT),NAT也俗称三角模式,要求RS和LVS 在一个二层网络,并且LVS是RS的网关,这样回包一定会到网关,网关再次做SNAT,这样client看到SNAT后的src ip是LVS ip而不是RS-ip。默认实现不支持ful-NAT,所以像公有云厂商为了适应公有云场景基本都会定制实现ful-NAT模式的lvs。

我们不难猜想,由于Kubernetes Service需要使用端口映射功能,因此kube-proxy必然只能使用ipvs的NAT模式。

如下Masq表示MASQUERADE(也就是SNAT),跟iptables里面的 MASQUERADE 是一个意思

1
2
3
# ipvsadm -L -n  |grep 70.130 -A12
TCP 10.68.70.130:12380 rr
-> 172.20.185.217:9376 Masq 1 0 0

为什么clusterIP不能ping通

集群内访问cluster ip(不能ping,只能cluster ip+port)就是在到达网卡之前被内核iptalbes做了dnat/snat, cluster IP是一个虚拟ip,可以针对具体的服务固定下来,这样服务后面的pod可以随便变化。

iptables模式的svc会ping不通clusterIP,可以看如下iptables和route(留意:–reject-with icmp-port-unreachable):

1
2
3
4
5
6
7
8
9
10
11
12
#ping 10.96.229.40
PING 10.96.229.40 (10.96.229.40) 56(84) bytes of data.
^C
--- 10.96.229.40 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 999ms

#iptables-save |grep 10.96.229.40
-A KUBE-SERVICES -d 10.96.229.40/32 -p tcp -m comment --comment "***-service:https has no endpoints" -m tcp --dport 8443 -j REJECT --reject-with icmp-port-unreachable

#ip route get 10.96.229.40
10.96.229.40 via 11.164.219.253 dev eth0 src 11.164.219.119
cache

如果用ipvs实现的clusterIP是可以ping通的:

  • 如果用iptables 来做转发是ping不通的,因为iptables里面这条规则只处理tcp包,reject了icmp
  • ipvs实现的clusterIP都能ping通
  • ipvs下的clusterIP ping通了也不是转发到pod,ipvs负载均衡只转发tcp协议的包
  • ipvs 的clusterIP在本地配置了route路由到回环网卡,这个包是lo网卡回复的

ipvs实现的clusterIP,在本地有添加路由到lo网卡

image.png

然后在本机抓包(到ipvs后端的pod上抓不到icmp包):

image.png

从上面可以看出显然ipvs只会转发tcp包到后端pod,所以icmp包不会通过ipvs转发到pod上,同时在本地回环网卡lo上抓到了进去的icmp包。因为本地添加了一条路由规则,目标clusterIP被指示发到lo网卡上,lo网卡回复了这个ping包,所以通了。

NodePort Service

这种类型的 Service 也能被宿主机和 pod 访问,但与 ClusterIP 不同的是,它还能被 集群外的服务访问。

  • External node IP + port in NodePort range to any endpoint (pod), e.g. 10.0.0.1:31000
  • Enables access from outside

实现上,kube-apiserver 会从预留的端口范围内分配一个端口给 Service,然后 每个宿主机上的 kube-proxy 都会创建以下规则:

1
2
3
4
5
6
7
8
9
10
11
12
-t nat -A {PREROUTING, OUTPUT} -m conntrack --ctstate NEW -j KUBE-SERVICES

-A KUBE-SERVICES ! -s 1.1.0.0/16 -d 3.3.3.3/32 -p tcp -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 3.3.3.3/32 -p tcp -m tcp --dport 80 -j KUBE-SVC-NGINX
# 如果前面两条都没匹配到(说明不是 ClusterIP service 流量),并且 dst 是 LOCAL,跳转到 KUBE-NODEPORTS
-A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS

-A KUBE-NODEPORTS -p tcp -m tcp --dport 31000 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m tcp --dport 31000 -j KUBE-SVC-NGINX

-A KUBE-SVC-NGINX -m statistic --mode random --probability 0.50 -j KUBE-SEP-NGINX1
-A KUBE-SVC-NGINX -j KUBE-SEP-NGINX2
  1. 前面几步和 ClusterIP Service 一样;如果没匹配到 ClusterIP 规则,则跳转到 KUBE-NODEPORTS chain。
  2. KUBE-NODEPORTS chain 里做 Service 匹配,但这次只匹配协议类型和目的端口号。
  3. 匹配成功后,转到对应的 KUBE-SVC- chain,后面的过程跟 ClusterIP 是一样的。

NodePort 的一些问题

  • 首先endpoint回复不能走node 1给client,因为会被client reset(如果在node1上将src ip替换成node2的ip可能会路由不通)。回复包在 node1上要snat给node2
  • 经过snat后endpoint没法拿到client ip(slb之类是通过option带过来)
1
2
3
4
5
6
7
8
9
          client
\ ^
\ \
v \
node 1 <--- node 2
| ^ SNAT
| | --->
v |
endpoint

可以将 Service 的 spec.externalTrafficPolicy 字段设置为 local,这就保证了所有 Pod 通过 Service 收到请求之后,一定可以看到真正的、外部 client 的源地址。

而这个机制的实现原理也非常简单:这时候,一台宿主机上的 iptables 规则,会设置为只将 IP 包转发给运行在这台宿主机上的 Pod。所以这时候,Pod 就可以直接使用源地址将回复包发出,不需要事先进行 SNAT 了。这个流程,如下所示:

1
2
3
4
5
6
7
8
9
      client
^ / \
/ / \
/ v X
node 1 node 2
^ |
| |
| v
endpoint

当然,这也就意味着如果在一台宿主机上,没有任何一个被代理的 Pod 存在,比如上图中的 node 2,那么你使用 node 2 的 IP 地址访问这个 Service,就是无效的。此时,你的请求会直接被 DROP 掉。

kube-proxy

在 Kubernetes v1.0 版本,代理完全在 userspace 实现。Kubernetes v1.1 版本新增了 iptables 代理模式,但并不是默认的运行模式。从 Kubernetes v1.2 起,默认使用 iptables 代理。在 Kubernetes v1.8.0-beta.0 中,添加了 ipvs 代理模式

kube-proxy相当于service的管理方,业务流量不会走到kube-proxy,业务流量的负载均衡都是由内核层面的iptables或者ipvs来分发。

kube-proxy的三种模式:

image.png

一直以来,基于 iptables 的 Service 实现,都是制约 Kubernetes 项目承载更多量级的 Pod 的主要障碍。

ipvs 就是用于解决在大量 Service 时,iptables 规则同步变得不可用的性能问题。与 iptables 比较像的是,ipvs 的实现虽然也基于 netfilter 的钩子函数,但是它却使用哈希表作为底层的数据结构并且工作在内核态,这也就是说 ipvs 在重定向流量和同步代理规则有着更好的性能。

除了能够提升性能之外,ipvs 也提供了多种类型的负载均衡算法,除了最常见的 Round-Robin 之外,还支持最小连接、目标哈希、最小延迟等算法,能够很好地提升负载均衡的效率。

而相比于 iptables,IPVS 在内核中的实现其实也是基于 Netfilter 的 NAT 模式,所以在转发这一层上,理论上 IPVS 并没有显著的性能提升。但是,IPVS 并不需要在宿主机上为每个 Pod 设置 iptables 规则,而是把对这些“规则”的处理放到了内核态,从而极大地降低了维护这些规则的代价。这也正印证了我在前面提到过的,“将重要操作放入内核态”是提高性能的重要手段。

IPVS 模块只负责上述的负载均衡和代理功能。而一个完整的 Service 流程正常工作所需要的包过滤、SNAT 等操作,还是要靠 iptables 来实现。只不过,这些辅助性的 iptables 规则数量有限,也不会随着 Pod 数量的增加而增加。

ipvs 和 iptables 都是基于 Netfilter 实现的。

Kubernetes 中已经使用 ipvs 作为 kube-proxy 的默认代理模式。

1
/opt/kube/bin/kube-proxy --bind-address=172.26.137.117 --cluster-cidr=172.20.0.0/16 --hostname-override=172.26.137.117 --kubeconfig=/etc/kubernetes/kube-proxy.kubeconfig --logtostderr=true --proxy-mode=ipvs

image.png

port-forward

port-forward后外部也能够像nodePort一样访问到,但是port-forward不适合大流量,一般用于管理端口,启动的时候port-forward会固定转发到一个具体的Pod上,也没有负载均衡的能力。

1
2
#在本机监听1080端口,并转发给后端的svc/nginx-ren(总是给发给svc中的一个pod)
kubectl port-forward --address 0.0.0.0 svc/nginx-ren 1080:80

kubectl looks up a Pod from the service information provided on the command line and forwards directly to a Pod rather than forwarding to the ClusterIP/Service port and allowing the cluster to load balance the service like regular service traffic.

The portforward.go Complete function is where kubectl portforward does the first look up for a pod from options via AttachablePodForObjectFn:

The AttachablePodForObjectFn is defined as attachablePodForObject in this interface, then here is the attachablePodForObject function.

To my (inexperienced) Go eyes, it appears the attachablePodForObject is the thing kubectl uses to look up a Pod to from a Service defined on the command line.

Then from there on everything deals with filling in the Pod specific PortForwardOptions (which doesn’t include a service) and is passed to the kubernetes API.

Service 和 DNS 的关系

Service 和 Pod 都会被分配对应的 DNS A 记录(从域名解析 IP 的记录)。

对于 ClusterIP 模式的 Service 来说(比如我们上面的例子),它的 A 记录的格式是:..svc.cluster.local。当你访问这条 A 记录的时候,它解析到的就是该 Service 的 VIP 地址。

而对于指定了 clusterIP=None 的 Headless Service 来说,它的 A 记录的格式也是:..svc.cluster.local。但是,当你访问这条 A 记录的时候,它返回的是所有被代理的 Pod 的 IP 地址的集合。当然,如果你的客户端没办法解析这个集合的话,它可能会只会拿到第一个 Pod 的 IP 地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#kubectl get pod -l app=mysql-r -o wide
NAME READY STATUS RESTARTS IP NODE
mysql-r-0 2/2 Running 0 172.20.120.143 172.26.137.118
mysql-r-1 2/2 Running 4 172.20.248.143 172.26.137.116
mysql-r-2 2/2 Running 0 172.20.185.209 172.26.137.117

/ # nslookup mysql-r-1.mysql-r
Server: 10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name: mysql-r-1.mysql-r
Address 1: 172.20.248.143 mysql-r-1.mysql-r.default.svc.cluster.local
/ #
/ # nslookup mysql-r-2.mysql-r
Server: 10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name: mysql-r-2.mysql-r
Address 1: 172.20.185.209 mysql-r-2.mysql-r.default.svc.cluster.local

#如果service是headless(也就是明确指定了 clusterIP: None)
/ # nslookup mysql-r
Server: 10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name: mysql-r
Address 1: 172.20.185.209 mysql-r-2.mysql-r.default.svc.cluster.local
Address 2: 172.20.248.143 mysql-r-1.mysql-r.default.svc.cluster.local
Address 3: 172.20.120.143 mysql-r-0.mysql-r.default.svc.cluster.local

#如果service 没有指定 clusterIP: None,也就是会分配一个clusterIP给集群
/ # nslookup mysql-r
Server: 10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name: mysql-r
Address 1: 10.68.90.172 mysql-r.default.svc.cluster.local

不是每个pod都会向DNS注册,只有:

  • StatefulSet中的POD会向dns注册,因为他们要保证顺序行
  • POD显式指定了hostname和subdomain,说明要靠hostname/subdomain来解析
  • Headless Service代理的POD也会注册

Ingress

kube-proxy 只能路由 Kubernetes 集群内部的流量,而我们知道 Kubernetes 集群的 Pod 位于 CNI 创建的外网络中,集群外部是无法直接与其通信的,因此 Kubernetes 中创建了 ingress 这个资源对象,它由位于 Kubernetes 边缘节点(这样的节点可以是很多个也可以是一组)的 Ingress controller 驱动,负责管理南北向流量,Ingress 必须对接各种 Ingress Controller 才能使用,比如 nginx ingress controller、traefik。Ingress 只适用于 HTTP 流量,使用方式也很简单,只能对 service、port、HTTP 路径等有限字段匹配来路由流量,这导致它无法路由如 MySQL、Redis 和各种私有 RPC 等 TCP 流量。要想直接路由南北向的流量,只能使用 Service 的 LoadBalancer 或 NodePort,前者需要云厂商支持,后者需要进行额外的端口管理。有些 Ingress controller 支持暴露 TCP 和 UDP 服务,但是只能使用 Service 来暴露,Ingress 本身是不支持的,例如 nginx ingress controller,服务暴露的端口是通过创建 ConfigMap 的方式来配置的。

Ingress是授权入站连接到达集群服务的规则集合。 你可以给Ingress配置提供外部可访问的URL、负载均衡、SSL、基于名称的虚拟主机等。 用户通过POST Ingress资源到API server的方式来请求ingress。

1
2
3
4
5
 internet
|
[ Ingress ]
--|-----|--
[ Services ]

可以将 Ingress 配置为服务提供外部可访问的 URL、负载均衡流量、终止 SSL/TLS,以及提供基于名称的虚拟主机等能力。 Ingress 控制器 通常负责通过负载均衡器来实现 Ingress,尽管它也可以配置边缘路由器或其他前端来帮助处理流量。

Ingress 不会公开任意端口或协议。 将 HTTP 和 HTTPS 以外的服务公开到 Internet 时,通常使用 Service.Type=NodePort 或 Service.Type=LoadBalancer 类型的服务。

Ingress 其实不是Service的一个类型,但是它可以作用于多个Service,作为集群内部服务的入口。Ingress 能做许多不同的事,比如根据不同的路由,将请求转发到不同的Service上等等。

image.png

Ingress 对象,其实就是 Kubernetes 项目对“反向代理”的一种抽象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: cafe-ingress
spec:
tls:
- hosts:
- cafe.example.com
secretName: cafe-secret
rules:
- host: cafe.example.com
http:
paths:
- path: /tea --入口url路径
backend:
serviceName: tea-svc --对应的service
servicePort: 80
- path: /coffee
backend:
serviceName: coffee-svc
servicePort: 80

在实际的使用中,你只需要从社区里选择一个具体的 Ingress Controller,把它部署在 Kubernetes 集群里即可。然后,这个 Ingress Controller 会根据你定义的 Ingress 对象,提供对应的代理能力。

目前,业界常用的各种反向代理项目,比如 Nginx、HAProxy、Envoy、Traefik 等,都已经为 Kubernetes 专门维护了对应的 Ingress Controller。

一个 Ingress Controller 可以根据 Ingress 对象和被代理后端 Service 的变化,来自动进行更新的 Nginx 负载均衡器。

eBPF(extended Berkeley Packet Filter)和网络

eBPF允许程序对内核本身进行编程(即 通过程序动态修改内核的行为。传统方式要么是给内核打补丁,要么是修改内核源码 重新编译)。一句话来概括:编写代码监听内核事件,当事件发生时,BPF 代码就会在内核执行。

eBPF 最早出现在 3.18 内核中,此后原来的 BPF 就被称为 “经典” BPF(classic BPF, cBPF),cBPF 现在基本已经废弃了。很多人知道 cBPF 是因为它是 tcpdump 的包过滤语言。现在,Linux 内核只运行 eBPF,内核会将加载的 cBPF 字节码 透明地转换成 eBPF 再执行。如无特殊说明,本文中所说的 BPF 都是泛指 BPF 技术。

2015年eBPF 添加了一个新 fast path:XDP,XDP 是 eXpress DataPath 的缩写,支持在网卡驱动中运行 eBPF 代码(在软件中最早可以处理包的位置),而无需将包送 到复杂的协议栈进行处理,因此处理代价很小,速度极快。

BPF 当时用于 tcpdump,在内核中尽量前面的位置抓包,它不会 crash 内核;

bcc 是 tracing frontend for eBPF。

内核添加了一个新 socket 类型 AF_XDP。它提供的能力是:在零拷贝( zero-copy)的前提下将包从网卡驱动送到用户空间。

AF_XDP 提供的能力与 DPDK 有点类似,不过:

  • DPDK 需要重写网卡驱动,需要额外维护用户空间的驱动代码。
  • AF_XDP 在复用内核网卡驱动的情况下,能达到与 DPDK 一样的性能。

而且由于复用了内核基础设施,所有的网络管理工具还都是可以用的,因此非常方便, 而 DPDK 这种 bypass 内核的方案导致绝大大部分现有工具都用不了了。

由于所有这些操作都是发生在 XDP 层的,因此它称为 AF_XDP。插入到这里的 BPF 代码 能直接将包送到 socket。

Facebook 公布了生产环境 XDP+eBPF 使用案例(DDoS & LB)

  • 用 XDP/eBPF 重写了原来基于 IPVS 的 L4LB,性能 10x。
  • eBPF 经受住了严苛的考验:从 2017 开始,每个进入 facebook.com 的包,都是经过了 XDP & eBPF 处理的。

Cilium 1.6 发布 第一次支持完全干掉基于 iptables 的 kube-proxy,全部功能基于 eBPF。Cilium 1.8 支持基于 XDP 的 Service 负载均衡和 host network policies。

传统的 kube-proxy 处理 Kubernetes Service 时,包在内核中的 转发路径是怎样的?如下图所示:

image.png

步骤:

  1. 网卡收到一个包(通过 DMA 放到 ring-buffer)。
  2. 包经过 XDP hook 点。
  3. 内核给包分配内存,此时才有了大家熟悉的 skb(包的内核结构体表示),然后 送到内核协议栈。
  4. 包经过 GRO 处理,对分片包进行重组。
  5. 包进入 tc(traffic control)的 ingress hook。接下来,所有橙色的框都是 Netfilter 处理点。
  6. Netfilter:在 PREROUTING hook 点处理 raw table 里的 iptables 规则。
  7. 包经过内核的连接跟踪(conntrack)模块。
  8. Netfilter:在 PREROUTING hook 点处理 mangle table 的 iptables 规则。
  9. Netfilter:在 PREROUTING hook 点处理 nat table 的 iptables 规则。
  10. 进行路由判断(FIB:Forwarding Information Base,路由条目的内核表示,译者注) 。接下来又是四个 Netfilter 处理点。
  11. Netfilter:在 FORWARD hook 点处理 mangle table 里的iptables 规则。
  12. Netfilter:在 FORWARD hook 点处理 filter table 里的iptables 规则。
  13. Netfilter:在 POSTROUTING hook 点处理 mangle table 里的iptables 规则。
  14. Netfilter:在 POSTROUTING hook 点处理 nat table 里的iptables 规则。
  15. 包到达 TC egress hook 点,会进行出方向(egress)的判断,例如判断这个包是到本 地设备,还是到主机外。
  16. 对大包进行分片。根据 step 15 判断的结果,这个包接下来可能会:发送到一个本机 veth 设备,或者一个本机 service endpoint, 或者,如果目的 IP 是主机外,就通过网卡发出去。

Cilium 如何处理POD之间的流量(东西向流量)

image.png

如上图所示,Socket 层的 BPF 程序主要处理 Cilium 节点的东西向流量(E-W)。

  • 将 Service 的 IP:Port 映射到具体的 backend pods,并做负载均衡。
  • 当应用发起 connect、sendmsg、recvmsg 等请求(系统调用)时,拦截这些请求, 并根据请求的IP:Port 映射到后端 pod,直接发送过去。反向进行相反的变换。

这里实现的好处:性能更高。

  • 不需要包级别(packet leve)的地址转换(NAT)。在系统调用时,还没有创建包,因此性能更高。
  • 省去了 kube-proxy 路径中的很多中间节点(intermediate node hops) 可以看出,应用对这种拦截和重定向是无感知的(符合 Kubernetes Service 的设计)。

Cilium处理外部流量(南北向流量)

image.png

集群外来的流量到达 node 时,由 XDP 和 tc 层的 BPF 程序进行处理, 它们做的事情与 socket 层的差不多,将 Service 的 IP:Port 映射到后端的 PodIP:Port,如果 backend pod 不在本 node,就通过网络再发出去。发出去的流程我们 在前面 Cilium eBPF 包转发路径 讲过了。

这里 BPF 做的事情:执行 DNAT。这个功能可以在 XDP 层做,也可以在 TC 层做,但 在XDP 层代价更小,性能也更高。

总结起来,Cilium的核心理念就是:

  • 将东西向流量放在离 socket 层尽量近的地方做。
  • 将南北向流量放在离驱动(XDP 和 tc)层尽量近的地方做。

性能比较

测试环境:两台物理节点,一个发包,一个收包,收到的包做 Service loadbalancing 转发给后端 Pods。

image.png

可以看出:

  • Cilium XDP eBPF 模式能处理接收到的全部 10Mpps(packets per second)。
  • Cilium tc eBPF 模式能处理 3.5Mpps。
  • kube-proxy iptables 只能处理 2.3Mpps,因为它的 hook 点在收发包路径上更后面的位置。
  • kube-proxy ipvs 模式这里表现更差,它相比 iptables 的优势要在 backend 数量很多的时候才能体现出来。

cpu:

  • XDP 性能最好,是因为 XDP BPF 在驱动层执行,不需要将包 push 到内核协议栈。
  • kube-proxy 不管是 iptables 还是 ipvs 模式,都在处理软中断(softirq)上消耗了大量 CPU。

利用 ebpf sockmap/redirection 提升 socket 性能

通过bpf监听socket来拦截所有sendmsg事件,如果是发送到本地另一个socket那么bpf就绕过TCP/IP协议栈,直接将msg送给对方socket。依赖用cgroups来指定监听的sockets事件

img

实现这个功能依赖两个东西:

  1. sockmap:这是一个存储 socket 信息的映射表。作用:

    • 一段 BPF 程序监听所有的内核 socket 事件,并将新建的 socket 记录到这个 map;
    • 另一段 BPF 程序拦截所有 sendmsg 系统调用,然后去 map 里查找 socket 对端,之后 调用 BPF 函数绕过 TCP/IP 协议栈,直接将数据发送到对端的 socket queue。
  2. cgroups:绑定cgroup下的进程,从而将这些进程下的所有 socket 加入到sockmap。

参考资料

https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network Kubernetes 网络疑难杂症排查方法

https://blog.csdn.net/qq_36183935/article/details/90734936 kube-proxy ipvs模式详解

http://arthurchiao.art/blog/ebpf-and-k8s-zh/ 大规模微服务利器:eBPF 与 Kubernetes

http://arthurchiao.art/blog/cilium-life-of-a-packet-pod-to-service-zh/ Life of a Packet in Cilium:实地探索 Pod-to-Service 转发路径及 BPF 处理逻辑

http://arthurchiao.art/blog/understanding-ebpf-datapath-in-cilium-zh/ 深入理解 Cilium 的 eBPF 收发包路径(datapath)(KubeCon, 2019)

利用 ebpf sockmap/redirection 提升 socket 性能

利用 eBPF 支撑大规模 K8s Service (LPC, 2019)

https://jiayu0x.com/2014/12/02/iptables-essential-summary/

imroc 电子书

delay ack拉高实际rt的case

发表于 2020-09-16 | 分类于 performance

delay ack拉高实际rt的case

案例描述

开发人员发现client到server的rtt是2.5ms,每个请求1ms server就能处理完毕,但是监控发现的rt不是3.5(1+2.5),而是6ms,想知道这个6ms怎么来的?

如下业务监控图:实际处理时间(逻辑服务时间1ms,rtt2.4ms,加起来3.5ms),但是系统监控到的rt(蓝线)是6ms,如果一个请求分很多响应包串行发给client,这个6ms是正常的(1+2.4*N),但实际上如果send buffer足够的话,按我们前面的理解多个响应包会并发发出去,所以如果整个rt是3.5ms才是正常的。

image.png

分析

抓包来分析原因:

image.png

实际看到大量的response都是3.5ms左右,符合我们的预期,但是有少量rt被delay ack严重影响了

从下图也可以看到有很多rtt超过3ms的,这些超长时间的rtt会最终影响到整个服务rt

image.png

kubernetes容器网络

发表于 2020-08-31 | 分类于 docker

kubernetes 容器网络

cni 网络

cni0 is a Linux network bridge device, all veth devices will connect to this bridge, so all Pods on the same node can communicate with each other, as explained in Kubernetes Network Model and the hotel analogy above.

cni(Container Network Interface)

CNI 全称为 Container Network Interface,是用来定义容器网络的一个 规范。containernetworking/cni 是一个 CNCF 的 CNI 实现项目,包括基本额 bridge,macvlan等基本网络插件。

一般将cni各种网络插件的可执行文件二进制放到 /opt/cni/bin ,在 /etc/cni/net.d/ 下创建配置文件,剩下的就交给 K8s 或者 containerd 了,我们不关心也不了解其实现。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ls -lh /opt/cni/bin/
总用量 90M
-rwxr-x--- 1 root root 4.0M 12月 23 09:39 bandwidth
-rwxr-x--- 1 root root 35M 12月 23 09:39 calico
-rwxr-x--- 1 root root 35M 12月 23 09:39 calico-ipam
-rwxr-x--- 1 root root 3.0M 12月 23 09:39 flannel
-rwxr-x--- 1 root root 3.5M 12月 23 09:39 host-local
-rwxr-x--- 1 root root 3.1M 12月 23 09:39 loopback
-rwxr-x--- 1 root root 3.8M 12月 23 09:39 portmap
-rwxr-x--- 1 root root 3.3M 12月 23 09:39 tuning

[root@hygon3 15:55 /root]
#ls -lh /etc/cni/net.d/
总用量 12K
-rw-r--r-- 1 root root 607 12月 23 09:39 10-calico.conflist
-rw-r----- 1 root root 292 12月 23 09:47 10-flannel.conflist
-rw------- 1 root root 2.6K 12月 23 09:39 calico-kubeconfig

CNI 插件都是直接通过 exec 的方式调用,而不是通过 socket 这样 C/S 方式,所有参数都是通过环境变量、标准输入输出来实现的。

Step-by-step communication from Pod 1 to Pod 6:

  1. Package leaves *Pod 1 netns* through the *eth1* interface and reaches the root netns* through the virtual interface veth1*;
  2. Package leaves veth1* and reaches cni0*, looking for ***Pod 6***’s address;
  3. Package leaves cni0* and is redirected to eth0*;
  4. Package leaves *eth0* from Master 1* and reaches the gateway*;
  5. Package leaves the *gateway* and reaches the *root netns* through the eth0* interface on Worker 1*;
  6. Package leaves eth0* and reaches cni0*, looking for ***Pod 6***’s address;
  7. Package leaves *cni0* and is redirected to the *veth6* virtual interface;
  8. Package leaves the *root netns* through *veth6* and reaches the *Pod 6 netns* though the *eth6* interface;

image-20220115124747936

flannel 网络

假如POD1访问POD4:

  1. 从POD1中出来的包先到Bridge cni0上(因为POD1对应的veth挂在了cni0上)
  2. 然后进入到宿主机网络,宿主机有路由 10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink ,也就是目标ip 10.244.2.3的包交由 flannel.1 来处理
  3. flanneld 进程将包封装成vxlan 丢到eth0从宿主机1离开(封装后的目标ip是192.168.2.91)
  4. 这个封装后的vxlan udp包正确路由到宿主机2
  5. 然后经由 flanneld 解包成 10.244.2.3 ,命中宿主机2上的路由:10.244.2.0/24 dev cni0 proto kernel scope link src 10.244.2.1 ,交给cni0(这里会过宿主机iptables)
  6. cni0将包送给POD4

image-20220115132938290

对应宿主机查询到的ip、路由信息(和上图不是对应的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ip -d -4 addr show cni0
475: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default qlen 1000
link/ether 8e:34:ba:e2:a4:c6 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65535
bridge forward_delay 1500 hello_time 200 max_age 2000 ageing_time 30000 stp_state 0 priority 32768 vlan_filtering 0 vlan_protocol 802.1Q bridge_id 8000.8e:34:ba:e2:a4:c6 designated_root 8000.8e:34:ba:e2:a4:c6 root_port 0 root_path_cost 0 topology_change 0 topology_change_detected 0 hello_timer 0.00 tcn_timer 0.00 topology_change_timer 0.00 gc_timer 161.46 vlan_default_pvid 1 vlan_stats_enabled 0 group_fwd_mask 0 group_address 01:80:c2:00:00:00 mcast_snooping 1 mcast_router 1 mcast_query_use_ifaddr 0 mcast_querier 0 mcast_hash_elasticity 4 mcast_hash_max 512 mcast_last_member_count 2 mcast_startup_query_count 2 mcast_last_member_interval 100 mcast_membership_interval 26000 mcast_querier_interval 25500 mcast_query_interval 12500 mcast_query_response_interval 1000 mcast_startup_query_interval 3124 mcast_stats_enabled 0 mcast_igmp_version 2 mcast_mld_version 1 nf_call_iptables 0 nf_call_ip6tables 0 nf_call_arptables 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
inet 192.168.3.1/24 brd 192.168.3.255 scope global cni0
valid_lft forever preferred_lft forever

#ip -d -4 addr show flannel.1
474: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
link/ether fe:49:64:ae:36:af brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65535
vxlan id 1 local 10.133.2.252 dev bond0 srcport 0 0 dstport 8472 nolearning ttl auto ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
inet 192.168.3.0/32 brd 192.168.3.0 scope global flannel.1
valid_lft forever preferred_lft forever

[root@hygon239 20:06 /root]
#kubectl describe node hygon252 | grep -C5 -i vtep //可以看到VetpMAC 以及对应的宿主机IP(vxlan封包后的IP)
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/os=linux
kubernetes.io/arch=amd64
kubernetes.io/hostname=hygon252
kubernetes.io/os=linux
Annotations: flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"fe:49:64:ae:36:af"}
flannel.alpha.coreos.com/backend-type: vxlan
flannel.alpha.coreos.com/kube-subnet-manager: true
flannel.alpha.coreos.com/public-ip: 10.133.2.252
kubeadm.alpha.kubernetes.io/cri-socket: /var/run/dockershim.sock
node.alpha.kubernetes.io/ttl: 0

image-20220115133500854

kubernetes calico 网络

1
2
3
4
kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml

#或者老版本的calico
curl https://docs.projectcalico.org/v3.15/manifests/calico.yaml -o calico.yaml

默认calico用的是ipip封包(这个性能跟原生网络差多少有待验证,本质也是overlay网络,比flannel那种要好很多吗?)

跨宿主机的两个容器之间的流量链路是:

cali-容器eth0->宿主机cali27dce37c0e8->tunl0->内核ipip模块封包->物理网卡(ipip封包后)—远程–> 物理网卡->内核ipip模块解包->tunl0->cali-容器

image.png

Calico IPIP模式对物理网络无侵入,符合云原生容器网络要求;使用IPIP封包,性能略低于Calico BGP模式;无法使用传统防火墙管理、也无法和存量网络直接打通。Pod在Node做SNAT访问外部,Pod流量不易被监控。

calico ipip网络不通

集群有五台机器192.168.0.110-114, 同时每个node都有另外一个ip:192.168.3.110-114,部分节点之间不通。每台机器部署好calico网络后,会分配一个 /26 CIRD 子网(64个ip)。

案例1

目标机是10.122.127.128(宿主机ip 192.168.3.112),如果从10.122.17.64(宿主机ip 192.168.3.110) ping 10.122.127.128不通,查看10.122.127.128路由表:

1
2
3
4
5
6
[root@az3-k8s-13 ~]# ip route |grep tunl0
10.122.17.64/26 via 10.122.127.128 dev tunl0 //这条路由不通
[root@az3-k8s-13 ~]# ip route del 10.122.17.64/26 via 10.122.127.128 dev tunl0 ; ip route add 10.122.17.64/26 via 192.168.3.110 dev tunl0 proto bird onlink

[root@az3-k8s-13 ~]# ip route |grep tunl0
10.122.17.64/26 via 192.168.3.110 dev tunl0 proto bird onlink //这样就通了

在10.122.127.128抓包如下,明显可以看到icmp request到了 tunl0网卡,tunl0网卡也回复了,但是回复包没有经过kernel ipip模块封装后发到eth1上:

image.png

正常机器应该是这样,上图不正常的时候缺少红框中的reply:

image.png

解决:

1
2
ip route del 10.122.17.64/26 via 10.122.127.128 dev tunl0 ; 
ip route add 10.122.17.64/26 via 192.168.3.110 dev tunl0 proto bird onlink

删除错误路由增加新的路由就可以了,新增路由的意思是从tunl0发给10.122.17.64/26的包下一跳是 192.168.3.110。

via 192.168.3.110 表示下一跳的ip

onlink参数的作用:
使用这个参数将会告诉内核,不必检查网关是否可达。因为在linux内核中,网关与本地的网段不同是被认为不可达的,从而拒绝执行添加路由的操作。

因为tunl0网卡ip的 CIDR 是32,也就是不属于任何子网,那么这个网卡上的路由没有网关,配置路由的话必须是onlink, 内核存也没法根据子网来选择到这块网卡,所以还会加上 dev 指定网卡。

案例2

集群有五台机器192.168.0.110-114, 同时每个node都有另外一个ip:192.168.3.110-114,只有node2没有192.168.3.111这个ip,结果node2跟其他节点都不通:

1
2
3
4
5
6
7
8
9
10
11
12
#calicoctl node status
Calico process is running.

IPv4 BGP status
+---------------+-------------------+-------+------------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+---------------+-------------------+-------+------------+-------------+
| 192.168.0.111 | node-to-node mesh | up | 2020-08-29 | Established |
| 192.168.3.112 | node-to-node mesh | up | 2020-08-29 | Established |
| 192.168.3.113 | node-to-node mesh | up | 2020-08-29 | Established |
| 192.168.3.114 | node-to-node mesh | up | 2020-08-29 | Established |
+---------------+-------------------+-------+------------+-------------+

从node4 ping node2,然后在node2上抓包,可以看到 icmp request都发到了node2上,但是node2收到后没有发给tunl0:

image.png

所以icmp没有回复,这里的问题在于kernel收到包后为什么不给tunl0

同样,在node2上ping node4,同时在node2上抓包,可以看到发给node4的request包和reply包:

image.png

从request包可以看到src ip 是0.111, dest ip是 3.113,因为 node2 没有192.168.3.111这个ip

非常关键的我们看到node4的回复包 src ip 不是3.113,而是0.113(根据node4的路由就应该是0.113)

image.png

这就是问题所在,从node4过来的ipip包src ip都是0.113,实际这里ipip能认识的只是3.113.

如果这个时候在3.113机器上把0.113网卡down掉,那么3.113上的:

10.122.124.128/26 via 192.168.0.111 dev tunl0 proto bird onlink 路由被自动删除,3.113将不再回复request。这是因为calico记录的node2的ip是192.168.0.111,所以会自动增加

解决办法,在node4上删除这条路由记录,也就是强制让回复包走3.113网卡,这样收发的ip就能对应上了

1
2
3
4
ip route del 192.168.0.0/24 dev eth0 proto kernel scope link src 192.168.0.113
//同时将默认路由改到3.113
ip route del default via 192.168.0.253 dev eth0;
ip route add default via 192.168.3.253 dev eth1

最终OK后,node4上的ip route是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@az3-k8s-14 ~]# ip route
default via 192.168.3.253 dev eth1
10.122.17.64/26 via 192.168.3.110 dev tunl0 proto bird onlink
10.122.124.128/26 via 192.168.0.111 dev tunl0 proto bird onlink
10.122.127.128/26 via 192.168.3.112 dev tunl0 proto bird onlink
blackhole 10.122.157.128/26 proto bird
10.122.157.129 dev cali19f6ea143e3 scope link
10.122.157.130 dev cali09e016ead53 scope link
10.122.157.131 dev cali0ad3225816d scope link
10.122.157.132 dev cali55a5ff1a4aa scope link
10.122.157.133 dev cali01cf8687c65 scope link
10.122.157.134 dev cali65232d7ada6 scope link
10.122.173.128/26 via 192.168.3.114 dev tunl0 proto bird onlink
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.3.0/24 dev eth1 proto kernel scope link src 192.168.3.113

正常后的抓包, 注意这里drequest的est ip 和reply的 src ip终于一致了:

1
2
3
4
5
6
7
8
9
//request
00:16:3e:02:06:1e > ee:ff:ff:ff:ff:ff, ethertype IPv4 (0x0800), length 118: (tos 0x0, ttl 64, id 57971, offset 0, flags [DF], proto IPIP (4), length 104)
192.168.0.111 > 192.168.3.110: (tos 0x0, ttl 64, id 18953, offset 0, flags [DF], proto ICMP (1), length 84)
10.122.124.128 > 10.122.17.64: ICMP echo request, id 22001, seq 4, length 64

//reply
ee:ff:ff:ff:ff:ff > 00:16:3e:02:06:1e, ethertype IPv4 (0x0800), length 118: (tos 0x0, ttl 64, id 2565, offset 0, flags [none], proto IPIP (4), length 104)
192.168.3.110 > 192.168.0.111: (tos 0x0, ttl 64, id 26374, offset 0, flags [none], proto ICMP (1), length 84)
10.122.17.64 > 10.122.124.128: ICMP echo reply, id 22001, seq 4, length 64

总结下来这两个案例都还是对路由不够了解,特别是案例2,因为有了多个网卡后导致路由更复杂。calico ipip的基本原理就是利用内核进行ipip封包,然后修改路由来保证网络的畅通。

flannel网络不通

firewalld

在麒麟系统的物理机上通过kubeadm setup集群,发现有的环境flannel网络不通,在宿主机上ping 其它物理机flannel.0网卡的ip,通过在对端宿主机抓包发现icmp收到后被防火墙扔掉了,抓包中可以看到错误信息:icmp unreachable - admin prohibited

下图中正常的icmp是直接ping 物理机ip

image-20211228203650921

The “admin prohibited filter” seen in the tcpdump output means there is a firewall blocking a connection. It does it by sending back an ICMP packet meaning precisely that: the admin of that firewall doesn’t want those packets to get through. It could be a firewall at the destination site. It could be a firewall in between. It could be iptables on the Linux system.

发现有问题的环境中宿主机的防火墙设置报错了:

1
2
12月 28 23:35:08 hygon253 firewalld[10493]: WARNING: COMMAND_FAILED: '/usr/sbin/iptables -w10 -t filter -X DOCKER-ISOLATION-STAGE-1' failed: iptables: No chain/target/match by that name.
12月 28 23:35:08 hygon253 firewalld[10493]: WARNING: COMMAND_FAILED: '/usr/sbin/iptables -w10 -t filter -F DOCKER-ISOLATION-STAGE-2' failed: iptables: No chain/target/match by that name.

应该是因为启动docker的时候 firewalld 是运行着的

Do you have firewalld enabled, and was it (re)started after docker was started? If so, then it’s likely that firewalld wiped docker’s IPTables rules. Restarting the docker daemon should re-create those rules.

停掉 firewalld 服务可以解决这个问题

掉电重启后flannel网络不通

flannel能收到包,但是cni0收不到包,说明包进到了目标宿主机,但是从flannel解开udp转送到cni的时候出了问题,大概率是iptables 拦截了包

1
2
3
4
5
It seems docker version >=1.13 will add iptables rule like below,and it make this issue happen:
iptables -P FORWARD DROP

All you need to do is add a rule below:
iptables -P FORWARD ACCEPT

清理

cni信息

1
2
/etc/cni/net.d/*
/var/lib/cni/ 下存放有ip分配信息

calico创建的tunl0网卡是个tunnel,可以通过 ip tunnel show来查看,清理不掉(重启可以清理掉tunl0)

1
2
3
4
5
6
ip link set dev tunl0 name tunl0_fallback

或者
/sbin/ip link set eth1 down
/sbin/ip link set eth1 name eth123
/sbin/ip link set eth123 up

flannel

1
2
ip link delete flannel.1
ip link delete cni0

netns

以下case创建一个名为 ren 的netns,然后在里面增加一对虚拟网卡veth1 veth1_p, veth1放置在ren里面,veth1_p 放在物理机上,给他们配置上ip并up就能通了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
1004  [2021-10-27 10:49:08] ip netns add ren
1005 [2021-10-27 10:49:12] ip netns show
1006 [2021-10-27 10:49:22] ip netns exec ren route //为空
1007 [2021-10-27 10:49:29] ip netns exec ren iptables -L
1008 [2021-10-27 10:49:55] ip link add veth1 type veth peer name veth1_p //此时宿主机上能看到这两块网卡
1009 [2021-10-27 10:50:07] ip link set veth1 netns ren //将veth1从宿主机默认网络空间挪到ren中,宿主机中看不到veth1了
1010 [2021-10-27 10:50:18] ip netns exec ren route
1011 [2021-10-27 10:50:25] ip netns exec ren iptables -L
1012 [2021-10-27 10:50:39] ifconfig
1013 [2021-10-27 10:50:51] ip link list
1014 [2021-10-27 10:51:29] ip netns exec ren ip link list
1017 [2021-10-27 10:53:27] ip netns exec ren ip addr add 172.19.0.100/24 dev veth1
1018 [2021-10-27 10:53:31] ip netns exec ren ip link list
1019 [2021-10-27 10:53:39] ip netns exec ren ifconfig
1020 [2021-10-27 10:53:42] ip netns exec ren ifconfig -a
1021 [2021-10-27 10:54:13] ip netns exec ren ip link set dev veth1 up
1022 [2021-10-27 10:54:16] ip netns exec ren ifconfig
1023 [2021-10-27 10:54:22] ping 172.19.0.100
1024 [2021-10-27 10:54:35] ifconfig -a
1025 [2021-10-27 10:55:03] ip netns exec ren ip addr add 172.19.0.101/24 dev veth1_p
1026 [2021-10-27 10:55:10] ip addr add 172.19.0.101/24 dev veth1_p
1027 [2021-10-27 10:55:16] ifconfig veth1_p
1028 [2021-10-27 10:55:30] ip link set dev veth1_p up
1029 [2021-10-27 10:55:32] ifconfig veth1_p
1030 [2021-10-27 10:55:38] ping 172.19.0.101
1031 [2021-10-27 10:55:43] ping 172.19.0.100
1032 [2021-10-27 10:55:53] ip link set dev veth1_p down
1033 [2021-10-27 10:55:54] ping 172.19.0.100
1034 [2021-10-27 10:55:58] ping 172.19.0.101
1035 [2021-10-27 10:56:08] ifconfig veth1_p
1036 [2021-10-27 10:56:32] ping 172.19.0.101
1037 [2021-10-27 10:57:04] ip netns exec ren route
1038 [2021-10-27 10:57:52] ip netns exec ren ping 172.19.0.101
1039 [2021-10-27 10:57:58] ip link set dev veth1_p up
1040 [2021-10-27 10:57:59] ip netns exec ren ping 172.19.0.101
1041 [2021-10-27 10:58:06] ip netns exec ren ping 172.19.0.100
1042 [2021-10-27 10:58:14] ip netns exec ren ifconfig
1043 [2021-10-27 10:58:19] ip netns exec ren route
1044 [2021-10-27 10:58:26] ip netns exec ren ping 172.19.0.100 -I veth1
1045 [2021-10-27 10:58:58] ifconfig veth1_p
1046 [2021-10-27 10:59:10] ping 172.19.0.100
1047 [2021-10-27 10:59:26] ip netns exec ren ping 172.19.0.101 -I veth1

把网卡加入到docker0的bridge下
1160 [2021-10-27 12:17:37] brctl show
1161 [2021-10-27 12:18:05] ip link set dev veth3_p master docker0
1162 [2021-10-27 12:18:09] ip link set dev veth1_p master docker0
1163 [2021-10-27 12:18:13] ip link set dev veth2 master docker0
1164 [2021-10-27 12:18:15] brctl show

btctl showmacs br0

Linux 上存在一个默认的网络命名空间,Linux 中的 1 号进程初始使用该默认空间。Linux 上其它所有进程都是由 1 号进程派生出来的,在派生 clone 的时候如果没有额外特别指定,所有的进程都将共享这个默认网络空间。

所有的网络设备刚创建出来都是在宿主机默认网络空间下的。可以通过 ip link set 设备名 netns 网络空间名 将设备移动到另外一个空间里去,socket也是归属在某一个网络命名空间下的,由创建socket进程所在的netns来决定socket所在的netns

1
2
3
4
5
6
7
8
9
10
11
12
//file: net/socket.c
int sock_create(int family, int type, int protocol, struct socket **res)
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}

//file: include/net/sock.h
static inline
void sock_net_set(struct sock *sk, struct net *net)
{
write_pnet(&sk->sk_net, net);
}

内核提供了三种操作命名空间的方式,分别是 clone、setns 和 unshare。ip netns add 使用的是 unshare,原理和 clone 是类似的。

Image

每个 net 下都包含了自己的路由表、iptable 以及内核参数配置等等

参考资料

https://morven.life/notes/networking-3-ipip/

https://www.cnblogs.com/bakari/p/10564347.html

https://www.cnblogs.com/goldsunshine/p/10701242.html

MySQL JDBC StreamResult 和 net_write_timeout

发表于 2020-07-03 | 分类于 MySQL

MySQL JDBC StreamResult 和 net_write_timeout

MySQL JDBC 拉取数据的三种方式

MySQL JDBC 在从 MySQL 拉取数据的时候有三种方式:

  1. 简单模式,也就是默认模式,数据都先要从MySQL Server发到client的OS TCP buffer,然后JDBC把 OS buffer读取到JVM内存中,读取到JVM内存的过程中憋着不让client读取,全部读完再通知inputStream.read(). 数据大的话容易导致JVM OOM
  2. useCursorFetch=true,配合FetchSize,也就是MySQL Server把查到的数据先缓存到本地磁盘,然后按照FetchSize挨个发给client。这需要占用MySQL很高的IOPS(先写磁盘缓存),其次每次Fetch需要一个RTT,效率不高。
  3. Stream读取,Stream读取是在执行SQL前设置FetchSize:statement.setFetchSize(Integer.MIN_VALUE),同时确保游标是只读、向前滚动的(为游标的默认值),MySQL JDBC内置的操作方法是将Statement强制转换为:com.mysql.jdbc.StatementImpl,调用其方法:enableStreamingResults(),这2者达到的效果是一致的,都是启动Stream流方式读取数据。这个时候MySQL不停地发数据,inputStream.read()不停地读取。一般来说发数据更快些,很快client的OS TCP recv buffer就满了,这时MySQL停下来等buffer有空闲就继续发数据。等待过程中如果超过 net_write_timeout MySQL就会报错,中断这次查询。

从这里的描述来看,数据小的时候第一种方式还能接受,但是数据大了容易OOM,方式三看起来不错,但是要特别注意 net_write_timeout。

1和3对MySQL Server来说处理上没有啥区别,也感知不到这两种方式的不同。只是对1来说从OS Buffer中的数据复制到JVM内存中速度快,JVM攒多了数据内存就容易爆掉;对3来说JDBC一条条将OS Buffer中的数据复制到JVM(内存复制速度快)同时返回给execute挨个处理(慢),一般来说挨个处理要慢一些,这就导致了从OS Buffer中复制数据较慢,容易导致 TCP Receive Buffer满了,那么MySQL Server感知到的就是TCP 传输窗口为0了,导致暂停传输数据。

在数据量很小的时候方式三没什么优势,因为总是多一次set net_write_tiemout,也就是多了一次RTT。

img

MySQL timeout

  1. Creates a statement by calling Connection.createStatement().
  2. Calls Statement.executeQuery().
  3. The statement transmits the Query to MySqlServer by using the internal connection.
  4. The statement creates a new timeout-execution thread for timeout process.
  5. For version 5.1.x, it changes to assign 1 thread for each connection.
  6. Registers the timeout execution to the thread.
  7. Timeout occurs.
  8. The timeout-execution thread creates a connection that has the same configurations as the statement.
  9. Transmits the cancel Query (KILL QUERY “connectionId“) by using the connection.

Figure 6: QueryTimeout Execution Process for MySQL JDBC Statement (5.0.8).

参考《揭秘JDBC超时机制》

net_read_timeout

Command-Line Format --net-read-timeout=#
System Variable net_read_timeout
Scope Global, Session
Dynamic Yes
Type Integer
Default Value 30
Minimum Value 1
Maximum Value 31536000
Unit seconds

The number of seconds to wait for more data from a connection before aborting the read. When the server is reading from the client, net_read_timeout is the timeout value controlling when to abort.

如下图,MySQL Server监听3017端口,195228号包 客户端发一个SQL 给 MySQL Server,但是似乎这个时候正好网络异常,30秒钟后(从 SQL 请求的前一个 ack 开始算,Server应该一直都没有收到),Server 端触发 net_read_timeout 超时异常(疑问:这里没有 net_read_timeout 描述的读取了一半的现象)

image-20230209155545142

解决方案:建议调大 net_read_timeout 以应对可能出现的网络丢包

net_write_timeout

show processlist 看到的State的值一直处于“Sending to client”,说明SQL这个语句已经执行完毕,而此时由于请求的数据太多,MySQL不停写入net buffer,而net buffer又不停的将数据写入服务端的网络棧,服务器端的网络栈(socket send buffer)被写满了,又没有被客户端读取并消化,这时读数据的流程就被MySQL暂停了。直到客户端完全读取了服务端网络棧的数据,这个状态才会消失。

先看下 net_write_timeout的解释:The number of seconds to wait for a block to be written to a connection before aborting the write. 只针对执行查询中的等待超时,网络不好,tcp buffer满了(应用迟迟不读走数据)等容易导致mysql server端报net_write_timeout错误,指的是mysql server hang在那里长时间无法发送查询结果。

Property Value
Command-Line Format --net-write-timeout=#
System Variable net_write_timeout
Scope Global, Session
Dynamic Yes
Type Integer
Default Value 60
Minimum Value 1

报这个错就是RDS等了net_write_timeout这么久没写数据,可能是客户端卡死没有读走数据,也可能是从多个分片挨个拉取,还没开始拉第7片前面6片拉取耗时就超过了net_write_timeout。

案例:DRDS 到 MySQL 多个分片拉取数据生成了许多 cursor 并发执行,但拉数据的时候是串行拉取的,如果用户端拉取数据过慢会导致最后一个 cursor 执行完成之后要等待很久.会超过 MySQL 的 net_write_timeout 配置从而引发报错. 也就是最后一个cursor打开后一直没有去读取数据,直到MySQL Server 触发 net_write_timeout 异常

首先可以尝试在 DRDS jdbcurl 配置 netTimeoutForStreamingResults 参数,设置为 0 可以使其一直等待,或设置一个合理的值(秒).

从JDBC驱动中可以看到,当调用PreparedStatement的executeQuery() 方法的时候,如果我们是去获取流式resultset的话,就会默认执行SET net_write_timeout= ? 这个命令去重新设置timeout时间。源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (doStreaming && this.connection.getNetTimeoutForStreamingResults() > 0) {  
java.sql.Statement stmt = null;
try {
stmt = this.connection.createStatement(); ((com.mysql.jdbc.StatementImpl)stmt).executeSimpleNonQuery(this.connection, "SET net_write_timeout="
+ this.connection.getNetTimeoutForStreamingResults());
} finally {
if (stmt != null) {
stmt.close();
}
}
}

//另外DRDS代码 AppLoader.java 中写死了net_write_timeout 8小时
ds.putConnectionProperties(ConnectionProperties.NET_WRITE_TIMEOUT, 28800);

而 this.connection.getNetTimeoutForStreamingResults() 默认是600秒,或者在JDBC连接串种通过属性 netTimeoutForStreamingResults 来指定。

netTimeoutForStreamingResults 默认值:

What value should the driver automatically set the server setting ‘net_write_timeout’ to when the streaming result sets feature is in use? Value has unit of seconds, the value “0” means the driver will not try and adjust this value.

Default Value 600
Since Version 5.1.0

一般在数据导出场景中容易出现 net_write_timeout 这个错误,比如这个错误堆栈:

或者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ErrorMessage:
Communications link failure
The last packet successfully received from the server was 7 milliseconds ago. The last packet sent successfully to the server was 709,806 milliseconds ago. - com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
The last packet successfully received from the server was 7 milliseconds ago. The last packet sent successfully to the server was 709,806 milliseconds ago.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:377)
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:1036)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3427)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3327)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3814)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:870)
at com.mysql.jdbc.MysqlIO.nextRow(MysqlIO.java:1928)
at com.mysql.jdbc.RowDataDynamic.nextRecord(RowDataDynamic.java:378)
at com.mysql.jdbc.RowDataDynamic.next(RowDataDynamic.java:358)
at com.mysql.jdbc.ResultSetImpl.next(ResultSetImpl.java:6337)
at com.alibaba.datax.plugin.rdbms.reader.CommonRdbmsReader$Task.startRead(CommonRdbmsReader.java:275)
at com.alibaba.datax.plugin.reader.drdsreader.DrdsReader$Task.startRead(DrdsReader.java:148)
at com.alibaba.datax.core.taskgroup.runner.ReaderRunner.run(ReaderRunner.java:62)
at java.lang.Thread.run(Thread.java:834)
Caused by: java.io.EOFException: Can not read response from server. Expected to read 258 bytes, read 54 bytes before connection was unexpectedly lost.
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:2914)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3387)
... 11 more

特别注意

JDBC驱动报如下错误

Application was streaming results when the connection failed. Consider raising value of ‘net_write_timeout’ on the server. - com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Application was streaming results when the connection failed. Consider raising value of ‘net_write_timeout’ on the server.

不一定是 net_write_timeout 设置过小导致的,JDBC 在 streaming 流模式下只要连接异常就会报如上错误,比如:

  • 连接被 TCP reset
  • 连接因为某种原因(比如 QueryTimeOut、比如用户监控kill 慢查询) 触发 kill Query导致连接中断

比如出现内核bug,内核卡死不发包的话,客户端同样报 net_write_timeout 错误

max_allowed_packet: 单个SQL或者单条记录的最大大小

一些其他的 Timeout

connectTimeout:表示等待和MySQL数据库建立socket链接的超时时间,默认值0,表示不设置超时,单位毫秒,建议30000。 JDBC驱动连接属性

queryTimeout:超时后jdbc驱动触发新建一个连接来发送一个 kill 给DB

socketTimeout:JDBC参数,表示客户端发送请求给MySQL数据库后block在read的等待数据的超时时间,linux系统默认的socketTimeout为30分钟,可以不设置。socketTimeout 超时不会触发发kill,只会断开tcp连接。

要特别注意socketTimeout仅仅是指等待socket数据时间,如果在传输数据那么这个值就没有用了。socketTimeout通过mysql-connector中的NativeProtocol最终设置在socketOptions上

image-20211024171459127

static final int SO_TIMEOUT。 Set a timeout on blocking Socket operations:

ServerSocket.accept();
SocketInputStream.read();
DatagramSocket.receive();

The option must be set prior to entering a blocking operation to take effect. If the timeout expires and the operation would continue to block, java.io.InterruptedIOException is raised. The Socket is not closed in this case.

Statement Timeout:用来限制statement的执行时长,timeout的值通过调用JDBC的java.sql.Statement.setQueryTimeout(int timeout) API进行设置。不过现在开发者已经很少直接在代码中设置,而多是通过框架来进行设置。

max_execution_time:The execution timeout for SELECT statements, in milliseconds. If the value is 0, timeouts are not enabled. MySQL 属性,可以set修改,一般用来设置一个查询最长不超过多少秒,避免一个慢查询一直在跑,跟statement timeout对应。

Property Value
Command-Line Format --max-execution-time=#
System Variable max_execution_time
Scope Global, Session
Dynamic Yes
Type Integer
Default Value 0

wait_timeout The number of seconds the server waits for activity on a noninteractive connection before closing it. MySQL 属性,一般设置tcp keepalive后这个值基本不会超时(这句话存疑 202110)。

On thread startup, the session wait_timeout value is initialized from the global wait_timeout value or from the global interactive_timeout value, depending on the type of client (as defined by the CLIENT_INTERACTIVE connect option to mysql_real_connect()). See also interactive_timeout.

Property Value
Command-Line Format --wait-timeout=#
System Variable wait_timeout
Scope Global, Session
Dynamic Yes
Type Integer
Default Value 28800
Minimum Value 1
Maximum Value (Other) 31536000
Maximum Value (Windows) 2147483

一般来说应该设置: max_execution_time/statement timeout < Tranction Timeout < socketTimeout

SocketTimeout

这个 httpclient 的bug 就是在 TCP 连接握手成功(只受ConnectTimeout影响,SocketTimeout还不起作用)后,还需要进行 SSL的数据交换(HandShake),但因为httpclient是在连接建立后(含 SSL HandShake)才设置的 SocketTimeout,导致在SSL HandShake的时候卡在了读数据,此时恰好还没设置SocketTimeout,导致连接永久卡死在SSL HandShake的读数据

所以代码的fix方案就是在建连接前就设置好 SocketTimeout。

一次 PreparedStatement 执行

useServerPrepStmts=true&cachePrepStmts=true

5.0.5版本后的驱动 useServerPrepStmts 默认值是false;另外跨Statement是没法重用PreparedStatement预编译的,还需要设置 cachePrepStmts 才可以。

对于打开预编译的URL(String url = “jdbc:mysql://localhost:3306/studb?useServerPrepStmts=true&cachePrepStmts=true”)获取数据库连接之后,本质是获取预编译语句 **pstmt = conn.prepareStatement(sql)**时会向MySQL服务端发送一个RPC,发送一个预编译的SQL模板(驱动会拼接MySQL预编译语句prepare s1 from ‘select * from user where id = ?’),然后MySQL服务端会编译好收到的SQL模板,再会为此预编译模板语句分配一个 serverStatementId发送给JDBC驱动,这样以后PreparedStatement就会持有当前预编译语句的服务端的serverStatementId,并且会把此 PreparedStatement缓存在当前数据库连接中,以后对于相同SQL模板的操作 **pstmt.executeUpdate()**,都用相同的PreparedStatement,执行SQL时只需要发送 serverStatementId 和参数,节省一次SQL编译, 直接执行。并且对于每一个连接(驱动端及MySQL服务端)都有自己的prepare cache,具体的源码实现是在com.mysql.jdbc.ServerPreparedStatement中实现。

根据SQL模板和设置的参数,解析成一条完整的SQL语句,最后根据MySQL协议,序列化成字节流,RPC发送给MySQL服务端

1
2
// 解析封装需要发送的SQL语句,序列化成MySQL协议对应的字节流
Buffer sendPacket = fillSendPacket();

准备好需要发送的MySQL协议的字节流(sendPacket)后,就可以一路通过ConnectionImpl.execSQL –> MysqlIO.sqlQueryDirect –> MysqlIO.send – > OutPutStram.write把字节流数据通过Socket发送给MySQL服务器,然后线程阻塞等待服务端返回结果数据,拿到数据后再根据MySQL协议反序列化成我们熟悉的ResultSet对象。

image-20230802101859567

SocketTimeoutException: Read timed out

如果SQL 超过 SocketTimeout 报错如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#java -cp .:./mysql-connector-java-5.1.45.jar Test "jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useServerPrepStmts=true&cachePrepStmts=true&connectTimeout=15000&socketTimeout=3700" root 123 "select sleep(10), id from sbtest1 where id= ?" 100
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

The last packet successfully received from the server was 3,705 milliseconds ago. The last packet sent successfully to the server was 3,705 milliseconds ago.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:990)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3559)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3459)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3900)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2527)
at com.mysql.jdbc.ServerPreparedStatement.serverExecute(ServerPreparedStatement.java:1283)
at com.mysql.jdbc.ServerPreparedStatement.executeInternal(ServerPreparedStatement.java:783)
at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1966)
at Test.main(Test.java:31)
Caused by: java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3008)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3469)
... 7 more

连接超时

在防火墙里设置了 3306 的包都 drop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#java -cp .:./mysql-connector-java-5.1.45.jar Test "jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useServerPrepStmts=true&cachePrepStmts=true&connectTimeout=15000&socketTimeout=3700" root 123 "select sleep(10), id from sbtest1 where id= ?" 100
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:990)
at com.mysql.jdbc.MysqlIO.<init>(MysqlIO.java:341)
at com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2186)
at com.mysql.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:2219)
at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2014)
at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:776)
at com.mysql.jdbc.JDBC4Connection.<init>(JDBC4Connection.java:47)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:386)
at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:330)
at java.sql.DriverManager.getConnection(DriverManager.java:664)
at java.sql.DriverManager.getConnection(DriverManager.java:247)
at Test.main(Test.java:26)
Caused by: java.net.ConnectException: 连接超时 (Connection timed out)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:607)
at com.mysql.jdbc.StandardSocketFactory.connect(StandardSocketFactory.java:211)
at com.mysql.jdbc.MysqlIO.<init>(MysqlIO.java:300)
... 15 more

SocketException connection timed out

image-20240522165928366

image-20240522165849942

案例

设置JDBC参数不合理(不设置的话默认值是:queryTimeout=10s,socketTimeout=10s),会导致在异常情况下,第二条get获得了第一条的结果,拿到了错误的数据,数据库则表现正常

socketTimeout触发后,连接抛CommunicationsException(严重异常,触发后连接应该断开), 但JDBC会检查请求是否被cancle了,如果cancle就会抛出MySQLTimeoutException异常,这是一个普通异常,连接会被重新放回连接池重用(导致下一个获取这个连接的线程可能会得到前一个请求的response)。

queryTimeout(queryTimeoutKillsConnection=True–来强制关闭连接)会触发启动一个新的连接向server发送 kill id的命令,MySQL5.7增加了max_statement_time/max_execution_time来做到在server上直接检测到这种查询,然后结束掉。

jdbc 和 RDS之间 socket_timeout

jdbc驱动设置socketTimeout=1459,如果是socketTimeout触发客户端断开后,server端的SQL会继续执行,如果是client被kill则server端的SQL会被终止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# java -cp /home/admin/drds-server/lib/*:. Test "jdbc:mysql://172.16.40.215:3008/bank_000000?socketTimeout=1459" "user" "pass" "select sleep(2)" "1"
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

The last packet successfully received from the server was 1,461 milliseconds ago. The last packet sent successfully to the server was 1,461 milliseconds ago.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:80)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3749)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3649)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4090)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2658)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2811)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2806)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2764)
at com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1399)
at Test.main(Test.java:29)
Caused by: java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3183)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3659)
... 8 more

或者开协程后的错误堆栈
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

The last packet successfully received from the server was 1,460 milliseconds ago. The last packet sent successfully to the server was 1,459 milliseconds ago.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:80)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3749)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3649)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4090)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2658)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2811)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2806)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2764)
at com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1399)
at Test.main(Test.java:29)
Caused by: java.net.SocketTimeoutException: time out
at sun.nio.ch.WispSocketImpl$1$1.read0(WispSocketImpl.java:244)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:208)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:201)
at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3183)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3659)
... 8 more

对应抓包,没有 kill动作

image-20220601141709318

CN 和 DN 间socket_timeout案例

设置CN到DN的socket_timeout为2秒,然后执行一个sleep

CN上抓包分析(stream 5是客户端到CN、stream6是CN到DN)如下,首先CN会计时2秒钟后发送quit给DN,然后断开和DN的连接,并返回一个错误给client,client发送quit断开连接:

image-20220601122556415

CN完整报错堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
2022-06-01 12:10:00.178 [ServerExecutor-bucket-2-19-thread-181] ERROR com.alibaba.druid.pool.DruidPooledStatement - [user=polardbx_root,host=10.101.32.6,port=43947,schema=bank] CommunicationsException, druid version 1.1.24, jdbcUrl : jdbc:mysql://172.16.40.215:3008/bank_000000?maintainTimeStats=false&rewriteBatchedStatements=false&failOverReadOnly=false&cacheResultSetMetadata=true&allowMultiQueries=true&clobberStreamingResults=true&autoReconnect=false&usePsMemOptimize=true&useServerPrepStmts=true&netTimeoutForStreamingResults=0&useSSL=false&metadataCacheSize=256&readOnlyPropagatesToServer=false&prepStmtCacheSqlLimit=4096&connectTimeout=5000&socketTimeout=9000000&cachePrepStmts=true&characterEncoding=utf8&prepStmtCacheSize=256, testWhileIdle true, idle millis 11861, minIdle 5, poolingCount 4, timeBetweenEvictionRunsMillis 60000, lastValidIdleMillis 11861, driver com.mysql.jdbc.Driver, exceptionSorter com.alibaba.polardbx.common.jdbc.sorter.MySQLExceptionSorter
2022-06-01 12:10:00.179 [ServerExecutor-bucket-2-19-thread-181] ERROR com.alibaba.druid.pool.DruidDataSource - [user=polardbx_root,host=10.101.32.6,port=43947,schema=bank] discard connection
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
at sun.reflect.GeneratedConstructorAccessor72.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3749)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3649)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4090)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2658)
at com.mysql.jdbc.ServerPreparedStatement.serverExecute(ServerPreparedStatement.java:1281)
at com.mysql.jdbc.ServerPreparedStatement.executeInternal(ServerPreparedStatement.java:782)
at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1367)
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:497)
at com.alibaba.polardbx.group.jdbc.TGroupDirectPreparedStatement.execute(TGroupDirectPreparedStatement.java:84)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQueryInner(MyJdbcHandler.java:1133)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQuery(MyJdbcHandler.java:990)
at com.alibaba.polardbx.repo.mysql.spi.MyPhyQueryCursor.doInit(MyPhyQueryCursor.java:83)
at com.alibaba.polardbx.executor.cursor.AbstractCursor.init(AbstractCursor.java:53)
at com.alibaba.polardbx.repo.mysql.spi.MyPhyQueryCursor.<init>(MyPhyQueryCursor.java:67)
at com.alibaba.polardbx.repo.mysql.spi.CursorFactoryMyImpl.repoCursor(CursorFactoryMyImpl.java:42)
at com.alibaba.polardbx.repo.mysql.handler.MyPhyQueryHandler.handle(MyPhyQueryHandler.java:24)
at com.alibaba.polardbx.executor.handler.HandlerCommon.handlePlan(HandlerCommon.java:102)
at com.alibaba.polardbx.executor.AbstractGroupExecutor.executeInner(AbstractGroupExecutor.java:58)
at com.alibaba.polardbx.executor.AbstractGroupExecutor.execByExecPlanNode(AbstractGroupExecutor.java:36)
at com.alibaba.polardbx.executor.TopologyExecutor.execByExecPlanNode(TopologyExecutor.java:34)
at com.alibaba.polardbx.transaction.TransactionExecutor.execByExecPlanNode(TransactionExecutor.java:120)
at com.alibaba.polardbx.executor.ExecutorHelper.executeByCursor(ExecutorHelper.java:155)
at com.alibaba.polardbx.executor.ExecutorHelper.execute(ExecutorHelper.java:70)
at com.alibaba.polardbx.executor.PlanExecutor.execByExecPlanNodeByOne(PlanExecutor.java:130)
at com.alibaba.polardbx.executor.PlanExecutor.execute(PlanExecutor.java:75)
at com.alibaba.polardbx.matrix.jdbc.TConnection.executeQuery(TConnection.java:682)
at com.alibaba.polardbx.matrix.jdbc.TConnection.executeSQL(TConnection.java:457)
at com.alibaba.polardbx.matrix.jdbc.TPreparedStatement.executeSQL(TPreparedStatement.java:65)
at com.alibaba.polardbx.matrix.jdbc.TStatement.executeInternal(TStatement.java:133)
at com.alibaba.polardbx.matrix.jdbc.TPreparedStatement.execute(TPreparedStatement.java:50)
at com.alibaba.polardbx.server.ServerConnection.innerExecute(ServerConnection.java:1131)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:883)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:850)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:844)
at com.alibaba.polardbx.server.handler.SelectHandler.handle(SelectHandler.java:82)
at com.alibaba.polardbx.server.handler.SelectHandler.handle(SelectHandler.java:31)
at com.alibaba.polardbx.server.ServerQueryHandler.executeSql(ServerQueryHandler.java:155)
at com.alibaba.polardbx.server.ServerQueryHandler.executeStatement(ServerQueryHandler.java:133)
at com.alibaba.polardbx.server.ServerQueryHandler.queryRaw(ServerQueryHandler.java:118)
at com.alibaba.polardbx.net.FrontendConnection.query(FrontendConnection.java:460)
at com.alibaba.polardbx.net.handler.FrontendCommandHandler.handle(FrontendCommandHandler.java:49)
at com.alibaba.polardbx.net.FrontendConnection.lambda$handleData$0(FrontendConnection.java:753)
at com.alibaba.polardbx.common.utils.thread.RunnableWithCpuCollector.run(RunnableWithCpuCollector.java:36)
at com.alibaba.polardbx.common.utils.thread.ServerThreadPool$RunnableAdapter.run(ServerThreadPool.java:793)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:874)
at com.alibaba.wisp.engine.WispTask.runOutsideWisp(WispTask.java:277)
at com.alibaba.wisp.engine.WispTask.runCommand(WispTask.java:252)
at com.alibaba.wisp.engine.WispTask.access$100(WispTask.java:33)
at com.alibaba.wisp.engine.WispTask$CacheableCoroutine.run(WispTask.java:223)
at java.dyn.CoroutineBase.startInternal(CoroutineBase.java:60)
Caused by: java.net.SocketTimeoutException: time out
at sun.nio.ch.WispSocketImpl$1$1.read0(WispSocketImpl.java:244)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:208)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:201)
at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3183)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3659)
... 53 common frames omitted
2022-06-01 12:10:00.179 [ServerExecutor-bucket-2-19-thread-181] WARN com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler - [user=polardbx_root,host=10.101.32.6,port=43947,schema=bank] [TDDL] [1461cdf8b2809000]Execute ERROR on GROUP: BANK_000000_GROUP, ATOM: dskey_bank_000000_group#pxc-xdb-s-pxcunrcbmk4g9lcpk0f24#172.16.40.215-3008#bank_000000, MERGE_UNION_SIZE:1, SQL: /*DRDS /10.101.32.6/1461cdf8b2809000/0// */SELECT SLEEP(?) AS `sleep(236)`, PARAM: [236], ERROR: Communications link failure, tddl version: 5.4.13-16522656
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
at sun.reflect.GeneratedConstructorAccessor72.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3749)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3649)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4090)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2658)
at com.mysql.jdbc.ServerPreparedStatement.serverExecute(ServerPreparedStatement.java:1281)
at com.mysql.jdbc.ServerPreparedStatement.executeInternal(ServerPreparedStatement.java:782)
at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1367)
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:497)
at com.alibaba.polardbx.group.jdbc.TGroupDirectPreparedStatement.execute(TGroupDirectPreparedStatement.java:84)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQueryInner(MyJdbcHandler.java:1133)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQuery(MyJdbcHandler.java:990)
at com.alibaba.polardbx.repo.mysql.spi.MyPhyQueryCursor.doInit(MyPhyQueryCursor.java:83)
at com.alibaba.polardbx.executor.cursor.AbstractCursor.init(AbstractCursor.java:53)
at com.alibaba.polardbx.repo.mysql.spi.MyPhyQueryCursor.<init>(MyPhyQueryCursor.java:67)
at com.alibaba.polardbx.repo.mysql.spi.CursorFactoryMyImpl.repoCursor(CursorFactoryMyImpl.java:42)
at com.alibaba.polardbx.repo.mysql.handler.MyPhyQueryHandler.handle(MyPhyQueryHandler.java:24)
at com.alibaba.polardbx.executor.handler.HandlerCommon.handlePlan(HandlerCommon.java:102)
at com.alibaba.polardbx.executor.AbstractGroupExecutor.executeInner(AbstractGroupExecutor.java:58)
at com.alibaba.polardbx.executor.AbstractGroupExecutor.execByExecPlanNode(AbstractGroupExecutor.java:36)
at com.alibaba.polardbx.executor.TopologyExecutor.execByExecPlanNode(TopologyExecutor.java:34)
at com.alibaba.polardbx.transaction.TransactionExecutor.execByExecPlanNode(TransactionExecutor.java:120)
at com.alibaba.polardbx.executor.ExecutorHelper.executeByCursor(ExecutorHelper.java:155)
at com.alibaba.polardbx.executor.ExecutorHelper.execute(ExecutorHelper.java:70)
at com.alibaba.polardbx.executor.PlanExecutor.execByExecPlanNodeByOne(PlanExecutor.java:130)
at com.alibaba.polardbx.executor.PlanExecutor.execute(PlanExecutor.java:75)
at com.alibaba.polardbx.matrix.jdbc.TConnection.executeQuery(TConnection.java:682)
at com.alibaba.polardbx.matrix.jdbc.TConnection.executeSQL(TConnection.java:457)
at com.alibaba.polardbx.matrix.jdbc.TPreparedStatement.executeSQL(TPreparedStatement.java:65)
at com.alibaba.polardbx.matrix.jdbc.TStatement.executeInternal(TStatement.java:133)
at com.alibaba.polardbx.matrix.jdbc.TPreparedStatement.execute(TPreparedStatement.java:50)
at com.alibaba.polardbx.server.ServerConnection.innerExecute(ServerConnection.java:1131)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:883)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:850)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:844)
at com.alibaba.polardbx.server.handler.SelectHandler.handle(SelectHandler.java:82)
at com.alibaba.polardbx.server.handler.SelectHandler.handle(SelectHandler.java:31)
at com.alibaba.polardbx.server.ServerQueryHandler.executeSql(ServerQueryHandler.java:155)
at com.alibaba.polardbx.server.ServerQueryHandler.executeStatement(ServerQueryHandler.java:133)
at com.alibaba.polardbx.server.ServerQueryHandler.queryRaw(ServerQueryHandler.java:118)
at com.alibaba.polardbx.net.FrontendConnection.query(FrontendConnection.java:460)
at com.alibaba.polardbx.net.handler.FrontendCommandHandler.handle(FrontendCommandHandler.java:49)
at com.alibaba.polardbx.net.FrontendConnection.lambda$handleData$0(FrontendConnection.java:753)
at com.alibaba.polardbx.common.utils.thread.RunnableWithCpuCollector.run(RunnableWithCpuCollector.java:36)
at com.alibaba.polardbx.common.utils.thread.ServerThreadPool$RunnableAdapter.run(ServerThreadPool.java:793)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:874)
at com.alibaba.wisp.engine.WispTask.runOutsideWisp(WispTask.java:277)
at com.alibaba.wisp.engine.WispTask.runCommand(WispTask.java:252)
at com.alibaba.wisp.engine.WispTask.access$100(WispTask.java:33)
at com.alibaba.wisp.engine.WispTask$CacheableCoroutine.run(WispTask.java:223)
at java.dyn.CoroutineBase.startInternal(CoroutineBase.java:60)
Caused by: java.net.SocketTimeoutException: time out
at sun.nio.ch.WispSocketImpl$1$1.read0(WispSocketImpl.java:244)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:208)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:201)
at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3183)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3659)
... 53 common frames omitted
2022-06-01 12:10:00.179 [ServerExecutor-bucket-2-19-thread-181] WARN com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler - [user=polardbx_root,host=10.101.32.6,port=43947,schema=bank] [TDDL] Reset conn socketTimeout failed, lastSocketTimeout is 9000000, tddl version: 5.4.13-16522656
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:80)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.Util.getInstance(Util.java:408)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:918)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:897)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:886)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:860)
at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1326)
at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1321)
at com.mysql.jdbc.ConnectionImpl.setNetworkTimeout(ConnectionImpl.java:5888)
at com.alibaba.polardbx.atom.utils.NetworkUtils.setNetworkTimeout(NetworkUtils.java:18)
at com.alibaba.polardbx.group.jdbc.TGroupDirectConnection.setNetworkTimeout(TGroupDirectConnection.java:433)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.resetPhyConnSocketTimeout(MyJdbcHandler.java:721)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQueryInner(MyJdbcHandler.java:1173)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQuery(MyJdbcHandler.java:990)
at com.alibaba.polardbx.repo.mysql.spi.MyPhyQueryCursor.doInit(MyPhyQueryCursor.java:83)
at com.alibaba.polardbx.executor.cursor.AbstractCursor.init(AbstractCursor.java:53)
at com.alibaba.polardbx.repo.mysql.spi.MyPhyQueryCursor.<init>(MyPhyQueryCursor.java:67)
at com.alibaba.polardbx.repo.mysql.spi.CursorFactoryMyImpl.repoCursor(CursorFactoryMyImpl.java:42)
at com.alibaba.polardbx.repo.mysql.handler.MyPhyQueryHandler.handle(MyPhyQueryHandler.java:24)
at com.alibaba.polardbx.executor.handler.HandlerCommon.handlePlan(HandlerCommon.java:102)
at com.alibaba.polardbx.executor.AbstractGroupExecutor.executeInner(AbstractGroupExecutor.java:58)
at com.alibaba.polardbx.executor.AbstractGroupExecutor.execByExecPlanNode(AbstractGroupExecutor.java:36)
at com.alibaba.polardbx.executor.TopologyExecutor.execByExecPlanNode(TopologyExecutor.java:34)
at com.alibaba.polardbx.transaction.TransactionExecutor.execByExecPlanNode(TransactionExecutor.java:120)
at com.alibaba.polardbx.executor.ExecutorHelper.executeByCursor(ExecutorHelper.java:155)
at com.alibaba.polardbx.executor.ExecutorHelper.execute(ExecutorHelper.java:70)
at com.alibaba.polardbx.executor.PlanExecutor.execByExecPlanNodeByOne(PlanExecutor.java:130)
at com.alibaba.polardbx.executor.PlanExecutor.execute(PlanExecutor.java:75)
at com.alibaba.polardbx.matrix.jdbc.TConnection.executeQuery(TConnection.java:682)
at com.alibaba.polardbx.matrix.jdbc.TConnection.executeSQL(TConnection.java:457)
at com.alibaba.polardbx.matrix.jdbc.TPreparedStatement.executeSQL(TPreparedStatement.java:65)
at com.alibaba.polardbx.matrix.jdbc.TStatement.executeInternal(TStatement.java:133)
at com.alibaba.polardbx.matrix.jdbc.TPreparedStatement.execute(TPreparedStatement.java:50)
at com.alibaba.polardbx.server.ServerConnection.innerExecute(ServerConnection.java:1131)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:883)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:850)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:844)
at com.alibaba.polardbx.server.handler.SelectHandler.handle(SelectHandler.java:82)
at com.alibaba.polardbx.server.handler.SelectHandler.handle(SelectHandler.java:31)
at com.alibaba.polardbx.server.ServerQueryHandler.executeSql(ServerQueryHandler.java:155)
at com.alibaba.polardbx.server.ServerQueryHandler.executeStatement(ServerQueryHandler.java:133)
at com.alibaba.polardbx.server.ServerQueryHandler.queryRaw(ServerQueryHandler.java:118)
at com.alibaba.polardbx.net.FrontendConnection.query(FrontendConnection.java:460)
at com.alibaba.polardbx.net.handler.FrontendCommandHandler.handle(FrontendCommandHandler.java:49)
at com.alibaba.polardbx.net.FrontendConnection.lambda$handleData$0(FrontendConnection.java:753)
at com.alibaba.polardbx.common.utils.thread.RunnableWithCpuCollector.run(RunnableWithCpuCollector.java:36)
at com.alibaba.polardbx.common.utils.thread.ServerThreadPool$RunnableAdapter.run(ServerThreadPool.java:793)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:874)
at com.alibaba.wisp.engine.WispTask.runOutsideWisp(WispTask.java:277)
at com.alibaba.wisp.engine.WispTask.runCommand(WispTask.java:252)
at com.alibaba.wisp.engine.WispTask.access$100(WispTask.java:33)
at com.alibaba.wisp.engine.WispTask$CacheableCoroutine.run(WispTask.java:223)
at java.dyn.CoroutineBase.startInternal(CoroutineBase.java:60)
2022-06-01 12:10:00.179 [ServerExecutor-bucket-2-19-thread-181] WARN com.alibaba.polardbx.executor.ExecutorHelper - [user=polardbx_root,host=10.101.32.6,port=43947,schema=bank] [TDDL] PhyQuery(node="BANK_000000_GROUP", sql="SELECT SLEEP(?) AS `sleep(236)`")
, tddl version: 5.4.13-16522656
2022-06-01 12:10:00.180 [ServerExecutor-bucket-2-19-thread-181] WARN com.alibaba.polardbx.server.ServerConnection - [user=polardbx_root,host=10.101.32.6,port=43947,schema=bank] [TDDL] [ERROR-CODE: 3009][1461cdf8b2809000] SQL: /*+TDDL:node(0) and SOCKET_TIMEOUT=2000 */ select sleep(236), tddl version: 5.4.13-16522656
com.alibaba.polardbx.common.exception.TddlRuntimeException: ERR-CODE: [TDDL-4614][ERR_EXECUTE_ON_MYSQL] Error occurs when execute on GROUP 'BANK_000000_GROUP' ATOM 'dskey_bank_000000_group#pxc-xdb-s-pxcunrcbmk4g9lcpk0f24#172.16.40.215-3008#bank_000000': Communications link failure
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.handleException(MyJdbcHandler.java:1935)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.generalHandlerException(MyJdbcHandler.java:1911)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQueryInner(MyJdbcHandler.java:1168)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQuery(MyJdbcHandler.java:990)
at com.alibaba.polardbx.repo.mysql.spi.MyPhyQueryCursor.doInit(MyPhyQueryCursor.java:83)
at com.alibaba.polardbx.executor.cursor.AbstractCursor.init(AbstractCursor.java:53)
at com.alibaba.polardbx.repo.mysql.spi.MyPhyQueryCursor.<init>(MyPhyQueryCursor.java:67)
at com.alibaba.polardbx.repo.mysql.spi.CursorFactoryMyImpl.repoCursor(CursorFactoryMyImpl.java:42)
at com.alibaba.polardbx.repo.mysql.handler.MyPhyQueryHandler.handle(MyPhyQueryHandler.java:24)
at com.alibaba.polardbx.executor.handler.HandlerCommon.handlePlan(HandlerCommon.java:102)
at com.alibaba.polardbx.executor.AbstractGroupExecutor.executeInner(AbstractGroupExecutor.java:58)
at com.alibaba.polardbx.executor.AbstractGroupExecutor.execByExecPlanNode(AbstractGroupExecutor.java:36)
at com.alibaba.polardbx.executor.TopologyExecutor.execByExecPlanNode(TopologyExecutor.java:34)
at com.alibaba.polardbx.transaction.TransactionExecutor.execByExecPlanNode(TransactionExecutor.java:120)
at com.alibaba.polardbx.executor.ExecutorHelper.executeByCursor(ExecutorHelper.java:155)
at com.alibaba.polardbx.executor.ExecutorHelper.execute(ExecutorHelper.java:70)
at com.alibaba.polardbx.executor.PlanExecutor.execByExecPlanNodeByOne(PlanExecutor.java:130)
at com.alibaba.polardbx.executor.PlanExecutor.execute(PlanExecutor.java:75)
at com.alibaba.polardbx.matrix.jdbc.TConnection.executeQuery(TConnection.java:682)
at com.alibaba.polardbx.matrix.jdbc.TConnection.executeSQL(TConnection.java:457)
at com.alibaba.polardbx.matrix.jdbc.TPreparedStatement.executeSQL(TPreparedStatement.java:65)
at com.alibaba.polardbx.matrix.jdbc.TStatement.executeInternal(TStatement.java:133)
at com.alibaba.polardbx.matrix.jdbc.TPreparedStatement.execute(TPreparedStatement.java:50)
at com.alibaba.polardbx.server.ServerConnection.innerExecute(ServerConnection.java:1131)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:883)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:850)
at com.alibaba.polardbx.server.ServerConnection.execute(ServerConnection.java:844)
at com.alibaba.polardbx.server.handler.SelectHandler.handle(SelectHandler.java:82)
at com.alibaba.polardbx.server.handler.SelectHandler.handle(SelectHandler.java:31)
at com.alibaba.polardbx.server.ServerQueryHandler.executeSql(ServerQueryHandler.java:155)
at com.alibaba.polardbx.server.ServerQueryHandler.executeStatement(ServerQueryHandler.java:133)
at com.alibaba.polardbx.server.ServerQueryHandler.queryRaw(ServerQueryHandler.java:118)
at com.alibaba.polardbx.net.FrontendConnection.query(FrontendConnection.java:460)
at com.alibaba.polardbx.net.handler.FrontendCommandHandler.handle(FrontendCommandHandler.java:49)
at com.alibaba.polardbx.net.FrontendConnection.lambda$handleData$0(FrontendConnection.java:753)
at com.alibaba.polardbx.common.utils.thread.RunnableWithCpuCollector.run(RunnableWithCpuCollector.java:36)
at com.alibaba.polardbx.common.utils.thread.ServerThreadPool$RunnableAdapter.run(ServerThreadPool.java:793)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:874)
at com.alibaba.wisp.engine.WispTask.runOutsideWisp(WispTask.java:277)
at com.alibaba.wisp.engine.WispTask.runCommand(WispTask.java:252)
at com.alibaba.wisp.engine.WispTask.access$100(WispTask.java:33)
at com.alibaba.wisp.engine.WispTask$CacheableCoroutine.run(WispTask.java:223)
at java.dyn.CoroutineBase.startInternal(CoroutineBase.java:60)
Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
at sun.reflect.GeneratedConstructorAccessor72.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3749)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3649)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4090)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2658)
at com.mysql.jdbc.ServerPreparedStatement.serverExecute(ServerPreparedStatement.java:1281)
at com.mysql.jdbc.ServerPreparedStatement.executeInternal(ServerPreparedStatement.java:782)
at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1367)
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:497)
at com.alibaba.polardbx.group.jdbc.TGroupDirectPreparedStatement.execute(TGroupDirectPreparedStatement.java:84)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQueryInner(MyJdbcHandler.java:1133)
... 44 common frames omitted
Caused by: java.net.SocketTimeoutException: time out
at sun.nio.ch.WispSocketImpl$1$1.read0(WispSocketImpl.java:244)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:208)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:201)
at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3183)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3659)
... 53 common frames omitted

应用和 DB 间丢包导致 keepalive 心跳失败

应用使用了 Druid 连接池来维护到 DB 间的所有长连接

应用和 DB 间丢包导致 keepalive 心跳失败,进而 OS会断开这个连接

image-20230322171621838

一个连接归还给Druid连接池都要做清理动作,就是第一个红框的rollback/autocommit=1

归还后OS 层面会探活TCP 连接,DB(4381端口)多次后多次不响应keepalive 后,OS 触发reset tcp断开连接,此时上层应用(比如Druid连接池、比如Tomcat)还不知道此连接在OS 层面已经断开

1
2
3
4
#sysctl -a |grep -i keepalive
net.ipv4.tcp_keepalive_intvl = 3
net.ipv4.tcp_keepalive_probes = 60
net.ipv4.tcp_keepalive_time = 20

继续过来一个新连接,业务取到这个连接执行查询就会报如下错误:

1
2
3
4
5
6
7
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

The last packet successfully received from the server was 162,776 milliseconds ago. The last packet sent successfully to the server was 162,776 milliseconds ago.

com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

The last packet successfully received from the server was 162,776 milliseconds ago. The last packet sent successfully to the server was 162,776 milliseconds ago.

这个错误就是因为OS层面连接断开了,并且断开了162秒(和截图时间戳能对应上)

对应的错误堆栈:

1
2
3
4
5
6
7
8
Caused by: java.net.SocketException: Connection timed out (Write failed)
at java.net.SocketOutputStream.socketWrite0(Native Method)
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
at java.net.SocketOutputStream.write(SocketOutputStream.java:155)
at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3725)
... 46 common frames omitted

kill 案例

kill mysql client

mysql client连cn执行一个很慢的SQL,然后kill掉mysql client

cn报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
2022-06-01 11:45:59.063 [ServerExecutor-bucket-0-17-thread-158] ERROR com.alibaba.druid.pool.DruidPooledStatement - [user=polardbx_root,host=10.101.32.6,port=50684,schema=bank] CommunicationsException, druid version 1.1.24, jdbcUrl : jdbc:mysql://172.16.40.215:3008/bank_000000?maintainTimeStats=false&rewriteBatchedStatements=false&failOverReadOnly=false&cacheResultSetMetadata=true&allowMultiQueries=true&clobberStreamingResults=true&autoReconnect=false&usePsMemOptimize=true&useServerPrepStmts=true&netTimeoutForStreamingResults=0&useSSL=false&metadataCacheSize=256&readOnlyPropagatesToServer=false&prepStmtCacheSqlLimit=4096&connectTimeout=5000&socketTimeout=9000000&cachePrepStmts=true&characterEncoding=utf8&prepStmtCacheSize=256, testWhileIdle true, idle millis 72028, minIdle 5, poolingCount 4, timeBetweenEvictionRunsMillis 60000, lastValidIdleMillis 345734, driver com.mysql.jdbc.Driver, exceptionSorter com.alibaba.polardbx.common.jdbc.sorter.MySQLExceptionSorter
2022-06-01 11:45:59.064 [ServerExecutor-bucket-0-17-thread-158] ERROR com.alibaba.druid.pool.DruidDataSource - [user=polardbx_root,host=10.101.32.6,port=50684,schema=bank] discard connection
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
at sun.reflect.GeneratedConstructorAccessor72.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
…………
at com.alibaba.wisp.engine.WispTask$CacheableCoroutine.run(WispTask.java:223)
at java.dyn.CoroutineBase.startInternal(CoroutineBase.java:60)
Caused by: java.net.SocketException: Socket is closed
at java.net.Socket.getSoTimeout(Socket.java:1291)
at sun.nio.ch.WispSocketImpl$1$1.read0(WispSocketImpl.java:249)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:208)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:201)
at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3183)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3659)
... 53 common frames omitted
2022-06-01 11:45:59.065 [ServerExecutor-bucket-0-17-thread-158] WARN com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler - [user=polardbx_root,host=10.101.32.6,port=50684,schema=bank] [TDDL] [1461c86bbe809001]Execute ERROR on GROUP: BANK_000000_GROUP, ATOM: dskey_bank_000000_group#pxc-xdb-s-pxcunrcbmk4g9lcpk0f24#172.16.40.215-3008#bank_000000, MERGE_UNION_SIZE:1, SQL: /*DRDS /10.101.32.6/1461c86bbe809001/0// */SELECT SLEEP(?) AS `sleep(236)`, PARAM: [236], ERROR: Communications link failure, tddl version: 5.4.13-16522656
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
at sun.reflect.GeneratedConstructorAccessor72.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
…………
at java.dyn.CoroutineBase.startInternal(CoroutineBase.java:60)
Caused by: java.net.SocketException: Socket is closed
at java.net.Socket.getSoTimeout(Socket.java:1291)
at sun.nio.ch.WispSocketImpl$1$1.read0(WispSocketImpl.java:249)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:208)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:201)
at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3183)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3659)
... 53 common frames omitted
2022-06-01 11:45:59.065 [ServerExecutor-bucket-0-17-thread-158] WARN com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler - [user=polardbx_root,host=10.101.32.6,port=50684,schema=bank] [TDDL] Reset conn socketTimeout failed, lastSocketTimeout is 9000000, tddl version: 5.4.13-16522656
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:80)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
…………
at com.alibaba.wisp.engine.WispTask$CacheableCoroutine.run(WispTask.java:223)
at java.dyn.CoroutineBase.startInternal(CoroutineBase.java:60)
2022-06-01 11:45:59.065 [ServerExecutor-bucket-0-17-thread-158] WARN com.alibaba.polardbx.executor.ExecutorHelper - [user=polardbx_root,host=10.101.32.6,port=50684,schema=bank] [TDDL] PhyQuery(node="BANK_000000_GROUP", sql="SELECT SLEEP(?) AS `sleep(236)`")
, tddl version: 5.4.13-16522656
2022-06-01 11:45:59.066 [ServerExecutor-bucket-0-17-thread-158] ERROR com.alibaba.polardbx.server.ServerConnection - [user=polardbx_root,host=10.101.32.6,port=50684,schema=bank] [TDDL] Interrupted unexpectedly for 1461c86bbe809001, tddl version: 5.4.13-16522656
java.lang.InterruptedException: null
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1310)
at com.alibaba.polardbx.common.utils.BooleanMutex$Sync.innerGet(BooleanMutex.java:136)
at com.alibaba.polardbx.common.utils.BooleanMutex.get(BooleanMutex.java:53)
at com.alibaba.polardbx.common.utils.thread.ServerThreadPool.waitByTraceId(ServerThreadPool.java:445)
at com.alibaba.polardbx.server.ServerConnection.innerExecute(ServerConnection.java:1291)
……
at com.alibaba.wisp.engine.WispTask.access$100(WispTask.java:33)
at com.alibaba.wisp.engine.WispTask$CacheableCoroutine.run(WispTask.java:223)
at java.dyn.CoroutineBase.startInternal(CoroutineBase.java:60)
2022-06-01 11:45:59.066 [ServerExecutor-bucket-0-17-thread-158] WARN com.alibaba.polardbx.server.ServerConnection - [user=polardbx_root,host=10.101.32.6,port=50684,schema=bank] [TDDL] [ERROR-CODE: 3009][1461c86bbe809001] SQL: /*+TDDL:node(0) and SOCKET_TIMEOUT=40000 */ select sleep(236), tddl version: 5.4.13-16522656
com.alibaba.polardbx.common.exception.TddlRuntimeException: ERR-CODE: [TDDL-4614][ERR_EXECUTE_ON_MYSQL] Error occurs when execute on GROUP 'BANK_000000_GROUP' ATOM 'dskey_bank_000000_group#pxc-xdb-s-pxcunrcbmk4g9lcpk0f24#172.16.40.215-3008#bank_000000': Communications link failure
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.handleException(MyJdbcHandler.java:1935)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.generalHandlerException(MyJdbcHandler.java:1911)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQueryInner(MyJdbcHandler.java:1168)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQuery(MyJdbcHandler.java:990)
…………
at com.alibaba.wisp.engine.WispTask$CacheableCoroutine.run(WispTask.java:223)
at java.dyn.CoroutineBase.startInternal(CoroutineBase.java:60)
Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
at sun.reflect.GeneratedConstructorAccessor72.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3749)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3649)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4090)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2658)
at com.mysql.jdbc.ServerPreparedStatement.serverExecute(ServerPreparedStatement.java:1281)
at com.mysql.jdbc.ServerPreparedStatement.executeInternal(ServerPreparedStatement.java:782)
at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1367)
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:497)
at com.alibaba.polardbx.group.jdbc.TGroupDirectPreparedStatement.execute(TGroupDirectPreparedStatement.java:84)
at com.alibaba.polardbx.repo.mysql.spi.MyJdbcHandler.executeQueryInner(MyJdbcHandler.java:1133)
... 44 common frames omitted
Caused by: java.net.SocketException: Socket is closed
at java.net.Socket.getSoTimeout(Socket.java:1291)
at sun.nio.ch.WispSocketImpl$1$1.read0(WispSocketImpl.java:249)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:208)
at sun.nio.ch.WispSocketImpl$1$1.read(WispSocketImpl.java:201)
at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3183)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3659)
... 53 common frames omitted
2022-06-01 11:45:59.071 [KillExecutor-15-thread-49] WARN com.alibaba.polardbx.server.ServerConnection - [user=polardbx_root,host=10.101.32.6,port=50684,schema=bank] [TDDL] Connection Killed, tddl version: 5.4.13-16522656

mysqld报错:

1
2022-06-01T11:45:58.915371+08:00 8218735 [Note] Aborted connection 8218735 to db: 'bank_000000' user: 'rds_polardb_x' host: '172.16.40.214' (Got an error reading communication packets)

172.16.40.214是客户端IP

抓包看到CN收到mysql client发过来的fin,CN回复fin断开连接

CN会给DN在新的连接上发Kill Query(stream 1596),同时会在原来的连接(stream 583)上发fin,然后原来的连接收到DN的response(被kill),然后CN发reset给DN

image-20220601120626629

下图是sleep 连接的收发包

image-20220601120417026

Kill jdbc client

Java jdbc client被kill后没有错误堆栈,kill后触发socket.close(对应client发送fin断开连接),kill后server端SQL也被立即中断

抓包:

image-20220601143200253

server端报错信息:

1
2022-06-01T14:33:52.204848+08:00 8288839 [Note] Aborted connection 8288839 to db: 'bank_000000' user: 'user' host: '172.16.40.214' (Got an error reading communication packets)

Statement timeout

1
2
3
4
# java -cp /home/admin/drds-server/lib/*:. Test "jdbc:mysql://172.16.40.215:3008/bank_000000?socketTimeout=5459" "user" "pass" "select sleep(180)" "1" 3
com.mysql.jdbc.exceptions.MySQLTimeoutException: Statement cancelled due to timeout or client request
at com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1419)
at Test.main(Test.java:31)

statement会设置一个timer,到时间还没有返回结果就创建一个新连接发送kill query

server 端收到kill后终止SQL执行,抓包看到Server端主动提前返回了错误

image-20220601152401387

参考资料

MySQL JDBC StreamResult通信原理浅析

如何创建一个自己连自己的TCP连接

发表于 2020-07-01 | 分类于 TCP

如何创建一个自己连自己的TCP连接

能不能建立一个tcp连接, src-ip:src-port 等于dest-ip:dest-port 呢?

最近有同时找我,说是发现了一个奇怪的问题,他的 MySQLD listen 28350 端口, Sysbench 和 MySQLD 部署在同一台机器上,然后压 MySQL,只要 MySQL 一挂掉就再也起不来,起不来是因为 28350 端口被 Sysbench 抢走了,如下图,对 Sysbench 来说他已经连上 28350 的 MySQL 了(注意 ESTABLISHED 状态):

img

所以问题就是能不能建立一个自己连自己的连接呢?建立后有什么现象和后果?

测试

执行

1
# nc 192.168.0.79 18082 -p 18082

然后就能看到

1
2
# netstat -ant |grep 18082
tcp 0 0 192.168.0.79:18082 192.168.0.79:18082 ESTABLISHED

比较神奇,这个连接的srcport等于destport,并且完全可以工作,也能收发数据。这有点颠覆大家的理解,端口能重复使用?

port range

我们都知道linux下本地端口范围由参数控制

1
2
# cat /proc/sys/net/ipv4/ip_local_port_range 
10000 65535

所以也经常看到一个误解:一台机器上最多能创建65535个TCP连接

到底一台机器上最多能创建多少个TCP连接

在内存、文件句柄足够的话可以创建的连接是没有限制的,那么/proc/sys/net/ipv4/ip_local_port_range指定的端口范围到底是什么意思呢?

一个TCP连接只要保证四元组(src-ip src-port dest-ip dest-port)唯一就可以了,而不是要求src port唯一,比如:

1
2
3
4
5
6
# netstat -ant |grep 18089
tcp 0 0 192.168.1.79:18089 192.168.1.79:22 ESTABLISHED
tcp 0 0 192.168.1.79:18089 192.168.1.79:18080 ESTABLISHED
tcp 0 0 192.168.0.79:18089 192.168.0.79:22 TIME_WAIT
tcp 0 0 192.168.1.79:22 192.168.1.79:18089 ESTABLISHED
tcp 0 0 192.168.1.79:18080 192.168.1.79:18089 ESTABLISHED

从前三行可以清楚地看到18089被用了三次,第一第二行src-ip、dest-ip也是重复的,但是dest port不一样,第三行的src-port还是18089,但是src-ip变了。

所以一台机器能创建的TCP连接是没有限制的,而ip_local_port_range是指没有bind的时候OS随机分配端口的范围,但是分配到的端口要同时满足五元组唯一,这样 ip_local_port_range 限制的是连同一个目标(dest-ip和dest-port一样)的port的数量(请忽略本地多网卡的情况,因为dest-ip为以后route只会选用一个本地ip)。

但是如果程序调用的是bind函数(bind(ip,port=0))这个时候是让系统绑定到某个网卡和自动分配的端口,此时系统没有办法确定接下来这个socket是要去connect还是listen. 如果是listen的话,那么肯定是不能出现端口冲突的,如果是connect的话,只要满足4元组唯一即可。在这种情况下,系统只能尽可能满足更强的要求,就是先要求端口不能冲突,即使之后去connect的时候4元组是唯一的。

bind()的时候内核是还不知道四元组的,只知道src_ip、src_port,所以这个时候单网卡下src_port是没法重复的,但是connect()的时候已经知道了四元组的全部信息,所以只要保证四元组唯一就可以了,那么这里的src_port完全是可以重复使用的。

自己连自己的连接

我们来看自己连自己发生了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# strace nc 192.168.0.79 18084 -p 18084
execve("/usr/bin/nc", ["nc", "192.168.0.79", "18084", "-p", "18084"], [/* 31 vars */]) = 0
brk(NULL) = 0x23d4000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f213f394000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=23295, ...}) = 0
mmap(NULL, 23295, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f213f38e000
close(3) = 0
open("/lib64/libssl.so.10", O_RDONLY|O_CLOEXEC) = 3
………………
munmap(0x7f213f393000, 4096) = 0
open("/usr/share/ncat/ca-bundle.crt", O_RDONLY) = -1 ENOENT (No such file or directory)
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
fcntl(3, F_GETFL) = 0x2 (flags O_RDWR)
fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(18084), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
//注意这里bind后直接就是connect,没有listen
connect(3, {sa_family=AF_INET, sin_port=htons(18084), sin_addr=inet_addr("192.168.0.79")}, 16) = -1 EINPROGRESS (Operation now in progress)
select(4, [3], [3], [3], {10, 0}) = 1 (out [3], left {9, 999998})
getsockopt(3, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
select(4, [0 3], [], [], NULL

抓包看看,正常三次握手,但是syn的seq和syn+ack的seq是一样的

image.png

这个连接算是常说的TCP simultaneous open,simultaneous open指的是两个不同port同时发syn建连接。而这里是先创建了一个socket,然后socket bind到18084端口上(作为local port,因为nc指定了local port),然后执行 connect, 连接到的目标也是192.168.0.79:18084,而这个目标正好是刚刚创建的socket,也就是自己连自己(连接双方总共只有一个socket)。因为一个socket充当了两个角色(client、server),握手的时候发syn,自己收到自己发的syn,就相当于两个角色simultaneous open了。

正常一个连接一定需要两个socket参与(这两个socket不一定要在两台机器上),而这个连接只用了一个socket就创建了,还能正常传输数据。但是仔细观察发数据的时候发放的seq增加(注意tcp_len 11那里的seq),收方的seq也增加了11,这是因为本来这就是用的同一个socket。正常两个socket通讯不是这样的。

那么这种情况为什么没有当做bug被处理呢?

TCP simultanous open

在tcp连接的定义中,通常都是一方先发起连接,假如两边同时发起连接,也就是两个socket同时给对方发 syn 呢? 这在内核中是支持的,就叫同时打开(simultaneous open)。

image.png

​ 摘自《tcp/ip卷1》

可以清楚地看到这个连接建立用了四次握手,然后连接建立了,当然也有 simultanous close(3次挥手成功关闭连接)。如下内核代码 net/ipv4/tcp_input.c 的5924行中就说明了允许这种自己连自己的连接(当然也允许simultanous open). 也就是允许一个socket本来应该收到 syn+ack(发出syn后), 结果收到了syn的情况,而一个socket自己连自己又是这种情况的特例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th)
{
5916 /* PAWS check. */
//PAWS机制全称Protect Againest Wrapped Sequence numbers,
//目的是为了解决在高带宽下,TCP序号可能被重复使用而带来的问题。
5917 if (tp->rx_opt.ts_recent_stamp && tp->rx_opt.saw_tstamp &&
5918 tcp_paws_reject(&tp->rx_opt, 0))
5919 goto discard_and_undo;
5920 //在socket发送syn后收到了一个syn(正常应该收到syn+ack),这里是允许的。
5921 if (th->syn) {
5922 /* We see SYN without ACK. It is attempt of
5923 * simultaneous connect with crossed SYNs.
5924 * Particularly, it can be connect to self. //自己连自己
5925 */
5926 tcp_set_state(sk, TCP_SYN_RECV);
5927
5928 if (tp->rx_opt.saw_tstamp) {
5929 tp->rx_opt.tstamp_ok = 1;
5930 tcp_store_ts_recent(tp);
5931 tp->tcp_header_len =
5932 sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
5933 } else {
5934 tp->tcp_header_len = sizeof(struct tcphdr);
5935 }
5936
5937 tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
5938 tp->copied_seq = tp->rcv_nxt;
5939 tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;
5940
5941 /* RFC1323: The window in SYN & SYN/ACK segments is
5942 * never scaled.
5943 */

也就是在发送syn进入SYN_SENT状态之后,收到对端发来的syn包后不会RST,而是处理流程如下,调用tcp_set_state(sk, TCP_SYN_RECV)进入SYN_RECV状态,以及调用tcp_send_synack(sk)向对端发送syn+ack。

自己连自己的原理解释

第一我们要理解Kernel是支持simultaneous open(同时打开)的,也就是说socket发走syn后,本来应该收到一个syn+ack的,但是实际收到了一个syn(没有ack),这是允许的。这叫TCP连接同时打开(同时给对方发syn),四次握手然后建立连接成功。

自己连自己又是simultaneous open的一个特例,特别在这个连接只有一个socket参与,发送、接收都是同一个socket,自然也会是发syn后收到了自己的syn(自己发给自己),然后依照simultaneous open连接也能创建成功。

这个bind到18084 local port的socket又要连接到 18084 port上,而这个18084 socket已经bind到了socket(也就是自己),就形成了两个socket 的simultaneous open一样,内核又允许这种simultaneous open,所以就形成了自己连自己,也就是一个socket在自己给自己收发数据,所以看到收方和发放的seq是一样的。

可以用python来重现这个连接连自己的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import socket
import time

connected=False
while (not connected):
try:
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_TCP,socket.TCP_NODELAY,1)
sock.bind(('', 18084)) //sock 先bind到18084
sock.connect(('127.0.0.1',18084)) //然后同一个socket连自己
connected=True
except socket.error,(value,message):
print message

if not connected:
print "reconnect"

print "tcp self connection occurs!"
print "netstat -an|grep 18084"
time.sleep(1800)

这里connect前如果没有bind那么系统就会从 local port range 分配一个可用port。

bind成功后会将ip+port放入hash表来判重,这就是我们常看到的 Bind to *** failed (IOD #1): Address already in use 异常。所以一台机器上,如果有多个ip,是可以将同一个port bind多次的,但是bind的时候如果不指定ip,也就是bind(‘0’, port) 还是会冲突。

connect成功后会将四元组放入ehash来判定连接的重复性。如果connect四元组冲突了就会报如下错误

1
2
# nc 192.168.0.82 8080 -p 29798 -s 192.168.0.79
Ncat: Cannot assign requested address.

问题解决

知道原因就好解决了,有如下两个方案

  1. 正常应该通过 port_range 限制随机端口的使用范围(就是给 Sysbench 这些客户端使用的),而 Listen 使用的端口在 port_range 之外,这样就不会出现自己连自己的连接了
  2. 将 listen 端口添加到 /proc/sys/net/ipv4/ip_local_reserved_ports 中

方案2示例(推荐该方案)如下:

1
2
3
4
# echo 3306,32768,1024-3000,32769-65535 >/proc/sys/net/ipv4/ip_local_reserved_ports
//合并连续的区间
# cat /proc/sys/net/ipv4/ip_local_reserved_ports
1024-3000,3306,32768-65535

以上两个方法都可以解决这个问题,方案2 简直是为这种情况量身打造的

bind 和 connect、listen

当对一个TCP socket调用connect函数时,如果这个socket没有bind指定的端口号,操作系统会为它选择一个当前未被使用的端口号,这个端口号被称为ephemeral port, 范围可以在/proc/sys/net/ipv4/ip_local_port_range里查看。假设30000这个端口被选为ephemeral port。

如果这个socket指定了local port那么socket创建后会执行bind将这个socket bind到这个port。比如:

1
2
3
4
5
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
fcntl(3, F_GETFL) = 0x2 (flags O_RDWR)
fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(18084), sin_addr=inet_addr("0.0.0.0")}, 16) = 0

image.png

listen

image-20200702131215819

参考资料

https://segmentfault.com/a/1190000002396411

linux中TCP的socket、bind、listen、connect和accept的实现

How Linux allows TCP introspection The inner workings of bind and listen on Linux.

https://idea.popcount.org/2014-04-03-bind-before-connect/

程序员如何学习和构建网络知识体系

发表于 2020-05-24 | 分类于 network

程序员如何学习和构建网络知识体系

大家学习网络知识的过程中经常发现当时看懂了,很快又忘了,最典型的比如TCP三次握手、为什么要握手,大家基本都看过,但是种感觉还差那么一点点。都要看是因为面试官总要问,所以不能不知道啊。

我们来看一个典型的面试问题:

问:为什么TCP是可靠的?
答:因为TCP有连接(或者回答因为TCP有握手)

追问:为什么有连接就可靠了?(面试的人估计心里在骂,你这不是傻逼么,有连接就可靠啊)

追问:这个TCP连接的本质是什么?网络上给你保留了一个带宽所以能可靠?
答:……懵了(或者因为TCP有ack,所以可靠)

追问:握手的本质是什么?为什么握手就可靠了
答:因为握手需要ack
追问:那这个ack也只是保证握手可靠,握手是怎么保证后面可靠的?握手本质做了什么事情?

追问:有了ack可靠后还会带来什么问题(比如发一个包ack一下,肯定是可行的,但是效率不行,面试官想知道的是这里TCP怎么传输的,从而引出各个buffer、拥塞窗口的概念)

基本上我发现99%的程序员会回答TCP相对UDP是可靠的,70%以上的程序员会告诉你可靠是因为有ack(其他的会告诉你可靠是因为握手或者有连接),再追问下次就开始王顾左右而言他、胡言乱语。

我的理解:

物理上没有一个连接的东西在这里,udp也类似会占用端口、ip,但是大家都没说过udp的连接。而本质上我们说tcp的握手是指tcp是协商和维护一些状态信息的,这个状态信息就包含seq、ack、窗口/buffer,tcp握手就是协商出来这些初始值。这些状态才是我们平时所说的tcp连接的本质。

这说明大部分程序员对问题的本质的理解上出了问题,或者教科书描述的过于教条不够接地气所以看完书本质没get到。

想想 费曼学习方法 中对事物本质的理解的重要性。

## 重点掌握如下两篇文章

一个网络包是如何到达目的地的 – 这篇可以帮你掌握网络如何运转,在本机上从端口、ip、mac地址如何一层层封上去,链路上每一个点拆开mac看看,拆看ip看看,然后替换mac地址继续扔到链路的下一跳,这样一跳跳到达目的。

对BDP、Buffer、各种窗口、rt的理解和运用 这一篇可以让你入门TCP

以上两篇都是站在程序员的角度来剖析关于网络我们应该掌握哪些,也许第一篇有点像网工要掌握的,实际我不这么认为,目前很流行的微服务化、云原生对网络的要求更高了,大多时候需要程序员去掌握这些,也就是在网络包从你的网卡离开你才有资格呼叫网工,否则成本很高!

我本周还碰到了网络不通的问题

我的测试机器不能连外网(公司安全策略)

走流程申请开通,开通后会在测试机器安装客户端以及安全配置文件

但仍然不通,客户端自检都能通

我的排查就是第一篇文章:ping 公网ip;ip route get 公网-ip;ping 网关;

很快就发现是路由的问题,公网ip正好命中了docker 容器添加的某个路由,以及默认路由缺失

如果我自己不会那就开工单、描述问题、call各种人、申请权限……

我碰到的程序员一看到网络连接异常就吓尿了,不关我的事,网络不通,但是在call人前你至少可以做:

  1. ping ip 通不通(也有个别禁掉了icmp)
  2. telnet ip port通不通
  3. 网络包发出去没有(抓包)
  4. 是不是都不通还是只有你的机器不通

来看一个案例

我第一次看RFC1180的时候是震惊的,觉得讲述的太好了,2000字就把一本教科书的知识阐述的无比清晰、透彻。但是实际上我发现很快就忘了,而且大部分程序员基本都是这样

RFC1180写的确实很好,清晰简洁,图文并茂,结构逻辑合理,但是对于95%的程序员没有什么用,当时看的时候很爽、也觉得自己理解了、学会了,实际上看完几周后就忘得差不多了。问题出在这种RFC偏理论多一点看起来完全没有体感无法感同身受,所以即使似乎当时看懂了,但是忘得也快,需要一篇结合实践的文章来帮助理解

在这个问题上,让我深刻地理解到:

一流的人看RFC就够了,差一些的人看《TCP/IP卷1》,再差些的人要看一个个案例带出来的具体知识的书籍了,比如《wireshark抓包艺术》,人和人的学习能力有差别必须要承认。

也就是我们要认识到每个个人的学习能力的差异,我超级认同这篇文章中的一个评论

看完深有感触,尤其是后面的知识效率和工程效率型的区别。以前总是很中二的觉得自己看一遍就理解记住了,结果一次次失败又怀疑自己的智商是不是有问题,其实就是把自己当作知识效率型来用了。一个不太恰当的形容就是,有颗公主心却没公主命!

嗯,大部分时候我们都觉得自己看一遍就理解了记住了能实用解决问题了,实际上了是马上忘了,停下来想想自己是不是这样的?在网络的相关知识上大部分看RFC、TCP卷1等东西是很难实际理解的,还是要靠实践来建立对知识的具体的理解,而网络相关的东西基本离大家有点远(大家不回去读tcp、ip源码,纯粹是靠对书本的理解),所以很难建立具体的概念,所以这里有个必杀技就是学会抓包和用wireshark看包,同时针对实际碰到的文题来抓包、看包分析。

比如这篇《从计算机知识到落地能力,你欠缺了什么?》就对上述问题最好的阐述,程序员最常碰到的网络问题就是网络为啥不通?

这是最好建立对网络知识具体理解和实践的机会,你把《从计算机知识到落地能力,你欠缺了什么?》实践完再去看RFC1180 就明白了。通过案例把RFC1180抽象的描述给它具体化、场景化了,理解起来就很轻松不容易忘记了。

经验一: 通过具体的东西(案例、抓包)来建立对网络基础的理解

image-20220221151815993

不要追求知识的广度

学习网络知识过程中,不建议每个知识点都去看,因为很快会忘记,我的方法是只看经常碰到的问题点,碰到一个点把他学透理解明白。

比如我曾经碰到过 nslookup OK but ping fail–看看老司机是如何解决问题的,解决问题的方法肯定比知识点重要多了,同时透过一个问题怎么样通篇来理解一大块知识,让这块原理真正在你的知识体系中扎根下来 , 这个问题Google上很多人在搜索,说明很普遍,但是没找到有资料能把这个问题说清楚,所以借着这个机会就把 Linux下的 NSS(name service switch)的原理搞懂了。要不然碰到问题老司机告诉你改下 /etc/hosts 或者 /etc/nsswitch 或者 /etc/resolv.conf 之类的问题就能解决,但是你一直不知道这三个文件怎么起作用的,也就是你碰到过这种问题也解决过但是下次碰到类似的问题你不一定能解决。

当然对我来说为了解决这个问题最后写了4篇跟域名解析相关的文章,从windows到linux,涉及到vpn、glibc、docker等各种场景,我把他叫做场景驱动。后来换来工作环境从windows换到mac后又补了一篇mac下的路由、dns文章。

关于场景驱动学习的方法可以看这篇总结

TCP是最复杂的,要从实用出发

比如拥塞算法基本大家不会用到,了解下就行,你想想你有碰到过因为拥塞算法导致的问题吗?极少是吧。还有拥塞窗口、慢启动,这个实际中碰到的概率不高,面试要问你基本上是属于炫技类型。

实际碰到更多的是传输效率(对BDP、Buffer、各种窗口、rt的理解和运用),还有为什么连不通、连接建立不起来、为什么收到包不回复、为什么要reset、为什么丢包了之类的问题。

关于为什么连不通,我碰到了这个问题,随后在这个问题的基础上进行了总结,得到客户端建连接的时候抛异常,可能的原因(握手失败,建不上连接):

  • 网络不通,诊断:ping ip
  • 端口不通, 诊断:telnet ip port
  • rp_filter 命中(rp_filter=1, 多网卡环境), 诊断: netstat -s | grep -i filter
  • 防火墙、命中iptables 被扔掉了,可以试试22端口起sshd 能否正常访问,能的话说明是端口被干了
  • snat/dnat的时候宿主机port冲突,内核会扔掉 syn包。诊断: sudo conntrack -S | grep insert_failed //有不为0的
  • Firewalld 或者 iptables
  • 全连接队列满的情况,诊断: netstat -s | egrep “listen|LISTEN”
  • syn flood攻击, 诊断:同上
  • 服务端的内核参数 net.ipv4.tcp_tw_recycle(4.12内核删除这个参数了) 和 net.ipv4.tcp_timestamps 的值都为 1时,服务器会检查每一个 SYN报文中的时间戳(Timestamp,跟同一ip下最近一次 FIN包时间对比),若 Timestamp 不是递增的关系,就扔掉这个SYN包(诊断:netstat -s | grep “ passive connections rejected because of time stamp”),常见触发时间戳非递增场景:
    1. 4.10 内核,一直必现大概率性丢包。4.11 改成了 per-destination host的算法
    2. tcpping 这种时间戳按连接随机的,必现大概率持续丢包
    3. 同一个客户端通过直连或者 DNAT 后两条链路到同一个服务端,客户端生成时间戳是 by dst ip,导致大概率持续丢包
    4. 经过NAT/LVS 后多个客户端被当成一个客户端,小概率偶尔出现
    5. 网路链路复杂/链路长容易导致包乱序,进而出发丢包,取决于网络会小概率出现——通过 tc qdisc 可以来构造丢包重现该场景
    6. 客户端修改 net.ipv4.tcp_timestamps
      • 1->0,触发持续60秒大概率必现的丢包,60秒后恢复
      • 0->1 持续大概率一直丢包60秒; 60秒过后如果网络延时略高且客户端并发大一直有上一次 FIN 时间戳大于后续SYN 会一直概率性丢包持续下去;如果停掉所有流量,重启客户端流量,恢复正常
      • 2->1 丢包,情况同2
      • 1->2 不触发丢包
  • 若服务器所用端口是 time_wait 状态,这时新连接刚好和 time_wait 5元组重复,一般服务器不会回复syn+ack 而是回复time_wait 前的ack
  • NAT 哈希表满导致 ECS 实例丢包 nf_conntrack full, 诊断: dmesg |grep conntrack

为什么 drop SYN 包时不去看四元组?因为tiem_wait 状态是 per-host

0->1 60秒后仍然持续丢包:

image-20240803095126448

2->1 60秒后持续丢包:(非常神奇:在310客户端改不影响自己,导致510客户端(网络延时大)一直丢包,直到510 客户端重启流量才能恢复)

image-20240803093817441

tcp_reuse 参数只对客户端有效(客户端是指主动发起 fin 的一方),启用后会回收超过 1 秒的 time_wait 状态端口重复使用:参考:https://ata.atatech.org/articles/11020082442

如果服务端是Time_wait 状态时收到 SYN 包怎么办?

https://developer.aliyun.com/article/1262180

tcp connect 的流程是这样的:

1、tcp发出SYN建链报文后,报文到ip层需要进行路由查询

2、路由查询完成后,报文到arp层查询下一跳mac地址

3、如果本地没有对应网关的arp缓存,就需要缓存住这个报文,发起arp请求

4、arp层收到arp回应报文之后,从缓存中取出SYN报文,完成mac头填写并发送给驱动。

问题在于,arp层报文缓存队列长度默认为3。如果你运气不好,刚好赶上缓存已满,这个报文就会被丢弃。

TCP层发现SYN报文发出去3s(1s+2s)还没有回应,就会重发一个SYN。这就是为什么少数连接会3s后才能建链。

幸运的是,arp层缓存队列长度是可配置的,用 sysctl -a | grep unres_qlen 就能看到,默认值为3

Time_Wait

socket.close 默认是四次挥手,但如果tw bucket 满了就直接走 reset,比如很多机器设置的是 5000 net.ipv4.tcp_max_tw_buckets = 5000

bucket 溢出对应的监控指标:TCPTimeWaitOverflow

1
2
3
4
5
6
7
#netstat -s | grep -i overflow
439 times the listen queue of a socket overflowed
TCPTimeWaitOverflow: 377310115

#netstat -s | grep -i overflow
439 times the listen queue of a socket overflowed
TCPTimeWaitOverflow: 377314175

总结

  • 一定要会用tcpdump和wireshark(纯工具,没有任何门槛,用不好只有一个原因: 懒)
  • 多实践(因为网络知识离我们有点远、有点抽象),用好各种工具,工具能帮我们看到、摸到
  • 不要追求知识面的广度,深抠几个具体的知识点然后让这些点建立体系
  • 不要为那些基本用不到的偏门知识花太多精力,天天用的都学不过来对吧。

参考资料

per-connection random offset:https://lwn.net/Articles/708021/

1…456…10
twitter @plantegg

twitter @plantegg

191 日志
18 分类
282 标签
RSS
© 2025 twitter @plantegg
由 Hexo 强力驱动
主题 - NexT.Mist
本站总访问量次 本站访客数人次
访问人数 人 次