性能瓶颈--CPU(平均负载)
操作系统的发展
单进程时代
这个时候的每一个程序就是一个进程,直到这个进程运行完毕,才能接着运行下一个进程,这是单进程的串行时代。但这有个问题,首先,如果进程需要接收数据,阻塞了,那么cpu就在白白浪费,为了避免这种十分不必要的浪费,怎么办呢,多进程。
多进程时代
为了避免单进程串行运行阻塞导致的cpu浪费,多进程的解决方式是如果一个进程阻塞了,那么cpu会切换到另外等待执行的进程上。这样就完美了。
但是日子久了,程序员发现,多进程有个问题啊,进程拥有的资源太多了,cpu在切换的时候把时间都花在了资源的创建,切换,销毁。真正用cpu在计算的时间太少了,cpu还是利用不起来啊。尤其是再是个多核cpu,进程一会在这个cpu上一会在那个cpu上,更浪费宝贵的cpu。看来得需要一个轻量级的进程了,
线程时代
在期盼中,线程诞生了,一个进程中可以运行多个线程,由于线程的上下文切换是十分便捷的。进程为线程提供了虚拟内存全局变量。线程的切换只需要将自己的一些栈和寄存器切换保存就行了。但是多线程还是很坑啊,具体看看为什么Nginx采用多进程单线程的原因就知道了。这里不多做说明。
新兴的协程
既然线程依旧还有问题,程序员不放弃啊,协程诞生了,运行于线程之上。它非常轻量,一个goroutine只占几KB,并且这几KB就足够goroutine运行完,要想一个线程是要占用几MB的。协程本身就是运行在一组线程之上,不需要频繁的创建、销毁线程,而是对线程的复用。
cpu的性能诊断
说了这么多,cpu的瓶颈究竟是从哪来的呢?什么会造成cpu的性能瓶颈。其实要不就是任务太重,要不就用的不当。(可能说了一句废话)。要想知道为什么cpu会产生瓶颈,还是得了解cpu的运行原理。又回到了最枯燥的地方。
应用程序在服务器上运行的大致是这样的一个架构。应用层,运行依赖的环境,操作系统和硬件。
程序运行架构.png
下面这张是经典的性能分析工具图谱。其实看到这张图,好多人可能以为又是要开始飘了。。。
linux_perf_tools_full.png
cpu性能指标
- 平均负载
- cpu使用率
- cpu上下文切换 (进程,线程)
- 中断 (硬中断,软中断)
其实 cpu 的性能瓶颈就是围绕着这几点展开的
平均负载
- 含义:平均负载指处于可运行状态 和 不可中断状态 的平均进程数,运行队列中的活跃进程数的平均值。所以其不仅有正在使用CPU的进程,还包括等待CPU和等待IO的进程。
R:Running or Runnable状态,表示进程在CPU的就绪队列中,正在运行或等待运行
D:Disk Sleep缩写,表示不可中断状态睡眠,一般表示进程正在与硬件交互,并且交互过程中不允许其他进程打断(进程不响应异步信号),
举例:
比如执行read系统调用对某个设备文件进行读操作时需要用D来标注,避免进程与设备交互的过程被打断。
D状态的进程是不响应信号量的,因为已经完全陷入了内核态,想要杀死D状态的进程,要么重启,要么满足他请求的资源。
Z:Zombie的缩写,指的是实际进程已经结束但父进程并未回收他的资源(PID,进程描述符等)
S:Interrruptible Sleep的缩写,可中断状态睡眠,进程因等待某个事件而被挂起。当事件发生时,进程将会被添加到就绪队列等待调度,进程状态更新为R
I:idle缩写用在不可中断睡眠的内核线程上,硬件交互导致的不可中断进程为D,但某些内核线程可能没有任何负载,使用Idle来区分这种状态,注意D状态会导致平均负载升高,但是I状态的进程不会。(例如等待socket连接,等待信号量)
T:进程停止状态,可以发送SIGSTOP信号来停止进程。这个暂停的进程可以通过发送SIGCONT信号让进城继续运行
X:死亡状态,这个状态指示一个返回状态,在任务列表无法看到这种状态的进程。
下图是进程之间的转换关系,同样是引用原博主的。
process_state.png
一般来说,cpu的负载率需要小于服务器的cpu核数,这样才是正常的。不过需要注意的是cpu的负载和cpu的使用率是没有直接关系的,cpu的使用率是反应cpu花在计算上的时间。而平均负载只是统计处于R和D状态的进程。
对于CPU密集型应用,进程占用了大量的CPU会导致平均负载升高,这是CPU使用率和平均负载的变化是一致的(处于R状态的进程多)
对于I/O密集型应用,进程大部分时间在等待I/O,虽然会导致平均负载升高,然而CPU使用率却不会升高(处于D状态的进程多)
对于大量等待CPU的的进程调度也会导致平均负载升高,此时的CPU使用率也会比较高。
cpu密集场景模拟
压缩和解压、加密和解密是十分消耗CPU的。所以通过这样的方法模拟cpu密集的场景。
$ cat /dev/urandom | gzip -9 | gzip -d | gzip -9 | gzip -d > /dev/null
执行后观察load
$ uptime
23:20:42 up 36 min, 5 users, load average: 2.38, 2.14, 1.37
load 是升高了,但似乎不是很高(我的虚拟机是4核的)
看看cpu的使用率。
$ mpstat -P ALL 1
平均时间: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
平均时间: all 73.54 0.00 24.28 0.00 0.00 0.04 0.00 0.00 0.00 2.14
平均时间: 0 74.46 0.00 23.08 0.00 0.00 0.00 0.00 0.00 0.00 2.47
平均时间: 1 70.95 0.00 26.42 0.00 0.00 0.00 0.00 0.00 0.00 2.63
平均时间: 2 73.19 0.00 24.64 0.00 0.00 0.00 0.00 0.00 0.00 2.17
平均时间: 3 75.32 0.00 23.10 0.00 0.00 0.29 0.00 0.00 0.00 1.29
可以看到 %usr 占用很多,这证明cpu被利用在用户态,且被充分利用计算。想要看到底是哪个进程在消耗cpu,就需要pidstat 了。
$ pidstat -u 1
平均时间: UID PID %usr %system %guest %wait %CPU CPU Command
平均时间: 124 1064 0.96 0.96 0.00 0.00 1.92 - mongod
平均时间: 1001 6762 0.00 12.50 0.00 10.58 12.50 - cat
平均时间: 1001 6763 70.19 4.81 0.00 21.15 75.00 - gzip
平均时间: 1001 6764 10.58 0.96 0.00 7.69 11.54 - gzip
平均时间: 1001 6765 63.46 4.81 0.00 26.92 68.27 - gzip
平均时间: 1001 6766 10.58 0.00 0.00 18.27 10.58 - gzip
可以看到是gzip在消耗cpu。
I/O 密集型场景模拟
使用压测工具模拟读写临时文件
$ uptime
23:38:37 up 54 min, 5 users, load average: 3.36, 2.68, 1.15
load 是在上升的。
使用mpstat看看。
$ mpstat -P ALL 1
平均时间: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
平均时间: all 1.15 0.36 19.76 3.71 0.00 4.61 0.00 0.00 0.00 70.41
平均时间: 0 1.53 0.42 14.90 4.65 0.00 10.47 0.00 0.00 0.00 68.03
平均时间: 1 0.87 0.39 23.15 3.66 0.00 0.23 0.00 0.00 0.00 71.70
平均时间: 2 1.40 0.00 24.84 2.91 0.00 3.65 0.00 0.00 0.00 67.20
平均时间: 3 0.84 0.65 15.83 3.67 0.00 4.44 0.00 0.00 0.00 74.57
明显iowait在升高。
使用pidstat查看究竟是哪个进程导致的iowait这么高。
$ pidstat -u 1
平均时间: UID PID %usr %system %guest %wait %CPU CPU Command
平均时间: 0 7 0.00 2.75 0.00 0.92 2.75 - ksoftirqd/0
平均时间: 0 359 0.00 1.83 0.00 0.00 1.83 - kworker/0:1H
平均时间: 124 1064 0.92 0.00 0.00 0.00 0.92 - mongod
平均时间: 0 1281 2.75 0.00 0.00 0.00 2.75 - vmtoolsd
平均时间: 1001 2553 1.83 0.00 0.00 0.00 1.83 - vmtoolsd
平均时间: 0 6792 0.00 92.66 0.00 1.83 92.66 - kworker/u256:0
平均时间: 0 7565 0.00 0.92 0.00 4.59 0.92 - stress-ng-io
平均时间: 0 7566 0.92 90.83 0.00 0.00 91.74 - stress-ng-hdd
平均时间: 0 7592 0.00 3.67 0.00 0.00 3.67 - pidstat
大量进程的场景
还是使用压测神器 stress-ng。启用 20个进程进行运算圆周率 Π。
$ stress-ng -c 20 --cpu-method pi
stress-ng: info: [7624] defaulting to a 86400 second (1 day, 0.00 secs) run per stressor
stress-ng: info: [7624] dispatching hogs: 20 cpu
uptime 已经到达20左右
$ uptime
23:48:39 up 1:04, 5 users, load average: 18.26, 8.87, 4.82
运行mpstat查看cpu总体资源。
$ mpstat 1
Linux 4.15.0-46-generic (BattleAngel) 2019年03月20日 _x86_64_ (4 CPU)
23时49分32秒 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
23时49分33秒 all 99.75 0.00 0.25 0.00 0.00 0.00 0.00 0.00 0.00 0.00
23时49分34秒 all 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
几乎都是花在 %usr 上。
看看pidstat。
$ pidstat -u 1
平均时间: UID PID %usr %system %guest %wait %CPU CPU Command
平均时间: 1001 2553 0.96 0.00 0.00 0.00 0.96 - vmtoolsd
平均时间: 0 7625 18.27 0.00 0.00 80.77 18.27 - stress-ng-cpu
平均时间: 0 7626 23.08 0.00 0.00 74.04 23.08 - stress-ng-cpu
平均时间: 0 7627 19.23 0.00 0.00 79.81 19.23 - stress-ng-cpu
平均时间: 0 7628 18.27 0.00 0.00 81.73 18.27 - stress-ng-cpu
平均时间: 0 7629 20.19 0.00 0.00 78.85 20.19 - stress-ng-cpu
平均时间: 0 7630 20.19 0.00 0.00 79.81 20.19 -
20个进程在争抢4个cpu。进程们都在等待cpu的时间(就是%wait),自己正真运行的时间都不足25%
得出的结论是,cpu的负载load高的原因并不只是cpu使用率高导致的。高I/O 也有可能导致load增高。
前面说到了,cpu的瓶颈并不只局限于 load,还有上下文切换,中断等。这个后面会在做分析。这里还是觉得 倪鹏飞老师很厉害,https://time.geekbang.org/column/article/86157 。