《Linux 性能优化实战》第一周--CPU性能篇(上)

首先,非常感谢Go夜读杨文大佬组织了这样一个读书会, 可以让大家在繁忙的工作生活之余抽出时间来一起学习进步。希望能和群里小伙伴共同讨论学习,一起坚持下去。

第一本严格意义上并不是书籍,而是极客时间上倪老师的专栏《Linux性能优化实战》。无论是软件业务逻辑还是底层架构实现,我们在设计时都要尽可能的考虑性能开销,性能优化是软件系统中最有挑战的工作之一,比较考验程序员的综合能力。

在平时工作中我也会经常遇到一些性能相关的问题,不知如何排查和解决。希望读完这个专栏后能真正做到将性能问题和系统原理关联起来,特别是把系统从应用程序、库函数、系统调用再到内核和硬件等不同的层级贯穿起来


Lesson 1 如何学习Linux性能优化

解决性能问题首先需要理解应用程序和系统的少数几个基本原理,再进行大量的实战练习,建立起整体性能的全局观。

性能指标

高并发和响应快对应着性能优化的两个核心指标:吞吐延时

  • 应用负载角度:直接影响了产品终端的用户体验
  • 系统资源角度:资源使用率、饱和度等

性能问题的本质就是系统资源已经到达瓶颈,但请求的处理还不够快,无法支撑更多的请求。 性能分析实际上就是找出应用或系统的瓶颈,设法去避免或缓解它们。

  • 选择指标评估应用程序和系统性能
  • 为应用程序和系统设置性能目标
  • 进行性能基准测试
  • 性能分析定位瓶颈
  • 性能监控和告警

对于不同的性能问题要选取不同的性能分析工具。 下面是常用的Linux Performance Tools以及对应分析的性能问题类型。

学习重点

建立整体系统性能的全局观:

  • 理解最基本的几个系统知识原理
  • 掌握必要的性能工具
  • 通过实际的场景演练,贯穿不同的组件。

Lesson 2 到底应该怎么理解“平均负载”

平均负载:单位时间内,系统处于可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数。它和我们传统意义上理解的CPU使用率并没有直接关系。

其中不可中断进程是正处于内核态关键流程中的进程(如常见的等待设备的I/O响应)。不可中断状态实际上是系统对进程和硬件设备的一种保护机制。

平均负载多少时合理

实际生产环境中将系统的平均负载监控起来,根据历史数据判断负载的变化趋势。当负载存在明显升高趋势时,及时进行分析和调查。 当然也可以当设置阈值(如当平均负载高于CPU数量的70%时)

现实工作中我们会经常混淆平均负载和CPU使用率的概念,其实两者并不完全对等:

  • CPU密集型进程,大量CPU使用会导致平均负载升高,此时两者一致
  • I/O密集型进程,等待I/O也会导致平均负载升高,此时CPU使用率并不一定高
  • 大量等待CPU的进程调度会导致平均负载升高,此时CPU使用率也会比较高

平均负载案例分析

分别对上述三种场景进行分析,使用的工具为iostat,mpstat,pidstat

其中还用到了Linux系统压力测试工具stress

CPU密集型进程

stress --cpu 1 --timeout 600 #模拟CPU使用率100%
watch -d uptime              #查看平均负载变化情况
mpstat -P ALL 5              #查看CPU使用率的变化情况

此实验中,CPU使用率为100%,但是iowait只有0,说明平均负载高是由CPU使用率导致

pidstat -u 5 1               #可以查看具体哪个进程导致了CPU使用率高

I/O密集型进程

stress -i 1 --timeout 600    #模拟I/O压力,不停地执行sync
watch -d uptime              #查看平均负载变化情况
mpstat -P ALL 5 1              #查看CPU使用率的变化情况

此实验中,CPU使用率为23%,而iowait高达67%,说明平均负载高是由iowait升高导致

pidstat -u 5 1               #可以查看具体哪个进程导致了iowait高

大量进程

stress -c 8 --timeout 600    #模拟8进程(高于CPU核数)
watch -d uptime              #查看平均负载变化情况
mpstat -P ALL S              #查看CPU使用率的变化情况

此实验中,CPU严重过载,8个进程在抢占CPU,导致平均负载升高

pidstat -u 5 1               #可以查看具体哪个进程导致了CPU使用率高

###总结
平均负载高时可能是CPU密集型进程导致,也可能是I/O繁忙导致。具体分析时可以结合mpstat/pidstat工具辅助分析负载来源。


Lesson 3 经常说的CPU上下文切换是什么意思?(上)

