十年后数据库还是不敢拥抱NUMA?

十年后数据库还是不敢拥抱NUMA?

在2010年前后MySQL、PG、Oracle数据库在使用NUMA的时候碰到了性能问题,流传最广的这篇 MySQL – The MySQL “swap insanity” problem and the effects of the NUMA architecture 描述了性能问题的原因(文章中把原因找错了)以及解决方案:关闭NUMA。 实际这个原因是kernel实现的一个低级bug,这个Bug在2014年修复了,但是修复这么多年后仍然以讹传讹,这篇文章希望正本清源、扭转错误的认识。

背景

最近在做一次性能测试的时候发现MySQL实例有一个奇怪现象,在128core的物理机上运行三个MySQL实例,每个实例分别绑定32个物理core,绑定顺序就是第一个0-31、第二个32-63、第三个64-95,实际运行结果让人大跌眼镜,如下图

undefined

从CPU消耗来看差异巨大,高的实例CPU用到了2500%,低的才488%,差了5倍。但是神奇的是他们的QPS一样,执行的SQL也是一样

undefined
所有MySQL实例流量一样

那么问题来了为什么在同样的机器上、同样的流量下CPU使用率差了这么多? 换句话来问就是CPU使用率高就有效率吗?

这台物理机CPU 信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#lscpu
Architecture: aarch64
Byte Order: Little Endian
CPU(s): 128
On-line CPU(s) list: 0-127
Thread(s) per core: 1
Core(s) per socket: 64
Socket(s): 2
NUMA node(s): 1
Model: 3
BogoMIPS: 100.00
L1d cache: 32K
L1i cache: 32K
L2 cache: 2048K
L3 cache: 65536K
NUMA node0 CPU(s): 0-127
Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid

原因分析

先来看这两个MySQL 进程的Perf数据

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
#第二个 MySQL IPC只有第三个的30%多点,这就是为什么CPU高这么多,但是QPS差不多
perf stat -e branch-misses,bus-cycles,cache-misses,cache-references,cpu-cycles,instructions,L1-dcache-load-misses,L1-dcache-loads,L1-dcache-store-misses,L1-dcache-stores,L1-icache-load-misses,L1-icache-loads,branch-load-misses,branch-loads,dTLB-load-misses,iTLB-load-misses -a -p 61238
^C
Performance counter stats for process id '61238':

86,491,052 branch-misses (58.55%)
98,481,418,793 bus-cycles (55.64%)
113,095,618 cache-misses # 6.169 % of all cache refs (53.20%)
1,833,344,484 cache-references (52.00%)
101,516,165,898 cpu-cycles (57.09%)
4,229,190,014 instructions # 0.04 insns per cycle (55.91%)
111,780,025 L1-dcache-load-misses # 6.34% of all L1-dcache hits (55.40%)
1,764,421,570 L1-dcache-loads (52.62%)
112,261,128 L1-dcache-store-misses (49.34%)
1,814,998,338 L1-dcache-stores (48.51%)
219,372,119 L1-icache-load-misses (49.56%)
2,816,279,627 L1-icache-loads (49.15%)
85,321,093 branch-load-misses (50.38%)
1,038,572,653 branch-loads (50.65%)
45,166,831 dTLB-load-misses (51.98%)
29,892,473 iTLB-load-misses (52.56%)

1.163750756 seconds time elapsed

#第三个 MySQL
perf stat -e branch-misses,bus-cycles,cache-misses,cache-references,cpu-cycles,instructions,L1-dcache-load-misses,L1-dcache-loads,L1-dcache-store-misses,L1-dcache-stores,L1-icache-load-misses,L1-icache-loads,branch-load-misses,branch-loads,dTLB-load-misses,iTLB-load-misses -a -p 53400
^C
Performance counter stats for process id '53400':

295,575,513 branch-misses (40.51%)
110,934,600,206 bus-cycles (39.30%)
537,938,496 cache-misses # 8.310 % of all cache refs (38.99%)
6,473,688,885 cache-references (39.80%)
110,540,950,757 cpu-cycles (46.10%)
14,766,013,708 instructions # 0.14 insns per cycle (46.85%)
538,521,226 L1-dcache-load-misses # 8.36% of all L1-dcache hits (48.00%)
6,440,728,959 L1-dcache-loads (46.69%)
533,693,357 L1-dcache-store-misses (45.91%)
6,413,111,024 L1-dcache-stores (44.92%)
673,725,952 L1-icache-load-misses (42.76%)
9,216,663,639 L1-icache-loads (38.27%)
299,202,001 branch-load-misses (37.62%)
3,285,957,082 branch-loads (36.10%)
149,348,740 dTLB-load-misses (35.20%)
102,444,469 iTLB-load-misses (34.78%)

8.080841166 seconds time elapsed

从上面可以看到 IPC 差异巨大0.04 VS 0.14 ,也就是第一个MySQL的CPU效率很低,我们看到的CPU running实际是CPU在等待(stall)。

CPU的实际信息

找到同一个机型,但是NUMA开着的查了一下:

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
#lscpu
Architecture: aarch64
Byte Order: Little Endian
CPU(s): 128
On-line CPU(s) list: 0-127
Thread(s) per core: 1
Core(s) per socket: 64
Socket(s): 2
NUMA node(s): 16
Model: 3
BogoMIPS: 100.00
L1d cache: 32K
L1i cache: 32K
L2 cache: 2048K
L3 cache: 65536K
NUMA node0 CPU(s): 0-7
NUMA node1 CPU(s): 8-15
NUMA node2 CPU(s): 16-23
NUMA node3 CPU(s): 24-31
NUMA node4 CPU(s): 32-39
NUMA node5 CPU(s): 40-47
NUMA node6 CPU(s): 48-55
NUMA node7 CPU(s): 56-63
NUMA node8 CPU(s): 64-71
NUMA node9 CPU(s): 72-79
NUMA node10 CPU(s): 80-87
NUMA node11 CPU(s): 88-95
NUMA node12 CPU(s): 96-103
NUMA node13 CPU(s): 104-111
NUMA node14 CPU(s): 112-119
NUMA node15 CPU(s): 120-127
Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid

这告诉我们实际上这个机器有16个NUMA,跨NUMA访问内存肯定比访问本NUMA内的要慢几倍。

关于NUMA

如下图,是一个Intel Xeon E5 CPU的架构信息,左右两边的大红框分别是两个NUMA,每个NUMA的core访问直接插在自己红环上的内存必然很快,如果访问插在其它NUMA上的内存还要走两个红环之间上下的黑色箭头线路,所以要慢很多。

img

实际测试Intel的E5-2682(对应V42机型)和8269(对应V62机型) 的CPU跨Socket(这两块CPU内部不再是上图的红环Bus,而是改用了Mesh Bus一个Die就是一个NUMA,服务器有两路,也就是一个Socket就是一个NUMA),也就是跨NUMA访问内存的延迟是本Node延迟的将近2倍。测试工具从这里下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//E5-2682
Intel(R) Memory Latency Checker - v3.9
Measuring idle latencies (in ns)...
Numa node
Numa node 0 1
0 85.0 136.3
1 137.2 84.2

//8269
Intel(R) Memory Latency Checker - v3.9
Measuring idle latencies (in ns)...
Numa node
Numa node 0 1
0 78.6 144.1
1 144.7 78.5

开启NUMA会优先就近使用内存,在本NUMA上的内存不够的时候可以选择回收本地的PageCache还是到其它NUMA 上分配内存,这是可以通过Linux参数 zone_reclaim_mode 来配置的,默认是到其它NUMA上分配内存,也就是跟关闭NUMA是一样的。

这个架构距离是物理上就存在的不是你在BIOS里关闭了NUMA差异就消除了,我更愿意认为在BIOS里关掉NUMA只是掩耳盗铃。

以上理论告诉我们:也就是在开启NUMA和 zone_reclaim_mode 默认在内存不够的如果去其它NUMA上分配内存,比关闭NUMA要快很多而没有任何害处。

UMA和NUMA对比

The SMP/UMA architecture

img

The NUMA architecture

img

Modern multiprocessor systems mix these basic architectures as seen in the following diagram:

img

In this complex hierarchical scheme, processors are grouped by their physical location on one or the other multi-core CPU package or “node.” Processors within a node share access to memory modules as per the UMA shared memory architecture. At the same time, they may also access memory from the remote node using a shared interconnect, but with slower performance as per the NUMA shared memory architecture.

03-05-Broadwell_HCC_Architecture

对比测试Intel NUMA 性能

对如下Intel CPU进行一些测试,在开启NUMA的情况下

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
#lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 64
On-line CPU(s) list: 0-63
Thread(s) per core: 2
Core(s) per socket: 16
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 79
Model name: Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz
Stepping: 1
CPU MHz: 2500.000
CPU max MHz: 3000.0000
CPU min MHz: 1200.0000
BogoMIPS: 5000.06
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 40960K
NUMA node0 CPU(s): 0-15,32-47
NUMA node1 CPU(s): 16-31,48-63
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 ds_cpl vmx smx est tm2 ssse3 fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch ida arat epb invpcid_single pln pts dtherm spec_ctrl ibpb_support tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm rdt rdseed adx smap xsaveopt cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local cat_l3

#numastat
node0 node1
numa_hit 129600200 60501102
numa_miss 0 0
numa_foreign 0 0
interleave_hit 108648 108429
local_node 129576548 60395061
other_node 23652 106041

我在这个64core的物理机上运行一个MySQL 实例,先将MySQL进程绑定在0-63core,0-31core,以及0-15,32-47上

用sysbench对一亿条记录跑点查,数据都加载到内存中了:

  • 绑0-63core qps 不到8万,总cpu跑到5000%,降低并发的话qps能到11万;
  • 如果绑0-31core qps 12万,总cpu跑到3200%,IPC 0.29;
  • 如果绑同一个numa下的32core,qps飙到27万,总CPU跑到3200% IPC: 0.42;
  • 绑0-15个物理core,qps能到17万,绑32-47也是一样的效果;

undefined

从这个数据看起来即使Intel在只有两个NUMA的情况下跨性能差异也有2倍,可见正确的绑核方法收益巨大,尤其是在刷榜的情况下, NUMA更多性能差异应该会更大。

说明前面的理论是正确的。

来看看不通绑核情况下node之间的带宽利用情况:

image-20210525151537507

image-20210525151622425

实际在不开NUMA的同样CPU上,进行以上各种绑核测试,测试结果也完全一样。

如果比较读写混合场景的话肯定会因为写锁导致CPU跑起来,最终的性能差异也不会这么大,但是绑在同一个NUMA下的性能肯定要好,IPC也会高一些。具体好多少取决于锁的竞争程度。

为什么集团内外所有物理机都把NUMA关掉了呢?

10年前几乎所有的运维都会多多少少被NUMA坑害过,让我们看看究竟有多少种在NUMA上栽的方式:

最有名的是这篇 MySQL – The MySQL “swap insanity” problem and the effects of the NUMA architecture

我总结下这篇2010年的文章说的是啥:

  • 如果本NUMA内存不够的时候,Linux会优先回收PageCache内存,即使其它NUMA还有内存
  • 回收PageCache经常会造成系统卡顿,这个卡顿不能接受

所以文章给出的解决方案就是(三选一):

  • 关掉NUMA
  • 或者启动MySQL的时候指定不分NUMA,比如:/usr/bin/numactl –interleave all $cmd
  • 或者启动MySQL的时候先回收所有PageCache

我想这就是这么多人在上面栽了跟头,所以干脆一不做二不休干脆关了NUMA 一了百了。

但真的NUMA有这么糟糕?或者说Linux Kernel有这么笨,默认优先去回收PageCache吗?

Linux Kernel对NUMA内存的使用

实际我们使用NUMA的时候期望是:优先使用本NUMA上的内存,如果本NUMA不够了不要优先回收PageCache而是优先使用其它NUMA上的内存。

zone_reclaim_mode

事实上Linux识别到NUMA架构后,默认的内存分配方案就是:优先尝试在请求线程当前所处的CPU的Local内存上分配空间。如果local内存不足,优先淘汰local内存中无用的Page(Inactive,Unmapped)。然后才到其它NUMA上分配内存。

intel 芯片跨node延迟远低于其他家,所以跨node性能损耗不大

zone_reclaim_mode,它用来管理当一个内存区域(zone)内部的内存耗尽时,是从其内部进行内存回收还是可以从其他zone进行回收的选项:

zone_reclaim_mode:

Zone_reclaim_mode allows someone to set more or less aggressive approaches to
reclaim memory when a zone runs out of memory. If it is set to zero then no
zone reclaim occurs. Allocations will be satisfied from other zones / nodes
in the system.

zone_reclaim_mode的四个参数值的意义分别是:

0 = Allocate from all nodes before reclaiming memory
1 = Reclaim memory from local node vs allocating from next node
2 = Zone reclaim writes dirty pages out
4 = Zone reclaim swaps pages

1
2
# cat /proc/sys/vm/zone_reclaim_mode
0

我查了2.6.32以及4.19.91内核的机器 zone_reclaim_mode 都是默认0 ,也就是kernel会:优先使用本NUMA上的内存,如果本NUMA不够了不要优先回收PageCache而是优先使用其它NUMA上的内存。这也是我们想要的

Kernel文档也告诉大家默认就是0,但是为什么会出现优先回收了PageCache呢?

查看kernel提交记录

github kernel commit

undefined

undefined

undefined

关键是上图红框中的代码,node distance比较大(也就是开启了NUMA的话),强制将 zone_reclaim_mode设为1,这是2014年提交的代码,将这个强制设为1的逻辑去掉了。

这也就是为什么之前大佬们碰到NUMA问题后尝试修改 zone_reclaim_mode 没有效果,也就是2014年前只要开启了NUMA就强制线回收PageCache,即使设置zone_reclaim_mode也没有意义,真是个可怕的Bug。

验证一下zone_reclaim_mode 0是生效的

内核版本:3.10.0-327.ali2017.alios7.x86_64

测试方法

先将一个160G的文件加载到内存里,然后再用代码分配64G的内存出来使用。
单个NUMA node的内存为256G,本身用掉了60G,加上这次的160G的PageCache,和之前的一些其他PageCache,总的 PageCache用了179G,那么这个node总内存还剩256G-60G-179G,

如果这个时候再分配64G内存的话,本node肯定不够了,我们来看在 zone_reclaim_mode=0 的时候是优先回收PageCache还是分配了到另外一个NUMA node(这个NUMA node 有240G以上的内存空闲)

测试过程

分配64G内存

1
2
3
4
5
#taskset -c 0 ./alloc 64
To allocate 64GB memory
Used time: 39 seconds

#grep FilePages /sys/devices/system/node/node0/meminfo

undefined

从如上截图来看,再分配64G内存的时候即使node0不够了也没有回收node0上的PageCache,而是将内存跨NUMA分配到了node1上,符合预期!

释放这64G内存后,如下图可以看到node0回收了25G,剩下的39G都是在node1上:
undefined

将 /proc/sys/vm/zone_reclaim_mode 改成 1 继续同样的测试

可以看到zone_reclaim_mode 改成 1,node0内存不够了也没有分配node1上的内存,而是从PageCache回收了40G内存,整个分配64G内存的过程也比不回收PageCache慢了12秒,这12秒就是额外的卡顿

undefined

测试结论:从这个测试可以看到NUMA 在内存使用上不会优先回收 PageCache 了

innodb_numa_interleave

从5.7开始,mysql增加了对NUMA的无感知:innodb_numa_interleave,也就是在开了NUMA的机器上,使用内错交错来分配内存,相当于使用上关掉 NUMA

For the innodb_numa_interleave option to be available, MySQL must be compiled on a NUMA-enabled Linux system.

当开启了 innodb_numa_interleave 的话在为innodb buffer pool分配内存的时候将 NUMA memory policy 设置为 MPOL_INTERLEAVE 分配完后再设置回 MPOL_DEFAULT(OS默认内存分配行为,也就是zone_reclaim_mode指定的行为)。

innodb_numa_interleave参数是为innodb更精细化地分配innodb buffer pool 而增加的。很典型地innodb_numa_interleave为on只是更好地规避了前面所说的zone_reclaim_mode的kernel bug,修复后这个参数没有意义了

AUTOMATIC NUMA BALANCING

RedHat 7默认会自动让内存或者进程就近迁移,让内存和CPU距离更近以达到最好的效果

Automatic NUMA balancing improves the performance of applications running on NUMA hardware systems. It is enabled by default on Red Hat Enterprise Linux 7 systems.

An application will generally perform best when the threads of its processes are accessing memory on the same NUMA node as the threads are scheduled. Automatic NUMA balancing moves tasks (which can be threads or processes) closer to the memory they are accessing. It also moves application data to memory closer to the tasks that reference it. This is all done automatically by the kernel when automatic NUMA balancing is active.

对应参数

1
cat /proc/sys/kernel/numa_balancing shows 1

监控

查找相应的内存和调度器事件

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
#perf stat -e sched:sched_stick_numa,sched:sched_move_numa,sched:sched_swap_numa,migrate:mm_migrate_pages,minor-faults -p 7191
Performance counter stats for process id '7191':

0 sched:sched_stick_numa (100.00%)
1 sched:sched_move_numa (100.00%)
0 sched:sched_swap_numa
0 migrate:mm_migrate_pages
286 minor-faults

# perf stat -e sched:sched_stick_numa,sched:sched_move_numa,sched:sched_swap_numa,migrate:mm_migrate_pages,minor-faults -p PID
...
1 sched:sched_stick_numa
3 sched:sched_move_numa
41 sched:sched_swap_numa
5,239 migrate:mm_migrate_pages
50,161 minor-faults

