上一篇文章讲了高性能编程的工具,这一篇我们基于前面的一些知识点和工具来聊一下Linux下的性能优化(本知识点分为两篇,当前主要介绍CPU和内存性能优化)。
第一部分:CPU和内存性能度量
系统调用
这张图阐述一个应用程序需要经过这些模块调用,对于性能每一部分都可能会有影响,那么我们先需要了解每个模块需要怎么度量?
1、CPU度量
(1)CPU使用率
CPU使用率是最直观描述当前服务状态的情况,如果CPU使用率过高,则表示当前遇到了性能瓶颈,其中过高的这个具体值在线上一般是70%-90%之间,要么扩容服务,要么就排查性能问题。
查看性能工具有很多,最常用的是通过top -p 或者通过查看线程top -H -p 观察,另外可以使用上一篇的工具:mpstat -P ALL 1 2。
(2)用户进程消耗CPU
用户进程消耗CPU是常见的情况,往往和业务代码或者使用的库相关,比如大量的循环,JSON解析大包等,在用户代码层有很多耗CPU的操作,都会表现CPU使用率异常,定位其问题可以通过以下方式:
- 先通过ps或者top查询具体进程或者线程CPU消耗过高,然后查询pidstat -p 判断%usr %system %guest占比情况,判断是否为用户态消耗
- 由于用户态涉及用户代码,可以通过perf top查看具体调用函数或者查看查看日志分析;
(3)内核消耗CPU
消耗CPU不止用户进程,还包括内核进程,系统调用等内核消耗CPU,可能的原因有大量的内存拷贝,锁,大量的上下文切换等等,具体分析和上面类似:
- 先通过ps或者top查询具体进程或者线程CPU消耗过高,然后查询pidstat -p 判断%usr %system %guest占比情况,判断是否为内核态消耗;
- 然后可以通过perf top或者strace查看系统调用情况,或者通过mpstat分析,总结中断或者上下文切换频率来判断;
(4)CPU等待
CPU花费在等待上的时间,主要是看是否大量的IO导致,也可以通过top定位具体进程,然后跟踪和分析该进程或者线程的网络调用情况。
(5)Nice消耗CPU
描述的是花费的re-nicing进程上时间占比,主要是更改了进程的执行顺序或者优先级。
(6)平均负载
平均负载是一个判断系统快慢的重要原因,可能往往不是某个进程引起的,主要有两个指标:
- 队列中等待处理的进程数(TASK_RUNNING状态进程)
- 等待不可中断任务被完成的进程数(TASK_UNINTERRUPTIBLE状态进程)
如果被阻塞,平均负载就会增加,可以通过uptime查看,往往负载增加这个时候需要优化代码或者增加机器资源。
(7)运行进程
当前运行和已经在队列中的进程数,往往进程过多会导致CPU调度繁忙,比如之前多进程的Apache Server,所以可以根据当前CPU的核数决定进程个数,一般繁忙情况下的进程不建议超过2倍CPU(当前空闲的进程也不宜过大,建议不超过10倍)。
(8)阻塞进程
阻塞进程是当前未达到执行条件的进程,和上面的CPU等待事件对应,一般是IO问题导致,比如写文件数据过慢,或者socket读写数据未到达等等情况,如何分析呢?可以通过strace跟踪系统调用分析。
(9)上下文切换
在系统上发生上下文切换的情况,也是判断CPU负载的重要因素,大量的上下文切换可能和大量中断或者锁相关,上下文切换会导致CPU的缓存被刷新,数据需要从内存换入换出等。
排查方案是通过perf或者vmstat工具查询,比如vmstat输出(也可以通过vmstat -s查看):
[root@VM-16-16-centos ~]# vmstat 2 2
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 298404 96824 1189732 0 0 1 34 1 0 0 0 99 0 0
0 0 0 298284 96824 1189736 0 0 0 214 760 1315 1 0 99 1 0
其中system包括:CPU在内核态运行信息,包括in中断次数,cs上下文切换次数。
(10)中断
中断包含硬中断和软中断,硬中断是外设处理过程中产生的,通过硬件控制器通知cpu的状态变化,而软中断是通过模拟硬中断的一种信号处理方式,中断过多会导致CPU花费一些时间相应中断,这里也会影响性能,如何排查?通过命令行mpstat -P ALL 5 2可以查看:
[root@VM-16-16-centos ~]# mpstat -P ALL 5 2
Linux 4.18.0-348.7.1.el8_5.x86_64 (VM-16-16-centos) 2023年08月19日 _x86_64_ (2 CPU)
10时02分15秒 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
10时02分20秒 all 0.70 0.00 0.80 0.50 0.00 0.00 0.00 0.00 0.00 98.00
10时02分20秒 0 0.60 0.00 0.80 0.20 0.00 0.00 0.00 0.00 0.00 98.40
10时02分20秒 1 0.80 0.00 0.80 0.80 0.00 0.00 0.00 0.00 0.00 97.60
其中输出中包含的:
- %irq:CPU处理硬中断的时间占比
- %soft:CPU处理软中断的时间占比
2、内存度量
(1)空闲内存
通过free我们能看到当前内存情况:
[root@VM-0-11-centos ~]# free
total used free shared buff/cache available
Mem: 3880192 407228 713024 872 2759940 3182872
Swap: 0 0 0
- total:物理内存总量
- used:已经使用的物理内存量
- free:尚未使用的物理内存量
- shared:被共享使用的物理内存量
- buff:被缓存的物理内存量
- cache:被缓存的硬盘文件的物理内存量
- available:剩余可用的物理内存量,包括free + buff + cache - 系统预留的缓冲区
- Swap total:交换空间总量
- Swap used:已经使用的交换空间量
- Swap free:尚未使用的交换空间量
从上面可以看出,free的内存越大越好,这样有剩余足够多的物理内存可以使用。
(2)Swap
Swap如上面说的是交换空间的内存数据,是linux为了释放一部分物理内存将数据临时保存在Swap空间中,通过vmstat -s查看具体信息如下:
[root@VM-16-16-centos ~]# vmstat -s
1860492 K total memory
274936 K used memory
701576 K active memory
707432 K inactive memory
299040 K free memory
96824 K buffer memory
1189692 K swap cache
0 K total swap
0 K used swap
0 K free swap
12318019 non-nice user cpu ticks
124590 nice user cpu ticks
11848347 system cpu ticks
2844992141 idle cpu ticks
4677889 IO-wait cpu ticks
0 IRQ cpu ticks
208152 softirq cpu ticks
0 stolen cpu ticks
15879112 pages paged in
985253486 pages paged out
0 pages swapped in
0 pages swapped out
1330511648 interrupts
260667271 CPU context switches
1678004734 boot time
58996940 forks
其中如果pages swapped in和pages swapped out每秒增长很多大,表示内存上遇到了瓶颈,需要升级机器的内存或者优化代码。
(3)Slab
在Linux中,伙伴系统是以页为单位管理和分配内存,但是现实的需求却以字节为单位,假如我们需要申请20Bytes,总不能分配一页吧?那岂不是严重浪费内存。那么该如何分配呢?Slab分配器就应运而生了,专为小内存分配而生,Slab分配器分配内存以Byte为单位,但是Slab分配器并没有脱离伙伴系统,而是基于伙伴系统分配的大内存进一步细分成小内存分配,其作用如下:
- 节省空间,减少内存碎片化,Slab对小对象进行分配,不用为每个小对象分配一页
- 提高系统效率:当对象拥有者释放一个对象后,SLAB的处理是仅仅标记对象为空闲,并不做多少处理,而又有申请者申请相应大小的对象时,Slab会优先分配最近释放的对象
如果要排查Slab的详细信息,可以通过slabtop或者cat /proc/slabinfo,输出如下(执行slabtop):
Active / Total Objects (% used) : 1074142 / 1101790 (97.5%)
Active / Total Slabs (% used) : 39843 / 39843 (100.0%)
Active / Total Caches (% used) : 100 / 130 (76.9%)
Active / Total Size (% used) : 250498.05K / 253182.16K (98.9%)
Minimum / Average / Maximum Object : 0.01K / 0.23K / 8.00K
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
445302 445302 100% 0.10K 11418 39 45672K buffer_head
249102 249071 99% 0.19K 11862 21 47448K dentry
83616 83557 99% 1.00K 5226 16 83616K ext4_inode_cache
63240 40754 64% 0.04K 620 102 2480K ext4_extent_status
54376 54297 99% 0.57K 3884 14 31072K radix_tree_node
29547 29487 99% 0.19K 1407 21 5628K kmalloc-192
28544 28488 99% 0.06K 446 64 1784K kmalloc-64
21624 21624 100% 0.12K 636 34 2544K kernfs_node_cache
20400 20400 100% 0.05K 240 85 960K shared_policy_node
16276 15989 98% 0.58K 1252 13 10016K inode_cache
10914 10914 100% 0.04K 107 102 428K selinux_inode_security
7776 7776 100% 0.21K 432 18 1728K vm_area_struct
7232 3921 54% 0.12K 226 32 904K kmalloc-128
5376 5376 100% 0.02K 21 256 84K kmalloc-16
5376 5376 100% 0.03K 42 128 168K kmalloc-32
5120 5120 100% 0.01K 10 512 40K kmalloc-8
4344 4306 99% 0.66K 362 12 2896K proc_inode_cache
4096 4096 100% 0.03K 32 128 128K jbd2_revoke_record_s
3822 3822 100% 0.09K 91 42 364K kmalloc-96
3417 3217 94% 0.08K 67 51 268K anon_vma
3344 3344 100% 0.25K 209 16 836K kmalloc-256
3136 3136 100% 0.06K 49 64 196K ext4_free_data
2190 2190 100% 0.05K 30 73 120K avc_xperms_node
2112 2112 100% 1.00K 132 16 2112K kmalloc-1024
- OBJS:由于Slab是按照object管理的,这里是对象数量
- ACTIVE:当前活跃的objects数量
- USE:缓存的利用率
- OBJ SIZE:object的size的大小
- SLABS:Slab的个数
- OBJ/SLAB:每个Slab中object个数
- CACHE SIZE:缓存大小,这里是不精确值,可以忽略
- NAME:分配Slab的名字
我们可以从以上的信息中判断那些内核模块内存分配较多(比如OBJ SIZE过大),进而分析模块的性能瓶颈。
3、方法论
以下是我参照USE方法论整理排查性能度量指标流程,其中最大挑战点在于如何发现子模块中的问题并且分析问题?后续可以单独写一篇分析。
方法论
第二部分:系统层优化
1、CPU
(1)缓存
#define N 2048
long timecost(clock_t t1, clock_t t2)
{
long elapsed = ((double)t2 - t1) / CLOCKS_PER_SEC * 1000;
return elapsed;
}
int main(int argc, char **argv)
{
char arr[N][N];
{
clock_t start, end;
start = clock();
for (int i = 0; i < N; i++)
{
for (int j = 0; j < N; j++)
{
arr[i][j] = 0;
}
}
end = clock();
cout