mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 06:03:45 +08:00
mod
This commit is contained in:
318
极客时间专栏/Linux性能优化实战/CPU 性能篇/02 | 基础篇:到底应该怎么理解“平均负载”?.md
Normal file
318
极客时间专栏/Linux性能优化实战/CPU 性能篇/02 | 基础篇:到底应该怎么理解“平均负载”?.md
Normal file
@@ -0,0 +1,318 @@
|
||||
<audio id="audio" title="02 | 基础篇:到底应该怎么理解“平均负载”?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0e/21/0e361d8dd3dee87c8e042f8315d11021.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
每次发现系统变慢时,我们通常做的第一件事,就是执行top或者uptime命令,来了解系统的负载情况。比如像下面这样,我在命令行里输入了uptime命令,系统也随即给出了结果。
|
||||
|
||||
```
|
||||
$ uptime
|
||||
02:34:03 up 2 days, 20:14, 1 user, load average: 0.63, 0.83, 0.88
|
||||
|
||||
```
|
||||
|
||||
但我想问的是,你真的知道这里每列输出的含义吗?
|
||||
|
||||
我相信你对前面的几列比较熟悉,它们分别是当前时间、系统运行时间以及正在登录用户数。
|
||||
|
||||
```
|
||||
02:34:03 //当前时间
|
||||
up 2 days, 20:14 //系统运行时间
|
||||
1 user //正在登录用户数
|
||||
|
||||
```
|
||||
|
||||
而最后三个数字呢,依次则是过去1分钟、5分钟、15分钟的平均负载(Load Average)。
|
||||
|
||||
**平均负载**?这个词对很多人来说,可能既熟悉又陌生,我们每天的工作中,也都会提到这个词,但你真正理解它背后的含义吗?如果你们团队来了一个实习生,他揪住你不放,你能给他讲清楚什么是平均负载吗?
|
||||
|
||||
其实,6年前,我就遇到过这样的一个场景。公司一个实习生一直追问我,什么是平均负载,我支支吾吾半天,最后也没能解释明白。明明总看到也总会用到,怎么就说不明白呢?后来我静下来想想,其实还是自己的功底不够。
|
||||
|
||||
于是,这几年,我遇到问题,特别是基础问题,都会多问自己几个“为什么”,以求能够彻底理解现象背后的本质原理,用起来更灵活,也更有底气。
|
||||
|
||||
今天,我就带你来学习下,如何观测和理解这个最常见、也是最重要的系统指标。
|
||||
|
||||
我猜一定有人会说,平均负载不就是单位时间内的 CPU 使用率吗?上面的0.63,就代表CPU使用率是63%。其实并不是这样,如果你方便的话,可以通过执行man uptime命令,来了解平均负载的详细解释。
|
||||
|
||||
简单来说,平均负载是指单位时间内,系统处于**可运行状态**和**不可中断状态**的平均进程数,也就是**平均活跃进程数**,它和CPU使用率并没有直接关系。这里我先解释下,可运行状态和不可中断状态这俩词儿。
|
||||
|
||||
所谓可运行状态的进程,是指正在使用CPU或者正在等待CPU的进程,也就是我们常用ps命令看到的,处于R状态(Running 或 Runnable)的进程。
|
||||
|
||||
不可中断状态的进程则是正处于内核态关键流程中的进程,并且这些流程是不可打断的,比如最常见的是等待硬件设备的I/O响应,也就是我们在ps命令中看到的D状态(Uninterruptible Sleep,也称为Disk Sleep)的进程。
|
||||
|
||||
比如,当一个进程向磁盘读写数据时,为了保证数据的一致性,在得到磁盘回复前,它是不能被其他进程或者中断打断的,这个时候的进程就处于不可中断状态。如果此时的进程被打断了,就容易出现磁盘数据与进程数据不一致的问题。
|
||||
|
||||
所以,不可中断状态实际上是系统对进程和硬件设备的一种保护机制。
|
||||
|
||||
因此,你可以简单理解为,平均负载其实就是平均活跃进程数。平均活跃进程数,直观上的理解就是单位时间内的活跃进程数,但它实际上是活跃进程数的指数衰减平均值。这个“指数衰减平均”的详细含义你不用计较,这只是系统的一种更快速的计算方式,你把它直接当成活跃进程数的平均值也没问题。
|
||||
|
||||
既然平均的是活跃进程数,那么最理想的,就是每个CPU上都刚好运行着一个进程,这样每个CPU都得到了充分利用。比如当平均负载为2时,意味着什么呢?
|
||||
|
||||
<li>
|
||||
在只有2个CPU的系统上,意味着所有的CPU都刚好被完全占用。
|
||||
</li>
|
||||
<li>
|
||||
在4个CPU的系统上,意味着CPU有50%的空闲。
|
||||
</li>
|
||||
<li>
|
||||
而在只有1个CPU的系统中,则意味着有一半的进程竞争不到CPU。
|
||||
</li>
|
||||
|
||||
## 平均负载为多少时合理
|
||||
|
||||
讲完了什么是平均负载,现在我们再回到最开始的例子,不知道你能否判断出,在 uptime 命令的结果里,那三个时间段的平均负载数,多大的时候能说明系统负载高?或是多小的时候就能说明系统负载很低呢?
|
||||
|
||||
我们知道,平均负载最理想的情况是等于 CPU个数。所以在评判平均负载时,**首先你要知道系统有几个 CPU**,这可以通过 top 命令或者从文件 /proc/cpuinfo 中读取,比如:
|
||||
|
||||
```
|
||||
# 关于grep和wc的用法请查询它们的手册或者网络搜索
|
||||
$ grep 'model name' /proc/cpuinfo | wc -l
|
||||
2
|
||||
|
||||
```
|
||||
|
||||
有了CPU 个数,我们就可以判断出,当平均负载比 CPU 个数还大的时候,系统已经出现了过载。
|
||||
|
||||
不过,且慢,新的问题又来了。我们在例子中可以看到,平均负载有三个数值,到底该参考哪一个呢?
|
||||
|
||||
实际上,都要看。三个不同时间间隔的平均值,其实给我们提供了,分析**系统负载趋势**的数据来源,让我们能更全面、更立体地理解目前的负载状况。
|
||||
|
||||
打个比方,就像初秋时北京的天气,如果只看中午的温度,你可能以为还在7月份的大夏天呢。但如果你结合了早上、中午、晚上三个时间点的温度来看,基本就可以全方位了解这一天的天气情况了。
|
||||
|
||||
同样的,前面说到的CPU的三个负载时间段也是这个道理。
|
||||
|
||||
<li>
|
||||
如果1分钟、5分钟、15分钟的三个值基本相同,或者相差不大,那就说明系统负载很平稳。
|
||||
</li>
|
||||
<li>
|
||||
但如果1分钟的值远小于15 分钟的值,就说明系统最近1分钟的负载在减少,而过去15分钟内却有很大的负载。
|
||||
</li>
|
||||
<li>
|
||||
反过来,如果1分钟的值远大于 15 分钟的值,就说明最近1分钟的负载在增加,这种增加有可能只是临时性的,也有可能还会持续增加下去,所以就需要持续观察。一旦1分钟的平均负载接近或超过了CPU的个数,就意味着系统正在发生过载的问题,这时就得分析调查是哪里导致的问题,并要想办法优化了。
|
||||
</li>
|
||||
|
||||
这里我再举个例子,假设我们在一个单 CPU 系统上看到平均负载为 1.73,0.60,7.98,那么说明在过去 1 分钟内,系统有 73% 的超载,而在 15 分钟内,有 698% 的超载,从整体趋势来看,系统的负载在降低。
|
||||
|
||||
那么,在实际生产环境中,平均负载多高时,需要我们重点关注呢?
|
||||
|
||||
在我看来,**当平均负载高于 CPU 数量70%的时候**,你就应该分析排查负载高的问题了。一旦负载过高,就可能导致进程响应变慢,进而影响服务的正常功能。
|
||||
|
||||
但70%这个数字并不是绝对的,最推荐的方法,还是把系统的平均负载监控起来,然后根据更多的历史数据,判断负载的变化趋势。当发现负载有明显升高趋势时,比如说负载翻倍了,你再去做分析和调查。
|
||||
|
||||
## 平均负载与CPU使用率
|
||||
|
||||
现实工作中,我们经常容易把平均负载和 CPU 使用率混淆,所以在这里,我也做一个区分。
|
||||
|
||||
可能你会疑惑,既然平均负载代表的是活跃进程数,那平均负载高了,不就意味着 CPU 使用率高吗?
|
||||
|
||||
我们还是要回到平均负载的含义上来,平均负载是指单位时间内,处于可运行状态和不可中断状态的进程数。所以,它不仅包括了**正在使用 CPU** 的进程,还包括**等待 CPU** 和**等待 I/O** 的进程。
|
||||
|
||||
而 CPU 使用率,是单位时间内 CPU 繁忙情况的统计,跟平均负载并不一定完全对应。比如:
|
||||
|
||||
<li>
|
||||
CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时这两者是一致的;
|
||||
</li>
|
||||
<li>
|
||||
I/O 密集型进程,等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高;
|
||||
</li>
|
||||
<li>
|
||||
大量等待 CPU 的进程调度也会导致平均负载升高,此时的CPU使用率也会比较高。
|
||||
</li>
|
||||
|
||||
## 平均负载案例分析
|
||||
|
||||
下面,我们以三个示例分别来看这三种情况,并用 iostat、mpstat、pidstat 等工具,找出平均负载升高的根源。
|
||||
|
||||
因为案例分析都是基于机器上的操作,所以不要只是听听、看看就够了,最好还是跟着我实际操作一下。
|
||||
|
||||
### 你的准备
|
||||
|
||||
下面的案例都是基于 Ubuntu 18.04,当然,同样适用于其他 Linux 系统。我使用的案例环境如下所示。
|
||||
|
||||
<li>
|
||||
机器配置:2 CPU,8GB 内存。
|
||||
</li>
|
||||
<li>
|
||||
预先安装 stress 和 sysstat 包,如 apt install stress sysstat。
|
||||
</li>
|
||||
|
||||
在这里,我先简单介绍一下 stress 和 sysstat。
|
||||
|
||||
stress 是一个 Linux 系统压力测试工具,这里我们用作异常进程模拟平均负载升高的场景。
|
||||
|
||||
而 sysstat 包含了常用的 Linux 性能工具,用来监控和分析系统的性能。我们的案例会用到这个包的两个命令 mpstat 和 pidstat。
|
||||
|
||||
<li>
|
||||
mpstat 是一个常用的多核 CPU 性能分析工具,用来实时查看每个 CPU 的性能指标,以及所有CPU的平均指标。
|
||||
</li>
|
||||
<li>
|
||||
pidstat 是一个常用的进程性能分析工具,用来实时查看进程的 CPU、内存、I/O 以及上下文切换等性能指标。
|
||||
</li>
|
||||
|
||||
此外,每个场景都需要你开三个终端,登录到同一台 Linux 机器中。
|
||||
|
||||
实验之前,你先做好上面的准备。如果包的安装有问题,可以先在Google一下自行解决,如果还是解决不了,再来留言区找我,这事儿应该不难。
|
||||
|
||||
另外要注意,下面的所有命令,我们都是默认以 root 用户运行。所以,如果你是用普通用户登陆的系统,一定要先运行 sudo su root 命令切换到 root 用户。
|
||||
|
||||
如果上面的要求都已经完成了,你可以先用 uptime 命令,看一下测试前的平均负载情况:
|
||||
|
||||
```
|
||||
$ uptime
|
||||
..., load average: 0.11, 0.15, 0.09
|
||||
|
||||
```
|
||||
|
||||
### 场景一:CPU 密集型进程
|
||||
|
||||
首先,我们在第一个终端运行 stress 命令,模拟一个 CPU 使用率 100% 的场景:
|
||||
|
||||
```
|
||||
$ stress --cpu 1 --timeout 600
|
||||
|
||||
```
|
||||
|
||||
接着,在第二个终端运行uptime查看平均负载的变化情况:
|
||||
|
||||
```
|
||||
# -d 参数表示高亮显示变化的区域
|
||||
$ watch -d uptime
|
||||
..., load average: 1.00, 0.75, 0.39
|
||||
|
||||
```
|
||||
|
||||
最后,在第三个终端运行mpstat查看 CPU 使用率的变化情况:
|
||||
|
||||
```
|
||||
# -P ALL 表示监控所有CPU,后面数字5表示间隔5秒后输出一组数据
|
||||
$ mpstat -P ALL 5
|
||||
Linux 4.15.0 (ubuntu) 09/22/18 _x86_64_ (2 CPU)
|
||||
13:30:06 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
|
||||
13:30:11 all 50.05 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 49.95
|
||||
13:30:11 0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
|
||||
13:30:11 1 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
|
||||
```
|
||||
|
||||
从终端二中可以看到,1 分钟的平均负载会慢慢增加到 1.00,而从终端三中还可以看到,正好有一个 CPU 的使用率为 100%,但它的 iowait 只有 0。这说明,平均负载的升高正是由于 CPU 使用率为 100% 。
|
||||
|
||||
那么,到底是哪个进程导致了 CPU 使用率为 100% 呢?你可以使用 pidstat 来查询:
|
||||
|
||||
```
|
||||
# 间隔5秒后输出一组数据
|
||||
$ pidstat -u 5 1
|
||||
13:37:07 UID PID %usr %system %guest %wait %CPU CPU Command
|
||||
13:37:12 0 2962 100.00 0.00 0.00 0.00 100.00 1 stress
|
||||
|
||||
```
|
||||
|
||||
从这里可以明显看到,stress进程的CPU使用率为100%。
|
||||
|
||||
### 场景二:I/O 密集型进程
|
||||
|
||||
首先还是运行 stress 命令,但这次模拟 I/O 压力,即不停地执行 sync:
|
||||
|
||||
```
|
||||
$ stress -i 1 --timeout 600
|
||||
|
||||
```
|
||||
|
||||
还是在第二个终端运行uptime查看平均负载的变化情况:
|
||||
|
||||
```
|
||||
$ watch -d uptime
|
||||
..., load average: 1.06, 0.58, 0.37
|
||||
|
||||
```
|
||||
|
||||
然后,第三个终端运行mpstat查看 CPU 使用率的变化情况:
|
||||
|
||||
```
|
||||
# 显示所有CPU的指标,并在间隔5秒输出一组数据
|
||||
$ mpstat -P ALL 5 1
|
||||
Linux 4.15.0 (ubuntu) 09/22/18 _x86_64_ (2 CPU)
|
||||
13:41:28 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
|
||||
13:41:33 all 0.21 0.00 12.07 32.67 0.00 0.21 0.00 0.00 0.00 54.84
|
||||
13:41:33 0 0.43 0.00 23.87 67.53 0.00 0.43 0.00 0.00 0.00 7.74
|
||||
13:41:33 1 0.00 0.00 0.81 0.20 0.00 0.00 0.00 0.00 0.00 98.99
|
||||
|
||||
```
|
||||
|
||||
从这里可以看到,1 分钟的平均负载会慢慢增加到 1.06,其中一个 CPU 的系统CPU使用率升高到了 23.87,而 iowait 高达 67.53%。这说明,平均负载的升高是由于 iowait 的升高。
|
||||
|
||||
那么到底是哪个进程,导致 iowait 这么高呢?我们还是用 pidstat 来查询:
|
||||
|
||||
```
|
||||
# 间隔5秒后输出一组数据,-u表示CPU指标
|
||||
$ pidstat -u 5 1
|
||||
Linux 4.15.0 (ubuntu) 09/22/18 _x86_64_ (2 CPU)
|
||||
13:42:08 UID PID %usr %system %guest %wait %CPU CPU Command
|
||||
13:42:13 0 104 0.00 3.39 0.00 0.00 3.39 1 kworker/1:1H
|
||||
13:42:13 0 109 0.00 0.40 0.00 0.00 0.40 0 kworker/0:1H
|
||||
13:42:13 0 2997 2.00 35.53 0.00 3.99 37.52 1 stress
|
||||
13:42:13 0 3057 0.00 0.40 0.00 0.00 0.40 0 pidstat
|
||||
|
||||
```
|
||||
|
||||
可以发现,还是 stress 进程导致的。
|
||||
|
||||
### 场景三:大量进程的场景
|
||||
|
||||
当系统中运行进程超出 CPU 运行能力时,就会出现等待 CPU 的进程。
|
||||
|
||||
比如,我们还是使用 stress,但这次模拟的是 8 个进程:
|
||||
|
||||
```
|
||||
$ stress -c 8 --timeout 600
|
||||
|
||||
```
|
||||
|
||||
由于系统只有 2 个CPU,明显比 8 个进程要少得多,因而,系统的 CPU 处于严重过载状态,平均负载高达7.97:
|
||||
|
||||
```
|
||||
$ uptime
|
||||
..., load average: 7.97, 5.93, 3.02
|
||||
|
||||
```
|
||||
|
||||
接着再运行pidstat来看一下进程的情况:
|
||||
|
||||
```
|
||||
# 间隔5秒后输出一组数据
|
||||
$ pidstat -u 5 1
|
||||
14:23:25 UID PID %usr %system %guest %wait %CPU CPU Command
|
||||
14:23:30 0 3190 25.00 0.00 0.00 74.80 25.00 0 stress
|
||||
14:23:30 0 3191 25.00 0.00 0.00 75.20 25.00 0 stress
|
||||
14:23:30 0 3192 25.00 0.00 0.00 74.80 25.00 1 stress
|
||||
14:23:30 0 3193 25.00 0.00 0.00 75.00 25.00 1 stress
|
||||
14:23:30 0 3194 24.80 0.00 0.00 74.60 24.80 0 stress
|
||||
14:23:30 0 3195 24.80 0.00 0.00 75.00 24.80 0 stress
|
||||
14:23:30 0 3196 24.80 0.00 0.00 74.60 24.80 1 stress
|
||||
14:23:30 0 3197 24.80 0.00 0.00 74.80 24.80 1 stress
|
||||
14:23:30 0 3200 0.00 0.20 0.00 0.20 0.20 0 pidstat
|
||||
|
||||
```
|
||||
|
||||
可以看出,8 个进程在争抢 2 个 CPU,每个进程等待 CPU 的时间(也就是代码块中的 %wait 列)高达 75%。这些超出 CPU 计算能力的进程,最终导致 CPU 过载。
|
||||
|
||||
## 小结
|
||||
|
||||
分析完这三个案例,我再来归纳一下平均负载的理解。
|
||||
|
||||
平均负载提供了一个快速查看系统整体性能的手段,反映了整体的负载情况。但只看平均负载本身,我们并不能直接发现,到底是哪里出现了瓶颈。所以,在理解平均负载时,也要注意:
|
||||
|
||||
<li>
|
||||
平均负载高有可能是 CPU 密集型进程导致的;
|
||||
</li>
|
||||
<li>
|
||||
平均负载高并不一定代表 CPU 使用率高,还有可能是 I/O 更繁忙了;
|
||||
</li>
|
||||
<li>
|
||||
当发现负载高的时候,你可以使用 mpstat、pidstat 等工具,辅助分析负载的来源。
|
||||
</li>
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想邀请你一起来聊聊你所理解的平均负载,当你发现平均负载升高后,又是怎么分析排查的呢?你可以结合我前面的讲解,来总结自己的思考。欢迎在留言区和我讨论。
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/00/f2/00f868b7654dcb50ae2c91fd7688d2f2.jpg" alt="unpreview">](time://mall?url=https%3A%2F%2Fj.youzan.com%2F7_vgDi)<br>
|
||||
限量发售中,仅限5000份,3大体系,22个模块,定位工作中80%的高频问题。
|
||||
156
极客时间专栏/Linux性能优化实战/CPU 性能篇/03 | 基础篇:经常说的 CPU 上下文切换是什么意思?(上).md
Normal file
156
极客时间专栏/Linux性能优化实战/CPU 性能篇/03 | 基础篇:经常说的 CPU 上下文切换是什么意思?(上).md
Normal file
@@ -0,0 +1,156 @@
|
||||
<audio id="audio" title="03 | 基础篇:经常说的 CPU 上下文切换是什么意思?(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/be/3a/bea8e1df9d4cbddc0b66fc753e8cee3a.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节,我给你讲了要怎么理解平均负载( Load Average),并用三个案例展示了不同场景下平均负载升高的分析方法。这其中,多个进程竞争 CPU 就是一个经常被我们忽视的问题。
|
||||
|
||||
我想你一定很好奇,进程在竞争 CPU 的时候并没有真正运行,为什么还会导致系统的负载升高呢?看到今天的主题,你应该已经猜到了,CPU 上下文切换就是罪魁祸首。
|
||||
|
||||
我们都知道,Linux 是一个多任务操作系统,它支持远大于 CPU 数量的任务同时运行。当然,这些任务实际上并不是真的在同时运行,而是因为系统在很短的时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉。
|
||||
|
||||
而在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统事先帮它设置好 **CPU 寄存器和程序计数器**(Program Counter,PC)。
|
||||
|
||||
CPU 寄存器,是 CPU 内置的容量小、但速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此也被叫做 **CPU 上下文**。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/98/5f/98ac9df2593a193d6a7f1767cd68eb5f.png" alt="">
|
||||
|
||||
知道了什么是 CPU 上下文,我想你也很容易理解 **CPU 上下文切换**。CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
|
||||
|
||||
而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
|
||||
|
||||
我猜肯定会有人说,CPU 上下文切换无非就是更新了 CPU 寄存器的值嘛,但这些寄存器,本身就是为了快速运行任务而设计的,为什么会影响系统的 CPU 性能呢?
|
||||
|
||||
在回答这个问题前,不知道你有没有想过,操作系统管理的这些“任务”到底是什么呢?
|
||||
|
||||
也许你会说,任务就是进程,或者说任务就是线程。是的,进程和线程正是最常见的任务。但是除此之外,还有没有其他的任务呢?
|
||||
|
||||
不要忘了,硬件通过触发信号,会导致中断处理程序的调用,也是一种常见的任务。
|
||||
|
||||
所以,根据任务的不同,CPU 的上下文切换就可以分为几个不同的场景,也就是**进程上下文切换**、**线程上下文切换**以及**中断上下文切换**。
|
||||
|
||||
这节课我就带你来看看,怎么理解这几个不同的上下文切换,以及它们为什么会引发 CPU 性能相关问题。
|
||||
|
||||
## 进程上下文切换
|
||||
|
||||
Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中, CPU 特权等级的 Ring 0 和 Ring 3。
|
||||
|
||||
<li>
|
||||
内核空间(Ring 0)具有最高权限,可以直接访问所有资源;
|
||||
</li>
|
||||
<li>
|
||||
用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4d/a7/4d3f622f272c49132ecb9760310ce1a7.png" alt="">
|
||||
|
||||
换个角度看,也就是说,进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。
|
||||
|
||||
从用户态到内核态的转变,需要通过**系统调用**来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。
|
||||
|
||||
那么,系统调用的过程有没有发生 CPU 上下文的切换呢?答案自然是肯定的。
|
||||
|
||||
CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。
|
||||
|
||||
而系统调用结束后,CPU寄存器需要**恢复**原来保存的用户态,然后再切换到用户空间,继续运行进程。所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。
|
||||
|
||||
不过,需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。这跟我们通常所说的进程上下文切换是不一样的:
|
||||
|
||||
<li>
|
||||
进程上下文切换,是指从一个进程切换到另一个进程运行。
|
||||
</li>
|
||||
<li>
|
||||
而系统调用过程中一直是同一个进程在运行。
|
||||
</li>
|
||||
|
||||
所以,**系统调用过程通常称为特权模式切换,而不是上下文切换**。但实际上,系统调用过程中,CPU 的上下文切换还是无法避免的。
|
||||
|
||||
那么,进程上下文切换跟系统调用又有什么区别呢?
|
||||
|
||||
首先,你需要知道,进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
|
||||
|
||||
因此,进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和CPU寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
|
||||
|
||||
如下图所示,保存上下文和恢复上下文的过程并不是“免费”的,需要内核在 CPU 上运行才能完成。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/39/6b/395666667d77e718da63261be478a96b.png" alt="">
|
||||
|
||||
根据[ Tsuna ](https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html)的测试报告,每次上下文切换都需要几十纳秒到数微秒的 CPU 时间。这个时间还是相当可观的,特别是在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。这也正是上一节中我们所讲的,导致平均负载升高的一个重要因素。
|
||||
|
||||
另外,我们知道, Linux 通过 TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是在多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。
|
||||
|
||||
知道了进程上下文切换潜在的性能问题后,我们再来看,究竟什么时候会切换进程上下文。
|
||||
|
||||
显然,进程切换时才需要切换上下文,换句话说,只有在进程调度的时候,才需要切换上下文。Linux 为每个 CPU 都维护了一个就绪队列,将活跃进程(即正在运行和正在等待CPU的进程)按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最高和等待CPU时间最长的进程来运行。
|
||||
|
||||
那么,进程在什么时候才会被调度到 CPU 上运行呢?
|
||||
|
||||
最容易想到的一个时机,就是进程执行完终止了,它之前使用的CPU会释放出来,这个时候再从就绪队列里,拿一个新的进程过来运行。其实还有很多其他场景,也会触发进程调度,在这里我给你逐个梳理下。
|
||||
|
||||
其一,为了保证所有进程可以得到公平调度,CPU时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行。
|
||||
|
||||
其二,进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。
|
||||
|
||||
其三,当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。
|
||||
|
||||
其四,当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。
|
||||
|
||||
最后一个,发生硬件中断时,CPU上的进程会被中断挂起,转而执行内核中的中断服务程序。
|
||||
|
||||
了解这几个场景是非常有必要的,因为一旦出现上下文切换的性能问题,它们就是幕后凶手。
|
||||
|
||||
## 线程上下文切换
|
||||
|
||||
说完了进程的上下文切换,我们再来看看线程相关的问题。
|
||||
|
||||
线程与进程最大的区别在于,**线程是调度的基本单位,而进程则是资源拥有的基本单位**。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。所以,对于线程和进程,我们可以这么理解:
|
||||
|
||||
<li>
|
||||
当进程只有一个线程时,可以认为进程就等于线程。
|
||||
</li>
|
||||
<li>
|
||||
当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
|
||||
</li>
|
||||
<li>
|
||||
另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
|
||||
</li>
|
||||
|
||||
这么一来,线程的上下文切换其实就可以分为两种情况:
|
||||
|
||||
第一种, 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
|
||||
|
||||
第二种,前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
|
||||
|
||||
到这里你应该也发现了,虽然同为上下文切换,但同进程内的线程切换,要比多进程间的切换消耗更少的资源,而这,也正是多线程代替多进程的一个优势。
|
||||
|
||||
## 中断上下文切换
|
||||
|
||||
除了前面两种上下文切换,还有一个场景也会切换 CPU 上下文,那就是中断。
|
||||
|
||||
为了快速响应硬件的事件,**中断处理会打断进程的正常调度和执行**,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。
|
||||
|
||||
跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括CPU 寄存器、内核堆栈、硬件中断参数等。
|
||||
|
||||
**对同一个 CPU 来说,中断处理比进程拥有更高的优先级**,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。
|
||||
|
||||
另外,跟进程上下文切换一样,中断上下文切换也需要消耗CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。所以,当你发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。
|
||||
|
||||
## 小结
|
||||
|
||||
总结一下,不管是哪种场景导致的上下文切换,你都应该知道:
|
||||
|
||||
<li>
|
||||
CPU 上下文切换,是保证 Linux 系统正常工作的核心功能之一,一般情况下不需要我们特别关注。
|
||||
</li>
|
||||
<li>
|
||||
但过多的上下文切换,会把CPU时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,从而缩短进程真正运行的时间,导致系统的整体性能大幅下降。
|
||||
</li>
|
||||
|
||||
今天主要为你介绍这几种上下文切换的工作原理,下一节,我将继续案例实战,说说上下文切换问题的分析方法。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想邀请你一起来聊聊,你所理解的 CPU 上下文切换。你可以结合今天的内容,总结自己的思路和看法,写下你的学习心得。
|
||||
|
||||
欢迎在留言区和我讨论。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/56/52/565d66d658ad23b2f4997551db153852.jpg" alt="">
|
||||
250
极客时间专栏/Linux性能优化实战/CPU 性能篇/04 | 基础篇:经常说的 CPU 上下文切换是什么意思?(下).md
Normal file
250
极客时间专栏/Linux性能优化实战/CPU 性能篇/04 | 基础篇:经常说的 CPU 上下文切换是什么意思?(下).md
Normal file
@@ -0,0 +1,250 @@
|
||||
<audio id="audio" title="04 | 基础篇:经常说的 CPU 上下文切换是什么意思?(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/68/58/68653d49fda5636f3cdd6063f2accf58.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节,我给你讲了CPU上下文切换的工作原理。简单回顾一下,CPU 上下文切换是保证 Linux 系统正常工作的一个核心功能,按照不同场景,可以分为进程上下文切换、线程上下文切换和中断上下文切换。具体的概念和区别,你也要在脑海中过一遍,忘了的话及时查看上一篇。
|
||||
|
||||
今天我们就接着来看,究竟怎么分析CPU上下文切换的问题。
|
||||
|
||||
## 怎么查看系统的上下文切换情况
|
||||
|
||||
通过前面学习我们知道,过多的上下文切换,会把CPU 时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,缩短进程真正运行的时间,成了系统性能大幅下降的一个元凶。
|
||||
|
||||
既然上下文切换对系统性能影响那么大,你肯定迫不及待想知道,到底要怎么查看上下文切换呢?在这里,我们可以使用 vmstat 这个工具,来查询系统的上下文切换情况。
|
||||
|
||||
vmstat 是一个常用的系统性能分析工具,主要用来分析系统的内存使用情况,也常用来分析 CPU 上下文切换和中断的次数。
|
||||
|
||||
比如,下面就是一个 vmstat 的使用示例:
|
||||
|
||||
```
|
||||
# 每隔5秒输出1组数据
|
||||
$ vmstat 5
|
||||
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 7005360 91564 818900 0 0 0 0 25 33 0 0 100 0 0
|
||||
|
||||
```
|
||||
|
||||
我们一起来看这个结果,你可以先试着自己解读每列的含义。在这里,我重点强调下,需要特别关注的四列内容:
|
||||
|
||||
<li>
|
||||
cs(context switch)是每秒上下文切换的次数。
|
||||
</li>
|
||||
<li>
|
||||
in(interrupt)则是每秒中断的次数。
|
||||
</li>
|
||||
<li>
|
||||
r(Running or Runnable)是就绪队列的长度,也就是正在运行和等待CPU的进程数。
|
||||
</li>
|
||||
<li>
|
||||
b(Blocked)则是处于不可中断睡眠状态的进程数。
|
||||
</li>
|
||||
|
||||
可以看到,这个例子中的上下文切换次数 cs 是33次,而系统中断次数 in 则是25次,而就绪队列长度r和不可中断状态进程数b都是0。
|
||||
|
||||
vmstat 只给出了系统总体的上下文切换情况,要想查看每个进程的详细情况,就需要使用我们前面提到过的 pidstat 了。给它加上 -w 选项,你就可以查看每个进程上下文切换的情况了。
|
||||
|
||||
比如说:
|
||||
|
||||
```
|
||||
# 每隔5秒输出1组数据
|
||||
$ pidstat -w 5
|
||||
Linux 4.15.0 (ubuntu) 09/23/18 _x86_64_ (2 CPU)
|
||||
|
||||
08:18:26 UID PID cswch/s nvcswch/s Command
|
||||
08:18:31 0 1 0.20 0.00 systemd
|
||||
08:18:31 0 8 5.40 0.00 rcu_sched
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
这个结果中有两列内容是我们的重点关注对象。一个是 cswch ,表示每秒自愿上下文切换(voluntary context switches)的次数,另一个则是 nvcswch ,表示每秒非自愿上下文切换(non voluntary context switches)的次数。
|
||||
|
||||
这两个概念你一定要牢牢记住,因为它们意味着不同的性能问题:
|
||||
|
||||
<li>
|
||||
所谓**自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换**。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
|
||||
</li>
|
||||
<li>
|
||||
而**非自愿上下文切换,则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换**。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。
|
||||
</li>
|
||||
|
||||
## 案例分析
|
||||
|
||||
知道了怎么查看这些指标,另一个问题又来了,上下文切换频率是多少次才算正常呢?别急着要答案,同样的,我们先来看一个上下文切换的案例。通过案例实战演练,你自己就可以分析并找出这个标准了。
|
||||
|
||||
### 你的准备
|
||||
|
||||
今天的案例,我们将使用 sysbench 来模拟系统多线程调度切换的情况。
|
||||
|
||||
sysbench 是一个多线程的基准测试工具,一般用来评估不同系统参数下的数据库负载情况。当然,在这次案例中,我们只把它当成一个异常进程来看,作用是模拟上下文切换过多的问题。
|
||||
|
||||
下面的案例基于 Ubuntu 18.04,当然,其他的 Linux 系统同样适用。我使用的案例环境如下所示:
|
||||
|
||||
<li>
|
||||
机器配置:2 CPU,8GB 内存
|
||||
</li>
|
||||
<li>
|
||||
预先安装 sysbench 和 sysstat 包,如 apt install sysbench sysstat
|
||||
</li>
|
||||
|
||||
正式操作开始前,你需要打开三个终端,登录到同一台 Linux 机器中,并安装好上面提到的两个软件包。包的安装,可以先Google一下自行解决,如果仍然有问题的,在留言区写下你的情况。
|
||||
|
||||
另外注意,下面所有命令,都**默认以 root 用户运行**。所以,如果你是用普通用户登陆的系统,记住先运行 sudo su root 命令切换到 root 用户。
|
||||
|
||||
安装完成后,你可以先用 vmstat 看一下空闲系统的上下文切换次数:
|
||||
|
||||
```
|
||||
# 间隔1秒后输出1组数据
|
||||
$ 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 st
|
||||
0 0 0 6984064 92668 830896 0 0 2 19 19 35 1 0 99 0 0
|
||||
|
||||
```
|
||||
|
||||
这里你可以看到,现在的上下文切换次数 cs 是35,而中断次数 in 是19,r和b都是0。因为这会儿我并没有运行其他任务,所以它们就是空闲系统的上下文切换次数。
|
||||
|
||||
### 操作和分析
|
||||
|
||||
接下来,我们正式进入实战操作。
|
||||
|
||||
首先,在第一个终端里运行 sysbench ,模拟系统多线程调度的瓶颈:
|
||||
|
||||
```
|
||||
# 以10个线程运行5分钟的基准测试,模拟多线程切换的问题
|
||||
$ sysbench --threads=10 --max-time=300 threads run
|
||||
|
||||
```
|
||||
|
||||
接着,在第二个终端运行 vmstat ,观察上下文切换情况:
|
||||
|
||||
```
|
||||
# 每隔1秒输出1组数据(需要Ctrl+C才结束)
|
||||
$ vmstat 1
|
||||
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
|
||||
r b swpd free buff cache si so bi bo in cs us sy id wa st
|
||||
6 0 0 6487428 118240 1292772 0 0 0 0 9019 1398830 16 84 0 0 0
|
||||
8 0 0 6487428 118240 1292772 0 0 0 0 10191 1392312 16 84 0 0 0
|
||||
|
||||
```
|
||||
|
||||
你应该可以发现,cs 列的上下文切换次数从之前的 35 骤然上升到了 139 万。同时,注意观察其他几个指标:
|
||||
|
||||
<li>
|
||||
r 列:就绪队列的长度已经到了 8,远远超过了系统 CPU 的个数 2,所以肯定会有大量的 CPU 竞争。
|
||||
</li>
|
||||
<li>
|
||||
us(user)和 sy(system)列:这两列的CPU 使用率加起来上升到了 100%,其中系统 CPU 使用率,也就是 sy 列高达 84%,说明 CPU 主要是被内核占用了。
|
||||
</li>
|
||||
<li>
|
||||
in 列:中断次数也上升到了1万左右,说明中断处理也是个潜在的问题。
|
||||
</li>
|
||||
|
||||
综合这几个指标,我们可以知道,系统的就绪队列过长,也就是正在运行和等待CPU的进程数过多,导致了大量的上下文切换,而上下文切换又导致了系统 CPU 的占用率升高。
|
||||
|
||||
那么到底是什么进程导致了这些问题呢?
|
||||
|
||||
我们继续分析,在第三个终端再用 pidstat 来看一下, CPU 和进程上下文切换的情况:
|
||||
|
||||
```
|
||||
# 每隔1秒输出1组数据(需要 Ctrl+C 才结束)
|
||||
# -w参数表示输出进程切换指标,而-u参数则表示输出CPU使用指标
|
||||
$ pidstat -w -u 1
|
||||
08:06:33 UID PID %usr %system %guest %wait %CPU CPU Command
|
||||
08:06:34 0 10488 30.00 100.00 0.00 0.00 100.00 0 sysbench
|
||||
08:06:34 0 26326 0.00 1.00 0.00 0.00 1.00 0 kworker/u4:2
|
||||
|
||||
08:06:33 UID PID cswch/s nvcswch/s Command
|
||||
08:06:34 0 8 11.00 0.00 rcu_sched
|
||||
08:06:34 0 16 1.00 0.00 ksoftirqd/1
|
||||
08:06:34 0 471 1.00 0.00 hv_balloon
|
||||
08:06:34 0 1230 1.00 0.00 iscsid
|
||||
08:06:34 0 4089 1.00 0.00 kworker/1:5
|
||||
08:06:34 0 4333 1.00 0.00 kworker/0:3
|
||||
08:06:34 0 10499 1.00 224.00 pidstat
|
||||
08:06:34 0 26326 236.00 0.00 kworker/u4:2
|
||||
08:06:34 1000 26784 223.00 0.00 sshd
|
||||
|
||||
```
|
||||
|
||||
从pidstat的输出你可以发现,CPU 使用率的升高果然是 sysbench 导致的,它的 CPU 使用率已经达到了 100%。但上下文切换则是来自其他进程,包括非自愿上下文切换频率最高的 pidstat ,以及自愿上下文切换频率最高的内核线程 kworker 和 sshd。
|
||||
|
||||
不过,细心的你肯定也发现了一个怪异的事儿:pidstat 输出的上下文切换次数,加起来也就几百,比 vmstat 的 139 万明显小了太多。这是怎么回事呢?难道是工具本身出了错吗?
|
||||
|
||||
别着急,在怀疑工具之前,我们再来回想一下,前面讲到的几种上下文切换场景。其中有一点提到, Linux 调度的基本单位实际上是线程,而我们的场景 sysbench 模拟的也是线程的调度问题,那么,是不是 pidstat 忽略了线程的数据呢?
|
||||
|
||||
通过运行 man pidstat ,你会发现,pidstat 默认显示进程的指标数据,加上 -t 参数后,才会输出线程的指标。
|
||||
|
||||
所以,我们可以在第三个终端里, Ctrl+C 停止刚才的 pidstat 命令,再加上 -t 参数,重试一下看看:
|
||||
|
||||
```
|
||||
# 每隔1秒输出一组数据(需要 Ctrl+C 才结束)
|
||||
# -wt 参数表示输出线程的上下文切换指标
|
||||
$ pidstat -wt 1
|
||||
08:14:05 UID TGID TID cswch/s nvcswch/s Command
|
||||
...
|
||||
08:14:05 0 10551 - 6.00 0.00 sysbench
|
||||
08:14:05 0 - 10551 6.00 0.00 |__sysbench
|
||||
08:14:05 0 - 10552 18911.00 103740.00 |__sysbench
|
||||
08:14:05 0 - 10553 18915.00 100955.00 |__sysbench
|
||||
08:14:05 0 - 10554 18827.00 103954.00 |__sysbench
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
现在你就能看到了,虽然 sysbench 进程(也就是主线程)的上下文切换次数看起来并不多,但它的子线程的上下文切换次数却有很多。看来,上下文切换罪魁祸首,还是过多的 sysbench 线程。
|
||||
|
||||
我们已经找到了上下文切换次数增多的根源,那是不是到这儿就可以结束了呢?
|
||||
|
||||
当然不是。不知道你还记不记得,前面在观察系统指标时,除了上下文切换频率骤然升高,还有一个指标也有很大的变化。是的,正是中断次数。中断次数也上升到了1万,但到底是什么类型的中断上升了,现在还不清楚。我们接下来继续抽丝剥茧找源头。
|
||||
|
||||
既然是中断,我们都知道,它只发生在内核态,而 pidstat 只是一个进程的性能分析工具,并不提供任何关于中断的详细信息,怎样才能知道中断发生的类型呢?
|
||||
|
||||
没错,那就是从 /proc/interrupts 这个只读文件中读取。/proc 实际上是 Linux 的一个虚拟文件系统,用于内核空间与用户空间之间的通信。/proc/interrupts 就是这种通信机制的一部分,提供了一个只读的中断使用情况。
|
||||
|
||||
我们还是在第三个终端里, Ctrl+C 停止刚才的 pidstat 命令,然后运行下面的命令,观察中断的变化情况:
|
||||
|
||||
```
|
||||
# -d 参数表示高亮显示变化的区域
|
||||
$ watch -d cat /proc/interrupts
|
||||
CPU0 CPU1
|
||||
...
|
||||
RES: 2450431 5279697 Rescheduling interrupts
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
观察一段时间,你可以发现,变化速度最快的是**重调度中断**(RES),这个中断类型表示,唤醒空闲状态的 CPU 来调度新的任务运行。这是多处理器系统(SMP)中,调度器用来分散任务到不同 CPU 的机制,通常也被称为**处理器间中断**(Inter-Processor Interrupts,IPI)。
|
||||
|
||||
所以,这里的中断升高还是因为过多任务的调度问题,跟前面上下文切换次数的分析结果是一致的。
|
||||
|
||||
通过这个案例,你应该也发现了多工具、多方面指标对比观测的好处。如果最开始时,我们只用了 pidstat 观测,这些很严重的上下文切换线程,压根儿就发现不了了。
|
||||
|
||||
现在再回到最初的问题,每秒上下文切换多少次才算正常呢?
|
||||
|
||||
**这个数值其实取决于系统本身的 CPU 性能**。在我看来,如果系统的上下文切换次数比较稳定,那么从数百到一万以内,都应该算是正常的。但当上下文切换次数超过一万次,或者切换次数出现数量级的增长时,就很可能已经出现了性能问题。
|
||||
|
||||
这时,你还需要根据上下文切换的类型,再做具体分析。比方说:
|
||||
|
||||
<li>
|
||||
自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题;
|
||||
</li>
|
||||
<li>
|
||||
非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈;
|
||||
</li>
|
||||
<li>
|
||||
中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型。
|
||||
</li>
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我通过一个sysbench的案例,给你讲了上下文切换问题的分析思路。碰到上下文切换次数过多的问题时,**我们可以借助 vmstat 、 pidstat 和 /proc/interrupts 等工具**,来辅助排查性能问题的根源。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想请你一起来聊聊,你之前是怎么分析和排查上下文切换问题的。你可以结合这两节的内容和你自己的实际操作,来总结自己的思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中学习。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/56/52/565d66d658ad23b2f4997551db153852.jpg" alt="">
|
||||
@@ -0,0 +1,420 @@
|
||||
<audio id="audio" title="05 | 基础篇:某个应用的CPU使用率居然达到100%,我该怎么办?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5e/fc/5e4f576bc1f518a5a5fad90bf13cc1fc.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
通过前两节对平均负载和 CPU 上下文切换的学习,我相信你对 CPU 的性能已经有了初步了解。不过我还是想问一下,在学这个专栏前,你最常用什么指标来描述系统的 CPU 性能呢?我想你的答案,可能不是平均负载,也不是 CPU 上下文切换,而是另一个更直观的指标—— CPU 使用率。
|
||||
|
||||
我们前面说过,CPU 使用率是单位时间内 CPU 使用情况的统计,以百分比的方式展示。那么,作为最常用也是最熟悉的 CPU 指标,你能说出 CPU 使用率到底是怎么算出来的吗?再有,诸如 top、ps 之类的性能工具展示的 %user、%nice、 %system、%iowait 、%steal 等等,你又能弄清楚它们之间的不同吗?
|
||||
|
||||
今天我就带你了解 CPU 使用率的内容,同时,我也会以我们最常用的反向代理服务器 Nginx 为例,带你在一步步操作和分析中深入理解。
|
||||
|
||||
## CPU 使用率
|
||||
|
||||
在上一期我曾提到,Linux 作为一个多任务操作系统,将每个 CPU 的时间划分为很短的时间片,再通过调度器轮流分配给各个任务使用,因此造成多任务同时运行的错觉。
|
||||
|
||||
为了维护 CPU 时间,Linux 通过事先定义的节拍率(内核中表示为 HZ),触发时间中断,并使用全局变量 Jiffies 记录了开机以来的节拍数。每发生一次时间中断,Jiffies 的值就加 1。
|
||||
|
||||
节拍率 HZ 是内核的可配选项,可以设置为 100、250、1000 等。不同的系统可能设置不同数值,你可以通过查询 /boot/config 内核选项来查看它的配置值。比如在我的系统中,节拍率设置成了 250,也就是每秒钟触发 250 次时间中断。
|
||||
|
||||
```
|
||||
$ grep 'CONFIG_HZ=' /boot/config-$(uname -r)
|
||||
CONFIG_HZ=250
|
||||
|
||||
```
|
||||
|
||||
同时,正因为节拍率 HZ 是内核选项,所以用户空间程序并不能直接访问。为了方便用户空间程序,内核还提供了一个用户空间节拍率 USER_HZ,它总是固定为 100,也就是1/100秒。这样,用户空间程序并不需要关心内核中 HZ 被设置成了多少,因为它看到的总是固定值 USER_HZ。
|
||||
|
||||
Linux 通过 /proc 虚拟文件系统,向用户空间提供了系统内部状态的信息,而 /proc/stat 提供的就是系统的 CPU 和任务统计信息。比方说,如果你只关注 CPU 的话,可以执行下面的命令:
|
||||
|
||||
```
|
||||
# 只保留各个CPU的数据
|
||||
$ cat /proc/stat | grep ^cpu
|
||||
cpu 280580 7407 286084 172900810 83602 0 583 0 0 0
|
||||
cpu0 144745 4181 176701 86423902 52076 0 301 0 0 0
|
||||
cpu1 135834 3226 109383 86476907 31525 0 282 0 0 0
|
||||
|
||||
```
|
||||
|
||||
这里的输出结果是一个表格。其中,第一列表示的是 CPU 编号,如cpu0、cpu1 ,而第一行没有编号的 cpu ,表示的是所有 CPU 的累加。其他列则表示不同场景下 CPU 的累加节拍数,它的单位是 USER_HZ,也就是 10 ms(1/100秒),所以这其实就是不同场景下的 CPU 时间。
|
||||
|
||||
当然,这里每一列的顺序并不需要你背下来。你只要记住,有需要的时候,查询 man proc 就可以。不过,你要清楚man proc文档里每一列的涵义,它们都是CPU使用率相关的重要指标,你还会在很多其他的性能工具中看到它们。下面,我来依次解读一下。
|
||||
|
||||
<li>
|
||||
user(通常缩写为 us),代表用户态 CPU 时间。注意,它不包括下面的 nice 时间,但包括了 guest 时间。
|
||||
</li>
|
||||
<li>
|
||||
nice(通常缩写为 ni),代表低优先级用户态 CPU 时间,也就是进程的 nice 值被调整为 1-19 之间时的 CPU 时间。这里注意,nice 可取值范围是 -20 到 19,数值越大,优先级反而越低。
|
||||
</li>
|
||||
<li>
|
||||
system(通常缩写为sys),代表内核态 CPU 时间。
|
||||
</li>
|
||||
<li>
|
||||
idle(通常缩写为id),代表空闲时间。注意,它不包括等待 I/O 的时间(iowait)。
|
||||
</li>
|
||||
<li>
|
||||
iowait(通常缩写为 wa),代表等待 I/O 的 CPU 时间。
|
||||
</li>
|
||||
<li>
|
||||
irq(通常缩写为 hi),代表处理硬中断的 CPU 时间。
|
||||
</li>
|
||||
<li>
|
||||
softirq(通常缩写为 si),代表处理软中断的 CPU 时间。
|
||||
</li>
|
||||
<li>
|
||||
steal(通常缩写为 st),代表当系统运行在虚拟机中的时候,被其他虚拟机占用的 CPU 时间。
|
||||
</li>
|
||||
<li>
|
||||
guest(通常缩写为 guest),代表通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的 CPU 时间。
|
||||
</li>
|
||||
<li>
|
||||
guest_nice(通常缩写为 gnice),代表以低优先级运行虚拟机的时间。
|
||||
</li>
|
||||
|
||||
而我们通常所说的 **CPU 使用率,就是除了空闲时间外的其他时间占总 CPU 时间的百分比**,用公式来表示就是:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3e/09/3edcc7f908c7c1ddba4bbcccc0277c09.png" alt=""><br>
|
||||
根据这个公式,我们就可以从 /proc/stat 中的数据,很容易地计算出 CPU 使用率。当然,也可以用每一个场景的CPU时间,除以总的CPU时间,计算出每个场景的CPU使用率。
|
||||
|
||||
不过先不要着急计算,你能说出,直接用 /proc/stat 的数据,算的是什么时间段的 CPU 使用率吗?
|
||||
|
||||
看到这里,你应该想起来了,这是开机以来的节拍数累加值,所以直接算出来的,是开机以来的平均 CPU 使用率,一般没啥参考价值。
|
||||
|
||||
事实上,为了计算 CPU 使用率,性能工具一般都会取间隔一段时间(比如3秒)的两次值,作差后,再计算出这段时间内的平均 CPU 使用率,即
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/84/5a/8408bb45922afb2db09629a9a7eb1d5a.png" alt="">
|
||||
|
||||
这个公式,就是我们用各种性能工具所看到的CPU 使用率的实际计算方法。
|
||||
|
||||
现在,我们知道了系统 CPU 使用率的计算方法,那进程的呢?跟系统的指标类似,Linux 也给每个进程提供了运行情况的统计信息,也就是 /proc/[pid]/stat。不过,这个文件包含的数据就比较丰富了,总共有 52 列的数据。
|
||||
|
||||
当然,不用担心,因为你并不需要掌握每一列的含义。还是那句话,需要的时候,查 man proc 就行。
|
||||
|
||||
回过头来看,是不是说要查看 CPU 使用率,就必须先读取 /proc/stat 和 /proc/[pid]/stat 这两个文件,然后再按照上面的公式计算出来呢?
|
||||
|
||||
当然不是,各种各样的性能分析工具已经帮我们计算好了。不过要注意的是,**性能分析工具给出的都是间隔一段时间的平均 CPU 使用率,所以要注意间隔时间的设置**,特别是用多个工具对比分析时,你一定要保证它们用的是相同的间隔时间。
|
||||
|
||||
比如,对比一下 top 和 ps 这两个工具报告的 CPU 使用率,默认的结果很可能不一样,因为 top 默认使用 3 秒时间间隔,而 ps 使用的却是进程的整个生命周期。
|
||||
|
||||
## 怎么查看 CPU 使用率
|
||||
|
||||
知道了 CPU 使用率的含义后,我们再来看看要怎么查看 CPU 使用率。说到查看 CPU 使用率的工具,我猜你第一反应肯定是 top 和 ps。的确,top 和 ps 是最常用的性能分析工具:
|
||||
|
||||
<li>
|
||||
top 显示了系统总体的 CPU 和内存使用情况,以及各个进程的资源使用情况。
|
||||
</li>
|
||||
<li>
|
||||
ps 则只显示了每个进程的资源使用情况。
|
||||
</li>
|
||||
|
||||
比如,top 的输出格式为:
|
||||
|
||||
```
|
||||
# 默认每3秒刷新一次
|
||||
$ top
|
||||
top - 11:58:59 up 9 days, 22:47, 1 user, load average: 0.03, 0.02, 0.00
|
||||
Tasks: 123 total, 1 running, 72 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu(s): 0.3 us, 0.3 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
KiB Mem : 8169348 total, 5606884 free, 334640 used, 2227824 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 7497908 avail Mem
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
1 root 20 0 78088 9288 6696 S 0.0 0.1 0:16.83 systemd
|
||||
2 root 20 0 0 0 0 S 0.0 0.0 0:00.05 kthreadd
|
||||
4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
这个输出结果中,第三行 %Cpu 就是系统的 CPU 使用率,具体每一列的含义上一节都讲过,只是把CPU时间变换成了CPU使用率,我就不再重复讲了。不过需要注意,top 默认显示的是所有 CPU 的平均值,这个时候你只需要按下数字 1 ,就可以切换到每个 CPU 的使用率了。
|
||||
|
||||
继续往下看,空白行之后是进程的实时信息,每个进程都有一个 %CPU 列,表示进程的 CPU 使用率。它是用户态和内核态 CPU 使用率的总和,包括进程用户空间使用的 CPU、通过系统调用执行的内核空间 CPU 、以及在就绪队列等待运行的 CPU。在虚拟化环境中,它还包括了运行虚拟机占用的 CPU。
|
||||
|
||||
所以,到这里我们可以发现, top 并没有细分进程的用户态CPU和内核态 CPU。那要怎么查看每个进程的详细情况呢?你应该还记得上一节用到的 pidstat 吧,它正是一个专门分析每个进程 CPU 使用情况的工具。
|
||||
|
||||
比如,下面的pidstat命令,就间隔1秒展示了进程的5组CPU使用率,包括:
|
||||
|
||||
<li>
|
||||
用户态CPU使用率 (%usr);
|
||||
</li>
|
||||
<li>
|
||||
内核态CPU使用率(%system);
|
||||
</li>
|
||||
<li>
|
||||
运行虚拟机CPU使用率(%guest);
|
||||
</li>
|
||||
<li>
|
||||
等待 CPU使用率(%wait);
|
||||
</li>
|
||||
<li>
|
||||
以及总的CPU使用率(%CPU)。
|
||||
</li>
|
||||
|
||||
最后的 Average 部分,还计算了 5 组数据的平均值。
|
||||
|
||||
```
|
||||
# 每隔1秒输出一组数据,共输出5组
|
||||
$ pidstat 1 5
|
||||
15:56:02 UID PID %usr %system %guest %wait %CPU CPU Command
|
||||
15:56:03 0 15006 0.00 0.99 0.00 0.00 0.99 1 dockerd
|
||||
|
||||
...
|
||||
|
||||
Average: UID PID %usr %system %guest %wait %CPU CPU Command
|
||||
Average: 0 15006 0.00 0.99 0.00 0.00 0.99 - dockerd
|
||||
|
||||
```
|
||||
|
||||
## CPU 使用率过高怎么办?
|
||||
|
||||
通过 top、ps、pidstat 等工具,你能够轻松找到 CPU 使用率较高(比如 100% )的进程。接下来,你可能又想知道,占用 CPU 的到底是代码里的哪个函数呢?找到它,你才能更高效、更针对性地进行优化。
|
||||
|
||||
我猜你第一个想到的,应该是 GDB(The GNU Project Debugger), 这个功能强大的程序调试利器。的确,GDB 在调试程序错误方面很强大。但是,我又要来“挑刺”了。请你记住,GDB 并不适合在性能分析的早期应用。
|
||||
|
||||
为什么呢?因为 GDB 调试程序的过程会中断程序运行,这在线上环境往往是不允许的。所以,GDB 只适合用在性能分析的后期,当你找到了出问题的大致函数后,线下再借助它来进一步调试函数内部的问题。
|
||||
|
||||
那么哪种工具适合在第一时间分析进程的 CPU 问题呢?我的推荐是 perf。perf 是 Linux 2.6.31 以后内置的性能分析工具。它以性能事件采样为基础,不仅可以分析系统的各种事件和内核性能,还可以用来分析指定应用程序的性能问题。
|
||||
|
||||
使用 perf 分析 CPU 性能问题,我来说两种最常见、也是我最喜欢的用法。
|
||||
|
||||
第一种常见用法是 perf top,类似于 top,它能够实时显示占用 CPU 时钟最多的函数或者指令,因此可以用来查找热点函数,使用界面如下所示:
|
||||
|
||||
```
|
||||
$ perf top
|
||||
Samples: 833 of event 'cpu-clock', Event count (approx.): 97742399
|
||||
Overhead Shared Object Symbol
|
||||
7.28% perf [.] 0x00000000001f78a4
|
||||
4.72% [kernel] [k] vsnprintf
|
||||
4.32% [kernel] [k] module_get_kallsym
|
||||
3.65% [kernel] [k] _raw_spin_unlock_irqrestore
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
输出结果中,第一行包含三个数据,分别是采样数(Samples)、事件类型(event)和事件总数量(Event count)。比如这个例子中,perf 总共采集了 833 个 CPU 时钟事件,而总事件数则为 97742399。
|
||||
|
||||
另外,**采样数需要我们特别注意**。如果采样数过少(比如只有十几个),那下面的排序和百分比就没什么实际参考价值了。
|
||||
|
||||
再往下看是一个表格式样的数据,每一行包含四列,分别是:
|
||||
|
||||
<li>
|
||||
第一列 Overhead ,是该符号的性能事件在所有采样中的比例,用百分比来表示。
|
||||
</li>
|
||||
<li>
|
||||
第二列 Shared ,是该函数或指令所在的动态共享对象(Dynamic Shared Object),如内核、进程名、动态链接库名、内核模块名等。
|
||||
</li>
|
||||
<li>
|
||||
第三列 Object ,是动态共享对象的类型。比如 [.] 表示用户空间的可执行程序、或者动态链接库,而 [k] 则表示内核空间。
|
||||
</li>
|
||||
<li>
|
||||
最后一列 Symbol 是符号名,也就是函数名。当函数名未知时,用十六进制的地址来表示。
|
||||
</li>
|
||||
|
||||
还是以上面的输出为例,我们可以看到,占用 CPU 时钟最多的是 perf 工具自身,不过它的比例也只有 7.28%,说明系统并没有 CPU 性能问题。 perf top的使用你应该很清楚了吧。
|
||||
|
||||
接着再来看第二种常见用法,也就是 perf record 和 perf report。 perf top 虽然实时展示了系统的性能信息,但它的缺点是并不保存数据,也就无法用于离线或者后续的分析。而 perf record 则提供了保存数据的功能,保存后的数据,需要你用 perf report 解析展示。
|
||||
|
||||
```
|
||||
$ perf record # 按Ctrl+C终止采样
|
||||
[ perf record: Woken up 1 times to write data ]
|
||||
[ perf record: Captured and wrote 0.452 MB perf.data (6093 samples) ]
|
||||
|
||||
$ perf report # 展示类似于perf top的报告
|
||||
|
||||
```
|
||||
|
||||
在实际使用中,我们还经常为 perf top 和 perf record 加上 -g 参数,开启调用关系的采样,方便我们根据调用链来分析性能问题。
|
||||
|
||||
## 案例
|
||||
|
||||
下面我们就以 Nginx + PHP 的 Web 服务为例,来看看当你发现 CPU 使用率过高的问题后,要怎么使用 top 等工具找出异常的进程,又要怎么利用 perf 找出引发性能问题的函数。
|
||||
|
||||
### 你的准备
|
||||
|
||||
以下案例基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
|
||||
|
||||
<li>
|
||||
机器配置:2 CPU,8GB 内存
|
||||
</li>
|
||||
<li>
|
||||
预先安装 docker、sysstat、perf、ab 等工具,如 apt install docker.io sysstat linux-tools-common apache2-utils
|
||||
</li>
|
||||
|
||||
我先简单介绍一下这次新使用的工具 ab。ab(apache bench)是一个常用的 HTTP 服务性能测试工具,这里用来模拟 Ngnix 的客户端。由于 Nginx 和 PHP 的配置比较麻烦,我把它们打包成了两个 [Docker 镜像](https://github.com/feiskyer/linux-perf-examples/tree/master/nginx-high-cpu),这样只需要运行两个容器,就可以得到模拟环境。
|
||||
|
||||
注意,这个案例要用到两台虚拟机,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/90/3d/90c30b4f555218f77241bfe2ac27723d.png" alt="">
|
||||
|
||||
你可以看到,其中一台用作 Web 服务器,来模拟性能问题;另一台用作 Web 服务器的客户端,来给 Web 服务增加压力请求。使用两台虚拟机是为了相互隔离,避免“交叉感染”。
|
||||
|
||||
接下来,我们打开两个终端,分别 SSH 登录到两台机器上,并安装上面提到的工具。
|
||||
|
||||
还是同样的“配方”。下面的所有命令,都默认假设以 root 用户运行,如果你是普通用户身份登陆系统,一定要先运行 sudo su root 命令切换到 root 用户。到这里,准备工作就完成了。
|
||||
|
||||
不过,操作之前,我还想再说一点。这次案例中 PHP 应用的核心逻辑比较简单,大部分人一眼就可以看出问题,但你要知道,实际生产环境中的源码就复杂多了。
|
||||
|
||||
所以,我希望你在按照步骤操作之前,先不要查看源码(避免先入为主),而是**把它当成一个黑盒来分析。**这样,你可以更好地理解整个解决思路,怎么从系统的资源使用问题出发,分析出瓶颈所在的应用、以及瓶颈在应用中的大概位置。
|
||||
|
||||
### 操作和分析
|
||||
|
||||
接下来,我们正式进入操作环节。
|
||||
|
||||
首先,在第一个终端执行下面的命令来运行 Nginx 和 PHP 应用:
|
||||
|
||||
```
|
||||
$ docker run --name nginx -p 10000:80 -itd feisky/nginx
|
||||
$ docker run --name phpfpm -itd --network container:nginx feisky/php-fpm
|
||||
|
||||
```
|
||||
|
||||
然后,在第二个终端使用 curl 访问 http://[VM1的IP]:10000,确认 Nginx 已正常启动。你应该可以看到 It works! 的响应。
|
||||
|
||||
```
|
||||
# 192.168.0.10是第一台虚拟机的IP地址
|
||||
$ curl http://192.168.0.10:10000/
|
||||
It works!
|
||||
|
||||
```
|
||||
|
||||
接着,我们来测试一下这个 Nginx 服务的性能。在第二个终端运行下面的 ab 命令:
|
||||
|
||||
```
|
||||
# 并发10个请求测试Nginx性能,总共测试100个请求
|
||||
$ ab -c 10 -n 100 http://192.168.0.10:10000/
|
||||
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
|
||||
Copyright 1996 Adam Twiss, Zeus Technology Ltd,
|
||||
...
|
||||
Requests per second: 11.63 [#/sec] (mean)
|
||||
Time per request: 859.942 [ms] (mean)
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
从ab的输出结果我们可以看到,Nginx能承受的每秒平均请求数只有 11.63。你一定在吐槽,这也太差了吧。那到底是哪里出了问题呢?我们用 top 和 pidstat 再来观察下。
|
||||
|
||||
这次,我们在第二个终端,将测试的请求总数增加到 10000。这样当你在第一个终端使用性能分析工具时, Nginx 的压力还是继续。
|
||||
|
||||
继续在第二个终端,运行 ab 命令:
|
||||
|
||||
```
|
||||
$ ab -c 10 -n 10000 http://192.168.0.10:10000/
|
||||
|
||||
```
|
||||
|
||||
接着,回到第一个终端运行 top 命令,并按下数字 1 ,切换到每个 CPU 的使用率:
|
||||
|
||||
```
|
||||
$ top
|
||||
...
|
||||
%Cpu0 : 98.7 us, 1.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu1 : 99.3 us, 0.7 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
...
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
21514 daemon 20 0 336696 16384 8712 R 41.9 0.2 0:06.00 php-fpm
|
||||
21513 daemon 20 0 336696 13244 5572 R 40.2 0.2 0:06.08 php-fpm
|
||||
21515 daemon 20 0 336696 16384 8712 R 40.2 0.2 0:05.67 php-fpm
|
||||
21512 daemon 20 0 336696 13244 5572 R 39.9 0.2 0:05.87 php-fpm
|
||||
21516 daemon 20 0 336696 16384 8712 R 35.9 0.2 0:05.61 php-fpm
|
||||
|
||||
```
|
||||
|
||||
这里可以看到,系统中有几个 php-fpm 进程的 CPU 使用率加起来接近 200%;而每个 CPU 的用户使用率(us)也已经超过了 98%,接近饱和。这样,我们就可以确认,正是用户空间的 php-fpm 进程,导致CPU 使用率骤升。
|
||||
|
||||
那再往下走,怎么知道是 php-fpm 的哪个函数导致了 CPU 使用率升高呢?我们来用 perf 分析一下。在第一个终端运行下面的perf命令:
|
||||
|
||||
```
|
||||
# -g开启调用关系分析,-p指定php-fpm的进程号21515
|
||||
$ perf top -g -p 21515
|
||||
|
||||
```
|
||||
|
||||
按方向键切换到 php-fpm,再按下回车键展开 php-fpm 的调用关系,你会发现,调用关系最终到了 sqrt 和 add_function。看来,我们需要从这两个函数入手了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6e/10/6e58d2f7b1ace94501b1833bab16f210.png" alt="">
|
||||
|
||||
我们拷贝出 [Nginx 应用的源码](https://github.com/feiskyer/linux-perf-examples/blob/master/nginx-high-cpu/app/index.php),看看是不是调用了这两个函数:
|
||||
|
||||
```
|
||||
# 从容器phpfpm中将PHP源码拷贝出来
|
||||
$ docker cp phpfpm:/app .
|
||||
|
||||
# 使用grep查找函数调用
|
||||
$ grep sqrt -r app/ #找到了sqrt调用
|
||||
app/index.php: $x += sqrt($x);
|
||||
$ grep add_function -r app/ #没找到add_function调用,这其实是PHP内置函数
|
||||
|
||||
```
|
||||
|
||||
OK,原来只有 sqrt 函数在 app/index.php 文件中调用了。那最后一步,我们就该看看这个文件的源码了:
|
||||
|
||||
```
|
||||
$ cat app/index.php
|
||||
<?php
|
||||
// test only.
|
||||
$x = 0.0001;
|
||||
for ($i = 0; $i <= 1000000; $i++) {
|
||||
$x += sqrt($x);
|
||||
}
|
||||
|
||||
echo "It works!"
|
||||
|
||||
```
|
||||
|
||||
呀,有没有发现问题在哪里呢?我想你要笑话我了,居然犯了一个这么傻的错误,测试代码没删就直接发布应用了。为了方便你验证优化后的效果,我把修复后的应用也打包成了一个Docker镜像,你可以在第一个终端中执行下面的命令来运行它:
|
||||
|
||||
```
|
||||
# 停止原来的应用
|
||||
$ docker rm -f nginx phpfpm
|
||||
# 运行优化后的应用
|
||||
$ docker run --name nginx -p 10000:80 -itd feisky/nginx:cpu-fix
|
||||
$ docker run --name phpfpm -itd --network container:nginx feisky/php-fpm:cpu-fix
|
||||
|
||||
```
|
||||
|
||||
接着,到第二个终端来验证一下修复后的效果。首先Ctrl+C停止之前的ab命令后,再运行下面的命令:
|
||||
|
||||
```
|
||||
$ ab -c 10 -n 10000 http://10.240.0.5:10000/
|
||||
...
|
||||
Complete requests: 10000
|
||||
Failed requests: 0
|
||||
Total transferred: 1720000 bytes
|
||||
HTML transferred: 90000 bytes
|
||||
Requests per second: 2237.04 [#/sec] (mean)
|
||||
Time per request: 4.470 [ms] (mean)
|
||||
Time per request: 0.447 [ms] (mean, across all concurrent requests)
|
||||
Transfer rate: 375.75 [Kbytes/sec] received
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
从这里你可以发现,现在每秒的平均请求数,已经从原来的11变成了2237。
|
||||
|
||||
你看,就是这么很傻的一个小问题,却会极大的影响性能,并且查找起来也并不容易吧。当然,找到问题后,解决方法就简单多了,删除测试代码就可以了。
|
||||
|
||||
## 小结
|
||||
|
||||
CPU 使用率是最直观和最常用的系统性能指标,更是我们在排查性能问题时,通常会关注的第一个指标。所以我们更要熟悉它的含义,尤其要弄清楚用户(%user)、Nice(%nice)、系统(%system) 、等待 I/O(%iowait) 、中断(%irq)以及软中断(%softirq)这几种不同 CPU 的使用率。比如说:
|
||||
|
||||
<li>
|
||||
用户 CPU 和 Nice CPU 高,说明用户态进程占用了较多的 CPU,所以应该着重排查进程的性能问题。
|
||||
</li>
|
||||
<li>
|
||||
系统 CPU 高,说明内核态占用了较多的 CPU,所以应该着重排查内核线程或者系统调用的性能问题。
|
||||
</li>
|
||||
<li>
|
||||
I/O 等待 CPU 高,说明等待 I/O 的时间比较长,所以应该着重排查系统存储是不是出现了 I/O 问题。
|
||||
</li>
|
||||
<li>
|
||||
软中断和硬中断高,说明软中断或硬中断的处理程序占用了较多的 CPU,所以应该着重排查内核中的中断服务程序。
|
||||
</li>
|
||||
|
||||
碰到 CPU 使用率升高的问题,你可以借助 top、pidstat 等工具,确认引发 CPU 性能问题的来源;再使用 perf 等工具,排查出引起性能问题的具体函数。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想邀请你一起来聊聊,你所理解的 CPU 使用率,以及在发现 CPU 使用率升高时,你又是怎么分析的呢?你可以结合今天的内容,和你自己的操作记录,来总结思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/56/52/565d66d658ad23b2f4997551db153852.jpg" alt="">
|
||||
@@ -0,0 +1,386 @@
|
||||
<audio id="audio" title="06 | 案例篇:系统的 CPU 使用率很高,但为啥却找不到高 CPU 的应用?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0b/70/0be8a102696d191c057158c7c9818f70.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节我讲了 CPU 使用率是什么,并通过一个案例教你使用 top、vmstat、pidstat 等工具,排查高 CPU 使用率的进程,然后再使用 perf top 工具,定位应用内部函数的问题。不过就有人留言了,说似乎感觉高 CPU 使用率的问题,还是挺容易排查的。
|
||||
|
||||
那是不是所有 CPU 使用率高的问题,都可以这么分析呢?我想,你的答案应该是否定的。
|
||||
|
||||
回顾前面的内容,我们知道,系统的 CPU 使用率,不仅包括进程用户态和内核态的运行,还包括中断处理、等待 I/O 以及内核线程等。所以,**当你发现系统的 CPU 使用率很高的时候,不一定能找到相对应的高 CPU 使用率的进程**。
|
||||
|
||||
今天,我就用一个 Nginx + PHP 的 Web 服务的案例,带你来分析这种情况。
|
||||
|
||||
## 案例分析
|
||||
|
||||
### 你的准备
|
||||
|
||||
今天依旧探究系统CPU使用率高的情况,所以这次实验的准备工作,与上节课的准备工作基本相同,差别在于案例所用的 Docker 镜像不同。
|
||||
|
||||
本次案例还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
|
||||
|
||||
<li>
|
||||
机器配置:2 CPU,8GB 内存
|
||||
</li>
|
||||
<li>
|
||||
预先安装 docker、sysstat、perf、ab 等工具,如 apt install [docker.io](http://docker.io) sysstat linux-tools-common apache2-utils
|
||||
</li>
|
||||
|
||||
前面我们讲到过,ab(apache bench)是一个常用的 HTTP 服务性能测试工具,这里同样用来模拟 Nginx 的客户端。由于 Nginx 和 PHP 的配置比较麻烦,我把它们打包成了两个 [Docker 镜像](https://github.com/feiskyer/linux-perf-examples/tree/master/nginx-short-process),这样只需要运行两个容器,就可以得到模拟环境。
|
||||
|
||||
注意,这个案例要用到两台虚拟机,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/90/3d/90c30b4f555218f77241bfe2ac27723d.png" alt="">
|
||||
|
||||
你可以看到,其中一台用作 Web 服务器,来模拟性能问题;另一台用作 Web 服务器的客户端,来给 Web 服务增加压力请求。使用两台虚拟机是为了相互隔离,避免“交叉感染”。
|
||||
|
||||
接下来,我们打开两个终端,分别 SSH 登录到两台机器上,并安装上述工具。
|
||||
|
||||
同样注意,下面所有命令都默认以 root 用户运行,如果你是用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
|
||||
|
||||
走到这一步,准备工作就完成了。接下来,我们正式进入操作环节。
|
||||
|
||||
>
|
||||
温馨提示:案例中 PHP 应用的核心逻辑比较简单,你可能一眼就能看出问题,但实际生产环境中的源码就复杂多了。所以,我依旧建议,**操作之前别看源码**,避免先入为主,而要把它当成一个黑盒来分析。这样,你可以更好把握,怎么从系统的资源使用问题出发,分析出瓶颈所在的应用,以及瓶颈在应用中大概的位置。
|
||||
|
||||
|
||||
### 操作和分析
|
||||
|
||||
首先,我们在第一个终端,执行下面的命令运行 Nginx 和 PHP 应用:
|
||||
|
||||
```
|
||||
$ docker run --name nginx -p 10000:80 -itd feisky/nginx:sp
|
||||
$ docker run --name phpfpm -itd --network container:nginx feisky/php-fpm:sp
|
||||
|
||||
```
|
||||
|
||||
然后,在第二个终端,使用 curl 访问 http://[VM1的IP]:10000,确认 Nginx 已正常启动。你应该可以看到 It works! 的响应。
|
||||
|
||||
```
|
||||
# 192.168.0.10是第一台虚拟机的IP地址
|
||||
$ curl http://192.168.0.10:10000/
|
||||
It works!
|
||||
|
||||
```
|
||||
|
||||
接着,我们来测试一下这个 Nginx 服务的性能。在第二个终端运行下面的 ab 命令。要注意,与上次操作不同的是,这次我们需要并发100个请求测试Nginx性能,总共测试1000个请求。
|
||||
|
||||
```
|
||||
# 并发100个请求测试Nginx性能,总共测试1000个请求
|
||||
$ ab -c 100 -n 1000 http://192.168.0.10:10000/
|
||||
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
|
||||
Copyright 1996 Adam Twiss, Zeus Technology Ltd,
|
||||
...
|
||||
Requests per second: 87.86 [#/sec] (mean)
|
||||
Time per request: 1138.229 [ms] (mean)
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
从ab的输出结果我们可以看到,Nginx能承受的每秒平均请求数,只有 87 多一点,是不是感觉它的性能有点差呀。那么,到底是哪里出了问题呢?我们再用 top 和 pidstat 来观察一下。
|
||||
|
||||
这次,我们在第二个终端,将测试的并发请求数改成5,同时把请求时长设置为10分钟(-t 600)。这样,当你在第一个终端使用性能分析工具时, Nginx 的压力还是继续的。
|
||||
|
||||
继续在第二个终端运行 ab 命令:
|
||||
|
||||
```
|
||||
$ ab -c 5 -t 600 http://192.168.0.10:10000/
|
||||
|
||||
```
|
||||
|
||||
然后,我们在第一个终端运行 top 命令,观察系统的 CPU 使用情况:
|
||||
|
||||
```
|
||||
$ top
|
||||
...
|
||||
%Cpu(s): 80.8 us, 15.1 sy, 0.0 ni, 2.8 id, 0.0 wa, 0.0 hi, 1.3 si, 0.0 st
|
||||
...
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
6882 root 20 0 8456 5052 3884 S 2.7 0.1 0:04.78 docker-containe
|
||||
6947 systemd+ 20 0 33104 3716 2340 S 2.7 0.0 0:04.92 nginx
|
||||
7494 daemon 20 0 336696 15012 7332 S 2.0 0.2 0:03.55 php-fpm
|
||||
7495 daemon 20 0 336696 15160 7480 S 2.0 0.2 0:03.55 php-fpm
|
||||
10547 daemon 20 0 336696 16200 8520 S 2.0 0.2 0:03.13 php-fpm
|
||||
10155 daemon 20 0 336696 16200 8520 S 1.7 0.2 0:03.12 php-fpm
|
||||
10552 daemon 20 0 336696 16200 8520 S 1.7 0.2 0:03.12 php-fpm
|
||||
15006 root 20 0 1168608 66264 37536 S 1.0 0.8 9:39.51 dockerd
|
||||
4323 root 20 0 0 0 0 I 0.3 0.0 0:00.87 kworker/u4:1
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
观察 top 输出的进程列表可以发现,CPU 使用率最高的进程也只不过才 2.7%,看起来并不高。
|
||||
|
||||
然而,再看系统 CPU 使用率( %Cpu )这一行,你会发现,系统的整体 CPU 使用率是比较高的:用户 CPU 使用率(us)已经到了 80%,系统 CPU 为 15.1%,而空闲 CPU (id)则只有 2.8%。
|
||||
|
||||
为什么用户 CPU 使用率这么高呢?我们再重新分析一下进程列表,看看有没有可疑进程:
|
||||
|
||||
<li>
|
||||
docker-containerd 进程是用来运行容器的,2.7% 的 CPU 使用率看起来正常;
|
||||
</li>
|
||||
<li>
|
||||
Nginx 和 php-fpm 是运行 Web 服务的,它们会占用一些 CPU 也不意外,并且 2% 的 CPU 使用率也不算高;
|
||||
</li>
|
||||
<li>
|
||||
再往下看,后面的进程呢,只有 0.3% 的 CPU 使用率,看起来不太像会导致用户 CPU 使用率达到 80%。
|
||||
</li>
|
||||
|
||||
那就奇怪了,明明用户 CPU 使用率都80%了,可我们挨个分析了一遍进程列表,还是找不到高 CPU 使用率的进程。看来top是不管用了,那还有其他工具可以查看进程 CPU 使用情况吗?不知道你记不记得我们的老朋友 pidstat,它可以用来分析进程的 CPU 使用情况。
|
||||
|
||||
接下来,我们还是在第一个终端,运行 pidstat 命令:
|
||||
|
||||
```
|
||||
# 间隔1秒输出一组数据(按Ctrl+C结束)
|
||||
$ pidstat 1
|
||||
...
|
||||
04:36:24 UID PID %usr %system %guest %wait %CPU CPU Command
|
||||
04:36:25 0 6882 1.00 3.00 0.00 0.00 4.00 0 docker-containe
|
||||
04:36:25 101 6947 1.00 2.00 0.00 1.00 3.00 1 nginx
|
||||
04:36:25 1 14834 1.00 1.00 0.00 1.00 2.00 0 php-fpm
|
||||
04:36:25 1 14835 1.00 1.00 0.00 1.00 2.00 0 php-fpm
|
||||
04:36:25 1 14845 0.00 2.00 0.00 2.00 2.00 1 php-fpm
|
||||
04:36:25 1 14855 0.00 1.00 0.00 1.00 1.00 1 php-fpm
|
||||
04:36:25 1 14857 1.00 2.00 0.00 1.00 3.00 0 php-fpm
|
||||
04:36:25 0 15006 0.00 1.00 0.00 0.00 1.00 0 dockerd
|
||||
04:36:25 0 15801 0.00 1.00 0.00 0.00 1.00 1 pidstat
|
||||
04:36:25 1 17084 1.00 0.00 0.00 2.00 1.00 0 stress
|
||||
04:36:25 0 31116 0.00 1.00 0.00 0.00 1.00 0 atopacctd
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
观察一会儿,你是不是发现,所有进程的 CPU 使用率也都不高啊,最高的 Docker 和 Nginx 也只有 4% 和 3%,即使所有进程的 CPU 使用率都加起来,也不过是 21%,离 80% 还差得远呢!
|
||||
|
||||
最早的时候,我碰到这种问题就完全懵了:明明用户 CPU 使用率已经高达 80%,但我却怎么都找不到是哪个进程的问题。到这里,你也可以想想,你是不是也遇到过这种情况?还能不能再做进一步的分析呢?
|
||||
|
||||
后来我发现,会出现这种情况,很可能是因为前面的分析漏了一些关键信息。你可以先暂停一下,自己往上翻,重新操作检查一遍。或者,我们一起返回去分析 top 的输出,看看能不能有新发现。
|
||||
|
||||
现在,我们回到第一个终端,重新运行 top 命令,并观察一会儿:
|
||||
|
||||
```
|
||||
$ top
|
||||
top - 04:58:24 up 14 days, 15:47, 1 user, load average: 3.39, 3.82, 2.74
|
||||
Tasks: 149 total, 6 running, 93 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu(s): 77.7 us, 19.3 sy, 0.0 ni, 2.0 id, 0.0 wa, 0.0 hi, 1.0 si, 0.0 st
|
||||
KiB Mem : 8169348 total, 2543916 free, 457976 used, 5167456 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 7363908 avail Mem
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
6947 systemd+ 20 0 33104 3764 2340 S 4.0 0.0 0:32.69 nginx
|
||||
6882 root 20 0 12108 8360 3884 S 2.0 0.1 0:31.40 docker-containe
|
||||
15465 daemon 20 0 336696 15256 7576 S 2.0 0.2 0:00.62 php-fpm
|
||||
15466 daemon 20 0 336696 15196 7516 S 2.0 0.2 0:00.62 php-fpm
|
||||
15489 daemon 20 0 336696 16200 8520 S 2.0 0.2 0:00.62 php-fpm
|
||||
6948 systemd+ 20 0 33104 3764 2340 S 1.0 0.0 0:00.95 nginx
|
||||
15006 root 20 0 1168608 65632 37536 S 1.0 0.8 9:51.09 dockerd
|
||||
15476 daemon 20 0 336696 16200 8520 S 1.0 0.2 0:00.61 php-fpm
|
||||
15477 daemon 20 0 336696 16200 8520 S 1.0 0.2 0:00.61 php-fpm
|
||||
24340 daemon 20 0 8184 1616 536 R 1.0 0.0 0:00.01 stress
|
||||
24342 daemon 20 0 8196 1580 492 R 1.0 0.0 0:00.01 stress
|
||||
24344 daemon 20 0 8188 1056 492 R 1.0 0.0 0:00.01 stress
|
||||
24347 daemon 20 0 8184 1356 540 R 1.0 0.0 0:00.01 stress
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
这次从头开始看 top 的每行输出,咦?Tasks 这一行看起来有点奇怪,就绪队列中居然有 6 个 Running 状态的进程(6 running),是不是有点多呢?
|
||||
|
||||
回想一下 ab 测试的参数,并发请求数是 5。再看进程列表里, php-fpm 的数量也是 5,再加上 Nginx,好像同时有 6 个进程也并不奇怪。但真的是这样吗?
|
||||
|
||||
再仔细看进程列表,这次主要看 Running(R) 状态的进程。你有没有发现, Nginx 和所有的 php-fpm 都处于Sleep(S)状态,而真正处于 Running(R)状态的,却是几个 stress 进程。这几个 stress 进程就比较奇怪了,需要我们做进一步的分析。
|
||||
|
||||
我们还是使用 pidstat 来分析这几个进程,并且使用 -p 选项指定进程的 PID。首先,从上面 top 的结果中,找到这几个进程的 PID。比如,先随便找一个 24344,然后用 pidstat 命令看一下它的 CPU 使用情况:
|
||||
|
||||
```
|
||||
$ pidstat -p 24344
|
||||
|
||||
16:14:55 UID PID %usr %system %guest %wait %CPU CPU Command
|
||||
|
||||
```
|
||||
|
||||
奇怪,居然没有任何输出。难道是pidstat 命令出问题了吗?之前我说过,**在怀疑性能工具出问题前,最好还是先用其他工具交叉确认一下**。那用什么工具呢? ps 应该是最简单易用的。我们在终端里运行下面的命令,看看 24344 进程的状态:
|
||||
|
||||
```
|
||||
# 从所有进程中查找PID是24344的进程
|
||||
$ ps aux | grep 24344
|
||||
root 9628 0.0 0.0 14856 1096 pts/0 S+ 16:15 0:00 grep --color=auto 24344
|
||||
|
||||
```
|
||||
|
||||
还是没有输出。现在终于发现问题,原来这个进程已经不存在了,所以 pidstat 就没有任何输出。既然进程都没了,那性能问题应该也跟着没了吧。我们再用 top 命令确认一下:
|
||||
|
||||
```
|
||||
$ top
|
||||
...
|
||||
%Cpu(s): 80.9 us, 14.9 sy, 0.0 ni, 2.8 id, 0.0 wa, 0.0 hi, 1.3 si, 0.0 st
|
||||
...
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
6882 root 20 0 12108 8360 3884 S 2.7 0.1 0:45.63 docker-containe
|
||||
6947 systemd+ 20 0 33104 3764 2340 R 2.7 0.0 0:47.79 nginx
|
||||
3865 daemon 20 0 336696 15056 7376 S 2.0 0.2 0:00.15 php-fpm
|
||||
6779 daemon 20 0 8184 1112 556 R 0.3 0.0 0:00.01 stress
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
好像又错了。结果还跟原来一样,用户 CPU 使用率还是高达 80.9%,系统 CPU 接近 15%,而空闲 CPU 只有 2.8%,Running 状态的进程有 Nginx、stress等。
|
||||
|
||||
可是,刚刚我们看到stress 进程不存在了,怎么现在还在运行呢?再细看一下 top 的输出,原来,这次 stress 进程的 PID 跟前面不一样了,原来的 PID 24344 不见了,现在的是 6779。
|
||||
|
||||
进程的 PID 在变,这说明什么呢?在我看来,要么是这些进程在不停地重启,要么就是全新的进程,这无非也就两个原因:
|
||||
|
||||
<li>
|
||||
第一个原因,进程在不停地崩溃重启,比如因为段错误、配置错误等等,这时,进程在退出后可能又被监控系统自动重启了。
|
||||
</li>
|
||||
<li>
|
||||
第二个原因,这些进程都是短时进程,也就是在其他应用内部通过 exec 调用的外面命令。这些命令一般都只运行很短的时间就会结束,你很难用 top 这种间隔时间比较长的工具发现(上面的案例,我们碰巧发现了)。
|
||||
</li>
|
||||
|
||||
至于 stress,我们前面提到过,它是一个常用的压力测试工具。它的 PID 在不断变化中,看起来像是被其他进程调用的短时进程。要想继续分析下去,还得找到它们的父进程。
|
||||
|
||||
要怎么查找一个进程的父进程呢?没错,用 pstree 就可以用树状形式显示所有进程之间的关系:
|
||||
|
||||
```
|
||||
$ pstree | grep stress
|
||||
|-docker-containe-+-php-fpm-+-php-fpm---sh---stress
|
||||
| |-3*[php-fpm---sh---stress---stress]
|
||||
|
||||
```
|
||||
|
||||
从这里可以看到,stress 是被 php-fpm 调用的子进程,并且进程数量不止一个(这里是3个)。找到父进程后,我们能进入 app 的内部分析了。
|
||||
|
||||
首先,当然应该去看看它的源码。运行下面的命令,把案例应用的源码拷贝到 app 目录,然后再执行 grep 查找是不是有代码再调用 stress 命令:
|
||||
|
||||
```
|
||||
# 拷贝源码到本地
|
||||
$ docker cp phpfpm:/app .
|
||||
|
||||
# grep 查找看看是不是有代码在调用stress命令
|
||||
$ grep stress -r app
|
||||
app/index.php:// fake I/O with stress (via write()/unlink()).
|
||||
app/index.php:$result = exec("/usr/local/bin/stress -t 1 -d 1 2>&1", $output, $status);
|
||||
|
||||
```
|
||||
|
||||
找到了,果然是 app/index.php 文件中直接调用了 stress 命令。
|
||||
|
||||
再来看看[ app/index.php ](https://github.com/feiskyer/linux-perf-examples/blob/master/nginx-short-process/app/index.php)的源代码:
|
||||
|
||||
```
|
||||
$ cat app/index.php
|
||||
<?php
|
||||
// fake I/O with stress (via write()/unlink()).
|
||||
$result = exec("/usr/local/bin/stress -t 1 -d 1 2>&1", $output, $status);
|
||||
if (isset($_GET["verbose"]) && $_GET["verbose"]==1 && $status != 0) {
|
||||
echo "Server internal error: ";
|
||||
print_r($output);
|
||||
} else {
|
||||
echo "It works!";
|
||||
}
|
||||
?>
|
||||
|
||||
```
|
||||
|
||||
可以看到,源码里对每个请求都会调用一个 stress 命令,模拟 I/O 压力。从注释上看,stress 会通过 write() 和 unlink() 对 I/O 进程进行压测,看来,这应该就是系统 CPU 使用率升高的根源了。
|
||||
|
||||
不过,stress 模拟的是 I/O 压力,而之前在 top 的输出中看到的,却一直是用户 CPU 和系统 CPU 升高,并没见到 iowait 升高。这又是怎么回事呢?stress 到底是不是 CPU 使用率升高的原因呢?
|
||||
|
||||
我们还得继续往下走。从代码中可以看到,给请求加入 verbose=1 参数后,就可以查看 stress 的输出。你先试试看,在第二个终端运行:
|
||||
|
||||
```
|
||||
$ curl http://192.168.0.10:10000?verbose=1
|
||||
Server internal error: Array
|
||||
(
|
||||
[0] => stress: info: [19607] dispatching hogs: 0 cpu, 0 io, 0 vm, 1 hdd
|
||||
[1] => stress: FAIL: [19608] (563) mkstemp failed: Permission denied
|
||||
[2] => stress: FAIL: [19607] (394) <-- worker 19608 returned error 1
|
||||
[3] => stress: WARN: [19607] (396) now reaping child worker processes
|
||||
[4] => stress: FAIL: [19607] (400) kill error: No such process
|
||||
[5] => stress: FAIL: [19607] (451) failed run completed in 0s
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
看错误消息 mkstemp failed: Permission denied ,以及 failed run completed in 0s。原来 stress 命令并没有成功,它因为权限问题失败退出了。看来,我们发现了一个 PHP 调用外部 stress 命令的 bug:没有权限创建临时文件。
|
||||
|
||||
从这里我们可以猜测,正是由于权限错误,大量的 stress 进程在启动时初始化失败,进而导致用户 CPU 使用率的升高。
|
||||
|
||||
分析出问题来源,下一步是不是就要开始优化了呢?当然不是!既然只是猜测,那就需要再确认一下,这个猜测到底对不对,是不是真的有大量的 stress 进程。该用什么工具或指标呢?
|
||||
|
||||
我们前面已经用了 top、pidstat、pstree 等工具,没有发现大量的 stress 进程。那么,还有什么其他的工具可以用吗?
|
||||
|
||||
还记得上一期提到的 perf 吗?它可以用来分析 CPU 性能事件,用在这里就很合适。依旧在第一个终端中运行 perf record -g 命令 ,并等待一会儿(比如15秒)后按 Ctrl+C 退出。然后再运行 perf report 查看报告:
|
||||
|
||||
```
|
||||
# 记录性能事件,等待大约15秒后按 Ctrl+C 退出
|
||||
$ perf record -g
|
||||
|
||||
# 查看报告
|
||||
$ perf report
|
||||
|
||||
```
|
||||
|
||||
这样,你就可以看到下图这个性能报告:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c9/33/c99445b401301147fa41cb2b5739e833.png" alt="">
|
||||
|
||||
你看,stress 占了所有CPU时钟事件的 77%,而 stress 调用调用栈中比例最高的,是随机数生成函数 random(),看来它的确就是 CPU 使用率升高的元凶了。随后的优化就很简单了,只要修复权限问题,并减少或删除 stress 的调用,就可以减轻系统的 CPU 压力。
|
||||
|
||||
当然,实际生产环境中的问题一般都要比这个案例复杂,在你找到触发瓶颈的命令行后,却可能发现,这个外部命令的调用过程是应用核心逻辑的一部分,并不能轻易减少或者删除。
|
||||
|
||||
这时,你就得继续排查,为什么被调用的命令,会导致 CPU 使用率升高或 I/O 升高等问题。这些复杂场景的案例,我会在后面的综合实战里详细分析。
|
||||
|
||||
最后,在案例结束时,不要忘了清理环境,执行下面的 Docker 命令,停止案例中用到的 Nginx 进程:
|
||||
|
||||
```
|
||||
$ docker rm -f nginx phpfpm
|
||||
|
||||
```
|
||||
|
||||
## execsnoop
|
||||
|
||||
在这个案例中,我们使用了 top、pidstat、pstree 等工具分析了系统 CPU 使用率高的问题,并发现 CPU 升高是短时进程 stress 导致的,但是整个分析过程还是比较复杂的。对于这类问题,有没有更好的方法监控呢?
|
||||
|
||||
[execsnoop](https://github.com/brendangregg/perf-tools/blob/master/execsnoop) 就是一个专为短时进程设计的工具。它通过 ftrace 实时监控进程的 exec() 行为,并输出短时进程的基本信息,包括进程 PID、父进程 PID、命令行参数以及执行的结果。
|
||||
|
||||
比如,用 execsnoop 监控上述案例,就可以直接得到 stress 进程的父进程 PID 以及它的命令行参数,并可以发现大量的 stress 进程在不停启动:
|
||||
|
||||
```
|
||||
# 按 Ctrl+C 结束
|
||||
$ execsnoop
|
||||
PCOMM PID PPID RET ARGS
|
||||
sh 30394 30393 0
|
||||
stress 30396 30394 0 /usr/local/bin/stress -t 1 -d 1
|
||||
sh 30398 30393 0
|
||||
stress 30399 30398 0 /usr/local/bin/stress -t 1 -d 1
|
||||
sh 30402 30400 0
|
||||
stress 30403 30402 0 /usr/local/bin/stress -t 1 -d 1
|
||||
sh 30405 30393 0
|
||||
stress 30407 30405 0 /usr/local/bin/stress -t 1 -d 1
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
execsnoop 所用的 ftrace 是一种常用的动态追踪技术,一般用于分析 Linux 内核的运行时行为,后面课程我也会详细介绍并带你使用。
|
||||
|
||||
## 小结
|
||||
|
||||
碰到常规问题无法解释的 CPU 使用率情况时,首先要想到有可能是短时应用导致的问题,比如有可能是下面这两种情况。
|
||||
|
||||
<li>
|
||||
第一,**应用里直接调用了其他二进制程序,这些程序通常运行时间比较短,通过 top 等工具也不容易发现**。
|
||||
</li>
|
||||
<li>
|
||||
第二,**应用本身在不停地崩溃重启,而启动过程的资源初始化,很可能会占用相当多的 CPU**。
|
||||
</li>
|
||||
|
||||
对于这类进程,我们可以用 pstree 或者 execsnoop 找到它们的父进程,再从父进程所在的应用入手,排查问题的根源。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想邀请你一起来聊聊,你所碰到的 CPU 性能问题。有没有哪个印象深刻的经历可以跟我分享呢?或者,在今天的案例操作中,你遇到了什么问题,又解决了哪些呢?你可以结合我的讲述,总结自己的思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/56/52/565d66d658ad23b2f4997551db153852.jpg" alt="">
|
||||
201
极客时间专栏/Linux性能优化实战/CPU 性能篇/07 | 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(上).md
Normal file
201
极客时间专栏/Linux性能优化实战/CPU 性能篇/07 | 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(上).md
Normal file
@@ -0,0 +1,201 @@
|
||||
<audio id="audio" title="07 | 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8e/71/8edd3338f12bc51072bd88eb12ea1b71.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节,我用一个 Nginx+PHP 的案例,给你讲了服务器 CPU 使用率高的分析和应对方法。这里你一定要记得,当碰到无法解释的 CPU 使用率问题时,先要检查一下是不是短时应用在捣鬼。
|
||||
|
||||
短时应用的运行时间比较短,很难在 top 或者 ps 这类展示系统概要和进程快照的工具中发现,你需要使用记录事件的工具来配合诊断,比如 execsnoop 或者 perf top。
|
||||
|
||||
这些思路你不用刻意去背,多练习几次,多在操作中思考,你便能灵活运用。
|
||||
|
||||
另外,我们还讲到 CPU 使用率的类型。除了上一节提到的用户 CPU 之外,它还包括系统 CPU(比如上下文切换)、等待 I/O 的 CPU(比如等待磁盘的响应)以及中断 CPU(包括软中断和硬中断)等。
|
||||
|
||||
我们已经在上下文切换的文章中,一起分析了系统 CPU 使用率高的问题,剩下的等待 I/O 的 CPU 使用率(以下简称为 iowait)升高,也是最常见的一个服务器性能问题。今天我们就来看一个多进程I/O的案例,并分析这种情况。
|
||||
|
||||
## 进程状态
|
||||
|
||||
当 iowait 升高时,进程很可能因为得不到硬件的响应,而长时间处于不可中断状态。从 ps 或者 top 命令的输出中,你可以发现它们都处于 D 状态,也就是不可中断状态(Uninterruptible Sleep)。既然说到了进程的状态,进程有哪些状态你还记得吗?我们先来回顾一下。
|
||||
|
||||
top 和 ps 是最常用的查看进程状态的工具,我们就从 top 的输出开始。下面是一个 top 命令输出的示例,S列(也就是 Status 列)表示进程的状态。从这个示例里,你可以看到 R、D、Z、S、I 等几个状态,它们分别是什么意思呢?
|
||||
|
||||
```
|
||||
$ top
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
28961 root 20 0 43816 3148 4040 R 3.2 0.0 0:00.01 top
|
||||
620 root 20 0 37280 33676 908 D 0.3 0.4 0:00.01 app
|
||||
1 root 20 0 160072 9416 6752 S 0.0 0.1 0:37.64 systemd
|
||||
1896 root 20 0 0 0 0 Z 0.0 0.0 0:00.00 devapp
|
||||
2 root 20 0 0 0 0 S 0.0 0.0 0:00.10 kthreadd
|
||||
4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H
|
||||
6 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mm_percpu_wq
|
||||
7 root 20 0 0 0 0 S 0.0 0.0 0:06.37 ksoftirqd/0
|
||||
|
||||
```
|
||||
|
||||
我们挨个来看一下:
|
||||
|
||||
<li>
|
||||
**R** 是 Running 或 Runnable 的缩写,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。
|
||||
</li>
|
||||
<li>
|
||||
**D** 是 Disk Sleep 的缩写,也就是不可中断状态睡眠(Uninterruptible Sleep),一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。
|
||||
</li>
|
||||
<li>
|
||||
**Z** 是 Zombie 的缩写,如果你玩过“植物大战僵尸”这款游戏,应该知道它的意思。它表示僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符、PID 等)。
|
||||
</li>
|
||||
<li>
|
||||
**S** 是 Interruptible Sleep 的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态。
|
||||
</li>
|
||||
<li>
|
||||
**I** 是 Idle 的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上。前面说了,硬件交互导致的不可中断进程用 D 表示,但对某些内核线程来说,它们有可能实际上并没有任何负载,用 Idle 正是为了区分这种情况。要注意,D 状态的进程会导致平均负载升高, I 状态的进程却不会。
|
||||
</li>
|
||||
|
||||
当然了,上面的示例并没有包括进程的所有状态。除了以上 5 个状态,进程还包括下面这2个状态。
|
||||
|
||||
第一个是 **T 或者 t**,也就是 Stopped 或 Traced 的缩写,表示进程处于暂停或者跟踪状态。
|
||||
|
||||
向一个进程发送 SIGSTOP 信号,它就会因响应这个信号变成暂停状态(Stopped);再向它发送 SIGCONT 信号,进程又会恢复运行(如果进程是终端里直接启动的,则需要你用 fg 命令,恢复到前台运行)。
|
||||
|
||||
而当你用调试器(如 gdb)调试一个进程时,在使用断点中断进程后,进程就会变成跟踪状态,这其实也是一种特殊的暂停状态,只不过你可以用调试器来跟踪并按需要控制进程的运行。
|
||||
|
||||
另一个是 **X**,也就是 Dead 的缩写,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它。
|
||||
|
||||
了解了这些,我们再回到今天的主题。先看不可中断状态,这其实是为了保证进程数据与硬件状态一致,并且正常情况下,不可中断状态在很短时间内就会结束。所以,短时的不可中断状态进程,我们一般可以忽略。
|
||||
|
||||
但如果系统或硬件发生了故障,进程可能会在不可中断状态保持很久,甚至导致系统中出现大量不可中断进程。这时,你就得注意下,系统是不是出现了 I/O 等性能问题。
|
||||
|
||||
再看僵尸进程,这是多进程应用很容易碰到的问题。正常情况下,当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源;而子进程在结束时,会向它的父进程发送 SIGCHLD 信号,所以,父进程还可以注册 SIGCHLD 信号的处理函数,异步回收资源。
|
||||
|
||||
如果父进程没这么做,或是子进程执行太快,父进程还没来得及处理子进程状态,子进程就已经提前退出,那这时的子进程就会变成僵尸进程。换句话说,父亲应该一直对儿子负责,善始善终,如果不作为或者跟不上,都会导致“问题少年”的出现。
|
||||
|
||||
通常,僵尸进程持续的时间都比较短,在父进程回收它的资源后就会消亡;或者在父进程退出后,由 init 进程回收后也会消亡。
|
||||
|
||||
一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态。大量的僵尸进程会用尽 PID 进程号,导致新进程不能创建,所以这种情况一定要避免。
|
||||
|
||||
## 案例分析
|
||||
|
||||
接下来,我将用一个多进程应用的案例,带你分析大量不可中断状态和僵尸状态进程的问题。这个应用基于 C 开发,由于它的编译和运行步骤比较麻烦,我把它打包成了一个 [Docker 镜像](https://github.com/feiskyer/linux-perf-examples/tree/master/high-iowait-process)。这样,你只需要运行一个 Docker 容器就可以得到模拟环境。
|
||||
|
||||
### 你的准备
|
||||
|
||||
下面的案例仍然基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
|
||||
|
||||
<li>
|
||||
机器配置:2 CPU,8GB 内存
|
||||
</li>
|
||||
<li>
|
||||
预先安装 docker、sysstat、dstat 等工具,如 apt install [docker.io](http://docker.io) dstat sysstat
|
||||
</li>
|
||||
|
||||
这里,dstat 是一个新的性能工具,它吸收了 vmstat、iostat、ifstat 等几种工具的优点,可以同时观察系统的 CPU、磁盘 I/O、网络以及内存使用情况。
|
||||
|
||||
接下来,我们打开一个终端,SSH 登录到机器上,并安装上述工具。
|
||||
|
||||
注意,以下所有命令都默认以 root 用户运行,如果你用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
|
||||
|
||||
如果安装过程有问题,你可以先上网搜索解决,实在解决不了的,记得在留言区向我提问。
|
||||
|
||||
>
|
||||
温馨提示:案例应用的核心代码逻辑比较简单,你可能一眼就能看出问题,但实际生产环境中的源码就复杂多了。所以,我依旧建议,操作之前别看源码,避免先入为主,而要把它当成一个黑盒来分析,这样你可以更好地根据现象分析问题。你姑且当成你工作中的一次演练,这样效果更佳。
|
||||
|
||||
|
||||
### 操作和分析
|
||||
|
||||
安装完成后,我们首先执行下面的命令运行案例应用:
|
||||
|
||||
```
|
||||
$ docker run --privileged --name=app -itd feisky/app:iowait
|
||||
|
||||
```
|
||||
|
||||
然后,输入 ps 命令,确认案例应用已正常启动。如果一切正常,你应该可以看到如下所示的输出:
|
||||
|
||||
```
|
||||
$ ps aux | grep /app
|
||||
root 4009 0.0 0.0 4376 1008 pts/0 Ss+ 05:51 0:00 /app
|
||||
root 4287 0.6 0.4 37280 33660 pts/0 D+ 05:54 0:00 /app
|
||||
root 4288 0.6 0.4 37280 33668 pts/0 D+ 05:54 0:00 /app
|
||||
|
||||
```
|
||||
|
||||
从这个界面,我们可以发现多个 app 进程已经启动,并且它们的状态分别是 Ss+ 和 D+。其中,S 表示可中断睡眠状态,D 表示不可中断睡眠状态,我们在前面刚学过,那后面的 s 和 + 是什么意思呢?不知道也没关系,查一下man ps 就可以。现在记住,s 表示这个进程是一个会话的领导进程,而 + 表示前台进程组。
|
||||
|
||||
这里又出现了两个新概念,**进程组**和**会话**。它们用来管理一组相互关联的进程,意思其实很好理解。
|
||||
|
||||
<li>
|
||||
进程组表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员;
|
||||
</li>
|
||||
<li>
|
||||
而会话是指共享同一个控制终端的一个或多个进程组。
|
||||
</li>
|
||||
|
||||
比如,我们通过 SSH 登录服务器,就会打开一个控制终端(TTY),这个控制终端就对应一个会话。而我们在终端中运行的命令以及它们的子进程,就构成了一个个的进程组,其中,在后台运行的命令,构成后台进程组;在前台运行的命令,构成前台进程组。
|
||||
|
||||
明白了这些,我们再用 top 看一下系统的资源使用情况:
|
||||
|
||||
```
|
||||
# 按下数字 1 切换到所有 CPU 的使用情况,观察一会儿按 Ctrl+C 结束
|
||||
$ top
|
||||
top - 05:56:23 up 17 days, 16:45, 2 users, load average: 2.00, 1.68, 1.39
|
||||
Tasks: 247 total, 1 running, 79 sleeping, 0 stopped, 115 zombie
|
||||
%Cpu0 : 0.0 us, 0.7 sy, 0.0 ni, 38.9 id, 60.5 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu1 : 0.0 us, 0.7 sy, 0.0 ni, 4.7 id, 94.6 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
...
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
4340 root 20 0 44676 4048 3432 R 0.3 0.0 0:00.05 top
|
||||
4345 root 20 0 37280 33624 860 D 0.3 0.0 0:00.01 app
|
||||
4344 root 20 0 37280 33624 860 D 0.3 0.4 0:00.01 app
|
||||
1 root 20 0 160072 9416 6752 S 0.0 0.1 0:38.59 systemd
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
从这里你能看出什么问题吗?细心一点,逐行观察,别放过任何一个地方。忘了哪行参数意思的话,也要及时返回去复习。
|
||||
|
||||
好的,如果你已经有了答案,那就继续往下走,看看跟我找的问题是否一样。这里,我发现了四个可疑的地方。
|
||||
|
||||
<li>
|
||||
先看第一行的平均负载( Load Average),过去1 分钟、5 分钟和 15 分钟内的平均负载在依次减小,说明平均负载正在升高;而 1 分钟内的平均负载已经达到系统的 CPU 个数,说明系统很可能已经有了性能瓶颈。
|
||||
</li>
|
||||
<li>
|
||||
再看第二行的 Tasks,有 1 个正在运行的进程,但僵尸进程比较多,而且还在不停增加,说明有子进程在退出时没被清理。
|
||||
</li>
|
||||
<li>
|
||||
接下来看两个 CPU 的使用率情况,用户 CPU 和系统 CPU 都不高,但 iowait 分别是 60.5% 和 94.6%,好像有点儿不正常。
|
||||
</li>
|
||||
<li>
|
||||
最后再看每个进程的情况, CPU 使用率最高的进程只有 0.3%,看起来并不高;但有两个进程处于 D 状态,它们可能在等待 I/O,但光凭这里并不能确定是它们导致了 iowait 升高。
|
||||
</li>
|
||||
|
||||
我们把这四个问题再汇总一下,就可以得到很明确的两点:
|
||||
|
||||
<li>
|
||||
第一点,iowait 太高了,导致系统的平均负载升高,甚至达到了系统 CPU 的个数。
|
||||
</li>
|
||||
<li>
|
||||
第二点,僵尸进程在不断增多,说明有程序没能正确清理子进程的资源。
|
||||
</li>
|
||||
|
||||
那么,碰到这两个问题该怎么办呢?结合我们前面分析问题的思路,你先自己想想,动手试试,下节课我来继续“分解”。
|
||||
|
||||
## 小结
|
||||
|
||||
今天我们主要通过简单的操作,熟悉了几个必备的进程状态。用我们最熟悉的 ps 或者 top ,可以查看进程的状态,这些状态包括运行(R)、空闲(I)、不可中断睡眠(D)、可中断睡眠(S)、僵尸(Z)以及暂停(T)等。
|
||||
|
||||
其中,不可中断状态和僵尸状态,是我们今天学习的重点。
|
||||
|
||||
<li>
|
||||
不可中断状态,表示进程正在跟硬件交互,为了保护进程数据和硬件的一致性,系统不允许其他进程或中断打断这个进程。进程长时间处于不可中断状态,通常表示系统有 I/O 性能问题。
|
||||
</li>
|
||||
<li>
|
||||
僵尸进程表示进程已经退出,但它的父进程还没有回收子进程占用的资源。短暂的僵尸状态我们通常不必理会,但进程长时间处于僵尸状态,就应该注意了,可能有应用程序没有正常处理子进程的退出。
|
||||
</li>
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想请你思考一下今天的课后题,案例中发现的这两个问题,你会怎么分析呢?又应该怎么解决呢?你可以结合前面我们做过的案例分析,总结自己的思路,提出自己的问题。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/56/52/565d66d658ad23b2f4997551db153852.jpg" alt="">
|
||||
318
极客时间专栏/Linux性能优化实战/CPU 性能篇/08 | 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(下).md
Normal file
318
极客时间专栏/Linux性能优化实战/CPU 性能篇/08 | 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(下).md
Normal file
@@ -0,0 +1,318 @@
|
||||
<audio id="audio" title="08 | 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/84/6a/847b3639dfcc34379dc49eaea2c5626a.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节,我给你讲了Linux进程状态的含义,以及不可中断进程和僵尸进程产生的原因,我们先来简单复习下。
|
||||
|
||||
使用 ps 或者 top 可以查看进程的状态,这些状态包括运行、空闲、不可中断睡眠、可中断睡眠、僵尸以及暂停等。其中,我们重点学习了不可中断状态和僵尸进程:
|
||||
|
||||
<li>
|
||||
不可中断状态,一般表示进程正在跟硬件交互,为了保护进程数据与硬件一致,系统不允许其他进程或中断打断该进程。
|
||||
</li>
|
||||
<li>
|
||||
僵尸进程表示进程已经退出,但它的父进程没有回收该进程所占用的资源。
|
||||
</li>
|
||||
|
||||
上一节的最后,我用一个案例展示了处于这两种状态的进程。通过分析 top 命令的输出,我们发现了两个问题:
|
||||
|
||||
<li>
|
||||
第一,iowait 太高了,导致系统平均负载升高,并且已经达到了系统 CPU 的个数。
|
||||
</li>
|
||||
<li>
|
||||
第二,僵尸进程在不断增多,看起来是应用程序没有正确清理子进程的资源。
|
||||
</li>
|
||||
|
||||
相信你一定认真思考过这两个问题,那么,真相到底是什么呢?接下来,我们一起顺着这两个问题继续分析,找出根源。
|
||||
|
||||
首先,请你打开一个终端,登录到上次的机器中。然后执行下面的命令,重新运行这个案例:
|
||||
|
||||
```
|
||||
# 先删除上次启动的案例
|
||||
$ docker rm -f app
|
||||
# 重新运行案例
|
||||
$ docker run --privileged --name=app -itd feisky/app:iowait
|
||||
|
||||
```
|
||||
|
||||
## iowait 分析
|
||||
|
||||
我们先来看一下 iowait 升高的问题。
|
||||
|
||||
我相信,一提到 iowait 升高,你首先会想要查询系统的 I/O 情况。我一般也是这种思路,那么什么工具可以查询系统的 I/O 情况呢?
|
||||
|
||||
这里,我推荐的正是上节课要求安装的 dstat ,它的好处是,可以同时查看 CPU 和 I/O 这两种资源的使用情况,便于对比分析。
|
||||
|
||||
那么,我们在终端中运行 dstat 命令,观察 CPU 和 I/O 的使用情况:
|
||||
|
||||
```
|
||||
# 间隔1秒输出10组数据
|
||||
$ dstat 1 10
|
||||
You did not select any stats, using -cdngy by default.
|
||||
--total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system--
|
||||
usr sys idl wai stl| read writ| recv send| in out | int csw
|
||||
0 0 96 4 0|1219k 408k| 0 0 | 0 0 | 42 885
|
||||
0 0 2 98 0| 34M 0 | 198B 790B| 0 0 | 42 138
|
||||
0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 42 135
|
||||
0 0 84 16 0|5633k 0 | 66B 342B| 0 0 | 52 177
|
||||
0 3 39 58 0| 22M 0 | 66B 342B| 0 0 | 43 144
|
||||
0 0 0 100 0| 34M 0 | 200B 450B| 0 0 | 46 147
|
||||
0 0 2 98 0| 34M 0 | 66B 342B| 0 0 | 45 134
|
||||
0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 39 131
|
||||
0 0 83 17 0|5633k 0 | 66B 342B| 0 0 | 46 168
|
||||
0 3 39 59 0| 22M 0 | 66B 342B| 0 0 | 37 134
|
||||
|
||||
```
|
||||
|
||||
从 dstat 的输出,我们可以看到,每当 iowait 升高(wai)时,磁盘的读请求(read)都会很大。这说明 iowait 的升高跟磁盘的读请求有关,很可能就是磁盘读导致的。
|
||||
|
||||
那到底是哪个进程在读磁盘呢?不知道你还记不记得,上节在 top 里看到的不可中断状态进程,我觉得它就很可疑,我们试着来分析下。
|
||||
|
||||
我们继续在刚才的终端中,运行 top 命令,观察 D 状态的进程:
|
||||
|
||||
```
|
||||
# 观察一会儿按 Ctrl+C 结束
|
||||
$ top
|
||||
...
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
4340 root 20 0 44676 4048 3432 R 0.3 0.0 0:00.05 top
|
||||
4345 root 20 0 37280 33624 860 D 0.3 0.0 0:00.01 app
|
||||
4344 root 20 0 37280 33624 860 D 0.3 0.4 0:00.01 app
|
||||
...
|
||||
|
||||
|
||||
```
|
||||
|
||||
我们从 top 的输出找到 D 状态进程的 PID,你可以发现,这个界面里有两个 D 状态的进程,PID分别是 4344 和 4345。
|
||||
|
||||
接着,我们查看这些进程的磁盘读写情况。对了,别忘了工具是什么。一般要查看某一个进程的资源使用情况,都可以用我们的老朋友 pidstat,不过这次记得加上 -d 参数,以便输出 I/O 使用情况。
|
||||
|
||||
比如,以 4344 为例,我们在终端里运行下面的 pidstat 命令,并用 -p 4344 参数指定进程号:
|
||||
|
||||
```
|
||||
# -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒输出 3 组数据
|
||||
$ pidstat -d -p 4344 1 3
|
||||
06:38:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
06:38:51 0 4344 0.00 0.00 0.00 0 app
|
||||
06:38:52 0 4344 0.00 0.00 0.00 0 app
|
||||
06:38:53 0 4344 0.00 0.00 0.00 0 app
|
||||
|
||||
```
|
||||
|
||||
在这个输出中, kB_rd 表示每秒读的 KB 数, kB_wr 表示每秒写的 KB 数,iodelay 表示 I/O 的延迟(单位是时钟周期)。它们都是 0,那就表示此时没有任何的读写,说明问题不是 4344 进程导致的。
|
||||
|
||||
可是,用同样的方法分析进程 4345,你会发现,它也没有任何磁盘读写。
|
||||
|
||||
那要怎么知道,到底是哪个进程在进行磁盘读写呢?我们继续使用 pidstat,但这次去掉进程号,干脆就来观察所有进程的 I/O 使用情况。
|
||||
|
||||
在终端中运行下面的 pidstat 命令:
|
||||
|
||||
```
|
||||
# 间隔 1 秒输出多组数据 (这里是 20 组)
|
||||
$ pidstat -d 1 20
|
||||
...
|
||||
06:48:46 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
06:48:47 0 4615 0.00 0.00 0.00 1 kworker/u4:1
|
||||
06:48:47 0 6080 32768.00 0.00 0.00 170 app
|
||||
06:48:47 0 6081 32768.00 0.00 0.00 184 app
|
||||
|
||||
06:48:47 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
06:48:48 0 6080 0.00 0.00 0.00 110 app
|
||||
|
||||
06:48:48 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
06:48:49 0 6081 0.00 0.00 0.00 191 app
|
||||
|
||||
06:48:49 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
|
||||
06:48:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
06:48:51 0 6082 32768.00 0.00 0.00 0 app
|
||||
06:48:51 0 6083 32768.00 0.00 0.00 0 app
|
||||
|
||||
06:48:51 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
06:48:52 0 6082 32768.00 0.00 0.00 184 app
|
||||
06:48:52 0 6083 32768.00 0.00 0.00 175 app
|
||||
|
||||
06:48:52 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
06:48:53 0 6083 0.00 0.00 0.00 105 app
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
观察一会儿可以发现,的确是 app 进程在进行磁盘读,并且每秒读的数据有 32 MB,看来就是 app 的问题。不过,app 进程到底在执行啥 I/O 操作呢?
|
||||
|
||||
这里,我们需要回顾一下进程用户态和内核态的区别。进程想要访问磁盘,就必须使用系统调用,所以接下来,重点就是找出 app 进程的系统调用了。
|
||||
|
||||
strace 正是最常用的跟踪进程系统调用的工具。所以,我们从 pidstat 的输出中拿到进程的 PID 号,比如 6082,然后在终端中运行 strace 命令,并用 -p 参数指定 PID 号:
|
||||
|
||||
```
|
||||
$ strace -p 6082
|
||||
strace: attach: ptrace(PTRACE_SEIZE, 6082): Operation not permitted
|
||||
|
||||
```
|
||||
|
||||
这儿出现了一个奇怪的错误,strace 命令居然失败了,并且命令报出的错误是没有权限。按理来说,我们所有操作都已经是以 root 用户运行了,为什么还会没有权限呢?你也可以先想一下,碰到这种情况,你会怎么处理呢?
|
||||
|
||||
**一般遇到这种问题时,我会先检查一下进程的状态是否正常**。比如,继续在终端中运行 ps 命令,并使用 grep 找出刚才的 6082 号进程:
|
||||
|
||||
```
|
||||
$ ps aux | grep 6082
|
||||
root 6082 0.0 0.0 0 0 pts/0 Z+ 13:43 0:00 [app] <defunct>
|
||||
|
||||
```
|
||||
|
||||
果然,进程 6082 已经变成了 Z 状态,也就是僵尸进程。僵尸进程都是已经退出的进程,所以就没法儿继续分析它的系统调用。关于僵尸进程的处理方法,我们一会儿再说,现在还是继续分析 iowait 的问题。
|
||||
|
||||
到这一步,你应该注意到了,系统 iowait 的问题还在继续,但是 top、pidstat 这类工具已经不能给出更多的信息了。这时,我们就应该求助那些基于事件记录的动态追踪工具了。
|
||||
|
||||
你可以用 perf top 看看有没有新发现。再或者,可以像我一样,在终端中运行 perf record,持续一会儿(例如 15 秒),然后按 Ctrl+C 退出,再运行 perf report 查看报告:
|
||||
|
||||
```
|
||||
$ perf record -g
|
||||
$ perf report
|
||||
|
||||
```
|
||||
|
||||
接着,找到我们关注的 app 进程,按回车键展开调用栈,你就会得到下面这张调用关系图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/21/a1/21e79416e946ed049317a4b4c5a576a1.png" alt="">
|
||||
|
||||
这个图里的swapper 是内核中的调度进程,你可以先忽略掉。
|
||||
|
||||
我们来看其他信息,你可以发现, app 的确在通过系统调用 sys_read() 读取数据。并且从 new_sync_read 和 blkdev_direct_IO 能看出,进程正在对磁盘进行**直接读**,也就是绕过了系统缓存,每个读请求都会从磁盘直接读,这就可以解释我们观察到的 iowait 升高了。
|
||||
|
||||
看来,罪魁祸首是 app 内部进行了磁盘的直接 I/O啊!
|
||||
|
||||
下面的问题就容易解决了。我们接下来应该从代码层面分析,究竟是哪里出现了直接读请求。查看源码文件 [app.c](https://github.com/feiskyer/linux-perf-examples/blob/master/high-iowait-process/app.c),你会发现它果然使用了 O_DIRECT 选项打开磁盘,于是绕过了系统缓存,直接对磁盘进行读写。
|
||||
|
||||
```
|
||||
open(disk, O_RDONLY|O_DIRECT|O_LARGEFILE, 0755)
|
||||
|
||||
```
|
||||
|
||||
直接读写磁盘,对 I/O 敏感型应用(比如数据库系统)是很友好的,因为你可以在应用中,直接控制磁盘的读写。但在大部分情况下,我们最好还是通过系统缓存来优化磁盘 I/O,换句话说,删除 O_DIRECT 这个选项就是了。
|
||||
|
||||
[app-fix1.c](https://github.com/feiskyer/linux-perf-examples/blob/master/high-iowait-process/app-fix1.c) 就是修改后的文件,我也打包成了一个镜像文件,运行下面的命令,你就可以启动它了:
|
||||
|
||||
```
|
||||
# 首先删除原来的应用
|
||||
$ docker rm -f app
|
||||
# 运行新的应用
|
||||
$ docker run --privileged --name=app -itd feisky/app:iowait-fix1
|
||||
|
||||
```
|
||||
|
||||
最后,再用 top 检查一下:
|
||||
|
||||
```
|
||||
$ top
|
||||
top - 14:59:32 up 19 min, 1 user, load average: 0.15, 0.07, 0.05
|
||||
Tasks: 137 total, 1 running, 72 sleeping, 0 stopped, 12 zombie
|
||||
%Cpu0 : 0.0 us, 1.7 sy, 0.0 ni, 98.0 id, 0.3 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu1 : 0.0 us, 1.3 sy, 0.0 ni, 98.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
...
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
3084 root 20 0 0 0 0 Z 1.3 0.0 0:00.04 app
|
||||
3085 root 20 0 0 0 0 Z 1.3 0.0 0:00.04 app
|
||||
1 root 20 0 159848 9120 6724 S 0.0 0.1 0:09.03 systemd
|
||||
2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
|
||||
3 root 20 0 0 0 0 I 0.0 0.0 0:00.40 kworker/0:0
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
你会发现, iowait 已经非常低了,只有 0.3%,说明刚才的改动已经成功修复了 iowait 高的问题,大功告成!不过,别忘了,僵尸进程还在等着你。仔细观察僵尸进程的数量,你会郁闷地发现,僵尸进程还在不断的增长中。
|
||||
|
||||
## 僵尸进程
|
||||
|
||||
接下来,我们就来处理僵尸进程的问题。既然僵尸进程是因为父进程没有回收子进程的资源而出现的,那么,要解决掉它们,就要找到它们的根儿,**也就是找出父进程,然后在父进程里解决。**
|
||||
|
||||
父进程的找法我们前面讲过,最简单的就是运行 pstree 命令:
|
||||
|
||||
```
|
||||
# -a 表示输出命令行选项
|
||||
# p表PID
|
||||
# s表示指定进程的父进程
|
||||
$ pstree -aps 3084
|
||||
systemd,1
|
||||
└─dockerd,15006 -H fd://
|
||||
└─docker-containe,15024 --config /var/run/docker/containerd/containerd.toml
|
||||
└─docker-containe,3991 -namespace moby -workdir...
|
||||
└─app,4009
|
||||
└─(app,3084)
|
||||
|
||||
```
|
||||
|
||||
运行完,你会发现 3084 号进程的父进程是 4009,也就是 app 应用。
|
||||
|
||||
所以,我们接着查看 app 应用程序的代码,看看子进程结束的处理是否正确,比如有没有调用 wait() 或 waitpid() ,抑或是,有没有注册 SIGCHLD 信号的处理函数。
|
||||
|
||||
现在我们查看修复 iowait 后的源码文件 [app-fix1.c](https://github.com/feiskyer/linux-perf-examples/blob/master/high-iowait-process/app-fix1.c) ,找到子进程的创建和清理的地方:
|
||||
|
||||
```
|
||||
int status = 0;
|
||||
for (;;) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if(fork()== 0) {
|
||||
sub_process();
|
||||
}
|
||||
}
|
||||
sleep(5);
|
||||
}
|
||||
|
||||
while(wait(&status)>0);
|
||||
|
||||
```
|
||||
|
||||
循环语句本来就容易出错,你能找到这里的问题吗?这段代码虽然看起来调用了 wait() 函数等待子进程结束,但却错误地把 wait() 放到了 for 死循环的外面,也就是说,wait() 函数实际上并没被调用到,我们把它挪到 for 循环的里面就可以了。
|
||||
|
||||
修改后的文件我放到了 [app-fix2.c](https://github.com/feiskyer/linux-perf-examples/blob/master/high-iowait-process/app-fix2.c) 中,也打包成了一个 Docker 镜像,运行下面的命令,你就可以启动它:
|
||||
|
||||
```
|
||||
# 先停止产生僵尸进程的 app
|
||||
$ docker rm -f app
|
||||
# 然后启动新的 app
|
||||
$ docker run --privileged --name=app -itd feisky/app:iowait-fix2
|
||||
|
||||
```
|
||||
|
||||
启动后,再用 top 最后来检查一遍:
|
||||
|
||||
```
|
||||
$ top
|
||||
top - 15:00:44 up 20 min, 1 user, load average: 0.05, 0.05, 0.04
|
||||
Tasks: 125 total, 1 running, 72 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 0.0 us, 1.7 sy, 0.0 ni, 98.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu1 : 0.0 us, 1.3 sy, 0.0 ni, 98.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
...
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
3198 root 20 0 4376 840 780 S 0.3 0.0 0:00.01 app
|
||||
2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
|
||||
3 root 20 0 0 0 0 I 0.0 0.0 0:00.41 kworker/0:0
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
好了,僵尸进程(Z状态)没有了, iowait 也是 0,问题终于全部解决了。
|
||||
|
||||
## 小结
|
||||
|
||||
今天我用一个多进程的案例,带你分析系统等待 I/O 的 CPU 使用率(也就是 iowait%)升高的情况。
|
||||
|
||||
虽然这个案例是磁盘 I/O 导致了 iowait 升高,不过, **iowait 高不一定代表I/O 有性能瓶颈。当系统中只有 I/O 类型的进程在运行时,iowait 也会很高,但实际上,磁盘的读写远没有达到性能瓶颈的程度**。
|
||||
|
||||
因此,碰到 iowait 升高时,需要先用 dstat、pidstat 等工具,确认是不是磁盘 I/O 的问题,然后再找是哪些进程导致了 I/O。
|
||||
|
||||
等待 I/O 的进程一般是不可中断状态,所以用 ps 命令找到的 D 状态(即不可中断状态)的进程,多为可疑进程。但这个案例中,在 I/O 操作后,进程又变成了僵尸进程,所以不能用 strace 直接分析这个进程的系统调用。
|
||||
|
||||
这种情况下,我们用了 perf 工具,来分析系统的 CPU 时钟事件,最终发现是直接 I/O 导致的问题。这时,再检查源码中对应位置的问题,就很轻松了。
|
||||
|
||||
而僵尸进程的问题相对容易排查,使用 pstree 找出父进程后,去查看父进程的代码,检查 wait() / waitpid() 的调用,或是 SIGCHLD 信号处理函数的注册就行了。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想邀请你一起来聊聊,你碰到过的不可中断状态进程和僵尸进程问题。你是怎么分析它们的根源?又是怎么解决的?在今天的案例操作中,你又有什么新的发现吗?你可以结合我的讲述,总结自己的思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/56/52/565d66d658ad23b2f4997551db153852.jpg" alt="">
|
||||
144
极客时间专栏/Linux性能优化实战/CPU 性能篇/09 | 基础篇:怎么理解Linux软中断?.md
Normal file
144
极客时间专栏/Linux性能优化实战/CPU 性能篇/09 | 基础篇:怎么理解Linux软中断?.md
Normal file
@@ -0,0 +1,144 @@
|
||||
<audio id="audio" title="09 | 基础篇:怎么理解Linux软中断?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/61/bb/61b8d75070507d09726fafc5999521bb.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一期,我用一个不可中断进程的案例,带你学习了iowait(也就是等待I/O的CPU使用率)升高时的分析方法。这里你要记住,进程的不可中断状态是系统的一种保护机制,可以保证硬件的交互过程不被意外打断。所以,短时间的不可中断状态是很正常的。
|
||||
|
||||
但是,当进程长时间都处于不可中断状态时,你就得当心了。这时,你可以使用 dstat、pidstat 等工具,确认是不是磁盘 I/O 的问题,进而排查相关的进程和磁盘设备。关于磁盘 I/O 的性能问题,你暂且不用专门去背,我会在后续的 I/O 部分详细介绍,到时候理解了也就记住了。
|
||||
|
||||
其实除了 iowait,软中断(softirq)CPU使用率升高也是最常见的一种性能问题。接下来的两节课,我们就来学习软中断的内容,我还会以最常见的反向代理服务器 Nginx 的案例,带你分析这种情况。
|
||||
|
||||
## 从“取外卖”看中断
|
||||
|
||||
说到中断,我在前面[关于“上下文切换”的文章](https://time.geekbang.org/column/article/69859),简单说过中断的含义,先来回顾一下。中断是系统用来响应硬件设备请求的一种机制,它会打断进程的正常调度和执行,然后调用内核中的中断处理程序来响应设备的请求。
|
||||
|
||||
你可能要问了,为什么要有中断呢?我可以举个生活中的例子,让你感受一下中断的魅力。
|
||||
|
||||
比如说你订了一份外卖,但是不确定外卖什么时候送到,也没有别的方法了解外卖的进度,但是,配送员送外卖是不等人的,到了你这儿没人取的话,就直接走人了。所以你只能苦苦等着,时不时去门口看看外卖送到没,而不能干其他事情。
|
||||
|
||||
不过呢,如果在订外卖的时候,你就跟配送员约定好,让他送到后给你打个电话,那你就不用苦苦等待了,就可以去忙别的事情,直到电话一响,接电话、取外卖就可以了。
|
||||
|
||||
这里的“打电话”,其实就是一个中断。没接到电话的时候,你可以做其他的事情;只有接到了电话(也就是发生中断),你才要进行另一个动作:取外卖。
|
||||
|
||||
这个例子你就可以发现,**中断其实是一种异步的事件处理机制,可以提高系统的并发处理能力**。
|
||||
|
||||
由于中断处理程序会打断其他进程的运行,所以,**为了减少对正常进程运行调度的影响,中断处理程序就需要尽可能快地运行**。如果中断本身要做的事情不多,那么处理起来也不会有太大问题;但如果中断要处理的事情很多,中断服务程序就有可能要运行很长时间。
|
||||
|
||||
特别是,中断处理程序在响应中断时,还会临时关闭中断。这就会导致上一次中断处理完成之前,其他中断都不能响应,也就是说中断有可能会丢失。
|
||||
|
||||
那么还是以取外卖为例。假如你订了 2 份外卖,一份主食和一份饮料,并且是由2个不同的配送员来配送。这次你不用时时等待着,两份外卖都约定了电话取外卖的方式。但是,问题又来了。
|
||||
|
||||
当第一份外卖送到时,配送员给你打了个长长的电话,商量发票的处理方式。与此同时,第二个配送员也到了,也想给你打电话。
|
||||
|
||||
但是很明显,因为电话占线(也就是关闭了中断响应),第二个配送员的电话是打不通的。所以,第二个配送员很可能试几次后就走掉了(也就是丢失了一次中断)。
|
||||
|
||||
## 软中断
|
||||
|
||||
如果你弄清楚了“取外卖”的模式,那对系统的中断机制就很容易理解了。事实上,为了解决中断处理程序执行过长和中断丢失的问题,Linux 将中断处理过程分成了两个阶段,也就是**上半部和下半部**:
|
||||
|
||||
<li>
|
||||
**上半部用来快速处理中断**,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。
|
||||
</li>
|
||||
<li>
|
||||
**下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行**。
|
||||
</li>
|
||||
|
||||
比如说前面取外卖的例子,上半部就是你接听电话,告诉配送员你已经知道了,其他事儿见面再说,然后电话就可以挂断了;下半部才是取外卖的动作,以及见面后商量发票处理的动作。
|
||||
|
||||
这样,第一个配送员不会占用你太多时间,当第二个配送员过来时,照样能正常打通你的电话。
|
||||
|
||||
除了取外卖,我再举个最常见的网卡接收数据包的例子,让你更好地理解。
|
||||
|
||||
网卡接收到数据包后,会通过**硬件中断**的方式,通知内核有新的数据到了。这时,内核就应该调用中断处理程序来响应它。你可以自己先想一下,这种情况下的上半部和下半部分别负责什么工作呢?
|
||||
|
||||
对上半部来说,既然是快速处理,其实就是要把网卡的数据读到内存中,然后更新一下硬件寄存器的状态(表示数据已经读好了),最后再发送一个**软中断**信号,通知下半部做进一步的处理。
|
||||
|
||||
而下半部被软中断信号唤醒后,需要从内存中找到网络数据,再按照网络协议栈,对数据进行逐层解析和处理,直到把它送给应用程序。
|
||||
|
||||
所以,这两个阶段你也可以这样理解:
|
||||
|
||||
<li>
|
||||
上半部直接处理硬件请求,也就是我们常说的硬中断,特点是快速执行;
|
||||
</li>
|
||||
<li>
|
||||
而下半部则是由内核触发,也就是我们常说的软中断,特点是延迟执行。
|
||||
</li>
|
||||
|
||||
实际上,上半部会打断CPU正在执行的任务,然后立即执行中断处理程序。而下半部以内核线程的方式执行,并且每个 CPU 都对应一个软中断内核线程,名字为 “ksoftirqd/CPU编号”,比如说, 0 号CPU对应的软中断内核线程的名字就是 ksoftirqd/0。
|
||||
|
||||
不过要注意的是,软中断不只包括了刚刚所讲的硬件设备中断处理程序的下半部,一些内核自定义的事件也属于软中断,比如内核调度和RCU锁(Read-Copy Update 的缩写,RCU 是 Linux 内核中最常用的锁之一)等。
|
||||
|
||||
那要怎么知道你的系统里有哪些软中断呢?
|
||||
|
||||
## 查看软中断和内核线程
|
||||
|
||||
不知道你还记不记得,前面提到过的 proc 文件系统。它是一种内核空间和用户空间进行通信的机制,可以用来查看内核的数据结构,或者用来动态修改内核的配置。其中:
|
||||
|
||||
<li>
|
||||
/proc/softirqs 提供了软中断的运行情况;
|
||||
</li>
|
||||
<li>
|
||||
/proc/interrupts 提供了硬中断的运行情况。
|
||||
</li>
|
||||
|
||||
运行下面的命令,查看 /proc/softirqs 文件的内容,你就可以看到各种类型软中断在不同 CPU 上的累积运行次数:
|
||||
|
||||
```
|
||||
$ cat /proc/softirqs
|
||||
CPU0 CPU1
|
||||
HI: 0 0
|
||||
TIMER: 811613 1972736
|
||||
NET_TX: 49 7
|
||||
NET_RX: 1136736 1506885
|
||||
BLOCK: 0 0
|
||||
IRQ_POLL: 0 0
|
||||
TASKLET: 304787 3691
|
||||
SCHED: 689718 1897539
|
||||
HRTIMER: 0 0
|
||||
RCU: 1330771 1354737
|
||||
|
||||
```
|
||||
|
||||
在查看 /proc/softirqs 文件内容时,你要特别注意以下这两点。
|
||||
|
||||
第一,要注意软中断的类型,也就是这个界面中第一列的内容。从第一列你可以看到,软中断包括了10个类别,分别对应不同的工作类型。比如 NET_RX 表示网络接收中断,而 NET_TX 表示网络发送中断。
|
||||
|
||||
第二,要注意同一种软中断在不同 CPU 上的分布情况,也就是同一行的内容。正常情况下,同一种中断在不同 CPU 上的累积次数应该差不多。比如这个界面中,NET_RX 在 CPU0 和 CPU1 上的中断次数基本是同一个数量级,相差不大。
|
||||
|
||||
不过你可能发现,TASKLET 在不同CPU上的分布并不均匀。TASKLET 是最常用的软中断实现机制,每个 TASKLET 只运行一次就会结束 ,并且只在调用它的函数所在的 CPU 上运行。
|
||||
|
||||
因此,使用 TASKLET 特别简便,当然也会存在一些问题,比如说由于只在一个CPU上运行导致的调度不均衡,再比如因为不能在多个 CPU 上并行运行带来了性能限制。
|
||||
|
||||
另外,刚刚提到过,软中断实际上是以内核线程的方式运行的,每个 CPU 都对应一个软中断内核线程,这个软中断内核线程就叫做 ksoftirqd/CPU编号。那要怎么查看这些线程的运行状况呢?
|
||||
|
||||
其实用 ps 命令就可以做到,比如执行下面的指令:
|
||||
|
||||
```
|
||||
$ ps aux | grep softirq
|
||||
root 7 0.0 0.0 0 0 ? S Oct10 0:01 [ksoftirqd/0]
|
||||
root 16 0.0 0.0 0 0 ? S Oct10 0:01 [ksoftirqd/1]
|
||||
|
||||
```
|
||||
|
||||
注意,这些线程的名字外面都有中括号,这说明 ps 无法获取它们的命令行参数(cmline)。一般来说,ps 的输出中,名字括在中括号里的,一般都是内核线程。
|
||||
|
||||
## 小结
|
||||
|
||||
Linux 中的中断处理程序分为上半部和下半部:
|
||||
|
||||
<li>
|
||||
上半部对应硬件中断,用来快速处理中断。
|
||||
</li>
|
||||
<li>
|
||||
下半部对应软中断,用来异步处理上半部未完成的工作。
|
||||
</li>
|
||||
|
||||
Linux 中的软中断包括网络收发、定时、调度、RCU锁等各种类型,可以通过查看 /proc/softirqs 来观察软中断的运行情况。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想请你一起聊聊,你是怎么理解软中断的?你有没有碰到过因为软中断出现的性能问题?你又是怎么分析它们的瓶颈的呢?你可以结合今天的内容,总结自己的思路,写下自己的问题。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/56/52/565d66d658ad23b2f4997551db153852.jpg" alt="">
|
||||
268
极客时间专栏/Linux性能优化实战/CPU 性能篇/10 | 案例篇:系统的软中断CPU使用率升高,我该怎么办?.md
Normal file
268
极客时间专栏/Linux性能优化实战/CPU 性能篇/10 | 案例篇:系统的软中断CPU使用率升高,我该怎么办?.md
Normal file
@@ -0,0 +1,268 @@
|
||||
<audio id="audio" title="10 | 案例篇:系统的软中断CPU使用率升高,我该怎么办?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/01/c2/019804f0aadcebd7e9f44d3d31b2a0c2.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一期我给你讲了软中断的基本原理,我们先来简单复习下。
|
||||
|
||||
中断是一种异步的事件处理机制,用来提高系统的并发处理能力。中断事件发生,会触发执行中断处理程序,而中断处理程序被分为上半部和下半部这两个部分。
|
||||
|
||||
<li>
|
||||
上半部对应硬中断,用来快速处理中断;
|
||||
</li>
|
||||
<li>
|
||||
下半部对应软中断,用来异步处理上半部未完成的工作。
|
||||
</li>
|
||||
|
||||
Linux 中的软中断包括网络收发、定时、调度、RCU锁等各种类型,我们可以查看 proc 文件系统中的 /proc/softirqs ,观察软中断的运行情况。
|
||||
|
||||
在 Linux 中,每个 CPU 都对应一个软中断内核线程,名字是 ksoftirqd/CPU编号。当软中断事件的频率过高时,内核线程也会因为CPU 使用率过高而导致软中断处理不及时,进而引发网络收发延迟、调度缓慢等性能问题。
|
||||
|
||||
软中断 CPU 使用率过高也是一种最常见的性能问题。今天,我就用最常见的反向代理服务器 Nginx 的案例,教你学会分析这种情况。
|
||||
|
||||
## 案例
|
||||
|
||||
### 你的准备
|
||||
|
||||
接下来的案例基于 Ubuntu 18.04,也同样适用于其他的 Linux 系统。我使用的案例环境是这样的:
|
||||
|
||||
<li>
|
||||
机器配置:2 CPU、8 GB 内存。
|
||||
</li>
|
||||
<li>
|
||||
预先安装 docker、sysstat、sar 、hping3、tcpdump 等工具,比如 apt-get install [docker.io](http://docker.io) sysstat hping3 tcpdump。
|
||||
</li>
|
||||
|
||||
这里我又用到了三个新工具,sar、 hping3 和 tcpdump,先简单介绍一下:
|
||||
|
||||
<li>
|
||||
sar 是一个系统活动报告工具,既可以实时查看系统的当前活动,又可以配置保存和报告历史统计数据。
|
||||
</li>
|
||||
<li>
|
||||
hping3 是一个可以构造 TCP/IP 协议数据包的工具,可以对系统进行安全审计、防火墙测试等。
|
||||
</li>
|
||||
<li>
|
||||
tcpdump 是一个常用的网络抓包工具,常用来分析各种网络问题。
|
||||
</li>
|
||||
|
||||
本次案例用到两台虚拟机,我画了一张图来表示它们的关系。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5f/96/5f9487847e937f955ebc2ec86d490b96.png" alt="">
|
||||
|
||||
你可以看到,其中一台虚拟机运行 Nginx ,用来模拟待分析的 Web 服务器;而另一台当作Web 服务器的客户端,用来给 Nginx 增加压力请求。使用两台虚拟机的目的,是为了相互隔离,避免“交叉感染”。
|
||||
|
||||
接下来,我们打开两个终端,分别 SSH 登录到两台机器上,并安装上面提到的这些工具。
|
||||
|
||||
同以前的案例一样,下面的所有命令都默认以 root 用户运行,如果你是用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
|
||||
|
||||
如果安装过程中有什么问题,同样鼓励你先自己搜索解决,解决不了的,可以在留言区向我提问。如果你以前已经安装过了,就可以忽略这一点了。
|
||||
|
||||
### 操作和分析
|
||||
|
||||
安装完成后,我们先在第一个终端,执行下面的命令运行案例,也就是一个最基本的 Nginx 应用:
|
||||
|
||||
```
|
||||
# 运行Nginx服务并对外开放80端口
|
||||
$ docker run -itd --name=nginx -p 80:80 nginx
|
||||
|
||||
```
|
||||
|
||||
然后,在第二个终端,使用 curl 访问 Nginx 监听的端口,确认 Nginx 正常启动。假设 192.168.0.30 是 Nginx 所在虚拟机的 IP 地址,运行 curl 命令后你应该会看到下面这个输出界面:
|
||||
|
||||
```
|
||||
$ curl http://192.168.0.30/
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Welcome to nginx!</title>
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
接着,还是在第二个终端,我们运行 hping3 命令,来模拟 Nginx 的客户端请求:
|
||||
|
||||
```
|
||||
# -S参数表示设置TCP协议的SYN(同步序列号),-p表示目的端口为80
|
||||
# -i u100表示每隔100微秒发送一个网络帧
|
||||
# 注:如果你在实践过程中现象不明显,可以尝试把100调小,比如调成10甚至1
|
||||
$ hping3 -S -p 80 -i u100 192.168.0.30
|
||||
|
||||
```
|
||||
|
||||
现在我们再回到第一个终端,你应该发现了异常。是不是感觉系统响应明显变慢了,即便只是在终端中敲几个回车,都得很久才能得到响应?这个时候应该怎么办呢?
|
||||
|
||||
虽然在运行 hping3 命令时,我就已经告诉你,这是一个 SYN FLOOD 攻击,你肯定也会想到从网络方面入手,来分析这个问题。不过,在实际的生产环境中,没人直接告诉你原因。
|
||||
|
||||
所以,我希望你把 hping3 模拟 SYN FLOOD 这个操作暂时忘掉,然后重新从观察到的问题开始,分析系统的资源使用情况,逐步找出问题的根源。
|
||||
|
||||
那么,该从什么地方入手呢?刚才我们发现,简单的 SHELL 命令都明显变慢了,先看看系统的整体资源使用情况应该是个不错的注意,比如执行下 top 看看是不是出现了 CPU 的瓶颈。我们在第一个终端运行 top 命令,看一下系统整体的资源使用情况。
|
||||
|
||||
```
|
||||
# top运行后按数字1切换到显示所有CPU
|
||||
$ top
|
||||
top - 10:50:58 up 1 days, 22:10, 1 user, load average: 0.00, 0.00, 0.00
|
||||
Tasks: 122 total, 1 running, 71 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 0.0 us, 0.0 sy, 0.0 ni, 96.7 id, 0.0 wa, 0.0 hi, 3.3 si, 0.0 st
|
||||
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni, 95.6 id, 0.0 wa, 0.0 hi, 4.4 si, 0.0 st
|
||||
...
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
7 root 20 0 0 0 0 S 0.3 0.0 0:01.64 ksoftirqd/0
|
||||
16 root 20 0 0 0 0 S 0.3 0.0 0:01.97 ksoftirqd/1
|
||||
2663 root 20 0 923480 28292 13996 S 0.3 0.3 4:58.66 docker-containe
|
||||
3699 root 20 0 0 0 0 I 0.3 0.0 0:00.13 kworker/u4:0
|
||||
3708 root 20 0 44572 4176 3512 R 0.3 0.1 0:00.07 top
|
||||
1 root 20 0 225384 9136 6724 S 0.0 0.1 0:23.25 systemd
|
||||
2 root 20 0 0 0 0 S 0.0 0.0 0:00.03 kthreadd
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
这里你有没有发现异常的现象?我们从第一行开始,逐个看一下:
|
||||
|
||||
<li>
|
||||
平均负载全是0,就绪队列里面只有一个进程(1 running)。
|
||||
</li>
|
||||
<li>
|
||||
每个CPU的使用率都挺低,最高的CPU1的使用率也只有4.4%,并不算高。
|
||||
</li>
|
||||
<li>
|
||||
再看进程列表,CPU使用率最高的进程也只有 0.3%,还是不高呀。
|
||||
</li>
|
||||
|
||||
那为什么系统的响应变慢了呢?既然每个指标的数值都不大,那我们就再来看看,这些指标对应的更具体的含义。毕竟,哪怕是同一个指标,用在系统的不同部位和场景上,都有可能对应着不同的性能问题。
|
||||
|
||||
仔细看 top 的输出,两个 CPU的使用率虽然分别只有 3.3%和4.4%,但都用在了软中断上;而从进程列表上也可以看到,CPU使用率最高的也是软中断进程 ksoftirqd。看起来,软中断有点可疑了。
|
||||
|
||||
根据上一期的内容,既然软中断可能有问题,那你先要知道,究竟是哪类软中断的问题。停下来想想,上一节我们用了什么方法,来判断软中断类型呢?没错,还是proc文件系统。观察 /proc/softirqs 文件的内容,你就能知道各种软中断类型的次数。
|
||||
|
||||
不过,这里的各类软中断次数,又是什么时间段里的次数呢?它是系统运行以来的**累积中断次数**。所以我们直接查看文件内容,得到的只是累积中断次数,对这里的问题并没有直接参考意义。因为,这些**中断次数的变化速率**才是我们需要关注的。
|
||||
|
||||
那什么工具可以观察命令输出的变化情况呢?我想你应该想起来了,在前面案例中用过的 watch 命令,就可以定期运行一个命令来查看输出;如果再加上 -d 参数,还可以高亮出变化的部分,从高亮部分我们就可以直观看出,哪些内容变化得更快。
|
||||
|
||||
比如,还是在第一个终端,我们运行下面的命令:
|
||||
|
||||
```
|
||||
$ watch -d cat /proc/softirqs
|
||||
CPU0 CPU1
|
||||
HI: 0 0
|
||||
TIMER: 1083906 2368646
|
||||
NET_TX: 53 9
|
||||
NET_RX: 1550643 1916776
|
||||
BLOCK: 0 0
|
||||
IRQ_POLL: 0 0
|
||||
TASKLET: 333637 3930
|
||||
SCHED: 963675 2293171
|
||||
HRTIMER: 0 0
|
||||
RCU: 1542111 1590625
|
||||
|
||||
```
|
||||
|
||||
通过 /proc/softirqs 文件内容的变化情况,你可以发现, TIMER(定时中断)、NET_RX(网络接收)、SCHED(内核调度)、RCU(RCU锁)等这几个软中断都在不停变化。
|
||||
|
||||
其中,NET_RX,也就是网络数据包接收软中断的变化速率最快。而其他几种类型的软中断,是保证 Linux 调度、时钟和临界区保护这些正常工作所必需的,所以它们有一定的变化倒是正常的。
|
||||
|
||||
那么接下来,我们就从网络接收的软中断着手,继续分析。既然是网络接收的软中断,第一步应该就是观察系统的网络接收情况。这里你可能想起了很多网络工具,不过,我推荐今天的主人公工具 sar 。
|
||||
|
||||
sar 可以用来查看系统的网络收发情况,还有一个好处是,不仅可以观察网络收发的吞吐量(BPS,每秒收发的字节数),还可以观察网络收发的 PPS,即每秒收发的网络帧数。
|
||||
|
||||
我们在第一个终端中运行 sar 命令,并添加 -n DEV 参数显示网络收发的报告:
|
||||
|
||||
```
|
||||
# -n DEV 表示显示网络收发的报告,间隔1秒输出一组数据
|
||||
$ sar -n DEV 1
|
||||
15:03:46 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil
|
||||
15:03:47 eth0 12607.00 6304.00 664.86 358.11 0.00 0.00 0.00 0.01
|
||||
15:03:47 docker0 6302.00 12604.00 270.79 664.66 0.00 0.00 0.00 0.00
|
||||
15:03:47 lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
15:03:47 veth9f6bbcd 6302.00 12604.00 356.95 664.66 0.00 0.00 0.00 0.05
|
||||
|
||||
```
|
||||
|
||||
对于 sar 的输出界面,我先来简单介绍一下,从左往右依次是:
|
||||
|
||||
<li>
|
||||
第一列:表示报告的时间。
|
||||
</li>
|
||||
<li>
|
||||
第二列:IFACE 表示网卡。
|
||||
</li>
|
||||
<li>
|
||||
第三、四列:rxpck/s 和 txpck/s 分别表示每秒接收、发送的网络帧数,也就是 PPS。
|
||||
</li>
|
||||
<li>
|
||||
第五、六列:rxkB/s 和 txkB/s 分别表示每秒接收、发送的千字节数,也就是 BPS。
|
||||
</li>
|
||||
<li>
|
||||
后面的其他参数基本接近0,显然跟今天的问题没有直接关系,你可以先忽略掉。
|
||||
</li>
|
||||
|
||||
我们具体来看输出的内容,你可以发现:
|
||||
|
||||
<li>
|
||||
对网卡 eth0来说,每秒接收的网络帧数比较大,达到了 12607,而发送的网络帧数则比较小,只有 6304;每秒接收的千字节数只有 664 KB,而发送的千字节数更小,只有 358 KB。
|
||||
</li>
|
||||
<li>
|
||||
docker0 和 veth9f6bbcd 的数据跟 eth0 基本一致,只是发送和接收相反,发送的数据较大而接收的数据较小。这是 Linux 内部网桥转发导致的,你暂且不用深究,只要知道这是系统把 eth0 收到的包转发给 Nginx 服务即可。具体工作原理,我会在后面的网络部分详细介绍。
|
||||
</li>
|
||||
|
||||
从这些数据,你有没有发现什么异常的地方?
|
||||
|
||||
既然怀疑是网络接收中断的问题,我们还是重点来看 eth0 :接收的 PPS 比较大,达到 12607,而接收的 BPS 却很小,只有 664 KB。直观来看网络帧应该都是比较小的,我们稍微计算一下,664*1024/12607 = 54 字节,说明平均每个网络帧只有 54 字节,这显然是很小的网络帧,也就是我们通常所说的小包问题。
|
||||
|
||||
那么,有没有办法知道这是一个什么样的网络帧,以及从哪里发过来的呢?
|
||||
|
||||
使用 tcpdump 抓取 eth0 上的包就可以了。我们事先已经知道, Nginx 监听在 80 端口,它所提供的 HTTP 服务是基于 TCP 协议的,所以我们可以指定 TCP 协议和 80 端口精确抓包。
|
||||
|
||||
接下来,我们在第一个终端中运行 tcpdump 命令,通过 -i eth0 选项指定网卡 eth0,并通过 tcp port 80 选项指定 TCP 协议的 80 端口:
|
||||
|
||||
```
|
||||
# -i eth0 只抓取eth0网卡,-n不解析协议名和主机名
|
||||
# tcp port 80表示只抓取tcp协议并且端口号为80的网络帧
|
||||
$ tcpdump -i eth0 -n tcp port 80
|
||||
15:11:32.678966 IP 192.168.0.2.18238 > 192.168.0.30.80: Flags [S], seq 458303614, win 512, length 0
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
从 tcpdump 的输出中,你可以发现
|
||||
|
||||
<li>
|
||||
192.168.0.2.18238 > 192.168.0.30.80 ,表示网络帧从 192.168.0.2 的 18238 端口发送到 192.168.0.30 的 80 端口,也就是从运行 hping3 机器的 18238 端口发送网络帧,目的为 Nginx 所在机器的 80 端口。
|
||||
</li>
|
||||
<li>
|
||||
Flags [S] 则表示这是一个 SYN 包。
|
||||
</li>
|
||||
|
||||
再加上前面用 sar 发现的, PPS 超过 12000的现象,现在我们可以确认,这就是从 192.168.0.2 这个地址发送过来的 SYN FLOOD 攻击。
|
||||
|
||||
到这里,我们已经做了全套的性能诊断和分析。从系统的软中断使用率高这个现象出发,通过观察 /proc/softirqs 文件的变化情况,判断出软中断类型是网络接收中断;再通过 sar 和 tcpdump ,确认这是一个 SYN FLOOD 问题。
|
||||
|
||||
SYN FLOOD 问题最简单的解决方法,就是从交换机或者硬件防火墙中封掉来源 IP,这样 SYN FLOOD 网络帧就不会发送到服务器中。
|
||||
|
||||
至于 SYN FLOOD 的原理和更多解决思路,你暂时不需要过多关注,后面的网络章节里我们都会学到。
|
||||
|
||||
案例结束后,也不要忘了收尾,记得停止最开始启动的 Nginx 服务以及 hping3 命令。
|
||||
|
||||
在第一个终端中,运行下面的命令就可以停止 Nginx 了:
|
||||
|
||||
```
|
||||
# 停止 Nginx 服务
|
||||
$ docker rm -f nginx
|
||||
|
||||
```
|
||||
|
||||
然后到第二个终端中按下 Ctrl+C 就可以停止 hping3。
|
||||
|
||||
## 小结
|
||||
|
||||
软中断CPU使用率(softirq)升高是一种很常见的性能问题。虽然软中断的类型很多,但实际生产中,我们遇到的性能瓶颈大多是网络收发类型的软中断,特别是网络接收的软中断。
|
||||
|
||||
在碰到这类问题时,你可以借用 sar、tcpdump 等工具,做进一步分析。不要害怕网络性能,后面我会教你更多的分析方法。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想请你一起来聊聊,你所碰到的软中断问题。你所碰到的软中问题是哪种类型,是不是这个案例中的小包问题?你又是怎么分析它们的来源并解决的呢?可以结合今天的案例,总结你自己的思路和感受。如果遇到过其他问题,也可以留言给我一起解决。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/56/52/565d66d658ad23b2f4997551db153852.jpg" alt="">
|
||||
183
极客时间专栏/Linux性能优化实战/CPU 性能篇/11 | 套路篇:如何迅速分析出系统CPU的瓶颈在哪里?.md
Normal file
183
极客时间专栏/Linux性能优化实战/CPU 性能篇/11 | 套路篇:如何迅速分析出系统CPU的瓶颈在哪里?.md
Normal file
@@ -0,0 +1,183 @@
|
||||
<audio id="audio" title="11 | 套路篇:如何迅速分析出系统CPU的瓶颈在哪里?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6b/eb/6b026717ec1f3c13db39860ece0404eb.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
前几节里,我通过几个案例,带你分析了各种常见的 CPU 性能问题。通过这些,我相信你对 CPU 的性能分析已经不再陌生和恐惧,起码有了基本的思路,也了解了不少 CPU 性能的分析工具。
|
||||
|
||||
不过,我猜你可能也碰到了一个我曾有过的困惑: CPU 的性能指标那么多,CPU 性能分析工具也是一抓一大把,如果离开专栏,换成实际的工作场景,我又该观察什么指标、选择哪个性能工具呢?
|
||||
|
||||
不要担心,今天我就以多年的性能优化经验,给你总结出一个“又快又准”的瓶颈定位套路,告诉你在不同场景下,指标工具怎么选,性能瓶颈怎么找。
|
||||
|
||||
## CPU 性能指标
|
||||
|
||||
我们先来回顾下,描述 CPU 的性能指标都有哪些。你可以自己先找张纸,凭着记忆写一写;或者打开前面的文章,自己总结一下。
|
||||
|
||||
首先,**最容易想到的应该是 CPU 使用率**,这也是实际环境中最常见的一个性能指标。
|
||||
|
||||
CPU 使用率描述了非空闲时间占总 CPU 时间的百分比,根据 CPU 上运行任务的不同,又被分为用户CPU、系统CPU、等待I/O CPU、软中断和硬中断等。
|
||||
|
||||
<li>
|
||||
用户 CPU 使用率,包括用户态 CPU 使用率(user)和低优先级用户态 CPU 使用率(nice),表示 CPU 在用户态运行的时间百分比。用户 CPU 使用率高,通常说明有应用程序比较繁忙。
|
||||
</li>
|
||||
<li>
|
||||
系统 CPU 使用率,表示 CPU 在内核态运行的时间百分比(不包括中断)。系统 CPU 使用率高,说明内核比较繁忙。
|
||||
</li>
|
||||
<li>
|
||||
等待 I/O 的CPU使用率,通常也称为iowait,表示等待 I/O 的时间百分比。iowait 高,通常说明系统与硬件设备的 I/O 交互时间比较长。
|
||||
</li>
|
||||
<li>
|
||||
软中断和硬中断的 CPU 使用率,分别表示内核调用软中断处理程序、硬中断处理程序的时间百分比。它们的使用率高,通常说明系统发生了大量的中断。
|
||||
</li>
|
||||
<li>
|
||||
除了上面这些,还有在虚拟化环境中会用到的窃取 CPU 使用率(steal)和客户 CPU 使用率(guest),分别表示被其他虚拟机占用的 CPU 时间百分比,和运行客户虚拟机的 CPU 时间百分比。
|
||||
</li>
|
||||
|
||||
**第二个比较容易想到的,应该是平均负载(Load Average)**,也就是系统的平均活跃进程数。它反应了系统的整体负载情况,主要包括三个数值,分别指过去1分钟、过去5分钟和过去15分钟的平均负载。
|
||||
|
||||
理想情况下,平均负载等于逻辑 CPU 个数,这表示每个 CPU 都恰好被充分利用。如果平均负载大于逻辑CPU个数,就表示负载比较重了。
|
||||
|
||||
**第三个,也是在专栏学习前你估计不太会注意到的,进程上下文切换**,包括:
|
||||
|
||||
<li>
|
||||
无法获取资源而导致的自愿上下文切换;
|
||||
</li>
|
||||
<li>
|
||||
被系统强制调度导致的非自愿上下文切换。
|
||||
</li>
|
||||
|
||||
上下文切换,本身是保证 Linux 正常运行的一项核心功能。但过多的上下文切换,会将原本运行进程的 CPU 时间,消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,缩短进程真正运行的时间,成为性能瓶颈。
|
||||
|
||||
除了上面几种,**还有一个指标,CPU缓存的命中率**。由于CPU发展的速度远快于内存的发展,CPU的处理速度就比内存的访问速度快得多。这样,CPU在访问内存的时候,免不了要等待内存的响应。为了协调这两者巨大的性能差距,CPU缓存(通常是多级缓存)就出现了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/aa/33/aa08816b60e453b52b5fae5e63549e33.png" alt="">
|
||||
|
||||
就像上面这张图显示的,CPU缓存的速度介于CPU和内存之间,缓存的是热点的内存数据。根据不断增长的热点数据,这些缓存按照大小不同分为 L1、L2、L3 等三级缓存,其中 L1 和 L2 常用在单核中, L3 则用在多核中。
|
||||
|
||||
从 L1 到 L3,三级缓存的大小依次增大,相应的,性能依次降低(当然比内存还是好得多)。而它们的命中率,衡量的是CPU缓存的复用情况,命中率越高,则表示性能越好。
|
||||
|
||||
这些指标都很有用,需要我们熟练掌握,所以我总结成了一张图,帮你分类和记忆。你可以保存打印下来,随时查看复习,也可以当成CPU性能分析的“指标筛选”清单。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1e/07/1e66612e0022cd6c17847f3ab6989007.png" alt="">
|
||||
|
||||
## 性能工具
|
||||
|
||||
掌握了 CPU 的性能指标,我们还需要知道,怎样去获取这些指标,也就是工具的使用。
|
||||
|
||||
你还记得前面案例都用了哪些工具吗?这里我们也一起回顾一下CPU性能工具。
|
||||
|
||||
首先,平均负载的案例。我们先用 uptime, 查看了系统的平均负载;而在平均负载升高后,又用 mpstat 和 pidstat ,分别观察了每个 CPU 和每个进程 CPU 的使用情况,进而找出了导致平均负载升高的进程,也就是我们的压测工具 stress。
|
||||
|
||||
第二个,上下文切换的案例。我们先用 vmstat ,查看了系统的上下文切换次数和中断次数;然后通过 pidstat ,观察了进程的自愿上下文切换和非自愿上下文切换情况;最后通过 pidstat ,观察了线程的上下文切换情况,找出了上下文切换次数增多的根源,也就是我们的基准测试工具 sysbench。
|
||||
|
||||
第三个,进程 CPU 使用率升高的案例。我们先用 top ,查看了系统和进程的CPU使用情况,发现 CPU 使用率升高的进程是 php-fpm;再用 perf top ,观察 php-fpm 的调用链,最终找出 CPU 升高的根源,也就是库函数 sqrt() 。
|
||||
|
||||
第四个,系统的 CPU 使用率升高的案例。我们先用 top 观察到了系统CPU升高,但通过 top 和 pidstat ,却找不出高 CPU 使用率的进程。于是,我们重新审视 top 的输出,又从 CPU 使用率不高但处于 Running 状态的进程入手,找出了可疑之处,最终通过 perf record 和 perf report ,发现原来是短时进程在捣鬼。
|
||||
|
||||
另外,对于短时进程,我还介绍了一个专门的工具 execsnoop,它可以实时监控进程调用的外部命令。
|
||||
|
||||
第五个,不可中断进程和僵尸进程的案例。我们先用 top 观察到了 iowait 升高的问题,并发现了大量的不可中断进程和僵尸进程;接着我们用 dstat 发现是这是由磁盘读导致的,于是又通过 pidstat 找出了相关的进程。但我们用 strace 查看进程系统调用却失败了,最终还是用 perf 分析进程调用链,才发现根源在于磁盘直接 I/O 。
|
||||
|
||||
最后一个,软中断的案例。我们通过 top 观察到,系统的软中断 CPU 使用率升高;接着查看 /proc/softirqs, 找到了几种变化速率较快的软中断;然后通过 sar 命令,发现是网络小包的问题,最后再用 tcpdump ,找出网络帧的类型和来源,确定是一个 SYN FLOOD 攻击导致的。
|
||||
|
||||
到这里,估计你已经晕了吧,原来短短几个案例,我们已经用过十几种 CPU 性能工具了,而且每种工具的适用场景还不同呢!这么多的工具要怎么区分呢?在实际的性能分析中,又该怎么选择呢?
|
||||
|
||||
我的经验是,从两个不同的维度来理解它们,做到活学活用。
|
||||
|
||||
## 活学活用,把性能指标和性能工具联系起来
|
||||
|
||||
**第一个维度,从 CPU 的性能指标出发。也就是说,当你要查看某个性能指标时,要清楚知道哪些工具可以做到**。
|
||||
|
||||
根据不同的性能指标,对提供指标的性能工具进行分类和理解。这样,在实际排查性能问题时,你就可以清楚知道,什么工具可以提供你想要的指标,而不是毫无根据地挨个尝试,撞运气。
|
||||
|
||||
其实,我在前面的案例中已经多次用到了这个思路。比如用 top 发现了软中断 CPU 使用率高后,下一步自然就想知道具体的软中断类型。那在哪里可以观察各类软中断的运行情况呢?当然是 proc 文件系统中的 /proc/softirqs 这个文件。
|
||||
|
||||
紧接着,比如说,我们找到的软中断类型是网络接收,那就要继续往网络接收方向思考。系统的网络接收情况是什么样的?什么工具可以查到网络接收情况呢?在我们案例中,用的正是 dstat。
|
||||
|
||||
虽然你不需要把所有工具背下来,但如果能理解每个指标对应的工具的特性,一定更高效、更灵活地使用。这里,我把提供 CPU 性能指标的工具做成了一个表格,方便你梳理关系和理解记忆,当然,你也可以当成一个“指标工具”指南来使用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/59/ec/596397e1d6335d2990f70427ad4b14ec.png" alt="">
|
||||
|
||||
下面,我们再来看第二个维度。
|
||||
|
||||
**第二个维度,从工具出发。也就是当你已经安装了某个工具后,要知道这个工具能提供哪些指标**。
|
||||
|
||||
这在实际环境特别是生产环境中也是非常重要的,因为很多情况下,你并没有权限安装新的工具包,只能最大化地利用好系统中已经安装好的工具,这就需要你对它们有足够的了解。
|
||||
|
||||
具体到每个工具的使用方法,一般都支持丰富的配置选项。不过不用担心,这些配置选项并不用背下来。你只要知道有哪些工具、以及这些工具的基本功能是什么就够了。真正要用到的时候, 通过man 命令,查它们的使用手册就可以了。
|
||||
|
||||
同样的,我也将这些常用工具汇总成了一个表格,方便你区分和理解,自然,你也可以当成一个“工具指标”指南使用,需要时查表即可。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b0/ca/b0c67a7196f5ca4cc58f14f959a364ca.png" alt="">
|
||||
|
||||
## 如何迅速分析CPU的性能瓶颈
|
||||
|
||||
我相信到这一步,你对 CPU 的性能指标已经非常熟悉,也清楚每种性能指标分别能用什么工具来获取。
|
||||
|
||||
那是不是说,每次碰到 CPU 的性能问题,你都要把上面这些工具全跑一遍,然后再把所有的 CPU 性能指标全分析一遍呢?
|
||||
|
||||
你估计觉得这种简单查找的方式,就像是在傻找。不过,别笑话,因为最早的时候我就是这么做的。把所有的指标都查出来再统一分析,当然是可以的,也很可能找到系统的潜在瓶颈。
|
||||
|
||||
但是这种方法的效率真的太低了!耗时耗力不说,在庞大的指标体系面前,你一不小心可能就忽略了某个细节,导致白干一场。我就吃过好多次这样的苦。
|
||||
|
||||
所以,在实际生产环境中,我们通常都希望尽可能**快**地定位系统的瓶颈,然后尽可能**快**地优化性能,也就是要又快又准地解决性能问题。
|
||||
|
||||
那有没有什么方法,可以又快又准找出系统瓶颈呢?答案是肯定的。
|
||||
|
||||
虽然 CPU 的性能指标比较多,但要知道,既然都是描述系统的CPU性能,它们就不会是完全孤立的,很多指标间都有一定的关联。**想弄清楚性能指标的关联性,就要通晓每种性能指标的工作原理**。这也是为什么我在介绍每个性能指标时,都要穿插讲解相关的系统原理,希望你能记住这一点。
|
||||
|
||||
举个例子,用户 CPU 使用率高,我们应该去排查进程的用户态而不是内核态。因为用户 CPU 使用率反映的就是用户态的 CPU 使用情况,而内核态的 CPU 使用情况只会反映到系统 CPU 使用率上。
|
||||
|
||||
你看,有这样的基本认识,我们就可以缩小排查的范围,省时省力。
|
||||
|
||||
所以,为了**缩小排查范围,我通常会先运行几个支持指标较多的工具,如 top、vmstat 和 pidstat** 。为什么是这三个工具呢?仔细看看下面这张图,你就清楚了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7a/17/7a445960a4bc0a58a02e1bc75648aa17.png" alt="">
|
||||
|
||||
这张图里,我列出了 top、vmstat 和 pidstat 分别提供的重要的 CPU 指标,并用虚线表示关联关系,对应出了性能分析下一步的方向。
|
||||
|
||||
通过这张图你可以发现,这三个命令,几乎包含了所有重要的 CPU 性能指标,比如:
|
||||
|
||||
<li>
|
||||
从 top 的输出可以得到各种 CPU 使用率以及僵尸进程和平均负载等信息。
|
||||
</li>
|
||||
<li>
|
||||
从 vmstat 的输出可以得到上下文切换次数、中断次数、运行状态和不可中断状态的进程数。
|
||||
</li>
|
||||
<li>
|
||||
从 pidstat 的输出可以得到进程的用户 CPU 使用率、系统 CPU 使用率、以及自愿上下文切换和非自愿上下文切换情况。
|
||||
</li>
|
||||
|
||||
另外,这三个工具输出的很多指标是相互关联的,所以,我也用虚线表示了它们的关联关系,举几个例子你可能会更容易理解。
|
||||
|
||||
第一个例子,pidstat 输出的进程用户 CPU 使用率升高,会导致 top 输出的用户 CPU 使用率升高。所以,当发现 top 输出的用户 CPU 使用率有问题时,可以跟 pidstat 的输出做对比,观察是否是某个进程导致的问题。
|
||||
|
||||
而找出导致性能问题的进程后,就要用进程分析工具来分析进程的行为,比如使用 strace 分析系统调用情况,以及使用 perf 分析调用链中各级函数的执行情况。
|
||||
|
||||
第二个例子,top 输出的平均负载升高,可以跟 vmstat 输出的运行状态和不可中断状态的进程数做对比,观察是哪种进程导致的负载升高。
|
||||
|
||||
<li>
|
||||
如果是不可中断进程数增多了,那么就需要做 I/O 的分析,也就是用 dstat 或 sar 等工具,进一步分析 I/O 的情况。
|
||||
</li>
|
||||
<li>
|
||||
如果是运行状态进程数增多了,那就需要回到 top 和 pidstat,找出这些处于运行状态的到底是什么进程,然后再用进程分析工具,做进一步分析。
|
||||
</li>
|
||||
|
||||
最后一个例子,当发现 top 输出的软中断 CPU 使用率升高时,可以查看 /proc/softirqs 文件中各种类型软中断的变化情况,确定到底是哪种软中断出的问题。比如,发现是网络接收中断导致的问题,那就可以继续用网络分析工具 sar 和 tcpdump 来分析。
|
||||
|
||||
注意,我在这个图中只列出了最核心的几个性能工具,并没有列出所有。这么做,一方面是不想用大量的工具列表吓到你。在学习之初就接触所有或核心或小众的工具,不见得是好事。另一方面,是希望你能先把重心放在核心工具上,毕竟熟练掌握它们,就可以解决大多数问题。
|
||||
|
||||
所以,你可以保存下这张图,作为CPU性能分析的思路图谱。从最核心的这几个工具开始,通过我提供的那些案例,自己在真实环境里实践,拿下它们。
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我带你回忆了常见的 CPU 性能指标,梳理了常见的 CPU 性能观测工具,最后还总结了快速分析 CPU 性能问题的思路。
|
||||
|
||||
虽然 CPU 的性能指标很多,相应的性能分析工具也很多,但熟悉了各种指标的含义之后,你就会发现它们其实都有一定的关联。顺着这个思路,掌握常用的分析套路并不难。
|
||||
|
||||
## 思考
|
||||
|
||||
由于篇幅限制,我在这里只举了几个最常见的案例,帮你理解 CPU 性能问题的原理和分析方法。你肯定也碰到过很多跟这些案例不同的 CPU 性能问题吧。我想请你一起来聊聊,你碰到过什么不一样的 CPU 性能问题呢?你又是怎么分析出它的瓶颈的呢?
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
210
极客时间专栏/Linux性能优化实战/CPU 性能篇/12 | 套路篇:CPU 性能优化的几个思路.md
Normal file
210
极客时间专栏/Linux性能优化实战/CPU 性能篇/12 | 套路篇:CPU 性能优化的几个思路.md
Normal file
@@ -0,0 +1,210 @@
|
||||
<audio id="audio" title="12 | 套路篇:CPU 性能优化的几个思路" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/68/3e/68bba70fa760287920041912652a0f3e.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节我们一起回顾了常见的 CPU 性能指标,梳理了核心的 CPU 性能观测工具,最后还总结了快速分析 CPU 性能问题的思路。虽然 CPU 的性能指标很多,相应的性能分析工具也很多,但理解了各种指标的含义后,你就会发现它们其实都有一定的关联。
|
||||
|
||||
顺着这些关系往下理解,你就会发现,掌握这些常用的瓶颈分析套路,其实并不难。
|
||||
|
||||
在找到 CPU 的性能瓶颈后,下一步要做的就是优化了,也就是找出充分利用CPU的方法,以便完成更多的工作。
|
||||
|
||||
今天,我就来说说,优化 CPU 性能问题的思路和注意事项。
|
||||
|
||||
## 性能优化方法论
|
||||
|
||||
在我们历经千辛万苦,通过各种性能分析方法,终于找到引发性能问题的瓶颈后,是不是立刻就要开始优化了呢?别急,动手之前,你可以先看看下面这三个问题。
|
||||
|
||||
<li>
|
||||
首先,既然要做性能优化,那要怎么判断它是不是有效呢?特别是优化后,到底能提升多少性能呢?
|
||||
</li>
|
||||
<li>
|
||||
第二,性能问题通常不是独立的,如果有多个性能问题同时发生,你应该先优化哪一个呢?
|
||||
</li>
|
||||
<li>
|
||||
第三,提升性能的方法并不是唯一的,当有多种方法可以选择时,你会选用哪一种呢?是不是总选那个最大程度提升性能的方法就行了呢?
|
||||
</li>
|
||||
|
||||
如果你可以轻松回答这三个问题,那么二话不说就可以开始优化。
|
||||
|
||||
比如,在前面的不可中断进程案例中,通过性能分析,我们发现是因为一个进程的**直接 I/O** ,导致了 iowait 高达 90%。那是不是用“**直接 I/O 换成缓存 I/O**”的方法,就可以立即优化了呢?
|
||||
|
||||
按照上面讲的,你可以先自己思考下那三点。如果不能确定,我们一起来看看。
|
||||
|
||||
<li>
|
||||
第一个问题,直接 I/O 换成缓存 I/O,可以把 iowait 从 90% 降到接近 0,性能提升很明显。
|
||||
</li>
|
||||
<li>
|
||||
第二个问题,我们没有发现其他性能问题,直接 I/O 是唯一的性能瓶颈,所以不用挑选优化对象。
|
||||
</li>
|
||||
<li>
|
||||
第三个问题,缓存 I/O 是我们目前用到的最简单的优化方法,而且这样优化并不会影响应用的功能。
|
||||
</li>
|
||||
|
||||
好的,这三个问题很容易就能回答,所以立即优化没有任何问题。
|
||||
|
||||
但是,很多现实情况,并不像我举的例子那么简单。性能评估可能有多重指标,性能问题可能会多个同时发生,而且,优化某一个指标的性能,可能又导致其他指标性能的下降。
|
||||
|
||||
那么,面对这种复杂的情况,我们该怎么办呢?
|
||||
|
||||
接下来,我们就来深入分析这三个问题。
|
||||
|
||||
### 怎么评估性能优化的效果?
|
||||
|
||||
首先,来看第一个问题,怎么评估性能优化的效果。
|
||||
|
||||
我们解决性能问题的目的,自然是想得到一个性能提升的效果。为了评估这个效果,我们需要对系统的性能指标进行量化,并且要分别测试出优化前、后的性能指标,用前后指标的变化来对比呈现效果。我把这个方法叫做性能评估“三步走”。
|
||||
|
||||
<li>
|
||||
确定性能的量化指标。
|
||||
</li>
|
||||
<li>
|
||||
测试优化前的性能指标。
|
||||
</li>
|
||||
<li>
|
||||
测试优化后的性能指标。
|
||||
</li>
|
||||
|
||||
先看第一步,性能的量化指标有很多,比如 CPU 使用率、应用程序的吞吐量、客户端请求的延迟等,都可以评估性能。那我们应该选择什么指标来评估呢?
|
||||
|
||||
我的建议是**不要局限在单一维度的指标上**,你至少要从应用程序和系统资源这两个维度,分别选择不同的指标。比如,以 Web 应用为例:
|
||||
|
||||
<li>
|
||||
应用程序的维度,我们可以用**吞吐量和请求延迟**来评估应用程序的性能。
|
||||
</li>
|
||||
<li>
|
||||
系统资源的维度,我们可以用 **CPU 使用率**来评估系统的 CPU 使用情况。
|
||||
</li>
|
||||
|
||||
之所以从这两个不同维度选择指标,主要是因为应用程序和系统资源这两者间相辅相成的关系。
|
||||
|
||||
<li>
|
||||
好的应用程序是性能优化的最终目的和结果,系统优化总是为应用程序服务的。所以,必须要使用应用程序的指标,来评估性能优化的整体效果。
|
||||
</li>
|
||||
<li>
|
||||
系统资源的使用情况是影响应用程序性能的根源。所以,需要用系统资源的指标,来观察和分析瓶颈的来源。
|
||||
</li>
|
||||
|
||||
至于接下来的两个步骤,主要是为了对比优化前后的性能,更直观地呈现效果。如果你的第一步,是从两个不同维度选择了多个指标,那么在性能测试时,你就需要获得这些指标的具体数值。
|
||||
|
||||
还是以刚刚的 Web 应用为例,对应上面提到的几个指标,我们可以选择 ab 等工具,测试 Web 应用的并发请求数和响应延迟。而测试的同时,还可以用 vmstat、pidstat 等性能工具,观察系统和进程的 CPU 使用率。这样,我们就同时获得了应用程序和系统资源这两个维度的指标数值。
|
||||
|
||||
不过,在进行性能测试时,有两个特别重要的地方你需要注意下。
|
||||
|
||||
第一,要避免性能测试工具干扰应用程序的性能。通常,对 Web 应用来说,性能测试工具跟目标应用程序要在不同的机器上运行。
|
||||
|
||||
比如,在之前的 Nginx 案例中,我每次都会强调要用两台虚拟机,其中一台运行 Nginx 服务,而另一台运行模拟客户端的工具,就是为了避免这个影响。
|
||||
|
||||
第二,避免外部环境的变化影响性能指标的评估。这要求优化前、后的应用程序,都运行在相同配置的机器上,并且它们的外部依赖也要完全一致。
|
||||
|
||||
比如还是拿 Nginx 来说,就可以运行在同一台机器上,并用相同参数的客户端工具来进行性能测试。
|
||||
|
||||
### 多个性能问题同时存在,要怎么选择?
|
||||
|
||||
再来看第二个问题,开篇词里我们就说过,系统性能总是牵一发而动全身,所以性能问题通常也不是独立存在的。那当多个性能问题同时发生的时候,应该先去优化哪一个呢?
|
||||
|
||||
在性能测试的领域,流传很广的一个说法是“二八原则”,也就是说 80% 的问题都是由 20% 的代码导致的。只要找出这 20% 的位置,你就可以优化 80% 的性能。所以,我想表达的是,**并不是所有的性能问题都值得优化**。
|
||||
|
||||
我的建议是,动手优化之前先动脑,先把所有这些性能问题给分析一遍,找出最重要的、可以最大程度提升性能的问题,从它开始优化。这样的好处是,不仅性能提升的收益最大,而且很可能其他问题都不用优化,就已经满足了性能要求。
|
||||
|
||||
那关键就在于,怎么判断出哪个性能问题最重要。这其实还是我们性能分析要解决的核心问题,只不过这里要分析的对象,从原来的一个问题,变成了多个问题,思路其实还是一样的。
|
||||
|
||||
所以,你依然可以用我前面讲过的方法挨个分析,分别找出它们的瓶颈。分析完所有问题后,再按照因果等关系,排除掉有因果关联的性能问题。最后,再对剩下的性能问题进行优化。
|
||||
|
||||
如果剩下的问题还是好几个,你就得分别进行性能测试了。比较不同的优化效果后,选择能明显提升性能的那个问题进行修复。这个过程通常会花费较多的时间,这里,我推荐两个可以简化这个过程的方法。
|
||||
|
||||
第一,如果发现是系统资源达到了瓶颈,比如 CPU 使用率达到了 100%,那么首先优化的一定是系统资源使用问题。完成系统资源瓶颈的优化后,我们才要考虑其他问题。
|
||||
|
||||
第二,针对不同类型的指标,首先去优化那些由瓶颈导致的,性能指标变化幅度最大的问题。比如产生瓶颈后,用户 CPU 使用率升高了 10%,而系统 CPU 使用率却升高了 50%,这个时候就应该首先优化系统 CPU 的使用。
|
||||
|
||||
### 有多种优化方法时,要如何选择?
|
||||
|
||||
接着来看第三个问题,当多种方法都可用时,应该选择哪一种呢?是不是最大提升性能的方法,一定最好呢?
|
||||
|
||||
一般情况下,我们当然想选能最大提升性能的方法,这其实也是性能优化的目标。
|
||||
|
||||
但要注意,现实情况要考虑的因素却没那么简单。最直观来说,**性能优化并非没有成本**。性能优化通常会带来复杂度的提升,降低程序的可维护性,还可能在优化一个指标时,引发其他指标的异常。也就是说,很可能你优化了一个指标,另一个指标的性能却变差了。
|
||||
|
||||
一个很典型的例子是我将在网络部分讲到的 DPDK(Data Plane Development Kit)。DPDK 是一种优化网络处理速度的方法,它通过绕开内核网络协议栈的方法,提升网络的处理能力。
|
||||
|
||||
不过它有一个很典型的要求,就是要独占一个 CPU 以及一定数量的内存大页,并且总是以 100% 的 CPU 使用率运行。所以,如果你的 CPU 核数很少,就有点得不偿失了。
|
||||
|
||||
所以,在考虑选哪个性能优化方法时,你要综合多方面的因素。切记,不要想着“一步登天”,试图一次性解决所有问题;也不要只会“拿来主义”,把其他应用的优化方法原封不动拿来用,却不经过任何思考和分析。
|
||||
|
||||
## CPU 优化
|
||||
|
||||
清楚了性能优化最基本的三个问题后,我们接下来从应用程序和系统的角度,分别来看看如何才能降低 CPU 使用率,提高 CPU 的并行处理能力。
|
||||
|
||||
### 应用程序优化
|
||||
|
||||
首先,从应用程序的角度来说,降低 CPU 使用率的最好方法当然是,排除所有不必要的工作,只保留最核心的逻辑。比如减少循环的层次、减少递归、减少动态内存分配等等。
|
||||
|
||||
除此之外,应用程序的性能优化也包括很多种方法,我在这里列出了最常见的几种,你可以记下来。
|
||||
|
||||
<li>
|
||||
**编译器优化**:很多编译器都会提供优化选项,适当开启它们,在编译阶段你就可以获得编译器的帮助,来提升性能。比如, gcc 就提供了优化选项 -O2,开启后会自动对应用程序的代码进行优化。
|
||||
</li>
|
||||
<li>
|
||||
**算法优化**:使用复杂度更低的算法,可以显著加快处理速度。比如,在数据比较大的情况下,可以用 O(nlogn) 的排序算法(如快排、归并排序等),代替 O(n^2) 的排序算法(如冒泡、插入排序等)。
|
||||
</li>
|
||||
<li>
|
||||
**异步处理**:使用异步处理,可以避免程序因为等待某个资源而一直阻塞,从而提升程序的并发处理能力。比如,把轮询替换为事件通知,就可以避免轮询耗费 CPU 的问题。
|
||||
</li>
|
||||
<li>
|
||||
**多线程代替多进程**:前面讲过,相对于进程的上下文切换,线程的上下文切换并不切换进程地址空间,因此可以降低上下文切换的成本。
|
||||
</li>
|
||||
<li>
|
||||
**善用缓存**:经常访问的数据或者计算过程中的步骤,可以放到内存中缓存起来,这样在下次用时就能直接从内存中获取,加快程序的处理速度。
|
||||
</li>
|
||||
|
||||
### 系统优化
|
||||
|
||||
从系统的角度来说,优化 CPU 的运行,一方面要充分利用 CPU 缓存的本地性,加速缓存访问;另一方面,就是要控制进程的 CPU 使用情况,减少进程间的相互影响。
|
||||
|
||||
具体来说,系统层面的 CPU 优化方法也有不少,这里我同样列举了最常见的一些方法,方便你记忆和使用。
|
||||
|
||||
<li>
|
||||
**CPU 绑定**:把进程绑定到一个或者多个 CPU 上,可以提高 CPU 缓存的命中率,减少跨 CPU 调度带来的上下文切换问题。
|
||||
</li>
|
||||
<li>
|
||||
**CPU 独占**:跟 CPU 绑定类似,进一步将 CPU 分组,并通过 CPU 亲和性机制为其分配进程。这样,这些 CPU 就由指定的进程独占,换句话说,不允许其他进程再来使用这些CPU。
|
||||
</li>
|
||||
<li>
|
||||
**优先级调整**:使用 nice 调整进程的优先级,正值调低优先级,负值调高优先级。优先级的数值含义前面我们提到过,忘了的话及时复习一下。在这里,适当降低非核心应用的优先级,增高核心应用的优先级,可以确保核心应用得到优先处理。
|
||||
</li>
|
||||
<li>
|
||||
**为进程设置资源限制**:使用 Linux cgroups 来设置进程的 CPU 使用上限,可以防止由于某个应用自身的问题,而耗尽系统资源。
|
||||
</li>
|
||||
<li>
|
||||
**NUMA(Non-Uniform Memory Access)优化**:支持 NUMA 的处理器会被划分为多个 node,每个 node 都有自己的本地内存空间。NUMA 优化,其实就是让 CPU 尽可能只访问本地内存。
|
||||
</li>
|
||||
<li>
|
||||
**中断负载均衡**:无论是软中断还是硬中断,它们的中断处理程序都可能会耗费大量的CPU。开启 irqbalance 服务或者配置 smp_affinity,就可以把中断处理过程自动负载均衡到多个 CPU 上。
|
||||
</li>
|
||||
|
||||
## 千万避免过早优化
|
||||
|
||||
掌握上面这些优化方法后,我估计,很多人即使没发现性能瓶颈,也会忍不住把各种各样的优化方法带到实际的开发中。
|
||||
|
||||
不过,我想你一定听说过高德纳的这句名言, “过早优化是万恶之源”,我也非常赞同这一点,过早优化不可取。
|
||||
|
||||
因为,一方面,优化会带来复杂性的提升,降低可维护性;另一方面,需求不是一成不变的。针对当前情况进行的优化,很可能并不适应快速变化的新需求。这样,在新需求出现时,这些复杂的优化,反而可能阻碍新功能的开发。
|
||||
|
||||
所以,性能优化最好是逐步完善,动态进行,不追求一步到位,而要首先保证能满足当前的性能要求。当发现性能不满足要求或者出现性能瓶颈时,再根据性能评估的结果,选择最重要的性能问题进行优化。
|
||||
|
||||
## 总结
|
||||
|
||||
今天,我带你梳理了常见的 CPU 性能优化思路和优化方法。发现性能问题后,不要急于动手优化,而要先找出最重要的、可以获得最大性能提升的问题,然后再从应用程序和系统两个方面入手优化。
|
||||
|
||||
这样不仅可以获得最大的性能提升,而且很可能不需要优化其他问题,就已经满足了性能要求。
|
||||
|
||||
但是记住,一定要忍住“把 CPU 性能优化到极致”的冲动,因为 CPU 并不是唯一的性能因素。在后续的文章中,我还会介绍更多的性能问题,比如内存、网络、I/O 甚至是架构设计的问题。
|
||||
|
||||
如果不做全方位的分析和测试,只是单纯地把某个指标提升到极致,并不一定能带来整体的收益。
|
||||
|
||||
## 思考
|
||||
|
||||
由于篇幅的限制,我在这里只列举了几个最常见的 CPU 性能优化方法。除了这些,还有很多其他应用程序,或者系统资源角度的性能优化方法。我想请你一起来聊聊,你还知道哪些其他优化方法呢?
|
||||
|
||||
欢迎在留言区跟我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
178
极客时间专栏/Linux性能优化实战/CPU 性能篇/13 | 答疑(一):无法模拟出 RES 中断的问题,怎么办?.md
Normal file
178
极客时间专栏/Linux性能优化实战/CPU 性能篇/13 | 答疑(一):无法模拟出 RES 中断的问题,怎么办?.md
Normal file
@@ -0,0 +1,178 @@
|
||||
<audio id="audio" title="13 | 答疑(一):无法模拟出 RES 中断的问题,怎么办?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/61/4d/614e31077f92dcc9a8c7850ecb6e3c4d.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
专栏更新至今,四大基础模块之一的CPU性能篇,我们就已经学完了。很开心过半数同学还没有掉队,仍然在学习、积极实践操作,并且热情地留下了大量的留言。
|
||||
|
||||
这些留言中,我非常高兴地看到,很多同学已经做到了活学活用,用学过的案例思路,分析出了线上应用的性能瓶颈,解决了实际工作中的性能问题。 还有同学能够反复推敲思考,指出文章中某些不当或不严谨的叙述,我也十分感谢你,同时很乐意和你探讨。
|
||||
|
||||
此外,很多留言提出的问题也很有价值,大部分我都已经在app里回复,一些手机上不方便回复的或者很有价值的典型问题,我专门摘了出来,作为今天的答疑内容,集中回复。另一方面,也是为了保证所有人都能不漏掉任何一个重点。
|
||||
|
||||
今天是性能优化答疑的第一期。为了便于你学习理解,它们并不是严格按照文章顺序排列的。每个问题,我都附上了留言区提问的截屏。如果你需要回顾内容原文,可以扫描每个问题右下方的二维码查看。
|
||||
|
||||
## 问题1:性能工具版本太低,导致指标不全
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/ba/19084718d4682168fea4bb6cb27c4fba.png" alt="">
|
||||
|
||||
这是使用 CentOS 的同学普遍碰到的问题。在文章中,我的 pidstat 输出里有一个 %wait 指标,代表进程等待 CPU 的时间百分比,这是 systat 11.5.5 版本才引入的新指标,旧版本没有这一项。而CentOS 软件库里的 sysstat 版本刚好比这个低,所以没有这项指标。
|
||||
|
||||
不过,你也不用担心。前面我就强调过,工具只是查找分析的手段,指标才是我们重点分析的对象。如果你的pidstat 里没有显示,自然还有其他手段能找到这个指标。
|
||||
|
||||
比如说,在讲解系统原理和性能工具时,我一般会介绍一些 **proc 文件系统**的知识,教你看懂 proc 文件系统提供的各项指标。之所以这么做,一方面,当然是为了让你更直观地理解系统的工作原理;另一方面,其实是想给你展示,性能工具上能看到的各项性能指标的原始数据来源。
|
||||
|
||||
这样,在实际生产环境中,即使你很可能需要运行老版本的操作系统,还没有权限安装新的软件包,你也可以查看 proc 文件系统,获取自己想要的指标。
|
||||
|
||||
但是,性能分析的学习,我还是建议你要用最新的性能工具来学。新工具有更全面的指标,让你更容易上手分析。这个绝对的优势,可以让你更直观地得到想要的数据,也不容易让你打退堂鼓。
|
||||
|
||||
当然,初学时,你最好试着去理解性能工具的原理,或者熟悉了使用方法后,再回过头重新学习原理。这样,即使是在无法安装新工具的环境中,你仍然可以从 proc 文件系统或者其他地方,获得同样的指标,进行有效的分析。
|
||||
|
||||
## 问题2:使用 stress 命令,无法模拟 iowait 高的场景
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/34/43/34a354b22e351571e7f6a532e719fd43.png" alt=""> <img src="https://static001.geekbang.org/resource/image/e7/c5/e7ffb84e4c22b08c0b2db14e2f61fdc5.jpg" alt="">
|
||||
|
||||
使用 stress 无法模拟 iowait 升高,但是却看到了 sys 升高。这是因为案例中 的stress -i 参数,它表示通过系统调用 sync() 来模拟 I/O 的问题,但这种方法实际上并不可靠。
|
||||
|
||||
因为 sync() 的本意是刷新内存缓冲区的数据到磁盘中,以确保同步。如果缓冲区内本来就没多少数据,那读写到磁盘中的数据也就不多,也就没法产生 I/O 压力。
|
||||
|
||||
这一点,在使用 SSD 磁盘的环境中尤为明显,很可能你的 iowait 总是 0,却单纯因为大量的系统调用,导致了系统CPU使用率 sys 升高。
|
||||
|
||||
这种情况,我在留言中也回复过,推荐使用 stress-ng 来代替 stress。担心你没有看到留言,所以这里我再强调一遍。
|
||||
|
||||
你可以运行下面的命令,来模拟 iowait 的问题。
|
||||
|
||||
```
|
||||
# -i的含义还是调用sync,而—hdd则表示读写临时文件
|
||||
$ stress-ng -i 1 --hdd 1 --timeout 600
|
||||
|
||||
```
|
||||
|
||||
## 问题3:无法模拟出 RES 中断的问题
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/22/be/22d09f0924f7ae09a9dbcb7253b5b6be.jpg" alt="">
|
||||
|
||||
这个问题是说,即使运行了大量的线程,也无法模拟出重调度中断 RES 升高的问题。
|
||||
|
||||
其实我在 CPU 上下文切换的案例中已经提到,重调度中断是调度器用来分散任务到不同 CPU 的机制,也就是可以唤醒空闲状态的 CPU ,来调度新任务运行,而这通常借助**处理器间中断**(Inter-Processor Interrupts,IPI)来实现。
|
||||
|
||||
所以,这个中断在单核(只有一个逻辑 CPU)的机器上当然就没有意义了,因为压根儿就不会发生重调度的情况。
|
||||
|
||||
不过,正如留言所说,上下文切换的问题依然存在,所以你会看到, cs(context switch)从几百增加到十几万,同时 sysbench 线程的自愿上下文切换和非自愿上下文切换也都会大幅上升,特别是非自愿上下文切换,会上升到十几万。根据非自愿上下文的含义,我们都知道,这是过多的线程在争抢 CPU。
|
||||
|
||||
其实这个结论也可以从另一个角度获得。比如,你可以在 pidstat 的选项中,加入 -u 和 -t 参数,输出线程的 CPU 使用情况,你会看到下面的界面:
|
||||
|
||||
```
|
||||
$ pidstat -u -t 1
|
||||
|
||||
14:24:03 UID TGID TID %usr %system %guest %wait %CPU CPU Command
|
||||
14:24:04 0 - 2472 0.99 8.91 0.00 77.23 9.90 0 |__sysbench
|
||||
14:24:04 0 - 2473 0.99 8.91 0.00 68.32 9.90 0 |__sysbench
|
||||
14:24:04 0 - 2474 0.99 7.92 0.00 75.25 8.91 0 |__sysbench
|
||||
14:24:04 0 - 2475 2.97 6.93 0.00 70.30 9.90 0 |__sysbench
|
||||
14:24:04 0 - 2476 2.97 6.93 0.00 68.32 9.90 0 |__sysbench
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
从这个 pidstat 的输出界面,你可以发现,每个 stress 线程的 %wait 高达 70%,而 CPU 使用率只有不到 10%。换句话说, stress 线程大部分时间都消耗在了等待 CPU 上,这也表明,确实是过多的线程在争抢 CPU。
|
||||
|
||||
在这里顺便提一下,留言中很常见的一个错误。有些同学会拿 pidstat 中的 %wait 跟 top 中的 iowait% (缩写为wa)对比,其实这是没有意义的,因为它们是完全不相关的两个指标。
|
||||
|
||||
<li>
|
||||
pidstat 中, %wait 表示进程等待 CPU 的时间百分比。
|
||||
</li>
|
||||
<li>
|
||||
top 中 ,iowait% 则表示等待 I/O 的 CPU 时间百分比。
|
||||
</li>
|
||||
|
||||
回忆一下我们学过的进程状态,你应该记得,等待 CPU 的进程已经在 CPU 的就绪队列中,处于运行状态;而等待 I/O 的进程则处于不可中断状态。
|
||||
|
||||
另外,不同版本的 sysbench 运行参数也不是完全一样的。比如,在案例 Ubuntu 18.04 中,运行 sysbench 的格式为:
|
||||
|
||||
```
|
||||
$ sysbench --threads=10 --max-time=300 threads run
|
||||
|
||||
```
|
||||
|
||||
而在 Ubuntu 16.04 中,运行格式则为(感谢 Haku 留言分享的执行命令):
|
||||
|
||||
```
|
||||
$ sysbench --num-threads=10 --max-time=300 --test=threads run
|
||||
|
||||
```
|
||||
|
||||
## 问题4:无法模拟出I/O性能瓶颈,以及I/O压力过大的问题
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9e/d8/9e235aca4e92b68e84dba03881c591d8.png" alt="">
|
||||
|
||||
这个问题可以看成是上一个问题的延伸,只是把 stress 命令换成了一个在容器中运行的 app 应用。
|
||||
|
||||
事实上,在 I/O 瓶颈案例中,除了上面这个模拟不成功的留言,还有更多留言的内容刚好相反,说的是案例 I/O 压力过大,导致自己的机器出各种问题,甚至连系统都没响应了。
|
||||
|
||||
之所以这样,其实还是因为每个人的机器配置不同,既包括了 CPU 和内存配置的不同,更是因为磁盘的巨大差异。比如,机械磁盘(HDD)、低端固态磁盘(SSD)与高端固态磁盘相比,性能差异可能达到数倍到数十倍。
|
||||
|
||||
其实,我自己所用的案例机器也只是低端的 SSD,比机械磁盘稍微好一些,但跟高端固态磁盘还是比不了的。所以,相同操作下,我的机器上刚好出现 I/O 瓶颈,但换成一台使用机械磁盘的机器,可能磁盘 I/O 就被压死了(表现为使用率长时间100%),而换上好一些的 SSD 磁盘,可能又无法产生足够的 I/O 压力。
|
||||
|
||||
另外,由于我在案例中只查找了 /dev/xvd 和 /dev/sd 前缀的磁盘,而没有考虑到使用其他前缀磁盘(比如 /dev/nvme)的同学。如果你正好用的是其他前缀,你可能会碰到跟Vicky 类似的问题,也就是app 启动后又很快退出,变成 exited 状态。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a3/38/a30211eeb41194eb9b5aa193cda25238.png" alt="">
|
||||
|
||||
在这里,berryfl 同学提供了一个不错的建议:可以在案例中增加一个参数指定块设备,这样有需要的同学就不用自己编译和打包案例应用了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f3/2c/f351f346cbfc2b3c35d010536b23332c.png" alt="">
|
||||
|
||||
所以,在最新的案例中,我为 app 应用增加了三个选项。
|
||||
|
||||
<li>
|
||||
-d 设置要读取的磁盘,默认前缀为 `/dev/sd` 或者 `/dev/xvd` 的磁盘。
|
||||
</li>
|
||||
<li>
|
||||
-s 设置每次读取的数据量大小,单位为字节,默认为 67108864(也就是 64MB)。
|
||||
</li>
|
||||
<li>
|
||||
-c 设置每个子进程读取的次数,默认为 20 次,也就是说,读取 20*64MB 数据后,子进程退出。
|
||||
</li>
|
||||
|
||||
你可以点击 [Github](https://github.com/feiskyer/linux-perf-examples/tree/master/high-iowait-process) 查看它的源码,使用方法我写在了这里:
|
||||
|
||||
```
|
||||
$ docker run --privileged --name=app -itd feisky/app:iowait /app -d /dev/sdb -s 67108864 -c 20
|
||||
|
||||
```
|
||||
|
||||
案例运行后,你可以执行 docker logs 查看它的日志。正常情况下,你可以看到下面的输出:
|
||||
|
||||
```
|
||||
$ docker logs app
|
||||
Reading data from disk /dev/sdb with buffer size 67108864 and count 20
|
||||
|
||||
```
|
||||
|
||||
## 问题5:性能工具(如 vmstat)输出中,第一行数据跟其他行差别巨大
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ef/0f/efa8186b71c474bd40924a9038016e0f.png" alt="">
|
||||
|
||||
这个问题主要是说,在执行 vmstat 时,第一行数据跟其他行相比较,数值相差特别大。我相信不少同学都注意到了这个现象,这里我简单解释一下。
|
||||
|
||||
首先还是要记住,我总强调的那句话,**在碰到直观上解释不了的现象时,要第一时间去查命令手册**。
|
||||
|
||||
比如,运行 man vmstat 命令,你可以在手册中发现下面这句话:
|
||||
|
||||
```
|
||||
The first report produced gives averages since the last reboot. Additional reports give information on a sam‐
|
||||
pling period of length delay. The process and memory reports are instantaneous in either case.
|
||||
|
||||
```
|
||||
|
||||
也就是说,第一行数据是系统启动以来的平均值,其他行才是你在运行 vmstat 命令时,设置的间隔时间的平均值。另外,进程和内存的报告内容都是即时数值。
|
||||
|
||||
你看,这并不是什么不得了的事故,但如果我们不清楚这一点,很可能卡住我们的思维,阻止我们进一步的分析。这里我也不得不提一下,文档的重要作用。
|
||||
|
||||
授之以鱼,不如授之以渔。我们专栏的学习核心,一定是教会你**性能分析的原理和思路**,性能工具只是我们的路径和手段。所以,在提到各种性能工具时,我并没有详细解释每个工具的各种命令行选项的作用,一方面是因为你很容易通过文档查到这些,另一方面就是不同版本、不同系统中,个别选项的含义可能并不相同。
|
||||
|
||||
所以,不管因为哪个因素,自己man一下,一定是最快速并且最准确的方式。特别是,当你发现某些工具的输出不符合常识时,一定记住,第一时间查文档弄明白。实在读不懂文档的话,再上网去搜,或者在专栏里向我提问。
|
||||
|
||||
学习是一个“从薄到厚再变薄”的过程,我们从细节知识入手开始学习,积累到一定程度,需要整理成一个体系来记忆,这其中还要不断地对这个体系进行细节修补。有疑问、有反思才可以达到最佳的学习效果。
|
||||
|
||||
最后,欢迎继续在留言区写下你的疑问,我会持续不断地解答。我的目的仍然不变,希望可以和你一起,把文章的知识变成你的能力,我们不仅仅在实战中演练,也要在交流中进步。
|
||||
|
||||
|
||||
271
极客时间专栏/Linux性能优化实战/CPU 性能篇/14 | 答疑(二):如何用perf工具分析Java程序?.md
Normal file
271
极客时间专栏/Linux性能优化实战/CPU 性能篇/14 | 答疑(二):如何用perf工具分析Java程序?.md
Normal file
@@ -0,0 +1,271 @@
|
||||
<audio id="audio" title="14 | 答疑(二):如何用perf工具分析Java程序?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7d/ec/7d0510752b0453d81492527dd11a87ec.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
今天是我们第二期答疑,这期答疑的主题是我们多次用到的perf工具,内容主要包括前面案例中, perf 使用方法的各种疑问。
|
||||
|
||||
perf 在性能分析中非常有效,是我们每个人都需要掌握的核心工具。perf 的使用方法也很丰富,不过不用担心,目前你只要会用 perf record 和 perf report 就够了。而对于 perf 显示的调用栈中的某些内核符号,如果你不理解也没有关系,可以暂时跳过,并不影响我们的分析。
|
||||
|
||||
同样的,为了便于你学习理解,它们并不是严格按照文章顺序排列的,如果你需要回顾内容原文,可以扫描每个问题右下方的二维码查看。
|
||||
|
||||
## 问题 1: 使用 perf 工具时,看到的是16进制地址而不是函数名
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/94/69/94f67501d5be157a25a26d852b8c2869.png" alt="">
|
||||
|
||||
这也是留言比较多的一个问题,在 CentOS 系统中,使用 perf 工具看不到函数名,只能看到一些16进制格式的函数地址。
|
||||
|
||||
其实,只要你观察一下perf界面最下面的那一行,就会发现一个警告信息:
|
||||
|
||||
```
|
||||
Failed to open /opt/bitnami/php/lib/php/extensions/opcache.so, continuing without symbols
|
||||
|
||||
```
|
||||
|
||||
这说明,perf 找不到待分析进程依赖的库。当然,实际上这个案例中有很多依赖库都找不到,只不过,perf工具本身只在最后一行显示警告信息,所以你只能看到这一条警告。
|
||||
|
||||
这个问题,其实也是在分析Docker容器应用时,我们经常碰到的一个问题,因为容器应用依赖的库都在镜像里面。
|
||||
|
||||
针对这种情况,我总结了下面**四个解决方法**。
|
||||
|
||||
**第一个方法,在容器外面构建相同路径的依赖库**。这种方法从原理上可行,但是我并不推荐,一方面是因为找出这些依赖库比较麻烦,更重要的是,构建这些路径,会污染容器主机的环境。
|
||||
|
||||
**第二个方法,在容器内部运行 perf**。不过,这需要容器运行在特权模式下,但实际的应用程序往往只以普通容器的方式运行。所以,容器内部一般没有权限执行 perf 分析。
|
||||
|
||||
比方说,如果你在普通容器内部运行 perf record ,你将会看到下面这个错误提示:
|
||||
|
||||
```
|
||||
$ perf_4.9 record -a -g
|
||||
perf_event_open(..., PERF_FLAG_FD_CLOEXEC) failed with unexpected error 1 (Operation not permitted)
|
||||
perf_event_open(..., 0) failed unexpectedly with error 1 (Operation not permitted)
|
||||
|
||||
```
|
||||
|
||||
当然,其实你还可以通过配置 /proc/sys/kernel/perf_event_paranoid (比如改成-1),来允许非特权用户执行 perf 事件分析。
|
||||
|
||||
不过还是那句话,为了安全起见,这种方法我也不推荐。
|
||||
|
||||
**第三个方法,指定符号路径为容器文件系统的路径**。比如对于第05讲的应用,你可以执行下面这个命令:
|
||||
|
||||
```
|
||||
$ mkdir /tmp/foo
|
||||
$ PID=$(docker inspect --format {{.State.Pid}} phpfpm)
|
||||
$ bindfs /proc/$PID/root /tmp/foo
|
||||
$ perf report --symfs /tmp/foo
|
||||
|
||||
# 使用完成后不要忘记解除绑定
|
||||
$ umount /tmp/foo/
|
||||
|
||||
```
|
||||
|
||||
不过这里要注意,bindfs 这个工具需要你额外安装。bindfs 的基本功能是实现目录绑定(类似于 mount --bind),这里需要你安装的是 1.13.10 版本(这也是它的最新发布版)。
|
||||
|
||||
如果你安装的是旧版本,你可以到 [GitHub](https://github.com/mpartel/bindfs)上面下载源码,然后编译安装。
|
||||
|
||||
**第四个方法,在容器外面把分析纪录保存下来,再去容器里查看结果**。这样,库和符号的路径也就都对了。
|
||||
|
||||
比如,你可以这么做。先运行 perf record -g -p < pid>,执行一会儿(比如15秒)后,按Ctrl+C停止。
|
||||
|
||||
然后,把生成的 perf.data 文件,拷贝到容器里面来分析:
|
||||
|
||||
```
|
||||
$ docker cp perf.data phpfpm:/tmp
|
||||
$ docker exec -i -t phpfpm bash
|
||||
|
||||
```
|
||||
|
||||
接下来,在容器的 bash 中继续运行下面的命令,安装 perf 并使用 perf report 查看报告:
|
||||
|
||||
```
|
||||
$ cd /tmp/
|
||||
$ apt-get update && apt-get install -y linux-tools linux-perf procps
|
||||
$ perf_4.9 report
|
||||
|
||||
```
|
||||
|
||||
不过,这里也有两点需要你注意。
|
||||
|
||||
首先是perf工具的版本问题。在最后一步中,我们运行的工具是容器内部安装的版本 perf_4.9,而不是普通的 perf 命令。这是因为, perf 命令实际上是一个软连接,会跟内核的版本进行匹配,但镜像里安装的perf版本跟虚拟机的内核版本有可能并不一致。
|
||||
|
||||
另外,php-fpm 镜像是基于 Debian 系统的,所以安装 perf 工具的命令,跟 Ubuntu 也并不完全一样。比如, Ubuntu 上的安装方法是下面这样:
|
||||
|
||||
```
|
||||
$ apt-get install -y linux-tools-common linux-tools-generic linux-tools-$(uname -r))
|
||||
|
||||
```
|
||||
|
||||
而在 php-fpm 容器里,你应该执行下面的命令来安装 perf:
|
||||
|
||||
```
|
||||
$ apt-get install -y linux-perf
|
||||
|
||||
```
|
||||
|
||||
当你按照前面这几种方法操作后,你就可以在容器内部看到 sqrt 的堆栈:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/76/af/76f8d0f36210001e750b0a82026dedaf.png" alt="">
|
||||
|
||||
事实上,抛开我们的案例来说,即使是在非容器化的应用中,你也可能会碰到这个问题。假如你的应用程序在编译时,使用 strip 删除了ELF二进制文件的符号表,那么你同样也只能看到函数的地址。
|
||||
|
||||
现在的磁盘空间,其实已经足够大了。保留这些符号,虽然会导致编译后的文件变大,但对整个磁盘空间来说已经不是什么大问题。所以为了调试的方便,建议你还是把它们保留着。
|
||||
|
||||
顺便提一下,案例中各种工具的安装方法,可以算是我们专栏学习的基本功,这一点希望你能够熟悉并掌握。还是那句话,不会安装先查文档,还是不行就上网搜索或者在文章里留言提问。
|
||||
|
||||
在这里也要表扬一下,很多同学已经把摸索到的方法分享到了留言中。记录并分享,是一个很好的习惯。
|
||||
|
||||
## 问题 2:如何用perf工具分析Java程序
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1e/dc/1eb200e2a68da9a00b2ee009f3de94dc.png" alt=""><img src="https://static001.geekbang.org/resource/image/97/6d/97f609aae409bd9840f606c1d9bc7e6d.jpg" alt="">
|
||||
|
||||
这两个问题,其实是上一个 perf 问题的延伸。 像是Java这种通过 JVM 来运行的应用程序,运行堆栈用的都是 JVM 内置的函数和堆栈管理。所以,从系统层面你只能看到JVM的函数堆栈,而不能直接得到Java应用程序的堆栈。
|
||||
|
||||
perf_events 实际上已经支持了 JIT,但还需要一个 /tmp/perf-PID.map文件,来进行符号翻译。当然,开源项目 [perf-map-agent ](https://github.com/jvm-profiling-tools/perf-map-agent)可以帮你生成这个符号表。
|
||||
|
||||
此外,为了生成全部调用栈,你还需要开启JDK的选项 -XX:+PreserveFramePointer。因为这里涉及到大量的 Java 知识,我就不再详细展开了。如果你的应用刚好基于 Java ,那么你可以参考 Netflix 的技术博客 [Java in Flames](https://medium.com/netflix-techblog/java-in-flames-e763b3d32166) ,来查看详细的使用步骤。
|
||||
|
||||
说到这里,我也想强调一个问题,那就是学习性能优化时,不要一开始就把自己限定在具体的某个编程语言或者性能工具中,纠结于语言或工具的细节出不来。
|
||||
|
||||
掌握整体的分析思路,才是我们首先要做的。因为,性能优化的原理和思路,在任何编程语言中都是相通的。
|
||||
|
||||
## 问题 3:为什么 perf 的报告中,很多符号都不显示调用栈
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/05/4902af1ff4d710aa7cb150f44e3e3c05.png" alt="">
|
||||
|
||||
perf report 是一个可视化展示 perf.data 的工具。在第 08 讲的案例中,我直接给出了最终结果,并没有详细介绍它的参数。估计很多同学的机器在运行时,都碰到了跟路过同学一样的问题,看到的是下面这个界面。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/33/e9/335d41ccf24d93aafe0e4d511218b6e9.png" alt="">
|
||||
|
||||
这个界面可以清楚看到,perf report 的输出中,只有 swapper 显示了调用栈,其他所有符号都不能查看堆栈情况,包括我们案例中的 app 应用。
|
||||
|
||||
这种情况我们以前也遇到过,当你发现性能工具的输出无法理解时,应该怎么办呢?当然还是查工具的手册。比如,你可以执行 man perf-report 命令,找到 -g 参数的说明:
|
||||
|
||||
```
|
||||
-g, --call-graph=<print_type,threshold[,print_limit],order,sort_key[,branch],value>
|
||||
Display call chains using type, min percent threshold, print limit, call order, sort key, optional branch and value. Note that
|
||||
ordering is not fixed so any parameter can be given in an arbitrary order. One exception is the print_limit which should be
|
||||
preceded by threshold.
|
||||
|
||||
print_type can be either:
|
||||
- flat: single column, linear exposure of call chains.
|
||||
- graph: use a graph tree, displaying absolute overhead rates. (default)
|
||||
- fractal: like graph, but displays relative rates. Each branch of
|
||||
the tree is considered as a new profiled object.
|
||||
- folded: call chains are displayed in a line, separated by semicolons
|
||||
- none: disable call chain display.
|
||||
|
||||
threshold is a percentage value which specifies a minimum percent to be
|
||||
included in the output call graph. Default is 0.5 (%).
|
||||
|
||||
print_limit is only applied when stdio interface is used. It's to limit
|
||||
number of call graph entries in a single hist entry. Note that it needs
|
||||
to be given after threshold (but not necessarily consecutive).
|
||||
Default is 0 (unlimited).
|
||||
|
||||
order can be either:
|
||||
- callee: callee based call graph.
|
||||
- caller: inverted caller based call graph.
|
||||
Default is 'caller' when --children is used, otherwise 'callee'.
|
||||
|
||||
sort_key can be:
|
||||
- function: compare on functions (default)
|
||||
- address: compare on individual code addresses
|
||||
- srcline: compare on source filename and line number
|
||||
|
||||
branch can be:
|
||||
- branch: include last branch information in callgraph when available.
|
||||
Usually more convenient to use --branch-history for this.
|
||||
|
||||
value can be:
|
||||
- percent: diplay overhead percent (default)
|
||||
- period: display event period
|
||||
- count: display event count
|
||||
|
||||
```
|
||||
|
||||
通过这个说明可以看到,-g 选项等同于 --call-graph,它的参数是后面那些被逗号隔开的选项,意思分别是输出类型、最小阈值、输出限制、排序方法、排序关键词、分支以及值的类型。
|
||||
|
||||
我们可以看到,这里默认的参数是 graph,0.5,caller,function,percent,具体含义文档中都有详细讲解,这里我就不再重复了。
|
||||
|
||||
现在再回过头来看我们的问题,堆栈显示不全,相关的参数当然就是最小阈值 threshold。通过手册中对threshold的说明,我们知道,当一个事件发生比例高于这个阈值时,它的调用栈才会显示出来。
|
||||
|
||||
threshold 的默认值为 0.5%,也就是说,事件比例超过 0.5%时,调用栈才能被显示。再观察我们案例应用 app 的事件比例,只有 0.34%,低于 0.5%,所以看不到 app 的调用栈就很正常了。
|
||||
|
||||
这种情况下,你只需要给 perf report 设置一个小于 0.34% 的阈值,就可以显示我们想看到的调用图了。比如执行下面的命令:
|
||||
|
||||
```
|
||||
$ perf report -g graph,0.3
|
||||
|
||||
```
|
||||
|
||||
你就可以得到下面这个新的输出界面,展开 app 后,就可以看到它的调用栈了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b3/93/b34f95617d13088671f4d9c2b9134693.png" alt="">
|
||||
|
||||
## 问题 4:怎么理解 perf report 报告
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/d8/42bf6b82da73656d6c3dad20074f57d8.png" alt=""><img src="https://static001.geekbang.org/resource/image/b9/8b/b90140f7d41790f74982d431f7e0238b.png" alt="">
|
||||
|
||||
看到这里,我估计你也曾嘀咕过,为啥不一上来就用 perf 工具解决,还要执行那么多其他工具呢? 这个问题其实就给出了很好的解释。
|
||||
|
||||
在问题4的perf report 界面中,你也一定注意到了, swapper 高达 99% 的比例。直觉来说,我们应该直接观察它才对,为什么没那么做呢?
|
||||
|
||||
其实,当你清楚了 swapper 的原理后,就很容易理解我们为什么可以忽略它了。
|
||||
|
||||
看到swapper,你可能首先想到的是SWAP分区。实际上, swapper 跟 SWAP 没有任何关系,它只在系统初始化时创建 init 进程,之后,它就成了一个最低优先级的空闲任务。也就是说,当 CPU 上没有其他任务运行时,就会执行swapper 。所以,你可以称它为“空闲任务”。
|
||||
|
||||
回到我们的问题,在 perf report 的界面中,展开它的调用栈,你会看到, swapper 时钟事件都耗费在了 do_idle 上,也就是在执行空闲任务。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/12/bd/121dcefacba1b554accd0a90ef349fbd.png" alt="">
|
||||
|
||||
所以,分析案例时,我们直接忽略了前面这个 99% 的符号,转而分析后面只有 0.3% 的 app。其实从这里你也能理解,为什么我们一开始不先用 perf 分析。
|
||||
|
||||
因为在多任务系统中,次数多的事件,不一定就是性能瓶颈。所以,只观察到一个大数值,并不能说明什么问题。具体有没有瓶颈,还需要你观测多个方面的多个指标,来交叉验证。这也是我在套路篇中不断强调的一点。
|
||||
|
||||
另外,关于 Children 和 Self 的含义,手册里其实有详细说明,还很友好地举了一个例子,来说明它们的百分比的计算方法。简单来说,
|
||||
|
||||
<li>
|
||||
Self 是最后一列的符号(可以理解为函数)本身所占比例;
|
||||
</li>
|
||||
<li>
|
||||
Children 是这个符号调用的其他符号(可以理解为子函数,包括直接和间接调用)占用的比例之和。
|
||||
</li>
|
||||
|
||||
正如同学留言问到的,很多性能工具确实会对系统性能有一定影响。就拿 perf 来说,它需要在内核中跟踪内核栈的各种事件,那么不可避免就会带来一定的性能损失。这一点,虽然对大部分应用来说,没有太大影响,但对特定的某些应用(比如那些对时钟周期特别敏感的应用),可能就是灾难了。
|
||||
|
||||
所以,使用性能工具时,确实应该考虑工具本身对系统性能的影响。而这种情况,就需要你了解这些工具的原理。比如,
|
||||
|
||||
<li>
|
||||
perf 这种动态追踪工具,会给系统带来一定的性能损失。
|
||||
</li>
|
||||
<li>
|
||||
vmstat、pidstat 这些直接读取 proc 文件系统来获取指标的工具,不会带来性能损失。
|
||||
</li>
|
||||
|
||||
## 问题 5:性能优化书籍和参考资料推荐
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a0/ba/a0be73a43e756da48bdbdd01d71598ba.png" alt="">
|
||||
|
||||
我很高兴看到留言有这么高的学习热情,其实好多文章后面都有大量留言,希望我能推荐书籍和学习资料。这一点也是我乐意看到的。专栏学习一定不是你性能优化之旅的全部,能够带你入门、帮你解决实际问题、甚至是激发你的学习热情,已经让我非常开心。
|
||||
|
||||
在 [如何学习Linux性能优化](https://time.geekbang.org/column/article/69346) 的文章中,我曾经介绍过 Brendan Gregg,他是当之无愧的性能优化大师,你在各种 Linux 性能优化的文章中,基本都能看到他的那张性能工具图谱。
|
||||
|
||||
所以,关于性能优化的书籍,我最喜欢的其实正是他写的那本 《Systems Performance: Enterprise and the Cloud》。这本书也出了中文版,名字是《性能之巅:洞悉系统、企业与云计算》。
|
||||
|
||||
从出版时间来看,这本书确实算一本老书了,英文版的是 2013 年出版的。但是经典之所以成为经典,正是因为不会过时。这本书里的性能分析思路以及很多的性能工具,到今天依然适用。
|
||||
|
||||
另外,我也推荐你去关注他的个人网站 [http://www.brendangregg.com/](http://www.brendangregg.com/),特别是 [Linux Performance ](http://www.brendangregg.com/linuxperf.html)这个页面,包含了很多 Linux 性能优化的资料,比如:
|
||||
|
||||
<li>
|
||||
Linux性能工具图谱 ;
|
||||
</li>
|
||||
<li>
|
||||
性能分析参考资料;
|
||||
</li>
|
||||
<li>
|
||||
性能优化的演讲视频 。
|
||||
</li>
|
||||
|
||||
不过,这里很多内容会涉及到大量的内核知识,对初学者来说并不友好。但是,如果你想成为高手,辛苦和坚持都是不可避免的。所以,希望你在查看这些资料时,不要一遇到不懂的就打退堂鼓。任何东西的第一遍学习有不懂的地方很正常,忍住恐惧别放弃,继续往后走,前面很多问题可能会一并解决掉,再看第二遍、第三遍就更轻松了。
|
||||
|
||||
还是那句话,抓住主线不动摇,先从最基本的原理开始,掌握性能分析的思路,然后再逐步深入,探究细节,不要试图一口吃成个大胖子。
|
||||
|
||||
最后,欢迎继续在留言区写下你的疑问,我会持续不断地解答。我的目的仍然不变,希望可以和你一起,把文章的知识变成你的能力,我们不仅仅在实战中演练,也要在交流中进步。
|
||||
Reference in New Issue
Block a user