#perf stat -e sched:sched_stick_numa,sched:sched_move_numa,sched:sched_swap_numa,migrate:mm_migrate_pages,minor-faults -p 676322
Performance counter stats for process id '676322':

0 sched:sched_stick_numa
16 sched:sched_move_numa
0 sched:sched_swap_numa
24 migrate:mm_migrate_pages
2,079 minor-faults

总结

  • 放弃对NUMA的偏见吧,优先回收 PageCache 这个Bug早已修复了
  • 按NUMA绑定core收益巨大,即使只有两个NUMA的intel芯片,也有一倍以上的性能提升,在飞腾等其他芯片上收益更大
  • 没必要自欺欺人关掉NUMA了
  • RDS这样独占物理机的服务可以做到按NUMA来绑定core,收益可观
  • ECS售卖如果能够精确地按NUMA绑核的话性能,超卖比能高很多
  • 在刷tpcc数据的时候更应该开NUMA和正确绑核

我个人一直对集团所有机器默认关闭NUMA耿耿于怀,因为定制的物理机(BIOS也是定制的)BIOS默认就是关闭NUMA的,装机还得一台台手工打开(跪了,几十万台啊),算是理清了来龙去脉。因为一个kernel的bug让大家对NUMA一直有偏见,即使14年已经修复了,大家还是以讹传讹,没必要。

关于cpu为什么高但是没有产出的原因是因为CPU流水线长期stall,导致很低的IPC,所以性能自然上不去,可以看这篇文章

其他同学测试的结论:

  • Hadoop离线作业在 Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz 24 cores/socket * 2, Turbo Off 下打开NUMA后性能提升8%

一些其它不好解释的现象:

  1. 增加少量跨NUMA 的core进来时能增加QPS的,但是随着跨NUMA core越来越多(总core也越来越多)QPS反而会达到一个峰值后下降—效率低的core多了,抢走任务,执行得慢
  2. 压12-19和8-15同样8core,不跨NUMA的8-15性能只好5%左右(87873 VS 92801) — 难以解释
  3. 由1、2所知在测试少量core的时候跨NUMA性能下降体现不出来
  4. 在压0-31core的时候,如果运行 perf这个时候QPS反而会增加(13万上升到15万)— 抢走了一些CPU资源,让某个地方竞争反而减小了
  5. 综上在我个人理解是core越多的时候UPI压力到了瓶颈,才会出现加core性能反而下降

系列文章

CPU的制造和概念

[CPU 性能和Cache Line](/2021/05/16/CPU Cache Line 和性能/)

[Perf IPC以及CPU性能](/2021/05/16/Perf IPC以及CPU利用率/)

Intel、海光、鲲鹏920、飞腾2500 CPU性能对比

飞腾ARM芯片(FT2500)的性能测试

十年后数据库还是不敢拥抱NUMA?

一次海光物理机资源竞争压测的记录

[Intel PAUSE指令变化是如何影响自旋锁以及MySQL的性能的](/2019/12/16/Intel PAUSE指令变化是如何影响自旋锁以及MySQL的性能的/)

参考资料

https://www.redhat.com/files/summit/session-assets/2018/Performance-analysis-and-tuning-of-Red-Hat-Enterprise-Linux-Part-1.pdf

https://informixdba.wordpress.com/2015/10/16/zone-reclaim-mode/

https://queue.acm.org/detail.cfm?id=2513149

NUMA DEEP DIVE PART 1: FROM UMA TO NUMA 这是一个系列,都很干货,值得推荐

https://15721.courses.cs.cmu.edu/spring2016/papers/p743-leis.pdf Morsel-Driven Parallelism: A NUMA-Aware Query Evaluation Framework for the Many-Core Age 论文给出了很多numa-aware下的bandwidth、latency数据,以及对THC-H的影响

如果你觉得看完对你很有帮助可以通过如下方式找到我

find me on twitter: @plantegg

知识星球:https://t.zsxq.com/0cSFEUh2J

开了一个星球,在里面讲解一些案例、知识、学习方法,肯定没法让大家称为顶尖程序员(我自己都不是),只是希望用我的方法、知识、经验、案例作为你的垫脚石,帮助你快速、早日成为一个基本合格的程序员。

争取在星球内:

  • 养成基本动手能力
  • 拥有起码的分析推理能力–按我接触的程序员,大多都是没有逻辑的
  • 知识上教会你几个关键的知识点
image-20240324161113874

为什么这么多CLOSE_WAIT

为什么这么多CLOSE_WAIT

案例1:服务响应慢,经常连不上

应用发布新版本上线后,业务同学发现业务端口上的TCP连接处于CLOSE_WAIT状态的数量有积压,多的时候能堆积到几万个,有时候应用无法响应了

这个案例目标:怎么样才能获取举三反一的秘籍, 普通人为什么要案例来深化对理论知识的理解。

检查机器状态

img

img

从上述两个图中可以看到磁盘 sdb压力非常大,util经常会到 100%,这个时候对应地从top中也可以看到cpu wait%很高(这个ECS cpu本来竞争很激烈),st%一直非常高,所以整体留给应用的CPU不多,碰上磁盘缓慢的话,这时如果业务写日志是同步刷盘那么就会导致程序卡顿严重。

实际看到FGC的时间也是正常状态下的10倍了。

再看看实际上应用同步写日志到磁盘比较猛,平均20-30M,高的时候能到200M每秒。如果输出的时候磁盘卡住了那么就整个卡死了

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
#dstat
----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
usr sys idl wai hiq siq| read writ| recv send| in out | int csw
4 1 89 5 0 0|1549M 8533M| 0 0 | 521k 830k|6065k 7134
3 1 95 0 0 0|3044k 19M|1765k 85k| 0 84k| 329k 7770
5 1 93 0 0 0|3380k 18M|4050k 142k| 0 0 | 300k 8008
7 1 91 1 0 1|2788k 227M|5094k 141k| 0 28k| 316k 8644
4 1 93 2 0 0|2788k 55M|2897k 63k| 0 68k| 274k 6453
6 1 91 1 0 0|4464k 24M|3683k 98k| 0 28k| 299k 7379
7 1 91 1 0 0| 10M 34M|3655k 130k| 0 208k| 375k 8417
3 1 87 8 0 0|6940k 33M|1335k 91k| 0 112k| 334k 7369
3 1 88 7 0 0|4932k 16M|1918k 61k| 0 44k| 268k 6542
7 1 86 6 0 0|5508k 20M|5377k 111k| 0 0 | 334k 7998
7 2 88 3 0 0|5628k 115M|4713k 104k| 0 0 | 280k 7392
4 1 95 0 0 0| 0 732k|2940k 85k| 0 76k| 189k 7682
3 1 96 0 0 0| 0 800k|1809k 68k| 0 16k| 181k 9640
7 2 76 14 0 1|6300k 38M|3834k 132k| 0 0 | 333k 7502
7 2 90 1 0 0|3896k 19M|3786k 93k| 0 0 | 357k 7578
4 1 94 0 0 0|5732k 29M|2906k 806k| 0 0 | 338k 8966
4 1 94 1 0 0|6044k 17M|2202k 95k| 0 0 | 327k 7573
4 1 95 1 0 0|3524k 17M|2277k 88k| 0 0 | 299k 6462
4 1 96 0 0 0| 456k 14M|2770k 91k| 60k 0 | 252k 6644
6 2 92 0 0 0| 0 12M|4251k 847k| 0 0 | 264k 10k
3 1 92 4 0 0| 788k 204M|1555k 43k| 0 0 | 249k 6215
6 1 86 6 0 0|7180k 20M|2073k 92k| 0 0 | 303k 7028
11 4 84 1 0 0|6116k 29M|3079k 99k| 28k 0 | 263k 6605

磁盘util 100%和CLOSE_WAIT 强相关,也和理论比较符合,CLOSE_WAIT 就是连接被动关闭端的应用没调 socket.close

img

原因推断:

1)新发布的代码需要消耗更多的CPU,代码增加了新的逻辑 //这只是一个微小的诱因

2)机器本身资源(CPU /IO)很紧张 这两个条件下导致应用响应缓慢。 目前看到的稳定重现条件就是重启一个业务节点,重启会触发业务节点之间重新同步数据,以及重新推送很多数据到客户端的新连接上,这两件事情都会让应用CPU占用飙升响应缓慢,响应慢了之后会导致更多的心跳失效进一步加剧数据同步,然后就雪崩恶化了。最后表现就是看到系统卡死了,也就是 tcp buffer 中的数据也不读走、连接也不 close,连接大量堆积在 close_wait状态

img

CLOSE_WAIT的原因分析

先看TCP连接状态图

这是网络、书本上凡是描述TCP状态一定会出现的状态图,理论上看这个图能解决任何TCP状态问题。

image.png

反复看这个图的右下部分的CLOSE_WAIT ,从这个图里可以得到如下结论:

CLOSE_WAIT 是被动关闭端在等待应用进程的关闭

基本上这一结论要能帮助解决所有CLOSE_WAIT相关的问题,如果不能说明对这个知识点理解的不够。

案例1结论

机器超卖严重、IO卡顿,导致应用线程卡顿,来不及调用 socket.close()

案例2: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 状态拆解

通常,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, 所以二者刚好一致并且联动了

CLOSE_WAIT与TIME_WAIT

  • 简单说就是CLOSE_WAIT出现在被动断开连接端,一般过多就不太正常;
  • TIME_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 *:*

Intel AMD 鲲鹏 海光 飞腾性能PK

Intel AMD 鲲鹏 海光 飞腾性能PK

前言

本文在sysbench、tpcc等实践场景下对多款CPU的性能进行对比,同时分析各款CPU的硬件指标,最后分析不同场景下的实际性能和核心参数的关系。

本文的姊妹篇:十年后数据库还是不敢拥抱NUMA? 主要讲述的是 同一块CPU的不同NUMA结构配置带来的几倍性能差异,这些性能差异原因也可以从本文最后时延测试数据得到印证,一起阅读效果更好。

性能定义

同一个平台下(X86、ARM是两个平台)编译好的程序可以认为他们的 指令 数是一样的,那么执行效率就是每个时钟周期所能执行的指令数量了。

执行指令数量第一取决的就是CPU主频了,但是目前主流CPU都是2.5G左右,另外就是单核下的并行度(多发射)以及多核,再就是分支预测等,这些基本归结到了访问内存的延时。

X86和ARM这两不同平台首先指令就不一样了,然后还有上面所说的主频、内存时延的差异

IPC的说明:

IPC: insns per cycle insn/cycles 也就是每个时钟周期能执行的指令数量,越大程序跑的越快

程序的执行时间 = 指令数/(主频*IPC) //单核下,多核的话再除以核数

参与比较的几款CPU参数

先来看看测试所用到的几款CPU的主要指标,大家关注下主频、各级cache大小、numa结构

Hygon 7280

Hygon 7280 就是AMD Zen架构,最大IPC能到5.

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
架构:                           x86_64
CPU 运行模式: 32-bit, 64-bit
字节序: Little Endian
Address sizes: 43 bits physical, 48 bits virtual
CPU: 128
在线 CPU 列表: 0-127
每个核的线程数: 2
每个座的核数: 32
座: 2
NUMA 节点: 8
厂商 ID: HygonGenuine
CPU 系列: 24
型号: 1
型号名称: Hygon C86 7280 32-core Processor
步进: 1
CPU MHz: 2194.586
BogoMIPS: 3999.63
虚拟化: AMD-V
L1d 缓存: 2 MiB
L1i 缓存: 4 MiB
L2 缓存: 32 MiB
L3 缓存: 128 MiB
NUMA 节点0 CPU: 0-7,64-71
NUMA 节点1 CPU: 8-15,72-79
NUMA 节点2 CPU: 16-23,80-87
NUMA 节点3 CPU: 24-31,88-95
NUMA 节点4 CPU: 32-39,96-103
NUMA 节点5 CPU: 40-47,104-111
NUMA 节点6 CPU: 48-55,112-119
NUMA 节点7 CPU: 56-63,120-127

AMD EPYC 7H12

AMD EPYC 7H12 64-Core(ECS,非物理机),最大IPC能到5.

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
# lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 64
On-line CPU(s) list: 0-63
Thread(s) per core: 2
Core(s) per socket: 16
座: 2
NUMA 节点: 2
厂商 ID: AuthenticAMD
CPU 系列: 23
型号: 49
型号名称: AMD EPYC 7H12 64-Core Processor
步进: 0
CPU MHz: 2595.124
BogoMIPS: 5190.24
虚拟化: AMD-V
超管理器厂商: KVM
虚拟化类型: 完全
L1d 缓存: 32K
L1i 缓存: 32K
L2 缓存: 512K
L3 缓存: 16384K
NUMA 节点0 CPU: 0-31
NUMA 节点1 CPU: 32-63

Intel

这次对比测试用到了两块Intel CPU,分别是 8163、8269 。他们的信息如下,最大IPC 是4:

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
#lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 96
On-line CPU(s) list: 0-95
Thread(s) per core: 2
Core(s) per socket: 24
Socket(s): 2
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz
Stepping: 4
CPU MHz: 2499.121
CPU max MHz: 3100.0000
CPU min MHz: 1000.0000
BogoMIPS: 4998.90
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 1024K
L3 cache: 33792K
NUMA node0 CPU(s): 0-95

-----8269CY
#lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 104
On-line CPU(s) list: 0-103
Thread(s) per core: 2
Core(s) per socket: 26
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz
Stepping: 7
CPU MHz: 3200.000
CPU max MHz: 3800.0000
CPU min MHz: 1200.0000
BogoMIPS: 4998.89
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 1024K
L3 cache: 36608K
NUMA node0 CPU(s): 0-25,52-77
NUMA node1 CPU(s): 26-51,78-103

鲲鹏920

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
[root@ARM 19:15 /root/lmbench3]
#numactl -H
available: 4 nodes (0-3)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
node 0 size: 192832 MB
node 0 free: 146830 MB
node 1 cpus: 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
node 1 size: 193533 MB
node 1 free: 175354 MB
node 2 cpus: 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
node 2 size: 193533 MB
node 2 free: 175718 MB
node 3 cpus: 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
node 3 size: 193532 MB
node 3 free: 183643 MB
node distances:
node 0 1 2 3
0: 10 12 20 22
1: 12 10 22 24
2: 20 22 10 12
3: 22 24 12 10

node 0 <------------ socket distance ------------> node 2
| (die distance) | (die distance)
node 1 node 3


#lscpu
Architecture: aarch64
Byte Order: Little Endian
CPU(s): 96
On-line CPU(s) list: 0-95
Thread(s) per core: 1
Core(s) per socket: 48
Socket(s): 2
NUMA node(s): 4
Model: 0
CPU max MHz: 2600.0000
CPU min MHz: 200.0000
BogoMIPS: 200.00
L1d cache: 64K
L1i cache: 64K
L2 cache: 512K
L3 cache: 24576K //一个Die下24core共享24M L3,每个core 1MB
NUMA node0 CPU(s): 0-23
NUMA node1 CPU(s): 24-47
NUMA node2 CPU(s): 48-71
NUMA node3 CPU(s): 72-95
Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma dcpop asimddp asimdfhm

飞腾2500

飞腾2500用nop去跑IPC的话,只能到1,但是跑其它代码能到2.33,理论值据说也是4但是我没跑到过

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
#lscpu
Architecture: aarch64
Byte Order: Little Endian
CPU(s): 128
On-line CPU(s) list: 0-127
Thread(s) per core: 1
Core(s) per socket: 64
Socket(s): 2
NUMA node(s): 16
Model: 3
BogoMIPS: 100.00
L1d cache: 32K
L1i cache: 32K
L2 cache: 2048K
L3 cache: 65536K
NUMA node0 CPU(s): 0-7
NUMA node1 CPU(s): 8-15
NUMA node2 CPU(s): 16-23
NUMA node3 CPU(s): 24-31
NUMA node4 CPU(s): 32-39
NUMA node5 CPU(s): 40-47
NUMA node6 CPU(s): 48-55
NUMA node7 CPU(s): 56-63
NUMA node8 CPU(s): 64-71
NUMA node9 CPU(s): 72-79
NUMA node10 CPU(s): 80-87
NUMA node11 CPU(s): 88-95
NUMA node12 CPU(s): 96-103
NUMA node13 CPU(s): 104-111
NUMA node14 CPU(s): 112-119
NUMA node15 CPU(s): 120-127
Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid

单核以及超线程计算Prime性能比较

测试命令,这个测试命令无论在哪个CPU下,用2个物理核用时都是一个物理核的一半,所以这个计算是可以完全并行的

1
taskset -c 1 /usr/bin/sysbench --num-threads=1 --test=cpu --cpu-max-prime=50000 run //单核绑一个core; 2个thread就绑一对HT

测试结果为耗时,单位秒

测试项 AMD EPYC 7H12 2.5G CentOS 7.9 Hygon 7280 2.1GHz CentOS Hygon 7280 2.1GHz 麒麟 Intel 8269 2.50G Intel 8163 CPU @ 2.50GHz Intel E5-2682 v4 @ 2.50GHz
单核 prime 50000 耗时 59秒 IPC 0.56 77秒 IPC 0.55 89秒 IPC 0.56; 83 0.41 105秒 IPC 0.41 109秒 IPC 0.39
HT prime 50000 耗时 57秒 IPC 0.31 74秒 IPC 0.29 87秒 IPC 0.29 48 0.35 60秒 IPC 0.36 74秒 IPC 0.29