CPU上下文切换,就是把前一个任务的CPU上下文(CPU寄存器和PC)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的位置,运行新任务。其中,保存下来的上下文会存储在系统内核中,待任务重新调度执行时再加载,保证原来的任务状态不受影响。

按照任务类型,CPU上下文切换分为:

  • 进程上下文切换
  • 线程上下文切换
  • 中断上下文切换

进程上下文切换

Linux进程按照等级权限将进程的运行空间分为内核空间和用户空间。从用户态向内核态转变时需要通过系统调用来完成。

一次系统调用过程其实进行了两次CPU上下文切换:

  • CPU寄存器中用户态的指令位置先保存起来,CPU寄存器更新为内核态指令的位置,跳转到内核态运行内核任务;
  • 系统调用结束后,CPU寄存器恢复原来保存的用户态数据,再切换到用户空间继续运行。

系统调用过程中并不会涉及虚拟内存等进程用户态资源,也不会切换进程。和传统意义上的进程上下文切换不同。因此系统调用通常称为特权模式切换

进程是由内核管理和调度的,进程上下文切换只能发生在内核态。 因此相比系统调用来说,在保存当前进程的内核状态和CPU寄存器之前,需要先把该进程的虚拟内存,栈保存下来。再加载新进程的内核态后,还要刷新进程的虚拟内存和用户栈。

进程只有在调度到CPU上运行时才需要切换上下文,有以下几种场景: CPU时间片轮流分配,系统资源不足导致进程挂起,进程通过sleep函数主动挂起,高优先级进程抢占时间片,硬件中断时CPU上的进程被挂起转而执行内核中的中断服务。

线程上下文切换

线程上下文切换分为两种:

  • 前后线程同属于一个进程,切换时虚拟内存资源不变,只需要切换线程的私有数据,寄存器等;
  • 前后线程属于不同进程,与进程上下文切换相同。

同进程的线程切换消耗资源较少,这也是多线程的优势。

中断上下文切换

中断上下文切换并不涉及到进程的用户态,因此中断上下文只包括内核态中断服务程序执行所必须的状态(CPU寄存器,内核堆栈,硬件中断参数等)。

中断处理优先级比进程高,所以中断上下文切换和进程上下文切换不会同时发生。

总结

尽量在程序中使用多线程,因为多线程上下文切换资源消耗较少。


Lesson 4 经常说的CPU上下文切换是什么意思?(下)

通过vmstat可以查看系统总体的上下文切换情况。

vmstat 5         #每隔5s输出一组数据
  • cs (context switch) 每秒上下文切换次数
  • in (interrupt) 每秒中断次数
  • r (runnning or runnable)就绪队列的长度,正在运行和等待CPU的进程数
  • b (Blocked) 处于不可中断睡眠状态的进程数

要查看每个进程的详细情况,需要使用pidstat来查看每个进程上下文切换情况

pidstat -w 5
  • cswch 每秒自愿上下文切换次数 (进程无法获取所需资源导致的上下文切换)
  • nvcswch 每秒非自愿上下文切换次数 (时间片轮流等系统强制调度)

实验分析

vmstat 1 1    #首先获取空闲系统的上下文切换次数
sysbench --threads=10 --max-time=300 threads run #模拟多线程切换问题

vmstat 1 1    #新终端观察上下文切换情况
此时发现cs数据明显升高,同时观察其他指标:
r列: 远超系统CPU个数,说明存在大量CPU竞争
us和sy列: sy列占比80%,说明CPU主要被内核占用
in列: 中断次数明显上升,说明中断处理也是潜在问题

综合以上分析,说明运行/等待CPU的进程过多,导致大量的上下文切换,上下文切换导致系统的CPU占用率高。

pidstat -w -u 1  #查看到底哪个进程导致的问题

从结果中看出是sysbench导致CPU使用率过高,但是pidstat输出的上下文次数加起来也并不多。分析sysbench模拟的是线程的切换,因此需要在pidstat后加-t参数查看线程指标。

另外对于中断次数过多,我们可以通过/proc/interrupts文件读取

watch -d cat /proc/interrupts

发现次数变化速度最快的是重调度中断(RES),该中断用来唤醒空闲状态的CPU来调度新的任务运行。分析还是因为过多任务的调度问题,和上下文切换分析一致。

总结

每秒上下文切换多少才正常呢?
经验来说,当每秒上下文切换超过1w次,或者切换次数出现数量级的增长时,系统可能出现了性能问题。此时可以根据上下文切换类型来具体分析是I/O问题还是CPU瓶颈,或者具体哪一类中断导致的异常。


