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,170 @@
<audio id="audio" title="01 基础篇 | 如何用数据观测Page Cache" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f9/84/f98yy95aff1459863b134b975bced684.mp3"></audio>
你好我是邵亚方。今天我想和你聊一聊Page Cache的话题。
Page Cache你应该不陌生了如果你是一名应用开发者或者Linux运维人员那么在工作中你可能遇见过与Page Cache有关的场景比如
- 服务器的load飙高
- 服务器的I/O吞吐飙高
- 业务响应时延出现大的毛刺;
- 业务平均访问时延明显增加。
这些问题很可能是由于Page Cache管理不到位引起的因为Page Cache管理不当除了会增加系统I/O吞吐外还会引起业务性能抖动我在生产环境上处理过很多这类问题。
据我观察这类问题出现后业务开发人员以及运维人员往往会束手无策究其原因在于他们对Page Cache的理解仅仅停留在概念上并不清楚Page Cache如何和应用、系统关联起来对它引发的问题自然会束手无策了。所以要想不再踩Page Cache的坑你必须对它有个清晰的认识。
那么在我看来认识Page Cache最简单的方式就是用数据说话通过具体的数据你会更加深入地理解Page Cache的本质。为了帮你消化和理解我会用两节课的时间用数据剖析什么是Page Cache为什么需要Page CachePage Cache的产生和回收是什么样的。这样一来你会从本质到表象透彻理解它深切感受它和你的应用程序之间的关系从而能更好地理解上面提到的四个问题。
不过在这里我想给你提个醒要学习今天的内容你最好具备一些Linux编程的基础比如如何打开一个文件如何读写一个文件如何关闭一个文件等等。这样你理解今天的内容会更加容易当然了不具备也没有关系如果遇到你实在看不懂的地方你可以查阅《UNIX环境高级编程》这本书它是每一位Linux开发者以及运维人员必看的入门书籍。
好了,话不多说,我们进入今天的学习。
## 什么是Page Cache
我记得很多应用开发者或者运维在向我寻求帮助解决Page Cache引起的问题时总是喜欢问我Page Cache到底是属于内核还是属于用户针对这样的问题我一般会让他们先看下面这张图
<img src="https://static001.geekbang.org/resource/image/f3/1b/f344917f3cacd5bc06ae7c743a217f1b.png" alt="" title="应用程序产生Page Cache的逻辑示意图 ">
通过这张图片你可以清楚地看到红色的地方就是Page Cache**很明显Page Cache是内核管理的内存也就是说它属于内核不属于用户。**
那咱们怎么来观察Page Cache呢其实在Linux上直接查看Page Cache的方式有很多包括/proc/meminfo、free 、/proc/vmstat命令等它们的内容其实是一致的。
我们拿/proc/meminfo命令举例看一下如果你想了解/proc/meminfo中每一项具体含义的话可以去看[Kernel Documentation](https://www.kernel.org/doc/Documentation/filesystems/proc.rst)的meminfo这一节它详细解释了每一项的具体含义Kernel Documentation是应用开发者想要了解内核最简单、直接的方式
```
$ cat /proc/meminfo
...
Buffers: 1224 kB
Cached: 111472 kB
SwapCached: 36364 kB
Active: 6224232 kB
Inactive: 979432 kB
Active(anon): 6173036 kB
Inactive(anon): 927932 kB
Active(file): 51196 kB
Inactive(file): 51500 kB
...
Shmem: 10000 kB
...
SReclaimable: 43532 kB
...
```
根据上面的数据你可以简单得出这样的公式等式两边之和都是112696 KB
>
Buffers + Cached + SwapCached = Active(file) + Inactive(file) + Shmem + SwapCached
**那么等式两边的内容就是我们平时说的Page Cache。**请注意你没有看错两边都有SwapCached之所以要把它放在等式里就是说它也是Page Cache的一部分。
接下来我带你分析一下这些项的具体含义。等式右边这些项把Buffers和Cached做了一下细分分为了Active(file)Inactive(file) 和Shmem因为Buffers更加依赖于内核实现在不同内核版本中它的含义可能有些不一致而等式右边和应用程序的关系更加直接所以我们从等式右边来分析。
在Page Cache中Active(file)+Inactive(file)是File-backed page与文件对应的内存页是你最需要关注的部分。因为你平时用的mmap()内存映射方式和buffered I/O来消耗的内存就属于这部分**最重要的是,这部分在真实的生产环境上也最容易产生问题,**我们在接下来的课程案例篇会重点分析它。
而SwapCached是在打开了Swap分区后把Inactive(anon)+Active(anon)这两项里的匿名页给交换到磁盘swap out然后再读入到内存swap in后分配的内存。**由于读入到内存后原来的Swap File还在所以SwapCached也可以认为是File-backed page即属于Page Cache。**这样做的目的也是为了减少I/O。你是不是觉得这个过程有些复杂我们用一张图直观地看一下
<img src="https://static001.geekbang.org/resource/image/a9/7b/a9010acc1d7b55d5c24562d2c32b437b.jpg" alt="">
我希望你能通过这个简单的示意图明白SwapCached是怎么产生的。在这个过程中你要注意SwapCached只在Swap分区打开的情况下才会有而我建议你在生产环境中关闭Swap分区因为Swap过程产生的I/O会很容易引起性能抖动。
除了SwapCachedPage Cache中的Shmem是指匿名共享映射这种方式分配的内存free命令中shared这一项比如tmpfs临时文件系统这部分在真实的生产环境中产生的问题比较少不是我们今天的重点内容我们这节课不对它做过多关注你知道有这回事就可以了。
当然了很多同学也喜欢用free命令来查看系统中有多少Page Cache会根据buff/cache来判断存在多少Page Cache。如果你对free命令有所了解的话肯定知道free命令也是通过解析/proc/meminfo得出这些统计数据的这些都可以通过free工具的源码来找到。free命令的源码是开源你可以去看下[procfs](https://gitlab.com/procps-ng/procps)里的free.c文件源码是最直接的理解方式它会加深你对free命令的理解。
不过你是否好奇过free命令中的buff/cache究竟是指什么呢我们在这里先简单地看一下
```
$ free -k
total used free shared buff/cache available
Mem: 7926580 7277960 492392 10000 156228 430680
Swap: 8224764 380748 7844016
```
通过procfs源码里面的[proc/sysinfo.c](https://gitlab.com/procps-ng/procps/-/blob/master/proc/sysinfo.c)这个文件你可以发现buff/cache包括下面这几项
>
buff/cache = Buffers + Cached + SReclaimable
通过前面的数据我们也可以验证这个公式: 1224 + 111472 + 43532的和是156228。
另外,这里你要注意,你在做比较的过程中,一定要考虑到这些数据是动态变化的,而且执行命令本身也会带来内存开销,所以这个等式未必会严格相等,不过你不必怀疑它的正确性。
从这个公式中你能看到free命令中的buff/cache是由Buffers、Cached和SReclaimable这三项组成的它强调的是内存的可回收性也就是说可以被回收的内存会统计在这一项。
其中SReclaimable是指可以被回收的内核内存包括dentry和inode等。而这部分内容是内核非常细节性的东西对于应用开发者和运维人员理解起来相对有些难度所以我们在这里不多说。
掌握了Page Cache具体由哪些部分构成之后在它引发一些问题时你就能够知道需要去观察什么。比如说应用本身消耗内存RSS不多的情况下整个系统的内存使用率还是很高那不妨去排查下是不是Shmem(共享内存)消耗了太多内存导致的。
讲到这儿我想你应该对Page Cache有了一些直观的认识了吧当然了有的人可能会说内核的Page Cache这么复杂我不要不可以么
我相信有这样想法的人不在少数如果不用内核管理的Page Cache那有两种思路来进行处理
- 第一种应用程序维护自己的Cache做更加细粒度的控制比如MySQL就是这样做的你可以参考[MySQL Buffer Pool](https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html) 它的实现复杂度还是很高的。对于大多数应用而言实现自己的Cache成本还是挺高的不如内核的Page Cache来得简单高效。
- 第二种直接使用Direct I/O来绕过Page Cache不使用Cache了省的去管它了。这种方法可行么那我们继续用数据说话看看这种做法的问题在哪儿
## 为什么需要Page Cache
通过第一张图你其实已经可以直观地看到标准I/O和内存映射会先把数据写入到Page Cache这样做会通过减少I/O次数来提升读写效率。我们看一个具体的例子。首先我们来生成一个1G大小的新文件然后把Page Cache清空确保文件内容不在内存中以此来比较第一次读文件和第二次读文件耗时的差异。具体的流程如下。
先生成一个1G的文件
>
$ dd if=/dev/zero of=/home/yafang/test/dd.out bs=4096 count=$((1024*256))
其次清空Page Cache需要先执行一下sync来将脏页第二节课我会解释一下什么是脏页同步到磁盘再去drop cache。
>
$ sync &amp;&amp; echo 3 &gt; /proc/sys/vm/drop_caches
第一次读取文件的耗时如下:
```
$ time cat /home/yafang/test/dd.out &amp;&gt; /dev/null
real 0m5.733s
user 0m0.003s
sys 0m0.213s
```
再次读取文件的耗时如下:
```
$ time cat /home/yafang/test/dd.out &amp;&gt; /dev/null
real 0m0.132s
user 0m0.001s
sys 0m0.130s
```
通过这样详细的过程你可以看到第二次读取文件的耗时远小于第一次的耗时这是因为第一次是从磁盘来读取的内容磁盘I/O是比较耗时的而第二次读取的时候由于文件内容已经在第一次读取时被读到内存了所以是直接从内存读取的数据内存相比磁盘速度是快很多的。**这就是Page Cache存在的意义减少I/O提升应用的I/O速度。**
所以如果你不想为了很细致地管理内存而增加应用程序的复杂度那你还是乖乖使用内核管理的Page Cache吧它是ROI(投入产出比)相对较高的一个方案。
你要知道,我们在做方案抉择时找到一个各方面都很完美的方案还是比较难的,大多数情况下都是经过权衡后来选择一个合适的方案。因为,我一直坚信,合适的就是最好的。
而我之所以说Page Cache是合适的而不是说它是最好的那是因为Page Cache的不足之处也是有的这个不足之处主要体现在它对应用程序太过于透明以至于应用程序很难有好方法来控制它。
为什么这么说呢要想知道这个答案你就需要了解Page Cache的产生过程这里卖个关子我在下一讲会跟你讨论。
## 课堂总结
我们这节课主要是讲述了如何很好地理解Page Cache在我看来要想很好的理解它直观的方式就是从数据入手所以我从如何观测Page Cache出发来带你认识什么是Page Cache然后再从它为什么容易产生问题出发带你回顾了它存在的意义我希望通过这样的方式帮你明确这样几个要点
1. Page Cache是属于内核的不属于用户。
1. Page Cache对应用提升I/O效率而言是一个投入产出比较高的方案所以它的存在还是有必要的。
在我看来如何管理好Page Cache最主要的是你要知道如何来观测它以及观测关于它的一些行为有了这些数据做支撑你才能够把它和你的业务更好地结合起来。而且在我看来当你对某一个概念很模糊、搞不清楚它到底是什么时最好的认知方式就是先搞明白如何来观测它然后动手去观测看看它究竟是如何变化的正所谓纸上得来终觉浅绝知此事要躬行
这节课就讲到这里下一节我们使用数据来观察Page Cache的产生和释放这样一来你就能了解Page Cache的整个生命周期从而对于它引发的一些问题能有一个大概的判断。
## 课后作业
最后我给你留一道思考题请你写一个程序来构造出来Page Cache然后观察/proc/meminfo和/proc/vmstat里面的数据是如何变化的 欢迎在留言区分享你的看法。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@@ -0,0 +1,200 @@
<audio id="audio" title="02 基础篇 | Page Cache是怎样产生和释放的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e6/a8/e6f7e83d26b67cace8722a729fa02ea8.mp3"></audio>
你好,我是邵亚方。
上一讲我们主要讲了“什么是Page Cache”What“为什么需要Page Cache”Why我们这堂课还需要继续了解一下“How”**也就是Page Cache是如何产生和释放的。**
在我看来对Page Cache的“What-Why-How”都有所了解之后你才会对它引发的问题比如说Page Cache引起的load飙高问题或者应用程序的RT抖动问题更加了然于胸从而防范于未然。
其实Page Cache是如何产生和释放的通俗一点的说就是它的“生”分配与“死”释放即Page Cache的生命周期那么接下来我们就先来看一下它是如何“诞生”的。
## Page Cache是如何“诞生”的
Page Cache的产生有两种不同的方式
- Buffered I/O标准I/O
- Memory-Mapped I/O存储映射I/O
这两种方式分别都是如何产生Page Cache的呢来看下面这张图
<img src="https://static001.geekbang.org/resource/image/4e/52/4eb820e15a5560dee4b847227ee75752.jpg" alt="" title="Page Cache产生方式示意图">
从图中你可以看到虽然二者都能产生Page Cache但是二者的还是有些差异的
标准I/O是写的(write(2))用户缓冲区(Userpace Page对应的内存),然后再将用户缓冲区里的数据拷贝到内核缓冲区(Pagecache Page对应的内存);如果是读的(read(2))话则是先从内核缓冲区拷贝到用户缓冲区再从用户缓冲区读数据也就是buffer和文件内容不存在任何映射关系。
对于存储映射I/O而言则是直接将Pagecache Page给映射到用户地址空间用户直接读写Pagecache Page中内容。
显然存储映射I/O要比标准I/O效率高一些毕竟少了“用户空间到内核空间互相拷贝”的过程。这也是很多应用开发者发现为什么使用内存映射I/O比标准I/O方式性能要好一些的主要原因。
我们来用具体的例子演示一下Page Cache是如何“诞生”的就以其中的标准I/O为例因为这是我们最常使用的一种方式如下是一个简单的示例脚本
```
#!/bin/sh
#这是我们用来解析的文件
MEM_FILE=&quot;/proc/meminfo&quot;
#这是在该脚本中将要生成的一个新文件
NEW_FILE=&quot;/home/yafang/dd.write.out&quot;
#我们用来解析的Page Cache的具体项
active=0
inactive=0
pagecache=0
IFS=' '
#从/proc/meminfo中读取File Page Cache的大小
function get_filecache_size()
{
items=0
while read line
do
if [[ &quot;$line&quot; =~ &quot;Active:&quot; ]]; then
read -ra ADDR &lt;&lt;&lt;&quot;$line&quot;
active=${ADDR[1]}
let &quot;items=$items+1&quot;
elif [[ &quot;$line&quot; =~ &quot;Inactive:&quot; ]]; then
read -ra ADDR &lt;&lt;&lt;&quot;$line&quot;
inactive=${ADDR[1]}
let &quot;items=$items+1&quot;
fi
if [ $items -eq 2 ]; then
break;
fi
done &lt; $MEM_FILE
}
#读取File Page Cache的初始大小
get_filecache_size
let filecache=&quot;$active + $inactive&quot;
#写一个新文件该文件的大小为1048576 KB
dd if=/dev/zero of=$NEW_FILE bs=1024 count=1048576 &amp;&gt; /dev/null
#文件写完后再次读取File Page Cache的大小
get_filecache_size
#两次的差异可以近似为该新文件内容对应的File Page Cache
#之所以用近似是因为在运行的过程中也可能会有其他Page Cache产生
let size_increased=&quot;$active + $inactive - $filecache&quot;
#输出结果
echo &quot;File size 1048576KB, File Cache increased&quot; $size_inc
```
在这里我提醒你一下在运行该脚本前你要确保系统中有足够多的free内存避免内存紧张产生回收行为最终的测试结果是这样的
>
File size 1048576KB, File Cache increased 1048648KB
通过这个脚本你可以看到,在创建一个文件的过程中,代码中/proc/meminfo里的Active(file)和Inactive(file)这两项会随着文件内容的增加而增加它们增加的大小跟文件大小是一致的这里之所以略有不同是因为系统中还有其他程序在运行。另外如果你观察得很仔细的话你会发现增加的Page Cache是Inactive(File)这一项,**你可以去思考一下为什么会这样?**这里就作为咱们这节课的思考题。
当然,这个过程看似简单,但是它涉及的内核机制还是很多的,换句话说,可能引起问题的地方还是很多的,我们用一张图简单描述下这个过程:
<img src="https://static001.geekbang.org/resource/image/c7/5e/c728b8a189fb4e35e536cf131c4d9b5e.jpg" alt="">
这个过程大致可以描述为首先往用户缓冲区buffer(这是Userspace Page)写入数据然后buffer中的数据拷贝到内核缓冲区这是Pagecache Page如果内核缓冲区中还没有这个Page就会发生Page Fault会去分配一个Page拷贝结束后该Pagecache Page是一个Dirty Page脏页然后该Dirty Page中的内容会同步到磁盘同步到磁盘后该Pagecache Page变为Clean Page并且继续存在系统中。
我建议你可以将Alloc Page理解为Page Cache的“诞生”将Dirty Page理解为Page Cache的婴幼儿时期最容易生病的时期将Clean Page理解为Page Cache的成年时期在这个时期就很少会生病了
**但是请注意,并不是所有人都有童年的**比如孙悟空一出生就是成人了Page Cache也一样如果是读文件产生的Page Cache它的内容跟磁盘内容是一致的所以它一开始是Clean Page除非改写了里面的内容才会变成Dirty Page返老还童
就像我们为了让婴幼儿健康成长,要悉心照料他/她一样为了提前发现或者预防婴幼儿时期的Page Cache发病我们也需要一些手段来观测它
```
$ cat /proc/vmstat | egrep &quot;dirty|writeback&quot;
nr_dirty 40
nr_writeback 2
```
如上所示nr_dirty表示当前系统中积压了多少脏页nr_writeback则表示有多少脏页正在回写到磁盘中他们两个的单位都是Page(4KB)。
通常情况下小朋友们Dirty Pages聚集在一起脏页积压不会有什么问题但在非常时期比如流感期间就很容易导致聚集的小朋友越多病症就会越严重。与此类似Dirty Pages如果积压得过多在某些情况下也会容易引发问题至于是哪些情况又会出现哪些问题我们会在案例篇中具体讲解。
明白了Page Cache的“诞生”后我们再来看一下Page Cache的“死亡”它是如何被释放的
## Page Cache是如何“死亡”的
你可以把Page Cache的回收行为(Page Reclaim)理解为Page Cache的“自然死亡”。
言归正传我们知道服务器运行久了后系统中free的内存会越来越少用free命令来查看大部分都会是used内存或者buff/cache内存比如说下面这台生产环境中服务器的内存使用情况
```
$ free -g
total used free shared buff/cache available
Mem: 125 41 6 0 79 82
Swap: 0 0 0
```
free命令中的buff/cache中的这些就是“活着”的Page Cache那它们什么时候会“死亡”被回收我们来看一张图
<img src="https://static001.geekbang.org/resource/image/1d/bb/1d430c93e397e23d67d12e28827c4bbb.jpg" alt="">
你可以看到应用在申请内存的时候即使没有free内存只要还有足够可回收的Page Cache就可以通过回收Page Cache的方式来申请到内存**回收的方式主要是两种:直接回收和后台回收。**
那它是具体怎么回收的呢你要怎么观察呢其实在我看来观察Page Cache直接回收和后台回收最简单方便的方式是使用sar
```
$ sar -B 1
02:14:01 PM pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
02:14:01 PM 0.14 841.53 106745.40 0.00 41936.13 0.00 0.00 0.00 0.00
02:15:01 PM 5.84 840.97 86713.56 0.00 43612.15 717.81 0.00 717.66 99.98
02:16:01 PM 95.02 816.53 100707.84 0.13 46525.81 3557.90 0.00 3556.14 99.95
02:17:01 PM 10.56 901.38 122726.31 0.27 54936.13 8791.40 0.00 8790.17 99.99
02:18:01 PM 108.14 306.69 96519.75 1.15 67410.50 14315.98 31.48 14319.38 99.80
02:19:01 PM 5.97 489.67 88026.03 0.18 48526.07 1061.53 0.00 1061.42 99.99
```
借助上面这些指标,你可以更加明确地观察内存回收行为,下面是这些指标的具体含义:
- pgscank/s : kswapd(后台回收线程)每秒扫描的page个数。
- pgscand/s: Application在内存申请过程中每秒直接扫描的page个数。
- pgsteal/s: 扫描的page中每秒被回收的个数。
- %vmeff: pgsteal/(pgscank+pgscand), 回收效率越接近100说明系统越安全越接近0说明系统内存压力越大。
这几个指标也是通过解析/proc/vmstat里面的数据来得出的对应关系如下
<img src="https://static001.geekbang.org/resource/image/46/aa/4604ec0da3f87ec003317fb3337fa9aa.jpg" alt="">
关于这几个指标我说一个小插曲要知道如果Linux Kernel本身设计不当会给你带来困扰。所以如果你观察到应用程序的结果跟你的预期并不一致也有可能是因为内核设计上存在问题你可以对内核持适当的怀疑态度哦下面这个是我最近遇到的一个案例。
如果你对Linus有所了解的话应该会知道Linus对Linux Kernel设计的第一原则是“never break the user space”。很多内核开发者在设计内核特性的时候会忽略掉新特性对应用程序的影响比如在前段时间就有人(Google的一个内核开发者)提交了一个patch来修改内存回收这些指标的含义但是最终被我和另外一个人(Facebook的一个内核开发者)把他的这个改动给否决掉了。具体的细节并不是咱们这节课的重点,我就不多说了,我建议你在课下看这个讨论:[[PATCH] mm: vmscan: consistent update to pgsteal and pgscan](https://lore.kernel.org/linux-mm/20200507204913.18661-1-shakeelb@google.com/),可以看一下内核开发者们在设计内核特性时是如何思考的,这会有利于你更加全面的去了解整个系统,从而让你的应用程序更好地融入到系统中。
## 课堂总结
以上就是本节课的全部内容了本节课我们主要讲了Page Caceh是如何“诞生”的以及如何“死亡”的我要强调这样几个重点
- Page Cache是在应用程序读写文件的过程中产生的所以在读写文件之前你需要留意是否还有足够的内存来分配Page Cache
- Page Cache中的脏页很容易引起问题你要重点注意这一块
- 在系统可用内存不足的时候就会回收Page Cache来释放出来内存我建议你可以通过sar或者/proc/vmstat来观察这个行为从而更好的判断问题是否跟回收有关
总的来说Page Cache的生命周期对于应用程序而言是相对比较透明的即它的分配与回收都是由操作系统来进行管理的。正是因为这种“透明”的特征所以应用程序才会难以控制Page CachePage Cache才会容易引发那么多问题。在接下来的案例篇里我们就来看看究竟会引发什么样子的问题以及你正确的分析思路是什么样子的。
## 课后作业
因为每个人的关注点都不一样,对问题的理解也不一样。假如你是一个应用开发者,你会更加关注应用的性能和稳定性;假如你是一个运维人员,你会更加关注系统的稳定性;假如你是初学内核的开发者,你会想要关注内核的实现机制。
所以我留了不同的作业题主题是围绕“Inactive与Active Page Cache的关系”当然了对应的难度也不同
<li>
如果你是一名应用开发者那么我想问问你为什么第一次读写某个文件Page Cache是Inactive的如何让它变成Active的呢在什么情况下Active的又会变成Inactive的呢明白了这个问题你会对应用性能调优有更加深入的理解。
</li>
<li>
如果你是一名运维人员那么建议你思考一下系统中有哪些控制项可以影响Inactive与Active Page Cache的大小或者二者的比例
</li>
<li>
如果你是一名初学内核的开发者那么我想问你对于匿名页而言当产生一个匿名页后它会首先放在Active链表上而对于文件页而言当产生一个文件页后它会首先放在Inactive链表上。请问为什么会这样子这是合理的吗欢迎在留言区分享你的看法。
</li>
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@@ -0,0 +1,188 @@
<audio id="audio" title="03 案例篇 | 如何处理Page Cache难以回收产生的load飙高问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/06/2b/06fef6388306bdcc63edcf79ca73612b.mp3"></audio>
你好我是邵亚方。今天这节课我想跟你聊一聊怎么处理在生产环境中因为Page Cache管理不当引起的系统load飙高的问题。
相信你在平时的工作中应该会或多或少遇到过这些情形系统很卡顿敲命令响应非常慢应用程序的RT变得很高或者抖动得很厉害。在发生这些问题时很有可能也伴随着系统load飙得很高。
那这是什么原因导致的呢?据我观察,大多是有三种情况:
- 直接内存回收引起的load飙高
- 系统中脏页积压过多引起的load飙高
- 系统NUMA策略配置不当引起的load飙高。
这是应用开发者和运维人员向我咨询最多的几种情况。问题看似很简单,但如果对问题产生的原因理解得不深,解决起来就会很棘手,甚至配置得不好,还会带来负面的影响。
所以这节课我们一起来分析下这三种情况可以说搞清楚了这几种情况你差不多就能解决掉绝大部分Page Cache引起的load飙高问题了。如果你对问题的原因排查感兴趣也不要着急在第5讲我会带你学习load飙高问题的分析方法。
接下来,我们就来逐一分析下这几类情况。
## 直接内存回收引起load飙高或者业务时延抖动
直接内存回收是指在进程上下文同步进行内存回收那么它具体是怎么引起load飙高的呢
因为直接内存回收是在进程申请内存的过程中同步进行的回收而这个回收过程可能会消耗很多时间进而导致进程的后续行为都被迫等待这样就会造成很长时间的延迟以及系统的CPU利用率会升高最终引起load飙高。
我们详细地描述一下这个过程,为了尽量不涉及太多技术细节,我会用一张图来表示,这样你理解起来会更容易。
<img src="https://static001.geekbang.org/resource/image/fe/00/fe84eb2bd4956bbbdd5b0259df8c9400.jpg" alt="" title="内存回收过程">
从图里你可以看到在开始内存回收后首先进行后台异步回收上图中蓝色标记的地方这不会引起进程的延迟如果后台异步回收跟不上进程内存申请的速度就会开始同步阻塞回收导致延迟上图中红色和粉色标记的地方这就是引起load高的地址
那么针对直接内存回收引起load飙高或者业务RT抖动的问题一个解决方案就是**及早地触发后台回收来避免应用程序进行直接内存回收,那具体要怎么做呢?**
我们先来了解一下后台回收的原理,如图:
<img src="https://static001.geekbang.org/resource/image/44/72/44d471fdae7376eb13e6e6bfc70b3172.jpg" alt="">
它的意思是当内存水位低于watermark low时就会唤醒kswapd进行后台回收然后kswapd会一直回收到watermark high。
那么我们可以增大min_free_kbytes这个配置选项来及早地触发后台回收该选项最终控制的是内存回收水位不过内存回收水位是内核里面非常细节性的知识点我们可以先不去讨论。
>
vm.min_free_kbytes = 4194304
对于大于等于128G的系统而言将min_free_kbytes设置为4G比较合理这是我们在处理很多这种问题时总结出来的一个经验值既不造成较多的内存浪费又能避免掉绝大多数的直接内存回收。
该值的设置和总的物理内存并没有一个严格对应的关系我们在前面也说过如果配置不当会引起一些副作用所以在调整该值之前我的建议是你可以渐进式地增大该值比如先调整为1G观察sar -B中pgscand是否还有不为0的情况如果存在不为0的情况继续增加到2G再次观察是否还有不为0的情况来决定是否增大以此类推。
在这里你需要注意的是即使将该值增加得很大还是可能存在pgscand不为0的情况这个略复杂涉及到内存碎片和连续内存申请我们在此先不展开你知道有这么回事儿就可以了。那么这个时候你要考虑的是业务是否可以容忍如果可以容忍那就没有必要继续增加了也就是说增大该值并不是完全避免直接内存回收而是尽量将直接内存回收行为控制在业务可以容忍的范围内。
这个方法可以用在3.10.0以后的内核上对应的操作系统为CentOS-7以及之后更新的操作系统
当然了,这样做也有一些缺陷:提高了内存水位后,应用程序可以直接使用的内存量就会减少,这在一定程度上浪费了内存。所以在调整这一项之前,你需要先思考一下,**应用程序更加关注什么,如果关注延迟那就适当地增大该值,如果关注内存的使用量那就适当地调小该值。**
除此之外对CentOS-6(对应于2.6.32内核版本)而言,还有另外一种解决方案:
>
vm.extra_free_kbytes = 4194304
那就是将extra_free_kbytes 配置为4G。extra_free_kbytes在3.10以及以后的内核上都被废弃掉了,不过由于在生产环境中还存在大量的机器运行着较老版本内核,你使用到的也可能会是较老版本的内核,所以在这里还是有必要提一下。它的大致原理如下所示:
<img src="https://static001.geekbang.org/resource/image/7d/d2/7d9e537e23489cd4f5f34fedcd6f89d2.jpg" alt="">
extra_free_kbytes的目的是为了解决min_free_kbyte造成的内存浪费但是这种做法并没有被内核主线接收因为这种行为很难维护会带来一些麻烦感兴趣的可以看一下这个讨论[add extra free kbytes tunable](https://lkml.org/lkml/2013/2/17/210)
总的来说通过调整内存水位在一定程度上保障了应用的内存申请但是同时也带来了一定的内存浪费因为系统始终要保障有这么多的free内存这就压缩了Page Cache的空间。调整的效果你可以通过/proc/zoneinfo来观察
```
$ egrep &quot;min|low|high&quot; /proc/zoneinfo
...
min 7019
low 8773
high 10527
...
```
其中min、low、high分别对应上图中的三个内存水位。你可以观察一下调整前后min、low、high的变化。需要提醒你的是内存水位是针对每个内存zone进行设置的所以/proc/zoneinfo里面会有很多zone以及它们的内存水位你可以不用去关注这些细节。
## 系统中脏页过多引起load飙高
接下来我们分析下由于系统脏页过多引起load飙高的情况。在前一个案例中我们也提到直接回收过程中如果存在较多脏页就可能涉及在回收过程中进行回写这可能会造成非常大的延迟而且因为这个过程本身是阻塞式的所以又可能进一步导致系统中处于D状态的进程数增多最终的表现就是系统的load值很高。
我们来看一下这张图这是一个典型的脏页引起系统load值飙高的问题场景
<img src="https://static001.geekbang.org/resource/image/90/75/90c693c95d67cfaf89b86edbd1228d75.jpg" alt="">
如图所示如果系统中既有快速I/O设备又有慢速I/O设备比如图中的ceph RBD设备或者其他慢速存储设备比如HDD直接内存回收过程中遇到了正在往慢速I/O设备回写的page就可能导致非常大的延迟。
这里我多说一点。这类问题其实是不太好去追踪的为了更好追踪这种慢速I/O设备引起的抖动问题我也给Linux Kernel提交了一个patch来进行更好的追踪[mm/page-writeback: introduce tracepoint for wait_on_page_writeback()](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=19343b5bdd16ad4ae6b845ef829f68b683c4dfb5)这种做法是在原来的基础上增加了回写的设备这样子用户就能更好地将回写和具体设备关联起来从而判断问题是否是由慢速I/O设备导致的具体的分析方法我会在后面第5讲分析篇里重点来讲
那如何解决这类问题呢?一个比较省事的解决方案是控制好系统中积压的脏页数据。很多人知道需要控制脏页,但是往往并不清楚如何来控制好这个度,脏页控制的少了可能会影响系统整体的效率,脏页控制的多了还是会触发问题,所以我们接下来看下如何来衡量好这个“度”。
首先你可以通过sar -r来观察系统中的脏页个数
```
$ sar -r 1
07:30:01 PM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
09:20:01 PM 5681588 2137312 27.34 0 1807432 193016 2.47 534416 1310876 4
09:30:01 PM 5677564 2141336 27.39 0 1807500 204084 2.61 539192 1310884 20
09:40:01 PM 5679516 2139384 27.36 0 1807508 196696 2.52 536528 1310888 20
09:50:01 PM 5679548 2139352 27.36 0 1807516 196624 2.51 536152 1310892 24
```
kbdirty就是系统中的脏页大小它同样也是对/proc/vmstat中nr_dirty的解析。你可以通过调小如下设置来将系统脏页个数控制在一个合理范围:
>
<p>vm.dirty_background_bytes = 0<br>
vm.dirty_background_ratio = 10<br>
vm.dirty_bytes = 0<br>
vm.dirty_expire_centisecs = 3000<br>
vm.dirty_ratio = 20</p>
调整这些配置项有利有弊调大这些值会导致脏页的积压但是同时也可能减少了I/O的次数从而提升单次刷盘的效率调小这些值可以减少脏页的积压但是同时也增加了I/O的次数降低了I/O的效率。
**至于这些值调整大多少比较合适,也是因系统和业务的不同而异,我的建议也是一边调整一边观察,将这些值调整到业务可以容忍的程度就可以了,即在调整后需要观察业务的服务质量(SLA)要确保SLA在可接受范围内**。调整的效果你可以通过/proc/vmstat来查看
```
$ grep &quot;nr_dirty_&quot; /proc/vmstat
nr_dirty_threshold 366998
nr_dirty_background_threshold 183275
```
你可以观察一下调整前后这两项的变化。**这里我要给你一个避免踩坑的提示**解决该方案中的设置项如果设置不妥会触发一个内核Bug这是我在2017年进行性能调优时发现的一个内核Bug我给社区提交了一个patch将它fix掉了具体的commit见[writeback: schedule periodic writeback with sysctl](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=94af584692091347baea4d810b9fc6e0f5483d42) [](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=94af584692091347baea4d810b9fc6e0f5483d42) , commit log清晰地描述了该问题我建议你有时间看一看。
## 系统NUMA策略配置不当引起的load飙高
除了我前面提到的这两种引起系统load飙高或者业务延迟抖动的场景之外还有另外一种场景也会引起load飙高那就是系统NUMA策略配置不当引起的load飙高。
比如说我们在生产环境上就曾经遇到这样的问题系统中还有一半左右的free内存但还是频频触发direct reclaim导致业务抖动得比较厉害。后来经过排查发现是由于设置了zone_reclaim_mode这是NUMA策略的一种。
设置zone_reclaim_mode的目的是为了增加业务的NUMA亲和性但是在实际生产环境中很少会有对NUMA特别敏感的业务这也是为什么内核将该配置从默认配置1修改为了默认配置0: [mm: disable zone_reclaim_mode by default](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4f9b16a64753d0bb607454347036dc997fd03b82) 配置为0之后就避免了在其他node有空闲内存时不去使用这些空闲内存而是去回收当前node的Page Cache也就是说通过减少内存回收发生的可能性从而避免它引发的业务延迟。
那么如何来有效地衡量业务延迟问题是否由zone reclaim引起的呢它引起的延迟究竟有多大呢这个衡量和观察方法也是我贡献给Linux Kernel的[mm/vmscan: add tracepoints for node reclaim](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=132bb8cfc9e081238e7e2fd0c37c8c75ad0d2963) 大致的思路就是利用linux的tracepoint来做这种量化分析这是性能开销相对较小的一个方案。
我们可以通过numactl来查看服务器的NUMA信息如下是两个node的服务器
```
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 24 25 26 27 28 29 30 31 32 33 34 35
node 0 size: 130950 MB
node 0 free: 108256 MB
node 1 cpus: 12 13 14 15 16 17 18 19 20 21 22 23 36 37 38 39 40 41 42 43 44 45 46 47
node 1 size: 131072 MB
node 1 free: 122995 MB
node distances:
node 0 1
0: 10 21
1: 21 10
```
其中CPU0112435的local node为node 0而CPU12233647的local node为node 1。如下图所示
<img src="https://static001.geekbang.org/resource/image/80/e2/80e7c19a8f310d5bf30d368cef86bbe2.jpg" alt="">
推荐将zone_reclaim_mode配置为0。
>
vm.zone_reclaim_mode = 0
因为相比内存回收的危害而言NUMA带来的性能提升几乎可以忽略所以配置为0利远大于弊。
好了对于Page Cache管理不当引起的系统load飙高和业务时延抖动问题我们就分析到这里希望通过这篇的学习在下次你遇到直接内存回收引起的load飙高问题时不再束手无策。
总的来说这些问题都是Page Cache难以释放而产生的问题那你是否想过是不是Page Cache很容易释放就不会产生问题了这个答案可能会让你有些意料不到Page Cache容易释放也有容易释放的问题。这到底是怎么回事呢我们下节课来分析下这方面的案例。
## 课堂总结
这节课我们讲的这几个案例都是内存回收过程中引起的load飙高问题。关于内存回收这事我们可以做一个形象的类比。我们知道内存是操作系统中很重要的一个资源它就像我们在生活过程中很重要的一个资源——钱一样如果你的钱内存足够多那想买什么就可以买什么而不用担心钱花完内存用完后要吃土引起load飙高
但是现实情况是我们每个人用来双十一购物的钱(内存)总是有限的,在买东西(运行程序)的时候总需要精打细算,一旦预算快超了(内存快不够了),就得把一些不重要的东西(把一些不活跃的内容)从购物车里删除掉(回收掉),好腾出资金(空闲的内存)来买更想买的东西(运行需要运行的程序)。
我们讲的这几个案例都可以通过调整系统参数/配置来解决,调整系统参数/配置也是应用开发者和运维人员在发生了内核问题时所能做的改动。比如说直接内存回收引起load飙高时就去调整内存水位设置脏页积压引起load飙高时就需要去调整脏页的水位NUMA策略配置不当引起load飙高时就去检查是否需要关闭该策略。同时我们在做这些调整的时候一定要边调整边观察业务的服务质量确保SLA是可以接受的。
如果你想要你的系统更加稳定,你的业务性能更好,你不妨去研究一下系统中的可配置项,看看哪些配置可以帮助你的业务。
## 课后作业
这节课我给你布置的作业是针对直接内存回收的,现在你已经知道直接内存回收容易产生问题,是我们需要尽量避免的,那么我的问题是:请你执行一些模拟程序来构造出直接内存回收的场景(小提示: 你可以通过sar -B中的pgscand来判断是否有了直接内存回收。欢迎在留言区分享你的看法。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@@ -0,0 +1,207 @@
<audio id="audio" title="04 案例篇 | 如何处理Page Cache容易回收引起的业务性能问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5a/a7/5a1097233a4e86558c16e9320190c7a7.mp3"></audio>
你好我是邵亚方。我们在前一节课讲了Page Cache难以回收导致的load飙高问题这类问题是很直观的相信很多人都遭遇过。这节课我们则是来讲相反的一些问题即Page Cache太容易回收而引起的一些问题。
这类问题因为不直观所以陷阱会很多,应用开发者和运维人员会更容易踩坑,也正因为这类问题不直观,所以他们往往是一而再再而三地中招之后,才搞清楚问题到底是怎么回事。
我把大家经常遇到的这类问题做个总结,大致可以分为两方面:
- 误操作而导致Page Cache被回收掉进而导致业务性能下降明显
- 内核的一些机制导致业务Page Cache被回收从而引起性能下降。
如果你的业务对Page Cache比较敏感比如说你的业务数据对延迟很敏感或者再具体一点你的业务指标对TP9999分位要求较高那你对于这类性能问题应该多多少少有所接触。当然这并不意味着业务对延迟不敏感你就不需要关注这些问题了关注这类问题会让你对业务行为理解更深刻。
言归正传,我们来看下发生在生产环境中的案例。
## 对Page Cache操作不当产生的业务性能下降
我们先从一个相对简单的案例说起一起分析下误操作导致Page Cache被回收掉的情况它具体是怎样发生的。
我们知道对于Page Cache而言是可以通过drop_cache来清掉的很多人在看到系统中存在非常多的Page Cache时会习惯使用drop_cache来清理它们但是这样做是会有一些负面影响的比如说这些Page Cache被清理掉后可能会引起系统性能下降。为什么
其实这和inode有关那inode是什么意思呢inode是内存中对磁盘文件的索引进程在查找或者读取文件时就是通过inode来进行操作的我们用下面这张图来表示一下这种关系
<img src="https://static001.geekbang.org/resource/image/7e/99/7ef7747f71ef236e8cf5f9378d80da99.jpg" alt="">
如上图所示进程会通过inode来找到文件的地址空间address_space然后结合文件偏移会转换成page index来找具体的Page。如果该Page存在那就说明文件内容已经被读取到了内存如果该Page不存在那就说明不在内存中需要到磁盘中去读取。你可以理解为inode是Pagecache Page页缓存的页的宿主host如果inode不存在了那么PageCache Page也就不存在了。
如果你使用过drop_cache来释放inode的话应该会清楚它有几个控制选项我们可以通过写入不同的数值来释放不同类型的cache用户数据Page Cache内核数据Slab或者二者都释放这些选项你可以去看[Kernel Documentation的描述](https://www.kernel.org/doc/Documentation/sysctl/vm.txt)。
<img src="https://static001.geekbang.org/resource/image/ab/7d/ab748f14be603df45c2570fe8e24707d.jpg" alt="">
于是这样就引入了一个容易被我们忽略的问题:**当我们执行echo 2来drop slab的时候********它也会把Page Cache给drop掉**,很多运维人员都会忽视掉这一点。
在系统内存紧张的时候运维人员或者开发人员会想要通过drop_caches的方式来释放一些内存但是由于他们清楚Page Cache被释放掉会影响业务性能所以就期望只去drop slab而不去drop pagecache。于是很多人这个时候就运行 echo 2 &gt; /proc/sys/vm/drop_caches但是结果却出乎了他们的意料Page Cache也被释放掉了业务性能产生了明显的下降。
很多人都遇到过这个场景系统正在运行着忽然Page Cache被释放掉了由于不清楚释放的原因所以很多人就会怀疑是不是由其他人/程序执行了drop_caches导致的。那有没有办法来观察这个inode释放引起Page Cache被释放的行为呢答案是有的。关于这一点我们在下一节课会讲到。我们先来分析下如何观察是否有人或者有程序执行过drop_caches。
由于drop_caches是一种内存事件内核会在/proc/vmstat中来记录这一事件所以我们可以通过/proc/vmstat来判断是否有执行过drop_caches。
```
$ grep drop /proc/vmstat
drop_pagecache 3
drop_slab 2
```
如上所示它们分别意味着pagecache被drop了3次通过echo 1 或者echo 3slab被drop了2次通过echo 2或者echo 3。如果这两个值在问题发生前后没有变化那就可以排除是有人执行了drop_caches否则可以认为是因为drop_caches引起的Page Cache被回收。
针对这类问题你除了在执行drop cache前三思而后行之外还有其他的一些根治的解决方案。在讲这些解决方案之前我们先来看一个更加复杂一点的案例它们有一些共性解决方案也类似只是接下来这个案例涉及的内核机制更加复杂。
## 内核机制引起Page Cache被回收而产生的业务性能下降
我们在前面已经提到过在内存紧张的时候会触发内存回收内存回收会尝试去回收reclaimable可以被回收的内存这部分内存既包含Page Cache又包含reclaimable kernel memory(比如slab)。我们可以用下图来简单描述这个过程:
<img src="https://static001.geekbang.org/resource/image/d3/3c/d33a7264358f20fb063cb49fc3f3163c.jpg" alt="">
我简单来解释一下这个图。Reclaimer是指回收者它可以是内核线程包括kswapd也可以是用户线程。回收的时候它会依次来扫描pagecache page和slab page中有哪些可以被回收的如果有的话就会尝试去回收如果没有的话就跳过。在扫描可回收page的过程中回收者一开始扫描的较少然后逐渐增加扫描比例直至全部都被扫描完。这就是内存回收的大致过程。
接下来我所要讲述的案例就发生在“relcaim slab”中我们从前一个案例已然知道如果inode被回收的话那么它对应的Page Cache也都会被回收掉所以如果业务进程读取的文件对应的inode被回收了那么该文件所有的Page Cache都会被释放掉这也是容易引起性能问题的地方。
那这个行为是否有办法观察?这同样也是可以通过/proc/vmstat来观察的/proc/vmstat简直无所不能这也是为什么我会在之前说内核开发者更习惯去观察/proc/vmstat
```
$ grep inodesteal /proc/vmstat
pginodesteal 114341
kswapd_inodesteal 1291853
```
这个行为对应的事件是inodesteal就是上面这两个事件其中kswapd_inodesteal是指在kswapd回收的过程中因为回收inode而释放的pagecache page个数pginodesteal是指kswapd之外其他线程在回收过程中因为回收inode而释放的pagecache page个数。所以在你发现业务的Page Cache被释放掉后你可以通过观察来发现是否因为该事件导致的。
在明白了Page Cache被回收掉是如何发生的以及知道了该如何观察之后我们来看下该如何解决这类问题。
## 如何避免Page Cache被回收而引起的性能问题
我们在分析一些问题时往往都会想这个问题是我的模块有问题呢还是别人的模块有问题。也就是说是需要修改我的模块来解决问题还是需要修改其他模块来解决问题。与此类似避免Page Cache里相对比较重要的数据被回收掉的思路也是有两种
- 从应用代码层面来优化;
- 从系统层面来调整。
从应用程序代码层面来解决是相对比较彻底的方案因为应用更清楚哪些Page Cache是重要的哪些是不重要的所以就可以明确地来对读写文件过程中产生的Page Cache区别对待。比如说对于重要的数据可以通过mlock(2)来保护它防止被回收以及被drop对于不重要的数据比如日志那可以通过madvise(2)告诉内核来立即释放这些Page Cache。
我们来看一个通过mlock(2)来保护重要数据防止被回收或者被drop的例子
```
#include &lt;sys/mman.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/stat.h&gt;
#include &lt;unistd.h&gt;
#include &lt;string.h&gt;
#include &lt;fcntl.h&gt;
#define FILE_NAME &quot;/home/yafang/test/mmap/data&quot;
#define SIZE (1024*1000*1000)
int main()
{
int fd;
char *p;
int ret;
fd = open(FILE_NAME, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR);
if (fd &lt; 0)
return -1;
/* Set size of this file */
ret = ftruncate(fd, SIZE);
if (ret &lt; 0)
return -1;
/* The current offset is 0, so we don't need to reset the offset. */
/* lseek(fd, 0, SEEK_CUR); */
/* Mmap virtual memory */
p = mmap(0, SIZE, PROT_READ|PROT_WRITE, MAP_FILE|MAP_SHARED, fd, 0);
if (!p)
return -1;
/* Alloc physical memory */
memset(p, 1, SIZE);
/* Lock these memory to prevent from being reclaimed */
mlock(p, SIZE);
/* Wait until we kill it specifically */
while (1) {
sleep(10);
}
/*
* Unmap the memory.
* Actually the kernel will unmap it automatically after the
* process exits, whatever we call munamp() specifically or not.
*/
munmap(p, SIZE);
return 0;
}
```
在这个例子中我们通过mlock(2)来锁住了读FILE_NAME这个文件内容对应的Page Cache。在运行上述程序之后我们来看下该如何来观察这种行为确认这些Page Cache是否被保护住了被保护了多大。这同样可以通过/proc/meminfo来观察:
```
$ egrep &quot;Unevictable|Mlocked&quot; /proc/meminfo
Unevictable: 1000000 kB
Mlocked: 1000000 kB
```
然后你可以发现drop_caches或者内存回收是回收不了这些内容的我们的目的也就达到了。
在有些情况下对应用程序而言修改源码是件比较麻烦的事如果可以不修改源码来达到目的那就最好不过了。Linux内核同样实现了这种不改应用程序的源码而从系统层面调整来保护重要数据的机制这个机制就是memory cgroup protection。
它大致的思路是将需要保护的应用程序使用memory cgroup来保护起来这样该应用程序读写文件过程中所产生的Page Cache就会被保护起来不被回收或者最后被回收。memory cgroup protection大致的原理如下图所示
<img src="https://static001.geekbang.org/resource/image/71/cc/71e7347264a54773d902aa92535b87cc.jpg" alt="">
如上图所示memory cgroup提供了几个内存水位控制线memory.{min, low, high, max} 。
<li>
<p>**memory.max**<br>
这是指memory cgroup内的进程最多能够分配的内存如果不设置的话就默认不做内存大小的限制。</p>
</li>
<li>
<p>**memory.high**<br>
如果设置了这一项当memory cgroup内进程的内存使用量超过了该值后就会立即被回收掉所以这一项的目的是为了尽快的回收掉不活跃的Page Cache。</p>
</li>
<li>
<p>**memory.low**<br>
这一项是用来保护重要数据的当memory cgroup内进程的内存使用量低于了该值后在内存紧张触发回收后就会先去回收不属于该memory cgroup的Page Cache等到其他的Page Cache都被回收掉后再来回收这些Page Cache。</p>
</li>
<li>
<p>**memory.min**<br>
这一项同样是用来保护重要数据的只不过与memoy.low有所不同的是当memory cgroup内进程的内存使用量低于该值后即使其他不在该memory cgroup内的Page Cache都被回收完了也不会去回收这些Page Cache可以理解为这是用来保护最高优先级的数据的。</p>
</li>
那么,**如果你想要保护你的Page Cache不被回收你就可以考虑将你的业务进程放在一个memory cgroup中然后设置memory.{min,low} 来进行保护与之相反如果你想要尽快释放你的Page Cache那你可以考虑设置memory.high来及时的释放掉不活跃的Page Cache。**
更加细节性的一些设置我们就不在这里讨论了,我建议你可以自己动手来设置后观察一下,这样你理解会更深刻。
## 课堂总结
我们在前一篇讲到了Page Cache回收困难引起的load飙高问题这也是很直观的一类问题在这一篇讲述的则是一类相反的问题即Page Cache太容易被回收而引起的一些问题这一类问题是不那么直观的一类问题。
对于很直观的问题,我们相对比较容易去观察分析,而且由于它们比较容易观察,所以也相对能够得到重视;对于不直观的问题,则不是那么容易观察分析,相对而言它们也容易被忽视。
外在的特征不明显,并不意味着它产生的影响不严重,就好比皮肤受伤流血了,我们知道需要立即止血这个伤病也能很容易得到控制;如果是内伤,比如心肝脾肺肾有了问题,则容易被忽视,但是这些问题一旦积压久了往往造成很严重的后果。所以对于这类不直观的问题,我们还是需要去重视它,而且尽量做到提前预防,比如说:
- 如果你的业务对Page Cache所造成的延迟比较敏感那你最好可以去保护它比如通过mlock或者memory cgroup来对它们进行保护
- 在你不明白Page Cache是因为什么原因被释放时你可以通过/proc/vmstat里面的一些指标来观察找到具体的释放原因然后再对症下药的去做优化。
## 课后作业
这节课给你布置的作业是与mlock相关的请你思考下进程调用mlock()来保护内存然后进程没有运行munlock()就退出了,在进程退出后,这部分内存还被保护吗,为什么?欢迎在留言区分享你的看法。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@@ -0,0 +1,126 @@
<audio id="audio" title="05 分析篇 | 如何判断问题是否由Page Cache产生的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/41/3c/41cf9a8919b116e3e1b327d7b40d973c.mp3"></audio>
你好,我是邵亚方。
在前面几节课里我们讲了Page Cache的一些基础知识以及如何去处理Page Cache引发的一些问题。这节课我们来讲讲如何判断问题是不是由Page Cache引起的。
我们知道一个问题往往牵扯到操作系统的很多模块比如说当系统出现load飙高的问题时可能是Page Cache引起的也可能是锁冲突太厉害物理资源CPU、内存、磁盘I/O、网络I/O有争抢导致的也可能是内核特性设计缺陷导致的等等。
如果我们没有判断清楚问题是如何引起的而贸然采取措施非但无法解决问题反而会引起其他负面影响比如说load飙高本来是Page Cache引起的如果你没有查清楚原因而误以为是网络引起的然后对网络进行限流看起来把问题解决了但是系统运行久了还是会出现load飙高而且限流这种行为还降低了系统负载能力。
那么当问题发生时我们如何判断它是不是由Page Cache引起的呢
## Linux问题的典型分析手段
Linux上有一些典型的问题分析手段从这些基本的分析方法入手你可以一步步判断出问题根因。这些分析手段可以简单地归纳为下图
<img src="https://static001.geekbang.org/resource/image/ee/c1/ee08329fc5eb7fb8ddff14dba9ebf0c1.jpg" alt="" title="Linux的典型分析手段">
从这张图中我们可以看到Linux内核主要是通过/proc和/sys把系统信息导出给用户当你不清楚问题发生的原因时你就可以试着去这几个目录下读取一下系统信息看看哪些指标异常。比如当你不清楚问题是否由Page Cache引起时你可以试着去把/proc/vmstat里面的信息给读取出来看看哪些指标单位时间内变化较大。如果pgscan相关指标变化较大那就可能是Page Cache引起的因为pgscan代表了Page Cache的内存回收行为它变化较大往往意味着系统内存压力很紧张。
/proc和/sys里面的信息可以给我们指出一个问题分析的大致方向我们可以判断出问题是不是由Page Cache引起的但是如果想要深入地分析问题知道Page Cache是如何引起问题的我们还需要掌握更加专业的分析手段专业的分析工具有ftraceebpfperf等。
当然了,这些专业工具的学习成本也相对略高一些,但你不能觉得它难、成本高,就不学了,因为掌握了这些分析工具后,再遇到疑难杂症,你分析起来会更加得心应手。
为了让你在遇到问题时更加方便地找到合适的分析工具,我借用[Bredan Gregg的一张图](https://www.slideshare.net/brendangregg/velocity-2015-linux-perf-tools/107),并根据自己的经验,把这张图略作了一些改进,帮助你学习该如何使用这些分析工具:
<img src="https://static001.geekbang.org/resource/image/0c/97/0ccc072485d8ca2b995a6e7b6a75da97.jpg" alt="">
在这张图里整体上追踪方式分为了静态追踪预置了追踪点和动态追踪需要借助probe
- 如果你想要追踪的东西已经有了预置的追踪点,那你直接使用这些预置追踪点就可以了;
- 如果没有预置追踪点那你就要看看是否可以使用probe(包括kprobe和uprobe)来实现。
因为分析工具自身也会对业务造成一些影响Heisenbug比如说使用strace会阻塞进程的运行再比如使用systemtap也会有加载编译的开销等**所以我们在使用这些工具之前也需要去详细了解下这些工具的副作用,以免引起意料之外的问题**。
比如我多年以前在使用systemtap的guru专家模式的时候因为没有考虑到systemtap进程异常退出后可能不会卸载systemtap模块从而引发系统panic的问题。
上面这些就是Linux问题的一些典型分析方法了解了这些分析方法你再遇到问题就能知道该选择什么样的工具来去分析。对于Page Cache而言首先我们可以通过/proc/vmstat来做一个大致判断然后再结合Page Cache的tracepoint来做更加深入的分析。
接下来我们一起分析两个具体问题。
## 系统现在load很高是由Page Cache引起的吗
我相信你肯定会遇到过这种场景业务一直稳定运行着却忽然出现很大的性能抖动或者系统一直稳定运行着却忽然出现较高的load值那怎么去判断这个问题是不是由Page Cache引起的呢在这里我根据自己多年的经验总结了一些分析的步骤。
分析问题的第一步就是需要对系统的概括做一个了解对于Page Cahe相关的问题我推荐你**使用sar来采集Page Cache的概况**,它是系统默认配置好的工具,使用起来非常简单方便。
我在课程的第1讲也提到了对sar的一些使用比如通过sar -B来分析分页信息(Paging statistics) 以及sar -r来分析内存使用情况统计(Memory utilization statistics)等。在这里我特别推荐你使用sar里面记录的PSIPressure-Stall Information信息来查看Page Cache产生压力情况尤其是给业务产生的压力而这些压力最终都会体现在load上。不过该功能需要4.20以上的内核版本才支持同时sar的版本也要更新到12.3.3版本以上。比如PSI中表示内存压力的如下输出
```
some avg10=45.49 avg60=10.23 avg300=5.41 total=76464318
full avg10=40.87 avg60=9.05 avg300=4.29 total=58141082
```
你需要重点关注avg10这一列它表示最近10s内存的平均压力情况如果它很大比如大于40那load飙高大概率是由于内存压力尤其是Page Cache的压力引起的。
明白了概况之后我们还需要进一步查看究竟是Page Cache的什么行为引起的系统压力。
因为sar采集的只是一些常用的指标它并没有覆盖Page Cache的所有行为比如说内存规整memory compaction、业务workingset等这些容易引起load飙高的问题点。在我们想要分析更加具体的原因时就需要去采集这些指标了。通常在Page Cache出问题时这些指标中的一个或多个都会有异常这里我给你列出一些常见指标
<img src="https://static001.geekbang.org/resource/image/ed/bb/ed990308aef09a5918e6855362284dbb.jpg" alt="">
采集完这些指标后我们就可以分析Page Cache异常是由什么引起的了。比如说当我们发现单位时间内compact_fail变化很大时那往往意味着系统内存碎片很严重已经很难申请到连续物理内存了这时你就需要去调整碎片指数或者手动触发内存规整来减缓因为内存碎片引起的压力了。
我们在前面的步骤中采集的数据指标可以帮助我们来定位到问题点究竟是什么比如下面这些问题点。但是有的时候我们还需要知道是什么东西在进行连续内存的申请从而来做更加有针对性的调整这就需要进行进一步的观察了。我们可以利用内核预置的相关tracepoint来做更加细致的分析。
<img src="https://static001.geekbang.org/resource/image/f1/f0/f14faca88b5a765690a6c1540517def0.jpg" alt="">
我们继续以内存规整(memory compaction)为例来看下如何利用tracepoint来对它进行观察
```
#首先来使能compcation相关的一些tracepoing
$ echo 1 &gt;
/sys/kernel/debug/tracing/events/compaction/mm_compaction_begin/enable
$ echo 1 &gt;
/sys/kernel/debug/tracing/events/compaction/mm_compaction_end/enable
#然后来读取信息当compaction事件触发后就会有信息输出
$ cat /sys/kernel/debug/tracing/trace_pipe
&lt;...&gt;-49355 [037] .... 1578020.975159: mm_compaction_begin:
zone_start=0x2080000 migrate_pfn=0x2080000 free_pfn=0x3fe5800
zone_end=0x4080000, mode=async
&lt;...&gt;-49355 [037] .N.. 1578020.992136: mm_compaction_end:
zone_start=0x2080000 migrate_pfn=0x208f420 free_pfn=0x3f4b720
zone_end=0x4080000, mode=async status=contended
```
从这个例子中的信息里我们可以看到是49355这个进程触发了compactionbegin和end这两个tracepoint触发的时间戳相减就可以得到compaction给业务带来的延迟我们可以计算出这一次的延迟为17ms。
很多时候由于采集的信息量太大,我们往往需要借助一些自动化分析的工具来分析,这样会很高效。比如我之前写过一个[perf script](https://lore.kernel.org/linux-mm/20191001144524.GB3321@techsingularity.net/T/)来分析直接内存回收对业务造成的延迟。另外你也可以参考Brendan Gregg基于bcc(eBPF)写的[direct reclaim snoop](https://github.com/iovisor/bcc/blob/master/tools/drsnoop.py)来观察进程因为direct reclaim而导致的延迟。
## 系统load值在昨天飙得很高是由Page Cache引起的吗
上面的问题是实时发生的,对实时问题来说,因为有现场信息可供采集,所以相对好分析一些。但是有时候,我们没有办法及时地去搜集现场信息,比如问题发生在深夜时,我们没有来得及去采集现场信息,这个时候就只能查看历史记录了。
我们可以根据sar的日志信息来判断当时发生了什么事情。我之前就遇到过类似的问题。
曾经有一个业务反馈说RT抖动得比较明显让我帮他们分析一下抖动的原因我把业务RT抖动的时间和sar -B里的pgscand不为0的时刻相比较后发现二者在很多时候都是吻合的。于是我推断业务抖动跟Page Cache回收存在一些关系然后我让业务方调vm.min_free_kbytes来验证效果业务方将该值从初始值90112调整为4G后效果立竿见影就几乎没有抖动了。
在这里我想再次强调一遍调整vm.min_free_kbytes会存在一些风险如果系统本身内存回收已经很紧张再去调大它极有可能触发OOM甚至引起系统宕机。所以在调大的时候一定要先做一些检查看看此时是否可以调整。
当然了如果你的sysstat版本较新并且内核版本较高那你也可以观察PSI记录的日志信息是否跟业务抖动相吻合。根据sar的这些信息我们可以推断出故障是否跟Page Cache相关。
既然是通过sar的日志信息来评判那么对日志信息的丰富度就有一定要求。你需要对常见的一些问题做一些归纳总结然后把这些常见问题相关联的指标记录在日志中供事后分析这样可以帮助你更加全面地分析问题尤其是发生频率较高的一些问题。
比如曾经我们的业务经常发生一些业务抖动在通过我们上述的分析手段分析出来是compation引起的问题后而且这类问题较多我们便把/proc/vmstat里compaction相关的指标我们在上面的表格里有写到具体是哪些指标记录到我们日志系统中。在业务再次出现抖动后我们就可以根据日志信息来判断是否跟compaction相关了。
## 课堂回顾
好了这节课我们就讲到这里我们简单回顾一下。这节课我们讲了Page Cache问题的分析方法论按照这个方法论我们几乎可以分析清楚Page Cache相关的所有问题而且也能帮助我们了解业务的内存访问模式从而帮助我们更好地对业务来做优化。
当然这套分析方法论不仅仅适用于Page Cache引发的问题对于系统其他层面引起的问题同样也适用。让我们再次回顾一下这些要点
- 在观察Page Cache的行为时你可以先从最简单易用的分析工具比如sar入手来得到一个概况然后再使用更加专业一些的工具比如tracepoint去做更细致的分析。这样你就能分析清楚Page Cache的详细行为以及它为什么会产生问题
- 对于很多的偶发性的问题往往需要采集很多的信息才能抓取出来问题现场这种场景下最好使用perf script来写一些自动化分析的工具来提升效率
- 如果你担心分析工具会对生产环境产生性能影响你可以把信息采集下来之后进行离线分析或者使用ebpf来进行自动过滤分析请注意ebpf需要高版本内核的支持。
这是我沉淀下来的定位问题的方法。也希望你在遇到问题时不逃避,刨根问底寻找根本原因是什么,相信你一定也会有自己的问题分析方法论,然后在出现问题时能够快速高效地找到原因。
## 课后作业
假设现在内存紧张, 有很多进程都在进行直接内存回收,如何统计出来都是哪些进程在进行直接内存回收呢?欢迎在留言区分享你的看法。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。