从上面的测试结果来看,简单纯计算场景下 AMD/海光 的单核能力还是比较强的,但是超线程完全不给力(数据库场景超线程就给力了);而Intel的超线程非常给力,一对超线程能达到单物理core的1.8倍,并且从E5到8269更是好了不少。
ARM基本都没有超线程所有没有跑鲲鹏、飞腾。

计算Prime毕竟太简单,让我们来看看他们在数据库下的真实能力吧

对比MySQL sysbench和tpcc性能

MySQL 默认用5.7.34社区版,操作系统默认是centos,测试中所有mysqld都做了绑核,一样的压力配置尽量将CPU跑到100%, HT表示将mysqld绑定到一对HT核。

sysbench点查

测试命令类似如下:

1
sysbench --test='/usr/share/doc/sysbench/tests/db/select.lua' --oltp_tables_count=1 --report-interval=1 --oltp-table-size=10000000  --mysql-port=3307 --mysql-db=sysbench_single --mysql-user=root --mysql-password='Bj6f9g96!@#'  --max-requests=0   --oltp_skip_trx=on --oltp_auto_inc=on  --oltp_range_size=5  --mysql-table-engine=innodb --rand-init=on   --max-time=300 --mysql-host=x86.51 --num-threads=4 run

测试结果分别取QPS/IPC两个数据(测试中的差异AMD、Hygon CPU跑在CentOS7.9, intel CPU、Kunpeng 920 跑在AliOS上, xdb表示用集团的xdb替换社区的MySQL Server, 麒麟是国产OS):

测试核数 AMD EPYC 7H12 2.5G Hygon 7280 2.1G Hygon 7280 2.1GHz 麒麟 Intel 8269 2.50G Intel 8163 2.50G Intel 8163 2.50G XDB5.7 鲲鹏 920-4826 2.6G 鲲鹏 920-4826 2.6G XDB8.0 FT2500 alisql 8.0 本地–socket
单核 24674 0.54 13441 0.46 10236 0.39 28208 0.75 25474 0.84 29376 0.89 9694 0.49 8301 0.46 3602 0.53
一对HT 36157 0.42 21747 0.38 19417 0.37 36754 0.49 35894 0.6 40601 0.65 无HT 无HT 无HT
4物理核 94132 0.52 49822 0.46 38033 0.37 90434 0.69 350% 87254 0.73 106472 0.83 34686 0.42 28407 0.39 14232 0.53
16物理核 325409 0.48 171630 0.38 134980 0.34 371718 0.69 1500% 332967 0.72 446290 0.85 //16核比4核好! 116122 0.35 94697 0.33 59199 0.6 8core:31210 0.59
32物理核 542192 0.43 298716 0.37 255586 0.33 642548 0.64 2700% 588318 0.67 598637 0.81 CPU 2400% 228601 0.36 177424 0.32 114020 0.65
说明:麒麟OS下CPU很难跑满,大致能跑到90%-95%左右,麒麟上装的社区版MySQL-5.7.29;飞腾要特别注意mysqld所在socket,同时以上飞腾数据都是走--socket压测锁的,32core走网络压测QPS为:99496(15%的网络损耗)

从上面的结果先看单物理核能力ARM 和 X86之间的差异还是很明显的

tpcc 1000仓

测试结果(测试中Hygon 7280分别跑在CentOS7.9和麒麟上, 鲲鹏/intel CPU 跑在AliOS、麒麟是国产OS):

tpcc测试数据,结果为1000仓,tpmC (NewOrders) ,未标注CPU 则为跑满了

测试核数 Intel 8269 2.50G Intel 8163 2.50G Hygon 7280 2.1GHz 麒麟 Hygon 7280 2.1G CentOS 7.9 鲲鹏 920-4826 2.6G 鲲鹏 920-4826 2.6G XDB8.0
1物理核 12392 9902 4706 7011 6619 4653
一对HT 17892 15324 8950 11778 无HT 无HT
4物理核 51525 40877 19387 380% 30046 23959 20101
8物理核 100792 81799 39664 750% 60086 42368 40572
16物理核 160798 抖动 140488 CPU抖动 75013 1400% 106419 1300-1550% 70581 1200% 79844
24物理核 188051 164757 1600-2100% 100841 1800-2000% 130815 1600-2100% 88204 1600% 115355
32物理核 195292 185171 2000-2500% 116071 1900-2400% 142746 1800-2400% 102089 1900% 143567
48物理核 19969l 195730 2100-2600% 128188 2100-2800% 149782 2000-2700% 116374 2500% 206055 4500%

测试过程CPU均跑满(未跑满的话会标注出来),IPC跑不起来性能就必然低,超线程虽然总性能好了但是会导致IPC降低(参考前面的公式)。可以看到对本来IPC比较低的场景,启用超线程后一般对性能会提升更大一些。

tpcc并发到一定程度后主要是锁导致性能上不去,所以超多核意义不大,可以做分库分表搞多个mysqld实例

比如在Hygon 7280 2.1GHz 麒麟上起两个MySQLD实例,每个实例各绑定32物理core,性能刚好翻倍:image-20210823082702539

32核的时候对比下MySQL 社区版在Hygon7280和Intel 8163下的表现,IPC的差异还是很明显的,基本和TPS差异一致:

image-20210817181752243

从sysbench和tpcc测试结果来看AMD和Intel差异不大,ARM和X86差异比较大,国产CPU还有很大的进步空间。就像前面所说抛开指令集的差异,主频差不多,内存管够为什么还有这么大的性能差别呢?

三款CPU的性能指标

下面让我们回到硬件本身的数据来看这个问题

先记住这个图,描述的是CPU访问寄存器、L1 cache、L2 cache等延时,关键记住他们的差异
各级延时

接下来用lmbench来测试各个机器的内存延时

stream主要用于测试带宽,对应的时延是在带宽跑满情况下的带宽。

lat_mem_rd用来测试操作不同数据大小的时延。

飞腾2500

用stream测试带宽和latency,可以看到带宽随着numa距离不断减少、对应的latency不断增加,到最近的numa node有10%的损耗,这个损耗和numactl给出的距离完全一致。跨socket访问内存latency是node内的3倍,带宽是三分之一,但是socket1性能和socket0性能完全一致。从这个延时来看如果要是跑一个32core的实例性能一定不会太好,并且抖动剧烈

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
time for i in $(seq 7 8 128); do echo $i; numactl -C $i -m 0 ./bin/stream -W 5 -N 5 -M 64M; done

#numactl -C 7 -m 0 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 2.84 nanoseconds
STREAM copy bandwidth: 5638.21 MB/sec
STREAM scale latency: 2.72 nanoseconds
STREAM scale bandwidth: 5885.97 MB/sec
STREAM add latency: 2.26 nanoseconds
STREAM add bandwidth: 10615.13 MB/sec
STREAM triad latency: 4.53 nanoseconds
STREAM triad bandwidth: 5297.93 MB/sec

#numactl -C 7 -m 1 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 3.16 nanoseconds
STREAM copy bandwidth: 5058.71 MB/sec
STREAM scale latency: 3.15 nanoseconds
STREAM scale bandwidth: 5074.78 MB/sec
STREAM add latency: 2.35 nanoseconds
STREAM add bandwidth: 10197.36 MB/sec
STREAM triad latency: 5.12 nanoseconds
STREAM triad bandwidth: 4686.37 MB/sec

#numactl -C 7 -m 2 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 3.85 nanoseconds
STREAM copy bandwidth: 4150.98 MB/sec
STREAM scale latency: 3.95 nanoseconds
STREAM scale bandwidth: 4054.30 MB/sec
STREAM add latency: 2.64 nanoseconds
STREAM add bandwidth: 9100.12 MB/sec
STREAM triad latency: 6.39 nanoseconds
STREAM triad bandwidth: 3757.70 MB/sec

#numactl -C 7 -m 3 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 3.69 nanoseconds
STREAM copy bandwidth: 4340.24 MB/sec
STREAM scale latency: 3.62 nanoseconds
STREAM scale bandwidth: 4422.18 MB/sec
STREAM add latency: 2.47 nanoseconds
STREAM add bandwidth: 9704.82 MB/sec
STREAM triad latency: 5.74 nanoseconds
STREAM triad bandwidth: 4177.85 MB/sec

[root@101a05001.cloud.a05.am11 /root/lmbench3]
#numactl -C 7 -m 7 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 3.95 nanoseconds
STREAM copy bandwidth: 4051.51 MB/sec
STREAM scale latency: 3.94 nanoseconds
STREAM scale bandwidth: 4060.63 MB/sec
STREAM add latency: 2.54 nanoseconds
STREAM add bandwidth: 9434.51 MB/sec
STREAM triad latency: 6.13 nanoseconds
STREAM triad bandwidth: 3913.36 MB/sec

[root@101a05001.cloud.a05.am11 /root/lmbench3]
#numactl -C 7 -m 10 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 8.80 nanoseconds
STREAM copy bandwidth: 1817.78 MB/sec
STREAM scale latency: 8.59 nanoseconds
STREAM scale bandwidth: 1861.65 MB/sec
STREAM add latency: 5.55 nanoseconds
STREAM add bandwidth: 4320.68 MB/sec
STREAM triad latency: 13.94 nanoseconds
STREAM triad bandwidth: 1721.76 MB/sec

[root@101a05001.cloud.a05.am11 /root/lmbench3]
#numactl -C 7 -m 11 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 9.27 nanoseconds
STREAM copy bandwidth: 1726.52 MB/sec
STREAM scale latency: 9.31 nanoseconds
STREAM scale bandwidth: 1718.10 MB/sec
STREAM add latency: 5.65 nanoseconds
STREAM add bandwidth: 4250.89 MB/sec
STREAM triad latency: 14.09 nanoseconds
STREAM triad bandwidth: 1703.66 MB/sec

//在另外一个socket上测试本numa,和node0性能完全一致
[root@101a0500 /root/lmbench3]
#numactl -C 88 -m 11 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 2.93 nanoseconds
STREAM copy bandwidth: 5454.67 MB/sec
STREAM scale latency: 2.96 nanoseconds
STREAM scale bandwidth: 5400.03 MB/sec
STREAM add latency: 2.28 nanoseconds
STREAM add bandwidth: 10543.42 MB/sec
STREAM triad latency: 4.52 nanoseconds
STREAM triad bandwidth: 5308.40 MB/sec

[root@101a0500 /root/lmbench3]
#numactl -C 7 -m 15 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 8.73 nanoseconds
STREAM copy bandwidth: 1831.77 MB/sec
STREAM scale latency: 8.81 nanoseconds
STREAM scale bandwidth: 1815.13 MB/sec
STREAM add latency: 5.63 nanoseconds
STREAM add bandwidth: 4265.21 MB/sec
STREAM triad latency: 13.09 nanoseconds
STREAM triad bandwidth: 1833.68 MB/sec

Lat_mem_rd 用cpu7访问node0和node15对比结果,随着数据的加大,延时在加大,64M时能有3倍差距,和上面测试一致

下图 第一列 表示读写数据的大小(单位M),第二列表示访问延时(单位纳秒),一般可以看到在L1/L2/L3 cache大小的地方延时会有跳跃,远超过L3大小后,延时就是内存延时了

image-20210924185044090

测试命令如下

1
numactl -C 7 -m 0 ./bin/lat_mem_rd -W 5 -N 5 -t 64M  //-C 7 cpu 7, -m 0 node0, -W 热身 -t stride

同样的机型,开关numa的测试结果,关numa 时延、带宽都差了几倍,所以一定要开NUMA

image-20210924192330025

鲲鹏920

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
#for i in $(seq 0 15); do echo core:$i; numactl -N $i -m 7 ./bin/stream  -W 5 -N 5 -M 64M; done
STREAM copy latency: 1.84 nanoseconds
STREAM copy bandwidth: 8700.75 MB/sec
STREAM scale latency: 1.86 nanoseconds
STREAM scale bandwidth: 8623.60 MB/sec
STREAM add latency: 2.18 nanoseconds
STREAM add bandwidth: 10987.04 MB/sec
STREAM triad latency: 3.03 nanoseconds
STREAM triad bandwidth: 7926.87 MB/sec

#numactl -C 7 -m 1 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 2.05 nanoseconds
STREAM copy bandwidth: 7802.45 MB/sec
STREAM scale latency: 2.08 nanoseconds
STREAM scale bandwidth: 7681.87 MB/sec
STREAM add latency: 2.19 nanoseconds
STREAM add bandwidth: 10954.76 MB/sec
STREAM triad latency: 3.17 nanoseconds
STREAM triad bandwidth: 7559.86 MB/sec

#numactl -C 7 -m 2 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 3.51 nanoseconds
STREAM copy bandwidth: 4556.86 MB/sec
STREAM scale latency: 3.58 nanoseconds
STREAM scale bandwidth: 4463.66 MB/sec
STREAM add latency: 2.71 nanoseconds
STREAM add bandwidth: 8869.79 MB/sec
STREAM triad latency: 5.92 nanoseconds
STREAM triad bandwidth: 4057.12 MB/sec

[root@ARM 19:14 /root/lmbench3]
#numactl -C 7 -m 3 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 3.94 nanoseconds
STREAM copy bandwidth: 4064.25 MB/sec
STREAM scale latency: 3.82 nanoseconds
STREAM scale bandwidth: 4188.67 MB/sec
STREAM add latency: 2.86 nanoseconds
STREAM add bandwidth: 8390.70 MB/sec
STREAM triad latency: 4.78 nanoseconds
STREAM triad bandwidth: 5024.25 MB/sec

#numactl -C 24 -m 3 ./bin/stream -W 5 -N 5 -M 64M
STREAM copy latency: 4.10 nanoseconds
STREAM copy bandwidth: 3904.63 MB/sec
STREAM scale latency: 4.03 nanoseconds
STREAM scale bandwidth: 3969.41 MB/sec
STREAM add latency: 3.07 nanoseconds
STREAM add bandwidth: 7816.08 MB/sec
STREAM triad latency: 5.06 nanoseconds
STREAM triad bandwidth: 4738.66 MB/sec

海光7280

可以看到跨numa(一个numa也就是一个socket,等同于跨socket)RT从1.5上升到2.5,这个数据比鲲鹏920要好很多。
这里还会测试同一块CPU设置不同数量的numa node对性能的影响,所以接下来的测试会列出numa 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
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
[root@hygon8 14:32 /root/lmbench-master]
#lscpu
架构: x86_64
CPU 运行模式: 32-bit, 64-bit
字节序: Little Endian
Address sizes: 43 bits physical, 48 bits virtual
CPU: 128
在线 CPU 列表: 0-127
每个核的线程数: 2
每个座的核数: 32
座: 2
NUMA 节点: 8
厂商 ID: HygonGenuine
CPU 系列: 24
型号: 1
型号名称: Hygon C86 7280 32-core Processor
步进: 1
CPU MHz: 2194.586
BogoMIPS: 3999.63
虚拟化: AMD-V
L1d 缓存: 2 MiB
L1i 缓存: 4 MiB
L2 缓存: 32 MiB
L3 缓存: 128 MiB
NUMA 节点0 CPU: 0-7,64-71
NUMA 节点1 CPU: 8-15,72-79
NUMA 节点2 CPU: 16-23,80-87
NUMA 节点3 CPU: 24-31,88-95
NUMA 节点4 CPU: 32-39,96-103
NUMA 节点5 CPU: 40-47,104-111
NUMA 节点6 CPU: 48-55,112-119
NUMA 节点7 CPU: 56-63,120-127

//可以看到7号core比15、23、31号core明显要快,就近访问node 0的内存,跨numa node(跨Die)没有内存交织分配
[root@hygon8 14:32 /root/lmbench-master]
#time for i in $(seq 7 8 64); do echo $i; numactl -C $i -m 0 ./bin/stream -W 5 -N 5 -M 64M; done
7
STREAM copy latency: 1.38 nanoseconds
STREAM copy bandwidth: 11559.53 MB/sec
STREAM scale latency: 1.16 nanoseconds
STREAM scale bandwidth: 13815.87 MB/sec
STREAM add latency: 1.40 nanoseconds
STREAM add bandwidth: 17145.85 MB/sec
STREAM triad latency: 1.44 nanoseconds
STREAM triad bandwidth: 16637.18 MB/sec
15
STREAM copy latency: 1.67 nanoseconds
STREAM copy bandwidth: 9591.77 MB/sec
STREAM scale latency: 1.56 nanoseconds
STREAM scale bandwidth: 10242.50 MB/sec
STREAM add latency: 1.45 nanoseconds
STREAM add bandwidth: 16581.00 MB/sec
STREAM triad latency: 2.00 nanoseconds
STREAM triad bandwidth: 12028.83 MB/sec
23
STREAM copy latency: 1.65 nanoseconds
STREAM copy bandwidth: 9701.49 MB/sec
STREAM scale latency: 1.53 nanoseconds
STREAM scale bandwidth: 10427.98 MB/sec
STREAM add latency: 1.42 nanoseconds
STREAM add bandwidth: 16846.10 MB/sec
STREAM triad latency: 1.97 nanoseconds
STREAM triad bandwidth: 12189.72 MB/sec
31
STREAM copy latency: 1.64 nanoseconds
STREAM copy bandwidth: 9742.86 MB/sec
STREAM scale latency: 1.52 nanoseconds
STREAM scale bandwidth: 10510.80 MB/sec
STREAM add latency: 1.45 nanoseconds
STREAM add bandwidth: 16559.86 MB/sec
STREAM triad latency: 1.92 nanoseconds
STREAM triad bandwidth: 12490.01 MB/sec
39
STREAM copy latency: 2.55 nanoseconds
STREAM copy bandwidth: 6286.25 MB/sec
STREAM scale latency: 2.51 nanoseconds
STREAM scale bandwidth: 6383.11 MB/sec
STREAM add latency: 1.76 nanoseconds
STREAM add bandwidth: 13660.83 MB/sec
STREAM triad latency: 3.68 nanoseconds
STREAM triad bandwidth: 6523.02 MB/sec

