You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

131 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 08 案例篇 | Shmem进程没有消耗内存内存哪去了
你好,我是邵亚方。
在前一节课我们讲述了进程堆内存的泄漏以及因为内存泄漏而导致的OOM的危害。这节课我们继续讲其他类型的内存泄漏这样你在发现系统内存越来越少时就能够想到会是什么在消耗内存。
有的内存泄漏会体现在进程内存里面这种相对好观察些而有的内存泄漏就很难观察了因为它们无法通过观察进程消耗的内存来进行判断从而容易被忽视比如Shmem内存泄漏就属于这种容易被忽视的这节课我们重点来讲讲它。
## 进程没有消耗内存,内存哪去了?
我生产环境上就遇到过一个真实的案例。我们的运维人员发现某几台机器used已使用的内存越来越多但是通过top以及其他一些命令却检查不出来到底是谁在占用内存。随着可用内存变得越来越少业务进程也被OOM killer给杀掉这给业务带来了比较严重的影响。于是他们向我寻求帮助看看产生问题的原因是什么。
我在之前的课程中也提到过,在遇到系统内存不足时,我们首先要做的是查看/proc/meminfo中哪些内存类型消耗较多然后再去做针对性分析。但是如果你不清楚/proc/meminfo里面每一项的含义即使知道了哪几项内存出现了异常也不清楚该如何继续去分析。所以你最好是记住/proc/meminfo里每一项的含义。
回到我们这个案例,通过查看这几台服务器的/proc/meminfo发现是Shmem的大小有些异常
```
$ cat /proc/meminfo
...
Shmem 16777216 kB
...
```
那么Shmem这一项究竟是什么含义呢该如何去进一步分析到底是谁在使用Shmem呢
我们在前面的基础篇里提到Shmem是指匿名共享内存即进程以mmapMAP\_ANON|MAP\_SHARED这种方式来申请的内存。你可能会有疑问进程以这种方式来申请的内存不应该是属于进程的RESresident比如下面这个简单的示例
```
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>
#define SIZE (1024*1024*1024)
int main()
{
char *p;
p = mmap(NULL, SIZE, PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0);
if (!p)
return -1;
memset(p, 1, SIZE);
while (1) {
sleep(1);
}
return 0;
}
```
运行该程序后通过top可以看到确实会体现在进程的RES里面而且还同时体现在了进程的SHR里面也就是说如果进程是以mmap这种方式来申请内存的话我们是可以通过进程的内存消耗来观察到的。
但是在我们生产环境上遇到的问题各个进程的RES都不大看起来和/proc/meminfo中的Shmem完全对应不起来这又是为什么呢
先说答案这跟一种特殊的Shmem有关。我们知道磁盘的速度是远远低于内存的有些应用程序为了提升性能会避免将一些无需持续化存储的数据写入到磁盘而是把这部分临时数据写入到内存中然后定期或者在不需要这部分数据时清理掉这部分内容来释放出内存。在这种需求下就产生了一种特殊的Shmemtmpfs。tmpfs如下图所示
![](https://static001.geekbang.org/resource/image/24/eb/248b083e0c263b096c66f8078e3a3aeb.jpg)
它是一种内存文件系统只存在于内存中它无需应用程序去申请和释放内存而是操作系统自动来规划好一部分空间应用程序只需要往这里面写入数据就可以了这样会很方便。我们可以使用moun命令或者df命令来看系统中tmpfs的挂载点
```
$ df -h
Filesystem Size Used Avail Use% Mounted on
...
tmpfs 16G 15G 1G 94% /run
...
```
就像进程往磁盘写文件一样进程写完文件之后就把文件给关闭掉了这些文件和进程也就不再有关联所以这些磁盘文件的大小不会体现在进程中。同样地tmpfs中的文件也一样它也不会体现在进程的内存占用上。讲到这里你大概已经猜到了我们Shmem占用内存多是不是因为Shmem中的tmpfs较大导致的呢
tmpfs是属于文件系统的一种。对于文件系统我们都可以通过df来查看它的使用情况。所以呢我们也可以通过df来看是不是tmpfs占用的内存较多结果发现确实是它消耗了很多内存。这个问题就变得很清晰了我们只要去分析tmpfs中存储的是什么文件就可以了。
我们在生产环境上还遇到过这样一个问题systemd不停地往tmpfs中写入日志但是没有去及时清理而tmpfs配置的初始值又太大这就导致systemd产生的日志量越来越多最终可用内存越来越少。
针对这个问题解决方案就是限制systemd所使用的tmpfs的大小在日志量达到tmpfs大小限制时自动地清理掉临时日志或者定期清理掉这部分日志这都可以通过systemd的配置文件来做到。tmpfs的大小可以通过如下命令比如调整为2G调整
```
$ mount -o remount,size=2G /run
```
tmpfs作为一种特殊的Shmem它消耗的内存是不会体现在进程内存中的这往往会给问题排查带来一些难度。要想高效地分析这种类型的问题你必须要去熟悉系统中的内存类型。除了tmpfs之外其他一些类型的内存也不会体现在进程内存中比如内核消耗的内存/proc/meminfo中的Slab高速缓存、KernelStack内核栈和VmallocUsed内核通过vmalloc申请的内存这些也是你在不清楚内存被谁占用时需要去排查的。
如果tmpfs消耗的内存越积越多而得不到清理最终的结果也是系统可用内存不足然后触发OOM来杀掉进程。它很有可能会杀掉很重要的进程或者是那些你认为不应该被杀掉的进程。
## OOM杀进程的危害
OOM杀进程的逻辑大致如下图所示
![](https://static001.geekbang.org/resource/image/15/ee/150863953f090f09179e87814322a5ee.jpg)
OOM killer在杀进程的时候会把系统中可以被杀掉的进程扫描一遍根据进程占用的内存以及配置的oom\_score\_adj来计算出进程最终的得分然后把得分oom\_score最大的进程给杀掉如果得分最大的进程有多个那就把先扫描到的那个给杀掉。
进程的oom\_score可以通过/proc/\[pid\]/oom\_score来查看你可以扫描一下你系统中所有进程的oom\_score其中分值最大的那个就是在发生OOM时最先被杀掉的进程。不过你需要注意由于oom\_score和进程的内存开销有关而进程的内存开销又是会动态变化的所以该值也会动态变化。
如果你不想这个进程被首先杀掉那你可以调整该进程的oom\_score\_adj改变这个oom\_score如果你的进程无论如何都不能被杀掉那你可以将oom\_score\_adj配置为-1000。
通常而言我们都需要将一些很重要的系统服务的oom\_score\_adj配置为-1000比如sshd因为这些系统服务一旦被杀掉我们就很难再登陆进系统了。
但是,除了系统服务之外,不论你的业务程序有多重要,都尽量不要将它配置为-1000。因为你的业务程序一旦发生了内存泄漏而它又不能被杀掉这就会导致随着它的内存开销变大OOM killer不停地被唤醒从而把其他进程一个个给杀掉我们之前在生产环境中就遇到过类似的案例。
OOM killer的作用之一就是找到系统中不停泄漏内存的进程然后把它给杀掉如果没有找对那就会误杀其他进程甚至是误杀了更为重要的业务进程。
OOM killer除了会杀掉一些无辜进程外它选择杀进程的策略也未必是正确的。接下来又到了给内核找茬的时刻了这也是我们这个系列课程的目的告诉你如何来学些Linux内核但同时我也要告诉你要对内核有怀疑态度。下面这个案例就是一个内核的Bug。
在我们的一个服务器上我们发现OOM killer在杀进程的时候总是会杀掉最先扫描到的进程而由于先扫描到的进程的内存太小就导致OOM杀掉进程后很难释放出足够多的内存然后很快再次发生OOM。
这是在Kubernetes环境下触发的一个问题Kubernetes会将某些重要的容器配置为Guaranteed [对应的oom\_score\_adj为-998](https://kubernetes.io/docs/tasks/administer-cluster/out-of-resource/)以防止系统OOM的时候把该重要的容器给杀掉。 然而如果容器内部发生了OOM就会触发这个内核Bug导致总是杀掉最先扫描到的那个进程。
针对该内核Bug我也给社区贡献了一个patch[mm, oom: make the calculation of oom badness more accurate](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9066e5cfb73cdbcdbb49e87999482ab615e9fc76)来修复这个选择不到合适进程的问题在这个patch的commit log里我详细地描述了该问题感兴趣的话你可以去看下。
## 课堂总结
这节课我们学习了tmpfs这种类型的内存泄漏以及它的观察方法这种类型的内存泄漏和其他进程内存泄漏最大的不同是你很难通过进程消耗的内存来判断是哪里在泄漏因为这种类型的内存不会体现在进程的RES中。但是如果你熟悉内存问题的常规分析方法你就能很快地找到问题所在。
* 在不清楚内存被谁消耗时,你可以通过/proc/meminfo找到哪种类型的内存开销比较大然后再对这种类型的内存做针对性分析。
* 你需要配置合适的OOM策略oom\_score\_adj来防止重要的业务被过早杀掉比如将重要业务的oom\_score\_adj调小为负值同时你也需要考虑误杀其他进程你可以通过比较进程的/proc/\[pid\]/oom\_score来判断出进程被杀的先后顺序。
* 再次强调一遍,你需要学习内核,但同时你也需要对内核持怀疑态度。
总之,你对不同内存类型的特点了解越多,你在分析内存问题的时候(比如内存泄漏问题)就会更加高效。熟练掌握这些不同的内存类型,你也能够在业务需要申请内存时选择合适的内存类型。
## 课后作业
请你运行几个程序分别设置不同的oom\_score\_adj并记录下它们的oom\_score是什么样的然后消耗系统内存触发OOM看看oom\_score和进程被杀的顺序是什么关系。欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。