Lesson 5 某个应用的CPU使用率达到100%,怎么办?

Linux作为多任务操作系统,将CPU时间划分为很短的时间片,通过调度器轮流分配给各个任务使用。为了维护CPU时间,Linux通过事先定义的节拍率,触发时间中断,并使用全局变了jiffies记录开机以来的节拍数。时间中断发生一次该值+1.

CPU使用率,除了空闲时间以外的其他时间占总CPU时间的百分比。可以通过/proc/stat中的数据来计算出CPU使用率。因为/proc/stat时开机以来的节拍数累加值,计算出来的是开机以来的平均CPU使用率,一般意义不大。可以间隔取一段时间的两次值作差来计算该段时间内的平均CPU使用率。 性能分析工具给出的都是间隔一段时间的平均CPU使用率,要注意间隔时间的设置。

CPU使用率可以通过top 或 ps来查看。分析进程的CPU问题可以通过perf,它以性能事件采样为基础,不仅可以分析系统的各种事件和内核性能,还可以用来分析指定应用程序的性能问题。

perf top / perf record / perf report (-g 开启调用关系的采样)

实验案例

sudo docker run --name nginx -p 10000:80 -itd feisky/nginx
sudo docker run --name phpfpm -itd --network container:nginx feisky/php-fpm

ab -c 10 -n 100 http://XXX.XXX.XXX.XXX:10000/ #测试Nginx服务性能

发现此时每秒可承受请求给长少,此时将测试的请求数从100增加到10000。 在另外一个终端运行top查看每个CPU的使用率。发现系统中几个php-fpm进程导致CPU使用率骤升。

接着用perf来分析具体是php-fpm中哪个函数导致该问题。

perf top -g -p XXXX #对某一个php-fpm进程进行分析

发现其中sqrt和add_function占用CPU过多, 此时查看源码找到原来是sqrt中在发布前没有删除测试代码段,存在一个百万次的循环导致。 将该无用代码删除后发现nginx负载能力明显提升。

总结

在碰到CPU使用率过高问题时,可以借助top pidstat确定引发CPU性能问题的来源,然后用perf来具体定位到引起性能问题的函数。可以用来代码性能进一步优化。


Lesson 6 系统的CPU使用率很高,为什么找不到高CPU的应用?

按照上篇的CPU性能问题分析套路,看起来CPU使用率问题很容易排查,真实场景中却不然。

实验案例分析

sudo docker run --name nginx -p 10000:80 -itd feisky/nginx:sp
sudo docker run --name phpfpm -itd --network container:nginx feisky/php-fpm:sp
ab -c 100 -n 1000 http://XXX.XXX.XXX.XXX:10000/ #并发100个请求测试

实验结果中每秒请求数依旧不高,我们将并发请求数降为5后,nginx负载能力依旧很低。

此时用top和pidstat发现系统CPU使用率过高,但是并没有发现CPU使用率高的进程。

出现这种情况一般时我们分析时遗漏的什么信息,重新运行top命令并观察一会。发现就绪队列中处于Running状态的进行过多,超过了我们的并发请求次数5. 再仔细查看进程运行数据,发现nginx和php-fpm都处于sleep状态,真正处于运行的却是几个stress进程。

下一步就利用pidstat分析这几个stress进程,发现没有任何输出。用ps aux交叉验证发现依旧不存在该进程。说明不是工具的问题。再top查看发现stress进程的进程号变化了,此时有可能时以下两种原因导致:

  • 进程不停的崩溃重启(如段错误/配置错误等),此时进程退出后可能又被监控系统重启;
  • 短时进程导致,即其他应用内部通过exec调用的外面命令,这些命令一般只运行很短时间就结束,很难用top这种间隔较长的工具来发现

可以通过pstree来查找 stress的父进程,找出调用关系。

pstree | grep stress

发现是php-fpm调用的该子进程,此时去查看源码可以看出每个请求都会调用一个stress命令来模拟I/O压力。 之前top显示的结果是CPU使用率升高,是否真的是由该stress命令导致的,还需要继续分析。 代码中给每个请求加了verbose=1的参数后可以查看stress命令的输出,在中断测试该命令结果显示stress命令运行时存在因权限问题导致的文件创建失败的bug。

此时依旧只是猜测,下一步继续通过perf工具来分析。性能报告显示确实时stress占用了大量的CPU,通过修复权限问题来优化解决即可。

总结

对于常规问题无法解释的CPU使用率场景,首先要想到可能时短时应用导致的问题。对于短时进程,还可以用execsnoop工具来进行监控。