如果这种芯片在bios里设置Die interleaving,4块die当成一个numa node吐出来给OS

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
#lscpu
架构: x86_64
CPU 运行模式: 32-bit, 64-bit
字节序: Little Endian
Address sizes: 43 bits physical, 48 bits virtual
CPU: 128
在线 CPU 列表: 0-127
每个核的线程数: 2
每个座的核数: 32
座: 2
NUMA 节点: 2
厂商 ID: HygonGenuine
CPU 系列: 24
型号: 1
型号名称: Hygon C86 7280 32-core Processor
步进: 1
CPU MHz: 2108.234
BogoMIPS: 3999.45
虚拟化: AMD-V
L1d 缓存: 2 MiB
L1i 缓存: 4 MiB
L2 缓存: 32 MiB
L3 缓存: 128 MiB
//注意这里bios配置了Die Interleaving Enable
//表示每路内多个Die内存交织分配,这样整个一个socket就是一个大Die
NUMA 节点0 CPU: 0-31,64-95
NUMA 节点1 CPU: 32-63,96-127


//enable die interleaving 后继续streaming测试
//最终测试结果表现就是7/15/23/31 core性能一致,因为默认一个numa内内存交织分配
//可以看到同一路下的四个die内存交织访问,所以4个node内存延时一样了(被平均),都不如8node就近快
[root@hygon3 16:09 /root/lmbench-master]
#time for i in $(seq 7 8 64); do echo $i; numactl -C $i -m 0 ./bin/stream -W 5 -N 5 -M 64M; done
7
STREAM copy latency: 1.48 nanoseconds
STREAM copy bandwidth: 10782.58 MB/sec
STREAM scale latency: 1.20 nanoseconds
STREAM scale bandwidth: 13364.38 MB/sec
STREAM add latency: 1.46 nanoseconds
STREAM add bandwidth: 16408.32 MB/sec
STREAM triad latency: 1.53 nanoseconds
STREAM triad bandwidth: 15696.00 MB/sec
15
STREAM copy latency: 1.51 nanoseconds
STREAM copy bandwidth: 10601.25 MB/sec
STREAM scale latency: 1.24 nanoseconds
STREAM scale bandwidth: 12855.87 MB/sec
STREAM add latency: 1.46 nanoseconds
STREAM add bandwidth: 16382.42 MB/sec
STREAM triad latency: 1.53 nanoseconds
STREAM triad bandwidth: 15691.48 MB/sec
23
STREAM copy latency: 1.50 nanoseconds
STREAM copy bandwidth: 10700.61 MB/sec
STREAM scale latency: 1.27 nanoseconds
STREAM scale bandwidth: 12634.63 MB/sec
STREAM add latency: 1.47 nanoseconds
STREAM add bandwidth: 16370.67 MB/sec
STREAM triad latency: 1.55 nanoseconds
STREAM triad bandwidth: 15455.75 MB/sec
31
STREAM copy latency: 1.50 nanoseconds
STREAM copy bandwidth: 10637.39 MB/sec
STREAM scale latency: 1.25 nanoseconds
STREAM scale bandwidth: 12778.99 MB/sec
STREAM add latency: 1.46 nanoseconds
STREAM add bandwidth: 16420.65 MB/sec
STREAM triad latency: 1.61 nanoseconds
STREAM triad bandwidth: 14946.80 MB/sec
39
STREAM copy latency: 2.35 nanoseconds
STREAM copy bandwidth: 6807.09 MB/sec
STREAM scale latency: 2.32 nanoseconds
STREAM scale bandwidth: 6906.93 MB/sec
STREAM add latency: 1.63 nanoseconds
STREAM add bandwidth: 14729.23 MB/sec
STREAM triad latency: 3.36 nanoseconds
STREAM triad bandwidth: 7151.67 MB/sec
47
STREAM copy latency: 2.31 nanoseconds
STREAM copy bandwidth: 6938.47 MB/sec

intel 8269CY

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
lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 104
On-line CPU(s) list: 0-103
Thread(s) per core: 2
Core(s) per socket: 26
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz
Stepping: 7
CPU MHz: 3200.000
CPU max MHz: 3800.0000
CPU min MHz: 1200.0000
BogoMIPS: 4998.89
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 1024K
L3 cache: 36608K
NUMA node0 CPU(s): 0-25,52-77
NUMA node1 CPU(s): 26-51,78-103

[root@numaopen.cloud.et93 /home/ren/lmbench3]
#time for i in $(seq 0 8 51); do echo $i; numactl -C $i -m 0 ./bin/stream -W 5 -N 5 -M 64M; done
0
STREAM copy latency: 1.15 nanoseconds
STREAM copy bandwidth: 13941.80 MB/sec
STREAM scale latency: 1.16 nanoseconds
STREAM scale bandwidth: 13799.89 MB/sec
STREAM add latency: 1.31 nanoseconds
STREAM add bandwidth: 18318.23 MB/sec
STREAM triad latency: 1.56 nanoseconds
STREAM triad bandwidth: 15356.72 MB/sec
16
STREAM copy latency: 1.12 nanoseconds
STREAM copy bandwidth: 14293.68 MB/sec
STREAM scale latency: 1.13 nanoseconds
STREAM scale bandwidth: 14162.47 MB/sec
STREAM add latency: 1.31 nanoseconds
STREAM add bandwidth: 18293.27 MB/sec
STREAM triad latency: 1.53 nanoseconds
STREAM triad bandwidth: 15692.47 MB/sec
32
STREAM copy latency: 1.52 nanoseconds
STREAM copy bandwidth: 10551.71 MB/sec
STREAM scale latency: 1.52 nanoseconds
STREAM scale bandwidth: 10508.33 MB/sec
STREAM add latency: 1.38 nanoseconds
STREAM add bandwidth: 17363.22 MB/sec
STREAM triad latency: 2.00 nanoseconds
STREAM triad bandwidth: 12024.52 MB/sec
40
STREAM copy latency: 1.49 nanoseconds
STREAM copy bandwidth: 10758.50 MB/sec
STREAM scale latency: 1.50 nanoseconds
STREAM scale bandwidth: 10680.17 MB/sec
STREAM add latency: 1.34 nanoseconds
STREAM add bandwidth: 17948.34 MB/sec
STREAM triad latency: 1.98 nanoseconds
STREAM triad bandwidth: 12133.22 MB/sec
48
STREAM copy latency: 1.49 nanoseconds
STREAM copy bandwidth: 10736.56 MB/sec
STREAM scale latency: 1.50 nanoseconds
STREAM scale bandwidth: 10692.93 MB/sec
STREAM add latency: 1.34 nanoseconds
STREAM add bandwidth: 17902.85 MB/sec
STREAM triad latency: 1.96 nanoseconds
STREAM triad bandwidth: 12239.44 MB/sec

Intel(R) Xeon(R) CPU E5-2682 v4

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
#time for i in $(seq 0 8 51); do echo $i; numactl -C $i -m 0 ./bin/stream -W 5 -N 5 -M 64M; done
0
STREAM copy latency: 1.59 nanoseconds
STREAM copy bandwidth: 10092.31 MB/sec
STREAM scale latency: 1.57 nanoseconds
STREAM scale bandwidth: 10169.16 MB/sec
STREAM add latency: 1.31 nanoseconds
STREAM add bandwidth: 18360.83 MB/sec
STREAM triad latency: 2.28 nanoseconds
STREAM triad bandwidth: 10503.81 MB/sec
8
STREAM copy latency: 1.55 nanoseconds
STREAM copy bandwidth: 10312.14 MB/sec
STREAM scale latency: 1.56 nanoseconds
STREAM scale bandwidth: 10283.70 MB/sec
STREAM add latency: 1.30 nanoseconds
STREAM add bandwidth: 18416.26 MB/sec
STREAM triad latency: 2.23 nanoseconds
STREAM triad bandwidth: 10777.08 MB/sec
16
STREAM copy latency: 2.02 nanoseconds
STREAM copy bandwidth: 7914.25 MB/sec
STREAM scale latency: 2.02 nanoseconds
STREAM scale bandwidth: 7919.85 MB/sec
STREAM add latency: 1.39 nanoseconds
STREAM add bandwidth: 17276.06 MB/sec
STREAM triad latency: 2.92 nanoseconds
STREAM triad bandwidth: 8231.18 MB/sec
24
STREAM copy latency: 1.99 nanoseconds
STREAM copy bandwidth: 8032.18 MB/sec
STREAM scale latency: 1.98 nanoseconds
STREAM scale bandwidth: 8061.12 MB/sec
STREAM add latency: 1.39 nanoseconds
STREAM add bandwidth: 17313.94 MB/sec
STREAM triad latency: 2.88 nanoseconds
STREAM triad bandwidth: 8318.93 MB/sec

#lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 64
On-line CPU(s) list: 0-63
Thread(s) per core: 2
Core(s) per socket: 16
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 79
Model name: Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz
Stepping: 1
CPU MHz: 2500.000
CPU max MHz: 3000.0000
CPU min MHz: 1200.0000
BogoMIPS: 5000.06
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 40960K
NUMA node0 CPU(s): 0-15,32-47
NUMA node1 CPU(s): 16-31,48-63

stream对比数据

总结下几个CPU用stream测试访问内存的RT以及抖动和带宽对比数据

最小RT 最大RT 最大copy bandwidth 最小copy bandwidth
申威3231(2numa node) 7.09 8.75 2256.59 MB/sec 1827.88 MB/sec
飞腾2500(16 numa node) 2.84 10.34 5638.21 MB/sec 1546.68 MB/sec
鲲鹏920(4 numa node) 1.84 3.87 8700.75 MB/sec 4131.81 MB/sec
海光7280(8 numa node) 1.38 2.58 11591.48 MB/sec 6206.99 MB/sec
海光5280(4 numa node) 1.22 2.52 13166.34 MB/sec 6357.71 MB/sec
Intel8269CY(2 numa node) 1.12 1.52 14293.68 MB/sec 10551.71 MB/sec
Intel E5-2682(2 numa node) 1.58 2.02 10092.31 MB/sec 7914.25 MB/sec

从以上数据可以看出这5款CPU性能一款比一款好,飞腾2500慢的core上延时快到intel 8269的10倍了,平均延时5倍以上了。延时数据基本和单核上测试sysbench TPS一致。

lat_mem_rd对比数据

用不同的node上的core 跑lat_mem_rd测试访问node0内存的RT,只取最大64M的时延,时延和node距离完全一致,这里就不再列出测试原始数据了。

RT变化
飞腾2500(16 numa node) core:0 149.976
core:8 168.805
core:16 191.415
core:24 178.283
core:32 170.814
core:40 185.699
core:48 212.281
core:56 202.479
core:64 426.176
core:72 444.367
core:80 465.894
core:88 452.245
core:96 448.352
core:104 460.603
core:112 485.989
core:120 490.402
鲲鹏920(4 numa node) core:0 117.323
core:24 135.337
core:48 197.782
core:72 219.416
海光7280(8 numa node) numa0 106.839
numa1 168.583
numa2 163.925
numa3 163.690
numa4 289.628
numa5 288.632
numa6 236.615
numa7 291.880
分割行
enabled die interleaving
core:0 153.005
core:16 152.458
core:32 272.057
core:48 269.441
海光5280(4 numa node) core:0 102.574
core:8 160.989
core:16 286.850
core:24 231.197
Intel 8269CY(2 numa node) core:0 69.792
core:26 93.107
申威3231(2numa node) core:0 215.146
core:32 282.443

测试命令:

1
for i in $(seq 0 8 127); do echo core:$i; numactl -C $i -m 0 ./bin/lat_mem_rd -W 5 -N 5 -t 64M; done >lat.log 2>&1

测试结果和numactl -H 看到的node distance完全一致,芯片厂家应该就是这样测试然后把这个延迟当做距离写进去了

最后用一张实际测试Inte E5 L1 、L2、L3的cache延时图来加深印象,可以看到在每级cache大小附近时延有个跳跃:
undefined
纵坐标是访问延时 纳秒,横坐标是cache大小 M,为什么上图没放内存延时,因为延时太大,放出来就把L1、L2的跳跃台阶压平了

结论

  • X86比ARM性能要好
  • AMD和Intel单核基本差别不大,Intel适合要求核多的大实例,AMD适合云上拆分售卖
  • 国产CPU还有比较大的进步空间
  • 性能上的差异在数据库场景下归因下来主要在CPU访问内存的时延上
  • 跨Numa Node时延差异很大,一定要开NUMA 就近访问内存
  • 数据库场景下大实例因为锁导致CPU很难跑满,建议 分库分表搞多个mysqld实例

如果你一定要知道一块CPU性能的话先看 内存延时 而不是 主频,各种CPU自家打榜一般都是简单计算场景,内存访问不多,但是实际业务中大部分时候又是高频访问内存的。

参考资料

十年后数据库还是不敢拥抱NUMA?
Intel PAUSE指令变化是如何影响自旋锁以及MySQL的性能的
lmbench测试要考虑cache等
CPU的制造和概念
CPU 性能和Cache Line
Perf IPC以及CPU性能
CPU性能和CACHE

海光CPU

海光CPU

海光物理机CPU相关信息

总共有16台如下的海光服务器

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
#lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 64
On-line CPU(s) list: 0-63
Thread(s) per core: 2 //每个物理core有两个超线程
Core(s) per socket: 16 //每路16个物理core
Socket(s): 2 //2路
NUMA node(s): 4
Vendor ID: HygonGenuine
CPU family: 24
Model: 1
Model name: Hygon C86 5280 16-core Processor
Stepping: 1
CPU MHz: 2455.552
CPU max MHz: 2500.0000
CPU min MHz: 1600.0000
BogoMIPS: 4999.26
Virtualization: AMD-V
L1d cache: 32K
L1i cache: 64K
L2 cache: 512K
L3 cache: 8192K
NUMA node0 CPU(s): 0-7,32-39
NUMA node1 CPU(s): 8-15,40-47
NUMA node2 CPU(s): 16-23,48-55
NUMA node3 CPU(s): 24-31,56-63
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid amd_dcm aperfmperf pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpb hw_pstate sme ssbd sev ibpb vmmcall fsgsbase bmi1 avx2 smep bmi2 MySQLeed adx smap clflushopt sha_ni xsaveopt xsavec xgetbv1 xsaves clzero irperf xsaveerptr arat npt lbrv svm_lock nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold avic v_vmsave_vmload vgif overflow_recov succor smca

#numactl -H
available: 4 nodes (0-3)
node 0 cpus: 0 1 2 3 4 5 6 7 32 33 34 35 36 37 38 39
node 0 size: 128854 MB
node 0 free: 89350 MB
node 1 cpus: 8 9 10 11 12 13 14 15 40 41 42 43 44 45 46 47
node 1 size: 129019 MB
node 1 free: 89326 MB
node 2 cpus: 16 17 18 19 20 21 22 23 48 49 50 51 52 53 54 55
node 2 size: 128965 MB
node 2 free: 86542 MB
node 3 cpus: 24 25 26 27 28 29 30 31 56 57 58 59 60 61 62 63
node 3 size: 129020 MB
node 3 free: 98227 MB
node distances:
node 0 1 2 3
0: 10 16 28 22
1: 16 10 22 28
2: 28 22 10 16
3: 22 28 16 10

AMD Zen 架构的CPU是胶水核,也就是把两个die拼一块封装成一块CPU,所以一块CPU内跨die之间延迟还是很高的。

64 个 core 的分配策略

1
2
3
4
5
physical         core      processor
0 0~15 0~15
1 0~15 16~31
0 0~15 32~47
1 0~15 48~63

海光bios配置

1
2
3
4
5
6
在grub.conf里面加入noibrs noibpb nopti nospectre_v2 nospectre_v1 l1tf=off nospec_store_bypass_disable no_stf_barrier mds=off tsx=on tsx_async_abort=off mitigations=off iommu.passthrough=1;持久化ip;挂盘参数defaults,noatime,nodiratime,lazytime,delalloc,nobarrier,data=writeback(因为后面步骤要重启,把一些OS优化也先做了)
2. bios设置里面
配置 Hygon 设定 --- DF选项 --- 内存交错 --- Channel
--- NB选项 --- 关闭iommu
打开CPB
风扇模式设置为高性能模式

海光简介

公司成立于2016年3月,当前送测处理器为其第一代1.0版本的7185对标处理器为Intel的E5-2680V4,其服务器样机为曙光H620-G30。

海光CPU的命名规则:
型号71xx
7:高端
1:海光1号
xx:sku

其后续roadmap如下图

image-20221026100149808

img

海光其产品规格如下,产品相对密集,但是产品之间差异化很小,频率总体接近。

img

AMD授权Zen IP给海光的操作是先成立合资公司,授权给合资公司基于Zen 研发新的 CPU,而且转让给中国的所有信息都符合美国出口法规**。**天津海光和AMD成立的合资公司可以修改AMD的CPU核,变相享有X86授权,而海光公司可以通过购买合资公司研发的CPU核,开发服务器CPU,不过仅仅局限于中国市场。

AMD与国内公司A成立合资公司B,合资公司B由AMD控股,负责开发CPU核(其实就是拿AMD现成的内核),然后公司A购买合资公司B开发的CPU核,以此为基础开发CPU,最终实现ARM卖IP核的翻版。

image-20221026100539350

image-20221026100646800

海光与AMD 的 Ryzen/EPYC 比较

