Linux性能优化实战CPU篇(二)
一、CPU使用率过高
1,CPU使用率
a>节拍率
为了维护CPU时间,Linux通过事先定义的节拍率(内核中表示为HZ),触发时间中断,并使用全局变量Jiffies记录开机以来的节拍数。每发生一次时间中断,Jiffies的值就加1
节拍率HZ是内核的可配置选项
#查看当前系统的节拍率为每秒钟250次时间中断 grep 'CONFIG_HZ=' /boot/config-$(uname -r) CONFIG_HZ=250
同时内核还提供了一个用户空间节拍率USER_HZ,固定值为100,也就是1/100秒
b>/proc虚拟文件系统
cpu 2032004 102648 238344 167130733 758440 15159 17878 0 cpu0 1022597 63462 141826 83528451 366530 9362 15386 0 cpu1 1009407 39185 96518 83602282 391909 5796 2492 0 intr 303194010 212852371 3 0 0 11 0 0 2 1 1 0 0 3 0 11097365 0 72615114 6628960 0 179 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ctxt 236095529 btime 1195210746 processes 401389 procs_running 1 procs_blocked 0
第一行的数值表示的是CPU总的使用情况,所以我们只要用第一行的数字计算就可以了。下表解析第一行各数值的含义:
参数 解析(单位:jiffies) user(2032004) 从系统启动开始累计到当前时刻,用户态的CPU时间,不包含 nice值为负进程。 nice(102648) 从系统启动开始累计到当前时刻,nice值为负的进程所占用的CPU时间 system (238344) 从系统启动开始累计到当前时刻,核心时间 idle (167130733) 从系统启动开始累计到当前时刻,除IO等待时间以外其它等待时间 iowait (758440) 从系统启动开始累计到当前时刻,IO等待时间 irq (15159) 从系统启动开始累计到当前时刻,硬中断时间 softirq (17878) 从系统启动开始累计到当前时刻,软中断时间
c>CPU使用率
CPU在t1到t2时间段即时利用率 = 1 - CPU空闲使用时间 / CPU总的使用时间
CPU在t1到t2时间段空闲使用时间 = (idle2 - idle1)
CPU在t1到t2时间段总的使用时间 = ( user2+ nice2+ system2+ idle2+ iowait2+ irq2+ softirq2) - ( user1+ nice1+ system1+ idle1+ iowait1+ irq1+ softirq1)
2,如何查看CPU使用率
a>top
top默认使用3秒时间间隔,它显示了系统总体的CPU和内存使用情况,以及各个进程的资源使用情况
top - 09:52:06 up 2 days, 53 min, 1 user, load average: 0.38, 0.69, 0.88 任务: 445 total, 1 running, 353 sleeping, 0 stopped, 4 zombie %Cpu(s): 2.3 us, 1.4 sy, 0.0 ni, 95.8 id, 0.0 wa, 0.0 hi, 0.5 si, 0.0 st KiB Mem : 16163124 total, 588480 free, 11530672 used, 4043972 buff/cache KiB Swap: 2097148 total, 1243900 free, 853248 used. 2415292 avail Mem
PID USER PR NI VIRT RES SHR � %CPU %MEM TIME+ COMMAND 11775 mi 20 0 4872384 499200 134812 S 11.5 3.1 42:41.12 gnome-shell 11318 mi 20 0 1355336 261508 226128 S 7.6 1.6 53:12.32 Xorg 21215 mi 20 0 3407932 604084 27340 S 6.6 3.7 135:50.48 WeChat.exe 21220 mi 20 0 10068 6080 684 S 6.2 0.0 120:01.52 wineserver.real 9586 mi 20 0 693000 36112 21156 S 4.6 0.2 0:19.49 gnome-terminal- 5907 mi 20 0 13.246g 3.962g 29032 S 3.9 25.7 632:20.02 java
第三行%Cpu就是系统的CPU使用率,具体每列的含义与/proc类似
b>ps
ps (英文全拼:process status)命令用于显示当前进程的状态,进程的整个生命周期
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 225968 8288 ? Ss 2月07 1:57 /lib/systemd/systemd --system --deserialize 19 root 2 0.0 0.0 0 0 ? S 2月07 0:00 [kthreadd]
具体含义
USER: 行程拥有者 PID: pid %CPU: 占用的 CPU 使用率 %MEM: 占用的记忆体使用率 VSZ: 占用的虚拟记忆体大小 RSS: 占用的记忆体大小 TTY: 终端的次要装置号码 (minor device number of tty) STAT: 该行程的状态:D: 无法中断的休眠状态 (通常 IO 的进程); R: 正在执行中; S: 静止状态; T: 暂停执行; Z: 不存在但暂时无法消除; W: 没有足够的记忆体分页可分配; <: 高优先序的行程; N: 低优先序的行程; L: 有记忆体分页分配并锁在记忆体内 (实时系统或捱A I/O) START: 行程开始时间 TIME: 执行的时间 COMMAND:所执行的指令
c>pidstat
pidstat [ 选项 ] [ <时间间隔> ] [ <次数> ]
可以指定时间间隔来输出CPU使用率
#每隔 1 秒输出一组数据,共输出 5 组 mi@mi:~$ pidstat 1 5 10时08分51秒 UID PID %usr %system %guest %wait %CPU CPU Command 10时08分52秒 1000 641 0.99 0.00 0.00 0.00 0.99 9 chrome 10时08分52秒 1000 996 0.00 0.99 0.00 0.00 0.99 4 chrome 10时08分52秒 1000 4387 0.99 0.99 0.00 0.00 1.98 0 pidstat 10时08分52秒 1000 5907 3.96 0.00 0.00 0.00 3.96 2 java ... 平均时间: UID PID %usr %system %guest %wait %CPU CPU Command 平均时间: 0 11 0.00 0.20 0.00 0.00 0.20 - rcu_sched 平均时间: 1000 548 0.20 0.20 0.00 0.00 0.40 - chrome 平均时间: 0 1745 0.00 0.20 0.00 0.00 0.20 - kworker/u24:4-events_unbound 平均时间: 0 2568 0.00 0.20 0.00 0.00 0.20 - kworker/u24:2-i915 平均时间: 0 3545 0.00 0.20 0.00 0.00 0.20 - kworker/9:2-events
-
%usr 用户态CPU使用率
-
%system 内核态CPU使用率
-
%guest 运行虚拟机CPU使用率
-
%wait 等待CPU使用率
-
%CPU 总的CPU使用率
-
最后的Average,则为计算了5组数据的平均值
3,CPU使用率过高怎么办?
a>采用top、ps、pidstat等工具,找到CPU使用率较高的进程
b>使用perf top 展示热点函数
输出结果中,第一行包含三组数据,分别是采样数(Samples)、事件类型(event)和事件总数量(Event count)。比如当前perf采集了833个CPU时钟事件,总事件数为97742399。
采样数需要尽可能的多,这样才有参考价值。解析表格样式的数据为:
- Overhead,是该符号的性能事件在所有采样中的比例,用百分比来表示
- Shared,是该函数或指令所在的动态共享对象(Dynamic Shared Object),如内核、进程名、动态链接库名、内核模块名等。
- Object,是动态共享对象的类型。比如 [.] 表示用户空间的可执行程序、或者动态链接库,而 [k] 则表示内核空间
- Symbol,函数名。当函数名未知时,用十六进制的地址来表示
从上图可以看出CPU时钟最多的是perf工具自身,其比例也只有7.28%,说明系统并没有CPU性能问题。
c>perf record 和 perf report
perf top 虽然实时展示了系统的性能信息,但他的缺点是并不保存数据,无法用于离线或后续的分析
perf record 则提供了保存数据的功能,保存后的数据,需要用perf report解析展示
//按 ctrl + c 终止采样 root@xc:~# perf record [ perf record: Woken up 1 times to write data ] [ perf record: Captured and wrote 3.679 MB perf.data (26683 samples) ] //展示类似于 perf top的报告 root@xc:~# perf report
在实际使用中,我们经常使用perf top 和 perf record 加上 -g 参数,开启调用关系的采样,方便调用链的分析
4,案例一
使用两台虚拟机,其中一台用作Web服务器,来模拟性能问题;另一台用作Web服务器的客户端,来给Web服务器增加压力请求。使用两台虚拟机是为了相互隔离,避免“交叉感染”。
VM1上启动应用:
VM2上测试并压测:
启动成功,进行性能压测
从ab输出结果可以看到,Nginx能承受的每秒平均请求数只有11.63。为了方便分析,我们将请求数增到10000,继续压测
VM1上分析原因
top,可以看到php-fpm的CPU使用率加起来将近200%;每个CPU的使用率均已超过98%
perf top ,使用-g 开启调用关系分析, -p 指定 php-fpm的进程号 21515
发现调用关系最终到sqrt和add_function出现占用的CPU时钟较高。
5,案例二(定位CPU升高的应用)
使用两台虚拟机,其中一台用作Web服务器,来模拟性能问题;另一台用作Web服务器的客户端
VM1上启动应用
VM2上测试并压测:
启动成功,进行压测
从ab结果可以看到,Nginx能承受的每秒平均请求数,只有87左右。接下来,将并发请求数改成5,延长请求时长为10分钟,方便vm1定位分析问题
VM1上分析原因
top分析,发现CPU使用最高的进程才2.7%,并不特别高。然而在CPU使用率(%Cpu),可以看到用户CPU使用率为80%,系统CPU为15.1%,而空闲CPU为2.8%
pidstat,观察发现CPU的使用率也都不高
再次采用top观察,发现Tasks中有个6个Running状态的进程,而观察咱们启动的php-fpm确都处于Sleep(S)状态,真正处于Running状态的时stress进程
采用pidstat进一步观察其中的一个stress进程,发现无输出
采用ps查看这个stress进程,任然无输出。
再次采用top命令查看,发现stress进程的进程号都已经发生变化
分析原因:
- 进程在不断地崩溃重启,比如因为配置错误等,进程在退出后可能又被监控系统自动重启
- 进程为短时进程,也就是在其他应用内部通过exec调用的外部命令。这些命令一般都只运行很短的时间就会结束,也很难通过top这种间隔时间较长的工具发现
通过pstree查找真实的stress进程的父进程,这里可以看到stress是被php-fpm调用的子进程,并且进程数量不止一个(这里是3个)
采用perf record -g命令,等待一会儿之后按Ctrl+C退出,再用perf report查看报告
发现 stress占用了CPU时钟事件的77%,而stress调用栈中比例较高的为函数random(),再进行具体的优化,就可以解决。
6,execsnoop
execsnoop-专门用于为追踪短时进程(瞬时进程)设计的工具;它通过 ftrace 实时监控进程的 exec() 行为,并输出短时进程的基本信息,包括进程 PID、父进程 PID、命令行参数以及执行的结果。
github地址: https://github.com/brendangregg/perf-tools/blob/master/execsnoop
如何安装使用:将上面的github的内容复制,然后写入execsnoop文件,并且加上x权限即可;
可以发现大量的stress进程在不停启动
二、僵尸进程
1,进程的状态
以top为例,在top中有一列为S列(也就是Status列),可以查看到有T、D、Z、S、I等几个状态
- R是Running或Runnable的缩写,表示进程在CPU的就绪队列中,正在运行或者正在等待运行
- D是Disk Sleep的缩写,也就是不可中断状态睡眠(Uninterruptible Sleep),一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程中断
- Z是Zombie的缩写,它表示僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符、PID等)
- S是Interruptible Sleep的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入R状态
- I是Idle的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上。前面说了,硬件交互导致的不可中断进程用D表示,但对某些内核线程来说,它们有可能实际上并没有任何负载,用Idle正是为了区分这种情况。因为,D状态的进程会导致平均负载升高,I状态的进程却不会
- T(t)是Stopped或Traced的缩写,表示进程处于暂停或跟踪状态。
- X是Dead的缩写,表示进程已经消亡,不能在top或ps中看到
当一个进程创建了子进程后,它应该通过系统调用wait()或waitpid()等待子进程结束,回收子进程的资源;而子进程在结束时,会向它的父进程发送SIGCHLD信号,所以父进程也可以注册SIGCHLD信号的处理函数,异步回收资源。
如果父进程没这么做,或子进程执行太快,父进程还没有来得及处理子进程状态,子进程就已经提前退出,那么这时的子进程就会变成僵尸进程。通常,僵尸进程持续的时间都比较短,在父进程回收它的资源后就会消亡;或者在父进程退出后,由init进程回收后也会消亡。
一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会处于僵尸状态。大量的僵尸进程会用尽PID进程号,导致新进程不能创建,所以这种情况需要避免。
2,案例分析
准备Ubuntu 18.04 机器配置 2CPU,8GB内存,预先安装docker、sysstat、dstat等工具
启动应用
ps查看应用为正常启动
Ss+和D+。其中S表示可中断睡眠,D表示不可中断睡眠。s表示这个进程是一个会话的领导进程,+表示前台进程组
top查看
- 第一行看平均负载(load average),过去1分钟、5分钟和15分钟的平均负载在依次缩小,说明平均负载在升高。1分钟内的平均负载为2,说明有可能已经达到性能瓶颈
- 第二行看Tasks,有1个正在运行的进程,但是发现zombie僵尸进程比较多,而且在不停的增加,说明有子进程在退出时未被清理
- 查看CPU的使用情况,发现用户CPU和系统CPU都不高,但是iowait分别时60.5%和94.6%
dstat分析iowait
可以看到当iowait(wai)升高时,磁盘的读请求(read)都会很大。这说明iowait的升高跟磁盘的读请求有关,很可能就是磁盘读导致的。接下来需要进一步分析具体的哪个进程读磁盘呢?
pidstat采用-d分析I/O和进程之间的情况
可以发现app进程每秒读的数据为32MB,基本可以断定是app应用导致。接下来继续追踪app进程在执行什么I/O操作?
strace追踪进程6082
ps查看6082进程情况,发现6082已经是僵尸进程。
目前iowait还在继续,但是top、pidstat这类工具已经不能给予帮助了,接下来采用perf查看
其中swapper是内核中的调度进程,可以先忽略。可以看到在app中通过系统调用sys_read()读取数据。并且从new_sync_read和blkdev_read_iter能看出,进程正在对磁盘进行直接读,也就是绕过了系统缓存,每个读请求都会直接从磁盘读取,导致iowait升高。 定位到原因后就可以打开源码发现在app.c的文件出现了O_DIRECT选项直接打开磁盘,删除这个选项
重启后,再使用top查看,发现iowait已经很低了只有0.3%。但是查看僵尸进程的数量还是在不断的增加
采用pstree定位父进程,发现还是app应用导致的僵尸进程。进一步查看并修复。