必须了解的Linux性能基础知识
前言
作为一名测试人员,难免需要维护业务线的测试环境。初时每当研发通知某某机器又挂了、某某机器太卡了,笔者完全两眼一抹黑,无从入手。而当了解了 linux 基本性能,熟悉其基本指标时,遇到问题往往心里有底,甚至得心应手。送人玫瑰,手有余香,特此整理,与君共享。 性能指标CPU 使用率
提起性能指标,最容易想到的是CPU使用率。linux 作为一个多任务系统,将每个 CPU 的时间划分为很短的时间片,通过调度器轮流分配给各个任务使用,造成多任务同时运行的错觉。而根据 CPU 上执行任务的不同,又可以细分为用户CPU、系统CPU、等待 I/O CPU、软中断CPU 和硬中断CPU 等。 用户CPU使用率(us + ni),包括用户态CPU使用率(user)和低优先级用户态CPU使用率(nice),表示 CPU 在用户状态运行的时间百分比。用户CPU使用率高,说明有应用程序比较繁忙。 系统CPU使用率(sy),表示 CPU 在内核状态运行的时间百分比。系统CPU使用率高,说明内核比较繁忙。 等待 I/O 使用率(wa),也称为 iowait,表示等待 I/O 的时间百分比。iowait 高,说明系统与硬件设备的 I/O 交互时间比较长。 软中断(si)和硬中断(hi)的 CPU使用率,分别表示内核调用软中断处理程序、硬中断处理程序的时间百分比。它们的使用率高,通常说明系统发生了大量的中断。 CPU使用率如何计算
CPU使用率描述了非空闲时间占总 CPU 时间的百分比。用公式表示为:
可以通过以下命令查看各个 CPU 的数据: $ cat /proc/stat | grep ^cpu # cpu us ni sys id wa hi si st guest gnice cpu 42849699018 205001 4396958253 10689835442 115714471 51747 397324892 0 0 0 cpu0 1448897830 9918 260556249 620723421 17589437 11620 80282872 0 0 0 cpu1 1802849978 8539 182700339 444025341 2238152 1 5138950 0 0 0 cpu2 1846754519 9857 177641895 405553744 1799005 0 4945465 0 0 0 cpu3 1854741192 10151 175660372 399834166 1748613 0 4771745 0 0 0 ...
这里输出的是一个表格,第一列为 CPU 的编号,如cpu0,而第一行没有 CPU 编号,表示所有 CPU 的累加。其他列表示不同任务下 CPU 的时间,每一列的顺序可以通过 man proc 命令查看。通过这些数据就可以计算出当前机器的 CPU使用率。然而,得到的 CPU 使用率是开机以来的平均CPU使用率,一般不具有参考价值。性能工具一般会取间隔一段时间的两次差值,计算这段时间内的平均CPU使用率,即:
如何查看CPU使用率
top 和 ps 是常用的性能分析工具。top 能够显示系统总体的CPU和内存使用情况,以及各进程的资源使用情况。ps 显示各进程的资源使用情况。例如,top 的输出格式为:$ top top - 21:23:36 up 283 days, 10:41, 2 users, load average: 21.96, 14.46, 13.76 Tasks: 2009 total, 10 running, 1994 sleeping, 0 stopped, 5 zombie Cpu(s): 32.5%us, 7.6%sy, 0.0%ni, 58.3%id, 0.6%wa, 0.0%hi, 0.9%si, 0.0%st Mem: 98979248k total, 95372168k used, 3607080k free, 1479704k buffers Swap: 0k total, 0k used, 0k free, 59440700k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 13159 user 20 0 9103m 6.0g 13m S 168 6.4 156503:16 mongod 31963 root 20 0 929m 882m 672 R 52 0.9 0:01.61 supervisord ...
其中,第三行 CPU 就是该机器的 CPU使用率, top 默认每 3 秒刷新一次。默认情况下显示的是所有 CPU 的平均值,如果想了解每个 CPU 的使用情况,只需按下数字 1 即可。例如:Cpu0 : 17.0%us, 10.2%sy, 0.0%ni, 64.9%id, 2.3%wa, 0.0%hi, 5.6%si, 0.0%st Cpu1 : 12.1%us, 5.5%sy, 0.0%ni, 82.1%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu2 : 24.5%us, 4.0%sy, 0.0%ni, 71.2%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st ...
从上我们可以发现, top 并没有细分进程的用户态CPU 和内核态CPU,如果想了解每个进程的详细情况,可以使用 pidstat 工具。例如:$ pidstat 1 1 Linux 3.2.0-23-generic (cs) 06/24/2021 _x86_64_ (24 CPU) 09:32:41 PM PID %usr %system %guest %CPU CPU Command 09:32:42 PM 1522 0.88 0.88 0.00 1.77 20 beam.smp ... 分析
一般来说,每种类型的 CPU使用率高,都有不同导致的条件,下面列出了这几种导致不同 CPU使用率高的情况以及着重排查点: 用户 CPU 和 Nice CPU 高,说明用户态进程占用了较多的 CPU,所以应该着重排查进程的性能问题。 系统 CPU 高,说明内核态占用了较多的 CPU,所以应该着重排查内核线程或者系统调用的性能问题。 I/O 等待 CPU 高,说明等待 I/O 的时间比较长,所以应该着重排查系统存储是不是出现了 I/O 问题。 软中断和硬中断高,说明软中断或硬中断的处理程序占用了较多的 CPU,所以应该着重排查内核中的中断服务程序。 平均负载
第二能想到的就是平均负载。很多人会认为,平均负载不就是单位时间内的 CPU使用率吗?其实并非如此。下面我们来一起熟悉下什么是平均负载。
通俗地说,平均负载是指单位时间内系统处于可运行状态和不可中断状态的平均进程数,即平均活跃进程数。从定义我们可以解读到其实跟 CPU使用率无直接关系。 linux 将进程的状态划分为以下几种:
R 是 Running 的缩写,表示进程正在运行或者正在等待运行。
D 是 Disk Sleep 的缩写,为不可中断睡眠状态,进程正与硬件进行交互,交互过程不允许被其他进程打断,以防止数据丢失。
Z 是 Zombie 的缩写,表示僵尸进程,进程实际已经结束,但是父进程还没回收其资源,就会出现该状态。
S 是 Interruptible Sleep 的缩写,称为可中断睡眠状态,表示进程因等待某个事件而被系统挂起。
I 是 Idle 的缩写,为空闲状态,用于不可中断睡眠的内核线程上。
T 是 Traced 的缩写,表示进程处于暂停状态。
X 是 Dead 的缩写,表示进程已经消亡。 如何查看
通常使用 uptime 或 top 命令查看。例如,uptime 的输出格式为:$ uptime 15:22:50 up 284 days, 4:40, 3 users, load average: 10.06, 12.15, 12.33
输出分别表示为当前时间、系统运行时间、正在登录用户数以及过去 1 分钟,5 分钟,15 分钟的平均负载。输出这三个时间的平均负载有什么含义呢? 数值基本相同,说明系统负载趋于平稳。 数值从过去 15 分钟到过去 1 分钟逐渐升高,说明系统运行任务在增多,需要我们去观察和判断这种升高是否合理。 数值从过去 15 分钟到过去 1 分钟在逐渐降低,说明系统运行任务在减少。
基于此就可以粗略判断当前的系统负载情况。上面的例子可以看到目前的系统负载下降的。 多少为合理
平均负载这一数据没有针对 CPU 个数作归一化处理,因此最理想情况是平均负载等于 CPU 的个数。在评判当前系统是否过载时,首先需要知道当前系统的 CPU 个数,可以通过 top 或者 lscpu 查看。例如:$ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 24 ...
当知道了 CPU 个数后,就可以结合平均负载来判断当前系统的运行情况了。一旦负载过高,会导致进程响应变慢,影响服务的正常功能。故当出现负载有明显升高趋势时,就需要我们去分析和调查了。通常认为平均负载低于 CPU 数量 70% 时为合理状态,但不是绝对的,还需要参考负载的变化趋势来作判断。 CPU上下文切换
前面描述了 linux 系统会在很短的时间内把 CPU 轮流分配给任务(进程),造成多个任务同时运行的错觉。而这些任务都是从哪里加载以及开始运行的呢?这就引出了 CPU上下文。CPU寄存器和程序计数器被叫做 CPU 的上下文,那么,CPU寄存器和程序计数器分别有什么作用呢? CPU寄存器,存放数据的小型存储区域,用来暂时存放参与运算的数据和运算结果。 程序计数器,用来存储 CPU 下一条指令位置。
而 CPU上下文切换,就是把前一个任务的 CPU上下文保存起来,然后加载新任务的上下文到寄存器和程序计数器中,最后跳转到程序计数器所指位置,执行新任务。根据任务的不同,可以分为三种切换场景:进程上下文切换、线程上下文切换以及中断上下文切换。 进程上下文切换
提到进程上下文切换,就不得不提到系统调用。linux 按照特权等级,把进程的运行空间划分为内核空间和用户空间。 内核空间,具有最高权限,可以访问所有资源。 用户空间,访问受限,不能直接访问内存等硬件设备,只能通过系统调用到内核中,由内核访问。
系统调用和普通函数调用非常相似,只不过,系统调用由操作系统核心提供,运行于内核态,而普通函数调用由函数库或用户提供,运行于用户态。比如,某一进程需要查看文件内容时,就需要系统调用来完成:首先调用 open() 打开文件,通过 read() 读取文件内容,并通过 write() 写到到标准输出,最后通过 close() 关闭文件。系统调用结束后,CPU寄存器恢复到原来保存的用户态,切换到用户空间,继续执行进程。
从上我们可以知道,系统调用是在进程内进行的,其与进程上下文切换有点不同,它不会涉及到虚拟内存等用户态资源。所以我们通常称系统调用为一种特权模式切换,而在系统调用用上下文切换是不可避免的,一次系统调用发生了两次上下文切换。
进程是由内核管理和调度的,进程的上下文切换在内核态进行,故其上下文切换要比系统调用多了一步:在保存当前进程的内核状态和 CPU寄存器之前,需要先把该进程的虚拟内存、栈等用户资源保存起来。
那么,什么时候需要进行上下文切换呢? 进程的系统资源不足时,进程就会被挂起,等到资源充足时才能进行。 进程通过 sleep 睡眠函数等方法主动挂起时。当有更高优先级的进程运行时,例如中断等,进程会被挂起,来保证高优先级进程正常运行。 线程上下文切换
我们知道,线程是调度的基本单位,而进程是资源拥有的基本单位,即内核中的任务调度实际对象是线程,进程只是给线程提供了虚拟内存等资源。如果进程中有多个线程,则线程会共享虚拟内存等资源。因此,对于线程上下文切换就有两种情况: 前后切换线程属于同一进程,则虚拟内存等资源就不需要切换,只需要切换线程需要的私有资源。 前后切换线程属于两个进程,此时跟进程上下文切换一致。
由此我们知道同一进程的线程上下文切换,要比进程间的上下文切换耗费更少资源,这也是多线程代替多进程的一大优势。 中断上下文切换
中断要比进程的优先级更高,其会打断进程的正常调度,因此中断程序需要更短小精悍,以便尽可能快地结束。
中断上下文切换与进程不同,其不需要虚拟内存、全局变量等用户态资源,只需要如 CPU寄存器、硬件中断参数等内核态中断服务程序所需要的状态。 如何查看
查看系统总体的上下文切换情况可以通过 vmstat 命令,其输出格式为:$ vmstat 1 1 procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- r b swpd free buff cache si so bi bo in cs us sy id wa 10 0 0 1653888 1477852 61630304 0 0 0 112 0 0 73 8 19 0
其中,需要关注以下几个参数: r (running),正在运行和等待运行的进程数。 b (blocked),不可中断睡眠状态的进程数。 in (interrupt),每秒中断次数。 cs (context switch), 每秒的上下文切换次数。
而如果我们想要查看进程的上下文切换,可以通过 pidstat 查看,输出格式为:# 通过参数 -w 可以查看进程每秒的上下文切换次数 $ pidstat -w 1 1 Linux 3.2.0-23-generic (cs) 06/26/2021 _x86_64_ (24 CPU) 03:19:25 PM PID cswch/s nvcswch/s Command 03:19:26 PM 1 0.85 0.00 init 03:19:26 PM 3 0.85 0.00 ksoftirqd/0 ...
其中,需要关注两个参数: cswch (voluntary context switch),每秒自愿上下文切换次数。当进程无法获取资源,比如内存、IO 等资源不足时,就会发生自愿上下文切换。 nvcswch (non voluntary context switch),每秒非自愿上下文切换次数。当进程的时间片已到,比如大量进程争抢 CPU 时,就会被系统强制调度,发生非自愿上下文切换。
如果我们想要查看线程间的上下文切换,还是可以通过 pidstat 查看,加上 -wt 参数选项,输出格式为:$ pidstat -wt 1 1 linux 3.2.0-23-generic (cs7) 06/26/2021 _x86_64_ (24 CPU) 03:34:07 PM TGID TID cswch/s nvcswch/s Command 03:34:07 PM 2200 - 0.64 0.00 flush-8:48 03:34:07 PM - 2200 0.64 0.00 |__flush-8:48 03:34:07 PM - 2524 136.54 0.00 |__influxd 03:34:07 PM - 2537 26.92 0.00 |__influxd 03:34:07 PM - 3701 12.82 3.21 |__1_scheduler 03:34:07 PM - 3702 0.64 0.00 |__2_scheduler 03:34:07 PM - 3725 1.28 0.00 |__aux ... 多少为合理
据统计,每次上下文切换需要耗费几十纳秒到数微秒的CPU时间,这个时间还是可以接受的。但是过多的上下文切换,会把 CPU 时间都耗费在寄存器、内存栈以及虚拟内存的保存和恢复上,缩短进程真正运行的时间,导致系统性能大幅下降。那么,我们肯定想了解,多少的 CPU上下文切换为合理情况呢?
这取决于 CPU 的性能。如果上下文切换稳定,那么从数百到上万都是合理的。但是当 CPU上下文切换呈数量级增大时,或者数值已超过上万次且不断增长时,就可能出现了性能问题,需要我们着重排查。 CPU缓存命中率
CPU缓存是为了协调CPU处理速度和内存访问速度之间的差距而出现的空间,可分为 L1、L2 和 L3 三级缓存。离 CPU 核心越近,缓存的读写速度就越快,因此三级缓存的读写速度快慢分别是: L1 > L2 > L3 .
如果 CPU 所操作的数据在缓存中,则可以直接读取,称为缓存命中。命中缓存可以带来很大的性能提升。因此,考虑 CPU 的缓存命中率对于 linux 性能也至关重要。
查看系统的内存情况,可通过 top 或 free 命令。比如,free 命令输出格式为:$ free total used free shared buffers cached Mem: 98979248 97562252 1416996 0 1478000 62018692 Swap: 0 0 0
输出分别是物理内存 Mem 和交换分区 Swap 的使用情况。其中,每一列分别是: total ,总内存大小。 used,已使用内存的大小。 free,未使用的内存大小。 shared,共享内存的大小。 buffers/cached,缓存和缓冲区的大小
查看 buffers 和 cache 的变化情况,可通过 vmstat 命令,输出格式:$ vmstat 1 1 procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- r b swpd free buff cache si so bi bo in cs us sy id wa 9 0 0 1571640 1478044 62034792 0 0 0 112 0 0 73 8 19 0 buffer 和 cache 就是上面所说的缓存和缓冲区的大小,单位为 KB 。bi 和 bo 分别表示块设备读取和写入的大小,单位为块 / 秒。linux 中块的大小是 1KB ,故单位等价于 KB/s 。实践
下面就目前机器出现的系统使用率过高的情况进行排查。
通过 top 可以查看当前的系统性能情况:$ top top - 16:41:29 up 285 days, 5:59, 2 users, load average: 9.82, 11.53, 12.92 Tasks: 2010 total, 9 running, 2000 sleeping, 0 stopped, 1 zombie Cpu(s): 34.5%us, 6.5%sy, 0.0%ni, 57.6%id, 0.5%wa, 0.0%hi, 0.9%si, 0.0%st Mem: 98979248k total, 97548032k used, 1431216k free, 1478084k buffers Swap: 0k total, 0k used, 0k free, 62122812k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 13159 user 20 0 9128m 6.1g 13m S 90 6.5 160516:12 mongod 19638 root 20 0 929m 882m 520 R 58 0.9 0:01.81 supervisord ...
CPU用户使用率为 34.5%,系统使用率为 6.5%,空闲时间占 57.6%,系统使用流畅。可以看出,当前的系统是比较空闲的。
然后,我执行某些操作。再通过 top 命令查看当前的系统性能情况:$ top Tasks: 2020 total, 16 running, 2004 sleeping, 0 stopped, 0 zombie Cpu(s): 64.5%us, 13.6%sy, 0.0%ni, 20.5%id, 0.2%wa, 0.0%hi, 1.1%si, 0.0%st Mem: 98979248k total, 98003524k used, 975724k free, 1478128k buffers Swap: 0k total, 0k used, 0k free, 62228740k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 13159 user 20 0 9128m 6.2g 13m S 249 6.5 160538:08 mongod 15108 root 20 0 929m 882m 620 R 73 0.9 0:02.24 supervisord 15121 root 20 0 929m 882m 620 R 66 0.9 0:02.03 supervisord 15122 root 20 0 929m 882m 620 R 66 0.9 0:02.03 supervisord ...
可以发现,系统使用率已经增至了原来的一倍。CPU使用率除了服务 mongod 最高以外,有几个 supervisord 启动的服务也出现了CPU使用率增高的情况。通过 pidstat 命令分析这些进程的系统资源占用情况:$ pidstat -p 15108 Linux 3.2.0-23-generic (cs) 06/26/2021 _x86_64_ (24 CPU) 05:03:14 PM PID %usr %system %guest %CPU CPU Command
发现该进程并没有输出任何的资源使用信息。是 pidstat 指令出故障了?可以通过 ps 指令来交叉确认一下:$ ps aux | grep 15108 2003 14211 0.0 0.0 8748 948 pts/2 S+ 17:07 0:00 grep --color=auto 15108
确实没有输出。现在发现问题了,原来是进程已经不存在了,所以 pidstat 没有任何输出。继续分析其他几个高 CPU使用率的进程,发现亦如此。既然进程没有了,那么性能问题应该已经没有了吧。通过 top 命令确认下:$ top Tasks: 2019 total, 18 running, 2001 sleeping, 0 stopped, 0 zombie Cpu(s): 60.5%us, 13.4%sy, 0.0%ni, 24.7%id, 0.4%wa, 0.0%hi, 1.1%si, 0.0%st Mem: 98979248k total, 98118260k used, 860988k free, 1478272k buffers Swap: 0k total, 0k used, 0k free, 62353676k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 13159 user 20 0 9129m 6.2g 13m S 260 6.5 160559:54 mongod 27114 root 20 0 929m 882m 580 R 63 0.9 0:01.94 supervisord 27145 root 20 0 929m 882m 600 R 60 0.9 0:01.87 supervisord ...
好像问题依旧存在,系统CPU使用率依旧很高。仔细查看 top 的输出,发现 supervisord 进程的 pid 每次都有变化,现在已经变成了 27114。进程的 PID 号在变化,说明了什么?有两点:进程在不断的重启,比如因为段错误、配置错误等,进程退出后可能又被监控系统重启了。 这些进程都是短时进程。这些进程一般只运行很短时间结束,很难通过 top 发现。
supervisor 是一个进程管理程序,能够监控进程状态,异常退出时能自动重启。目前看来,极有可能是第一种情况。通过 supervisor 来查看服务状态:
从 uptime 更新时间长短可以发现,很多服务在不断地重启。可能是服务配置或者代码问题引起的。其实是批量的服务出现配置问题导致了服务启动失败,然后 supervisor 监控到服务没有成功启动后又自动重启。在解决完这些服务问题后,通过 top 再次看看性能情况:$ top Tasks: 2012 total, 7 running, 2002 sleeping, 0 stopped, 3 zombie Cpu(s): 29.7%us, 5.4%sy, 0.0%ni, 63.5%id, 0.8%wa, 0.0%hi, 0.5%si, 0.0%st Mem: 98979248k total, 96231348k used, 2747900k free, 1470892k buffers Swap: 0k total, 0k used, 0k free, 60741708k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 13159 user 20 0 9131m 6.2g 13m S 80 6.6 160593:01 mongod 27545 user 20 0 41880 6636 2896 S 61 0.0 0:01.86 python 27549 root 20 0 929m 882m 616 R 38 0.9 0:01.15 supervisord ... 总结
在动手进行性能优化前,需要考虑以下几个问题: 如何判断优化是有效的呢? 有多个性能问题同时发生时,应该先优化哪一个? 提升方法有多种时,应该选择哪一种?
对于第一个问题,我们解决性能问题的目的,自然有一个性能提升的效果,即要对性能指标进行量化,需要确认优化前指标、优化后指标。当优化后指标与优化前指标有不同时,才能判定我们的优化有没有效果。比如上述例子中的系统CPU使用率优化前后的变化。
对于第二个问题,有一个 "二八原则",也就是 80% 的问题都是由 20% 的代码导致的。因此,并不是所有的性能问题都值得优化。在动手优化前,往往需要把所有性能问题分析一遍,找出最重要的、可以最大程度提升性能的问题。这个过程会花费较多的时间,下面有两个可以简化这一过程的方法: 如果发现出现系统瓶颈问题,那么首先优化的一定是系统资源问题。 针对不同类型的指标,首先要去优化那些性能指标变化幅度最大的问题。
对于第三个问题,一般来说,应该选择能最大程度提升性能的方法。但是需要注意的是,性能优化并非没有成本,很有可能你优化了一个指标,另一个指标的性能就变差了。 参考https://time.geekbang.org/column/article/69618 https://time.geekbang.org/column/article/69859 https://time.geekbang.org/column/article/70476 https://time.geekbang.org/column/article/72147 http://www.lyyyuna.com/2020/05/29/perftest-analysis-cpu1/ https://juejin.cn/post/6844903608371118094 https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/top.html