由于在 Zen 1 的基础上进行了大量的修改,海光 CPU 可以不用简单地称之为换壳 AMD 处理器了。但其性能相比同代原版 CPU 略差:整数性能基本相同,浮点性能显著降低——普通指令吞吐量只有基准水平的一半。海光 CPU 的随机数生成机制也被修改,加密引擎已被替换,不再对常见的 AES 指令进行加速,但覆盖了其他面向国内安全性的指令如 SM2、SM3 和 SM4。

相同

与 AMD 的 Ryzen/EPYC 相比,海光处理器究竟有哪些不同?总体而言,核心布局是相同的,缓存大小、TLB 大小和端口分配都相同,在基础级别上两者没有差异。CPU 仍然是 64KB 四路 L1 指令缓存,32KB 八路 L1 数据缓存,512KB 八路 L2 缓存以及 8MB 十六路 L3 缓存,与 Zen 1 核心完全相同。

不同

加密方式变化

在 Linux 内核升级中有关加密变化的信息已经明示。这些更新围绕 AMD 虚拟化功能(SEV)的安全加密进行。通常对于 EPYC 处理器来说,SEV 由 AMD 定义的加密协议控制,在这种情况下为 RSA、ECDSA、ECDH、SHA 和 AES。

但在海光 Dhyana 处理器中,SEV 被设计为使用 SM2、SM3 和 SM4 算法。在更新中有关 SM2 的部分声明道,这种算法基于椭圆曲线加密法,且需要其他私钥/公钥交换;SM3 是一种哈希算法,类似于 SHA-256;而 SM4 是类似于 AES-128 的分组密码算法。为支持这些算法所需的额外功能,其他指令也被加入到了 Linux 内核中。在说明文件中指出,这些算法已在 Hygon Dhyana Plus 处理器上成功进行测试,也已在 AMD 的 EPYC CPU 上成功测试。

此外,海光与 AMD 原版芯片最大的设计区别在于吞吐量,尽管整数性能相同,但海光芯片对于某些浮点指令并未做流水线处理,这意味着吞吐量和延迟都减小了:

img

这些对于最基础的任务来说也会有所影响,降低吞吐量的设计会让 CPU 在并行计算时性能受限。另外一个最大的变化,以及 Dhyana 与服务器版的「Dhyana Plus」版本之间的不同在于随机数生成的能力。

一次海光物理机资源竞争压测的记录

一次海光物理机资源竞争压测的记录

问题描述

问题描述如下

sysbench 压200个服务节点(每个4c16G,总共800core), 发现qps不能线性增加(200节点比100节点好1.2倍而已)。

如果压单个服务节点节点QPS 2.4万,CPU跑到390%(每个服务节点独占4个核),如果压200个服务节点(分布在16台64核的海光物理机上)平均每个服务节点节点QPS才1.2万。但是每个服务节点的CPU也跑到了390%左右。 现在的疑问就是为什么CPU跑上去了QPS打了个5折。

机器集群为16*64core 为1024core,也就是每个服务节点独占4core还有冗余

因为服务节点还需要通过LVS调用后端的多个MySQL集群,所以需要排除LVS、网络等链路瓶颈,然后找到根因是什么。

海光物理机CPU相关信息

总共有16台如下的海光服务器

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
#lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 64
On-line CPU(s) list: 0-63
Thread(s) per core: 2 //每个物理core有两个超线程
Core(s) per socket: 16 //每路16个物理core
Socket(s): 2 //2路
NUMA node(s): 4
Vendor ID: HygonGenuine
CPU family: 24
Model: 1
Model name: Hygon C86 5280 16-core Processor
Stepping: 1
CPU MHz: 2455.552
CPU max MHz: 2500.0000
CPU min MHz: 1600.0000
BogoMIPS: 4999.26
Virtualization: AMD-V
L1d cache: 32K
L1i cache: 64K
L2 cache: 512K
L3 cache: 8192K
NUMA node0 CPU(s): 0-7,32-39
NUMA node1 CPU(s): 8-15,40-47
NUMA node2 CPU(s): 16-23,48-55
NUMA node3 CPU(s): 24-31,56-63
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid amd_dcm aperfmperf pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpb hw_pstate sme ssbd sev ibpb vmmcall fsgsbase bmi1 avx2 smep bmi2 MySQLeed adx smap clflushopt sha_ni xsaveopt xsavec xgetbv1 xsaves clzero irperf xsaveerptr arat npt lbrv svm_lock nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold avic v_vmsave_vmload vgif overflow_recov succor smca

#numactl -H
available: 4 nodes (0-3)
node 0 cpus: 0 1 2 3 4 5 6 7 32 33 34 35 36 37 38 39
node 0 size: 128854 MB
node 0 free: 89350 MB
node 1 cpus: 8 9 10 11 12 13 14 15 40 41 42 43 44 45 46 47
node 1 size: 129019 MB
node 1 free: 89326 MB
node 2 cpus: 16 17 18 19 20 21 22 23 48 49 50 51 52 53 54 55
node 2 size: 128965 MB
node 2 free: 86542 MB
node 3 cpus: 24 25 26 27 28 29 30 31 56 57 58 59 60 61 62 63
node 3 size: 129020 MB
node 3 free: 98227 MB
node distances:
node 0 1 2 3
0: 10 16 28 22
1: 16 10 22 28
2: 28 22 10 16
3: 22 28 16 10

AMD Zen 架构的CPU是胶水核,也就是把两个die拼一块封装成一块CPU,所以一块CPU内跨die之间延迟还是很高的。

验证是否是上下游的瓶颈

需要先分析问题是否在LVS调用后端的多个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
40
41
42
43
44
45
#cat Test.java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
/*
* 目录:/home/admin/jdbc
*
* 编译:
* javac -cp /home/admin/lib/*:. Test.java
*
* 运行:
* java -cp /home/admin/MySQL-server/lib/*:. Test "jdbc:mysql://172.16.160.1:4261/qc_pay_0xwd_0002" "myhhzi0d" "jOXaC1Lbif-k" "select count(*) from pay_order where user_id=1169257092557639682 and order_no='201909292111250000102'" "100"
* */
public class Test {

public static void main(String args[]) throws NumberFormatException, InterruptedException, ClassNotFoundException {
Class.forName("com.mysql.jdbc.Driver");
String url = args[0];
String user = args[1];
String pass = args[2];
String sql = args[3];
String interval = args[4];
try {
Connection conn = DriverManager.getConnection(url, user, pass);
while (true) {
long start = System.currentTimeMillis();
for(int i=0; i<1000; ++i){
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
}
rs.close();
stmt.close();
Thread.sleep(Long.valueOf(interval));
}
long end = System.currentTimeMillis();
System.out.println("rt : " + (end - start));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}

然后通过传入不同的jdbc参数跑2组测试:

  1. 走服务节点执行指定id的点查;
  2. 直接从服务节点节点连MySQL指定id点查

上述2组测试同时跑在三组场景下:

  • A) 服务节点和MySQL都没有压力;
  • B) 跑1、2测试的服务节点没有压力,但是sysbench 在压别的服务节点,这样后端的MySQL是有sysbench压侧压力,LVS也有流量压力的;
  • C) sysbench压所有服务节点, 包含运行 1、2测试程序节点)

这样2组测试3个场景组合可以得到6组响应时间的测试数据

从最终得到6组数据来看可以排除链路以及MySQL的问题,瓶颈似乎还是在服务节点上

image.png

单独压一个服务节点节点并在上面跑测试,服务节点 CPU被压到 390%(每个服务节点 节点固定绑到4核), 这个时候整个宿主机压力不大,但是这四个核比较紧张了

1
2
3
4
5
6
7
#cat rt.log  | awk '{ print $3 }'  | awk '{if(min==""){min=max=$1}; if($1>max) {max=$1}; if($1<min) {min=$1}; total+=$1; count+=1} END {print "avg " total/count," | max "max," | min " min, "| count ", count}' ; cat MySQL.log  | awk '{ print $3 }'  | awk '{if(min==""){min=max=$1}; if($1>max) {max=$1}; if($1<min) {min=$1}; total+=$1; count+=1} END {print "avg " total/count," | max "max," | min " min, "| count ", count }';
avg 2589.13 | max 3385 | min 2502 | count 69
avg 1271.07 | max 1405 | min 1254 | count 141

[root@d42a01107.cloud.a02.am78 /root]
#taskset -pc 48759
pid 48759's current affinity list: 19,52-54

通过这6组测试数据可以看到,只有在整个系统都有压力(服务节点所在物理机、LVS、MySQL)的时候rt飙升最明显(C组数据),如果只是LVS、MySQL有压力,服务节点没有压力的时候可以看到数据还是很好的(B组数据)

分析宿主机资源竞争

perf分析

只压单个服务节点

image.png

image.png

从以上截图,可以看到关键的 insn per cycle 能到0.51和0.66(这个数值越大性能越好)

如果同时压物理机上的所有服务节点

image.png

image.png

从以上截图,可以看到关键的 insn per cycle 能降到了0.27和0.31(这个数值越大性能越好),基本相当于单压的5折

通过 perf list 找出所有Hardware event,然后对他们进行perf:

1
sudo perf stat -e branch-instructions,branch-misses,cache-references,cpu-cycles,instructions,stalled-cycles-backend,stalled-cycles-frontend,L1-dcache-load-misses,L1-dcache-loads,L1-dcache-prefetches,L1-icache-load-misses,L1-icache-loads,branch-load-misses,branch-loads,dTLB-load-misses,dTLB-loads,iTLB-load-misses,iTLB-loads -a -- `pidof java`

尝试不同的绑核后的一些数据

通过以上perf数据以及numa结构,尝试将不同服务进程绑定到指定的4个核上

试了以下三种绑核的办法:

1)docker swarm随机绑(以上测试都是用的这种默认方案);

2)一个服务节点绑连续4个core,这4个core都在同一个node;

3)一个服务节点绑4个core,这个4个core都在在同一个node,同时尽量HT在一起,也就是0,1,32,33 ; 2,3,34,35 这种绑法

结果是绑法2性能略好.

如果是绑法2,压单个服务节点 QPS能到2.3万;绑法1和3,压单个服务节点性能差别不明显,都是2万左右。

尝试将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组成。

Java进程用上HugePages后iTLB-load-misses从80%下降到了14%左右, dTLB也从30%下降到了20%,但是ipc变化不明显,QPS有不到10%的增加(不能确定是不是抖动所致)

image.png

在公有云ecs虚拟机上测试对性能没啥帮助,实际看到用掉的HuagPage不多,如果/proc/sys/vm/nr_hugepages 设置比较大的话JVM会因为内存不足起不来,两者内存似乎是互斥的

用sysbench验证一下海光服务器的多core能力

Intel E5 2682 2.5G VS hygon 7280 2.0G(Zen1)

image-20210813095409553

image-20210813095757299

由以上两个测试结果可以看出单核能力hygon 7280 强于 Intel 2682,但是hygon超线程能力还是没有任何提升。Intel用超线程计算能将耗时从109秒降到74秒。但是hygon(Zen1) 只是从89秒降到了87秒,基本没有变化。

再补充一个Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz 对比数据

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
#taskset -c 1,53 /usr/bin/sysbench --num-threads=2 --test=cpu --cpu-max-prime=50000 run
sysbench 0.5: multi-threaded system evaluation benchmark

Running the test with following options:
Number of threads: 2
Random number generator seed is 0 and will be ignored


Primer numbers limit: 50000

Threads started!


General statistics:
total time: 48.5571s
total number of events: 10000
total time taken by event execution: 97.0944s
response time:
min: 8.29ms
avg: 9.71ms
max: 20.88ms
approx. 95 percentile: 9.71ms

Threads fairness:
events (avg/stddev): 5000.0000/2.00
execution time (avg/stddev): 48.5472/0.01

#taskset -c 1 /usr/bin/sysbench --num-threads=1 --test=cpu --cpu-max-prime=50000 run
sysbench 0.5: multi-threaded system evaluation benchmark

Running the test with following options:
Number of threads: 1
Random number generator seed is 0 and will be ignored


Primer numbers limit: 50000

Threads started!


General statistics:
total time: 83.2642s
total number of events: 10000
total time taken by event execution: 83.2625s
response time:
min: 8.27ms
avg: 8.33ms
max: 10.03ms
approx. 95 percentile: 8.36ms

Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 83.2625/0.00

#lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 104
On-line CPU(s) list: 0-103
Thread(s) per core: 2
Core(s) per socket: 26
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz
Stepping: 7
CPU MHz: 3200.097
CPU max MHz: 3800.0000
CPU min MHz: 1200.0000
BogoMIPS: 4998.89
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 1024K
L3 cache: 36608K
NUMA node0 CPU(s): 0-25,52-77
NUMA node1 CPU(s): 26-51,78-103
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch ida arat epb invpcid_single pln pts dtherm spec_ctrl ibpb_support tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm mpx rdt avx512f avx512dq rdseed adx smap clflushopt avx512cdavx512bw avx512vl xsaveopt xsavec xgetbv1 cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local cat_l3 mba

用sysbench 测试Hygon C86 5280 16-core Processor,分别1、8、16、24、32、40、48、56、64 个thread,32个thread前都是完美的线性增加,32core之后基本不增长了,这个应该能说明这个服务器就是32core的能力

sysbench –threads=1 –cpu-max-prime=50000 cpu run

image.png

对比下intel的 Xeon 104core,也是物理52core,但是性能呈现完美线性

image.png

openssl场景多核能力验证

1
openssl speed aes-256-ige -multi N

intel 52 VS 26,可以看到52个线程的性能大概是26个的1.8倍

image.png

intel 104 VS 52 线程,性能还能提升1.4倍

image.png

海光32 VS 16, 性能能提升大概1.8倍,跟intel一致

image.png

海光64 VS 32, 性能能提升大概1.2倍

image.png

总结下就是,在物理core数以内的线程数intel和海光性能基本增加一致;但如果超过物理core数开始使用HT后海光明显相比Intel差了很多。

intel超线程在openssl场景下性能能提升40%,海光就只能提升20%了。

对比一下鲲鹏920 ARM架构的芯片

1
2
3
4
5
6
7
8
#numactl -H
available: 1 nodes (0)
node 0 cpus: 0 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
node 0 size: 773421 MB
node 0 free: 756092 MB
node distances:
node 0
0: 10

96核一起跑openssl基本就是1核的96倍,完美线性,这是因为鲲鹏就没有超线程,都是物理核。如果并发增加到192个,性能和96个基本一样的。

image.png

用Sysbench直接压MySQL oltp_read_only的场景

image.png

从1到10个thread的时候完美呈现线性,到20个thread就只比10个thread增加50%了,30thread比20增加40%,过了32个thread后增加10个core性能加不到10%了。

在32thread前,随着并发的增加 IPC也有所减少,这也是导致thread翻倍性能不能翻倍的一个主要原因。

基本也和openssl 场景一致,海光的HT基本可以忽略,做的不是很好。超过32个thread后(物理core数)性能增加及其微弱

结论

海光的一个core只有一个fpu,所以超线程算浮点完全没用.

FP处理所有矢量操作。简单的整数向量运算(例如shift,add)都可以在一个周期内完成,是AMD以前架构延迟的一半。基本浮点数学运算具有三个周期的延迟,其中包括乘法(用于双精度需要一个额外周期)。融合乘加是五个周期。

FP具有用于128位加载操作的单个管道。实际上,整个FP端都针对128位操作进行了优化。 Zen支持所有最新指令,例如SSE和AVX1/2。 256位AVX的设计方式是可以将它们作为两个独立的128位操作来执行。 Zen通过将这些指令作为两个操作。也就是说,Zen将256位操作分为两个µOP。同样,存储也是在128位块上完成的,从而使256位加载的有效吞吐量为每两个周期一个存储。这些管道之间的平衡相当好,因此大多数操作将安排至少两个管道,以保持每个周期至少一个这样的指令的吞吐量。暗示着,**256位操作将占用两倍的资源来完成操作(即2x寄存器,调度程序和端口)。这是AMD采取的一种折衷方案,有助于节省芯片空间和功耗。**相比之下,英特尔的竞争产品Skylake确实具有专用的256位电路。还应注意的是,英特尔的现代服务器级型号进一步扩展了此功能,以纳入支持AVX-512的专用512位电路,而性能最高的型号则具有二个专用的AVX-512单元。

此外,Zen还支持SHA和AES(并实现了2个AES单元),以提高加密性能。这些单位可以在浮点调度程序的管道0和1上找到。

这个也是为什么浮点比Intel X86会弱的原因。

一些其他对比结论

  • 对纯CPU 运算场景,并发不超过物理core时,比如Prime运算,比如DRDS(CPU bound,IO在网络,可以加并发弥补)
    • 海光的IPC能保持稳定;
    • intel的IPC有所下降,但是QPS在IPC下降后还能完美线性
  • 在openssl和MySQL oltp_read_only场景下
    • 如果并发没超过物理core数时,海光和Intel都能随着并发的翻倍性能能增加80%
    • 如果并发超过物理core数后,Intel还能随着并发的翻倍性能增加50%,海光增加就只有20%了
    • 简单理解在这两个场景下Intel的HT能发挥半个物理core的作用,海光的HT就只能发挥0.2个物理core的作用了
  • 海光5280/7280 是Zen1/Zen2的AMD 架构,每个core只有一个fpu,综上在多个场景下HT基本上都可以忽略

系列文章

CPU的制造和概念

[CPU 性能和Cache Line](/2021/05/16/CPU Cache Line 和性能/)

[Perf IPC以及CPU性能](/2021/05/16/Perf IPC以及CPU利用率/)

