This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,468 @@
<audio id="audio" title="46 | 案例篇:为什么应用容器化后,启动慢了很多?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c9/d7/c9c6454f1a0b04d99c95ca2c65ba4ad7.mp3"></audio>
你好,我是倪朋飞。
不知不觉,我们已经学完了整个专栏的四大基础模块,即 CPU、内存、文件系统和磁盘 I/O、以及网络的性能分析和优化。相信你已经掌握了这些基础模块的基本分析、定位思路并熟悉了相关的优化方法。
接下来,我们将进入最后一个重要模块—— 综合实战篇。这部分实战内容,也将是我们对前面所学知识的复习和深化。
我们都知道,随着 Kubernetes、Docker 等技术的普及,越来越多的企业,都已经走上了应用程序容器化的道路。我相信,你在了解学习这些技术的同时,一定也听说过不少,基于 Docker 的微服务架构带来的各种优势,比如:
<li>
使用 Docker ,把应用程序以及相关依赖打包到镜像中后,部署和升级更快捷;
</li>
<li>
把传统的单体应用拆分成多个更小的微服务应用后,每个微服务的功能都更简单,并且可以单独管理和维护;
</li>
<li>
每个微服务都可以根据需求横向扩展。即使发生故障,也只是局部服务不可用,而不像以前那样,导致整个服务不可用。
</li>
不过,任何技术都不是银弹。这些新技术,在带来诸多便捷功能之外,也带来了更高的复杂性,比如性能降低、架构复杂、排错困难等等。
今天,我就通过一个 Tomcat 案例,带你一起学习,如何分析应用程序容器化后的性能问题。
## 案例准备
今天的案例,我们只需要一台虚拟机。还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
<li>
机器配置2 CPU8GB 内存。
</li>
<li>
预先安装 docker、curl、jq、pidstat 等工具,如 apt install docker.io curl jq sysstat。
</li>
其中jq 工具专门用来在命令行中处理 json。为了更好的展示 json 数据,我们用这个工具,来格式化 json 输出。
你需要打开两个终端,登录到同一台虚拟机中,并安装上述工具。
注意,以下所有命令都默认以 root 用户运行,如果你用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
>
如果安装过程有问题,你可以先上网搜索解决,实在解决不了的,记得在留言区向我提问。
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
## 案例分析
我们今天要分析的案例,是一个 Tomcat 应用。Tomcat 是 Apache 基金会旗下Jakarta 项目开发的轻量级应用服务器,它基于 Java 语言开发。Docker 社区也维护着 Tomcat 的[官方镜像](https://hub.docker.com/_/tomcat),你可以直接使用这个镜像,来启动一个 Tomcat 应用。
我们的案例,也基于 Tomcat 的官方镜像构建,其核心逻辑很简单,就是分配一点儿内存,并输出 “Hello, world!”。
```
&lt;%
byte data[] = new byte[256*1024*1024];
out.println(&quot;Hello, wolrd!&quot;);
%&gt;
```
为了方便你运行,我已经将它打包成了一个 [Docker 镜像](https://github.com/feiskyer/linux-perf-examples/tree/master/tomcat) feisky/tomcat:8并推送到了 Docker Hub 中。你可以直接按照下面的步骤来运行它。
在终端一中,执行下面的命令,启动 Tomcat 应用,并监听 8080端口。如果一切正常你应该可以看到如下的输出
```
# -m表示设置内存为512MB
$ docker run --name tomcat --cpus 0.1 -m 512M -p 8080:8080 -itd feisky/tomcat:8
Unable to find image 'feisky/tomcat:8' locally
8: Pulling from feisky/tomcat
741437d97401: Pull complete
...
22cd96a25579: Pull complete
Digest: sha256:71871cff17b9043842c2ec99f370cc9f1de7bc121cd2c02d8e2092c6e268f7e2
Status: Downloaded newer image for feisky/tomcat:8
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
2df259b752db334d96da26f19166d662a82283057411f6332f3cbdbcab452249
```
从输出中你可以看到docker run 命令,会自动拉取镜像并启动容器。
这里顺便提一下,之前很多同学留言问,到底要怎么下载 Docker 镜像。其实,上面的 docker run就是自动下载镜像到本地后才开始运行的。
由于 Docker 镜像分多层管理所以在下载时你会看到每层的下载进度。除了像docker run 这样自动下载镜像外,你也可以分两步走,先下载镜像,然后再运行容器。
比如,你可以先运行下面的 docker pull 命令,下载镜像:
```
$ docker pull feisky/tomcat:8
8: Pulling from feisky/tomcat
Digest: sha256:71871cff17b9043842c2ec99f370cc9f1de7bc121cd2c02d8e2092c6e268f7e2
Status: Image is up to date for feisky/tomcat:8
```
显然,在我的机器中,镜像已存在,所以就不需要再次下载,直接返回成功就可以了。
接着,在终端二中使用 curl访问 Tomcat 监听的 8080 端口,确认案例已经正常启动:
```
$ curl localhost:8080
curl: (56) Recv failure: Connection reset by peer
```
不过很不幸curl 返回了 “Connection reset by peer” 的错误,说明 Tomcat 服务,并不能正常响应客户端请求。
是不是 Tomcat 启动出问题了呢?我们切换到终端一中,执行 docker logs 命令,查看容器的日志。这里注意,需要加上 -f 参数,表示跟踪容器的最新日志输出:
```
$ docker logs -f tomcat
Using CATALINA_BASE: /usr/local/tomcat
Using CATALINA_HOME: /usr/local/tomcat
Using CATALINA_TMPDIR: /usr/local/tomcat/temp
Using JRE_HOME: /docker-java-home/jre
Using CLASSPATH: /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar
```
从这儿你可以看到Tomcat 容器只打印了环境变量还没有应用程序初始化的日志。也就是说Tomcat 还在启动过程中,这时候去访问它,当然没有响应。
为了观察 Tomcat 的启动过程,我们在终端一中,继续保留 docker logs -f 命令,并在终端二中执行下面的命令,多次尝试访问 Tomcat
```
$ for ((i=0;i&lt;30;i++)); do curl localhost:8080; sleep 1; done
curl: (56) Recv failure: Connection reset by peer
curl: (56) Recv failure: Connection reset by peer
# 这儿会阻塞一会
Hello, wolrd!
curl: (52) Empty reply from server
curl: (7) Failed to connect to localhost port 8080: Connection refused
curl: (7) Failed to connect to localhost port 8080: Connection refused
```
观察一会儿可以看到一段时间后curl 终于给出了我们想要的结果 “Hello, wolrd!”。但是,随后又出现了 “Empty reply from server” ,和一直持续的 “Connection refused” 错误。换句话说Tomcat 响应一次请求后,就再也不响应了。
这是怎么回事呢?我们回到终端一中,观察 Tomcat 的日志,看看能不能找到什么线索。
从终端一中,你应该可以看到下面的输出:
```
18-Feb-2019 12:43:32.719 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/docs]
18-Feb-2019 12:43:33.725 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/docs] has finished in [1,006] ms
18-Feb-2019 12:43:33.726 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/manager]
18-Feb-2019 12:43:34.521 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/manager] has finished in [795] ms
18-Feb-2019 12:43:34.722 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler [&quot;http-nio-8080&quot;]
18-Feb-2019 12:43:35.319 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler [&quot;ajp-nio-8009&quot;]
18-Feb-2019 12:43:35.821 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 24096 ms
root@ubuntu:~#
```
从内容上可以看到Tomcat 在启动 24s 后完成初始化,并且正常启动。从日志上来看,没有什么问题。
不过,细心的你肯定注意到了最后一行,明显是回到了 Linux 的 SHELL 终端中,而没有继续等待 Docker 输出的容器日志。
输出重新回到 SHELL 终端,通常表示上一个命令已经结束。而我们的上一个命令,是 docker logs -f 命令。那么,它的退出就只有两种可能了,要么是容器退出了,要么就是 dockerd 进程退出了。
究竟是哪种情况呢?这就需要我们进一步确认了。我们可以在终端一中,执行下面的命令,查看容器的状态:
```
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0f2b3fcdd257 feisky/tomcat:8 &quot;catalina.sh run&quot; 2 minutes ago Exited (137) About a minute ago tomcat
```
你会看到,容器处于 Exited 状态,说明是第一种情况,容器已经退出。不过为什么会这样呢?显然,在前面容器的日志里,我们并没有发现线索,那就只能从 Docker 本身入手了。
我们可以调用 Docker 的 API查询容器的状态、退出码以及错误信息然后确定容器退出的原因。这些可以通过 docker inspect 命令来完成,比如,你可以继续执行下面的命令,通过 -f 选项设置只输出容器的状态:
```
# 显示容器状态jq用来格式化json输出
$ docker inspect tomcat -f '{{json .State}}' | jq
{
&quot;Status&quot;: &quot;exited&quot;,
&quot;Running&quot;: false,
&quot;Paused&quot;: false,
&quot;Restarting&quot;: false,
&quot;OOMKilled&quot;: true,
&quot;Dead&quot;: false,
&quot;Pid&quot;: 0,
&quot;ExitCode&quot;: 137,
&quot;Error&quot;: &quot;&quot;,
...
}
```
这次你可以看到,容器已经处于 exited 状态OOMKilled 是 trueExitCode 是 137。这其中OOMKilled 表示容器被 OOM 杀死了。
我们前面提到过OOM 表示内存不足时,某些应用会被系统杀死。可是,为什么内存会不足呢?我们的应用分配了 256 MB 的内存,而容器启动时,明明通过 -m 选项,设置了 512 MB 的内存,按说应该是足够的。
到这里,我估计你应该还记得,当 OOM 发生时,系统会把相关的 OOM 信息,记录到日志中。所以,接下来,我们可以在终端中执行 dmesg 命令,查看系统日志,并定位 OOM 相关的日志:
```
$ dmesg
[193038.106393] java invoked oom-killer: gfp_mask=0x14000c0(GFP_KERNEL), nodemask=(null), order=0, oom_score_adj=0
[193038.106396] java cpuset=0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53 mems_allowed=0
[193038.106402] CPU: 0 PID: 27424 Comm: java Tainted: G OE 4.15.0-1037 #39-Ubuntu
[193038.106404] Hardware name: Microsoft Corporation Virtual Machine/Virtual Machine, BIOS 090007 06/02/2017
[193038.106405] Call Trace:
[193038.106414] dump_stack+0x63/0x89
[193038.106419] dump_header+0x71/0x285
[193038.106422] oom_kill_process+0x220/0x440
[193038.106424] out_of_memory+0x2d1/0x4f0
[193038.106429] mem_cgroup_out_of_memory+0x4b/0x80
[193038.106432] mem_cgroup_oom_synchronize+0x2e8/0x320
[193038.106435] ? mem_cgroup_css_online+0x40/0x40
[193038.106437] pagefault_out_of_memory+0x36/0x7b
[193038.106443] mm_fault_error+0x90/0x180
[193038.106445] __do_page_fault+0x4a5/0x4d0
[193038.106448] do_page_fault+0x2e/0xe0
[193038.106454] ? page_fault+0x2f/0x50
[193038.106456] page_fault+0x45/0x50
[193038.106459] RIP: 0033:0x7fa053e5a20d
[193038.106460] RSP: 002b:00007fa0060159e8 EFLAGS: 00010206
[193038.106462] RAX: 0000000000000000 RBX: 00007fa04c4b3000 RCX: 0000000009187440
[193038.106463] RDX: 00000000943aa440 RSI: 0000000000000000 RDI: 000000009b223000
[193038.106464] RBP: 00007fa006015a60 R08: 0000000002000002 R09: 00007fa053d0a8a1
[193038.106465] R10: 00007fa04c018b80 R11: 0000000000000206 R12: 0000000100000768
[193038.106466] R13: 00007fa04c4b3000 R14: 0000000100000768 R15: 0000000010000000
[193038.106468] Task in /docker/0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53 killed as a result of limit of /docker/0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53
[193038.106478] memory: usage 524288kB, limit 524288kB, failcnt 77
[193038.106480] memory+swap: usage 0kB, limit 9007199254740988kB, failcnt 0
[193038.106481] kmem: usage 3708kB, limit 9007199254740988kB, failcnt 0
[193038.106481] Memory cgroup stats for /docker/0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53: cache:0KB rss:520580KB rss_huge:450560KB shmem:0KB mapped_file:0KB dirty:0KB writeback:0KB inactive_anon:0KB active_anon:520580KB inactive_file:0KB active_file:0KB unevictable:0KB
[193038.106494] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name
[193038.106571] [27281] 0 27281 1153302 134371 1466368 0 0 java
[193038.106574] Memory cgroup out of memory: Kill process 27281 (java) score 1027 or sacrifice child
[193038.148334] Killed process 27281 (java) total-vm:4613208kB, anon-rss:517316kB, file-rss:20168kB, shmem-rss:0kB
[193039.607503] oom_reaper: reaped process 27281 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
```
从 dmesg 的输出,你就可以看到很详细的 OOM 记录了。你应该可以看到下面几个关键点。
<li>
第一,被杀死的是一个 java 进程。从内核调用栈上的 mem_cgroup_out_of_memory 可以看出,它是因为超过 cgroup 的内存限制,而被 OOM 杀死的。
</li>
<li>
第二java 进程是在容器内运行的,而容器内存的使用量和限制都是 512M524288kB。目前使用量已经达到了限制所以会导致 OOM。
</li>
<li>
第三被杀死的进程PID 为 27281虚拟内存为 4.3Gtotal-vm:4613208kB匿名内存为 505Manon-rss:517316kB页内存为 19M20168kB。换句话说匿名内存是主要的内存占用。而且匿名内存加上页内存总共是 524M已经超过了 512M 的限制。
</li>
综合这几点可以看出Tomcat 容器的内存主要用在了匿名内存中,而匿名内存,其实就是主动申请分配的堆内存。
不过,为什么 Tomcat 会申请这么多的堆内存呢要知道Tomcat 是基于 Java 开发的,所以应该不难想到,这很可能是 JVM 堆内存配置的问题。
我们知道JVM 根据系统的内存总量,来自动管理堆内存,不明确配置的话,堆内存的默认限制是物理内存的四分之一。不过,前面我们已经限制了容器内存为 512 Mjava 的堆内存到底是多少呢?
我们继续在终端中,执行下面的命令,重新启动 tomcat 容器,并调用 java 命令行来查看堆内存大小:
```
# 重新启动容器
$ docker rm -f tomcat
$ docker run --name tomcat --cpus 0.1 -m 512M -p 8080:8080 -itd feisky/tomcat:8
# 查看堆内存,注意单位是字节
$ docker exec tomcat java -XX:+PrintFlagsFinal -version | grep HeapSize
uintx ErgoHeapSizeLimit = 0 {product}
uintx HeapSizePerGCThread = 87241520 {product}
uintx InitialHeapSize := 132120576 {product}
uintx LargePageHeapSizeThreshold = 134217728 {product}
uintx MaxHeapSize := 2092957696 {product}
```
你可以看到初始堆内存的大小InitialHeapSize是 126MB而最大堆内存则是 1.95GB,这可比容器限制的 512 MB 大多了。
之所以会这么大,其实是因为,容器内部看不到 Docker 为它设置的内存限制。虽然在启动容器时,我们通过 -m 512M 选项,给容器设置了 512M 的内存限制。但实际上,从容器内部看到的限制,却并不是 512M。
我们在终端中,继续执行下面的命令:
```
$ docker exec tomcat free -m
total used free shared buff/cache available
Mem: 7977 521 1941 0 5514 7148
Swap: 0 0 0
```
果然,容器内部看到的内存,仍是主机内存。
知道了问题根源,解决方法就很简单了,给 JVM 正确配置内存限制为 512M 就可以了。
比如,你可以执行下面的命令,通过环境变量 JAVA_OPTS=-Xmx512m -Xms512m 把JVM 的初始内存和最大内存都设为 512MB
```
# 删除问题容器
$ docker rm -f tomcat
# 运行新的容器
$ docker run --name tomcat --cpus 0.1 -m 512M -e JAVA_OPTS='-Xmx512m -Xms512m' -p 8080:8080 -itd feisky/tomcat:8
```
接着,再切换到终端二中,重新在循环中执行 curl 命令,查看 Tomcat 的响应:
```
$ for ((i=0;i&lt;30;i++)); do curl localhost:8080; sleep 1; done
curl: (56) Recv failure: Connection reset by peer
curl: (56) Recv failure: Connection reset by peer
Hello, wolrd!
Hello, wolrd!
Hello, wolrd!
```
可以看到,刚开始时,显示的还是 “Connection reset by peer” 错误。不过,稍等一会儿后,就是连续的 “Hello, wolrd!” 输出了。这说明, Tomcat 已经正常启动。
这时,我们切换回终端一,执行 docker logs 命令,查看 Tomcat 容器的日志:
```
$ docker logs -f tomcat
...
18-Feb-2019 12:52:00.823 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/manager]
18-Feb-2019 12:52:01.422 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/manager] has finished in [598] ms
18-Feb-2019 12:52:01.920 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler [&quot;http-nio-8080&quot;]
18-Feb-2019 12:52:02.323 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler [&quot;ajp-nio-8009&quot;]
18-Feb-2019 12:52:02.523 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 22798 ms
```
这次Tomcat 也正常启动了。不过,最后一行的启动时间,似乎比较刺眼。启动过程,居然需要 22 秒,这也太慢了吧。
由于这个时间是花在容器启动上的,要排查这个问题,我们就要重启容器,并借助性能分析工具来分析容器进程。至于工具的选用,回顾一下我们前面的案例,我觉得可以先用 top 看看。
我们切换到终端二中,运行 top 命令;然后再切换到终端一,执行下面的命令,重启容器:
```
# 删除旧容器
$ docker rm -f tomcat
# 运行新容器
$ docker run --name tomcat --cpus 0.1 -m 512M -e JAVA_OPTS='-Xmx512m -Xms512m' -p 8080:8080 -itd feisky/tomcat:8
```
接着,再切换到终端二,观察 top 的输出:
```
$ top
top - 12:57:18 up 2 days, 5:50, 2 users, load average: 0.00, 0.02, 0.00
Tasks: 131 total, 1 running, 74 sleeping, 0 stopped, 0 zombie
%Cpu0 : 3.0 us, 0.3 sy, 0.0 ni, 96.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 5.7 us, 0.3 sy, 0.0 ni, 94.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 8169304 total, 2465984 free, 500812 used, 5202508 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 7353652 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
29457 root 20 0 2791736 73704 19164 S 10.0 0.9 0:01.61 java 27349 root 20 0 1121372 96760 39340 S 0.3 1.2 4:20.82 dockerd
27376 root 20 0 1031760 43768 21680 S 0.3 0.5 2:44.47 docker-containe 29430 root 20 0 7376 3604 3128 S 0.3 0.0 0:00.01 docker-containe
1 root 20 0 78132 9332 6744 S 0.0 0.1 0:16.12 systemd
```
从 top 的输出,我们可以发现,
<li>
从系统整体来看,两个 CPU 的使用率分别是 3% 和 5.7% ,都不算高,大部分还是空闲的;可用内存还有 7GB7353652 avail Mem也非常充足。
</li>
<li>
具体到进程上java 进程的 CPU 使用率为 10%,内存使用 0.9%,其他进程就都很低了。
</li>
这些指标都不算高,看起来都没啥问题。不过,事实究竟如何呢?我们还得继续找下去。由于 java 进程的 CPU 使用率最高,所以要把它当成重点,继续分析其性能情况。
说到进程的性能分析工具,你一定也想起了 pidstat。接下来我们就用 pidstat 再来分析一下。我们回到终端一中,执行 pidstat 命令:
```
# -t表示显示线程-p指定进程号
$ pidstat -t -p 29457 1
12:59:59 UID TGID TID %usr %system %guest %wait %CPU CPU Command
13:00:00 0 29457 - 0.00 0.00 0.00 0.00 0.00 0 java
13:00:00 0 - 29457 0.00 0.00 0.00 0.00 0.00 0 |__java
13:00:00 0 - 29458 0.00 0.00 0.00 0.00 0.00 1 |__java
...
13:00:00 0 - 29491 0.00 0.00 0.00 0.00 0.00 0 |__java
```
结果中各种CPU使用率全是0看起来不对呀。再想想我们有没有漏掉什么线索呢对了这时候容器启动已经结束了在没有客户端请求的情况下Tomcat 本身啥也不用做CPU 使用率当然是 0。
为了分析启动过程中的问题,我们需要再次重启容器。继续在终端一,按下 Ctrl+C 停止 pidstat 命令;然后执行下面的命令,重启容器。成功重启后,拿到新的 PID再重新运行 pidstat 命令:
```
# 删除旧容器
$ docker rm -f tomcat
# 运行新容器
$ docker run --name tomcat --cpus 0.1 -m 512M -e JAVA_OPTS='-Xmx512m -Xms512m' -p 8080:8080 -itd feisky/tomcat:8
# 查询新容器中进程的Pid
$ PID=$(docker inspect tomcat -f '{{.State.Pid}}')
# 执行 pidstat
$ pidstat -t -p $PID 1
12:59:28 UID TGID TID %usr %system %guest %wait %CPU CPU Command
12:59:29 0 29850 - 10.00 0.00 0.00 0.00 10.00 0 java
12:59:29 0 - 29850 0.00 0.00 0.00 0.00 0.00 0 |__java
12:59:29 0 - 29897 5.00 1.00 0.00 86.00 6.00 1 |__java
...
12:59:29 0 - 29905 3.00 0.00 0.00 97.00 3.00 0 |__java
12:59:29 0 - 29906 2.00 0.00 0.00 49.00 2.00 1 |__java
12:59:29 0 - 29908 0.00 0.00 0.00 45.00 0.00 0 |__java
```
仔细观察这次的输出,你会发现,虽然 CPU 使用率(%CPU很低但等待运行的使用率%wait却非常高最高甚至已经达到了 97%。这说明,这些线程大部分时间都在等待调度,而不是真正的运行。
>
注:如果你看不到 %wait 指标,请先升级 sysstat 后再试试。
为什么CPU 使用率这么低,线程的大部分时间还要等待 CPU 呢?由于这个现象因 Docker 而起,自然的,你应该想到,这可能是因为 Docker 为容器设置了限制。
再回顾一下,案例开始时容器的启动命令。我们用 --cpus 0.1 ,为容器设置了 0.1 个 CPU 的限制,也就是 10% 的 CPU。这里也就可以解释为什么 java 进程只有 10% 的 CPU 使用率,也会大部分时间都在等待了。
找出原因,最后的优化也就简单了,把 CPU 限制增大就可以了。比如,你可以执行下面的命令,将 CPU 限制增大到 1 ;然后再重启,并观察启动日志:
```
# 删除旧容器
$ docker rm -f tomcat
# 运行新容器
$ docker run --name tomcat --cpus 1 -m 512M -e JAVA_OPTS='-Xmx512m -Xms512m' -p 8080:8080 -itd feisky/tomcat:8
# 查看容器日志
$ docker logs -f tomcat
...
18-Feb-2019 12:54:02.139 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 2001 ms
```
现在可以看到Tomcat 的启动过程,只需要 2 秒就完成了,果然比前面的 22 秒快多了。
虽然我们通过增大 CPU 的限制,解决了这个问题。不过再碰到类似问题,你可能会觉得这种方法太麻烦了。因为要设置容器的资源限制,还需要我们预先评估应用程序的性能。显然还有更简单的方法,比如说直接去掉限制,让容器跑就是了。
不过,这种简单方法,却很可能带来更严重的问题。没有资源限制,就意味着容器可以占用整个系统的资源。这样,一旦任何应用程序发生异常,都有可能拖垮整台机器。
实际上,这也是在各大容器平台上最常见的一个问题。一开始图省事不设限,但当容器数量增长上来的时候,就会经常出现各种异常问题。最终查下来,可能就是因为某个应用资源使用过高,导致整台机器短期内无法响应。只有设置了资源限制,才能确保杜绝类似问题。
## 小结
今天,我带你学习了,如何分析容器化后应用程序性能下降的问题。
如果你在 Docker 容器中运行 Java 应用,一定要确保,在设置容器资源限制的同时,配置好 JVM 的资源选项(比如堆内存等)。当然,如果你可以升级 Java 版本,那么升级到 Java 10 ,就可以自动解决类似问题了。
当碰到容器化的应用程序性能时,你依然可以使用,我们前面讲过的各种方法来分析和定位。只不过要记得,容器化后的性能分析,跟前面内容稍微有些区别,比如下面这几点。
<li>
容器本身通过 cgroups 进行资源隔离,所以,在分析时要考虑 cgroups 对应用程序的影响。
</li>
<li>
容器的文件系统、网络协议栈等跟主机隔离。虽然在容器外面,我们也可以分析容器的行为,不过有时候,进入容器的命名空间内部,可能更为方便。
</li>
<li>
容器的运行可能还会依赖于其他组件,比如各种网络插件(比如 CNI、存储插件比如 CSI、设备插件比如 GPU让容器的性能分析更加复杂。如果你需要分析容器性能别忘了考虑它们对性能的影响。
</li>
## 思考
最后,我想邀请你一起来聊聊,你碰到过的容器性能问题。你是怎么分析它们的?又是怎么解决根源问题的?你可以结合我的讲解,总结自己的思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,306 @@
<audio id="audio" title="47 | 案例篇:服务器总是时不时丢包,我该怎么办?(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/54/1f/5445ae2cde4e67d01d2a4e5302ae901f.mp3"></audio>
你好,我是倪朋飞。
上一节,我们梳理了,应用程序容器化后性能下降的分析方法。一起先简单回顾下。
容器利用 Linux 内核提供的命名空间技术,将不同应用程序的运行隔离起来,并用统一的镜像,来管理应用程序的依赖环境。这为应用程序的管理和维护,带来了极大的便捷性,并进一步催生了微服务、云原生等新一代技术架构。
不过,虽说有很多优势,但容器化也会对应用程序的性能带来一定影响。比如,上一节我们一起分析的 Java 应用,就容易发生启动过慢、运行一段时间后 OOM 退出等问题。当你碰到这种问题时,不要慌,我们前面四大基础模块中的各种思路,都依然适用。
实际上我们专栏中的很多案例都在容器中运行。容器化后应用程序会通过命名空间进行隔离。所以你在分析时不要忘了结合命名空间、cgroups、iptables 等来综合分析。比如:
<li>
cgroups 会影响容器应用的运行;
</li>
<li>
iptables 中的 NAT会影响容器的网络性能
</li>
<li>
叠加文件系统,会影响应用的 I/O 性能等。
</li>
关于 NAT 的影响,我在网络模块的 [如何优化NAT性能](https://time.geekbang.org/column/article/83189) 文章中,已经为你介绍了很多优化思路。今天,我们一起来看另一种情况,也就是丢包的分析方法。
所谓丢包,是指在网络数据的收发过程中,由于种种原因,数据包还没传输到应用程序中,就被丢弃了。这些被丢弃包的数量,除以总的传输包数,也就是我们常说的**丢包率**。丢包率是网络性能中最核心的指标之一。
丢包通常会带来严重的性能下降,特别是对 TCP 来说,丢包通常意味着网络拥塞和重传,进而还会导致网络延迟增大、吞吐降低。
接下来,我就以最常用的反向代理服务器 Nginx 为例,带你一起看看,如何分析网络丢包的问题。由于内容比较多,这个案例将分为上下两篇来讲解,今天我们先看第一部分内容。
## 案例准备
今天的案例需要用到两台虚拟机,还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
<li>
机器配置2 CPU8GB 内存。
</li>
<li>
预先安装 docker、curl、hping3 等工具,如 apt install docker.io curl hping3。
</li>
这些工具,我们在前面的案例中已经多次使用,这里就不再重复介绍。
现在,打开两个终端,分别登录到这两台虚拟机中,并安装上述工具。
注意,以下所有命令都默认以 root 用户运行,如果你用普通用户身份登陆系统,请运行 sudo su root 命令,切换到 root 用户。
>
如果安装过程有问题,你可以先上网搜索解决,实在解决不了的,记得在留言区向我提问。
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
## 案例分析
我们今天要分析的案例是一个 Nginx 应用如下图所示hping3 和 curl 是 Nginx 的客户端。
<img src="https://static001.geekbang.org/resource/image/7d/1b/7d8cb9a2ce1c3bad4d74f46a632f671b.png" alt="">
为了方便你运行,我已经把它打包成了一个 Docker 镜像,并推送到 Docker Hub 中。你可以直接按照下面的步骤来运行它。
在终端一中执行下面的命令,启动 Nginx 应用并在80端口监听。如果一切正常你应该可以看到如下的输出
```
$ docker run --name nginx --hostname nginx --privileged -p 80:80 -itd feisky/nginx:drop
dae0202cc27e5082b282a6aeeb1398fcec423c642e63322da2a97b9ebd7538e0
```
然后,执行 docker ps 命令查询容器的状态你会发现容器已经处于运行状态Up
```
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
dae0202cc27e feisky/nginx:drop &quot;/start.sh&quot; 4 minutes ago Up 4 minutes 0.0.0.0:80-&gt;80/tcp nginx
```
不过,从 docker ps 的输出,我们只能知道容器处于运行状态,至于 Nginx 是否可以正常处理外部请求,还需要进一步的确认。
接着,我们切换到终端二中,执行下面的 hping3 命令,进一步验证 Nginx 是不是真的可以正常访问了。注意,这里我没有使用 ping是因为 ping 基于 ICMP 协议,而 Nginx 使用的是 TCP 协议。
```
# -c表示发送10个请求-S表示使用TCP SYN-p指定端口为80
$ hping3 -c 10 -S -p 80 192.168.0.30
HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=3 win=5120 rtt=7.5 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=4 win=5120 rtt=7.4 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=5 win=5120 rtt=3.3 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=7 win=5120 rtt=3.0 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=6 win=5120 rtt=3027.2 ms
--- 192.168.0.30 hping statistic ---
10 packets transmitted, 5 packets received, 50% packet loss
round-trip min/avg/max = 3.0/609.7/3027.2 ms
```
从 hping3 的输出中,我们可以发现,发送了 10 个请求包,却只收到了 5 个回复50% 的包都丢了。再观察每个请求的 RTT 可以发现RTT 也有非常大的波动变化,小的时候只有 3ms而大的时候则有 3s。
根据这些输出我们基本能判断已经发生了丢包现象。可以猜测3s 的 RTT ,很可能是因为丢包后重传导致的。那到底是哪里发生了丢包呢?
排查之前,我们可以回忆一下 Linux 的网络收发流程,先从理论上分析,哪里有可能会发生丢包。你不妨拿出手边的笔和纸,边回忆边在纸上梳理,思考清楚再继续下面的内容。
在这里,为了帮你理解网络丢包的原理,我画了一张图,你可以保存并打印出来使用:
<img src="https://static001.geekbang.org/resource/image/dd/fd/dd5b4050d555b1c23362456e357dfffd.png" alt="">
从图中你可以看出,可能发生丢包的位置,实际上贯穿了整个网络协议栈。换句话说,全程都有丢包的可能。比如我们从下往上看:
<li>
在两台 VM 连接之间,可能会发生传输失败的错误,比如网络拥塞、线路错误等;
</li>
<li>
在网卡收包后,环形缓冲区可能会因为溢出而丢包;
</li>
<li>
在链路层可能会因为网络帧校验失败、QoS 等而丢包;
</li>
<li>
在 IP 层,可能会因为路由失败、组包大小超过 MTU 等而丢包;
</li>
<li>
在传输层,可能会因为端口未监听、资源占用超过内核限制等而丢包;
</li>
<li>
在套接字层,可能会因为套接字缓冲区溢出而丢包;
</li>
<li>
在应用层,可能会因为应用程序异常而丢包;
</li>
<li>
此外,如果配置了 iptables 规则,这些网络包也可能因为 iptables 过滤规则而丢包。
</li>
当然,上面这些问题,还有可能同时发生在通信的两台机器中。不过,由于我们没对 VM2 做任何修改,并且 VM2 也只运行了一个最简单的 hping3 命令,这儿不妨假设它是没有问题的。
为了简化整个排查过程,我们还可以进一步假设, VM1 的网络和内核配置也没问题。这样一来,有可能发生问题的位置,就都在容器内部了。
现在我们切换回终端一,执行下面的命令,进入容器的终端中:
```
$ docker exec -it nginx bash
root@nginx:/#
```
在这里简单说明一下,接下来的所有分析,前面带有 **root@nginx:/#** 的操作,都表示在容器中进行。
>
注意:实际环境中,容器内部和外部都有可能发生问题。不过不要担心,容器内、外部的分析步骤和思路都是一样的,只不过要花更多的时间而已。
那么, 接下来,我们就可以从协议栈中,逐层排查丢包问题。
### 链路层
首先来看最底下的链路层。当缓冲区溢出等原因导致网卡丢包时Linux 会在网卡收发数据的统计信息中,记录下收发错误的次数。
你可以通过 ethtool 或者 netstat ,来查看网卡的丢包记录。比如,可以在容器中执行下面的命令,查看丢包情况:
```
root@nginx:/# netstat -i
Kernel Interface table
Iface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0 100 31 0 0 0 8 0 0 0 BMRU
lo 65536 0 0 0 0 0 0 0 0 LRU
```
输出中的 RX-OK、RX-ERR、RX-DRP、RX-OVR 分别表示接收时的总包数、总错误数、进入Ring Buffer 后因其他原因(如内存不足)导致的丢包数以及 Ring Buffer 溢出导致的丢包数。
TX-OK、TX-ERR、TX-DRP、TX-OVR 也代表类似的含义,只不过是指发送时对应的各个指标。
>
注意,由于 Docker 容器的虚拟网卡,实际上是一对 veth pair一端接入容器中用作 eth0另一端在主机中接入 docker0 网桥中。veth 驱动并没有实现网络统计的功能,所以使用 ethtool -S 命令,无法得到网卡收发数据的汇总信息。
从这个输出中,我们没有发现任何错误,说明容器的虚拟网卡没有丢包。不过要注意,如果用 tc 等工具配置了 QoS那么 tc 规则导致的丢包,就不会包含在网卡的统计信息中。
所以接下来,我们还要检查一下 eth0 上是否配置了 tc 规则,并查看有没有丢包。我们继续容器终端中,执行下面的 tc 命令,不过这次注意添加 -s 选项,以输出统计信息:
```
root@nginx:/# tc -s qdisc show dev eth0
qdisc netem 800d: root refcnt 2 limit 1000 loss 30%
Sent 432 bytes 8 pkt (dropped 4, overlimits 0 requeues 0)
backlog 0b 0p requeues 0
```
从 tc 的输出中可以看到, eth0 上面配置了一个网络模拟排队规则qdisc netem并且配置了丢包率为 30%loss 30%)。再看后面的统计信息,发送了 8 个包,但是丢了 4 个。
看来,应该就是这里,导致 Nginx 回复的响应包,被 netem 模块给丢了。
既然发现了问题,解决方法也就很简单了,直接删掉 netem 模块就可以了。我们可以继续在容器终端中,执行下面的命令,删除 tc 中的 netem 模块:
```
root@nginx:/# tc qdisc del dev eth0 root netem loss 30%
```
删除后,问题到底解决了没?我们切换到终端二中,重新执行刚才的 hping3 命令,看看现在还有没有问题:
```
$ hping3 -c 10 -S -p 80 192.168.0.30
HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=0 win=5120 rtt=7.9 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=2 win=5120 rtt=1003.8 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=5 win=5120 rtt=7.6 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=6 win=5120 rtt=7.4 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=9 win=5120 rtt=3.0 ms
--- 192.168.0.30 hping statistic ---
10 packets transmitted, 5 packets received, 50% packet loss
round-trip min/avg/max = 3.0/205.9/1003.8 ms
```
不幸的是,从 hping3 的输出中,我们可以看到,跟前面现象一样,还是 50% 的丢包RTT 的波动也仍旧很大,从 3ms 到 1s。
显然,问题还是没解决,丢包还在继续发生。不过,既然链路层已经排查完了,我们就继续向上层分析,看看网络层和传输层有没有问题。
### 网络层和传输层
我们知道,在网络层和传输层中,引发丢包的因素非常多。不过,其实想确认是否丢包,是非常简单的事,因为 Linux 已经为我们提供了各个协议的收发汇总情况。
我们继续在容器终端中,执行下面的 netstat -s 命令,就可以看到协议的收发汇总,以及错误信息了:
```
root@nginx:/# netstat -s
Ip:
Forwarding: 1 //开启转发
31 total packets received //总收包数
0 forwarded //转发包数
0 incoming packets discarded //接收丢包数
25 incoming packets delivered //接收的数据包数
15 requests sent out //发出的数据包数
Icmp:
0 ICMP messages received //收到的ICMP包数
0 input ICMP message failed //收到ICMP失败数
ICMP input histogram:
0 ICMP messages sent //ICMP发送数
0 ICMP messages failed //ICMP失败数
ICMP output histogram:
Tcp:
0 active connection openings //主动连接数
0 passive connection openings //被动连接数
11 failed connection attempts //失败连接尝试数
0 connection resets received //接收的连接重置数
0 connections established //建立连接数
25 segments received //已接收报文数
21 segments sent out //已发送报文数
4 segments retransmitted //重传报文数
0 bad segments received //错误报文数
0 resets sent //发出的连接重置数
Udp:
0 packets received
...
TcpExt:
11 resets received for embryonic SYN_RECV sockets //半连接重置数
0 packet headers predicted
TCPTimeouts: 7 //超时数
TCPSynRetrans: 4 //SYN重传数
...
```
netstat 汇总了 IP、ICMP、TCP、UDP 等各种协议的收发统计信息。不过,我们的目的是排查丢包问题,所以这里主要观察的是错误数、丢包数以及重传数。
根据上面的输出,你可以看到,只有 TCP 协议发生了丢包和重传,分别是:
<li>
11 次连接失败重试11 failed connection attempts
</li>
<li>
4 次重传4 segments retransmitted
</li>
<li>
11 次半连接重置11 resets received for embryonic SYN_RECV sockets
</li>
<li>
4 次 SYN 重传TCPSynRetrans
</li>
<li>
7 次超时TCPTimeouts
</li>
这个结果告诉我们TCP 协议有多次超时和失败重试,并且主要错误是半连接重置。换句话说,主要的失败,都是三次握手失败。
不过,虽然在这儿看到了这么多失败,但具体失败的根源还是无法确定。所以,我们还需要继续顺着协议栈来分析。接下来的几层又该如何分析呢?你不妨自己先来思考操作一下,下一节我们继续来一起探讨。
## 小结
网络丢包,通常会带来严重的性能下降,特别是对 TCP 来说,丢包通常意味着网络拥塞和重传,进一步还会导致网络延迟增大、吞吐降低。
今天的这个案例,我们学会了如何从链路层、网络层和传输层等入手,分析网络丢包的问题。不过,案例最后,我们还没有找出最终的性能瓶颈,下一节,我将继续为你讲解。
## 思考
最后,给你留一个思考题,也是案例最后提到的问题。
今天我们只分析了链路层、网络层以及传输层等。而根据 TCP/IP 协议栈和 Linux 网络收发原理,还有很多我们没分析到的地方。那么,接下来,我们又该如何分析,才能破获这个案例,找出“真凶”呢?
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,249 @@
<audio id="audio" title="48 | 案例篇:服务器总是时不时丢包,我该怎么办?(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b7/9e/b75675051a14cf9f23da317de07cd29e.mp3"></audio>
你好,我是倪朋飞。
上一节,我们一起学习了如何分析网络丢包的问题,特别是从链路层、网络层以及传输层等主要的协议栈中进行分析。
不过,通过前面这几层的分析,我们还是没有找出最终的性能瓶颈。看来,还是要继续深挖才可以。今天,我们就来继续分析这个未果的案例。
在开始下面的内容前,你可以先回忆一下上节课的内容,并且自己动脑想一想,除了我们提到的链路层、网络层以及传输层之外,还有哪些潜在问题可能会导致丢包呢?
### iptables
首先我们要知道除了网络层和传输层的各种协议iptables 和内核的连接跟踪机制也可能会导致丢包。所以,这也是发生丢包问题时,我们必须要排查的一个因素。
我们先来看看连接跟踪,我已经在 [如何优化NAT性能](https://time.geekbang.org/column/article/83189) 文章中,给你讲过连接跟踪的优化思路。要确认是不是连接跟踪导致的问题,其实只需要对比当前的连接跟踪数和最大连接跟踪数即可。
不过,由于连接跟踪在 Linux 内核中是全局的(不属于网络命名空间),我们需要退出容器终端,回到主机中来查看。
你可以在容器终端中,执行 exit ;然后执行下面的命令,查看连接跟踪数:
```
# 容器终端中执行exit
root@nginx:/# exit
exit
# 主机终端中查询内核配置
$ sysctl net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_max = 262144
$ sysctl net.netfilter.nf_conntrack_count
net.netfilter.nf_conntrack_count = 182
```
从这儿你可以看到,连接跟踪数只有 182而最大连接跟踪数则是 262144。显然这里的丢包不可能是连接跟踪导致的。
接着,再来看 iptables。回顾一下 iptables 的原理,它基于 Netfilter 框架,通过一系列的规则,对网络数据包进行过滤(如防火墙)和修改(如 NAT
这些 iptables 规则,统一管理在一系列的表中,包括 filter用于过滤、nat用于NAT、mangle用于修改分组数据 和 raw用于原始数据包等。而每张表又可以包括一系列的链用于对 iptables 规则进行分组管理。
对于丢包问题来说,最大的可能就是被 filter 表中的规则给丢弃了。要弄清楚这一点,就需要我们确认,那些目标为 DROP 和 REJECT 等会弃包的规则,有没有被执行到。
你可以把所有的 iptables 规则列出来,根据收发包的特点,跟 iptables 规则进行匹配。不过显然,如果 iptables 规则比较多,这样做的效率就会很低。
当然,更简单的方法,就是直接查询 DROP 和 REJECT 等规则的统计信息,看看是否为 0。如果统计值不是 0 ,再把相关的规则拎出来进行分析。
我们可以通过 iptables -nvL 命令,查看各条规则的统计信息。比如,你可以执行下面的 docker exec 命令,进入容器终端;然后再执行下面的 iptables 命令,就可以看到 filter 表的统计数据了:
```
# 在主机中执行
$ docker exec -it nginx bash
# 在容器中执行
root@nginx:/# iptables -t filter -nvL
Chain INPUT (policy ACCEPT 25 packets, 1000 bytes)
pkts bytes target prot opt in out source destination
6 240 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.29999999981
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 15 packets, 660 bytes)
pkts bytes target prot opt in out source destination
6 264 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.29999999981
```
从 iptables 的输出中,你可以看到,两条 DROP 规则的统计数值不是 0它们分别在 INPUT 和 OUTPUT 链中。这两条规则实际上是一样的,指的是使用 statistic 模块,进行随机 30% 的丢包。
再观察一下它们的匹配规则。0.0.0.0/0 表示匹配所有的源 IP 和目的 IP也就是会对所有包都进行随机 30% 的丢包。看起来,这应该就是导致部分丢包的“罪魁祸首”了。
既然找出了原因,接下来的优化就比较简单了。比如,把这两条规则直接删除就可以了。我们可以在容器终端中,执行下面的两条 iptables 命令,删除这两条 DROP 规则:
```
root@nginx:/# iptables -t filter -D INPUT -m statistic --mode random --probability 0.30 -j DROP
root@nginx:/# iptables -t filter -D OUTPUT -m statistic --mode random --probability 0.30 -j DROP
```
删除后,问题是否就被解决了呢?我们可以切换到终端二中,重新执行刚才的 hping3 命令,看看现在是否正常:
```
$ hping3 -c 10 -S -p 80 192.168.0.30
HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=0 win=5120 rtt=11.9 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=1 win=5120 rtt=7.8 ms
...
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=9 win=5120 rtt=15.0 ms
--- 192.168.0.30 hping statistic ---
10 packets transmitted, 10 packets received, 0% packet loss
round-trip min/avg/max = 3.3/7.9/15.0 ms
```
这次输出你可以看到,现在已经没有丢包了,并且延迟的波动变化也很小。看来,丢包问题应该已经解决了。
不过,到目前为止,我们一直使用的 hping3 工具,只能验证案例 Nginx 的 80 端口处于正常监听状态,却还没有访问 Nginx 的 HTTP 服务。所以不要匆忙下结论结束这次优化我们还需要进一步确认Nginx 能不能正常响应 HTTP 请求。
我们继续在终端二中,执行如下的 curl 命令,检查 Nginx 对 HTTP 请求的响应:
```
$ curl --max-time 3 http://192.168.0.30
curl: (28) Operation timed out after 3000 milliseconds with 0 bytes received
```
从 curl 的输出中,你可以发现,这次连接超时了。可是,刚才我们明明用 hping3 验证了端口正常,现在却发现 HTTP 连接超时,是不是因为 Nginx 突然异常退出了呢?
不妨再次运行 hping3 来确认一下:
```
$ hping3 -c 3 -S -p 80 192.168.0.30
HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=0 win=5120 rtt=7.8 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=1 win=5120 rtt=7.7 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=2 win=5120 rtt=3.6 ms
--- 192.168.0.30 hping statistic ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 3.6/6.4/7.8 ms
```
奇怪hping3 的结果显示Nginx 的 80 端口确确实实还是正常状态。这该如何是好呢?别忘了,我们还有个大杀器——抓包操作。看来有必要抓包看看了。
### tcpdump
接下来,我们切换回终端一,在容器终端中,执行下面的 tcpdump 命令,抓取 80 端口的包:
```
root@nginx:/# tcpdump -i eth0 -nn port 80
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
```
然后,切换到终端二中,再次执行前面的 curl 命令:
```
$ curl --max-time 3 http://192.168.0.30/
curl: (28) Operation timed out after 3000 milliseconds with 0 bytes received
```
等到curl 命令结束后,再次切换回终端一,查看 tcpdump 的输出:
```
14:40:00.589235 IP 10.255.255.5.39058 &gt; 172.17.0.2.80: Flags [S], seq 332257715, win 29200, options [mss 1418,sackOK,TS val 486800541 ecr 0,nop,wscale 7], length 0
14:40:00.589277 IP 172.17.0.2.80 &gt; 10.255.255.5.39058: Flags [S.], seq 1630206251, ack 332257716, win 4880, options [mss 256,sackOK,TS val 2509376001 ecr 486800541,nop,wscale 7], length 0
14:40:00.589894 IP 10.255.255.5.39058 &gt; 172.17.0.2.80: Flags [.], ack 1, win 229, options [nop,nop,TS val 486800541 ecr 2509376001], length 0
14:40:03.589352 IP 10.255.255.5.39058 &gt; 172.17.0.2.80: Flags [F.], seq 76, ack 1, win 229, options [nop,nop,TS val 486803541 ecr 2509376001], length 0
14:40:03.589417 IP 172.17.0.2.80 &gt; 10.255.255.5.39058: Flags [.], ack 1, win 40, options [nop,nop,TS val 2509379001 ecr 486800541,nop,nop,sack 1 {76:77}], length 0
```
经过这么一系列的操作,从 tcpdump 的输出中,我们就可以看到:
<li>
前三个包是正常的 TCP 三次握手,这没问题;
</li>
<li>
但第四个包却是在 3 秒以后了并且还是客户端VM2发送过来的 FIN 包,也就说明,客户端的连接关闭了。
</li>
我想,根据 curl 设置的 3 秒超时选项,你应该能猜到,这是因为 curl 命令超时后退出了。
我把这一过程,用 TCP 交互的流程图(实际上来自 Wireshark 的 Flow Graph来表示你可以更清楚地看到上面这个问题
<img src="https://static001.geekbang.org/resource/image/a8/c2/a81bd7639a1f81c23bc6d2e030af97c2.png" alt=""><br>
这里比较奇怪的是,我们并没有抓取到 curl 发来的 HTTP GET 请求。那么,究竟是网卡丢包了,还是客户端压根儿就没发过来呢?
我们可以重新执行 netstat -i 命令,确认一下网卡有没有丢包问题:
```
root@nginx:/# netstat -i
Kernel Interface table
Iface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0 100 157 0 344 0 94 0 0 0 BMRU
lo 65536 0 0 0 0 0 0 0 0 LRU
```
从 netstat 的输出中你可以看到接收丢包数RX-DRP是 344果然是在网卡接收时丢包了。不过问题也来了为什么刚才用 hping3 时不丢包,现在换成 GET 就收不到了呢?
还是那句话,遇到搞不懂的现象,不妨先去查查工具和方法的原理。我们可以对比一下这两个工具:
<li>
hping3 实际上只发送了 SYN 包;
</li>
<li>
而 curl 在发送 SYN 包后,还会发送 HTTP GET 请求。
</li>
HTTP GET ,本质上也是一个 TCP 包,但跟 SYN 包相比,它还携带了 HTTP GET 的数据。
那么,通过这个对比,你应该想到了,这可能是 MTU 配置错误导致的。为什么呢?
其实,仔细观察上面 netstat 的输出界面,第二列正是每个网卡的 MTU 值。eth0 的 MTU 只有 100而以太网的 MTU 默认值是 1500这个100 就显得太小了。
当然MTU 问题是很好解决的,把它改成 1500 就可以了。我们继续在容器终端中,执行下面的命令,把容器 eth0 的 MTU 改成 1500
```
root@nginx:/# ifconfig eth0 mtu 1500
```
修改完成后,再切换到终端二中,再次执行 curl 命令,确认问题是否真的解决了:
```
$ curl --max-time 3 http://192.168.0.30/
&lt;!DOCTYPE html&gt;
&lt;html&gt;
...
&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
```
非常不容易呀,这次终于看到了熟悉的 Nginx 响应,说明丢包的问题终于彻底解决了。
当然,案例结束前,不要忘记停止今天的 Nginx 应用。你可以切换回终端一,在容器终端中执行 exit 命令,退出容器终端:
```
root@nginx:/# exit
exit
```
最后,再执行下面的 docker 命令,停止并删除 Nginx 容器:
```
$ docker rm -f nginx
```
## 小结
今天,我继续带你分析了网络丢包的问题。特别是在时不时丢包的情况下,定位和优化都需要我们花心思重点投入。
网络丢包问题的严重性不言而喻。碰到丢包问题时,我们还是要从 Linux 网络收发的流程入手,结合 TCP/IP 协议栈的原理来逐层分析。
## 思考
最后,我想邀请你一起来聊聊,你碰到过的网络丢包问题。你是怎么分析它们的根源?又是怎么解决的?你可以结合我的讲解,总结自己的思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,354 @@
<audio id="audio" title="49 | 案例篇:内核线程 CPU 利用率太高,我该怎么办?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fc/37/fc09ab6d80b0d233f2adb2f7a88ecd37.mp3"></audio>
你好,我是倪朋飞。
上一期,我们一起梳理了,网络时不时丢包的分析定位和优化方法。先简单回顾一下。
网络丢包,通常会带来严重的性能下降,特别是对 TCP 来说,丢包通常意味着网络拥塞和重传,进而会导致网络延迟增大以及吞吐量降低。
而分析丢包问题,还是用我们的老套路,从 Linux 网络收发的流程入手,结合 TCP/IP 协议栈的原理来逐层分析。
其实,在排查网络问题时,我们还经常碰到的一个问题,就是内核线程的 CPU 使用率很高。比如,在高并发的场景中,内核线程 ksoftirqd 的 CPU 使用率通常就会比较高。回顾一下前面学过的 CPU 和网络模块,你应该知道,这是网络收发的软中断导致的。
而要分析 ksoftirqd 这类 CPU 使用率比较高的内核线程,如果用我前面介绍过的那些分析方法,你一般需要借助于其他性能工具,进行辅助分析。
比如,还是以 ksoftirqd 为例,如果你怀疑是网络问题,就可以用 sar、tcpdump 等分析网络流量,进一步确认网络问题的根源。
不过,显然,这种方法在实际操作中需要步骤比较多,可能并不算快捷。你肯定也很想知道,有没有其他更简单的方法,可以直接观察内核线程的行为,更快定位瓶颈呢?
今天,我就继续以 ksoftirqd 为例,带你一起看看,如何分析内核线程的性能问题。
## 内核线程
既然要讲内核线程的性能问题,在案例开始之前,我们就先来看看,有哪些常见的内核线程。
我们知道,在 Linux 中,用户态进程的“祖先”,都是 PID 号为 1 的 init 进程。比如,现在主流的 Linux 发行版中init 都是 systemd 进程;而其他的用户态进程,会通过 systemd 来进行管理。
稍微想一下 Linux 中的各种进程,除了用户态进程外,还有大量的内核态线程。按说内核态的线程,应该先于用户态进程启动,可是 systemd 只管理用户态进程。那么,内核态线程又是谁来管理的呢?
实际上Linux 在启动过程中,有三个特殊的进程,也就是 PID 号最小的三个进程。
<li>
0 号进程为 idle 进程,这也是系统创建的第一个进程,它在初始化 1 号和 2 号进程后,演变为空闲任务。当 CPU 上没有其他任务执行时,就会运行它。
</li>
<li>
1 号进程为 init 进程,通常是 systemd 进程,在用户态运行,用来管理其他用户态进程。
</li>
<li>
2 号进程为 kthreadd 进程,在内核态运行,用来管理内核线程。
</li>
所以,要查找内核线程,我们只需要从 2 号进程开始,查找它的子孙进程即可。比如,你可以使用 ps 命令,来查找 kthreadd 的子进程:
```
$ ps -f --ppid 2 -p 2
UID PID PPID C STIME TTY TIME CMD
root 2 0 0 12:02 ? 00:00:01 [kthreadd]
root 9 2 0 12:02 ? 00:00:21 [ksoftirqd/0]
root 10 2 0 12:02 ? 00:11:47 [rcu_sched]
root 11 2 0 12:02 ? 00:00:18 [migration/0]
...
root 11094 2 0 14:20 ? 00:00:00 [kworker/1:0-eve]
root 11647 2 0 14:27 ? 00:00:00 [kworker/0:2-cgr]
```
从上面的输出你能够看到内核线程的名称CMD都在中括号里这一点我们前面内容也有提到过。所以更简单的方法就是直接查找名称包含中括号的进程。比如
```
$ ps -ef | grep &quot;\[.*\]&quot;
root 2 0 0 08:14 ? 00:00:00 [kthreadd]
root 3 2 0 08:14 ? 00:00:00 [rcu_gp]
root 4 2 0 08:14 ? 00:00:00 [rcu_par_gp]
...
```
了解内核线程的基本功能,对我们排查问题有非常大的帮助。比如,我们曾经在软中断案例中提到过 ksoftirqd。它是一个用来处理软中断的内核线程并且每个 CPU 上都有一个。
如果你知道了这一点,那么,以后遇到 ksoftirqd 的 CPU 使用高的情况,就会首先怀疑是软中断的问题,然后从软中断的角度来进一步分析。
其实,除了刚才看到的 kthreadd 和 ksoftirqd 外,还有很多常见的内核线程,我们在性能分析中都经常会碰到,比如下面这几个内核线程。
<li>
**kswapd0**:用于内存回收。在 [Swap变高](https://time.geekbang.org/column/article/75797) 案例中,我曾介绍过它的工作原理。
</li>
<li>
**kworker**:用于执行内核工作队列,分为绑定 CPU (名称格式为 kworker/CPU86330和未绑定 CPU名称格式为 kworker/uPOOL86330两类。
</li>
<li>
**migration**:在负载均衡过程中,把进程迁移到 CPU 上。每个 CPU 都有一个 migration 内核线程。
</li>
<li>
**jbd2**/sda1-8jbd 是 Journaling Block Device 的缩写,用来为文件系统提供日志功能,以保证数据的完整性;名称中的 sda1-8表示磁盘分区名称和设备号。每个使用了 ext4 文件系统的磁盘分区,都会有一个 jbd2 内核线程。
</li>
<li>
**pdflush**:用于将内存中的脏页(被修改过,但还未写入磁盘的文件页)写入磁盘(已经在 3.10 中合并入了 kworker 中)。
</li>
了解这几个容易发生性能问题的内核线程,有助于我们更快地定位性能瓶颈。接下来,我们来看今天的案例。
## 案例准备
今天的案例还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
<li>
机器配置2 CPU8GB 内存。
</li>
<li>
预先安装 docker、perf、hping3、curl 等工具,如 apt install docker.io linux-tools-common hping3。
</li>
本次案例用到两台虚拟机,我画了一张图来表示它们的关系。
<img src="https://static001.geekbang.org/resource/image/7d/11/7dd0763a14713940e7c762a62387dd11.png" alt="">
你需要打开两个终端,分别登录这两台虚拟机中,并安装上述工具。
注意,以下所有命令都默认以 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/
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
...
```
接着,还是在第二个终端中,运行 hping3 命令,模拟 Nginx 的客户端请求:
```
# -S参数表示设置TCP协议的SYN同步序列号-p表示目的端口为80
# -i u10表示每隔10微秒发送一个网络帧
# 注如果你在实践过程中现象不明显可以尝试把10调小比如调成5甚至1
$ hping3 -S -p 80 -i u10 192.168.0.30
```
现在,我们再回到第一个终端,你应该就会发现异常——系统的响应明显变慢了。我们不妨执行 top观察一下系统和进程的 CPU 使用情况:
```
$ top
top - 08:31:43 up 17 min, 1 user, load average: 0.00, 0.00, 0.02
Tasks: 128 total, 1 running, 69 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.3 us, 0.3 sy, 0.0 ni, 66.8 id, 0.3 wa, 0.0 hi, 32.4 si, 0.0 st
%Cpu1 : 0.0 us, 0.3 sy, 0.0 ni, 65.2 id, 0.0 wa, 0.0 hi, 34.5 si, 0.0 st
KiB Mem : 8167040 total, 7234236 free, 358976 used, 573828 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 7560460 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
9 root 20 0 0 0 0 S 7.0 0.0 0:00.48 ksoftirqd/0
18 root 20 0 0 0 0 S 6.9 0.0 0:00.56 ksoftirqd/1
2489 root 20 0 876896 38408 21520 S 0.3 0.5 0:01.50 docker-containe
3008 root 20 0 44536 3936 3304 R 0.3 0.0 0:00.09 top
1 root 20 0 78116 9000 6432 S 0.0 0.1 0:11.77 systemd
...
```
从 top 的输出中,你可以看到,两个 CPU 的软中断使用率都超过了 30%;而 CPU 使用率最高的进程,正好是软中断内核线程 ksoftirqd/0 和 ksoftirqd/1。
虽然,我们已经知道了 ksoftirqd 的基本功能,可以猜测是因为大量网络收发,引起了 CPU 使用率升高;但它到底在执行什么逻辑,我们却并不知道。
对于普通进程,我们要观察其行为有很多方法,比如 strace、pstack、lsof 等等。但这些工具并不适合内核线程,比如,如果你用 pstack ,或者通过 /proc/pid/stack 查看 ksoftirqd/0进程号为 9的调用栈时分别可以得到以下输出
```
$ pstack 9
Could not attach to target 9: Operation not permitted.
detach: No such process
```
```
$ cat /proc/9/stack
[&lt;0&gt;] smpboot_thread_fn+0x166/0x170
[&lt;0&gt;] kthread+0x121/0x140
[&lt;0&gt;] ret_from_fork+0x35/0x40
[&lt;0&gt;] 0xffffffffffffffff
```
显然pstack 报出的是不允许挂载进程的错误;而 /proc/9/stack 方式虽然有输出,但输出中并没有详细的调用栈情况。
那还有没有其他方法,来观察内核线程 ksoftirqd 的行为呢?
既然是内核线程,自然应该用到内核中提供的机制。回顾一下我们之前用过的 CPU 性能工具,我想你肯定还记得 perf ,这个内核自带的性能剖析工具。
perf 可以对指定的进程或者事件进行采样,并且还可以用调用栈的形式,输出整个调用链上的汇总信息。 我们不妨就用 perf ,来试着分析一下进程号为 9 的 ksoftirqd。
继续在终端一中,执行下面的 perf record 命令;并指定进程号 9 ,以便记录 ksoftirqd 的行为:
```
# 采样30s后退出
$ perf record -a -g -p 9 -- sleep 30
```
稍等一会儿,在上述命令结束后,继续执行 `perf report`命令,你就可以得到 perf 的汇总报告。按上下方向键以及回车键,展开比例最高的 ksoftirqd 后,你就可以得到下面这个调用关系链图:
<img src="https://static001.geekbang.org/resource/image/73/01/73f5e9a9e510b9f3bf634c5e94e67801.png" alt="">
从这个图中,你可以清楚看到 ksoftirqd 执行最多的调用过程。虽然你可能不太熟悉内核源码,但通过这些函数,我们可以大致看出它的调用栈过程。
<li>
net_rx_action 和 netif_receive_skb表明这是接收网络包rx 表示 receive
</li>
<li>
br_handle_frame 表明网络包经过了网桥br 表示 bridge
</li>
<li>
br_nf_pre_routing ,表明在网桥上执行了 netfilter 的 PREROUTINGnf 表示 netfilter。而我们已经知道 PREROUTING 主要用来执行 DNAT所以可以猜测这里有 DNAT 发生。
</li>
<li>
br_pass_frame_up表明网桥处理后再交给桥接的其他桥接网卡进一步处理。比如在新的网卡上接收网络包、执行 netfilter 过滤规则等等。
</li>
我们的猜测对不对呢?实际上,我们案例最开始用 Docker 启动了容器,而 Docker 会自动为容器创建虚拟网卡、桥接到 docker0 网桥并配置 NAT 规则。这一过程,如下图所示:
<img src="https://static001.geekbang.org/resource/image/72/70/72f2f1fa7a464e4465108d4eadcc1b70.png" alt="">
当然了,前面 perf report 界面的调用链还可以继续展开。但很不幸,我的屏幕不够大,如果展开更多的层级,最后几个层级会超出屏幕范围。这样,即使我们能看到大部分的调用过程,却也不能说明后面层级就没问题。
那么,有没有更好的方法,来查看整个调用栈的信息呢?
## 火焰图
针对 perf 汇总数据的展示问题Brendan Gragg 发明了[火焰图](http://www.brendangregg.com/flamegraphs.html),通过矢量图的形式,更直观展示汇总结果。下图就是一个针对 mysql 的火焰图示例。
<img src="https://static001.geekbang.org/resource/image/68/61/68b80d299b23b0cee518001f78960f61.png" alt="">
(图片来自 Brendan Gregg [博客](http://www.brendangregg.com/flamegraphs.html)
这张图看起来像是跳动的火焰,因此也就被称为火焰图。要理解火焰图,我们最重要的是区分清楚横轴和纵轴的含义。
<li>
**横轴表示采样数和采样比例**。一个函数占用的横轴越宽,就代表它的执行时间越长。同一层的多个函数,则是按照字母来排序。
</li>
<li>
**纵轴表示调用栈**,由下往上根据调用关系逐个展开。换句话说,上下相邻的两个函数中,下面的函数,是上面函数的父函数。这样,调用栈越深,纵轴就越高。
</li>
另外,要注意图中的颜色,并没有特殊含义,只是用来区分不同的函数。
火焰图是动态的矢量图格式,所以它还支持一些动态特性。比如,鼠标悬停到某个函数上时,就会自动显示这个函数的采样数和采样比例。而当你用鼠标点击函数时,火焰图就会把该层及其上的各层放大,方便你观察这些处于火焰图顶部的调用栈的细节。
上面 mysql 火焰图的示例,就表示了 CPU 的繁忙情况,这种火焰图也被称为 on-CPU 火焰图。如果我们根据性能分析的目标来划分,火焰图可以分为下面这几种。
<li>
**on-CPU 火焰图**:表示 CPU 的繁忙情况,用在 CPU 使用率比较高的场景中。
</li>
<li>
**off-CPU 火焰图**:表示 CPU 等待 I/O、锁等各种资源的阻塞情况。
</li>
<li>
**内存火焰图**:表示内存的分配和释放情况。
</li>
<li>
**热/冷火焰图**:表示将 on-CPU 和 off-CPU 结合在一起综合展示。
</li>
<li>
**差分火焰图**:表示两个火焰图的差分情况,红色表示增长,蓝色表示衰减。差分火焰图常用来比较不同场景和不同时期的火焰图,以便分析系统变化前后对性能的影响情况。
</li>
了解了火焰图的含义和查看方法后,接下来,我们再回到案例,运用火焰图来观察刚才 perf record 得到的记录。
## 火焰图分析
首先,我们需要生成火焰图。我们先下载几个能从 perf record 记录生成火焰图的工具,这些工具都放在 [https://github.com/brendangregg/FlameGraph](https://github.com/brendangregg/FlameGraph) 上面。你可以执行下面的命令来下载:
```
$ git clone https://github.com/brendangregg/FlameGraph
$ cd FlameGraph
```
安装好工具后,要生成火焰图,其实主要需要三个步骤:
<li>
执行 perf script ,将 perf record 的记录转换成可读的采样记录;
</li>
<li>
执行 stackcollapse-perf.pl 脚本,合并调用栈信息;
</li>
<li>
执行 flamegraph.pl 脚本,生成火焰图。
</li>
不过,在 Linux 中,我们可以使用管道,来简化这三个步骤的执行过程。假设刚才用 perf record 生成的文件路径为 /root/perf.data执行下面的命令你就可以直接生成火焰图
```
$ perf script -i /root/perf.data | ./stackcollapse-perf.pl --all | ./flamegraph.pl &gt; ksoftirqd.svg
```
执行成功后,使用浏览器打开 ksoftirqd.svg ,你就可以看到生成的火焰图了。如下图所示:
<img src="https://static001.geekbang.org/resource/image/6d/cd/6d4f1fece12407906aacedf5078e53cd.png" alt="">
根据刚刚讲过的火焰图原理,这个图应该从下往上看,沿着调用栈中最宽的函数来分析执行次数最多的函数。这儿看到的结果,其实跟刚才的 perf report 类似,但直观了很多,中间这一团火,很明显就是最需要我们关注的地方。
我们顺着调用栈由下往上看(顺着图中蓝色箭头),就可以得到跟刚才 perf report 中一样的结果:
<li>
最开始,还是 net_rx_action 到 netif_receive_skb 处理网络收包;
</li>
<li>
然后, br_handle_frame 到 br_nf_pre_routing ,在网桥中接收并执行 netfilter 钩子函数;
</li>
<li>
再向上, br_pass_frame_up 到 netif_receive_skb ,从网桥转到其他网络设备又一次接收。
</li>
不过最后,到了 ip_forward 这里,已经看不清函数名称了。所以我们需要点击 ip_forward展开最上面这一块调用栈
<img src="https://static001.geekbang.org/resource/image/41/a3/416291ba2f9c039a0507f913572a21a3.png" alt="">
这样,就可以进一步看到 ip_forward 后的行为,也就是把网络包发送出去。根据这个调用过程,再结合我们前面学习的网络收发和 TCP/IP 协议栈原理,这个流程中的网络接收、网桥以及 netfilter 调用等,都是导致软中断 CPU 升高的重要因素,也就是影响网络性能的潜在瓶颈。
不过,回想一下网络收发的流程,你可能会觉得它缺了好多步骤。
比如,这个堆栈中并没有 TCP 相关的调用,也没有连接跟踪 conntrack 相关的函数。实际上这些流程都在其他更小的火焰中你可以点击上图左上角的“Reset Zoom”回到完整火焰图中再去查看其他小火焰的堆栈。
所以,在理解这个调用栈时要注意。从任何一个点出发、纵向来看的整个调用栈,其实只是最顶端那一个函数的调用堆栈,而非完整的内核网络执行流程。
另外,整个火焰图不包含任何时间的因素,所以并不能看出横向各个函数的执行次序。
到这里,我们就找出了内核线程 ksoftirqd 执行最频繁的函数调用堆栈,而这个堆栈中的各层级函数,就是潜在的性能瓶颈来源。这样,后面想要进一步分析、优化时,也就有了根据。
## 小结
今天这个案例,你可能会觉得比较熟悉。实际上,这个案例,正是我们专栏 CPU 模块中的 [软中断案例](https://time.geekbang.org/column/article/72147)。
当时,我们从软中断 CPU 使用率的角度入手,用网络抓包的方法找出了瓶颈来源,确认是测试机器发送的大量 SYN 包导致的。而通过今天的 perf 和火焰图方法,我们进一步找出了软中断内核线程的热点函数,其实也就找出了潜在的瓶颈和优化方向。
其实,如果遇到的是内核线程的资源使用异常,很多常用的进程级性能工具并不能帮上忙。这时,你就可以用内核自带的 perf 来观察它们的行为找出热点函数进一步定位性能瓶。当然perf 产生的汇总报告并不够直观,所以我也推荐你用火焰图来协助排查。
实际上,火焰图方法同样适用于普通进程。比如,在分析 Nginx、MySQL 等各种应用场景的性能问题时,火焰图也能帮你更快定位热点函数,找出潜在性能问题。
## 思考
最后,我想邀请你一起来聊聊,你碰到过的内核线程性能问题。你是怎么分析它们的根源?又是怎么解决的?你可以结合我的讲述,总结自己的思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,283 @@
<audio id="audio" title="50 | 案例篇:动态追踪怎么用?(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c3/62/c331ddcb21e3bee79a4b0975e7984262.mp3"></audio>
你好,我是倪朋飞。
上一节,我以 ksoftirqd CPU 使用率高的问题为例,带你一起学习了内核线程 CPU 使用率高时的分析方法。先简单回顾一下。
当碰到内核线程的资源使用异常时,很多常用的进程级性能工具,并不能直接用到内核线程上。这时,我们就可以使用内核自带的 perf 来观察它们的行为找出热点函数进一步定位性能瓶颈。不过perf 产生的汇总报告并不直观,所以我通常也推荐用火焰图来协助排查。
其实,使用 perf 对系统内核线程进行分析时,内核线程依然还在正常运行中,所以这种方法也被称为动态追踪技术。
**动态追踪技术,通过探针机制,来采集内核或者应用程序的运行信息,从而可以不用修改内核和应用程序的代码,就获得丰富的信息,帮你分析、定位想要排查的问题。**
以往,在排查和调试性能问题时,我们往往需要先为应用程序设置一系列的断点(比如使用 GDB然后以手动或者脚本比如 GDB 的 Python 扩展)的方式,在这些断点处分析应用程序的状态。或者,增加一系列的日志,从日志中寻找线索。
不过,断点往往会中断应用的正常运行;而增加新的日志,往往需要重新编译和部署。这些方法虽然在今天依然广泛使用,但在排查复杂的性能问题时,往往耗时耗力,更会对应用的正常运行造成巨大影响。
此外,这类方式还有大量的性能问题。比如,出现的概率小,只有线上环境才能碰到。这种难以复现的问题,亦是一个巨大挑战。
而动态追踪技术的出现,就为这些问题提供了完美的方案:它既不需要停止服务,也不需要修改应用程序的代码;所有一切还按照原来的方式正常运行时,就可以帮你分析出问题的根源。
同时,相比以往的进程级跟踪方法(比如 ptrace动态追踪往往只会带来很小的性能损耗通常在 5% 或者更少)。
既然动态追踪有这么多好处,那么,都有哪些动态追踪的方法,又该如何使用这些动态追踪方法呢?今天,我就带你一起来看看这个问题。由于动态追踪涉及的知识比较多,我将分为上、下两篇为你讲解,先来看今天这部分内容。
## 动态追踪
说到动态追踪Dynamic Tracing就不得不提源于 Solaris 系统的 DTrace。DTrace 是动态追踪技术的鼻祖,它提供了一个通用的观测框架,并可以使用 D 语言进行自由扩展。
DTrace 的工作原理如下图所示。**它的运行常驻在内核中,用户可以通过 dtrace 命令把D 语言编写的追踪脚本,提交到内核中的运行时来执行**。DTrace 可以跟踪用户态和内核态的所有事件,并通过一些列的优化措施,保证最小的性能开销。
<img src="https://static001.geekbang.org/resource/image/61/a6/6144b1947373bd5668010502bd0e45a6.png" alt="">
(图片来自 [BSDCan](https://www.bsdcan.org/2017/schedule/attachments/433_dtrace_internals.html#(24))
虽然直到今天DTrace 本身依然无法在 Linux 中运行,但它同样对 Linux 动态追踪产生了巨大的影响。很多工程师都尝试过把 DTrace 移植到 Linux 中,这其中,最著名的就是 RedHat 主推的 SystemTap。
同 DTrace 一样SystemTap 也定义了一种类似的脚本语言,方便用户根据需要自由扩展。不过,不同于 DTraceSystemTap 并没有常驻内核的运行时,它需要先把脚本编译为内核模块,然后再插入到内核中执行。这也导致 SystemTap 启动比较缓慢,并且依赖于完整的调试符号表。
<img src="https://static001.geekbang.org/resource/image/e0/db/e09aa4a00aee93f27f0d666a2bb1c4db.png" alt="">
(图片来自[动态追踪技术漫谈](https://openresty.org/posts/dynamic-tracing/)
总的来说为了追踪内核或用户空间的事件Dtrace 和 SystemTap 都会把用户传入的追踪处理函数(一般称为 Action关联到被称为探针的检测点上。这些探针实际上也就是各种动态追踪技术所依赖的事件源。
### 动态追踪的事件源
根据事件类型的不同,**动态追踪所使用的事件源,可以分为静态探针、动态探针以及硬件事件等三类**。它们的关系如下图所示:
<img src="https://static001.geekbang.org/resource/image/ba/61/ba6c9ed0dcccc7f4f46bb19c69946e61.png" alt="">
(图片来自 [Brendan Gregg Blog](http://www.brendangregg.com/perf.html#Events)
其中,**硬件事件通常由性能监控计数器 PMCPerformance Monitoring Counter产生**,包括了各种硬件的性能情况,比如 CPU 的缓存、指令周期、分支预测等等。
**静态探针,是指事先在代码中定义好,并编译到应用程序或者内核中的探针**。这些探针只有在开启探测功能时才会被执行到未开启时并不会执行。常见的静态探针包括内核中的跟踪点tracepoints和 USDTUserland Statically Defined Tracing探针。
<li>
跟踪点tracepoints实际上就是在源码中插入的一些带有控制条件的探测点这些探测点允许事后再添加处理函数。比如在内核中最常见的静态跟踪方法就是 printk即输出日志。Linux 内核定义了大量的跟踪点,可以通过内核编译选项,来开启或者关闭。
</li>
<li>
USDT 探针,全称是用户级静态定义跟踪,需要在源码中插入 DTRACE_PROBE() 代码,并编译到应用程序中。不过,也有很多应用程序内置了 USDT 探针,比如 MySQL、PostgreSQL 等。
</li>
**动态探针,则是指没有事先在代码中定义,但却可以在运行时动态添加的探针**,比如函数的调用和返回等。动态探针支持按需在内核或者应用程序中添加探测点,具有更高的灵活性。常见的动态探针有两种,即用于内核态的 kprobes 和用于用户态的 uprobes。
<li>
kprobes 用来跟踪内核态的函数,包括用于函数调用的 kprobe 和用于函数返回的 kretprobe。
</li>
<li>
uprobes 用来跟踪用户态的函数,包括用于函数调用的 uprobe 和用于函数返回的 uretprobe。
</li>
>
注意kprobes 需要内核编译时开启 CONFIG_KPROBE_EVENTS而 uprobes 则需要内核编译时开启 CONFIG_UPROBE_EVENTS。
### 动态追踪机制
而在这些探针的基础上Linux 也提供了一系列的动态追踪机制,比如 ftrace、perf、eBPF 等。
**ftrace** 最早用于函数跟踪后来又扩展支持了各种事件跟踪功能。ftrace 的使用接口跟我们之前提到的 procfs 类似,它通过 debugfs4.1 以后也支持 tracefs以普通文件的形式向用户空间提供访问接口。
这样,不需要额外的工具,你就可以通过挂载点(通常为 /sys/kernel/debug/tracing 目录)内的文件读写,来跟 ftrace 交互,跟踪内核或者应用程序的运行事件。
**perf** 是我们的老朋友了,我们在前面的好多案例中,都使用了它的事件记录和分析功能,这实际上只是一种最简单的静态跟踪机制。你也可以通过 perf 来自定义动态事件perf probe只关注真正感兴趣的事件。
**eBPF** 则在 BPFBerkeley Packet Filter的基础上扩展而来不仅支持事件跟踪机制还可以通过自定义的 BPF 代码(使用 C 语言来自由扩展。所以eBPF 实际上就是常驻于内核的运行时,可以说就是 Linux 版的 DTrace。
除此之外,还有很多内核外的工具,也提供了丰富的动态追踪功能。最常见的就是前面提到的 **SystemTap**,我们之前多次使用过的 **BCC**BPF Compiler Collection以及常用于容器性能分析的 **sysdig** 等。
而在分析大量事件时,使用我们上节课提到的火焰图,可以将大量数据可视化展示,让你更直观发现潜在的问题。
接下来,我就通过几个例子,带你来看看,要怎么使用这些机制,来动态追踪内核和应用程序的执行情况。以下案例还是基于 Ubuntu 18.04 系统,同样适用于其他系统。
>
注意,以下所有命令都默认以 root 用户运行,如果你用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
## ftrace
我们先来看 ftrace。刚刚提到过ftrace 通过 debugfs或者 tracefs为用户空间提供接口。所以使用 ftrace往往是从切换到 debugfs 的挂载点开始。
```
$ cd /sys/kernel/debug/tracing
$ ls
README instances set_ftrace_notrace trace_marker_raw
available_events kprobe_events set_ftrace_pid trace_options
...
```
如果这个目录不存在,则说明你的系统还没有挂载 debugfs你可以执行下面的命令来挂载它
```
$ mount -t debugfs nodev /sys/kernel/debug
```
ftrace 提供了多个跟踪器,用于跟踪不同类型的信息,比如函数调用、中断关闭、进程调度等。具体支持的跟踪器取决于系统配置,你可以执行下面的命令,来查询所有支持的跟踪器:
```
$ cat available_tracers
hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop
```
这其中function 表示跟踪函数的执行function_graph 则是跟踪函数的调用关系,也就是生成直观的调用关系图。这便是最常用的两种跟踪器。
除了跟踪器外,使用 ftrace 前,还需要确认跟踪目标,包括内核函数和内核事件。其中,
<li>
函数就是内核中的函数名。
</li>
<li>
而事件,则是内核源码中预先定义的跟踪点。
</li>
同样地,你可以执行下面的命令,来查询支持的函数和事件:
```
$ cat available_filter_functions
$ cat available_events
```
明白了这些基本信息,接下来,我就以 ls 命令为例,带你一起看看 ftrace 的使用方法。
为了列出文件ls 命令会通过 open 系统调用打开目录文件,而 open 在内核中对应的函数名为 do_sys_open。 所以,我们要做的第一步,就是把要跟踪的函数设置为 do_sys_open
```
$ echo do_sys_open &gt; set_graph_function
```
接下来,第二步,配置跟踪选项,开启函数调用跟踪,并跟踪调用进程:
```
$ echo function_graph &gt; current_tracer
$ echo funcgraph-proc &gt; trace_options
```
接着,第三步,也就是开启跟踪:
```
$ echo 1 &gt; tracing_on
```
第四步,执行一个 ls 命令后,再关闭跟踪:
```
$ ls
$ echo 0 &gt; tracing_on
```
第五步,也是最后一步,查看跟踪结果:
```
$ cat trace
# tracer: function_graph
#
# CPU TASK/PID DURATION FUNCTION CALLS
# | | | | | | | | |
0) ls-12276 | | do_sys_open() {
0) ls-12276 | | getname() {
0) ls-12276 | | getname_flags() {
0) ls-12276 | | kmem_cache_alloc() {
0) ls-12276 | | _cond_resched() {
0) ls-12276 | 0.049 us | rcu_all_qs();
0) ls-12276 | 0.791 us | }
0) ls-12276 | 0.041 us | should_failslab();
0) ls-12276 | 0.040 us | prefetch_freepointer();
0) ls-12276 | 0.039 us | memcg_kmem_put_cache();
0) ls-12276 | 2.895 us | }
0) ls-12276 | | __check_object_size() {
0) ls-12276 | 0.067 us | __virt_addr_valid();
0) ls-12276 | 0.044 us | __check_heap_object();
0) ls-12276 | 0.039 us | check_stack_object();
0) ls-12276 | 1.570 us | }
0) ls-12276 | 5.790 us | }
0) ls-12276 | 6.325 us | }
...
```
在最后得到的输出中:
<li>
第一列表示运行的 CPU
</li>
<li>
第二列是任务名称和进程 PID
</li>
<li>
第三列是函数执行延迟;
</li>
<li>
最后一列,则是函数调用关系图。
</li>
你可以看到,函数调用图,通过不同级别的缩进,直观展示了各函数间的调用关系。
当然,我想你应该也发现了 ftrace 的使用缺点——五个步骤实在是麻烦,用起来并不方便。不过,不用担心, [trace-cmd](https://git.kernel.org/pub/scm/utils/trace-cmd/trace-cmd.git/) 已经帮你把这些步骤给包装了起来。这样,你就可以在同一个命令行工具里,完成上述所有过程。
你可以执行下面的命令,来安装 trace-cmd
```
# Ubuntu
$ apt-get install trace-cmd
# CentOS
$ yum install trace-cmd
```
安装好后,原本的五步跟踪过程,就可以简化为下面这两步:
```
$ trace-cmd record -p function_graph -g do_sys_open -O funcgraph-proc ls
$ trace-cmd report
...
ls-12418 [000] 85558.075341: funcgraph_entry: | do_sys_open() {
ls-12418 [000] 85558.075363: funcgraph_entry: | getname() {
ls-12418 [000] 85558.075364: funcgraph_entry: | getname_flags() {
ls-12418 [000] 85558.075364: funcgraph_entry: | kmem_cache_alloc() {
ls-12418 [000] 85558.075365: funcgraph_entry: | _cond_resched() {
ls-12418 [000] 85558.075365: funcgraph_entry: 0.074 us | rcu_all_qs();
ls-12418 [000] 85558.075366: funcgraph_exit: 1.143 us | }
ls-12418 [000] 85558.075366: funcgraph_entry: 0.064 us | should_failslab();
ls-12418 [000] 85558.075367: funcgraph_entry: 0.075 us | prefetch_freepointer();
ls-12418 [000] 85558.075368: funcgraph_entry: 0.085 us | memcg_kmem_put_cache();
ls-12418 [000] 85558.075369: funcgraph_exit: 4.447 us | }
ls-12418 [000] 85558.075369: funcgraph_entry: | __check_object_size() {
ls-12418 [000] 85558.075370: funcgraph_entry: 0.132 us | __virt_addr_valid();
ls-12418 [000] 85558.075370: funcgraph_entry: 0.093 us | __check_heap_object();
ls-12418 [000] 85558.075371: funcgraph_entry: 0.059 us | check_stack_object();
ls-12418 [000] 85558.075372: funcgraph_exit: 2.323 us | }
ls-12418 [000] 85558.075372: funcgraph_exit: 8.411 us | }
ls-12418 [000] 85558.075373: funcgraph_exit: 9.195 us | }
...
```
你会发现trace-cmd 的输出,跟上述 cat trace 的输出是类似的。
通过这个例子我们知道,当你想要了解某个内核函数的调用过程时,使用 ftrace ,就可以跟踪到它的执行过程。
## 小结
今天,我带你一起学习了常见的动态追踪方法。所谓动态追踪,就是在系统或应用程序正常运行时,通过内核中提供的探针来动态追踪它们的行为,从而辅助排查出性能瓶颈。
而在 Linux 系统中,常见的动态追踪方法包括 ftrace、perf、eBPF 以及 SystemTap 等。当你已经定位了某个内核函数,但不清楚它的实现原理时,就可以用 ftrace 来跟踪它的执行过程。至于其他动态追踪方法,我将在下节课继续为你详细解读。
## 思考
最后给你留一个思考题。今天的案例中我们使用Linux 内核提供的 ftrace 机制,来了解内核函数的执行过程;而上节课我们则用了 perf 和火焰图,来观察内核的调用堆栈。
根据这两个案例,你觉得这两种方法有什么不一样的地方?当需要了解内核的行为时,如何在二者中选择,或者说,这两种方法分别适用于什么样的场景呢?你可以结合今天的内容,和你自己的操作记录,来总结思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,430 @@
<audio id="audio" title="51 | 案例篇:动态追踪怎么用?(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a2/c3/a294305d88312740240a738c11dc4ac3.mp3"></audio>
你好,我是倪朋飞。
上一节,我带你一起学习了常见的动态追踪方法。所谓动态追踪,就是在系统或者应用程序正常运行的时候,通过内核中提供的探针,来动态追踪它们的行为,从而辅助排查出性能问题的瓶颈。
使用动态追踪,可以在不修改代码、不重启服务的情况下,动态了解应用程序或者内核的行为,这对排查线上问题、特别是不容易重现的问题尤其有效。
在 Linux 系统中,常见的动态追踪方法包括 ftrace、perf、eBPF 以及 SystemTap 等。上节课,我们具体学习了 ftrace 的使用方法。今天,我们再来一起看看其他几种方法。
## perf
perf 已经是我们的老朋友了。在前面的案例中,我们多次用到它,来查找应用程序或者内核中的热点函数,从而定位性能瓶颈。而在内核线程 CPU 高的案例中,我们还使用火焰图动态展示 perf 的事件记录,从而更直观地发现了问题。
不过,我们前面使用 perf record/top时都是先对事件进行采样然后再根据采样数评估各个函数的调用频率。实际上perf 的功能远不止于此。比如,
<li>
perf 可以用来分析 CPU cache、CPU 迁移、分支预测、指令周期等各种硬件事件;
</li>
<li>
perf 也可以只对感兴趣的事件进行动态追踪。
</li>
接下来,我们还是以内核函数 do_sys_open以及用户空间函数 readline 为例,看一看 perf 动态追踪的使用方法。
同 ftrace 一样,你也可以通过 perf list ,查询所有支持的事件:
```
$ perf list
```
然后,在 perf 的各个子命令中添加 --event 选项,设置追踪感兴趣的事件。如果这些预定义的事件不满足实际需要,你还可以使用 perf probe 来动态添加。而且除了追踪内核事件外perf 还可以用来跟踪用户空间的函数。
**我们先来看第一个 perf 示例,内核函数 do_sys_open 的例子**。你可以执行 perf probe 命令,添加 do_sys_open 探针:
```
$ perf probe --add do_sys_open
Added new event:
probe:do_sys_open (on do_sys_open)
You can now use it in all perf tools, such as:
perf record -e probe:do_sys_open -aR sleep 1
```
探针添加成功后,就可以在所有的 perf 子命令中使用。比如,上述输出就是一个 perf record 的示例,执行它就可以对 10s 内的 do_sys_open 进行采样:
```
$ perf record -e probe:do_sys_open -aR sleep 10
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.148 MB perf.data (19 samples) ]
```
而采样成功后,就可以执行 perf script ,来查看采样结果了:
```
$ perf script
perf 12886 [000] 89565.879875: probe:do_sys_open: (ffffffffa807b290)
sleep 12889 [000] 89565.880362: probe:do_sys_open: (ffffffffa807b290)
sleep 12889 [000] 89565.880382: probe:do_sys_open: (ffffffffa807b290)
sleep 12889 [000] 89565.880635: probe:do_sys_open: (ffffffffa807b290)
sleep 12889 [000] 89565.880669: probe:do_sys_open: (ffffffffa807b290)
```
输出中,同样也列出了调用 do_sys_open 的任务名称、进程 PID 以及运行的 CPU 等信息。不过,对于 open 系统调用来说,只知道它被调用了并不够,我们需要知道的是,进程到底在打开哪些文件。所以,实际应用中,我们还希望追踪时能显示这些函数的参数。
对于内核函数来说,你当然可以去查看内核源码,找出它的所有参数。不过还有更简单的方法,那就是直接从调试符号表中查询。执行下面的命令,你就可以知道 do_sys_open 的所有参数:
```
$ perf probe -V do_sys_open
Available variables at do_sys_open
@&lt;do_sys_open+0&gt;
char* filename
int dfd
int flags
struct open_flags op
umode_t mode
```
从这儿可以看出,我们关心的文件路径,就是第一个字符指针参数(也就是字符串),参数名称为 filename。如果这个命令执行失败就说明调试符号表还没有安装。那么你可以执行下面的命令安装调试信息后重试
```
# Ubuntu
$ apt-get install linux-image-`uname -r`-dbgsym
# CentOS
$ yum --enablerepo=base-debuginfo install -y kernel-debuginfo-$(uname -r)
```
找出参数名称和类型后,就可以把参数加到探针中了。不过由于我们已经添加过同名探针,所以在这次添加前,需要先把旧探针给删掉:
```
# 先删除旧的探针
perf probe --del probe:do_sys_open
# 添加带参数的探针
$ perf probe --add 'do_sys_open filename:string'
Added new event:
probe:do_sys_open (on do_sys_open with filename:string)
You can now use it in all perf tools, such as:
perf record -e probe:do_sys_open -aR sleep 1
```
新的探针添加后,重新执行 record 和 script 子命令,采样并查看记录:
```
# 重新采样记录
$ perf record -e probe:do_sys_open -aR ls
# 查看结果
$ perf script
perf 13593 [000] 91846.053622: probe:do_sys_open: (ffffffffa807b290) filename_string=&quot;/proc/13596/status&quot;
ls 13596 [000] 91846.053995: probe:do_sys_open: (ffffffffa807b290) filename_string=&quot;/etc/ld.so.cache&quot;
ls 13596 [000] 91846.054011: probe:do_sys_open: (ffffffffa807b290) filename_string=&quot;/lib/x86_64-linux-gnu/libselinux.so.1&quot;
ls 13596 [000] 91846.054066: probe:do_sys_open: (ffffffffa807b290) filename_string=&quot;/lib/x86_64-linux-gnu/libc.so.6”
...
# 使用完成后不要忘记删除探针
$ perf probe --del probe:do_sys_open
```
现在,你就可以看到每次调用 open 时打开的文件了。不过,这个结果是不是看着很熟悉呢?
其实,在我们使用 strace 跟踪进程的系统调用时,也经常会看到这些动态库的影子。比如,使用 strace 跟踪 ls 时,你可以得到下面的结果:
```
$ strace ls
...
access(&quot;/etc/ld.so.nohwcap&quot;, F_OK) = -1 ENOENT (No such file or directory)
access(&quot;/etc/ld.so.preload&quot;, R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, &quot;/etc/ld.so.cache&quot;, O_RDONLY|O_CLOEXEC) = 3
...
access(&quot;/etc/ld.so.nohwcap&quot;, F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, &quot;/lib/x86_64-linux-gnu/libselinux.so.1&quot;, O_RDONLY|O_CLOEXEC) = 3
...
```
你估计在想既然strace 也能得到类似结果本身又容易操作为什么我们还要用perf 呢?
实际上,很多人只看到了 strace 简单易用的好处,却忽略了它对进程性能带来的影响。从原理上来说,**strace 基于系统调用 ptrace 实现**,这就带来了两个问题。
<li>
由于ptrace 是系统调用,就需要在内核态和用户态切换。当事件数量比较多时,繁忙的切换必然会影响原有服务的性能;
</li>
<li>
ptrace 需要借助 SIGSTOP 信号挂起目标进程。这种信号控制和进程挂起,会影响目标进程的行为。
</li>
所以,在性能敏感的应用(比如数据库)中,我并不推荐你用 strace (或者其他基于 ptrace 的性能工具)去排查和调试。
在 strace 的启发下,结合内核中的 utrace 机制, perf 也提供了一个 trace 子命令,是取代 strace 的首选工具。相对于 ptrace 机制来说perf trace 基于内核事件,自然要比进程跟踪的性能好很多。
perf trace 的使用方法如下所示,跟 strace 其实很像:
```
$ perf trace ls
? ( ): ls/14234 ... [continued]: execve()) = 0
0.177 ( 0.013 ms): ls/14234 brk( ) = 0x555d96be7000
0.224 ( 0.014 ms): ls/14234 access(filename: 0xad98082 ) = -1 ENOENT No such file or directory
0.248 ( 0.009 ms): ls/14234 access(filename: 0xad9add0, mode: R ) = -1 ENOENT No such file or directory
0.267 ( 0.012 ms): ls/14234 openat(dfd: CWD, filename: 0xad98428, flags: CLOEXEC ) = 3
0.288 ( 0.009 ms): ls/14234 fstat(fd: 3&lt;/usr/lib/locale/C.UTF-8/LC_NAME&gt;, statbuf: 0x7ffd2015f230 ) = 0
0.305 ( 0.011 ms): ls/14234 mmap(len: 45560, prot: READ, flags: PRIVATE, fd: 3 ) = 0x7efe0af92000
0.324 Dockerfile test.sh
( 0.008 ms): ls/14234 close(fd: 3&lt;/usr/lib/locale/C.UTF-8/LC_NAME&gt; ) = 0
...
```
不过perf trace 还可以进行系统级的系统调用跟踪(即跟踪所有进程),而 strace 只能跟踪特定的进程。
**第二个 perf 的例子是用户空间的库函数**。以 bash 调用的库函数 readline 为例,使用类似的方法,可以跟踪库函数的调用(基于 uprobes
readline 的作用,是从终端中读取用户输入,并把这些数据返回调用方。所以,跟 open 系统调用不同的是,我们更关注 readline 的调用结果。
我们执行下面的命令,通过 -x 指定 bash 二进制文件的路径,就可以动态跟踪库函数。这其实就是跟踪了所有用户在 bash 中执行的命令:
```
# 为/bin/bash添加readline探针
$ perf probe -x /bin/bash 'readline%return +0($retval):string
# 采样记录
$ perf record -e probe_bash:readline__return -aR sleep 5
# 查看结果
$ perf script
bash 13348 [000] 93939.142576: probe_bash:readline__return: (5626ffac1610 &lt;- 5626ffa46739) arg1=&quot;ls&quot;
# 跟踪完成后删除探针
$ perf probe --del probe_bash:readline__return
```
当然,如果你不确定探针格式,也可以通过下面的命令,查询所有支持的函数和函数参数:
```
# 查询所有的函数
$ perf probe -x /bin/bash —funcs
# 查询函数的参数
$ perf probe -x /bin/bash -V readline
Available variables at readline
@&lt;readline+0&gt;
char* prompt
```
跟内核函数类似,如果你想要查看普通应用的函数名称和参数,那么在应用程序的二进制文件中,同样需要包含调试信息。
## eBPF 和 BCC
ftrace 和 perf 的功能已经比较丰富了,不过,它们有一个共同的缺陷,那就是不够灵活,没法像 DTrace 那样通过脚本自由扩展。
而 eBPF 就是 Linux 版的 DTrace可以通过C 语言自由扩展(这些扩展通过 LLVM 转换为 BPF 字节码后,加载到内核中执行)。下面这张图,就表示了 eBPF 追踪的工作原理:
<img src="https://static001.geekbang.org/resource/image/a3/e9/a3547f2ac1d4d75b850a02a2735560e9.png" alt="">
(图片来自 [THE NEW STACK](https://thenewstack.io/long-last-linux-gets-dynamic-tracing/)
从图中你可以看到eBPF 的执行需要三步:
<li>
从用户跟踪程序生成 BPF 字节码;
</li>
<li>
加载到内核中运行;
</li>
<li>
向用户空间输出结果。
</li>
所以从使用上来说eBPF 要比我们前面看到的 ftrace 和 perf ,都更加繁杂。
实际上,在 eBPF 执行过程中,编译、加载还有 maps 等操作,对所有的跟踪程序来说都是通用的。把这些过程通过 Python 抽象起来,也就诞生了 BCCBPF Compiler Collection
BCC 把 eBPF 中的各种事件源(比如 kprobe、uprobe、tracepoint 等)和数据操作(称为 Maps也都转换成了 Python 接口(也支持 lua。这样使用 BCC 进行动态追踪时,编写简单的脚本就可以了。
不过要注意,因为需要跟内核中的数据结构交互,真正核心的事件处理逻辑,还是需要我们用 C 语言来编写。
至于 BCC 的安装方法,在内存模块的[缓存案例](https://time.geekbang.org/column/article/0?cid=140)中,我就已经介绍过了。如果你还没有安装过,可以执行下面的命令来安装(其他系统的安装请参考[这里](https://github.com/iovisor/bcc/blob/master/INSTALL.md)
```
# Ubuntu
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
echo &quot;deb https://repo.iovisor.org/apt/$(lsb_release -cs) $(lsb_release -cs) main&quot; | sudo tee /etc/apt/sources.list.d/iovisor.list
sudo apt-get update
sudo apt-get install bcc-tools libbcc-examples linux-headers-$(uname -r)
# REHL 7.6
yum install bcc-tools
```
安装后BCC 会把所有示例(包括 Python 和 lua放到 /usr/share/bcc/examples 目录中:
```
$ ls /usr/share/bcc/examples
hello_world.py lua networking tracing
```
接下来,还是以 do_sys_open 为例,我们一起来看看,如何用 eBPF 和 BCC 实现同样的动态跟踪。
通常,我们可以把 BCC 应用,拆分为下面这四个步骤。
第一,跟所有的 Python 模块使用方法一样,在使用之前,先导入要用到的模块:
```
from bcc import BPF
```
第二,需要定义事件以及处理事件的函数。这个函数需要用 C 语言来编写,作用是初始化刚才导入的 BPF 对象。这些用 C 语言编写的处理函数,要以字符串的形式送到 BPF 模块中处理:
```
# define BPF program (&quot;&quot;&quot; is used for multi-line string).
# '#' indicates comments for python, while '//' indicates comments for C.
prog = &quot;&quot;&quot;
#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;uapi/linux/limits.h&gt;
#include &lt;linux/sched.h&gt;
// define output data structure in C
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
char fname[NAME_MAX];
};
BPF_PERF_OUTPUT(events);
// define the handler for do_sys_open.
// ctx is required, while other params depends on traced function.
int hello(struct pt_regs *ctx, int dfd, const char __user *filename, int flags){
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
if (bpf_get_current_comm(&amp;data.comm, sizeof(data.comm)) == 0) {
bpf_probe_read(&amp;data.fname, sizeof(data.fname), (void *)filename);
}
events.perf_submit(ctx, &amp;data, sizeof(data));
return 0;
}
&quot;&quot;&quot;
# load BPF program
b = BPF(text=prog)
# attach the kprobe for do_sys_open, and set handler to hello
b.attach_kprobe(event=&quot;do_sys_open&quot;, fn_name=&quot;hello&quot;)
```
第三步,是定义一个输出函数,并把输出函数跟 BPF 事件绑定:
```
# process event
start = 0
def print_event(cpu, data, size):
global start
# events type is data_t
event = b[&quot;events&quot;].event(data)
if start == 0:
start = event.ts
time_s = (float(event.ts - start)) / 1000000000
print(&quot;%-18.9f %-16s %-6d %-16s&quot; % (time_s, event.comm, event.pid, event.fname))
# loop with callback to print_event
b[&quot;events&quot;].open_perf_buffer(print_event)
```
最后一步,就是执行事件循环,开始追踪 do_sys_open 的调用:
```
# print header
print(&quot;%-18s %-16s %-6s %-16s&quot; % (&quot;TIME(s)&quot;, &quot;COMM&quot;, &quot;PID&quot;, &quot;FILE”))
# start the event polling loop
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
```
我们把上面几个步骤的代码,保存到文件 trace-open.py 中,然后就可以用 Python 来运行了。如果一切正常,你可以看到如下输出:
```
$ python trace-open.py
TIME(s) COMM PID FILE
0.000000000 irqbalance 1073 /proc/interrupts
0.000175401 irqbalance 1073 /proc/stat
0.000258802 irqbalance 1073 /proc/irq/9/smp_affinity
0.000290102 irqbalance 1073 /proc/irq/0/smp_affinity
```
从输出中,你可以看到 irqbalance 进程(你的环境中可能还会有其他进程)正在打开很多文件,而 irqbalance 依赖这些文件中读取的内容,来执行中断负载均衡。
通过这个简单的示例你也可以发现eBPF 和 BCC 的使用,其实比 ftrace 和 perf 有更高的门槛。想用 BCC 开发自己的动态跟踪程序,至少要熟悉 C 语言、Python 语言、被跟踪事件或函数的特征(比如内核函数的参数和返回格式)以及 eBPF 提供的各种数据操作方法。
不过,因为强大的灵活性,虽然 eBPF 在使用上有一定的门槛,却也无法阻止它成为目前最热门、最受关注的动态追踪技术。
当然BCC 软件包也内置了很多已经开发好的实用工具,默认安装到 /usr/share/bcc/tools/ 目录中,它们的使用场景如下图所示:
<img src="https://static001.geekbang.org/resource/image/fc/21/fc5f387a982db98c49c7cefb77342c21.png" alt="">
(图片来自 [Linux Extended BPF (eBPF) Tracing Tools](http://www.brendangregg.com/ebpf.html#bcc)
这些工具,一般都可以直接拿来用。而在编写其他的动态追踪脚本时,它们也是最好的参考资料。不过,有一点需要你特别注意,很多 eBPF 的新特性,都需要比较新的[内核版本](https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md)(如下图所示)。如果某些工具无法运行,很可能就是因为使用了当前内核不支持的特性。
<img src="https://static001.geekbang.org/resource/image/61/e8/61abce1affc770a15dae7d489e50a8e8.png" alt="">
(图片来自 [Linux Extended BPF (eBPF) Tracing Tools](http://www.brendangregg.com/ebpf.html#bcc)
## SystemTap 和 sysdig
除了前面提到的 ftrace、perf、eBPF 和 BCC 外SystemTap 和 sysdig 也是常用的动态追踪工具。
**SystemTap** 也是一种可以通过脚本进行自由扩展的动态追踪技术。在 eBPF 出现之前SystemTap 是Linux 系统中,功能最接近 DTrace 的动态追踪机制。不过要注意SystemTap 在很长时间以来都游离于内核之外(而 eBPF 自诞生以来,一直根植在内核中)。
所以从稳定性上来说SystemTap 只在 RHEL 系统中好用,在其他系统中则容易出现各种异常问题。当然,反过来说,支持 3.x 等旧版本的内核,也是 SystemTap 相对于 eBPF 的一个巨大优势。
**sysdig** 则是随着容器技术的普及而诞生的主要用于容器的动态追踪。sysdig 汇集了一些列性能工具的优势可以说是集百家之所长。我习惯用这个公式来表示sysdig的特点 sysdig = strace + tcpdump + htop + iftop + lsof + docker inspect。
而在最新的版本中(内核版本 &gt;= 4.14sysdig 还可以通过 eBPF 来进行扩展,所以,也可以用来追踪内核中的各种函数和事件。
## 如何选择追踪工具
到这里,你可能又觉得头大了,这么多动态追踪工具,在实际场景中到底该怎么选择呢?还是那句话,具体性能工具的选择,就要从具体的工作原理来入手。
这两节课,我们已经把常见工具的原理和特点都介绍过了,你可以先自己思考区分一下,不同场景的工具选择问题。比如:
<li>
在不需要很高灵活性的场景中,使用 perf 对性能事件进行采样,然后再配合火焰图辅助分析,就是最常用的一种方法;
</li>
<li>
而需要对事件或函数调用进行统计分析(比如观察不同大小的 I/O 分布)时,就要用 SystemTap 或者 eBPF通过一些自定义的脚本来进行数据处理。
</li>
在这里,我也总结了几个常见的动态追踪使用场景,并且分别推荐了适合的工具。你可以保存这个表格,方便自己查找并使用。
<img src="https://static001.geekbang.org/resource/image/5a/25/5a2b2550547d5eaee850bfb806f76625.png" alt="">
## 小结
今天,我主要带你学习了 perf、eBPF 和 BCC 等动态追踪方法,并总结了不同场景中如何选择动态追踪方法。
在 Linux 系统中,常见的动态追踪方法,包括 ftrace、perf、eBPF 以及 SystemTap 等。在大多数性能问题中,使用 perf 配合火焰图是一个不错的方法。如果这满足不了你的要求,那么:
<li>
在新版的内核中eBPF 和 BCC 是最灵活的动态追踪方法;
</li>
<li>
而在旧版本内核中,特别是在 RHEL 系统中,由于 eBPF 支持受限SystemTap 往往是更好的选择。
</li>
此外,在使用动态追踪技术时,为了得到分析目标的详细信息,一般需要内核以及应用程序的调试符号表。动态追踪实际上也是在这些符号(包括函数和事件)上进行的,所以易读易理解的符号,有助于加快动态追踪的过程。
## 思考
最后,我想邀请你一起来聊聊,你所理解的动态追踪技术。你有没有在实际环境中用过动态追踪呢?这么多的动态追踪方法,你一般会怎么选择呢?你可以结合今天的内容,和你自己的操作记录,来总结思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,573 @@
<audio id="audio" title="52 | 案例篇:服务吞吐量下降很厉害,怎么分析?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9c/b7/9c27018228efbf07b14ae9dbdac531b7.mp3"></audio>
你好,我是倪朋飞。
上一节,我们一起学习了怎么使用动态追踪来观察应用程序和内核的行为。先简单来回顾一下。
所谓动态追踪,就是在系统或者应用程序还在正常运行的时候,通过内核中提供的探针,来动态追踪它们的行为,从而辅助排查出性能问题的瓶颈。
使用动态追踪,便可以在不修改代码也不重启服务的情况下,动态了解应用程序或者内核的行为。这对排查线上的问题、特别是不容易重现的问题尤其有效。
在 Linux 系统中,常见的动态追踪方法包括 ftrace、perf、eBPF/BCC 以及 SystemTap 等。
<li>
使用 perf 配合火焰图寻找热点函数,是一个比较通用的性能定位方法,在很多场景中都可以使用。
</li>
<li>
如果这仍满足不了你的要求那么在新版的内核中eBPF 和 BCC 是最灵活的动态追踪方法。
</li>
<li>
而在旧版本内核,特别是在 RHEL 系统中,由于 eBPF 支持受限SystemTap 和 ftrace 往往是更好的选择。
</li>
在 [网络请求延迟变大](https://time.geekbang.org/column/article/82833) 的案例中,我带你一起分析了一个网络请求延迟增大的问题。当时我们分析知道,那是由于服务器端开启 TCP 的 Nagle 算法,而客户端却开启了延迟确认所导致的。
其实,除了延迟问题外,网络请求的吞吐量下降,是另一个常见的性能问题。那么,针对这种吞吐量下降问题,我们又该如何进行分析呢?
接下来,我就以最常用的反向代理服务器 Nginx 为例,带你一起看看,如何分析服务吞吐量下降的问题。
## 案例准备
今天的案例需要用到两台虚拟机,还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
<li>
机器配置2 CPU8GB 内存。
</li>
<li>
预先安装 docker、curl、wrk、perf、FlameGraph 等工具,比如
</li>
```
# 安装必备docker、curl和perf
$ apt-get install -y docker.io curl build-essential linux-tools-common
# 安装火焰图工具
$ git clone https://github.com/brendangregg/FlameGraph
# 安装wrk
$ git clone https://github.com/wg/wrk
$ cd wrk &amp;&amp; make &amp;&amp; sudo cp wrk /usr/local/bin/
```
这些工具,我们在前面的案例中已经多次使用,这儿就不再重复。你可以打开两个终端,分别登录到这两台虚拟机中,并安装上述工具。
>
注意,以下所有命令都默认以 root 用户运行,如果你用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
## 案例分析
我们今天要分析的案例是一个 Nginx + PHP 应用,它们的关系如下图所示:
<img src="https://static001.geekbang.org/resource/image/e9/bb/e90d67270cf703aba6c487584d17cfbb.png" alt="">
其中wrk 和 curl 是 Nginx 的客户端,而 PHP 应用则是一个简单的 Hello World
```
&lt;?php
echo &quot;Hello World!&quot;
?&gt;
```
为了方便你运行,我已经把案例应用打包成了两个 Docker 镜像,并推送到 Docker Hub 中。你可以直接按照下面的步骤来运行它。
同时,为了分析方便,这两个容器都将运行在 host network 模式中。这样,我们就不用切换到容器的网络命名空间,而可以直接观察它们的套接字状态。
我们先在终端一中,执行下面的命令,启动 Nginx 应用,并监听在 80 端口。如果一切正常,你应该可以看到如下的输出:
```
$ docker run --name nginx --network host --privileged -itd feisky/nginx-tp
6477c607c13b37943234755a14987ffb3a31c33a7f04f75bb1c190e710bce19e
$ docker run --name phpfpm --network host --privileged -itd feisky/php-fpm-tp
09e0255159f0c8a647e22cd68bd097bec7efc48b21e5d91618ff29b882fa7c1f
```
然后,执行 docker ps 命令查询容器的状态你会发现容器已经处于运行状态Up
```
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
09e0255159f0 feisky/php-fpm-tp &quot;php-fpm -F --pid /o…&quot; 28 seconds ago Up 27 seconds phpfpm
6477c607c13b feisky/nginx-tp &quot;/init.sh&quot; 29 seconds ago Up 28 seconds nginx
```
不过,从 docker ps 的输出,我们只能知道容器处于运行状态。至于 Nginx 能不能正常处理外部的请求,还需要我们进一步确认。
接着,切换到终端二中,执行下面的 curl 命令,进一步验证 Nginx 能否正常访问。如果你看到 “Hello World!” 的输出,说明 Nginx+PHP 的应用已经正常启动了:
```
$ curl http://192.168.0.30
Hello World!
```
>
提示:如果你看到不一样的结果,可以再次执行 docker ps -a 确认容器的状态,并执行 docker logs &lt;容器名&gt; 来查看容器日志,从而找出原因。
接下来,我们就来测试一下,案例中 Nginx 的吞吐量。
我们继续在终端二中,执行 wrk 命令,来测试 Nginx 的性能:
```
# 默认测试时间为10s请求超时2s
$ wrk --latency -c 1000 http://192.168.0.30
Running 10s test @ http://192.168.0.30
2 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 14.82ms 42.47ms 874.96ms 98.43%
Req/Sec 550.55 1.36k 5.70k 93.10%
Latency Distribution
50% 11.03ms
75% 15.90ms
90% 23.65ms
99% 215.03ms
1910 requests in 10.10s, 573.56KB read
Non-2xx or 3xx responses: 1910
Requests/sec: 189.10
Transfer/sec: 56.78KB
```
从 wrk 的结果中,你可以看到吞吐量(也就是每秒请求数)只有 189并且所有 1910 个请求收到的都是异常响应(非 2xx 或 3xx。这些数据显然表明吞吐量太低了并且请求处理都失败了。这是怎么回事呢
根据 wrk 输出的统计结果,我们可以看到,总共传输的数据量只有 573 KB那就肯定不会是带宽受限导致的。所以我们应该从请求数的角度来分析。
分析请求数,特别是 HTTP 的请求数,有什么好思路吗?当然就要从 TCP 连接数入手。
### 连接数优化
要查看 TCP 连接数的汇总情况,首选工具自然是 ss 命令。为了观察 wrk 测试时发生的问题,我们在终端二中再次启动 wrk并且把总的测试时间延长到 30 分钟:
```
# 测试时间30分钟
$ wrk --latency -c 1000 -d 1800 http://192.168.0.30
```
然后,回到终端一中,观察 TCP 连接数:
```
$ ss -s
Total: 177 (kernel 1565)
TCP: 1193 (estab 5, closed 1178, orphaned 0, synrecv 0, timewait 1178/0), ports 0
Transport Total IP IPv6
* 1565 - -
RAW 1 0 1
UDP 2 2 0
TCP 15 12 3
INET 18 14 4
FRAG 0 0 0
```
从这里看出wrk 并发 1000 请求时,建立连接数只有 5而 closed 和 timewait 状态的连接则有 1100 多 。其实从这儿你就可以发现两个问题:
<li>
一个是建立连接数太少了;
</li>
<li>
另一个是 timewait 状态连接太多了。
</li>
分析问题,自然要先从相对简单的下手。我们先来看第二个关于 timewait 的问题。在之前的 NAT 案例中,我已经提到过,内核中的连接跟踪模块,有可能会导致 timewait 问题。我们今天的案例还是基于 Docker 运行,而 Docker 使用的 iptables ,就会使用连接跟踪模块来管理 NAT。那么怎么确认是不是连接跟踪导致的问题呢
其实,最简单的方法,就是通过 dmesg 查看系统日志,如果有连接跟踪出了问题,应该会看到 nf_conntrack 相关的日志。
我们可以继续在终端一中,运行下面的命令,查看系统日志:
```
$ dmesg | tail
[88356.354329] nf_conntrack: nf_conntrack: table full, dropping packet
[88356.354374] nf_conntrack: nf_conntrack: table full, dropping packet
```
从日志中,你可以看到 nf_conntrack: table full, dropping packet 的错误日志。这说明,正是连接跟踪导致的问题。
这种情况下,我们应该想起前面学过的两个内核选项——连接跟踪数的最大限制 nf_conntrack_max ,以及当前的连接跟踪数 nf_conntrack_count。执行下面的命令你就可以查询这两个选项
```
$ sysctl net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_max = 200
$ sysctl net.netfilter.nf_conntrack_count
net.netfilter.nf_conntrack_count = 200
```
这次的输出中,你可以看到最大的连接跟踪限制只有 200并且全部被占用了。200 的限制显然太小,不过相应的优化也很简单,调大就可以了。
我们执行下面的命令,将 nf_conntrack_max 增大:
```
# 将连接跟踪限制增大到1048576
$ sysctl -w net.netfilter.nf_conntrack_max=1048576
```
连接跟踪限制增大后,对 Nginx 吞吐量的优化效果如何呢?我们不妨再来测试一下。你可以切换到终端二中,按下 Ctrl+C ;然后执行下面的 wrk 命令,重新测试 Nginx 的性能:
```
# 默认测试时间为10s请求超时2s
$ wrk --latency -c 1000 http://192.168.0.30
...
54221 requests in 10.07s, 15.16MB read
Socket errors: connect 0, read 7, write 0, timeout 110
Non-2xx or 3xx responses: 45577
Requests/sec: 5382.21
Transfer/sec: 1.50MB
```
从 wrk 的输出中,你可以看到,连接跟踪的优化效果非常好,吞吐量已经从刚才的 189 增大到了 5382。看起来性能提升了将近 30 倍,
不过,这是不是就能说明,我们已经把 Nginx 的性能优化好了呢?
别急,我们再来看看 wrk 汇报的其他数据。果然10s 内的总请求数虽然增大到了 5 万,但是有 4 万多响应异常,说白了,真正成功的只有 8000多个54221-45577=8644
很明显,大部分请求的响应都是异常的。那么,该怎么分析响应异常的问题呢?
### 工作进程优化
由于这些响应并非 Socket error说明 Nginx 已经收到了请求,只不过,响应的状态码并不是我们期望的 2xx (表示成功)或 3xx表示重定向。所以这种情况下搞清楚 Nginx 真正的响应就很重要了。
不过这也不难,我们切换回终端一,执行下面的 docker 命令,查询 Nginx 容器日志就知道了:
```
$ docker logs nginx --tail 3
192.168.0.2 - - [15/Mar/2019:2243:27 +0000] &quot;GET / HTTP/1.1&quot; 499 0 &quot;-&quot; &quot;-&quot; &quot;-&quot;
192.168.0.2 - - [15/Mar/2019:22:43:27 +0000] &quot;GET / HTTP/1.1&quot; 499 0 &quot;-&quot; &quot;-&quot; &quot;-&quot;
192.168.0.2 - - [15/Mar/2019:22:43:27 +0000] &quot;GET / HTTP/1.1&quot; 499 0 &quot;-&quot; &quot;-&quot; &quot;-&quot;
```
从 Nginx 的日志中,我们可以看到,响应状态码为 499。
499 并非标准的 HTTP 状态码,而是由 Nginx 扩展而来表示服务器端还没来得及响应时客户端就已经关闭连接了。换句话说问题在于服务器端处理太慢客户端因为超时wrk超时时间为2s主动断开了连接。
既然问题出在了服务器端处理慢,而案例本身是 Nginx+PHP 的应用,那是不是可以猜测,是因为 PHP 处理过慢呢?
我么可以在终端中,执行下面的 docker 命令,查询 PHP 容器日志:
```
$ docker logs phpfpm --tail 5
[15-Mar-2019 22:28:56] WARNING: [pool www] server reached max_children setting (5), consider raising it
[15-Mar-2019 22:43:17] WARNING: [pool www] server reached max_children setting (5), consider raising it
```
从这个日志中我们可以看到两条警告信息server reached max_children setting (5),并建议增大 max_children。
max_children 表示 php-fpm 子进程的最大数量,当然是数值越大,可以同时处理的请求数就越多。不过由于这是进程问题,数量增大,也会导致更多的内存和 CPU 占用。所以,我们还不能设置得过大。
一般来说,每个 php-fpm 子进程可能会占用 20 MB 左右的内存。所以,你可以根据内存和 CPU个数估算一个合理的值。这儿我把它设置成了 20并将优化后的配置重新打包成了 Docker 镜像。你可以执行下面的命令来执行它:
```
# 停止旧的容器
$ docker rm -f nginx phpfpm
# 使用新镜像启动Nginx和PHP
$ docker run --name nginx --network host --privileged -itd feisky/nginx-tp:1
$ docker run --name phpfpm --network host --privileged -itd feisky/php-fpm-tp:1
```
然后我们切换到终端二,再次执行下面的 wrk 命令,重新测试 Nginx 的性能:
```
# 默认测试时间为10s请求超时2s
$ wrk --latency -c 1000 http://192.168.0.30
...
47210 requests in 10.08s, 12.51MB read
Socket errors: connect 0, read 4, write 0, timeout 91
Non-2xx or 3xx responses: 31692
Requests/sec: 4683.82
Transfer/sec: 1.24MB
```
从 wrk 的输出中,可以看到,虽然吞吐量只有 4683比刚才的 5382 少了一些;但是测试期间成功的请求数却多了不少,从原来的 8000增长到了 1500047210-31692=15518
不过,虽然性能有所提升,可 4000 多的吞吐量显然还是比较差的,并且大部分请求的响应依然还是异常。接下来,该怎么去进一步提升 Nginx 的吞吐量呢?
### 套接字优化
回想一下网络性能的分析套路,以及 Linux 协议栈的原理我们可以从从套接字、TCP 协议等逐层分析。而分析的第一步,自然还是要观察有没有发生丢包现象。
我们切换到终端二中,重新运行测试,这次还是要用 -d 参数延长测试时间,以便模拟性能瓶颈的现场:
```
# 测试时间30分钟
$ wrk --latency -c 1000 -d 1800 http://192.168.0.30
```
然后回到终端一中,观察有没有发生套接字的丢包现象:
```
# 只关注套接字统计
$ netstat -s | grep socket
73 resets received for embryonic SYN_RECV sockets
308582 TCP sockets finished time wait in fast timer
8 delayed acks further delayed because of locked socket
290566 times the listen queue of a socket overflowed
290566 SYNs to LISTEN sockets dropped
# 稍等一会,再次运行
$ netstat -s | grep socket
73 resets received for embryonic SYN_RECV sockets
314722 TCP sockets finished time wait in fast timer
8 delayed acks further delayed because of locked socket
344440 times the listen queue of a socket overflowed
344440 SYNs to LISTEN sockets dropped
```
根据两次统计结果中 socket overflowed 和 sockets dropped 的变化,你可以看到,有大量的套接字丢包,并且丢包都是套接字队列溢出导致的。所以,接下来,我们应该分析连接队列的大小是不是有异常。
你可以执行下面的命令,查看套接字的队列大小:
```
$ ss -ltnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 10 10 0.0.0.0:80 0.0.0.0:* users:((&quot;nginx&quot;,pid=10491,fd=6),(&quot;nginx&quot;,pid=10490,fd=6),(&quot;nginx&quot;,pid=10487,fd=6))
LISTEN 7 10 *:9000 *:* users:((&quot;php-fpm&quot;,pid=11084,fd=9),...,(&quot;php-fpm&quot;,pid=10529,fd=7))
```
这次可以看到Nginx 和 php-fpm 的监听队列 Send-Q只有 10而 nginx 的当前监听队列长度 Recv-Q已经达到了最大值php-fpm 也已经接近了最大值。很明显,套接字监听队列的长度太小了,需要增大。
关于套接字监听队列长度的设置,既可以在应用程序中,通过套接字接口调整,也支持通过内核选项来配置。我们继续在终端一中,执行下面的命令,分别查询 Nginx 和内核选项对监听队列长度的配置:
```
# 查询nginx监听队列长度配置
$ docker exec nginx cat /etc/nginx/nginx.conf | grep backlog
listen 80 backlog=10;
# 查询php-fpm监听队列长度
$ docker exec phpfpm cat /opt/bitnami/php/etc/php-fpm.d/www.conf | grep backlog
; Set listen(2) backlog.
;listen.backlog = 511
# somaxconn是系统级套接字监听队列上限
$ sysctl net.core.somaxconn
net.core.somaxconn = 10
```
从输出中可以看到Nginx 和 somaxconn 的配置都是 10而 php-fpm 的配置也只有 511显然都太小了。那么优化方法就是增大这三个配置比如可以把 Nginx 和 php-fpm 的队列长度增大到 8192而把 somaxconn 增大到 65536。
同样地,我也把这些优化后的 Nginx ,重新打包成了两个 Docker 镜像,你可以执行下面的命令来运行它:
```
# 停止旧的容器
$ docker rm -f nginx phpfpm
# 使用新镜像启动Nginx和PHP
$ docker run --name nginx --network host --privileged -itd feisky/nginx-tp:2
$ docker run --name phpfpm --network host --privileged -itd feisky/php-fpm-tp:2
```
然后,切换到终端二中,重新测试 Nginx 的性能:
```
$ wrk --latency -c 1000 http://192.168.0.30
...
62247 requests in 10.06s, 18.25MB read
Non-2xx or 3xx responses: 62247
Requests/sec: 6185.65
Transfer/sec: 1.81MB
```
现在的吞吐量已经增大到了 6185并且在测试的时候如果你在终端一中重新执行 **netstat -s | grep socket**,还会发现,现在已经没有套接字丢包问题了。
不过,这次 Nginx 的响应,再一次全部失败了,都是 Non-2xx or 3xx。这是怎么回事呢我们再去终端一中查看 Nginx 日志:
```
$ docker logs nginx --tail 10
2019/03/15 16:52:39 [crit] 15#15: *999779 connect() to 127.0.0.1:9000 failed (99: Cannot assign requested address) while connecting to upstream, client: 192.168.0.2, server: localhost, request: &quot;GET / HTTP/1.1&quot;, upstream: &quot;fastcgi://127.0.0.1:9000&quot;, host: &quot;192.168.0.30&quot;
```
你可以看到Nginx 报出了无法连接 fastcgi 的错误,错误消息是 Connect 时, Cannot assign requested address。这个错误消息对应的错误代码为 EADDRNOTAVAIL表示 IP 地址或者端口号不可用。
在这里,显然只能是端口号的问题。接下来,我们就来分析端口号的情况。
### 端口号优化
根据网络套接字的原理,当客户端连接服务器端时,需要分配一个临时端口号,而 Nginx 正是 PHP-FPM 的客户端。端口号的范围并不是无限的最多也只有6万多。
我们执行下面的命令,就可以查询系统配置的临时端口号范围:
```
$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range=20000 20050
```
你可以看到临时端口的范围只有50个显然太小了 。优化方法很容易想到,增大这个范围就可以了。比如,你可以执行下面的命令,把端口号范围扩展为 “10000 65535”
```
$ sysctl -w net.ipv4.ip_local_port_range=&quot;10000 65535&quot;
net.ipv4.ip_local_port_range = 10000 65535
```
优化完成后,我们再次切换到终端二中,测试性能:
```
$ wrk --latency -c 1000 http://192.168.0.30/
...
32308 requests in 10.07s, 6.71MB read
Socket errors: connect 0, read 2027, write 0, timeout 433
Non-2xx or 3xx responses: 30
Requests/sec: 3208.58
Transfer/sec: 682.15KB
```
这次,异常的响应少多了 ,不过,吞吐量也下降到了 3208。并且这次还出现了很多 Socket read errors。显然还得进一步优化。
### 火焰图
前面我们已经优化了很多配置。这些配置在优化网络的同时,却也会带来其他资源使用的上升。这样来看,是不是说明其他资源遇到瓶颈了呢?
我们不妨在终端二中,执行下面的命令,重新启动长时间测试:
```
# 测试时间30分钟
$ wrk --latency -c 1000 -d 1800 http://192.168.0.30
```
然后,切换回终端一中,执行 top ,观察 CPU 和内存的使用:
```
$ top
...
%Cpu0 : 30.7 us, 48.7 sy, 0.0 ni, 2.3 id, 0.0 wa, 0.0 hi, 18.3 si, 0.0 st
%Cpu1 : 28.2 us, 46.5 sy, 0.0 ni, 2.0 id, 0.0 wa, 0.0 hi, 23.3 si, 0.0 st
KiB Mem : 8167020 total, 5867788 free, 490400 used, 1808832 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 7361172 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
20379 systemd+ 20 0 38068 8692 2392 R 36.1 0.1 0:28.86 nginx
20381 systemd+ 20 0 38024 8700 2392 S 33.8 0.1 0:29.29 nginx
1558 root 20 0 1118172 85868 39044 S 32.8 1.1 22:55.79 dockerd
20313 root 20 0 11024 5968 3956 S 27.2 0.1 0:22.78 docker-containe
13730 root 20 0 0 0 0 I 4.0 0.0 0:10.07 kworker/u4:0-ev
```
从 top 的结果中可以看到,可用内存还是很充足的,但系统 CPU 使用率sy比较高两个 CPU 的系统 CPU 使用率都接近 50%,且空闲 CPU 使用率只有 2%。再看进程部分CPU 主要被两个 Nginx 进程和两个 docker 相关的进程占用,使用率都是 30% 左右。
CPU 使用率上升了,该怎么进行分析呢?我想,你已经还记得我们多次用到的 perf再配合前两节讲过的火焰图很容易就能找到系统中的热点函数。
我们保持终端二中的 wrk 继续运行;在终端一中,执行 perf 和 flamegraph 脚本,生成火焰图:
```
# 执行perf记录事件
$ perf record -g
# 切换到FlameGraph安装路径执行下面的命令生成火焰图
$ perf script -i ~/perf.data | ./stackcollapse-perf.pl --all | ./flamegraph.pl &gt; nginx.svg
```
然后,使用浏览器打开生成的 nginx.svg ,你就可以看到下面的火焰图:
<img src="https://static001.geekbang.org/resource/image/89/c6/8933557b5eb8c8f41a629e751fd7f0c6.png" alt="">
根据我们讲过的火焰图原理,这个图应该从下往上、沿着调用栈中最宽的函数,来分析执行次数最多的函数。
这儿中间的 do_syscall_64、tcp_v4_connect、inet_hash_connect 这个堆栈很明显就是最需要关注的地方。inet_hash_connect() 是 Linux 内核中负责分配临时端口号的函数。所以,这个瓶颈应该还在临时端口的分配上。
在上一步的“端口号”优化中,临时端口号的范围,已经优化成了 “10000 65535”。这显然是一个非常大的范围那么端口号的分配为什么又成了瓶颈呢
一时想不到也没关系,我们可以暂且放下,先看看其他因素的影响。再顺着 inet_hash_connect 往堆栈上面查看下一个热点是__init_check_established 函数。而这个函数的目的是检查端口号是否可用。结合这一点你应该可以想到如果有大量连接占用着端口那么检查端口号可用的函数不就会消耗更多的CPU吗
实际是否如此呢?我们可以继续在终端一中运行 ss 命令, 查看连接状态统计:
```
$ ss -s
TCP: 32775 (estab 1, closed 32768, orphaned 0, synrecv 0, timewait 32768/0), ports 0
...
```
这回可以看到,有大量连接(这儿是 32768处于 timewait 状态,而 timewait 状态的连接,本身会继续占用端口号。如果这些端口号可以重用,那么自然就可以缩短 __init_check_established 的过程。而 Linux 内核中,恰好有一个 tcp_tw_reuse 选项,用来控制端口号的重用。
我们在终端一中,运行下面的命令,查询它的配置:
```
$ sysctl net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_reuse = 0
```
你可以看到tcp_tw_reuse 是0也就是禁止状态。其实看到这里我们就能理解为什么临时端口号的分配会是系统运行的热点了。当然优化方法也很容易把它设置成 1 就可以开启了。
我把优化后的应用,也打包成了两个 Docker 镜像,你可以执行下面的命令来运行:
```
# 停止旧的容器
$ docker rm -f nginx phpfpm
# 使用新镜像启动Nginx和PHP
$ docker run --name nginx --network host --privileged -itd feisky/nginx-tp:3
$ docker run --name phpfpm --network host --privileged -itd feisky/php-fpm-tp:3
```
容器启动后,切换到终端二中,再次测试优化后的效果:
```
$ wrk --latency -c 1000 http://192.168.0.30/
...
52119 requests in 10.06s, 10.81MB read
Socket errors: connect 0, read 850, write 0, timeout 0
Requests/sec: 5180.48
Transfer/sec: 1.07MB
```
现在的吞吐量已经达到了 5000 多,并且只有少量的 Socket errors也不再有 Non-2xx or 3xx 的响应了。说明一切终于正常了。
案例的最后,不要忘记执行下面的命令,删除案例应用:
```
# 停止nginx和phpfpm容器
$ docker rm -f nginx phpfpm
```
## 小结
今天,我带你一起学习了服务吞吐量下降后的分析方法。其实,从这个案例你也可以看出,性能问题的分析,总是离不开系统和应用程序的原理。
实际上,分析性能瓶颈,最核心的也正是掌握运用这些原理。
<li>
首先,利用各种性能工具,收集想要的性能指标,从而清楚系统和应用程序的运行状态;
</li>
<li>
其次,拿目前状态跟系统原理进行比较,不一致的地方,就是我们要重点分析的对象。
</li>
从这个角度出发,再进一步借助 perf、火焰图、bcc 等动态追踪工具,找出热点函数,就可以定位瓶颈的来源,确定相应的优化方法。
## 思考
最后,我想邀请你一起来聊聊,你碰到过的吞吐量下降问题。你是怎么分析它们的根源?又是怎么解决的?你可以结合我的讲述,总结自己的思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,102 @@
<audio id="audio" title="53 | 套路篇:系统监控的综合思路" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/25/72/253616d694bc48e396c99a11e8701b72.mp3"></audio>
你好,我是倪朋飞。
在前面的内容中,我为你介绍了很多性能分析的原理、思路以及相关的工具。不过,在实际的性能分析中,一个很常见的现象是,明明发生了性能瓶颈,但当你登录到服务器中想要排查的时候,却发现瓶颈已经消失了。或者说,性能问题总是时不时地发生,但却很难找出发生规律,也很难重现。
当面对这样的场景时,你可能会发现,我们前面介绍的各种工具、方法都“失效“了。为什么呢?因为它们都需要在性能问题发生的时刻才有效,而在这些事后分析的场景中,我们就很难发挥它们的威力了。
那该怎么办呢?置之不理吗?其实以往,很多应用都是等到用户抱怨响应慢了,或者系统崩溃了后,才发现系统或者应用程序的性能出现了问题。虽然最终也能发现问题,但显然,这种方法是不可取的,因为严重影响了用户的体验。
而要解决这个问题,就要搭建监控系统,把系统和应用程序的运行状况监控起来,并定义一系列的策略,在发生问题时第一时间告警通知。一个好的监控系统,不仅可以实时暴露系统的各种问题,更可以根据这些监控到的状态,自动分析和定位大致的瓶颈来源,从而更精确地把问题汇报给相关团队处理。
要做好监控,最核心的就是全面的、可量化的指标,这包括系统和应用两个方面。
从系统来说,监控系统要涵盖系统的整体资源使用情况,比如我们前面讲过的 CPU、内存、磁盘和文件系统、网络等各种系统资源。
而从应用程序来说监控系统要涵盖应用程序内部的运行状态这既包括进程的CPU、磁盘I/O 等整体运行状况,更需要包括诸如接口调用耗时、执行过程中的错误、内部对象的内存使用等应用程序内部的运行状况。
今天,我就带你一起来看看,如何对 Linux 系统进行监控。而在下一节,我将继续为你讲解应用程序监控的思路。
## USE 法
在开始监控系统之前,你肯定最想知道,怎么才能用简洁的方法,来描述系统资源的使用情况。你当然可以使用专栏中学到的各种性能工具,来分别收集各种资源的使用情况。不过不要忘记,每种资源的性能指标可都有很多,使用过多指标本身耗时耗力不说,也不容易为你建立起系统整体的运行状况。
在这里,我为你介绍一种专门用于性能监控的 USEUtilization Saturation and Errors法。USE 法把系统资源的性能指标,简化成了三个类别,即使用率、饱和度以及错误数。
<li>
使用率表示资源用于服务的时间或容量百分比。100% 的使用率,表示容量已经用尽或者全部时间都用于服务。
</li>
<li>
饱和度表示资源的繁忙程度通常与等待队列的长度相关。100% 的饱和度,表示资源无法接受更多的请求。
</li>
<li>
错误数表示发生错误的事件个数。错误数越多,表明系统的问题越严重。
</li>
这三个类别的指标,涵盖了系统资源的常见性能瓶颈,所以常被用来快速定位系统资源的性能瓶颈。这样,无论是对 CPU、内存、磁盘和文件系统、网络等硬件资源还是对文件描述符数、连接数、连接跟踪数等软件资源USE 方法都可以帮你快速定位出,是哪一种系统资源出现了性能瓶颈。
那么,对于每一种系统资源,又有哪些常见的性能指标呢?回忆一下我们讲过的各种系统资源原理,并不难想到相关的性能指标。这里,我把常见的性能指标画了一张表格,方便你在需要时查看。
<img src="https://static001.geekbang.org/resource/image/cc/ee/ccd7a9350c270c0168bad6cc8d0b8aee.png" alt="">
不过需要注意的是USE 方法只关注能体现系统资源性能瓶颈的核心指标,但这并不是说其他指标不重要。诸如系统日志、进程资源使用量、缓存使用量等其他各类指标,也都需要我们监控起来。只不过,它们通常用作辅助性能分析,而 USE 方法的指标,则直接表明了系统的资源瓶颈。
## 监控系统
掌握 USE 方法以及需要监控的性能指标后,接下来要做的,就是建立监控系统,把这些指标保存下来;然后,根据这些监控到的状态,自动分析和定位大致的瓶颈来源;最后,再通过告警系统,把问题及时汇报给相关团队处理。
可以看出,一个完整的监控系统通常由数据采集、数据存储、数据查询和处理、告警以及可视化展示等多个模块组成。所以,要从头搭建一个监控系统,其实也是一个很大的系统工程。
不过,幸运的是,现在已经有很多开源的监控工具可以直接使用,比如最常见的 Zabbix、Nagios、Prometheus 等等。
下面,我就以 Prometheus 为例,为你介绍这几个组件的基本原理。如下图所示,就是 Prometheus 的基本架构:
<img src="https://static001.geekbang.org/resource/image/7f/56/7f9c36db17785097ef9d186fd782ce56.png" alt="">
(图片来自 [prometheus.io](https://prometheus.io/docs/introduction/overview/)
先看数据采集模块。最左边的 Prometheus targets 就是数据采集的对象,而 Retrieval 则负责采集这些数据。从图中你也可以看到Prometheus 同时支持 Push 和 Pull 两种数据采集模式。
<li>
Pull 模式,由服务器端的采集模块来触发采集。只要采集目标提供了 HTTP 接口,就可以自由接入(这也是最常用的采集模式)。
</li>
<li>
Push 模式,则是由各个采集目标主动向 Push Gateway用于防止数据丢失推送指标再由服务器端从 Gateway 中拉取过去(这是移动应用中最常用的采集模式)。
</li>
由于需要监控的对象通常都是动态变化的Prometheus 还提供了服务发现的机制,可以自动根据预配置的规则,动态发现需要监控的对象。这在 Kubernetes 等容器平台中非常有效。
第二个是数据存储模块。为了保持监控数据的持久化,图中的 TSDBTime series database模块负责将采集到的数据持久化到 SSD 等磁盘设备中。TSDB 是专门为时间序列数据设计的一种数据库,特点是以时间为索引、数据量大并且以追加的方式写入。
第三个是数据查询和处理模块。刚才提到的 TSDB在存储数据的同时其实还提供了数据查询和基本的数据处理功能而这也就是 PromQL 语言。PromQL 提供了简洁的查询、过滤功能,并且支持基本的数据处理方法,是告警系统和可视化展示的基础。
第四个是告警模块。右上角的 AlertManager 提供了告警的功能,包括基于 PromQL 语言的触发条件、告警规则的配置管理以及告警的发送等。不过虽然告警是必要的但过于频繁的告警显然也不可取。所以AlertManager 还支持通过分组、抑制或者静默等多种方式来聚合同类告警,并减少告警数量。
最后一个是可视化展示模块。Prometheus 的 web UI 提供了简单的可视化界面,用于执行 PromQL 查询语句,但结果的展示比较单调。不过,一旦配合 Grafana就可以构建非常强大的图形界面了。
介绍完了这些组件,想必你对每个模块都有了比较清晰的认识。接下来,我们再来继续深入了解这些组件结合起来的整体功能。
比如,以刚才提到的 USE 方法为例,我使用 Prometheus可以收集 Linux 服务器的 CPU、内存、磁盘、网络等各类资源的使用率、饱和度和错误数指标。然后通过 Grafana 以及 PromQL 查询语句,就可以把它们以图形界面的方式直观展示出来。
<img src="https://static001.geekbang.org/resource/image/e5/91/e55600aa21fd6e8d96373f950b2a9991.png" alt=""><img src="https://static001.geekbang.org/resource/image/28/86/28410012526e7f91c93ce3db31e68286.png" alt="">
## 小结
今天,我带你一起梳理了系统监控的基本思路。
系统监控的核心是资源的使用情况包括CPU、内存、磁盘和文件系统、网络等硬件资源以及文件描述符数、连接数、连接跟踪数等软件资源。而这些资源都可以通过 USE 法来建立核心性能指标。
USE 法把系统资源的性能指标,简化成了三个类别,即使用率、饱和度以及错误数。 这三者任一类别过高时,都代表相对应的系统资源有可能存在性能瓶颈。
基于 USE 法建立性能指标后,还需要通过一套完整的监控系统,把这些指标从采集、存储、查询、处理,再到告警和可视化展示等串联起来。你可以基于 Zabbix、Prometheus 等各种开源的监控产品,构建这套监控系统。这样,不仅可以将系统资源的瓶颈快速暴露出来,还可以借助监控的历史,事后追查定位问题。
当然,除了系统监控之外,应用程序的监控也是必不可少的,我将在下一节课继续为你拆解。
## 思考
最后,我想邀请你一起来聊聊,你是怎么监控系统性能的。你通常会监控哪些系统的性能指标,又是如何搭建监控系统、如何根据这些指标来定位系统资源瓶颈的?你可以结合我的讲述,总结自己的思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,111 @@
<audio id="audio" title="54 | 套路篇:应用监控的一般思路" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d0/04/d026bfa36fb4a4ee592e1a2c23c53704.mp3"></audio>
你好,我是倪朋飞。
上一节,我带你学习了,如何使用 USE 法来监控系统的性能,先简单回顾一下。
系统监控的核心是资源的使用情况这既包括CPU、内存、磁盘、文件系统、网络等硬件资源也包括文件描述符数、连接数、连接跟踪数等软件资源。而要描述这些资源瓶颈最简单有效的方法就是 USE 法。
USE 法把系统资源的性能指标,简化为了三个类别:使用率、饱和度以及错误数。 当这三者之中任一类别的指标过高时,都代表相对应的系统资源可能存在性能瓶颈。
基于 USE 法建立性能指标后,我们还需要通过一套完整的监控系统,把这些指标从采集、存储、查询、处理,再到告警和可视化展示等贯穿起来。这样,不仅可以将系统资源的瓶颈快速暴露出来,还可以借助监控的历史数据,来追踪定位性能问题的根源。
除了上一节讲到的系统资源需要监控之外,应用程序的性能监控,当然也是必不可少的。今天,我就带你一起来看看,如何监控应用程序的性能。
## 指标监控
跟系统监控一样,在构建应用程序的监控系统之前,首先也需要确定,到底需要监控哪些指标。特别是要清楚,有哪些指标可以用来快速确认应用程序的性能问题。
对系统资源的监控USE 法简单有效,却不代表其适合应用程序的监控。举个例子,即使在 CPU 使用率很低的时候,也不能说明应用程序就没有性能瓶颈。因为应用程序可能会因为锁或者 RPC 调用等,导致响应缓慢。
所以,**应用程序的核心指标,不再是资源的使用情况,而是请求数、错误率和响应时间**。这些指标不仅直接关系到用户的使用体验,还反映应用整体的可用性和可靠性。
有了请求数、错误率和响应时间这三个黄金指标之后,我们就可以快速知道,应用是否发生了性能问题。但是,只有这些指标显然还是不够的,因为发生性能问题后,我们还希望能够快速定位“性能瓶颈区”。所以,在我看来,下面几种指标,也是监控应用程序时必不可少的。
**第一个,是应用进程的资源使用情况**,比如进程占用的 CPU、内存、磁盘 I/O、网络等。使用过多的系统资源导致应用程序响应缓慢或者错误数升高是一个最常见的性能问题。
**第二个,是应用程序之间调用情况**,比如调用频率、错误数、延时等。由于应用程序并不是孤立的,如果其依赖的其他应用出现了性能问题,应用自身性能也会受到影响。
**第三个,是应用程序内部核心逻辑的运行情况**,比如关键环节的耗时以及执行过程中的错误等。由于这是应用程序内部的状态,从外部通常无法直接获取到详细的性能数据。所以,应用程序在设计和开发时,就应该把这些指标提供出来,以便监控系统可以了解其内部运行状态。
有了应用进程的资源使用指标,你就可以把系统资源的瓶颈跟应用程序关联起来,从而迅速定位因系统资源不足而导致的性能问题;
<li>
有了应用程序之间的调用指标,你可以迅速分析出一个请求处理的调用链中,到底哪个组件才是导致性能问题的罪魁祸首;
</li>
<li>
而有了应用程序内部核心逻辑的运行性能,你就可以更进一步,直接进入应用程序的内部,定位到底是哪个处理环节的函数导致了性能问题。
</li>
基于这些思路,我相信你就可以构建出,描述应用程序运行状态的性能指标。再将这些指标纳入我们上一期提到的监控系统(比如 Prometheus + Grafana就可以跟系统监控一样一方面通过告警系统把问题及时汇报给相关团队处理另一方面通过直观的图形界面动态展示应用程序的整体性能。
除此之外,由于业务系统通常会涉及到一连串的多个服务,形成一个复杂的分布式调用链。为了迅速定位这类跨应用的性能瓶颈,你还可以使用 Zipkin、Jaeger、Pinpoint 等各类开源工具,来构建全链路跟踪系统。
比如,下图就是一个 Jaeger 调用链跟踪的示例。
<img src="https://static001.geekbang.org/resource/image/ab/d1/ab375bcb9883625a2b604b74fdc6d1d1.png" alt=""><br>
(图片来自 [Jaeger 文档](https://www.jaegertracing.io/docs/1.11/)
全链路跟踪可以帮你迅速定位出,在一个请求处理过程中,哪个环节才是问题根源。比如,从上图中,你就可以很容易看到,这是 Redis 超时导致的问题。
全链路跟踪除了可以帮你快速定位跨应用的性能问题外,还可以帮你生成线上系统的调用拓扑图。这些直观的拓扑图,在分析复杂系统(比如微服务)时尤其有效。
## 日志监控
性能指标的监控,可以让你迅速定位发生瓶颈的位置,不过只有指标的话往往还不够。比如,同样的一个接口,当请求传入的参数不同时,就可能会导致完全不同的性能问题。所以,除了指标外,我们还需要对这些指标的上下文信息进行监控,而日志正是这些上下文的最佳来源。
对比来看,
<li>
指标是特定时间段的数值型测量数据,通常以时间序列的方式处理,适合于实时监控。
</li>
<li>
而日志则完全不同,日志都是某个时间点的字符串消息,通常需要对搜索引擎进行索引后,才能进行查询和汇总分析。
</li>
对日志监控来说,最经典的方法,就是使用 ELK 技术栈,即使用 Elasticsearch、Logstash 和 Kibana 这三个组件的组合。
如下图所示,就是一个经典的 ELK 架构图:
<img src="https://static001.geekbang.org/resource/image/c2/05/c25cfacff4f937273964c8e9f0729405.png" alt=""><br>
(图片来自[elastic.co](https://www.elastic.co/elasticon/conf/2017/sf/sustainable-harvesting-how-a-few-geeks-learned-to-elastic-stack-logs)
这其中,
<li>
Logstash 负责对从各个日志源采集日志,然后进行预处理,最后再把初步处理过的日志,发送给 Elasticsearch 进行索引。
</li>
<li>
Elasticsearch 负责对日志进行索引,并提供了一个完整的全文搜索引擎,这样就可以方便你从日志中检索需要的数据。
</li>
<li>
Kibana 则负责对日志进行可视化分析,包括日志搜索、处理以及绚丽的仪表板展示等。
</li>
下面这张图,就是一个 Kibana 仪表板的示例,它直观展示了 Apache 的访问概况。
<img src="https://static001.geekbang.org/resource/image/69/e7/69e53058a70063f60ff793a2bd88f7e7.png" alt=""><br>
(图片来自[elastic.co](https://www.elastic.co/elasticon/conf/2017/sf/sustainable-harvesting-how-a-few-geeks-learned-to-elastic-stack-logs)
值得注意的是ELK 技术栈中的 Logstash 资源消耗比较大。所以,在资源紧张的环境中,我们往往使用资源消耗更低的 Fluentd来替代 Logstash也就是所谓的 EFK 技术栈)。
## 小结
今天,我为你梳理了应用程序监控的基本思路。应用程序的监控,可以分为指标监控和日志监控两大部分:
<li>
指标监控主要是对一定时间段内性能指标进行测量,然后再通过时间序列的方式,进行处理、存储和告警。
</li>
<li>
日志监控则可以提供更详细的上下文信息,通常通过 ELK 技术栈来进行收集、索引和图形化展示。
</li>
在跨多个不同应用的复杂业务场景中,你还可以构建全链路跟踪系统。这样可以动态跟踪调用链中各个组件的性能,生成整个流程的调用拓扑图,从而加快定位复杂应用的性能问题。
## 思考
最后,我想邀请你一起来聊聊,你是怎么监控应用程序的性能的。你通常会监控哪些应用程序的性能指标,又是如何搭建链路跟踪和日志监控系统,来定位应用瓶颈的?你可以结合我的讲述,总结自己的思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,158 @@
<audio id="audio" title="55 | 套路篇:分析性能问题的一般步骤" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/23/cb/23be92dc47377b7cdda1eea3870d5acb.mp3"></audio>
你好,我是倪朋飞。
上一节,我们一起学习了,应用程序监控的基本思路,先简单回顾一下。
应用程序的监控,可以分为指标监控和日志监控两大块。
<li>
指标监控,主要是对一定时间段内的性能指标进行测量,然后再通过时间序列的方式,进行处理、存储和告警。
</li>
<li>
而日志监控,则可以提供更详细的上下文信息,通常通过 ELK 技术栈,来进行收集、索引和图形化展示。
</li>
在跨多个不同应用的复杂业务场景中,你还可以构建全链路跟踪系统。这样,你就可以动态跟踪调用链中各个组件的性能,生成整个应用的调用拓扑图,从而加快定位复杂应用的性能问题。
不过,如果你收到监控系统的告警,发现系统资源或者应用程序出现性能瓶颈,又该如何进一步分析它的根源呢?今天,我就分别从系统资源瓶颈和应用程序瓶颈这两个角度,带你一起来看看,性能分析的一般步骤。
## 系统资源瓶颈
首先来看系统资源的瓶颈,这也是最为常见的性能问题。
在系统监控的综合思路篇中,我曾经介绍过,系统资源的瓶颈,可以通过 USE 法,即**使用率、饱和度以及错误数这三类指标来衡量**。系统的资源,可以分为硬件资源和软件资源两类。
<li>
如 CPU、内存、磁盘和文件系统以及网络等都是最常见的硬件资源。
</li>
<li>
而文件描述符数、连接跟踪数、套接字缓冲区大小等,则是典型的软件资源。
</li>
这样,在你收到监控系统告警时,就可以对照这些资源列表,再根据指标的不同来进行定位。
实际上,咱们专栏前四大模块的核心,正是学会去分析这些资源瓶颈导致的性能问题。所以,当你碰到了系统资源的性能瓶颈时,前面模块的所有思路、方法以及工具,都完全可以照用。
接下来,我就从 CPU 性能、内存性能、磁盘和文件系统 I/O 性能以及网络性能等四个方面,带你回顾一下它们的分析步骤。
### CPU性能分析
第一种最常见的系统资源是 CPU。关于 CPU 的性能分析方法,我在[如何迅速分析出系统CPU的瓶颈](https://time.geekbang.org/column/article/72685)中,已经为你整理了一个迅速分析 CPU 性能瓶颈的思路。
还记得这张图吗?利用 top、vmstat、pidstat、strace 以及 perf 等几个最常见的工具,获取 CPU 性能指标后,再结合进程与 CPU 的工作原理,就可以迅速定位出 CPU 性能瓶颈的来源。
<img src="https://static001.geekbang.org/resource/image/23/cd/238ee65ac4c8e32ef4f96fb0ba8cb0cd.png" alt="">
实际上top、pidstat、vmstat 这类工具所汇报的 CPU 性能指标,都源自 /proc 文件系统(比如/proc/loadavg、/proc/stat、/proc/softirqs 等)。这些指标,都应该通过监控系统监控起来。虽然并非所有指标都需要报警,但这些指标却可以加快性能问题的定位分析。
比如说,当你收到系统的用户 CPU 使用率过高告警时,从监控系统中直接查询到,导致 CPU 使用率过高的进程;然后再登录到进程所在的 Linux 服务器中,分析该进程的行为。
你可以使用 strace查看进程的系统调用汇总也可以使用 perf 等工具,找出进程的热点函数;甚至还可以使用动态追踪的方法,来观察进程的当前执行过程,直到确定瓶颈的根源。
### 内存性能分析
说完了 CPU 的性能分析,再来看看第二种系统资源,即内存。关于内存性能的分析方法,我在[如何“快准狠”找到系统内存的问题](https://time.geekbang.org/column/article/76460)中,也已经为你整理了一个快速分析的思路。
下面这张图,就是一个迅速定位内存瓶颈的流程。我们可以通过 free 和 vmstat 输出的性能指标,确认内存瓶颈;然后,再根据内存问题的类型,进一步分析内存的使用、分配、泄漏以及缓存等,最后找出问题的来源。
<img src="https://static001.geekbang.org/resource/image/29/98/292d64ac6bce0fe6a7a6b4250f34e998.png" alt="">
同 CPU 性能一样,很多内存的性能指标,也来源于 /proc 文件系统(比如 /proc/meminfo、/proc/slabinfo等它们也都应该通过监控系统监控起来。这样当你收到内存告警时就可以从监控系统中直接得到上图中的各项性能指标从而加快性能问题的定位过程。
比如说,当你收到内存不足的告警时,首先可以从监控系统中。找出占用内存最多的几个进程。然后,再根据这些进程的内存占用历史,观察是否存在内存泄漏问题。确定出最可疑的进程后,再登录到进程所在的 Linux 服务器中,分析该进程的内存空间或者内存分配,最后弄清楚进程为什么会占用大量内存。
### 磁盘和文件系统I/O性能分析
接下来,我们再来看第三种系统资源,即磁盘和文件系统的 I/O。关于磁盘和文件系统的 I/O 性能分析方法,我在[如何迅速分析出系统I/O的瓶颈](https://time.geekbang.org/column/article/79001)中也已经为你整理了一个快速分析的思路。
我们来看下面这张图。当你使用 iostat 发现磁盘I/O 存在性能瓶颈(比如 I/O 使用率过高、响应时间过长或者等待队列长度突然增大等)后,可以再通过 pidstat、 vmstat 等,确认 I/O 的来源。接着,再根据来源的不同,进一步分析文件系统和磁盘的使用率、缓存以及进程的 I/O 等,从而揪出 I/O 问题的真凶。
<img src="https://static001.geekbang.org/resource/image/e0/0b/e075287cff9b32ba3964746fdaf2960b.png" alt="">
同 CPU 和内存性能类似,很多磁盘和文件系统的性能指标,也来源于 /proc 和 /sys 文件系统(比如 /proc/diskstats、/sys/block/sda/stat 等)。自然,它们也应该通过监控系统监控起来。这样,当你收到 I/O 性能告警时,就可以从监控系统中,直接得到上图中的各项性能指标,从而加快性能定位的过程。
比如说,当你发现某块磁盘的 I/O 使用率为 100% 时,首先可以从监控系统中,找出 I/O 最多的进程。然后,再登录到进程所在的 Linux 服务器中,借助 strace、lsof、perf 等工具,分析该进程的 I/O 行为。最后,再结合应用程序的原理,找出大量 I/O 的原因。
### 网络性能分析
最后的网络性能,其实包含两类资源,即网络接口和内核资源。在[网络性能优化的几个思路](https://time.geekbang.org/column/article/83783)中,我也曾提到过,网络性能的分析,要从 Linux 网络协议栈的原理来切入。下面这张图,就是 Linux 网络协议栈的基本原理,包括应用层、套机字接口、传输层、网络层以及链路层等。
<img src="https://static001.geekbang.org/resource/image/a1/3f/a118911721f9b67ce9c83de15666753f.png" alt="">
而要分析网络的性能,自然也是要从这几个协议层入手,通过使用率、饱和度以及错误数这几类性能指标,观察是否存在性能问题。比如
<li>
在链路层,可以从网络接口的吞吐量、丢包、错误以及软中断和网络功能卸载等角度分析;
</li>
<li>
在网络层,可以从路由、分片、叠加网络等角度进行分析;
</li>
<li>
在传输层,可以从 TCP、UDP 的协议原理出发,从连接数、吞吐量、延迟、重传等角度进行分析;
</li>
<li>
在应用层,可以从应用层协议(如 HTTP 和 DNS、请求数QPS、套接字缓存等角度进行分析。
</li>
同前面几种资源类似,网络的性能指标也都来源于内核,包括 /proc 文件系统(如 /proc/net、网络接口以及conntrack等内核模块。这些指标同样需要被监控系统监控。这样当你收到网络告警时就可以从监控系统中查询这些协议层的各项性能指标从而更快定位出性能问题。
比如,当你收到网络不通的告警时,就可以从监控系统中,查找各个协议层的丢包指标,确认丢包所在的协议层。然后,从监控系统的数据中,确认网络带宽、缓冲区、连接跟踪数等软硬件,是否存在性能瓶颈。最后,再登录到发生问题的 Linux 服务器中,借助 netstat、tcpdump、bcc 等工具,分析网络的收发数据,并且结合内核中的网络选项以及 TCP 等网络协议的原理,找出问题的来源。
## 应用程序瓶颈
除了以上这些来自网络资源的瓶颈外,还有很多瓶颈,其实直接来自应用程序。比如,最典型的应用程序性能问题,就是吞吐量(并发请求数)下降、错误率升高以及响应时间增大。
不过,在我看来,这些应用程序性能问题虽然各种各样,但就其本质来源,实际上只有三种,也就是资源瓶颈、依赖服务瓶颈以及应用自身的瓶颈。
第一种资源瓶颈,其实还是指刚才提到的 CPU、内存、磁盘和文件系统 I/O、网络以及内核资源等各类软硬件资源出现了瓶颈从而导致应用程序的运行受限。对于这种情况我们就可以用前面系统资源瓶颈模块提到的各种方法来分析。
第二种依赖服务的瓶颈,也就是诸如数据库、分布式缓存、中间件等应用程序,直接或者间接调用的服务出现了性能问题,从而导致应用程序的响应变慢,或者错误率升高。这说白了就是跨应用的性能问题,使用全链路跟踪系统,就可以帮你快速定位这类问题的根源。
最后一种,应用程序自身的性能问题,包括了多线程处理不当、死锁、业务算法的复杂度过高等等。对于这类问题,在我们前面讲过的应用程序指标监控以及日志监控中,观察关键环节的耗时和内部执行过程中的错误,就可以帮你缩小问题的范围。
不过,由于这是应用程序内部的状态,外部通常不能直接获取详细的性能数据,所以就需要应用程序在设计和开发时,就提供出这些指标,以便监控系统可以了解应用程序的内部运行状态。
如果这些手段过后还是无法找出瓶颈,你还可以用系统资源模块提到的各类进程分析工具,来进行分析定位。比如:
<li>
你可以用 strace观察系统调用
</li>
<li>
使用 perf 和火焰图,分析热点函数;
</li>
<li>
甚至使用动态追踪技术,来分析进程的执行状态。
</li>
当然,系统资源和应用程序本来就是相互影响、相辅相成的一个整体。实际上,很多资源瓶颈,也是应用程序自身运行导致的。比如,进程的内存泄漏,会导致系统内存不足;进程过多的 I/O 请求,会拖慢整个系统的 I/O 请求等。
所以,很多情况下,资源瓶颈和应用自身瓶颈,其实都是同一个问题导致的,并不需要我们重复分析。
## 小结
今天,我带你从系统资源瓶颈和应用程序瓶颈这两个角度,梳理了性能问题分析的一般步骤。
从系统资源瓶颈的角度来说USE 法是最为有效的方法,即从使用率、饱和度以及错误数这三个方面,来分析 CPU、内存、磁盘和文件系统 I/O、网络以及内核资源限制等各类软硬件资源。关于这些资源的分析方法我也带你一起回顾了咱们专栏前面几大模块的分析套路。
从应用程序瓶颈的角度来说,我们可以把性能问题的来源,分为资源瓶颈、依赖服务瓶颈以及应用自身瓶颈这三类。
<li>
资源瓶颈跟系统资源瓶颈,本质是一样的。
</li>
<li>
依赖服务瓶颈,你可以使用全链路跟踪系统进行定位。
</li>
<li>
而应用自身的问题,你可以通过系统调用、热点函数,或者应用自身的指标监控以及日志监控等,进行分析定位。
</li>
值得注意的是,虽然我把瓶颈分为了系统和应用两个角度,但在实际运行时,这两者往往是相辅相成、相互影响的。**系统是应用的运行环境,系统的瓶颈会导致应用的性能下降;而应用的不合理设计,也会引发系统资源的瓶颈**。我们做性能分析,就是要结合应用程序和操作系统的原理,揪出引发问题的真凶。
## 思考
最后,我想邀请你一起来聊聊,你平时是怎么分析和定位性能问题的?有没有哪个印象深刻的经历可以跟我分享呢?你可以结合我的讲述,总结自己的思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,179 @@
<audio id="audio" title="56 | 套路篇:优化性能问题的一般方法" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ab/bf/abc94d214a1eb5f4c63ccdfae647f7bf.mp3"></audio>
你好,我是倪朋飞。
上一节,我带你一起梳理了,性能问题分析的一般步骤。先带你简单回顾一下。
我们可以从系统资源瓶颈和应用程序瓶颈,这两个角度来分析性能问题的根源。
从系统资源瓶颈的角度来说USE 法是最为有效的方法,即从使用率、饱和度以及错误数这三个方面,来分析 CPU、内存、磁盘和文件系统 I/O、网络以及内核资源限制等各类软硬件资源。至于这些资源的分析方法我也带你一起回顾了咱们专栏前面几大模块的分析套路。
从应用程序瓶颈的角度来说,可以把性能问题的来源,分为资源瓶颈、依赖服务瓶颈以及应用自身的瓶颈这三类。
<li>
资源瓶颈的分析思路,跟系统资源瓶颈是一样的。
</li>
<li>
依赖服务的瓶颈,可以使用全链路跟踪系统,进行快速定位。
</li>
<li>
而应用自身的问题,则可以通过系统调用、热点函数,或者应用自身的指标和日志等,进行分析定位。
</li>
当然,虽然系统和应用是两个不同的角度,但在实际运行时,它们往往相辅相成、相互影响。
<li>
系统是应用的运行环境,系统瓶颈会导致应用的性能下降。
</li>
<li>
而应用程序不合理的设计,也会引发系统资源的瓶颈。
</li>
我们做性能分析,就是要结合应用程序和操作系统的原理,揪出引发问题的“真凶“。
找到性能问题的来源后,整个优化工作其实也就完成了一大半,因为这些瓶颈为我们指明了优化的方向。不过,对于性能优化来说,又有哪些常见的方法呢?
今天,我就带你一起来看看,性能优化的一般方法。同上一节的性能分析一样,我们也可以从系统和应用程序,这两个不同的角度来进行性能优化。
## 系统优化
首先来看系统的优化。在上一节我曾经介绍过USE 法可以用来分析系统软硬件资源的瓶颈,那么,相对应的优化方法,当然也是从这些资源瓶颈入手。
实际上,咱们专栏的前四个模块,除了最核心的系统资源瓶颈分析之外,也已经包含了这些常见资源瓶颈的优化方法。
接下来,我就从 CPU 性能、内存性能、磁盘和文件系统 I/O 性能以及网络性能等四个方面,带你回顾一下它们的优化方法。
### CPU 优化
首先来看 CPU 性能的优化方法。在[CPU 性能优化的几个思路](https://time.geekbang.org/column/article/73151)中,我曾经介绍过,**CPU 性能优化的核心,在于排除所有不必要的工作、充分利用 CPU 缓存并减少进程调度对性能的影响。**
从这几个方面出发,我相信你已经想到了很多的优化方法。这里,我主要强调一下,最典型的三种优化方法。
<li>
第一种,把进程绑定到一个或者多个 CPU 上,充分利用 CPU 缓存的本地性,并减少进程间的相互影响。
</li>
<li>
第二种,为中断处理程序开启多 CPU 负载均衡,以便在发生大量中断时,可以充分利用多 CPU 的优势分摊负载。
</li>
<li>
第三种,使用 Cgroups 等方法,为进程设置资源限制,避免个别进程消耗过多的 CPU。同时为核心应用程序设置更高的优先级减少低优先级任务的影响。
</li>
### 内存优化
说完了 CPU 的性能优化,我们再来看看,怎么优化内存的性能。在[如何“快准狠”找到系统内存的问题](https://time.geekbang.org/column/article/76460)中我曾经为你梳理了常见的一些内存问题比如可用内存不足、内存泄漏、Swap 过多、缺页异常过多以及缓存过多等等。所以,说白了,内存性能的优化,也就是要解决这些内存使用的问题。
在我看来,你可以通过以下几种方法,来优化内存的性能。
<li>
第一种除非有必要Swap 应该禁止掉。这样就可以避免 Swap 的额外 I/O ,带来内存访问变慢的问题。
</li>
<li>
第二种,使用 Cgroups 等方法,为进程设置内存限制。这样就可以避免个别进程消耗过多内存,而影响了其他进程。对于核心应用,还应该降低 oom_score避免被 OOM 杀死。
</li>
<li>
第三种,使用大页、内存池等方法,减少内存的动态分配,从而减少缺页异常。
</li>
### 磁盘和文件系统I/O优化
接下来,我们再来看第三类系统资源,即磁盘和文件系统 I/O 的优化方法。在[磁盘 I/O 性能优化的几个思路](https://time.geekbang.org/column/article/79368) 中,我已经为你梳理了一些常见的优化思路,这其中有三种最典型的方法。
<li>
第一种,也是最简单的方法,通过 SSD 替代 HDD、或者使用 RAID 等方法提升I/O性能。
</li>
<li>
第二种,针对磁盘和应用程序 I/O 模式的特征,选择最适合的 I/O 调度算法。比如SSD 和虚拟机中的磁盘,通常用的是 noop 调度算法;而数据库应用,更推荐使用 deadline 算法。
</li>
<li>
第三,优化文件系统和磁盘的缓存、缓冲区,比如优化脏页的刷新频率、脏页限额,以及内核回收目录项缓存和索引节点缓存的倾向等等。
</li>
除此之外,使用不同磁盘隔离不同应用的数据、优化文件系统的配置选项、优化磁盘预读、增大磁盘队列长度等,也都是常用的优化思路。
### 网络优化
最后一个是网络的性能优化。在[网络性能优化的几个思路](https://time.geekbang.org/column/article/83783)中,我也已经为你梳理了一些常见的优化思路。这些优化方法都是从 Linux 的网络协议栈出发,针对每个协议层的工作原理进行优化。这里,我同样强调一下,最典型的几种网络优化方法。
首先,从内核资源和网络协议的角度来说,我们可以对内核选项进行优化,比如:
<li>
你可以增大套接字缓冲区、连接跟踪表、最大半连接数、最大文件描述符数、本地端口范围等内核资源配额;
</li>
<li>
也可以减少 TIMEOUT 超时时间、SYN+ACK 重传数、Keepalive 探测时间等异常处理参数;
</li>
<li>
还可以开启端口复用、反向地址校验,并调整 MTU 大小等降低内核的负担。
</li>
这些都是内核选项优化的最常见措施。
其次,从网络接口的角度来说,我们可以考虑对网络接口的功能进行优化,比如:
<li>
你可以将原来 CPU 上执行的工作,卸载到网卡中执行,即开启网卡的 GRO、GSO、RSS、VXLAN 等卸载功能;
</li>
<li>
也可以开启网络接口的多队列功能,这样,每个队列就可以用不同的中断号,调度到不同 CPU 上执行;
</li>
<li>
还可以增大网络接口的缓冲区大小以及队列长度等,提升网络传输的吞吐量。
</li>
最后,在极限性能情况(比如 C10M内核的网络协议栈可能是最主要的性能瓶颈所以一般会考虑绕过内核协议栈。
<li>
你可以使用 DPDK 技术跳过内核协议栈直接由用户态进程用轮询的方式来处理网络请求。同时再结合大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。
</li>
<li>
你还可以使用内核自带的 XDP 技术,在网络包进入内核协议栈前,就对其进行处理。这样,也可以达到目的,获得很好的性能。
</li>
## 应用程序优化
说完了系统软硬件资源的优化,接下来,我们再来看看应用程序的优化思路。
虽然系统的软硬件资源,是保证应用程序正常运行的基础,但你要知道,**性能优化的最佳位置,还是应用程序内部**。为什么这么说呢?我简单举两个例子你就明白了。
第一个例子,是系统 CPU 使用率sys%过高的问题。有时候出现问题虽然表面现象是系统CPU 使用率过高,但待你分析过后,很可能会发现,应用程序的不合理系统调用才是罪魁祸首。这种情况下,优化应用程序内部系统调用的逻辑,显然要比优化内核要简单也有用得多。
再比如说,数据库的 CPU 使用率高、I/O 响应慢,也是最常见的一种性能问题。这种问题,一般来说,并不是因为数据库本身性能不好,而是应用程序不合理的表结构或者 SQL 查询语句导致的。这时候,优化应用程序中数据库表结构的逻辑或者 SQL 语句,显然要比优化数据库本身,能带来更大的收益。
所以,在观察性能指标时,你应该先查看**应用程序的响应时间、吞吐量以及错误率**等指标,因为它们才是性能优化要解决的终极问题。以终为始,从这些角度出发,你一定能想到很多优化方法,而我比较推荐下面几种方法。
<li>
第一,从 CPU 使用的角度来说,简化代码、优化算法、异步处理以及编译器优化等,都是常用的降低 CPU 使用率的方法,这样可以利用有限的 CPU处理更多的请求。
</li>
<li>
第二,从数据访问的角度来说,使用缓存、写时复制、增加 I/O 尺寸等,都是常用的减少磁盘 I/O 的方法,这样可以获得更快的数据处理速度。
</li>
<li>
第三,从内存管理的角度来说,使用大页、内存池等方法,可以预先分配内存,减少内存的动态分配,从而更好地内存访问性能。
</li>
<li>
第四,从网络的角度来说,使用 I/O 多路复用、长连接代替短连接、DNS 缓存等方法,可以优化网络 I/O 并减少网络请求数,从而减少网络延时带来的性能问题。
</li>
<li>
第五,从进程的工作模型来说,异步处理、多线程或多进程等,可以充分利用每一个 CPU 的处理能力,从而提高应用程序的吞吐能力。
</li>
除此之外你还可以使用消息队列、CDN、负载均衡等各种方法来优化应用程序的架构将原来单机要承担的任务调度到多台服务器中并行处理。这样也往往能获得更好的整体性能。
## 小结
今天,我带你一起,从系统和应用程序这两个角度,梳理了常见的性能优化方法。
从系统的角度来说CPU、内存、磁盘和文件系统 I/O、网络以及内核数据结构等各类软硬件资源为应用程序提供了运行的环境也是我们性能优化的重点对象。你可以参考咱们专栏前面四个模块的优化篇优化这些资源。
从应用程序的角度来说,降低 CPU 使用,减少数据访问和网络 I/O使用缓存、异步处理以及多进程多线程等都是常用的性能优化方法。除了这些单机优化方法调整应用程序的架构或是利用水平扩展将任务调度到多台服务器中并行处理也是常用的优化思路。
虽然性能优化的方法很多,不过,我还是那句话,一定要避免过早优化。性能优化往往会提高复杂性,这一方面降低了可维护性,另一方面也为适应复杂多变的新需求带来障碍。
所以,性能优化最好是逐步完善,动态进行;不追求一步到位,而要首先保证,能满足当前的性能要求。发现性能不满足要求或者出现性能瓶颈后,再根据性能分析的结果,选择最重要的性能问题进行优化。
## 思考
最后,我想邀请你一起来聊聊,当碰到性能问题后,你是怎么进行优化的?有没有哪个印象深刻的经历可以跟我分享呢?你可以结合我的讲述,总结自己的思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,132 @@
<audio id="audio" title="57 | 套路篇Linux 性能工具速查" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5e/c2/5ed68a32cd1313aaba8506babfc9cbc2.mp3"></audio>
你好,我是倪朋飞。
上一节,我带你一起梳理了常见的性能优化思路,先简单回顾一下。
我们可以从系统和应用程序两个角度,来进行性能优化。
<li>
从系统的角度来说,主要是对 CPU、内存、网络、磁盘 I/O 以及内核软件资源等进行优化。
</li>
<li>
而从应用程序的角度来说,主要是简化代码、降低 CPU 使用、减少网络请求和磁盘 I/O并借助缓存、异步处理、多进程和多线程等提高应用程序的吞吐能力。
</li>
性能优化最好逐步完善,动态进行。不要追求一步到位,而要首先保证能满足当前的性能要求。性能优化通常意味着复杂度的提升,也意味着可维护性的降低。
如果你发现单机的性能调优带来过高复杂度,一定不要沉迷于单机的极限性能,而要从软件架构的角度,以水平扩展的方法来提升性能。
工欲善其事,必先利其器。我们知道,在性能分析和优化时,借助合适的性能工具,可以让整个过程事半功倍。你还记得有哪些常用的性能工具吗?今天,我就带你一起梳理一下常用的性能工具,以便你在需要时,可以迅速找到自己想要的。
## 性能工具速查
在梳理性能工具之前,首先给你提一个问题,那就是,在什么情况下,我们才需要去查找、挑选性能工具呢?你可以先自己想一下,再继续下面的内容。
其实在我看来,只有当你想了解某个性能指标,却不知道该怎么办的时候,才会想到,“要是有一个性能工具速查表就好了”这个问题。如果已知一个性能工具可用,我们更多会去查看这个工具的手册,找出它的功能、用法以及注意事项。
关于工具手册的查看man 应该是我们最熟悉的方法,我在专栏中多次介绍过。实际上,除了 man 之外,还有另外一个查询命令手册的方法,也就是 info。
info 可以理解为 man 的详细版本提供了诸如节点跳转等更强大的功能。相对来说man 的输出比较简洁,而 info 的输出更详细。所以,我们通常使用 man 来查询工具的使用方法只有在man 的输出不太好理解时,才会再去参考 info 文档。
当然,我说过了,要查询手册,前提一定是已知哪个工具可用。如果你还不知道要用哪个工具,就要根据想了解的指标,去查找有哪些工具可用。这其中:
<li>
有些工具不需要额外安装,就可以直接使用,比如内核的 /proc 文件系统;
</li>
<li>
而有些工具,则需要安装额外的软件包,比如 sar、pidstat、iostat 等。
</li>
**所以,在选择性能工具时,除了要考虑性能指标这个目的外,还要结合待分析的环境来综合考虑**。比如,实际环境是否允许安装软件包,是否需要新的内核版本等。
明白了工具选择的基本原则后,我们来看 Linux 的性能工具。首先还是要推荐下面这张图也就是Brendan Gregg 整理的性能工具谱图。我在专栏中多次提到过,你肯定也已经参考过。<br>
<img src="https://static001.geekbang.org/resource/image/b0/01/b07ca95ef8a3d2c89b0996a042d33901.png" alt=""><br>
(图片来自 [brendangregg.com](http://www.brendangregg.com/linuxperf.html)
这张图从 Linux 内核的各个子系统出发,汇总了对各个子系统进行性能分析时,你可以选择的工具。不过,虽然这个图是性能分析最好的参考资料之一,它其实还不够具体。
比如,当你需要查看某个性能指标时,这张图里对应的子系统部分,可能有多个性能工具可供选择。但实际上,并非所有这些工具都适用,具体要用哪个,还需要你去查找每个工具的手册,对比分析做出选择。
那么,有没有更好的方法来理解这些工具呢?**我的建议,还是从性能指标出发,根据性能指标的不同,将性能工具划分为不同类型**。比如,最常见的就是可以根据 CPU、内存、磁盘 I/O 以及网络的各类性能指标,将这些工具进行分类。
接下来,我就从 CPU、内存、磁盘 I/O 以及网络等几个角度,梳理这些常见的 Linux 性能工具,特别是从性能指标的角度出发,理清楚到底有哪些工具,可以用来监测特定的性能指标。这些工具,实际上贯穿在我们专栏各模块的各个案例中。为了方便你查看,我将它们都整理成了表格,并增加了每个工具的使用场景。
## CPU性能工具
首先,从 CPU 的角度来说,主要的性能指标就是 CPU 的使用率、上下文切换以及 CPU Cache 的命中率等。下面这张图就列出了常见的 CPU 性能指标。<br>
<img src="https://static001.geekbang.org/resource/image/9a/69/9a211905538faffb5b3221ee01776a69.png" alt=""><br>
从这些指标出发,再把 CPU 使用率,划分为系统和进程两个维度,我们就可以得到,下面这个 CPU 性能工具速查表。注意,因为每种性能指标都可能对应多种工具,我在每个指标的说明中,都帮你总结了这些工具的特点和注意事项。这些也是你需要特别关注的地方。<br>
<img src="https://static001.geekbang.org/resource/image/28/b0/28cb85011289f83804c51c1fb275dab0.png" alt="">
## 内存性能工具
接着我们来看内存方面。从内存的角度来说,主要的性能指标,就是系统内存的分配和使用、进程内存的分配和使用以及 SWAP 的用量。下面这张图列出了常见的内存性能指标。<br>
<img src="https://static001.geekbang.org/resource/image/ee/c0/ee36f73b9213063b3bcdaed2245944c0.png" alt=""><br>
从这些指标出发,我们就可以得到如下表所示的内存性能工具速查表。同 CPU 性能工具一样,这儿我也帮你梳理了,常见工具的特点和注意事项。<br>
<img src="https://static001.geekbang.org/resource/image/79/f8/79ad5caf0a2c105b7e9ce77877d493f8.png" alt=""><br>
最后一行pcstat的源码链接为 [https://github.com/tobert/pcstat](https://github.com/tobert/pcstat)
## 磁盘I/O性能工具
接下来,从文件系统和磁盘 I/O 的角度来说,主要性能指标,就是文件系统的使用、缓存和缓冲区的使用,以及磁盘 I/O 的使用率、吞吐量和延迟等。下面这张图列出了常见的 I/O 性能指标。<br>
<img src="https://static001.geekbang.org/resource/image/72/3b/723431a944034b51a9ef13a8a1d4d03b.png" alt=""><br>
从这些指标出发,我们就可以得到,下面这个文件系统和磁盘 I/O 性能工具速查表。同 CPU和内存性能工具一样我也梳理出了这些工具的特点和注意事项。<br>
<img src="https://static001.geekbang.org/resource/image/c2/a3/c232dcb4185f7b7ba95c126889cf6fa3.png" alt="">
## 网络性能工具
最后,从网络的角度来说,主要性能指标就是吞吐量、响应时间、连接数、丢包数等。根据 TCP/IP 网络协议栈的原理,我们可以把这些性能指标,进一步细化为每层协议的具体指标。这里我同样用一张图,分别从链路层、网络层、传输层和应用层,列出了各层的主要指标。<br>
<img src="https://static001.geekbang.org/resource/image/37/a4/37d04c213acfa650bd7467e3000356a4.png" alt=""><br>
从这些指标出发,我们就可以得到下面的网络性能工具速查表。同样的,我也帮你梳理了各种工具的特点和注意事项。<br>
<img src="https://static001.geekbang.org/resource/image/5d/5d/5dde213baffd7811ab73c82883b2a75d.png" alt="">
## 基准测试工具
除了性能分析外,很多时候,我们还需要对系统性能进行基准测试。比如,
<li>
在文件系统和磁盘 I/O 模块中,我们使用 fio 工具,测试了磁盘 I/O 的性能。
</li>
<li>
在网络模块中,我们使用 iperf、pktgen 等,测试了网络的性能。
</li>
<li>
而在很多基于 Nginx 的案例中,我们则使用 ab、wrk 等,测试 Nginx 应用的性能。
</li>
除了专栏里介绍过的这些工具外,对于 Linux 的各个子系统来说,还有很多其他的基准测试工具可能会用到。下面这张图,是 Brendan Gregg 整理的 Linux 基准测试工具图谱,你可以保存下来,在需要时参考。<br>
<img src="https://static001.geekbang.org/resource/image/f0/e9/f094f489049602e1058e02edc708e6e9.png" alt=""><br>
(图片来自 [brendangregg.com](http://www.brendangregg.com/linuxperf.html)
## 小结
今天,我们一起梳理了常见的性能工具,并从 CPU、内存、文件系统和磁盘 I/O、网络以及基准测试等不同的角度汇总了各类性能指标所对应的性能工具速查表。
当分析性能问题时,大的来说,主要有这么两个步骤:
<li>
第一步,从性能瓶颈出发,根据系统和应用程序的运行原理,确认待分析的性能指标。
</li>
<li>
第二步,根据这些图表,选出最合适的性能工具,然后了解并使用工具,从而更快观测到需要的性能数据。
</li>
虽然 Linux 的性能指标和性能工具都比较多,但熟悉了各指标含义后,你自然就会发现这些工具同性能指标间的关联。顺着这个思路往下走,掌握这些工具的选用其实并不难。
当然,正如咱们专栏一直强调的,不要把性能工具当成性能分析和优化的全部。
<li>
一方面,性能分析和优化的核心,是对系统和应用程序运行原理的掌握,而性能工具只是辅助你更快完成这个过程的帮手。
</li>
<li>
另一方面,完善的监控系统,可以提供绝大部分性能分析所需的基准数据。从这些数据中,你很可能就能大致定位出性能瓶颈,也就不用再去手动执行各类工具了。
</li>
## 思考
最后,我想邀请你一起来聊聊,你都使用过哪些性能工具。你通常是怎么选择性能工具的?又是如何想到要用这些性能工具,来排查和分析性能问题的?你可以结合我的讲述,总结自己的思路。
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,113 @@
<audio id="audio" title="58 | 答疑(六):容器冷启动如何性能分析?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6d/81/6de71d147a37e4b45d568952460f1481.mp3"></audio>
你好,我是倪朋飞。
专栏更新至今,咱们专栏最后一部分——综合案例模块也要告一段落了。很高兴看到你没有掉队,仍然在积极学习思考、实践操作,并热情地分享你在实际环境中,遇到过的各种性能问题的分析思路以及优化方法。
今天是性能优化答疑的第六期。照例,我从综合案例模块的留言中,摘出了一些典型问题,作为今天的答疑内容,集中回复。为了便于你学习理解,它们并不是严格按照文章顺序排列的。每个问题,我都附上了留言区提问的截屏。如果你需要回顾内容原文,可以扫描每个问题右下方的二维码查看。
## 问题1容器冷启动性能分析
<img src="https://static001.geekbang.org/resource/image/be/0c/be00340409bdc188fe7e807996c7e70c.png" alt="">
在[为什么应用容器化后,启动慢了很多](https://time.geekbang.org/column/article/84953)中我们一起分析了容器化所导致的应用程序启动缓慢的问题。简单回顾一下当时的案例Docker 通过 Cgroups 给容器设置了内存限制,但是容器并未意识到 ,所以还是分配了过多内存,导致被系统 OOM 杀死。
这个案例的根源实际上比较简单Tony 同学就此提了一个更深入的问题。
我们知道,容器为应用程序的管理带来了巨大的便捷,诸如 Serverless只关注应用的运行而无需关注服务器、FaaSFunction as a Service等新型的软件架构也都基于容器技术来构建。不过虽然容器启动已经很快了但在启动新容器也就是冷启动的时候启动时间相对于应用程序的性能要求来说还是过长了。
那么,应该怎么来分析和优化冷启动的性能呢?
这个问题最核心的一点,其实就是要弄清楚,启动时间到底都花在哪儿了。一般来说,一个 Serverless 服务的启动,包括:
<li>
事件触发比如收到新的HTTP调用请求
</li>
<li>
资源调度;
</li>
<li>
镜像拉取;
</li>
<li>
网络配置;
</li>
<li>
启动应用等几个过程。
</li>
这几个过程所消耗的时间,都可以通过链路跟踪的方式来监控,进而就可以定位出耗时最多的一个或者多个流程。
紧接着,针对耗时最多的流程,我们可以通过应用程序监控或者动态追踪的方法,定位出耗时最多的字模块,这样也就找出了要优化的瓶颈点。
比如,镜像拉取流程,可以通过缓存热点镜像来减少镜像拉取时间;网络配置流程,可以通过网络资源预分配进行加速;而资源调度和容器启动,也可以通过复用预先创建好的容器来进行优化。
## 问题2CPU火焰图和内存火焰图有什么不同
<img src="https://static001.geekbang.org/resource/image/90/38/90a871e0dad35f71f80f1efdc9be5538.png" alt="">
在[内核线程 CPU 利用率过高的案例](https://time.geekbang.org/column/article/86330)中,我们一起通过 perf 和火焰图工具,生成了内核热点函数调用栈的动态矢量图,并定位出性能问题发生时,执行最为频繁的内核函数。
由于案例分析中,我们主要关注的是 CPU 的繁忙情况,所以这时候生成的火焰图,被称为 on-CPU 火焰图。事实上,除此之外,还有 off-CPU、内存等不同的火焰图分别表示 CPU 的阻塞和内存的分配释放情况。
所以李逍遥同学提了出一个很好的问题同样都是火焰图CPU 火焰图和内存火焰图,在生成数据时到底有什么不同?
这个问题恰好问到了最核心的点上。CPU 火焰图和内存火焰图,最大的差别其实就是数据来源的不同,也就是函数堆栈不同,而火焰图的格式还是完全一样的。
<li>
对 CPU 火焰图来说,采集的数据主要是消耗 CPU 的函数;
</li>
<li>
而对内存火焰图来说,采集的数据主要是内存分配、释放、换页等内存管理函数。
</li>
举个例子,我们在使用 perf record 时,默认的采集事件 cpu-cycles ,就是采集 on-CPU 数据,而生成的火焰图就是 CPU 火焰图。通过 perf record -e page-fault 将采集事件换成 page-fault 后,就可以采集内存缺页的数据,生成的火焰图自然就成了内存火焰图。
## 问题3perf probe失败怎么办
<img src="https://static001.geekbang.org/resource/image/45/27/45e3bd4566d77fa9c5677b7ea9a82827.png" alt="">
在[动态追踪怎么用](https://time.geekbang.org/column/article/86710)中,我们一起通过几个案例,学习了 perf、bcc 等动态追踪工具的使用方法。这些动态追踪方法,可以在不修改代码、不重启服务的情况下,让你动态了解应用程序或内核的执行过程。这对于排查情况复杂、难复现的问题尤其有效。
在使用动态追踪工具时,由于十六进制格式的函数地址并不容易理解,就需要我们借助调试信息,将它们转换为更直观的函数名。对于内核来说,我已经多次提到过,需要安装 debuginfo。不过针对应用程序又该怎么办呢
这里其实有两种方法。
第一种方法,假如应用程序提供了调试信息软件包,那你就可以直接安装来使用。比如,对于我们案例中的 bash 来说,就可以通过下面的命令,来安装它的调试信息:
```
# Ubuntu
apt-get install -y bash-dbgsym
# Centos
debuginfo-install bash
```
第二种方法,使用源码重新编译应用程序,并开启编译器的调试信息开关,比如可以为 gcc 增加 -g 选项。
## 问题4RED法监控微服务应用
<img src="https://static001.geekbang.org/resource/image/31/30/310b2bd6af3628b3fd3c85cd83a34530.png" alt="">
在[系统监控的综合思路](https://time.geekbang.org/column/article/87980)中,我为你介绍了监控系统资源性能时常用的 USE 法。USE 法把系统资源的性能指标,简化成了三类:使用率、饱和度以及错误数。三者之中任一类别的指标过高时,都代表相应的系统资源可能有性能瓶颈。
不过,对应用程序的监控来说,这些指标显然就不合适了。因为应用程序的核心指标,是请求数、错误数和响应时间。那该怎么办呢?这其实,正是 Adam 同学在留言中提到的 RED 方法。
RED 方法,是 Weave Cloud 在监控微服务性能时,结合 Prometheus 监控所提出的一种监控思路——即对微服务来说监控它们的请求数Rate、错误数Errors以及响应时间Duration。所以RED 方法适用于微服务应用的监控,而 USE 方法适用于系统资源的监控。
## 问题5深入内核的方法
<img src="https://static001.geekbang.org/resource/image/73/13/73bdcda4daf3da8b1706ed480bffe413.png" alt=""><img src="https://static001.geekbang.org/resource/image/fe/98/fedc0101abec096969b021a7306de798.png" alt="">
在定位性能问题时,我们通过 perf、ebpf、systemtap 等各种方法排查时很可能会发现问题的热点在内核中的某个函数中。而青石和xfan的问题就是如何去了解、深入 Linux 内核的原理,特别是想弄清楚,性能工具展示的内核函数到底是什么含义。
其实,要了解内核函数的含义,最好的方法,就是去查询所用内核版本的源代码。这里,我推荐 [https://elixir.bootlin.com](https://elixir.bootlin.com) 这个网站。使用方法也很简单,从左边选择内核版本,再通过内核函数名称去搜索就可以了。
之所以推荐这个网站,是因为它不仅可以让你快速搜索函数定位,还为所有的函数、变量、宏定义等,都提供了快速跳转的功能。这样,当你看到不明白的函数或变量时,点击就可以跳转到相应的定义处。
此外,对于 eBPF来说除了可以通过内核源码来了解我更推荐你从 [BPF Compiler Collection (BCC)](https://github.com/iovisor/bcc) 这个项目开始。BCC 提供了很多短小的示例,可以帮你快速了解 eBPF 的工作原理,并熟悉 eBPF 程序的开发思路。了解这些基本的用法后,再去深入 eBPF 的内部,就会轻松很多。
今天我主要回答这些问题,同时也欢迎你继续在留言区写下疑问和感想,我会持续不断地在留言区跟你交流。希望借助每一次的答疑和交流,可以和你一起,把专栏中的各种知识转化为你的能力。