mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 14:13:46 +08:00
mod
This commit is contained in:
124
极客时间专栏/Linux内核技术实战课/内核态CPU利用率飙高问题/17 基础篇 | CPU是如何执行任务的?.md
Normal file
124
极客时间专栏/Linux内核技术实战课/内核态CPU利用率飙高问题/17 基础篇 | CPU是如何执行任务的?.md
Normal file
@@ -0,0 +1,124 @@
|
||||
<audio id="audio" title="17 基础篇 | CPU是如何执行任务的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8c/fc/8cb9696ba6fde7d76b15e3eb0466d7fc.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
如果你做过性能优化的话,你应该有过这些思考,比如说:
|
||||
|
||||
- 如何让CPU读取数据更快一些?
|
||||
- 同样的任务,为什么有时候执行得快,有时候执行得慢?
|
||||
- 我的任务有些比较重要,CPU如果有争抢时,我希望可以先执行这些任务,这该怎么办呢?
|
||||
- 多线程并行读写数据是如何保障同步的?
|
||||
- …
|
||||
|
||||
要想明白这些问题,你就需要去了解CPU是如何执行任务的,只有明白了CPU的执行逻辑,你才能更好地控制你的任务执行,从而获得更好的性能。
|
||||
|
||||
## CPU是如何读写数据的 ?
|
||||
|
||||
我先带你来看下CPU的架构,因为你只有理解了CPU的架构,你才能更好地理解CPU是如何执行指令的。CPU的架构图如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a4/7f/a418fbfc23d96aeb4813f1db4cbyy17f.jpg" alt="" title="CPU架构">
|
||||
|
||||
你可以直观地看到,对于现代处理器而言,一个实体CPU通常会有两个逻辑线程,也就是上图中的Core 0和Core 1。每个Core都有自己的L1 Cache,L1 Cache又分为dCache和iCache,对应到上图就是L1d和L1i。L1 Cache只有Core本身可以看到,其他的Core是看不到的。同一个实体CPU中的这两个Core会共享L2 Cache,其他的实体CPU是看不到这个L2 Cache的。所有的实体CPU会共享L3 Cache。这就是典型的CPU架构。
|
||||
|
||||
相信你也看到,在CPU外还会有内存(DRAM)、磁盘等,这些存储介质共同构成了体系结构里的金字塔存储层次。如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6e/3d/6eace3466bc42185887a351c6c3e693d.jpg" alt="" title="金字塔存储层次">
|
||||
|
||||
在这个“金字塔”中,越往下,存储容量就越大,它的速度也会变得越慢。Jeff Dean曾经研究过CPU对各个存储介质的访问延迟,具体你可以看下[latency](https://gist.github.com/jboner/2841832)里的数据,里面详细记录了每个存储层次的访问延迟,这也是我们在性能优化时必须要知道的一些延迟数据。你可不要小瞧它,在某些场景下,这些不同存储层次的访问延迟差异可能会导致非常大的性能差异。
|
||||
|
||||
我们就以Cache访问延迟(L1 0.5ns,L2 10ns)和内存访问延迟(100ns)为例,我给你举一个实际的案例来说明访问延迟的差异对性能的影响。
|
||||
|
||||
之前我在做网络追踪系统时,为了更方便地追踪TCP连接,我给Linux Kernel提交了一个PATCH来记录每个连接的编号,具体你可以参考这个commit:[net: init sk_cookie for inet socket](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc4&id=c6849a3ac17e336811f1d5bba991d2a9bdc47af1)。该PATCH的大致作用是,在每次创建一个新的TCP连接时(关于TCP这部分知识,你可以去温习上一个模块的内容),它都会使用net namespace(网络命名空间)中的cookie_gen生成一个cookie给这个新建的连接赋值。
|
||||
|
||||
可是呢,在这个PATCH被合入后,Google工程师Eric Dumazet发现在他的SYN Flood测试中网络吞吐量会下降约24%。后来经过分析发现,这是因为net namespace中所有TCP连接都在共享cookie_gen。在高并发情况下,瞬间会有非常多的新建TCP连接,这时候cookie_gen就成了一个非常热的数据,从而被缓存在Cache中。如果cookie_gen的内容被修改的话,Cache里的数据就会失效,那么当有其他新建连接需要读取这个数据时,就不得不再次从内存中去读取。而你知道,内存的延迟相比Cache的延迟是大很多的,这就导致了严重的性能下降。这个问题就是典型的False Sharing,也就是Cache伪共享问题。
|
||||
|
||||
正因为这个PATCH给高并发建连这种场景带来了如此严重的性能损耗,所以它就被我们给回退(Revert)了,你具体可以看[Revert "net: init sk_cookie for inet socket"](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc4&id=a06ac0d67d9fda7c255476c6391032319030045d)这个commit。不过,cookie_gen对于网络追踪还是很有用的,比如说在使用ebpf来追踪cgroup的TCP连接时,所以后来Facebook的一个工程师把它从net namespace这个结构体里移了出来,[改为了一个全局变量](https://github.com/torvalds/linux/commit/cd48bdda4fb82c2fe569d97af4217c530168c99c)。
|
||||
|
||||
由于net namespace被很多TCP连接共享,因此这个结构体非常容易产生这类Cache伪共享问题,Eric Dumazet也在这里引入过一个Cache伪共享问题:[net: reorder ‘struct net’ fields to avoid false sharing](https://github.com/torvalds/linux/commit/2a06b8982f8f2f40d03a3daf634676386bd84dbc)。
|
||||
|
||||
接下来,我们就来看一下Cache伪共享问题究竟是怎么回事。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ed/9f/ed552cedfb95d0a3af920eca78c3069f.jpg" alt="" title="Cache Line False Sharing">
|
||||
|
||||
如上图所示,两个CPU上并行运行着两个不同线程,它们同时从内存中读取两个不同的数据,这两个数据的地址在物理内存上是连续的,它们位于同一个Cache Line中。CPU从内存中读数据到Cache是以Cache Line为单位的,所以该Cache Line里的数据被同时读入到了这两个CPU的各自Cache中。紧接着这两个线程分别改写不同的数据,每次改写Cache中的数据都会将整个Cache Line置为无效。因此,虽然这两个线程改写的数据不同,但是由于它们位于同一个Cache Line中,所以一个CPU中的线程在写数据时会导致另外一个CPU中的Cache Line失效,而另外一个CPU中的线程在读写数据时就会发生cache miss,然后去内存读数据,这就大大降低了性能。
|
||||
|
||||
Cache伪共享问题可以说是性能杀手,我们在写代码时一定要留意那些频繁改写的共享数据,必要的时候可以将它跟其他的热数据放在不同的Cache Line中避免伪共享问题,就像我们在内核代码里经常看到的____cacheline_aligned所做的那样。
|
||||
|
||||
那怎么来观测Cache伪共享问题呢?你可以使用[perf c2c](https://man7.org/linux/man-pages/man1/perf-c2c.1.html)这个命令,但是这需要较新版本内核支持才可以。不过,perf同样可以观察cache miss的现象,它对很多性能问题的分析还是很有帮助的。
|
||||
|
||||
CPU在写完Cache后将Cache置为无效(invalidate), 这本质上是为了保障多核并行计算时的数据一致性,一致性问题是Cache这个存储层次很典型的问题。
|
||||
|
||||
我们再来看内存这个存储层次中的典型问题:并行计算时的竞争,即两个CPU同时去操作同一个物理内存地址时的竞争。关于这类问题,我举一些简单的例子给你说明一下。
|
||||
|
||||
以C语言为例:
|
||||
|
||||
```
|
||||
struct foo {
|
||||
int a;
|
||||
int b;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
在这段示例代码里,我们定义了一个结构体,该结构体里的两个成员a和b在地址上是连续的。如果CPU 0去写a,同时CPU 1去读b的话,此时不会有竞争,因为a和b是不同的地址。不过,a和b由于在地址上是连续的,它们可能会位于同一个Cache Line中,所以为了防止前面提到的Cache伪共享问题,我们可以强制将b的地址设置为Cache Line对齐地址,如下:
|
||||
|
||||
```
|
||||
struct foo {
|
||||
int a;
|
||||
int b ____cacheline_aligned;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
接下来,我们看下另外一种情况:
|
||||
|
||||
```
|
||||
struct foo {
|
||||
int a:1;
|
||||
int b:1;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
这个示例程序定义了两个位域(bit field),a和b的地址是一样的,只是属于该地址的不同bit。在这种情况下,CPU 0去写a (a = 1),同时CPU 1去写b (b = 1),就会产生竞争。在总线仲裁后,先写的数据就会被后写的数据给覆盖掉。这就是执行RMW操作时典型的竞争问题。在这种场景下,就需要同步原语了,比如使用atomic操作。
|
||||
|
||||
关于位操作,我们来看一个实际的案例。这是我前段时间贡献给Linux内核的一个PATCH:[psi: Move PF_MEMSTALL out of task->flags](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc4&id=1066d1b6974e095d5a6c472ad9180a957b496cd6),它在struct task_struct这个结构体里增加了一个in_memstall的位域,在该PATCH里无需考虑多线程并行操作该位域时的竞争问题,你知道这是为什么吗?我将它作为一个课后思考题留给你,欢迎你在留言区与我讨论交流。为了让这个问题简单些,我给你一个提示:如果你留意过task_struct这个结构体里的位域被改写的情况,你会发现只有current(当前在运行的线程)可以写,而其他线程只能去读。但是PF_*这些全局flag可以被其他线程写,而不仅仅是current来写。
|
||||
|
||||
Linux内核里的task_struct结构体就是用来表示一个线程的,每个线程都有唯一对应的task_struct结构体,它也是内核进行调度的基本单位。我们继续来看下CPU是如何选择线程来执行的。
|
||||
|
||||
## CPU是如何选择线程执行的 ?
|
||||
|
||||
你知道,一个系统中可能会运行着非常多的线程,这些线程数可能远超系统中的CPU核数,这时候这些任务就需要排队,每个CPU都会维护着自己运行队列(runqueue)里的线程。这个运行队列的结构大致如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/66/62/6649d7e5984a3b9cd003fcbc97bfde62.jpg" alt="" title="CPU运行队列">
|
||||
|
||||
每个CPU都有自己的运行队列(runqueue),需要运行的线程会被加入到这个队列中。因为有些线程的优先级高,Linux内核为了保障这些高优先级任务的执行,设置了不同的调度类(Scheduling Class),如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/15/b1/1507d0ef23d5d1cd33769dd1953cffb1.jpg" alt="">
|
||||
|
||||
这几个调度类的优先级如下:Deadline > Realtime > Fair。Linux内核在选择下一个任务执行时,会按照该顺序来进行选择,也就是先从dl_rq里选择任务,然后从rt_rq里选择任务,最后从cfs_rq里选择任务。所以实时任务总是会比普通任务先得到执行。
|
||||
|
||||
如果你的某些任务对延迟容忍度很低,比如说在嵌入式系统中就有很多这类任务,那就可以考虑将你的任务设置为实时任务,比如将它设置为SCHED_FIFO的任务:
|
||||
|
||||
>
|
||||
$ chrt -f -p 1 1327
|
||||
|
||||
|
||||
如果你不做任何设置的话,用户线程在默认情况下都是普通线程,也就是属于Fair调度类,由CFS调度器来进行管理。CFS调度器的目的是为了实现线程运行的公平性,举个例子,假设一个CPU上有两个线程需要执行,那么每个线程都将分配50%的CPU时间,以保障公平性。其实,各个线程之间执行时间的比例,也是可以人为干预的,比如在Linux上可以调整进程的nice值来干预,从而让优先级高一些的线程执行更多时间。这就是CFS调度器的大致思想。
|
||||
|
||||
好了,我们这堂课就先讲到这里。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
我来总结一下这节课的知识点:
|
||||
|
||||
- 要想明白CPU是如何执行任务的,你首先需要去了解CPU的架构;
|
||||
- CPU的存储层次对大型软件系统的性能影响会很明显,也是你在性能调优时需要着重考虑的;
|
||||
- 高并发场景下的Cache Line伪共享问题是一个普遍存在的问题,你需要留意一下它;
|
||||
- 系统中需要运行的线程数可能大于CPU核数,这样就会导致线程排队等待CPU,这可能会导致一些延迟。如果你的任务对延迟容忍度低,你可以通过一些手段来人为干预Linux默认的调度策略。
|
||||
|
||||
## 课后作业
|
||||
|
||||
这节课的作业就是我们前面提到的思考题:在[psi: Move PF_MEMSTALL out of task->flags](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc4&id=1066d1b6974e095d5a6c472ad9180a957b496cd6)这个PATCH中,为什么没有考虑多线程并行操作新增加的位域(in_memstall)时的竞争问题?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
@@ -0,0 +1,201 @@
|
||||
<audio id="audio" title="18 案例篇 | 业务是否需要使用透明大页:水可载舟,亦可覆舟?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/db/bb/dbb6d45c207540a177d925bae52d74bb.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
我们这节课的案例来自于我在多年以前帮助业务团队分析的一个稳定性问题。当时,业务团队反映说他们有一些服务器的CPU利用率会异常飙高,然后很快就能恢复,并且持续的时间不长,大概几秒到几分钟,从监控图上可以看到它像一些毛刺。
|
||||
|
||||
因为这类问题是普遍存在的,所以我就把该问题的定位分析过程分享给你,希望你以后遇到CPU利用率飙高的问题时,知道该如何一步步地分析。
|
||||
|
||||
CPU利用率是一个很笼统的概念,在遇到CPU利用率飙高的问题时,我们需要看看CPU到底在忙哪类事情,比如说CPU是在忙着处理中断、等待I/O、执行内核函数?还是在执行用户函数?这个时候就需要我们细化CPU利用率的监控,因为监控这些细化的指标对我们分析问题很有帮助。
|
||||
|
||||
## 细化CPU利用率监控
|
||||
|
||||
这里我们以常用的top命令为例,来看看CPU更加细化的利用率指标(不同版本的top命令显示可能会略有不同):
|
||||
|
||||
%Cpu(s): 12.5 us, 0.0 sy, 0.0 ni, 87.4 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
|
||||
top命令显示了us、sy、ni、id、wa、hi、si和st这几个指标,这几个指标之和为100。那你可能会有疑问,细化CPU利用率指标的监控会不会带来明显的额外开销?答案是不会的,因为CPU利用率监控通常是去解析/proc/stat文件,而这些文件中就包含了这些细化的指标。
|
||||
|
||||
我们继续来看下上述几个指标的具体含义,这些含义你也可以从[top手册](https://man7.org/linux/man-pages/man1/top.1.html)里来查看:
|
||||
|
||||
```
|
||||
us, user : time running un-niced user processes
|
||||
sy, system : time running kernel processes
|
||||
ni, nice : time running niced user processes
|
||||
id, idle : time spent in the kernel idle handler
|
||||
wa, IO-wait : time waiting for I/O completion
|
||||
hi : time spent servicing hardware interrupts
|
||||
si : time spent servicing software interrupts
|
||||
st : time stolen from this vm by the hypervisor
|
||||
|
||||
```
|
||||
|
||||
上述指标的具体含义以及注意事项如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/37/a5/3756d973a1f7f350bf600c9438f1a4a5.jpg" alt="">
|
||||
|
||||
在上面这几项中,idle和wait是CPU不工作的时间,其余的项都是CPU工作的时间。idle和wait的主要区别是,idle是CPU无事可做,而wait则是CPU想做事却做不了。你也可以将wait理解为是一类特殊的idle,即该CPU上有至少一个线程阻塞在I/O时的idle。
|
||||
|
||||
而我们通过对CPU利用率的细化监控发现,案例中的CPU利用率飙高是由sys利用率变高导致的,也就是说sys利用率会忽然飙高一下,比如在usr低于30%的情况下,sys会高于15%,持续几秒后又恢复正常。
|
||||
|
||||
所以,接下来我们就需要抓取sys利用率飙高的现场。
|
||||
|
||||
## 抓取sys利用率飙高现场
|
||||
|
||||
我们在前面讲到,CPU的sys利用率高,说明内核函数执行花费了太多的时间,所以我们需要采集CPU在sys飙高的瞬间所执行的内核函数。采集内核函数的方法有很多,比如:
|
||||
|
||||
- 通过perf可以采集CPU的热点,看看sys利用率高时,哪些内核耗时的CPU利用率高;
|
||||
- 通过perf的call-graph功能可以查看具体的调用栈信息,也就是线程是从什么路径上执行下来的;
|
||||
- 通过perf的annotate功能可以追踪到线程是在内核函数的哪些语句上比较耗时;
|
||||
- 通过ftrace的function-graph功能可以查看这些内核函数的具体耗时,以及在哪个路径上耗时最大。
|
||||
|
||||
不过,这些常用的追踪方式在这种瞬间消失的问题上是不太适用的,因为它们更加适合采集一个时间段内的信息。
|
||||
|
||||
那么针对这种瞬时的状态,我希望有一个系统快照,把当前CPU正在做的工作记录下来,然后我们就可以结合内核源码分析为什么sys利用率会高了。
|
||||
|
||||
有一个工具就可以很好地追踪这种系统瞬时状态,即系统快照,它就是sysrq。sysrq是我经常用来分析内核问题的工具,用它可以观察当前的内存快照、任务快照,可以构造vmcore把系统的所有信息都保存下来,甚至还可以在内存紧张的时候用它杀掉内存开销最大的那个进程。sysrq可以说是分析很多疑难问题的利器。
|
||||
|
||||
要想用sysrq来分析问题,首先需要使能sysyrq。我建议你将sysrq的所有功能都使能,你无需担心会有什么额外开销,而且这也没有什么风险。使能方式如下:
|
||||
|
||||
>
|
||||
$ sysctl -w kernel.sysrq = 1
|
||||
|
||||
|
||||
sysrq的功能被使能后,你可以使用它的-t选项把当前的任务快照保存下来,看看系统中都有哪些任务,以及这些任务都在干什么。使用方式如下:
|
||||
|
||||
>
|
||||
$ echo t > /proc/sysrq-trigger
|
||||
|
||||
|
||||
然后任务快照就会被打印到内核缓冲区,这些任务快照信息你可以通过dmesg命令来查看:
|
||||
|
||||
>
|
||||
$ dmesg
|
||||
|
||||
|
||||
当时我为了抓取这种瞬时的状态,写了一个脚本来采集,如下就是一个简单的脚本示例:
|
||||
|
||||
```
|
||||
#!/bin/sh
|
||||
|
||||
while [ 1 ]; do
|
||||
top -bn2 | grep "Cpu(s)" | tail -1 | awk '{
|
||||
# $2 is usr, $4 is sys.
|
||||
if ($2 < 30.0 && $4 > 15.0) {
|
||||
# save the current usr and sys into a tmp file
|
||||
while ("date" | getline date) {
|
||||
split(date, str, " ");
|
||||
prefix=sprintf("%s_%s_%s_%s", str[2],str[3], str[4], str[5]);
|
||||
}
|
||||
|
||||
sys_usr_file=sprintf("/tmp/%s_info.highsys", prefix);
|
||||
print $2 > sys_usr_file;
|
||||
print $4 >> sys_usr_file;
|
||||
|
||||
# run sysrq
|
||||
system("echo t > /proc/sysrq-trigger");
|
||||
}
|
||||
}'
|
||||
sleep 1m
|
||||
done
|
||||
|
||||
```
|
||||
|
||||
这个脚本会检测sys利用率高于15%同时usr较低的情况,也就是说检测CPU是否在内核里花费了太多时间。如果出现这种情况,就会运行sysrq来保存当前任务快照。你可以发现这个脚本设置的是1分钟执行一次,之所以这么做是因为不想引起很大的性能开销,而且当时业务团队里有几台机器差不多是一天出现两三次这种状况,有些机器每次可以持续几分钟,所以这已经足够了。不过,如果你遇到的问题出现的频率更低,持续时间更短,那就需要更加精确的方法了。
|
||||
|
||||
## 透明大页:水可载舟,亦可覆舟?
|
||||
|
||||
我们把脚本部署好后,就把问题现场抓取出来了。从dmesg输出的信息中,我们发现处于R状态的线程都在进行compcation(内存规整),线程的调用栈如下所示(这是一个比较古老的内核,版本为2.6.32):
|
||||
|
||||
```
|
||||
java R running task 0 144305 144271 0x00000080
|
||||
ffff88096393d788 0000000000000086 ffff88096393d7b8 ffffffff81060b13
|
||||
ffff88096393d738 ffffea003968ce50 000000000000000e ffff880caa713040
|
||||
ffff8801688b0638 ffff88096393dfd8 000000000000fbc8 ffff8801688b0640
|
||||
|
||||
Call Trace:
|
||||
[<ffffffff81060b13>] ? perf_event_task_sched_out+0x33/0x70
|
||||
[<ffffffff8100bb8e>] ? apic_timer_interrupt+0xe/0x20
|
||||
[<ffffffff810686da>] __cond_resched+0x2a/0x40
|
||||
[<ffffffff81528300>] _cond_resched+0x30/0x40
|
||||
[<ffffffff81169505>] compact_checklock_irqsave+0x65/0xd0
|
||||
[<ffffffff81169862>] compaction_alloc+0x202/0x460
|
||||
[<ffffffff811748d8>] ? buffer_migrate_page+0xe8/0x130
|
||||
[<ffffffff81174b4a>] migrate_pages+0xaa/0x480
|
||||
[<ffffffff81169660>] ? compaction_alloc+0x0/0x460
|
||||
[<ffffffff8116a1a1>] compact_zone+0x581/0x950
|
||||
[<ffffffff8116a81c>] compact_zone_order+0xac/0x100
|
||||
[<ffffffff8116a951>] try_to_compact_pages+0xe1/0x120
|
||||
[<ffffffff8112f1ba>] __alloc_pages_direct_compact+0xda/0x1b0
|
||||
[<ffffffff8112f80b>] __alloc_pages_nodemask+0x57b/0x8d0
|
||||
[<ffffffff81167b9a>] alloc_pages_vma+0x9a/0x150
|
||||
[<ffffffff8118337d>] do_huge_pmd_anonymous_page+0x14d/0x3b0
|
||||
[<ffffffff8152a116>] ? rwsem_down_read_failed+0x26/0x30
|
||||
[<ffffffff8114b350>] handle_mm_fault+0x2f0/0x300
|
||||
[<ffffffff810ae950>] ? wake_futex+0x40/0x60
|
||||
[<ffffffff8104a8d8>] __do_page_fault+0x138/0x480
|
||||
[<ffffffff810097cc>] ? __switch_to+0x1ac/0x320
|
||||
[<ffffffff81527910>] ? thread_return+0x4e/0x76e
|
||||
[<ffffffff8152d45e>] do_page_fault+0x3e/0xa0
|
||||
[<ffffffff8152a815>] page_fault+0x25/0x30
|
||||
|
||||
```
|
||||
|
||||
从该调用栈我们可以看出,此时这个java线程在申请THP(do_huge_pmd_anonymous_page)。THP就是透明大页,它是一个2M的连续物理内存。但是,因为这个时候物理内存中已经没有连续2M的内存空间了,所以触发了direct compaction(直接内存规整),内存规整的过程可以用下图来表示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/db/51/db981eb703a88ae85458618355789251.jpg" alt="" title="THP Compaction">
|
||||
|
||||
这个过程并不复杂,在进行compcation时,线程会从前往后扫描已使用的movable page,然后从后往前扫描free page,扫描结束后会把这些movable page给迁移到free page里,最终规整出一个2M的连续物理内存,这样THP就可以成功申请内存了。
|
||||
|
||||
direct compaction这个过程是很耗时的,而且在2.6.32版本的内核上,该过程需要持有粗粒度的锁,所以在运行过程中线程还可能会主动检查(_cond_resched)是否有其他更高优先级的任务需要执行。如果有的话就会让其他线程先执行,这便进一步加剧了它的执行耗时。这也就是sys利用率飙高的原因。关于这些,你也都可以从内核源码的注释来看到:
|
||||
|
||||
```
|
||||
/*
|
||||
* Compaction requires the taking of some coarse locks that are potentially
|
||||
* very heavily contended. Check if the process needs to be scheduled or
|
||||
* if the lock is contended. For async compaction, back out in the event
|
||||
* if contention is severe. For sync compaction, schedule.
|
||||
* ...
|
||||
*/
|
||||
|
||||
```
|
||||
|
||||
在我们找到了原因之后,为了快速解决生产环境上的这些问题,我们就把该业务服务器上的THP关掉了,关闭后系统变得很稳定,再也没有出现过sys利用率飙高的问题。关闭THP可以使用下面这个命令:
|
||||
|
||||
>
|
||||
$ echo never > /sys/kernel/mm/transparent_hugepage/enabled
|
||||
|
||||
|
||||
关闭了生产环境上的THP后,我们又在线下测试环境中评估了THP对该业务的性能影响,我们发现THP并不能给该业务带来明显的性能提升,即使是在内存不紧张、不会触发内存规整的情况下。这也引起了我的思考,**THP究竟适合什么样的业务呢?**
|
||||
|
||||
这就要从THP的目的来说起了。我们长话短说,THP的目的是用一个页表项来映射更大的内存(大页),这样可以减少Page Fault,因为需要的页数少了。当然,这也会提升TLB命中率,因为需要的页表项也少了。如果进程要访问的数据都在这个大页中,那么这个大页就会很热,会被缓存在Cache中。而大页对应的页表项也会出现在TLB中,从上一讲的存储层次我们可以知道,这有助于性能提升。但是反过来,假设应用程序的数据局部性比较差,它在短时间内要访问的数据很随机地位于不同的大页上,那么大页的优势就会消失。
|
||||
|
||||
因此,我们基于大页给业务做性能优化的时候,首先要评估业务的数据局部性,尽量把业务的热点数据聚合在一起,以便于充分享受大页的优势。以我在华为任职期间所做的大页性能优化为例,我们将业务的热点数据聚合在一起,然后将这些热点数据分配到大页上,再与不使用大页的情况相比,最终发现这可以带来20%以上的性能提升。对于TLB较小的架构(比如MIPS这种架构),它可以带来50%以上的性能提升。当然了,我们在这个过程中也对内核的大页代码做了很多优化,这里就不展开说了。
|
||||
|
||||
针对THP的使用,我在这里给你几点建议:
|
||||
|
||||
- 不要将/sys/kernel/mm/transparent_hugepage/enabled配置为always,你可以将它配置为madvise。如果你不清楚该如何来配置,那就将它配置为never;
|
||||
- 如果你想要用THP优化业务,最好可以让业务以madvise的方式来使用大页,即通过修改业务代码来指定特定数据使用THP,因为业务更熟悉自己的数据流;
|
||||
- 很多时候修改业务代码会很麻烦,如果你不想修改业务代码的话,那就去优化THP的内核代码吧。
|
||||
|
||||
好了,这节课就讲到这里。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
我们来回顾一下这节课的要点:
|
||||
|
||||
- 细化CPU利用率监控,在CPU利用率高时,你需要查看具体是哪一项指标比较高;
|
||||
- sysrq是分析内核态CPU利用率高的利器,也是分析很多内核疑难问题的利器,你需要去了解如何使用它;
|
||||
- THP可以给业务带来性能提升,但也可能会给业务带来严重的稳定性问题,你最好以madvise的方式使用它。如果你不清楚如何使用它,那就把它关闭。
|
||||
|
||||
## 课后作业
|
||||
|
||||
我们这节课的作业有三种,你可以根据自己的情况进行选择:
|
||||
|
||||
- 如果你是应用开发者,请问如何来观察系统中分配了多少THP?
|
||||
- 如果你是初级内核开发者,请问在进行compaction时,哪些页可以被迁移?哪些不可以被迁移?
|
||||
- 如果你是高级内核开发者,假设现在让你来设计让程序的代码段也可以使用hugetlbfs,那你觉得应该要做什么?
|
||||
|
||||
欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
@@ -0,0 +1,193 @@
|
||||
<audio id="audio" title="19 案例篇 | 网络吞吐高的业务是否需要开启网卡特性呢?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4b/6f/4bd85ab1bf99d82a0c98b3a1879efd6f.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
通过上一讲我们对CPU利用率的细化,相信你已经知道,对于应用而言,它的目标是让CPU的开销尽量用在执行用户代码上,而非其他方面。usr利用率越高,说明CPU的效率越高。如果usr低,就说明CPU执行应用的效率不高。在[第18讲](https://time.geekbang.org/column/article/292060)里,我们还讲了CPU时间浪费在sys里的案例。那今天这一讲,我们一起来看看CPU在softirq上花费过多时间所引起的业务性能下降问题,这也是我们在生产环境中经常遇到的一类问题。接下来我会为你讲解相关案例,以及这类问题常用的观察方法。
|
||||
|
||||
## 中断与业务进程之间是如何相互干扰的?
|
||||
|
||||
这是我多年以前遇到的一个案例,当时业务反馈说为了提升QPS(Query per Second),他们开启了RPS(Receivce Packet Steering)来模拟网卡多队列,没想到开启RPS反而导致了QPS明显下降,不知道是什么原因。
|
||||
|
||||
其实,这类特定行为改变引起的性能下降问题相对好分析一些。最简单的方式就是去对比这个行为前后的性能数据。即使你不清楚RPS是什么,也不知道它背后的机制,你也可以采集需要的性能指标进行对比分析,然后判断问题可能出在哪里。这些性能指标包括CPU指标,内存指标,I/O指标,网络指标等,我们可以使用dstat来观察它们的变化。
|
||||
|
||||
在业务打开RPS之前的性能指标:
|
||||
|
||||
```
|
||||
$ dstat
|
||||
You did not select any stats, using -cdngy by default.
|
||||
----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
|
||||
usr sys idl wai hiq siq| read writ| recv send| in out | int csw
|
||||
64 23 6 0 0 7| 0 8192B|7917k 12M| 0 0 | 27k 1922
|
||||
64 22 6 0 0 8| 0 0 |7739k 12M| 0 0 | 26k 2210
|
||||
61 23 9 0 0 7| 0 0 |7397k 11M| 0 0 | 25k 2267
|
||||
|
||||
```
|
||||
|
||||
打开了RPS之后的性能指标:
|
||||
|
||||
```
|
||||
$ dstat
|
||||
You did not select any stats, using -cdngy by default.
|
||||
----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
|
||||
usr sys idl wai hiq siq| read writ| recv send| in out | int csw
|
||||
62 23 4 0 0 12| 0 0 |7096k 11M| 0 0 | 49k 2261
|
||||
74 13 4 0 0 9| 0 0 |4003k 6543k| 0 0 | 31k 2004
|
||||
59 22 5 0 0 13| 0 4096B|6710k 10M| 0 0 | 48k 2220
|
||||
|
||||
```
|
||||
|
||||
我们可以看到,打开RPS后,CPU的利用率有所升高。其中,siq即软中断利用率明显增加,int即硬中断频率也明显升高,而net这一项里的网络吞吐数据则有所下降。也就是说,在网络吞吐不升反降的情况下,系统的硬中断和软中断都明显增加。由此我们可以推断出,网络吞吐的下降应该是中断增加导致的结果。
|
||||
|
||||
那么,接下来我们就需要分析到底是什么类型的软中断和硬中断增加了,以便于找到问题的源头。
|
||||
|
||||
系统中存在很多硬中断,这些硬中断及其发生频率我们都可以通过/proc/interruptes这个文件来查看:
|
||||
|
||||
```
|
||||
$ cat /proc/interrupts
|
||||
|
||||
```
|
||||
|
||||
如果你想要了解某个中断的详细情况,比如中断亲和性,那你可以通过/proc/irq/[irq_num]来查看,比如:
|
||||
|
||||
```
|
||||
$ cat /proc/irq/123/smp_affinity
|
||||
|
||||
```
|
||||
|
||||
软中断可以通过/proc/softirq来查看:
|
||||
|
||||
```
|
||||
$ cat /proc/softirqs
|
||||
|
||||
```
|
||||
|
||||
当然了,你也可以写一些脚本来观察各个硬中断和软中断的发生频率,从而更加直观地查看是哪些中断发生得太频繁。
|
||||
|
||||
关于硬中断和软中断的区别,你可能多少有些了解,软中断是用来处理硬中断在短时间内无法完成的任务的。硬中断由于执行时间短,所以如果它的发生频率不高的话,一般不会给业务带来明显影响。但是由于内核里关中断的地方太多,所以进程往往会给硬中断带来一些影响,比如进程关中断时间太长会导致网络报文无法及时处理,进而引起业务性能抖动。
|
||||
|
||||
我们在生产环境中就遇到过这种关中断时间太长引起的抖动案例,比如cat /proc/slabinfo这个操作里的逻辑关中断太长,它会致使业务RT抖动。这是因为该命令会统计系统中所有的slab数量,并显示出来,在统计的过程中会关中断。如果系统中的slab数量太多,就会导致关中断的时间太长,进而引起网络包阻塞,ping延迟也会因此明显变大。所以,在生产环境中我们要尽量避免去采集/proc/slabinfo,否则可能会引起业务抖动。
|
||||
|
||||
由于/proc/slabinfo很危险,所以它的访问权限也从2.6.32版本时的0644[更改为了后来](https://lwn.net/Articles/431818/)[的](https://lwn.net/Articles/431818/)[0400](https://lwn.net/Articles/431818/),也就是说只有root用户才能够读取它,这在一定程度上避免了一些问题。如果你的系统是2.6.32版本的内核,你就需要特别注意该问题。
|
||||
|
||||
如果你要分析因中断关闭时间太长而引发的问题,有一种最简单的方式,就是使用ftrace的irqsoff功能。它不仅可以统计出中断被关闭了多长时间,还可以统计出为什么会关闭中断。不过,你需要注意的是,irqsoff功能依赖于CONFIG_IRQSOFF_TRACER这个配置项,如果你的内核没有打开该配置项,那你就需要使用其他方式来去追踪了。
|
||||
|
||||
如何使用irqsoff呢?首先,你需要去查看你的系统是否支持了irqsoff这个tracer:
|
||||
|
||||
```
|
||||
$ cat /sys/kernel/debug/tracing/available_tracers
|
||||
|
||||
```
|
||||
|
||||
如果显示的内容包含了irqsoff,说明系统支持该功能,你就可以打开它进行追踪了:
|
||||
|
||||
```
|
||||
$ echo irqsoff > /sys/kernel/debug/tracing/current_tracer
|
||||
|
||||
```
|
||||
|
||||
接下来,你就可以通过/sys/kernel/debug/tracing/trace_pipe和trace这两个文件,来观察系统中的irqsoff事件了。
|
||||
|
||||
我们知道,相比硬中断,软中断的执行时间会长一些,而且它也会抢占正在执行进程的CPU,从而导致进程在它运行期间只能等待。所以,相对而言它会更容易给业务带来延迟。那我们怎么对软中断的执行频率以及执行耗时进行观测呢?你可以通过如下两个tracepoints来进行观测:
|
||||
|
||||
```
|
||||
/sys/kernel/debug/tracing/events/irq/softirq_entry
|
||||
/sys/kernel/debug/tracing/events/irq/softirq_exit
|
||||
|
||||
```
|
||||
|
||||
这两个tracepoint分别表示软中断的进入和退出,退出时间减去进入时间就是该软中断这一次的耗时。关于tracepoint采集数据的分析方式,我们在之前的课程里已经讲过多次,所以就不在这里继续描述了。
|
||||
|
||||
如果你的内核版本比较新,支持eBPF功能,那你同样可以使用bcc里的[softirqs.py](https://github.com/iovisor/bcc/blob/master/tools/softirqs.py)这个工具来进行观测。它会统计软中断的次数和耗时,这对我们分析软中断引起的业务延迟来说,是比较方便的。
|
||||
|
||||
为了避免软中断太过频繁,进程无法得到CPU而被饿死的情况,内核引入了ksoftirqd这个机制。如果所有的软中断在短时间内无法被处理完,内核就会唤醒ksoftirqd处理接下来的软中断。ksoftirqd与普通进程的优先级一样,也就是说它会和普通进程公平地使用CPU,这在一定程度上可以避免用户进程被饿死的情况,特别是对于那些更高优先级的实时用户进程而言。
|
||||
|
||||
不过,这也会带来一些问题。如果ksoftrirqd长时间得不到CPU,就会致使软中断的延迟变得很大,它引起的典型问题也是ping延迟。如果ping包无法在软中断里得到处理,就会被ksoftirqd处理。而ksoftirqd的实时性是很难得到保障的,可能需要等其他线程执行完,ksoftirqd才能得到执行,这就会导致ping延迟变得很大。
|
||||
|
||||
要观测ksoftirqd延迟的问题,你可以使用bcc里的[runqlat.py](https://github.com/iovisor/bcc/blob/master/tools/runqlat.py)。这里我们以网卡中断为例,它唤醒ksoftirqd的逻辑大致如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8b/01/8b3f5bfa5571dcc855c4f6cd7dc4ce01.jpg" alt="" title="中断与ksoftirqd">
|
||||
|
||||
我们具体来看看这个过程:软中断被唤醒后会检查一遍软中断向量表,逐个处理这些软中断;处理完一遍后,它会再次检查,如果又有新的软中断要处理,就会唤醒ksoftrqd来处理。ksoftirqd是per-cpu的内核线程,每个CPU都有一个。对于CPU1而言,它运行的是ksoftirqd/1这个线程。ksoftirqd/1被唤醒后会检查软中断向量表并进行处理。如果你使用ps来查看ksoftirqd/1的优先级,会发现它其实就是一个普通线程(对应的Nice值为0):
|
||||
|
||||
```
|
||||
$ ps -eo "pid,comm,ni" | grep softirqd
|
||||
9 ksoftirqd/0 0
|
||||
16 ksoftirqd/1 0
|
||||
21 ksoftirqd/2 0
|
||||
26 ksoftirqd/3 0
|
||||
|
||||
```
|
||||
|
||||
总之,在软中断处理这部分,内核需要改进的地方还有很多。
|
||||
|
||||
## softirq是如何影响业务的?
|
||||
|
||||
在我们对硬中断和软中断进行观察后发现,使能RPS后增加了很多CAL(Function Call Interrupts)硬中断。CAL是通过软件触发硬中断的一种方式,可以指定CPU以及需要执行的中断处理程序。它也常被用来进行CPU间通信(IPI),当一个CPU需要其他CPU来执行特定中断处理程序时,就可以通过CAL中断来进行。
|
||||
|
||||
如果你对RPS的机制有所了解的话,应该清楚RPS就是通过CAL这种方式来让其他CPU去接收网络包的。为了验证这一点,我们可以通过mpstat这个命令来观察各个CPU利用率情况。
|
||||
|
||||
使能RPS之前的CPU利用率如下所示:
|
||||
|
||||
```
|
||||
$ mpstat -P ALL 1
|
||||
Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
|
||||
Average: all 70.18 0.00 19.28 0.00 0.00 5.86 0.00 0.00 0.00 4.68
|
||||
Average: 0 73.25 0.00 21.50 0.00 0.00 0.00 0.00 0.00 0.00 5.25
|
||||
Average: 1 58.85 0.00 14.46 0.00 0.00 23.44 0.00 0.00 0.00 3.24
|
||||
Average: 2 74.50 0.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 5.50
|
||||
Average: 3 74.25 0.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 4.75
|
||||
|
||||
```
|
||||
|
||||
使能RPS之后各个CPU的利用率情况为:
|
||||
|
||||
```
|
||||
$ mpstat -P ALL 1
|
||||
Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
|
||||
Average: all 66.21 0.00 17.73 0.00 0.00 11.15 0.00 0.00 0.00 4.91
|
||||
Average: 0 68.17 0.00 18.33 0.00 0.00 7.67 0.00 0.00 0.00 5.83
|
||||
Average: 1 60.57 0.00 15.81 0.00 0.00 20.80 0.00 0.00 0.00 2.83
|
||||
Average: 2 69.95 0.00 19.20 0.00 0.00 7.01 0.00 0.00 0.00 3.84
|
||||
Average: 3 66.39 0.00 17.64 0.00 0.00 8.99 0.00 0.00 0.00 6.99
|
||||
|
||||
```
|
||||
|
||||
我们可以看到,使能RPS之后,softirq在各个CPU之间更加均衡了一些,本来只有CPU1在处理softirq,使能后每个CPU都会处理softirq,并且CPU1的softirq利用率降低了一些。这就是RPS的作用:让网络收包软中断处理在各个CPU间更加均衡,以防止其在某个CPU上达到瓶颈。你可以看到,使能RPS后整体的%soft比原来高了很多。
|
||||
|
||||
理论上,处理网络收包软中断的CPU变多,那么在单位时间内这些CPU应该可以处理更多的网络包,从而提升系统整体的吞吐。可是,在我们的案例中,为什么会引起业务的QPS不升反降呢?
|
||||
|
||||
其实,答案同样可以从CPU利用率中得出。我们可以看到在使能RPS之前,CPU利用率已经很高了,达到了90%以上,也就是说CPU已经在超负荷工作了。而打开RPS,RPS又会消耗额外的CPU时间来模拟网卡多队列特性,这就会导致CPU更加超负荷地工作,从而进一步挤压用户进程的处理时间。因此,我们会发现,在打开RPS后%usr的利用率下降了一些。
|
||||
|
||||
我们知道%usr是衡量用户进程执行时间的一个指标,%usr越高意味着业务代码的运行时间越多。如果%usr下降,那就意味着业务代码的运行时间变少了,在业务没有进行代码优化的前提下,这显然是一个危险的信号。
|
||||
|
||||
由此我们可以发现,RPS的本质就是把网卡特性(网卡多队列)给upload到CPU,通过牺牲CPU时间来提升网络吞吐。如果你的系统已经很繁忙了,那么再使用该特性无疑是雪上加霜。所以,你需要注意,使用RPS的前提条件是:系统的整体CPU利用率不能太高。
|
||||
|
||||
找到问题后,我们就把该系统的RPS特性关闭了。如果你的网卡比较新,它可能会支持硬件多队列。硬件多队列是在网卡里做负载均衡,在这种场景下硬件多队列会很有帮助。我们知道,与upload相反的方向是offload,就是把CPU的工作给offload到网卡上去处理,这样可以把CPU解放出来,让它有更多的时间执行用户代码。关于网卡的offload特性,我们就不在此讨论了。
|
||||
|
||||
好了,这节课就讲到这里。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
我们来简单回顾一下这节课的重点:
|
||||
|
||||
- 硬中断、软中断以及ksoftirqd这个内核线程,它们与用户线程之间的关系是相对容易引发业务抖动的地方,你需要掌握它们的观测方式;
|
||||
- 硬中断对业务的主要影响体现在硬中断的发生频率上,但是它也容易受线程影响,因为内核里关中断的地方有很多;
|
||||
- 软中断的执行时间如果太长,就会给用户线程带来延迟,你需要避免你的系统中存在耗时较大的软中断处理程序。如果有的话,你需要去做优化;
|
||||
- ksoftirqd的优先级与用户线程是一致的,因此,如果软中断处理函数是在ksoftirqd里执行的,那它可能会有一些延迟;
|
||||
- RPS的本质是网卡特性unload到CPU,靠牺牲CPU时间来提升吞吐,你需要结合你的业务场景来评估是否需要开启它。如果你的网卡支持了硬件多队列,那么就可以直接使用硬件多队列了。
|
||||
|
||||
## 课后作业
|
||||
|
||||
我们这节课的作业有两种,你可以根据自己的情况进行选择。
|
||||
|
||||
- 入门:
|
||||
|
||||
请问如果软中断以及硬中断被关闭的时间太长,会发生什么事?
|
||||
|
||||
- 高级:
|
||||
|
||||
如果想要追踪网络数据包在内核缓冲区停留了多长时间才被应用读走,你觉得应该如何来追踪?
|
||||
|
||||
欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
157
极客时间专栏/Linux内核技术实战课/内核态CPU利用率飙高问题/20 分析篇 | 如何分析CPU利用率飙高问题 ?.md
Normal file
157
极客时间专栏/Linux内核技术实战课/内核态CPU利用率飙高问题/20 分析篇 | 如何分析CPU利用率飙高问题 ?.md
Normal file
@@ -0,0 +1,157 @@
|
||||
<audio id="audio" title="20 分析篇 | 如何分析CPU利用率飙高问题 ?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/19/60/1966ed0049810c56aa6cd161453ae260.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
如果你是一名应用开发者,那你应该知道如何去分析应用逻辑,对于如何优化应用代码提升系统性能也应该有自己的一套经验。而我们这节课想要讨论的是,如何拓展你的边界,让你能够分析代码之外的模块,以及对你而言几乎是黑盒的Linux内核。
|
||||
|
||||
在很多情况下,应用的性能问题都需要通过分析内核行为来解决,因此,内核提供了非常多的指标供应用程序参考。当应用出现问题时,我们可以查看到底是哪些指标出现了异常,然后再做进一步分析。不过,这些内核导出的指标并不能覆盖所有的场景,我们面临的问题可能更加棘手:应用出现性能问题,可是系统中所有的指标都看起来没有异常。相信很多人都为此抓狂过。那出现这种情况时,内核到底有没有问题呢,它究竟在搞什么鬼?这节课我就带你探讨一下如何分析这类问题。
|
||||
|
||||
我们知道,对于应用开发者而言,应用程序的边界是系统调用,进入到系统调用中就是Linux内核了。所以,要想拓展分析问题的边界,你首先需要知道该怎么去分析应用程序使用的系统调用函数。对于内核开发者而言,边界同样是系统调用,系统调用之外是应用程序。如果内核开发者想要拓展分析问题的边界,也需要知道如何利用系统调用去追踪应用程序的逻辑。
|
||||
|
||||
## 如何拓展你分析问题的边界?
|
||||
|
||||
作为一名内核开发者,我对应用程序逻辑的了解没有对内核的了解那么深。不过,当应用开发者向我寻求帮助时,尽管我对他们的应用逻辑一无所知,但这并不影响我对问题的分析,因为我知道如何借助分析工具追踪应用程序的逻辑。经过一系列追踪之后,我就能对应用程序有一个大概的认识。
|
||||
|
||||
我常用来追踪应用逻辑的工具之一就是strace。strace可以用来分析应用和内核的“边界”——系统调用。借助strace,我们不仅能够了解应用执行的逻辑,还可以了解内核逻辑。那么,作为应用开发者的你,就可以借助这个工具来拓展你分析应用问题的边界。
|
||||
|
||||
strace可以跟踪进程的系统调用、特定的系统调用以及系统调用的执行时间。很多时候,我们通过系统调用的执行时间,就能判断出业务延迟发生在哪里。比如我们想要跟踪一个多线程程序的系统调用情况,那就可以这样使用strace:
|
||||
|
||||
>
|
||||
$ strace -T -tt -ff -p pid -o strace.out
|
||||
|
||||
|
||||
不过,在使用strace跟踪进程之前,我希望你可以先明白strace的工作原理,这也是我们这节课的目的:你不只要知道怎样使用工具,更要明白工具的原理,这样在出现问题时,你就能明白该工具是否适用了。
|
||||
|
||||
## 了解工具的原理,不要局限于如何使用它
|
||||
|
||||
strace工具的原理如下图所示(我们以上面的那个命令为例来说明):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bc/6f/bc04236262f16e0b69842dafd503616f.jpg" alt="" title="strace基本原理">
|
||||
|
||||
我们从图中可以看到,对于正在运行的进程而言,strace可以attach到目标进程上,这是通过ptrace这个系统调用实现的(gdb工具也是如此)。ptrace的PTRACE_SYSCALL会去追踪目标进程的系统调用;目标进程被追踪后,每次进入syscall,都会产生SIGTRAP信号并暂停执行;追踪者通过目标进程触发的SIGTRAP信号,就可以知道目标进程进入了系统调用,然后追踪者会去处理该系统调用,我们用strace命令观察到的信息输出就是该处理的结果;追踪者处理完该系统调用后,就会恢复目标进程的执行。被恢复的目标进程会一直执行下去,直到下一个系统调用。
|
||||
|
||||
你可以发现,目标进程每执行一次系统调用都会被打断,等strace处理完后,目标进程才能继续执行,这就会给目标进程带来比较明显的延迟。因此,在生产环境中我不建议使用该命令,如果你要使用该命令来追踪生产环境的问题,那就一定要做好预案。
|
||||
|
||||
假设我们使用strace跟踪到,线程延迟抖动是由某一个系统调用耗时长导致的,那么接下来我们该怎么继续追踪呢?这就到了应用开发者和运维人员需要拓展分析边界的时刻了,对内核开发者来说,这才算是分析问题的开始。
|
||||
|
||||
## 学会使用内核开发者常用的分析工具
|
||||
|
||||
我们以一个实际案例来说明吧。有一次,业务开发者反馈说他们用strace追踪发现业务的pread(2)系统调用耗时很长,经常会有几十毫秒(ms)的情况,甚至能够达到秒级,但是不清楚接下来该如何分析,因此让我帮他们分析一下。
|
||||
|
||||
因为已经明确了问题是由pread(2)这个系统调用引起的,所以对内核开发者而言,后续的分析就相对容易了。分析这类问题最合适的工具是ftrace,我们可以使用ftrace的function_trace功能来追踪pread(2)这个系统调用到底是在哪里耗费了这么长的时间。
|
||||
|
||||
要想追踪pread(2)究竟在哪里耗时长,我们就需要知道该系统调用对应的内核函数是什么。我们有两种途径可以方便地获取到系统调用对应的内核函数:
|
||||
|
||||
- 查看[include/linux/syscalls.h](https://elixir.bootlin.com/linux/v5.9-rc6/source/include/linux/syscalls.h)文件里的内核函数:
|
||||
|
||||
你可以看到,与pread有关的函数有多个,由于我们的系统是64bit的,只需关注64bit相关的系统调用就可以了,所以我们锁定在ksys_pread64和sys_read64这两个函数上。[通过该头文件里的注释](https://elixir.bootlin.com/linux/v5.9-rc6/source/include/linux/syscalls.h#L1234)我们能知道,前者是内核使用的,后者是导出给用户的。那么在内核里,我们就需要去追踪前者。另外,请注意,不同内核版本对应的函数可能不一致,我们这里是以最新内核代码(5.9-rc)为例来说明的。
|
||||
|
||||
- 通过/proc/kallsyms这个文件来查找:
|
||||
|
||||
>
|
||||
<p>$ cat /proc/kallsyms | grep pread64<br>
|
||||
…<br>
|
||||
ffffffffa02ef3d0 T ksys_pread64<br>
|
||||
…</p>
|
||||
|
||||
|
||||
/proc/kallsyms里的每一行都是一个符号,其中第一列是符号地址,第二列是符号的属性,第三列是符号名字,比如上面这个信息中的T就表示全局代码符号,我们可以追踪这类的符号。关于这些符号属性的含义,你可以通过[man nm](https://man7.org/linux/man-pages/man1/nm.1p.html)来查看。
|
||||
|
||||
接下来我们就使用ftrace的function_graph功能来追踪ksys_pread64这个函数,看看究竟是内核的哪里耗时这么久。function_graph的使用方式如下:
|
||||
|
||||
```
|
||||
# 首先设置要追踪的函数
|
||||
$ echo ksys_pread64 > /sys/kernel/debug/tracing/set_graph_function
|
||||
|
||||
# 其次设置要追踪的线程的pid,如果有多个线程,那需要将每个线程都逐个写入
|
||||
$ echo 6577 > /sys/kernel/debug/tracing/set_ftrace_pid
|
||||
$ echo 6589 >> /sys/kernel/debug/tracing/set_ftrace_pid
|
||||
|
||||
# 将function_graph设置为当前的tracer,来追踪函数调用情况
|
||||
$ echo function_graph > /sys/kernel/debug/tracing/current_trace
|
||||
|
||||
```
|
||||
|
||||
然后我们就可以通过/sys/kernel/debug/tracing/trace_pipe来查看它的输出了,下面就是我追踪到的耗时情况:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/68/fc/689eacfa3ef10c236221f1b2051ab5fc.png" alt="">
|
||||
|
||||
我们可以发现pread(2)有102ms是阻塞在io_schedule()这个函数里的,io_schedule()的意思是,该线程因I/O阻塞而被调度走,线程需要等待I/O完成才能继续执行。在function_graph里,我们同样也能看到**pread****(**<strong>2**</strong>)**是如何一步步执行到io_schedule的,由于整个流程比较长,我在这里只把关键的调用逻辑贴出来:
|
||||
|
||||
```
|
||||
21) | __lock_page_killable() {
|
||||
21) 0.073 us | page_waitqueue();
|
||||
21) | __wait_on_bit_lock() {
|
||||
21) | prepare_to_wait_exclusive() {
|
||||
21) 0.186 us | _raw_spin_lock_irqsave();
|
||||
21) 0.051 us | _raw_spin_unlock_irqrestore();
|
||||
21) 1.339 us | }
|
||||
21) | bit_wait_io() {
|
||||
21) | io_schedule() {
|
||||
|
||||
```
|
||||
|
||||
我们可以看到,**pread(2)**是从__lock_page_killable这个函数调用下来的。当pread(2)从磁盘中读文件到内存页(page)时,会先lock该page,读完后再unlock。如果该page已经被别的线程lock了,比如在I/O过程中被lock,那么pread(2)就需要等待。等该page被I/O线程unlock后,pread(2)才能继续把文件内容读到这个page中。我们当时遇到的情况是:在pread(2)从磁盘中读取文件内容到一个page中的时候,该page已经被lock了,于是调用pread(2)的线程就在这里等待。这其实是合理的内核逻辑,没有什么问题。接下来,我们就需要看看为什么该page会被lock了这么久。
|
||||
|
||||
因为线程是阻塞在磁盘I/O里的,所以我们需要查看一下系统的磁盘I/O情况,我们可以使用iostat来观察:
|
||||
|
||||
>
|
||||
$ iostat -dxm 1
|
||||
|
||||
|
||||
追踪信息如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ca/04/ca94121ff716f75c171e2a3380d14d04.png" alt="">
|
||||
|
||||
其中,sdb是业务pread(2)读取的磁盘所在的文件,通常情况下它的读写量很小,但是我们从上图中可以看到,磁盘利用率(%util)会随机出现比较高的情况,接近100%。而且avgrq-sz很大,也就是说出现了很多I/O排队的情况。另外,w/s比平时也要高很多。我们还可以看到,由于此时存在大量的I/O写操作,磁盘I/O排队严重,磁盘I/O利用率也很高。根据这些信息我们可以判断,之所以pread(2)读磁盘文件耗时较长,很可能是因为被写操作饿死导致的。因此,我们接下来需要排查到底是谁在进行写I/O操作。
|
||||
|
||||
通过iotop观察I/O行为,我们发现并没有用户线程在进行I/O写操作,写操作几乎都是内核线程kworker来执行的,也就是说用户线程把内容写在了Page Cache里,然后kwoker将这些Page Cache中的内容再同步到磁盘中。这就涉及到了我们这门课程第一个模块的内容了:如何观测Page Cache的行为。
|
||||
|
||||
## 自己写分析工具
|
||||
|
||||
如果你现在还不清楚该如何来观测Page Cache的行为,那我建议你再从头仔细看一遍我们这门课程的第一个模块,我在这里就不细说了。不过,我要提一下在Page Cache模块中未曾提到的一些方法,这些方法用于判断内存中都有哪些文件以及这些文件的大小。
|
||||
|
||||
常规方式是用fincore和mincore,不过它们都比较低效。这里有一个更加高效的方式:通过写一个内核模块遍历inode来查看Page Cache的组成。该模块的代码较多,我只说一下核心的思想,伪代码大致如下:
|
||||
|
||||
```
|
||||
iterate_supers // 遍历super block
|
||||
iterate_pagecache_sb // 遍历superblock里的inode
|
||||
list_for_each_entry(inode, &sb->s_inodes, i_sb_list)
|
||||
// 记录该inode的pagecache大小
|
||||
nrpages = inode->i_mapping->nrpages;
|
||||
/* 获取该inode对应的dentry,然后根据该dentry来查找文件路径;
|
||||
* 请注意inode可能没有对应的dentry,因为dentry可能被回收掉了,
|
||||
* 此时就无法查看该inode对应的文件名了。
|
||||
*/
|
||||
dentry = dentry_from_inode(inode);
|
||||
dentry_path_raw(dentry, filename, PATH_MAX);
|
||||
|
||||
```
|
||||
|
||||
使用这种方式不仅可以查看进程正在打开的文件,也能查看文件已经被进程关闭,但文件内容还在内存中的情况。所以这种方式分析起来会更全面。
|
||||
|
||||
通过查看Page Cache的文件内容,我们发现某些特定的文件占用的内存特别大,但是这些文件都是一些离线业务的文件,也就是不重要业务的文件。因为离线业务占用了大量的Page Cache,导致该在线业务的workingset大大减小,所以pread(2)在读文件内容时经常命中不了Page Cache,进而需要从磁盘来读文件,也就是说该在线业务存在大量的pagein和pageout。
|
||||
|
||||
至此,问题的解决方案也就有了:我们可以通过限制离线业务的Page Cache大小,来保障在线业务的workingset,防止它出现较多的refault。经过这样调整后,业务再也没有出现这种性能抖动了。
|
||||
|
||||
你是不是对我上面提到的这些名字感到困惑呢?也不清楚inode和Page Cache是什么关系?如果是的话,那就说明你没有好好学习我们这门课程的Page Cache模块,我建议你从头再仔细学习一遍。
|
||||
|
||||
好了,我们这节课就讲到这里。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
我们这节课的内容,对于应用开发者和运维人员而言是有些难度的。我之所以讲这些有难度的内容,就是希望你可以拓展分析问题的边界。这节课的内容对内核开发者而言基本都是基础知识,如果你看不太明白,说明你对内核的理解还不够,你需要花更多的时间好好学习它。我研究内核已经有很多年了,尽管如此,我还是觉得自己对它的理解仍然不够深刻,需要持续不断地学习和研究,而我也一直在这么做。
|
||||
|
||||
我们现在回顾一下这节课的重点:
|
||||
|
||||
- strace工具是应用和内核的边界,如果你是一名应用开发者,并且想去拓展分析问题的边界,那你就需要去了解strace的原理,还需要了解如何去分析strace发现的问题;
|
||||
- ftrace是分析内核问题的利器,你需要去了解它;
|
||||
- 你需要根据自己的问题来实现特定的问题分析工具,要想更好地实现这些分析工具,你必须掌握很多内核细节。
|
||||
|
||||
## 课后作业
|
||||
|
||||
关于我们这节课的“自己写分析工具”这部分,我给你留一个作业,这也是我没有精力和时间去做的一件事:请你在sysrq里实现一个功能,让它可以显示出系统中所有R和D状态的任务,以此来帮助开发者分析系统load飙高的问题。
|
||||
|
||||
我在我们的内核里已经实现了该功能,不过在推给Linux内核时,maintainer希望我可以用另一种方式来实现。由于那个时候我在忙其他事情,这件事便被搁置了下来。如果你实现得比较好,你可以把它提交给Linux内核,提交的时候你也可以cc一下我(laoar.shao@gmail.com)。对了,你在实现时,也可以参考我之前的提交记录:[scheduler: enhancement to show_state_filter and SysRq](https://lore.kernel.org/patchwork/patch/818962/)。欢迎你在留言区与我讨论。
|
||||
|
||||
最后,感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友。
|
||||
Reference in New Issue
Block a user