Intel、海光、鲲鹏920、飞腾2500 CPU性能对比

飞腾ARM芯片(FT2500)的性能测试

十年后数据库还是不敢拥抱NUMA?

一次海光物理机资源竞争压测的记录

[Intel PAUSE指令变化是如何影响自旋锁以及MySQL的性能的](/2019/12/16/Intel PAUSE指令变化是如何影响自旋锁以及MySQL的性能的/)

参考资料

How to use Huge Pages with Java and Linux这个资料中提到了Java使用HugePage的时候启动进程的用户权限问题,在我的docker容器中用的admin启动的进程,测试验证是不需要按资料中的设置。

Intel PAUSE指令变化是如何影响自旋锁以及MySQL的性能的

华为TaiShan服务器ARMNginx应用调优案例 大量绑核、中断、Numa等相关调优信息

macOS下如何使用iTerm2访问水木社区

macOS下如何使用iTerm2访问水木社区

关键字: macOS、iTerm 、Dracula、ssh、bbs.newsmth.net

windows下有各种Term软件来帮助我们通过ssh访问bbs.newsmth.net, 但是工作环境切换到macOS后发现FTerm、CTerm这样的工具都没有对应的了。但是term下访问 bbs.newsmth.net 简直是太爽了,所以本文希望解决这个问题。

问题

ssh 访问 bbs.newsmth.net 是没问题的,但是要解决配色和字符编码问题

解决编码

在iTerm2的配置中增加一个profile,如下图 smth,主要是改字符编码集为 GB 18030,然后修改配色方案,我喜欢的Dracula不适合SMTH,十大完全看不了。

image-20210602133111201

执行脚本如下:

1
2
3
4
#cat /usr/local/bin/smth
echo -e "\033]50;SetProfile=smth\a" //切换到smth的profile,也就是切换到了 GB18030
sshpass -p'密码' ssh -o ServerAliveInterval=60 水木id@bbs.newsmth.net
echo -e "\033]50;SetProfile=Default\a" //切换回UTF8

SetProfile=smth用来解决profile切换,连smth term前切换成GB 18030,断开的时候恢复成UTF-8,要不然的话正常工作的命令行就乱码了。

解决配色问题

然后还是在profile里面把smth的配色方案改成:Tango Dark, 一切简直是完美,工作灌水两不误,别人还发现不了

最终效果

目录(右边是工作窗口):

image.png

十大,这个十大颜色和右边工作模式的配色方案不一样

image.png

断开后恢复成 Dracula 配色和UTF-8编码,不影响工作,别的工作tab也还是正常使用utf8

image.png

别的term网站也是类似,比如小百合、byr、ptt等

TCP疑难问题案例汇总

TCP疑难问题案例汇总

碰到各种奇葩的TCP相关问题,所以汇总记录一下。分析清楚这些问题的所有来龙去脉,就能帮你在TCP知识体系里建立几个坚固的抓手,让TCP知识慢慢在抓手之间生长和互通

服务不响应的现象或者奇怪异常的原因分析

一个黑盒程序奇怪行为的分析 listen端口上很快就全连接队列溢出了,导致整个程序不响应了

举三反一–从理论知识到实际问题的推导 服务端出现大量CLOSE_WAIT 个数正好 等于somaxconn(调整somaxconn大小后 CLOSE_WAIT 也会跟着变成一样的值)

活久见,TCP连接互串了 应用每过一段时间总是会抛出几个连接异常的错误,需要查明原因。排查后发现是TCP连接互串了,这个案例实在是很珍惜,所以记录一下。

如何创建一个自己连自己的TCP连接

传输速度分析

案例:TCP传输速度案例分析(长肥网络、rt升高、delay ack的影响等)

原理:就是要你懂TCP–性能和发送接收Buffer的关系:发送窗口大小(Buffer)、接收窗口大小(Buffer)对TCP传输速度的影响,以及怎么观察窗口对传输速度的影响。BDP、RT、带宽对传输速度又是怎么影响的

就是要你懂TCP–最经典的TCP性能问题 Nagle和Delay ack

就是要你懂TCP–性能优化大全

TCP队列问题以及连接数

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

就是要你懂TCP队列–通过实战案例来展示问题

就是要你懂TCP–半连接队列和全连接队列

就是要你懂TCP–握手和挥手

防火墙和reset定位分析

对ttl、identification等的运用

关于TCP连接的Keepalive和reset

就是要你懂网络–谁动了我的TCP连接

TCP相关参数

TCP相关参数解释

网络通不通是个大问题–半夜鸡叫

网络丢包

工具技巧篇

netstat定位性能案例

[netstat timer keepalive explain](/2017/08/28/netstat –timer/)

就是要你懂网络监控–ss用法大全

就是要你懂抓包–WireShark之命令行版tshark

[通过tcpdump对Unix Domain Socket 进行抓包解析](/2018/01/01/通过tcpdump对Unix Socket 进行抓包解析/)

如何追踪网络流量

一个黑盒程序奇怪行为的分析

一个黑盒程序奇怪行为的分析

问题描述:

从银行金主baba手里拿到一个区块链程序,监听4000,在我们的环境中4000端口上很快就全连接队列溢出了,导致整个程序不响应了。这个程序是黑盒子,没有源代码,但是在金主baba自己的环境运行正常(一样的OS)

如下图所示:

image.png

ss -lnt 看到全连接队列增长到了39,但是netstat -ant找不到这39个连接,本来是想看看队列堆了这么多连接,都是哪些ip连过来的,实际看不到这就奇怪了

同时验证过程发现我们的环境 4000端口上开了slb,也就是slb会不停滴探活4000端口,关掉slb探活后一切正常了。

所以总结下来问题就是:

  1. 为什么全连接队列里面的连接netstat看不到这些连接,但是ss能看到总数

  2. 为什么关掉slb就正常了

  3. 为什么应用不accept连接,也不close(应用是个黑盒子)

分析

为什么全连接队列里面的连接netstat/ss都看不到(ss能看到总数)

这是因为这些连接都是探活连接,三次握手后很快被slb reset了,在OS层面这个连接已经被释放,所以肯定看不见。反过来想要是netstat能看见这个连接,那么它的状态是什么? reset吗?tcp连接状态里肯定是没有reset状态的。

ss看到的总数是指只要这个连接没有被accept,那么连接队列里就还有这个连接,通过ss也能看到连接队列数量。

为什么会产生这个错误理解–全连接队列里面的连接netstat一定要能看到?

那是因为正常情况都是能看到的,从没有考虑过握手后很快reset的情况。也没反问过如果能看到这个连接该是什么状态呢?

这个连接被reset后,kernel会将全连接队列数量减1吗?

不会,按照我们的理解连接被reset释放后,那么kernel要释放全连接队列里面的这个连接,因为这些动作都是kernel负责,上层没法处理这个reset。实际上内核认为所有 listen 到的连接, 必须要 accept 走, 用户有权利知道存在过这么一个连接。

也就是reset后,连接在内核层面释放了,所以netstat/ss看不到,但是全连接队列里面的应用数不会减1,只有应用accept后队列才会减1,accept这个空连接后读写会报错。基本可以认为全连接队列溢出了,主要是应用accept太慢导致的。

当应用从accept队列读取到已经被reset了的连接的时候应用层会得到一个报错信息。

什么时候连接状态变成 ESTABLISHED

三次握手成功就变成 ESTABLISHED 了,三次握手成功的第一是收到第三步的ack并且全连接队列没有满,不需要用户态来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 *:*

为什么关掉slb就正常了

slb探活逻辑是向监听端口发起三次握手,握手成功后立即发送一个reset断开连接

这是一个完整的探活过程:

image.png

关掉就正常后要结合第三个问题来讲

为什么应用不accept连接,也不close(应用是个黑盒子)

因为应用是个黑盒子,看不到源代码,只能从行为来分析了

从行为来看,这个应用在三次握手后,会主动给client发送一个12字节的数据,但是这个逻辑写在了accept主逻辑内部,一旦主动给client发12字节数据失败(比如这个连接reset了)那么一直卡在这里导致应用不再accept也不再close。

正确的实现逻辑是,accept在一个单独的线程里,一旦accept到一个新连接,那么就开启一个新的线程来处理这个新连接的读写。accept线程专注accept。

关掉slb后应用有机会发出这12个字节,然后accept就能继续了,否则就卡死了。

一些验证

nc测试连接4000端口

1
2
3
4
5
6
7
8
9
10
11
# nc -p 12346 dcep-blockchain-1 4000
 //握手后4000返回的内容

抓包:
11:03:16.762547 IP dcep-blockchain-1.12346 > dcep-blockchain-1.terabase: Flags [S], seq 397659761, win 43690, options [mss 65495,sackOK,TS val 2329725964 ecr 0,nop,wscale 7], length 0
04:42:24.466211 IP dcep-blockchain-1.terabase > dcep-blockchain-1.12346: Flags [S.], seq 4239354556, ack 397659762, win 43690, options [mss 65495,sackOK,TS val 2329725964 ecr 2329725964,nop,wscale 7], length 0
11:03:16.762571 IP dcep-blockchain-1.12346 > dcep-blockchain-1.terabase: Flags [.], ack 1, win 342, options [nop,nop,TS val 2329725964 ecr 2329725964], length 0

----到这三次握手完毕,下面是隔了大概1.5ms,4000发了12字节给nc
11:03:16.763893 IP dcep-blockchain-1.terabase > dcep-blockchain-1.12346: Flags [P.], seq 1:13, ack 1, win 342, options [nop,nop,TS val 2329725966 ecr 2329725964], length 12
11:03:16.763904 IP dcep-blockchain-1.12346 > dcep-blockchain-1.terabase: Flags [.], ack 13, win 342, options [nop,nop,TS val 2329725966 ecr 2329725966], length 0

如果在上面的1.5ms之间nc reset了这个连接,那么这12字节就发不出来了

握手后Server主动发数据的行为非常像MySQL Server

MySQL Server在收到mysql client连接后会主动发送 Server Greeting、版本号、认证方式等给client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#nc -p 12345 127.0.0.1 3306
J
5.6.29�CuaV9v0xo�!
qCHRrGRIJqvzmysql_native_password

#tcpdump -i any port 12345 -ennt
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
In 00:00:00:00:00:00 ethertype IPv4 (0x0800), length 76: 127.0.0.1.12345 > 127.0.0.1.3306: Flags [S], seq 3186409724, win 43690, options [mss 65495,sackOK,TS val 3967896050 ecr 0,nop,wscale 7], length 0
In 00:00:00:00:00:00 ethertype IPv4 (0x0800), length 76: 127.0.0.1.3306 > 127.0.0.1.12345: Flags [S.], seq 4188709983, ack 3186409725, win 43690, options [mss 65495,sackOK,TS val 3967896051 ecr 3967896050,nop,wscale 7], length 0
In 00:00:00:00:00:00 ethertype IPv4 (0x0800), length 68: 127.0.0.1.12345 > 127.0.0.1.3306: Flags [.], ack 1, win 342, options [nop,nop,TS val 3967896051 ecr 3967896051], length 0 // 握手完毕
In 00:00:00:00:00:00 ethertype IPv4 (0x0800), length 146: 127.0.0.1.3306 > 127.0.0.1.12345: Flags [P.], seq 1:79, ack 1, win 342, options [nop,nop,TS val 3967896051 ecr 3967896051], length 78 //Server 发出Greeting
In 00:00:00:00:00:00 ethertype IPv4 (0x0800), length 68: 127.0.0.1.12345 > 127.0.0.1.3306: Flags [.], ack 79, win 342, options [nop,nop,TS val 3967896051 ecr 3967896051], length 0
In 00:00:00:00:00:00 ethertype IPv4 (0x0800), length 68: 127.0.0.1.3306 > 127.0.0.1.12345: Flags [F.], seq 79, ack 1, win 342, options [nop,nop,TS val 3967913551 ecr 3967896051], length 0
In 00:00:00:00:00:00 ethertype IPv4 (0x0800), length 68: 127.0.0.1.12345 > 127.0.0.1.3306: Flags [.], ack 80, win 342, options [nop,nop,TS val 3967913591 ecr 3967913551], length 0

如下是Server发出的长度为 78 的 Server Greeting信息内容

image.png

理论上如果slb探活连接检查MySQL Server的状态的时候也是很快reset了,如果MySQL Server程序写得烂的话也会出现同样的情况。

但是比如我们有实验验证MySQL Server 是否正常的时候会用 nc 去测试,一般以能看到

5.6.29�CuaV9v0xo�!
qCHRrGRIJqvzmysql_native_password

就认为MySQL Server是正常的。但是真的是这样吗?我们看看 nc 的如下案例

nc 6.4 快速fin