Lesson 7/8 系统中出现大量不可中断进程和僵尸进程怎么办?

进程状态

  • R Running/Runnable,表示进程在CPU的就绪队列中,正在运行或者等待运行;
  • D Disk Sleep,不可中断状态睡眠,一般表示进程正在跟硬件交互,并且交互过程中不允许被其他进程中断;
  • Z Zombie,僵尸进程,表示进程实际上已经结束,但是父进程还没有回收它的资源;
  • S Interruptible Sleep,可中断睡眠状态,表示进程因为等待某个事件而被系统挂起,当等待事件发生则会被唤醒并进入R状态;
  • I Idle,空闲状态,用在不可中断睡眠的内核线程上。 该状态不会导致平均负载升高;
  • T Stop/Traced,表示进程处于暂停或跟踪状态(SIGSTOP/SIGCONT, GDB调试);
  • X Dead,进程已经消亡,不会在top/ps中看到。

对于不可中断状态,一般都是在很短时间内结束,可忽略。但是如果系统或硬件发生故障,进程可能会保持不可中断状态很久,甚至系统中出现大量不可中断状态,此时需注意是否出现了I/O性能问题。

僵尸进程一般多进程应用容易遇到,父进程来不及处理子进程状态时子进程就提前退出,此时子进程就变成了僵尸进程。大量的僵尸进程会用尽PID进程号,导致新进程无法建立。

实验案例分析

磁盘O_DIRECT问题

sudo docker run --privileged --name=app -itd feisky/app:iowait
ps aux | grep '/app'

可以看到此时有多个app进程运行,状态分别时Ss+和D+。其中后面s表示进程是一个会话的领导进程,+号表示前台进程组。

其中进程组表示一组相互关联的进程,子进程是父进程所在组的组员。 会话指共享同一个控制终端的一个或多个进程组。

用top查看系统资源发现:1)平均负载在逐渐增加,且1分钟内平均负载达到了CPU个数,说明系统可能已经有了性能瓶颈;2)僵尸进程比较多且在不停增加;3)us和sys CPU使用率都不高,iowait却比较高;4)每个进程CPU使用率也不高,但有两个进程处于D状态,可能在等待IO。

分析目前数据可知:iowait过高导致系统平均负载升高,僵尸进程不断增长说明有程序没能正确清理子进程资源。

用dstat来分析,因为它可以同时查看CPU和I/O两种资源的使用情况,便于对比分析。

dstat 1 10    #间隔1秒输出10组数据

可以看到当wai(iowait)升高时磁盘请求read都会很大,说明iowait的升高和磁盘的读请求有关。接下来分析到底时哪个进程在读磁盘。

之前top查看的处于D状态的进程号,用pidstat -d -p XXX 展示进程的I/O统计数据。发现处于D状态的进程都没有任何读写操作。 在用pidstat -d 查看所有进程的I/O统计数据,看到app进程在进行磁盘读操作,每秒读取32MB的数据。进程访问磁盘必须使用系统调用处于内核态,接下来重点就是找到app进程的系统调用。

sudo strace -p XXX #对app进程调用进行跟踪

报错没有权限,因为已经时root权限了。所以遇到这种情况,首先要检查进程状态是否正常。 ps命令查找该进程已经处于Z状态,即僵尸进程。

这种情况下top pidstat之类的工具无法给出更多的信息,此时像第5篇一样,用perf record -d和perf report进行分析,查看app进程调用栈。

看到app确实在通过系统调用sys_read()读取数据,并且从new_sync_read和blkdev_direct_IO看出进程时进行直接读操作,请求直接从磁盘读,没有通过缓存导致iowait升高。

通过层层分析后,root cause是app内部进行了磁盘的直接I/O。然后定位到具体代码位置进行优化即可。

僵尸进程

上述优化后iowait显著下降,但是僵尸进程数量仍旧在增加。首先要定位僵尸进程的父进程,通过pstree -aps XXX,打印出该僵尸进程的调用树,发现父进程就是app进程。

查看app代码,看看子进程结束的处理是否正确(是否调用wait()/waitpid(),有没有注册SIGCHILD信号的处理函数等)。

总结

碰到iowait升高时,先用dstat pidstat等工具确认是否存在磁盘I/O问题,再找是哪些进程导致I/O,不能用strace直接分析进程调用时可以通过perf工具分析。

对于僵尸问题,用pstree找到父进程,然后看源码检查子进程结束的处理逻辑即可。

——————————————————————————————————————————————————————————————————
LastModify:2020-05-31