#nc –version
Ncat: Version 6.40 ( http://nmap.org/ncat )

用 nc 测试发现有一定的概率没有出现上面的Server Greeting信息,那么这是因为MySQL Server服务不正常了吗?

image.png

nc -i 3 10.97.170.11 3306 -w 4 -p 1234

-i 3 表示握手成功后 等三秒钟nc退出(发fin)

nc 6.4 握手后立即发fin断开连接,导致可能收不到Greeting,换成7.5或者mysql client就OK了

nc 7.5的抓包,明显可以看到nc在发fin前会先等4秒钟:

image.png

tcpping 模拟slb 探活

1
python tcpping.py -R -i 0.1 -t 1 dcep-blockchain-1 4000

-i 间隔0.1秒

-R reset断开连接

-t 超时时间1秒

执行如上代码,跟4000端口握手,然后立即发出reset断开连接(完全模拟slb探活行为),很快重现了问题

增加延时

-D 0.01表示握手成功后10ms后再发出reset(让应用有机会成功发出那12个字节),应用工作正常

1
python tcpping.py -R -i 0.1 -t 1 -D 0.01 dcep-blockchain-1 4000

总结

最大的错误认知就是 ss 看到的全连接队列数量,netstat也能看到。实际是不一定,而这个快速reset+应用不accept就导致了看不到这个现象

journald和rsyslogd

journald和rsyslogd

碰到rsyslog-8.24.0-34.1.al7.x86_64 的 rsyslogd 占用内存过高,于是分析了一下原因并学习了一下系统日志、rsyslog、journald之间的关系,流水账记录此文。

rsyslogd 占用内存过高的分析

rsyslogd使用了大概1.6-2G内存,不正常(正常情况下内存占用30-50M之间)

现象:

image.png

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
KiB Mem :  7971268 total,   131436 free,  7712020 used,   127812 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 43484 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
24850 admin 20 0 8743896 5.1g 0 S 2.0 66.9 1413:55 java
1318 root 20 0 2380404 1.6g 536 S 0.0 21.6 199:09.36 rsyslogd

# systemctl status rsyslog
● rsyslog.service - System Logging Service
Loaded: loaded (/usr/lib/systemd/system/rsyslog.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2020-10-20 16:01:01 CST; 3 months 8 days ago
Docs: man:rsyslogd(8)
http://www.rsyslog.com/doc/
Main PID: 1318 (rsyslogd)
CGroup: /system.slice/rsyslog.service
└─1318 /usr/sbin/rsyslogd -n

Jan 28 09:10:07 iZwz95gaul6x9167sqdqz4Z rsyslogd[1318]: sd_journal_get_cursor() failed: 'Cannot assign requested address' [v8.24.0-34.1.al7]
Jan 28 09:10:07 iZwz95gaul6x9167sqdqz4Z rsyslogd[1318]: imjournal: journal reloaded... [v8.24.0-34.1.al7 try http://www.rsyslog.com/e/0 ]
Jan 28 10:27:48 iZwz95gaul6x9167sqdqz4Z rsyslogd[1318]: sd_journal_get_cursor() failed: 'Cannot assign requested address' [v8.24.0-34.1.al7]
Jan 28 10:27:49 iZwz95gaul6x9167sqdqz4Z rsyslogd[1318]: imjournal: journal reloaded... [v8.24.0-34.1.al7 try http://www.rsyslog.com/e/0 ]
Jan 28 11:45:23 iZwz95gaul6x9167sqdqz4Z rsyslogd[1318]: sd_journal_get_cursor() failed: 'Cannot assign requested address' [v8.24.0-34.1.al7]
Jan 28 11:45:24 iZwz95gaul6x9167sqdqz4Z rsyslogd[1318]: imjournal: journal reloaded... [v8.24.0-34.1.al7 try http://www.rsyslog.com/e/0 ]
Jan 28 13:03:00 iZwz95gaul6x9167sqdqz4Z rsyslogd[1318]: sd_journal_get_cursor() failed: 'Cannot assign requested address' [v8.24.0-34.1.al7]
Jan 28 13:03:01 iZwz95gaul6x9167sqdqz4Z rsyslogd[1318]: imjournal: journal reloaded... [v8.24.0-34.1.al7 try http://www.rsyslog.com/e/0 ]
Jan 28 14:20:42 iZwz95gaul6x9167sqdqz4Z rsyslogd[1318]: sd_journal_get_cursor() failed: 'Cannot assign requested address' [v8.24.0-34.1.al7]
Jan 28 14:20:42 iZwz95gaul6x9167sqdqz4Z rsyslogd[1318]: imjournal: journal reloaded... [v8.24.0-34.1.al7 try http://www.rsyslog.com/e/0 ]


# grep HUPed /var/log/messages
Jan 24 03:39:15 iZwz95gaul6x9167sqdqz4Z rsyslogd: [origin software="rsyslogd" swVersion="8.24.0-34.1.al7" x-pid="1318" x-info="http://www.rsyslog.com"] rsyslogd was HUPed

# journalctl --verify
PASS: /var/log/journal/20190829214900434421844640356160/system@efef6fd56e2e4c9f861d0be25c8c0781-0000000001567546-0005b9e2e02a0a4f.journal
PASS: /var/log/journal/20190829214900434421844640356160/system@efef6fd56e2e4c9f861d0be25c8c0781-00000000015ae56b-0005b9ea76e922e9.journal
1be1e0: Data object references invalid entry at 1d03018
File corruption detected at /var/log/journal/20190829214900434421844640356160/system.journal:1d02d80 (of 33554432 bytes, 90%).
FAIL: /var/log/journal/20190829214900434421844640356160/system.journal (Bad message)

journalctl --verify命令检查发现系统日志卷文件损坏

问题根因

来自redhat官网的描述

image.png

以下是现场收集到的日志:

image.png

主要是rsyslogd的sd_journal_get_cursor报错,然后导致内存泄露。

journald 报Bad message, 跟rsyslogd内存泄露完全没关系,实际上升级rsyslogd后也有journald bad message,但是rsyslogd的内存一直稳定在30M以内

这个CSDN的文章中有完全一样的症状 但是作者的结论是:这是systemd的bug,在journald需要压缩的时候就会发生这个问题。实际上我用的是 systemd-219-62.6.al7.9.x86_64 比他描述的已经修复的版本还要要新,也还是有这个问题,所以这个结论是不对的

解决办法

1、重启rsyslog systemctl restart rsyslog 可以释放内存

2、升级rsyslog到rsyslog-8.24.0-38.1.al7.x86_64或更新的版本才能彻底修复这个问题

一些配置方法

修改配置/etc/rsyslog.conf,增加如下两行,然后重启systemctl restart rsyslog

1
2
3
$imjournalRatelimitInterval 0
$imjournalRatelimitBurst 0
12

1、关掉journal压缩配置

vi /etc/systemd/journald.conf,把#Compress=yes改成Compress=no,之后systemctl restart systemd-journald即可

2、限制rsyslogd 内存大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cat /etc/systemd/system/multi-user.target.wants/rsyslog.service

在Service配置中添加MemoryAccounting=yes,MemoryMax=80M,MemoryHigh=8M三项如下所示。
[Service]
Type=notify
EnvironmentFile=-/etc/sysconfig/rsyslog
ExecStart=/usr/sbin/rsyslogd -n $SYSLOGD_OPTIONS
Restart=on-failure
UMask=0066
StandardOutput=null
Restart=on-failure
MemoryAccounting=yes
MemoryMax=80M
MemoryHigh=8M

OOM kill

rsyslogd内存消耗过高后导致了OOM Kill

image.png

RSS对应物理内存,单位是4K(page大小),红框两个进程用了5G+2G,总内存是8G,所以触发OOM killer了

每次OOM Kill日志前后总带着systemd-journald的重启

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z journal: Permanent journal is using 520.0M (max allowed 500.0M, trying to leave 4.0G free of 83.7G available → current limit 520.0M).
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z journal: Journal started
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z kernel: AliYunDun invoked oom-killer: gfp_mask=0x6200ca(GFP_HIGHUSER_MOVABLE), nodemask=(null), order=0, oom_score_adj=0
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z kernel: AliYunDun cpuset=/ mems_allowed=0
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z kernel: CPU: 3 PID: 13296 Comm: AliYunDun Tainted: G OE 4.19.57-15.1.al7.x86_64 #1
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z kernel: Hardware name: Alibaba Cloud Alibaba Cloud ECS, BIOS 8c24b4c 04/01/2014
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z kernel: Call Trace:
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z kernel: dump_stack+0x5c/0x7b
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z kernel: dump_header+0x77/0x29f
***
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z kernel: [ 18118] 0 18118 28218 255 245760 0 0 sshd
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z kernel: Out of memory: Kill process 18665 (java) score 617 or sacrifice child
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z kernel: Killed process 18665 (java) total-vm:8446992kB, anon-rss:4905856kB, file-rss:0kB, shmem-rss:0kB
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z kernel: oom_reaper: reaped process 18665 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z systemd: systemd-journald.service watchdog timeout (limit 3min)!
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z rsyslogd: sd_journal_get_cursor() failed: 'Cannot assign requested address' [v8.24.0-34.1.al7]
Jan 28 19:03:04 iZwz95gaul6x9167sqdqz5Z rsyslogd: imjournal: journal reloaded... [v8.24.0-34.1.al7 try http://www.rsyslog.com/e/0 ]
Jan 28 20:14:38 iZwz95gaul6x9167sqdqz5Z rsyslogd: imjournal: journal reloaded... [v8.24.0-57.1.al7 try http://www.rsyslog.com/e/0 ]

image.png

OOM kill前大概率伴随着systemd-journald 重启是因为watch dog timeout(limit 3min),造成timeout的原因是journald定期要把日志刷到磁盘上,然后要么是内存不够,要么是io负载太重,导致刷磁盘这个过程非常慢,于是就timeout了。

当然systemd-journald 重启也不一定意味着OOM Killer,只是肯定是内存比较紧张了。

What is the difference between syslog, rsyslog and syslog-ng?

Basically, they are all the same, in the way they all permit the logging of data from different types of systems in a central repository.

But they are three different project, each project trying to improve the previous one with more reliability and functionalities.

The Syslog project was the very first project. It started in 1980. It is the root project to Syslog protocol. At this time Syslog is a very simple protocol. At the beginning it only supports UDP for transport, so that it does not guarantee the delivery of the messages.

Next came syslog-ng in 1998. It extends basic syslog protocol with new features like:

  • content-based filtering
  • Logging directly into a database
  • TCP for transport
  • TLS encryption

Next came Rsyslog in 2004. It extends syslog protocol with new features like:

  • RELP Protocol support
  • Buffered operation support

rsyslog和journald的基础知识

systemd-journald是用来协助rsyslog记录系统启动服务和服务启动失败的情况等等. systemd-journald使用内存保存记录, 系统重启记录会丢失. 所有还要用rsyslog来记录分类信息, 如上面/etc/rsyslog.d/listen.conf中的syslog分类.

systemd-journald跟随systemd开机就启动,能及时记录所有日志:

1
2
3
4
5
6
7
# systemd-analyze critical-chain systemd-journald.service
The time after the unit is active or started is printed after the "@" character.
The time the unit takes to start is printed after the "+" character.

systemd-journald.service +13ms
└─system.slice
└─-.slice

systemd-journald 由于是使用于内存的登录文件记录方式,因此重新开机过后,开机前的登录文件信息当然就不会被记载了。 为此,我们还是建议启动 rsyslogd 来协助分类记录!也就是说, systemd-journald 用来管理与查询这次开机后的登录信息,而 rsyslogd 可以用来记录以前及现在的所以数据到磁盘文件中,方便未来进行查询喔!

Tips 虽然 systemd-journald 所记录的数据其实是在内存中,但是系统还是利用文件的型态将它记录到 /run/log/ 下面! 不过我们从前面几章也知道, /run 在 CentOS 7 其实是内存内的数据,所以重新开机过后,这个 /run/log 下面的数据当然就被刷新,旧的当然就不再存在了!

其实鸟哥是这样想的,既然我们还有 rsyslog.service 以及 logrotate 的存在,因此这个 systemd-journald.service 产生的登录文件, 个人建议最好还是放置到 /run/log 的内存当中,以加快存取的速度!而既然 rsyslog.service 可以存放我们的登录文件, 似乎也没有必要再保存一份 journal 登录文件到系统当中就是了。单纯的建议!如何处理,依照您的需求即可喔!

system-journal服务监听 /dev/log socket获取日志, 保存在内存中, 并间歇性的写入/var/log/journal目录中.

rsyslog服务启动后监听/run/systemd/journal/socket 获取syslog类型日志, 并写入/var/log/messages文件中.

获取日志时需要记录日志条目的position/var/lib/rsyslog/imjournal.state文件中.

比如haproxy日志配置:

1
2
3
4
# cat /etc/haproxy/haproxy.cfg
global
# log发给journald(journald监听 /dev/log)
log /dev/log local1 warning

以下是drds 的iptables日志配置,将tcp reset包记录下来,默认iptable日志输出到/varlog/messages中(dmesg也能看到),然后可以通过rsyslog.d 配置将这部分日志输出到单独的文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 配置iptables 日志,增加 [drds] 标识
# cat /home/admin/drds-worker/install/drds_filter.conf
# Generated by iptables-save v1.4.21 on Wed Apr 1 11:39:31 2020
*filter
:INPUT ACCEPT [557:88127]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [527:171711]
-A INPUT -p tcp -m tcp ! --sport 3406 --tcp-flags RST RST -j LOG --log-prefix "[drds] " --log-level 7 --log-tcp-sequence --log-tcp-options --log-ip-options
# -A INPUT -p tcp -m tcp ! --dport 3406 --tcp-flags RST RST -j LOG --log-prefix "[drds] " --log-level7 --log-tcp-sequence --log-tcp-options --log-ip-options
-A OUTPUT -p tcp -m tcp ! --sport 3406 --tcp-flags RST RST -j LOG --log-prefix "[drds] " --log-level 7 --log-tcp-sequence --log-tcp-options --log-ip-options
COMMIT
# Completed on Wed Apr 1 11:39:31 2020

#通过rsyslogd将日志写出到指定位置(不配置的话默认输出到 dmesg)
# cat /etc/rsyslog.d/drds_filter_log.conf
:msg, startswith, "[drds]" -/home/admin/logs/tcp-rt/drds-tcp.log

journald log持久化

创建 /var/log/journal 文件夹后默认会持久化,设置持久化后 /run/log 里面就没有日志了

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 /etc/systemd/journald.conf
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Entries in this file show the compile time defaults.
# You can change settings by editing this file.
# Defaults can be restored by simply deleting this file.
#
# See journald.conf(5) for details.

[Journal]
#Storage=auto //默认如果有 /var/log/journal 目录就会持久化到这里
Compress=no
#Seal=yes
#SplitMode=uid
#SyncIntervalSec=5m
#RateLimitInterval=30s
#RateLimitBurst=1000
SystemMaxUse=500M //最多保留500M日志文件,免得撑爆磁盘
#SystemKeepFree=
#SystemMaxFileSize=
#RuntimeMaxUse=
#RuntimeKeepFree=
#RuntimeMaxFileSize=
#MaxRetentionSec=
#MaxFileSec=1month
#ForwardToSyslog=yes
#ForwardToKMsg=no
#ForwardToConsole=no
#ForwardToWall=yes
#TTYPath=/dev/console
#MaxLevelStore=debug
#MaxLevelSyslog=debug
#MaxLevelKMsg=notice
#MaxLevelConsole=info
#MaxLevelWall=emerg
#LineMax=48K

清理日志保留1M:journalctl –vacuum-size=1M

设置最大保留500M日志: journalctl –vacuum-size=500

rsyslogd

以下内容来自鸟哥的书:

CentOS 7 除了保有既有的 rsyslog.service 之外,其实最上游还使用了 systemd 自己的登录文件日志管理功能喔!他使用的是 systemd-journald.service 这个服务来支持的。基本上,系统由 systemd 所管理,那所有经由 systemd 启动的服务,如果再启动或结束的过程中发生一些问题或者是正常的讯息, 就会将该讯息由 systemd-journald.service 以二进制的方式记录下来,之后再将这个讯息发送给 rsyslog.service 作进一步的记载。

基本上, rsyslogd 针对各种服务与讯息记录在某些文件的配置文件就是 /etc/rsyslog.conf, 这个文件规定了“(1)什么服务 (2)的什么等级讯息 (3)需要被记录在哪里(设备或文件)” 这三个咚咚,所以设置的语法会是这样:

1
2
3
4
5
6
$cat /etc/rsyslog.conf
服务名称[.=!]讯息等级 讯息记录的文件名或设备或主机
# 下面以 mail 这个服务产生的 info 等级为例:
mail.info /var/log/maillog_info
# 这一行说明:mail 服务产生的大于等于 info 等级的讯息,都记录到
# /var/log/maillog_info 文件中的意思。

syslog 所制订的服务名称与软件调用的方式

CentOS 7.x 默认的 rsyslogd 本身就已经具有远程日志服务器的功能了, 只是默认并没有启动该功能而已。你可以通过 man rsyslogd 去查询一下相关的选项就能够知道啦! 既然是远程日志服务器,那么我们的 Linux 主机当然会启动一个端口来监听了,那个默认的端口就是 UDP 或 TCP 的 port 514

image.png

Server配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ cat /etc/rsyslog.conf
# 找到下面这几行:
# Provides UDP syslog reception
#$ModLoad imudp
#$UDPServerRun 514

# Provides TCP syslog reception
#$ModLoad imtcp
#$InputTCPServerRun 514
# 上面的是 UDP 端口,下面的是 TCP 端口!如果你的网络状态很稳定,就用 UDP 即可。
# 不过,如果你想要让数据比较稳定传输,那么建议使用 TCP 啰!所以修改下面两行即可!
$ModLoad imtcp
$InputTCPServerRun 514

# 2\. 重新启动与观察 rsyslogd 喔!
[root@study ~]# systemctl restart rsyslog.service
[root@study ~]# netstat -ltnp &#124; grep syslog
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:514 0.0.0.0:* LISTEN 2145/rsyslogd
tcp6 0 0 :::514 :::* LISTEN 2145/rsyslogd
# 嘿嘿!你的登录文件主机已经设置妥当啰!很简单吧!

client配置:

1
2
3
$ cat /etc/rsyslog.conf
*.* @@192.168.1.100
#*.* @192.168.1.100 # 若用 UDP 传输,设置要变这样!

常见的几个系统日志有哪些呢?一般而言,有下面几个:

  • /var/log/boot.log: 开机的时候系统核心会去侦测与启动硬件,接下来开始各种核心支持的功能启动等。这些流程都会记录在 /var/log/boot.log 里面哩! 不过这个文件只会存在这次开机启动的信息,前次开机的信息并不会被保留下来!
  • /var/log/cron: 还记得第十五章例行性工作调度吧?你的 crontab 调度有没有实际被进行? 进行过程有没有发生错误?你的 /etc/crontab 是否撰写正确?在这个登录文件内查询看看。
  • /var/log/dmesg: 记录系统在开机的时候核心侦测过程所产生的各项信息。由于 CentOS 默认将开机时核心的硬件侦测过程取消显示, 因此额外将数据记录一份在这个文件中;
  • /var/log/lastlog: 可以记录系统上面所有的帐号最近一次登陆系统时的相关信息。第十三章讲到的 lastlog 指令就是利用这个文件的记录信息来显示的。
  • /var/log/maillog 或 /var/log/mail/*: 记录邮件的往来信息,其实主要是记录 postfix (SMTP 协定提供者) 与 dovecot (POP3 协定提供者) 所产生的讯息啦。 SMTP 是发信所使用的通讯协定, POP3 则是收信使用的通讯协定。 postfix 与 dovecot 则分别是两套达成通讯协定的软件。
  • /var/log/messages: 这个文件相当的重要,几乎系统发生的错误讯息 (或者是重要的信息) 都会记录在这个文件中; 如果系统发生莫名的错误时,这个文件是一定要查阅的登录文件之一。
  • /var/log/secure: 基本上,只要牵涉到“需要输入帐号密码”的软件,那么当登陆时 (不管登陆正确或错误) 都会被记录在此文件中。 包括系统的 login 程序、图形接口登陆所使用的 gdm 程序、 su, sudo 等程序、还有网络连线的 ssh, telnet 等程序, 登陆信息都会被记载在这里;
  • /var/log/wtmp, /var/log/faillog: 这两个文件可以记录正确登陆系统者的帐号信息 (wtmp) 与错误登陆时所使用的帐号信息 (faillog) ! 我们在第十章谈到的 last 就是读取 wtmp 来显示的, 这对于追踪一般帐号者的使用行为很有帮助!
  • /var/log/httpd/, /var/log/samba/: 不同的网络服务会使用它们自己的登录文件来记载它们自己产生的各项讯息!上述的目录内则是个别服务所制订的登录文件。

journalctl 常用参数

1
2
3
4
5
6
7
-n or –lines= Show the most recent **n** number of log lines

-f or –follow Like a tail operation for viewing live updates

-S, –since=, -U, –until= Search based on a date. “2019-07-04 13:19:17”, “00:00:00”, “yesterday”, “today”, “tomorrow”, “now” are valid formats. For complete time and date specification, see systemd.time(7)

-u service unit

清理journald日志

journalctl –vacuum-size=1M && journalctl –vacuum-size=500

logrotate

1
2
3
4
5
6
7
8
/var/log/cron
{
sharedscripts
postrotate
/bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true
endscript
}

kill -HUP

Generally services keep the log files opened while they are running. This mean that they do not care if the log files are renamed/moved or deleted they will continue to write to the open file handled.

When logrotate move the files, the services keep writing to the same file.

Example: syslogd will write to /var/log/cron.log. Then logrotate will rename the file to /var/log/cron.log.1, so syslogd will keep writing to the open file /var/log/cron.log.1.

Sending the HUP signal to syslogd will force him to close existing file handle and open new file handle to the original path /var/log/cron.log which will create a new file.

The use of the HUP signal instead of another one is at the discretion of the program. Some services like php-fpm will listen to the USR1 signal to reopen it’s file handle without terminating itself.

不过还得看应用是否屏蔽了 HUP 信号

systemd

sudo systemctl list-unit-files –type=service | grep enabled //列出启动项

journalctl -b -1 //复审前一次启动, -2 复审倒数第 2 次启动. 重演你的系统启动的所有消息

sudo systemd-analyze blame sudo systemd-analyze critical-chain

systemd-analyze critical-chain –fuzz 1h

sudo systemd-analyze blame networkd

systemd-analyze critical-chain network.target local-fs.target

img

参考资料

一模一样的症状,但是根因找错了:rsyslog占用内存高

https://access.redhat.com/solutions/3705051

https://sunsea.im/rsyslogd-systemd-journald-high-memory-solution.html

鸟哥 journald 介绍

journalctl tail and cheatsheet

Journal的由来

TCP传输速度案例分析

TCP传输速度案例分析

前言

TCP传输速度受网络带宽和传输窗口的影响(接收、发送、拥塞窗口),带宽我们没办法改变,以下案例主要是讨论rt、窗口如何影响速度。

详细的buffer、rt对TCP传输速度的影响请看这篇:

就是要你懂TCP–性能和发送接收Buffer的关系:发送窗口大小(Buffer)、接收窗口大小(Buffer)对TCP传输速度的影响,以及怎么观察窗口对传输速度的影响。BDP、RT、带宽对传输速度又是怎么影响的

以及 就是要你懂TCP–最经典的TCP性能问题 Nagle和Delay ack

上面两篇以及下面几个案例读完,应该所有TCP传输速度问题都能解决了。

前后端rtt差异大+vip下载慢的案例

来源:https://mp.weixin.qq.com/s/er8vTKZUcahA6-Pf8DZBng 文章中的trace-cmd工具也不错

如下三个链路,有一个不正常了

image.png

首先通过 ss -it dst “ip:port” 来分析cwnd、ssthresh、buffer,到底是什么导致了传输慢

原因TCPLossProbe:

如果尾包发生了丢包,没有新包可发送触发多余的dup ack来实现快速重传,完全依赖RTO超时来重传,代价太大,那如何能优化解决这种尾丢包的情况。也就是在某些情况下一个可以的重传包就能触发ssthresh减半,从而导致传输速度上不来。

本案例中,因为client到TGW跨了地域,导致rtt增大,但是TGW和STGW之间的rtt很小,导致握手完毕后STGW认为和client的rtt很小,所以很快就触发了丢包重传,实际没有丢包,只是rtt变大了,所以触发了如上的TLP( PTO=max(2rtt, 10ms) , 因为只有一次重传并收到了 dup,还是不应该触发TLP,但是因为老版本kernel bug导致,4.0的kernel修复了这个问题, 函数 is_tlp_dupack)

握手完毕后第七号包很快重传了

image.png

观察:

netstat -s |grep TCPLossProbes

解决:

tcp_early_retrans可用于开启和关闭ER和TLP,默认是3(enable TLP and delayed ER),sysctl -w net.ipv4.tcp_early_retrans=2 关掉TLP

小结

kernel版本小于4.0+TLP开启+VIP代理导致RS认为rtt很小,实际比较大,这两个条件下就会出现如上问题。

这个问题一看就是跟client和VIP代理之间的rtt扩大有关系,不过不是因为扩大后发送窗口不够之类导致的。

长肥网络(高rtt)场景下tcp_metrics记录的ssthresh太小导致传输慢的案例

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

tcp_metrics会记录下之前已关闭tcp 连接的状态,包括发送端拥塞窗口和拥塞控制门限,如果之前网络有一段时间比较差或者丢包比较严重,就会导致tcp 的拥塞控制门限ssthresh降低到一个很低的值,这个值在连接结束后会被tcp_metrics cache 住,在新连接建立时,即使网络状况已经恢复,依然会继承 tcp_metrics 中cache 的一个很低的ssthresh 值,在长肥管道情况下,新连接经历短暂的“慢启动”后,随即进入缓慢的拥塞控制阶段, 导致连接速度很难在短时间内上去。而后面的连接,需要很特殊的场景之下才能将ssthresh 再次推到一个比较高的值缓存下来,因此很有很能在接下来的很长一段时间,连接的速度都会处于一个很低的水平

因为 tcp_metrics记录的ssthresh非常小,导致后面新的tcp连接传输数据时很快进入拥塞控制阶段,如果传输的文件不大的话就没有机会将ssthresh撑大。除非传输一个特别大的文件,忍受拥塞控制阶段的慢慢增长,最后tcp_metrics记录下撑大后的ssthresh,整个网络才会恢复正常。

所以关闭 tcp_metrics其实是个不错的选择: net.ipv4.tcp_no_metrics_save = 1

或者清除: sudo ip tcp_metrics flush all

从系统cache中查看 tcp_metrics item

$sudo ip tcp_metrics show | grep  100.118.58.7
100.118.58.7 age 1457674.290sec tw_ts 3195267888/5752641sec ago rtt 1000us rttvar 1000us ssthresh 361 cwnd 40 ----这两个值对传输性能很重要

192.168.1.100 age 1051050.859sec ssthresh 4 cwnd 2 rtt 4805us rttvar 4805us source 192.168.0.174 ---这条记录有问题,缓存的ssthresh 4 cwnd 2都太小,传输速度一定慢 

清除 tcp_metrics, sudo ip tcp_metrics flush all 
关闭 tcp_metrics 功能,net.ipv4.tcp_no_metrics_save = 1
sudo ip tcp_metrics delete 100.118.58.7

每个连接的ssthresh默认是个无穷大的值,但是内核会cache对端ip上次的ssthresh(大部分时候两个ip之间的拥塞窗口大小不会变),这样大概率到达ssthresh之后就基本拥塞了,然后进入cwnd的慢增长阶段。

长肥网络(rt很高、带宽也高)下接收窗口对传输性能的影响

最后通过一个实际碰到的案例,涉及到了接收窗口、发送Buffer以及高延时情况下的性能问题

案例描述:从中国访问美国的服务器下载图片,只能跑到220K,远远没有达到带宽能力,其中中美之间的网络延时时150ms,这个150ms已经不能再优化了。业务结构是:

client ——150ms—–>>>LVS—1ms–>>>美国的统一接入server—–1ms—–>>>nginx

通过下载一个4M的文件大概需要20秒,分别在client和nginx上抓包来分析这个问题(统一接入server没权限上去)

Nginx上抓包

image.png

从这里可以看到Nginx大概在60ms内就将4M的数据都发完了

client上抓包

image.png

从这个图上可以清楚看到大概每传输大概30K数据就有一个150ms的等待平台,这个150ms基本是client到美国的rt。

从我们前面的阐述可以清楚了解到因为rt比较高,统一接入server每发送30K数据后要等150ms才能收到client的ack,然后继续发送,猜是因为上面设置的发送buffer大概是30K。

检查统一接入server的配置,可以看到接入server的配置里面果然有个32K buffer设置

将buffer改大

速度可以到420K,但是还没有跑满带宽:

image.png

image.png

接着看一下client上的抓包

image.png

可以清楚看到 client的接收窗口是64K, 64K*1000/150=426K 这个64K很明显是16位的最大值,应该是TCP握手有一方不支持window scaling factor

那么继续分析一下握手包,syn:

image.png

说明client是支持的,再看 syn+ack:

image.png

可以看到服务端不支持,那就最大只能用到64K。需要修改服务端代理程序,这主要是LVS或者代理的锅。

如果内网之间rt很小这个锅不会爆发,一旦网络慢一点就把问题恶化了

比如这是这个应用的开发人员的反馈:

image.png

长肥网络就像是很长很宽的高速公路,上面可以同时跑很多车,而如果发车能力不够,就容易跑不满高速公路。
在rt很短的时候可以理解为高速公路很短,所以即使发车慢也还好,因为车很快就到了,到了后就又能发新车了。rt很长的话就要求更大的仓库了。

整个这个问题,我最初拿到的问题描述结构是这样的(不要笑程序员连自己的业务结构都描述不清):

client ——150ms—–>>>nginx

实际开发人员也不能完全描述清楚结构,从抓包中慢慢分析反推他们的结构,到最后问题的解决。

这个案例综合了发送窗口(32K)、接收窗口(64K,因为握手LVS不支持window scale)、rt很大将问题暴露出来(跨国网络,rt没法优化)。

nginx buffer 分析参考案例:https://juejin.cn/post/6875223721615818765 nginx上下游收发包速率不一致导致nginx buffer打爆, 关闭nginx proxy_buffering 可解 (作者:挖坑的张师傅)

image.png

应用层发包逻辑影响了BDP不能跑满

来自 dog250: 一行代码解决scp在Internet传输慢的问题(RT高的网络环境)

用scp在长链路上传输文件竟然慢到无法忍受!100~200毫秒往返时延的链路,wget下载文件吞吐可达40MBps,scp却只有9MBps。

这次不是因为buffer导致BDP跑不满,而是也scp业务层有自己流控的逻辑导致发包慢了

SSH允许在一个TCP连接上复用多个channel,需要对每一个channel做流控以保证公平,所以每个channel必须自己做而不是使用TCP的流控,OpenSSH的实现有问题。

delay ack拉高实际rt的案例

这个案例跟速度没有关系,只是解析监控图表上的rt为什么不符合逻辑地偏高了。

如下业务监控图:实际处理时间(逻辑服务时间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

参考资料

SSH Performance

Why when I transfer a file through SFTP, it takes longer than FTP?

mac 路由和DSN相关知识

mac 路由和DSN相关知识

Mac 下上网,尤其是在双网卡一起使用的时候, 一个网卡连内网,一个网卡连外网,经常会碰到ip不通(路由问题,比较好解决)或者dns解析不了问题. 或者是在通过VPN连公司网络会插入一些内网route,导致部分网络访问不了.

即使对Linux下的DNS解析无比熟悉了,但是在Mac下还是花了一些时间来折腾,配置不好路由和DNS是不配使用Mac的,所以记录下。

route

如果ip不通就看路由表, 根据内外网IP增加/删除相应的路由信息,常用命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sudo route -n add 10.176/16 192.168.3.1
sudo route -n add -net 10.176.0.0/16 192.168.3.1 //添加路由, 访问10.176.0.0/16 走192.168.3.1
sudo route -n delete -net 10.176.0.0/16 192.168.3.1
sudo route -n delete 0.0.0.0 192.168.184.1
sudo route -n add 0.0.0.0 192.168.184.1 //添加默认路由访问外网

sudo route -n delete 0.0.0.0 192.168.3.1
sudo route -n add 10.176/16 192.168.3.1
sudo route -n delete 0.0.0.0 192.168.184.1 -ifscope en0
sudo route -n add 0.0.0.0 192.168.184.1
sudo networksetup -setdnsservers 'Apple USB Ethernet Adapter' 202.106.196.115 202.106.0.20 114.114.114.114

sudo networksetup -setdnsservers 'USB 10/100/1000 LAN' 223.5.5.5 30.30.30.30 114.114.114.114

ip route get 8.8.8.8 //linux
route get 8.8.8.8 //macOS
netstat -rn //查看路由
netstat -nr -f inet //只看ipv4相关路由

如果本来IP能通,连上VPN后就通不了,那一定是VPN加入了一些更精细的路由导致原来的路由不通了,那么很简单停掉VPN就能恢复或者增加一条更精确的路有记录进去,或者删掉VPN增加的某条路由.

DNS 解析

mac下DNS解析问题搞起来比较费劲,相应的资料也不多, 经过上面的操作后如果IP能通,域名解析有问题,一般都是DNS解析出了问题

mac下 /etc/resolv.conf 不再用来解析域名, 只有nslookup能用到resolv.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
cat /etc/resolv.conf                                                
#
# macOS Notice
#
# This file is not consulted for DNS hostname resolution, address
# resolution, or the DNS query routing mechanism used by most
# processes on this system.
#
# To view the DNS configuration used by this system, use:
# scutil --dns

scutil --dns //查看DNS 解析器
scutil --nwi //查看网络

解析出了问题先检查nameserver

scutil –dns 一般会展示一大堆的resolver, 每个resolver又可以有多个nameserver

A scoped DNS query can use only specified network interfaces (e.g. Ethernet or WiFi), while non-scoped can use any available interface.

More verbosely, an application that wants to resolve a name, sends a request (either scoped or non-scoped) to a resolver (usually a DNS client application), if the resolver does not have the answer cached, it sends a DNS query to a particular nameserver (and this goes through one interface, so it is always “scoped”).

In your example resolver #1 “for scoped queries” can use only en0 interface (Ethernet).

修改 nameserver

默认用第一个resolver, 如果第一个resolver没有nameserver那么域名没法解析, 可以修改dns resolver的nameserver:

1
2
3
4
5
6
7
8
9
$networksetup -listallnetworkservices  //列出网卡service, 比如 wifi ,以下是我的 macOS 输出
An asterisk (*) denotes that a network service is disabled.
USB 10/100/1000 LAN
Apple USB Ethernet Adapter
Wi-Fi
Bluetooth PAN
Thunderbolt Bridge
$sudo networksetup -setdnsservers 'Wi-Fi' 202.106.196.115 202.106.0.20 114.114.114.114 //修改nameserver
$networksetup -getdnsservers Wi-Fi //查看对应的nameserver, 跟 scutil --dns 类似

如上, 只要是你的nameserver工作正常那么DNS就肯定回复了

删掉所有DNS nameserver:

One note to anyone wanting to remove the DNS, just write “empty” (without the quotes) instead of the DNS: sudo networksetup -setdnsservers <networkservice> empty

networksetup用法

查看设备和配置

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
$networksetup -listallnetworkservices
An asterisk (*) denotes that a network service is disabled.
USB 10/100/1000 LAN
Apple USB Ethernet Adapter
Wi-Fi
Bluetooth PAN
Thunderbolt Bridge
Thunderbolt Bridge 2

#查看网卡配置
$networksetup -getinfo "USB 10/100/1000 LAN"
DHCP Configuration
IP address: 30.25.25.195
Subnet mask: 255.255.255.128
Router: 30.25.25.254
Client ID:
IPv6 IP address: none
IPv6 Router: none
Ethernet Address: 44:67:52:02:16:d4

$networksetup -listallhardwareports
Hardware Port: USB 10/100/1000 LAN
Device: en7
Ethernet Address: 44:67:52:02:16:d4

Hardware Port: Wi-Fi
Device: en0
Ethernet Address: 88:66:5a:10:e4:2b

Hardware Port: Thunderbolt Bridge
Device: bridge0
Ethernet Address: 82:0a:d5:01:b4:00

VLAN Configurations
===================
$networksetup -getinfo "Thunderbolt Bridge"
DHCP Configuration
Client ID:
IPv6: Automatic
IPv6 IP address: none
IPv6 Router: none

//查看wifi和热点
networksetup -listpreferredwirelessnetworks en0
networksetup -getairportnetwork "en0"

dhcp、route、domain配置

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
[-setmanual networkservice ip subnet router]

[-setdhcp networkservice [clientid]]

[-setbootp networkservice]

[-setmanualwithdhcprouter networkservice ip]

[-getadditionalroutes networkservice]

[-setadditionalroutes networkservice [dest1 mask1 gate1] [dest2 mask2 gate2] ..

. [destN maskN gateN]]

#给网卡配置ip、网关
$ networksetup -getinfo "Apple USB Ethernet Adapter" DHCP Configuration
Client ID:
IPv6: Automatic
IPv6 IP address: none
IPv6 Router: none
Ethernet Address: (null)
$networksetup -setmanual "Apple USB Ethernet Adapter" 192.168.100.100 255.255.255.0 192.168.100.1
$networksetup -getinfo "Apple USB Ethernet Adapter"
Manual Configuration
IP address: 192.168.100.100
Subnet mask: 255.255.255.0
Router: 192.168.100.1
IPv6: Automatic
IPv6 IP address: none
IPv6 Router: none
Ethernet Address: (null)

代理配置

1
2
3
4
5
6
//ftp
[-getftpproxy networkservice]

[-setftpproxy networkservice domain portnumber authenticated username password]

[-setftpproxystate networkservice on | off]

网页

1
2
3
4
5
6
[-getwebproxy networkservice]
[-setwebproxy networkservice domain portnumber authenticated username password]
[-setwebproxystate networkservice on | off]

$networksetup -setwebproxy "Built-in Ethernet" proxy.company.com 80
$networksetup -setwebproxy "Built-In Ethernet" proxy.company.com 80 On authusername authpassword

Socks5 代理

1
2
3
4
5
6
$networksetup -setsocksfirewallproxy "USB 10/100/1000 LAN" 127.0.0.1 13659
$networksetup -getsocksfirewallproxy "USB 10/100/1000 LAN"
Enabled: Yes
Server: 127.0.0.1
Port: 13659
Authenticated Proxy Enabled: 0

总结

mac同时连wifi(外网或者vpn)和有线(内网), 如果内网干扰了访问外部ip, 就检查路由表,调整顺序. 如果内网干扰了dns,可以通过scutil –dns查看dns顺序到系统配置里去掉不必要的resolver

参考资料

macOS的networksetup命令来管理网络

在Mac下使用脚本重载proxy自动配置脚本(pac)

网络硬件相关知识

网络硬件相关知识

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

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

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

image.png

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

光纤接口:

image.png

单模光纤和多模光纤

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

多模光模块工作波长为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

多网卡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 设备种类。有效值为 nonepackagecache、或 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).

网卡命名

默认安装网卡所在位置来命名(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分钱建站速度秒杀三大门户网站站

如何用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/

一个有意思的问题

一个有意思的问题

问题描述

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连接

到底一台服务器上最多能创建多少个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核心为例,500050/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

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

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

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

场景描述

某客户通过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连接互串了

活久见,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针对秒杀场景的优化

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)

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

缩短锁时间

接下来的问题在于一个事务中有多条语句(最少也有一个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导致性能不高

业务优化

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

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

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

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 CacheBuffer 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 CacheBuffer 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