mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-18 23:23:43 +08:00
del
This commit is contained in:
179
极客时间专栏/geek/容器实战高手课/容器内存/08 | 容器内存:我的容器为什么被杀了?.md
Normal file
179
极客时间专栏/geek/容器实战高手课/容器内存/08 | 容器内存:我的容器为什么被杀了?.md
Normal file
@@ -0,0 +1,179 @@
|
||||
<audio id="audio" title="08 | 容器内存:我的容器为什么被杀了?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cf/53/cfefce1644c6c83d2e1dabc1b6fe0853.mp3"></audio>
|
||||
|
||||
你好,我是程远。
|
||||
|
||||
从这一讲内容开始,我们进入容器内存这个模块。在使用容器的时候,一定会伴随着Memory Cgroup。而Memory Cgroup给Linux原本就复杂的内存管理带来了新的变化,下面我们就一起来学习这一块内容。
|
||||
|
||||
今天这一讲,我们来解决容器在系统中消失的问题。
|
||||
|
||||
不知道你在使用容器时,有没有过这样的经历?一个容器在系统中运行一段时间后,突然消失了,看看自己程序的log文件,也没发现什么错误,不像是自己程序Crash,但是容器就是消失了。
|
||||
|
||||
那么这是怎么回事呢?接下来我们就一起来“破案”。
|
||||
|
||||
## 问题再现
|
||||
|
||||
容器在系统中被杀掉,其实只有一种情况,那就是容器中的进程使用了太多的内存。具体来说,就是容器里所有进程使用的内存量,超过了容器所在Memory Cgroup里的内存限制。这时Linux系统就会主动杀死容器中的一个进程,往往这会导致整个容器的退出。
|
||||
|
||||
我们可以做个简单的容器,模拟一下这种容器被杀死的场景。做容器的Dockerfile和代码,你可以从[这里](https://github.com/chengyli/training/tree/master/memory/oom)获得。
|
||||
|
||||
接下来,我们用下面的这个脚本来启动容器,我们先把这个容器的Cgroup内存上限设置为512MB(536870912 bytes)。
|
||||
|
||||
```
|
||||
#!/bin/bash
|
||||
docker stop mem_alloc;docker rm mem_alloc
|
||||
docker run -d --name mem_alloc registry/mem_alloc:v1
|
||||
|
||||
sleep 2
|
||||
CONTAINER_ID=$(sudo docker ps --format "{{.ID}}\t{{.Names}}" | grep -i mem_alloc | awk '{print $1}')
|
||||
echo $CONTAINER_ID
|
||||
|
||||
CGROUP_CONTAINER_PATH=$(find /sys/fs/cgroup/memory/ -name "*$CONTAINER_ID*")
|
||||
echo $CGROUP_CONTAINER_PATH
|
||||
|
||||
echo 536870912 > $CGROUP_CONTAINER_PATH/memory.limit_in_bytes
|
||||
cat $CGROUP_CONTAINER_PATH/memory.limit_in_bytes
|
||||
|
||||
```
|
||||
|
||||
好了,容器启动后,里面有一个小程序mem_alloc会不断地申请内存。当它申请的内存超过512MB的时候,你就会发现,我们启动的这个容器消失了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2d/5c/2db8dc6de63ae22c585e64fbf1a0395c.png" alt="">
|
||||
|
||||
这时候,如果我们运行`docker inspect` 命令查看容器退出的原因,就会看到容器处于"exited"状态,并且"OOMKilled"是true。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/da/2a/dafcb895b0c49b9d01b10d0bbac9102a.png" alt="">
|
||||
|
||||
那么问题来了,什么是OOM Killed呢?它和之前我们对容器Memory Cgroup做的设置有什么关系,又是怎么引起容器退出的?想搞清楚这些问题,我们就需要先理清楚基本概念。
|
||||
|
||||
### 如何理解OOM Killer?
|
||||
|
||||
我们先来看一看OOM Killer是什么意思。
|
||||
|
||||
OOM是Out of Memory的缩写,顾名思义就是内存不足的意思,而Killer在这里指需要杀死某个进程。那么OOM Killer就是**在Linux系统里如果内存不足时,就需要杀死一个正在运行的进程来释放一些内存。**
|
||||
|
||||
那么讲到这里,你可能会有个问题了,Linux里的程序都是调用malloc()来申请内存,如果内存不足,直接malloc()返回失败就可以,为什么还要去杀死正在运行的进程呢?
|
||||
|
||||
其实,这个和Linux进程的内存申请策略有关,Linux允许进程在申请内存的时候是overcommit的,这是什么意思呢?就是说允许进程申请超过实际物理内存上限的内存。
|
||||
|
||||
为了让你更好地理解,我给你举个例子说明。比如说,节点上的空闲物理内存只有512MB了,但是如果一个进程调用malloc()申请了600MB,那么malloc()的这次申请还是被允许的。
|
||||
|
||||
这是因为malloc()申请的是内存的虚拟地址,系统只是给了程序一个地址范围,由于没有写入数据,所以程序并没有得到真正的物理内存。物理内存只有程序真的往这个地址写入数据的时候,才会分配给程序。
|
||||
|
||||
可以看得出来,这种overcommit的内存申请模式可以带来一个好处,它可以有效提高系统的内存利用率。不过这也带来了一个问题,也许你已经猜到了,就是物理内存真的不够了,又该怎么办呢?
|
||||
|
||||
为了方便你理解,我给你打个比方,这个有点像航空公司在卖飞机票。售卖飞机票的时候往往是超售的。比如说实际上有100个位子,航空公司会卖105张机票,在登机的时候如果实际登机的乘客超过了100个,那么就需要按照一定规则,不允许多出的几位乘客登机了。
|
||||
|
||||
同样的道理,遇到内存不够的这种情况,Linux采取的措施就是杀死某个正在运行的进程。
|
||||
|
||||
那么你一定会问了,在发生OOM的时候,Linux到底是根据什么标准来选择被杀的进程呢?这就要提到一个在Linux内核里有一个 **oom_badness()函数**,就是它定义了选择进程的标准。其实这里的判断标准也很简单,函数中涉及两个条件:
|
||||
|
||||
第一,进程已经使用的物理内存页面数。
|
||||
|
||||
第二,每个进程的OOM校准值oom_score_adj。在/proc文件系统中,每个进程都有一个 /proc/<pid>/oom_score_adj的接口文件。我们可以在这个文件中输入-1000 到1000之间的任意一个数值,调整进程被OOM Kill的几率。
|
||||
|
||||
```
|
||||
adj = (long)p->signal->oom_score_adj;
|
||||
|
||||
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +mm_pgtables_bytes(p->mm) / PAGE_SIZE;
|
||||
|
||||
adj *= totalpages / 1000;
|
||||
points += adj;
|
||||
|
||||
```
|
||||
|
||||
结合前面说的两个条件,函数oom_badness()里的最终计算方法是这样的:
|
||||
|
||||
**用系统总的可用页面数,去乘以OOM校准值oom_score_adj,再加上进程已经使用的物理页面数,计算出来的值越大,那么这个进程被OOM Kill的几率也就越大。**
|
||||
|
||||
### 如何理解Memory Cgroup?
|
||||
|
||||
前面我们介绍了OOM Killer,容器发生OOM Kill大多是因为Memory Cgroup的限制所导致的,所以在我们还需要理解Memory Cgroup的运行机制。
|
||||
|
||||
在这个专栏的[第一讲](http://time.geekbang.org/column/article/308108)中,我们讲过Cgroups是容器的两大支柱技术之一,在CPU的章节中,我们也讲到了CPU Cgroups。那么按照同样的思路,我们想理解容器Memory,自然要讨论一下Memory Cgroup了。
|
||||
|
||||
Memory Cgroup也是Linux Cgroups子系统之一,它的作用是对一组进程的Memory使用做限制。Memory Cgroup的虚拟文件系统的挂载点一般在"/sys/fs/cgroup/memory"这个目录下,这个和CPU Cgroup类似。我们可以在Memory Cgroup的挂载点目录下,创建一个子目录作为控制组。
|
||||
|
||||
每一个控制组下面有不少参数,在这一讲里,这里我们只讲跟OOM最相关的3个参数:**memory.limit_in_bytes,memory.oom_control和memory.usage_in_bytes**。其他参数如果你有兴趣了解,可以参考内核的[文档说明](https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt)。
|
||||
|
||||
首先我们来看第一个参数,叫作memory.limit_in_bytes。请你注意,这个memory.limit_in_bytes是每个控制组里最重要的一个参数了。这是因为一个控制组里所有进程可使用内存的最大值,就是由这个参数的值来直接限制的。
|
||||
|
||||
那么一旦达到了最大值,在这个控制组里的进程会发生什么呢?
|
||||
|
||||
这就涉及到我要给你讲的第二个参数memory.oom_control了。这个memory.oom_control又是干啥的呢?当控制组中的进程内存使用达到上限值时,这个参数能够决定会不会触发OOM Killer。
|
||||
|
||||
如果没有人为设置的话,memory.oom_control的缺省值就会触发OOM Killer。这是一个控制组内的OOM Killer,和整个系统的OOM Killer的功能差不多,差别只是被杀进程的选择范围:控制组内的OOM Killer当然只能杀死控制组内的进程,而不能选节点上的其他进程。
|
||||
|
||||
如果我们要改变缺省值,也就是不希望触发OOM Killer,只要执行 `echo 1 > memory.oom_control` 就行了,这时候即使控制组里所有进程使用的内存达到memory.limit_in_bytes设置的上限值,控制组也不会杀掉里面的进程。
|
||||
|
||||
但是,我想提醒你,这样操作以后,就会影响到控制组中正在申请物理内存页面的进程。这些进程会处于一个停止状态,不能往下运行了。
|
||||
|
||||
最后,我们再来学习一下第三个参数,也就是memory.usage_in_bytes。这个参数是只读的,它里面的数值是当前控制组里所有进程实际使用的内存总和。
|
||||
|
||||
我们可以查看这个值,然后把它和memory.limit_in_bytes里的值做比较,根据接近程度来可以做个预判。这两个值越接近,OOM的风险越高。通过这个方法,我们就可以得知,当前控制组内使用总的内存量有没有OOM的风险了。
|
||||
|
||||
控制组之间也同样是树状的层级结构,在这个结构中,父节点的控制组里的memory.limit_in_bytes值,就可以限制它的子节点中所有进程的内存使用。
|
||||
|
||||
我用一个具体例子来说明,比如像下面图里展示的那样,group1里的memory.limit_in_bytes设置的值是200MB,它的子控制组group3里memory.limit_in_bytes值是500MB。那么,我们在group3里所有进程使用的内存总值就不能超过200MB,而不是500MB。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/07/6c65856f5dce81c064a63d6ffe0ca507.jpeg" alt=""><br>
|
||||
好了,我们这里介绍了Memory Cgroup最基本的概念,简单总结一下:
|
||||
|
||||
第一,Memory Cgroup中每一个控制组可以为一组进程限制内存使用量,一旦所有进程使用内存的总量达到限制值,缺省情况下,就会触发OOM Killer。这样一来,控制组里的“某个进程”就会被杀死。
|
||||
|
||||
第二,这里杀死“某个进程”的选择标准是,**控制组中总的可用页面乘以进程的oom_score_adj,加上进程已经使用的物理内存页面,所得值最大的进程,就会被系统选中杀死。**
|
||||
|
||||
## 解决问题
|
||||
|
||||
我们解释了Memory Cgroup和OOM Killer后,你应该明白了为什么容器在运行过程中会突然消失了。
|
||||
|
||||
对于每个容器创建后,系统都会为它建立一个Memory Cgroup的控制组,容器的所有进程都在这个控制组里。
|
||||
|
||||
一般的容器云平台,比如Kubernetes都会为容器设置一个内存使用的上限。这个内存的上限值会被写入Cgroup里,具体来说就是容器对应的Memory Cgroup控制组里memory.limit_in_bytes这个参数中。
|
||||
|
||||
所以,一旦容器中进程使用的内存达到了上限值,OOM Killer会杀死进程使容器退出。
|
||||
|
||||
**那么我们怎样才能快速确定容器发生了OOM呢?这个可以通过查看内核日志及时地发现。**
|
||||
|
||||
还是拿我们这一讲最开始发生OOM的容器作为例子。我们通过查看内核的日志,使用用 `journalctl -k` 命令,或者直接查看日志文件/var/log/message,我们会发现当容器发生OOM Kill的时候,内核会输出下面的这段信息,大致包含下面这三部分的信息:
|
||||
|
||||
第一个部分就是**容器里每一个进程使用的内存页面数量。**在"rss"列里,"rss'是Resident Set Size的缩写,指的就是进程真正在使用的物理内存页面数量。
|
||||
|
||||
比如下面的日志里,我们看到init进程的"rss"是1个页面,mem_alloc进程的"rss"是130801个页面,内存页面的大小一般是4KB,我们可以做个估算,130801 * 4KB大致等于512MB。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f6/ec/f681cd4d97a34ebb8a9458b7a0d5a9ec.png" alt="">
|
||||
|
||||
第二部分我们来看上面图片的 **"oom-kill:"** 这行,这一行里列出了发生OOM的Memroy Cgroup的控制组,我们可以从控制组的信息中知道OOM是在哪个容器发生的。
|
||||
|
||||
第三部分是图中 **"Killed process 7445 (mem_alloc)" 这行,它显示了最终被OOM Killer杀死的进程。**
|
||||
|
||||
我们通过了解内核日志里的这些信息,可以很快地判断出容器是因为OOM而退出的,并且还可以知道是哪个进程消耗了最多的Memory。
|
||||
|
||||
那么知道了哪个进程消耗了最大内存之后,我们就可以有针对性地对这个进程进行分析了,一般有这两种情况:
|
||||
|
||||
第一种情况是**这个进程本身的确需要很大的内存**,这说明我们给memory.limit_in_bytes里的内存上限值设置小了,那么就需要增大内存的上限值。
|
||||
|
||||
第二种情况是**进程的代码中有Bug,会导致内存泄漏,进程内存使用到达了Memory Cgroup中的上限。**如果是这种情况,就需要我们具体去解决代码里的问题了。
|
||||
|
||||
## 重点总结
|
||||
|
||||
这一讲我们从容器在系统中被杀的问题,学习了OOM Killer和Memory Cgroup这两个概念。
|
||||
|
||||
OOM Killer这个行为在Linux中很早就存在了,它其实是一种内存过载后的保护机制,通过牺牲个别的进程,来保证整个节点的内存不会被全部消耗掉。
|
||||
|
||||
在Cgroup的概念出现后,Memory Cgroup中每一个控制组可以对一组进程限制内存使用量,一旦所有进程使用内存的总量达到限制值,在缺省情况下,就会触发OOM Killer,控制组里的“某个进程”就会被杀死。
|
||||
|
||||
请注意,这里Linux系统肯定不能随心所欲地杀掉进程,那具体要用什么选择标准呢?
|
||||
|
||||
杀掉“某个进程”的选择标准,涉及到内核函数oom_badness()。具体的计算方法是 :**系统总的可用页面数乘以进程的OOM校准值oom_score_adj,再加上进程已经使用的物理页面数,计算出来的值越大,那么这个进程被OOM Kill的几率也就越大。**
|
||||
|
||||
接下来,我给你讲解了Memory Cgroup里最基本的三个参数,分别是**memory.limit_in_bytes, memory.oom_control 和 memory.usage_in_bytes。**我把这三个参数的作用,给你总结成了一张图。第一个和第三个参数,下一讲中我们还会用到,这里你可以先有个印象。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/81/2e3121a256b34bab80799002b2549881.jpeg" alt="">
|
||||
|
||||
容器因为OOM被杀,要如何处理呢?我们可以通过内核日志做排查,查看容器里内存使用最多的进程,然后对它进行分析。根据我的经验,解决思路要么是提高容器的最大内存限制,要么需要我们具体去解决进程代码的BUG。
|
||||
|
||||
## 思考题
|
||||
|
||||
在我们的例子[脚本](https://github.com/chengyli/training/blob/main/memory/oom/start_container.sh)基础上,你可以修改一下,在容器刚一启动,就在容器对应的Memory Cgroup中禁止OOM,看看接下来会发生什么?
|
||||
|
||||
欢迎留言和我分享你的想法和疑问。如果读完这篇文章有所收获,也欢迎分享给你的朋友。
|
||||
185
极客时间专栏/geek/容器实战高手课/容器内存/09 | Page Cache:为什么我的容器内存使用量总是在临界点?.md
Normal file
185
极客时间专栏/geek/容器实战高手课/容器内存/09 | Page Cache:为什么我的容器内存使用量总是在临界点?.md
Normal file
@@ -0,0 +1,185 @@
|
||||
<audio id="audio" title="09 | Page Cache:为什么我的容器内存使用量总是在临界点?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/45/46/45a99ac1436d94883a3aec4c3d9cd946.mp3"></audio>
|
||||
|
||||
你好,我是程远。
|
||||
|
||||
上一讲,我们讲了Memory Cgroup是如何控制一个容器的内存的。我们已经知道了,如果容器使用的物理内存超过了Memory Cgroup里的memory.limit_in_bytes值,那么容器中的进程会被OOM Killer杀死。
|
||||
|
||||
不过在一些容器的使用场景中,比如容器里的应用有很多文件读写,你会发现整个容器的内存使用量已经很接近Memory Cgroup的上限值了,但是在容器中我们接着再申请内存,还是可以申请出来,并且没有发生OOM。
|
||||
|
||||
这是怎么回事呢?今天这一讲我就来聊聊这个问题。
|
||||
|
||||
## 问题再现
|
||||
|
||||
我们可以用这里的[代码](https://github.com/chengyli/training/tree/main/memory/page_cache)做个容器镜像,然后用下面的这个脚本启动容器,并且设置容器Memory Cgroup里的内存上限值是100MB(104857600bytes)。
|
||||
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
docker stop page_cache;docker rm page_cache
|
||||
|
||||
if [ ! -f ./test.file ]
|
||||
then
|
||||
dd if=/dev/zero of=./test.file bs=4096 count=30000
|
||||
echo "Please run start_container.sh again "
|
||||
exit 0
|
||||
fi
|
||||
echo 3 > /proc/sys/vm/drop_caches
|
||||
sleep 10
|
||||
|
||||
docker run -d --init --name page_cache -v $(pwd):/mnt registry/page_cache_test:v1
|
||||
CONTAINER_ID=$(sudo docker ps --format "{{.ID}}\t{{.Names}}" | grep -i page_cache | awk '{print $1}')
|
||||
|
||||
echo $CONTAINER_ID
|
||||
CGROUP_CONTAINER_PATH=$(find /sys/fs/cgroup/memory/ -name "*$CONTAINER_ID*")
|
||||
echo 104857600 > $CGROUP_CONTAINER_PATH/memory.limit_in_bytes
|
||||
cat $CGROUP_CONTAINER_PATH/memory.limit_in_bytes
|
||||
|
||||
```
|
||||
|
||||
把容器启动起来后,我们查看一下容器的Memory Cgroup下的memory.limit_in_bytes和memory.usage_in_bytes这两个值。
|
||||
|
||||
如下图所示,我们可以看到容器内存的上限值设置为104857600bytes(100MB),而这时整个容器的已使用内存显示为104767488bytes,这个值已经非常接近上限值了。
|
||||
|
||||
我们把容器内存上限值和已使用的内存数值做个减法,104857600–104767488= 90112bytes,只差大概90KB左右的大小。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/11/2c/1192c3b6430dc1de84199e9da153502c.png" alt="">
|
||||
|
||||
但是,如果这时候我们继续启动一个程序,让这个程序申请并使用50MB的物理内存,就会发现这个程序还是可以运行成功,这时候容器并没有发生OOM的情况。
|
||||
|
||||
这时我们再去查看参数memory.usage_in_bytes,就会发现它的值变成了103186432bytes,比之前还少了一些。那这是怎么回事呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/79/7b/79ed17b066762a5385bf70758d4de87b.png" alt="">
|
||||
|
||||
## 知识详解:Linux系统有那些内存类型?
|
||||
|
||||
要解释刚才我们看到的容器里内存分配的现象,就需要先理解Linux操作系统里有哪几种内存的类型。
|
||||
|
||||
因为我们只有知道了内存的类型,才能明白每一种类型的内存,容器分别使用了多少。而且,对于不同类型的内存,一旦总内存增高到容器里内存最高限制的数值,相应的处理方式也不同。
|
||||
|
||||
## Linux内存类型
|
||||
|
||||
Linux的各个模块都需要内存,比如内核需要分配内存给页表,内核栈,还有slab,也就是内核各种数据结构的Cache Pool;用户态进程里的堆内存和栈的内存,共享库的内存,还有文件读写的Page Cache。
|
||||
|
||||
在这一讲里,我们讨论的Memory Cgroup里都不会对内核的内存做限制(比如页表,slab等)。所以我们今天主要讨论**与用户态相关的两个内存类型,RSS和Page Cache。**
|
||||
|
||||
### RSS
|
||||
|
||||
先看什么是RSS。RSS是Resident Set Size的缩写,简单来说它就是指进程真正申请到物理页面的内存大小。这是什么意思呢?
|
||||
|
||||
应用程序在申请内存的时候,比如说,调用malloc()来申请100MB的内存大小,malloc()返回成功了,这时候系统其实只是把100MB的虚拟地址空间分配给了进程,但是并没有把实际的物理内存页面分配给进程。
|
||||
|
||||
上一讲中,我给你讲过,当进程对这块内存地址开始做真正读写操作的时候,系统才会把实际需要的物理内存分配给进程。而这个过程中,进程真正得到的物理内存,就是这个RSS了。
|
||||
|
||||
比如下面的这段代码,我们先用malloc申请100MB的内存。
|
||||
|
||||
```
|
||||
p = malloc(100 * MB);
|
||||
if (p == NULL)
|
||||
return 0;
|
||||
|
||||
|
||||
```
|
||||
|
||||
然后,我们运行top命令查看这个程序在运行了malloc()之后的内存,我们可以看到这个程序的虚拟地址空间(VIRT)已经有了106728KB(~100MB),但是实际的物理内存RSS(top命令里显示的是RES,就是Resident的简写,和RSS是一个意思)在这里只有688KB。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fc/09/fce68702702bd94539357134ba32ab09.png" alt="">
|
||||
|
||||
接着我们在程序里等待30秒之后,我们再对这块申请的空间里写入20MB的数据。
|
||||
|
||||
```
|
||||
sleep(30);
|
||||
memset(p, 0x00, 20 * MB)
|
||||
|
||||
```
|
||||
|
||||
当我们用memset()函数对这块地址空间写入20MB的数据之后,我们再用top查看,这时候可以看到虚拟地址空间(VIRT)还是106728,不过物理内存RSS(RES)的值变成了21432(大小约为20MB), 这里的单位都是KB。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/ed/8ca5fda34f50166cbc48f6aab93479ed.png" alt="">
|
||||
|
||||
所以,通过刚才上面的小实验,我们可以验证RSS就是进程里真正获得的物理内存大小。
|
||||
|
||||
对于进程来说,RSS内存包含了进程的代码段内存,栈内存,堆内存,共享库的内存, 这些内存是进程运行所必须的。刚才我们通过malloc/memset得到的内存,就是属于堆内存。
|
||||
|
||||
具体的每一部分的RSS内存的大小,你可以查看/proc/[pid]/smaps文件。
|
||||
|
||||
### Page Cache
|
||||
|
||||
每个进程除了各自独立分配到的RSS内存外,如果进程对磁盘上的文件做了读写操作,Linux还会分配内存,把磁盘上读写到的页面存放在内存中,这部分的内存就是Page Cache。
|
||||
|
||||
Page Cache的主要作用是提高磁盘文件的读写性能,因为系统调用read()和write()的缺省行为都会把读过或者写过的页面存放在Page Cache里。
|
||||
|
||||
还是用我们这一讲最开始的的例子:代码程序去读取100MB的文件,在读取文件前,系统中Page Cache的大小是388MB,读取后Page Cache的大小是506MB,增长了大约100MB左右,多出来的这100MB,正是我们读取文件的大小。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/df/19e5319f0ac8821986efb005de8235df.png" alt="">
|
||||
|
||||
在Linux系统里只要有空闲的内存,系统就会自动地把读写过的磁盘文件页面放入到Page Cache里。那么这些内存都被Page Cache占用了,一旦进程需要用到更多的物理内存,执行malloc()调用做申请时,就会发现剩余的物理内存不够了,那该怎么办呢?
|
||||
|
||||
这就要提到Linux的内存管理机制了。** Linux的内存管理有一种内存页面回收机制(page frame reclaim),会根据系统里空闲物理内存是否低于某个阈值(wartermark),来决定是否启动内存的回收。**
|
||||
|
||||
内存回收的算法会根据不同类型的内存以及内存的最近最少用原则,就是LRU(Least Recently Used)算法决定哪些内存页面先被释放。因为Page Cache的内存页面只是起到Cache作用,自然是会被优先释放的。
|
||||
|
||||
所以,Page Cache是一种为了提高磁盘文件读写性能而利用空闲物理内存的机制。同时,内存管理中的页面回收机制,又能保证Cache所占用的页面可以及时释放,这样一来就不会影响程序对内存的真正需求了。
|
||||
|
||||
### RSS & Page Cache in Memory Cgroup
|
||||
|
||||
学习了RSS和Page Cache的基本概念之后,我们下面来看不同类型的内存,特别是RSS和Page Cache是如何影响Memory Cgroup的工作的。
|
||||
|
||||
我们先从Linux的内核代码看一下,从mem_cgroup_charge_statistics()这个函数里,我们可以看到Memory Cgroup也的确只是统计了RSS和Page Cache这两部分的内存。
|
||||
|
||||
RSS的内存,就是在当前Memory Cgroup控制组里所有进程的RSS的总和;而Page Cache这部分内存是控制组里的进程读写磁盘文件后,被放入到Page Cache里的物理内存。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a5/3e/a55c7d3e74e17yy613d83e220f93223e.png" alt="">
|
||||
|
||||
Memory Cgroup控制组里RSS内存和Page Cache内存的和,正好是memory.usage_in_bytes的值。
|
||||
|
||||
当控制组里的进程需要申请新的物理内存,而且memory.usage_in_bytes里的值超过控制组里的内存上限值memory.limit_in_bytes,这时我们前面说的Linux的内存回收(page frame reclaim)就会被调用起来。
|
||||
|
||||
那么在这个控制组里的page cache的内存会根据新申请的内存大小释放一部分,这样我们还是能成功申请到新的物理内存,整个控制组里总的物理内存开销memory.usage_in_bytes 还是不会超过上限值memory.limit_in_bytes。
|
||||
|
||||
## 解决问题
|
||||
|
||||
明白了Memory Cgroup中内存类型的统计方法,我们再回过头看这一讲开头的问题,为什么memory.usage_in_bytes与memory.limit_in_bytes的值只相差了90KB,我们在容器中还是可以申请出50MB的物理内存?
|
||||
|
||||
我想你应该已经知道答案了,容器里肯定有大于50MB的内存是Page Cache,因为作为Page Cache的内存在系统需要新申请物理内存的时候(作为RSS)是可以被释放的。
|
||||
|
||||
知道了这个答案,那么我们怎么来验证呢?验证的方法也挺简单的,在Memory Cgroup中有一个参数memory.stat,可以显示在当前控制组里各种内存类型的实际的开销。
|
||||
|
||||
那我们还是拿这一讲的容器例子,再跑一遍代码,这次要查看一下memory.stat里的数据。
|
||||
|
||||
第一步,我们还是用同样的[脚本](https://github.com/chengyli/training/blob/main/memory/page_cache/start_container.sh)来启动容器,并且设置好容器的Memory Cgroup里的memory.limit_in_bytes值为100MB。
|
||||
|
||||
启动容器后,这次我们不仅要看memory.usage_in_bytes的值,还要看一下memory.stat。虽然memory.stat里的参数有不少,但我们目前只需要关注"cache"和"rss"这两个值。
|
||||
|
||||
我们可以看到,容器启动后,cache,也就是Page Cache占的内存是99508224bytes,大概是99MB,而RSS占的内存只有1826816bytes,也就是1MB多一点。
|
||||
|
||||
这就意味着,在这个容器的Memory Cgroup里大部分的内存都被用作了Page Cache,而这部分内存是可以被回收的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/fe/b5c30dec6f760fbafd81de264b323dfe.png" alt="">
|
||||
|
||||
那么我们再执行一下我们的[mem_alloc程序](https://github.com/chengyli/training/blob/main/memory/page_cache/mem-alloc/mem_alloc.c),申请50MB的物理内存。
|
||||
|
||||
我们可以再来查看一下memory.stat,这时候cache的内存值降到了46632960bytes,大概46MB,而rss的内存值到了54759424bytes,54MB左右吧。总的memory.usage_in_bytes值和之前相比,没有太多的变化。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c6/8a/c6835495bd44817cb5c60069d491c38a.png" alt="">
|
||||
|
||||
从这里我们发现,Page Cache内存对我们判断容器实际内存使用率的影响,目前Page Cache完全就是Linux内核的一个自动的行为,只要读写磁盘文件,只要有空闲的内存,就会被用作Page Cache。
|
||||
|
||||
所以,判断容器真实的内存使用量,我们不能用Memory Cgroup里的memory.usage_in_bytes,而需要用memory.stat里的rss值。这个很像我们用free命令查看节点的可用内存,不能看"free"字段下的值,而要看除去Page Cache之后的"available"字段下的值。
|
||||
|
||||
## 重点总结
|
||||
|
||||
这一讲我想让你知道,每个容器的Memory Cgroup在统计每个控制组的内存使用时包含了两部分,RSS和Page Cache。
|
||||
|
||||
RSS是每个进程实际占用的物理内存,它包括了进程的代码段内存,进程运行时需要的堆和栈的内存,这部分内存是进程运行所必须的。
|
||||
|
||||
Page Cache是进程在运行中读写磁盘文件后,作为Cache而继续保留在内存中的,它的目的是**为了提高磁盘文件的读写性能。**
|
||||
|
||||
当节点的内存紧张或者Memory Cgroup控制组的内存达到上限的时候,Linux会对内存做回收操作,这个时候Page Cache的内存页面会被释放,这样空出来的内存就可以分配给新的内存申请。
|
||||
|
||||
正是Page Cache内存的这种Cache的特性,对于那些有频繁磁盘访问容器,我们往往会看到它的内存使用率一直接近容器内存的限制值(memory.limit_in_bytes)。但是这时候,我们并不需要担心它内存的不够, 我们在判断一个容器的内存使用状况的时候,可以把Page Cache这部分内存使用量忽略,而更多的考虑容器中RSS的内存使用量。
|
||||
|
||||
## 思考题
|
||||
|
||||
在容器里启动一个写磁盘文件的程序,写入100MB的数据,查看写入前和写入后,容器对应的Memory Cgroup里memory.usage_in_bytes的值以及memory.stat里的rss/cache值。
|
||||
|
||||
欢迎在留言区写下你的思考或疑问,我们一起交流探讨。如果这篇文章让你有所收获,也欢迎你分享给更多的朋友,一起学习进步。
|
||||
194
极客时间专栏/geek/容器实战高手课/容器内存/10 | Swap:容器可以使用Swap空间吗?.md
Normal file
194
极客时间专栏/geek/容器实战高手课/容器内存/10 | Swap:容器可以使用Swap空间吗?.md
Normal file
@@ -0,0 +1,194 @@
|
||||
<audio id="audio" title="10 | Swap:容器可以使用Swap空间吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/17/92/17878f4c07c7b04c1f30473d2a8ba492.mp3"></audio>
|
||||
|
||||
你好,我是程远。这一讲,我们来看看容器中是否可以使用Swap空间。
|
||||
|
||||
用过Linux的同学应该都很熟悉Swap空间了,简单来说它就是就是一块磁盘空间。
|
||||
|
||||
当内存写满的时候,就可以把内存中不常用的数据暂时写到这个Swap空间上。这样一来,内存空间就可以释放出来,用来满足新的内存申请的需求。
|
||||
|
||||
它的好处是可以**应对一些瞬时突发的内存增大需求**,不至于因为内存一时不够而触发OOM Killer,导致进程被杀死。
|
||||
|
||||
那么对于一个容器,特别是容器被设置了Memory Cgroup之后,它还可以使用Swap空间吗?会不会出现什么问题呢?
|
||||
|
||||
## 问题再现
|
||||
|
||||
接下来,我们就结合一个小例子,一起来看看吧。
|
||||
|
||||
首先,我们在一个有Swap空间的节点上启动一个容器,设置好它的Memory Cgroup的限制,一起来看看接下来会发生什么。
|
||||
|
||||
如果你的节点上没有Swap分区,也没有关系,你可以用下面的[这组命令](https://github.com/chengyli/training/blob/main/memory/swap/create_swap.sh)来新建一个。
|
||||
|
||||
这个例子里,Swap空间的大小是20G,你可以根据自己磁盘空闲空间来决定这个Swap的大小。执行完这组命令之后,我们来运行free命令,就可以看到Swap空间有20G。
|
||||
|
||||
输出的结果你可以参考下面的截图。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/33/5b/337a5efa84fc64f5a7ab2b12295e8b5b.png" alt="">
|
||||
|
||||
然后我们再启动一个容器,和OOM那一讲里的[例子](https://github.com/chengyli/training/blob/main/memory/oom/start_container.sh)差不多,容器的Memory Cgroup限制为512MB,容器中的mem_alloc程序去申请2GB内存。
|
||||
|
||||
你会发现,这次和上次OOM那一讲里的情况不一样了,并没有发生OOM导致容器退出的情况,容器运行得好好的。
|
||||
|
||||
从下面的图中,我们可以看到,mem_alloc进程的RSS内存一直在512MB(RES: 515596)左右。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f3/dd/f3be95c49af5bed1965654dd79db7bdd.png" alt=""><br>
|
||||
那我们再看一下Swap空间,使用了1.5GB (used 1542144KB)。输出的结果如下图,简单计算一下,1.5GB + 512MB,结果正好是mem_alloc这个程序申请的2GB内存。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/3f/e922df98666ab06e80f816d81e11883f.png" alt="">
|
||||
|
||||
通过刚刚的例子,你也许会这么想,因为有了Swap空间,本来会被OOM Kill的容器,可以好好地运行了。初看这样似乎也挺好的,不过你仔细想想,这样一来,Memory Cgroup对内存的限制不就失去了作用么?
|
||||
|
||||
我们再进一步分析,如果一个容器中的程序发生了内存泄漏(Memory leak),那么本来Memory Cgroup可以及时杀死这个进程,让它不影响整个节点中的其他应用程序。结果现在这个内存泄漏的进程没被杀死,还会不断地读写Swap磁盘,反而影响了整个节点的性能。
|
||||
|
||||
你看,这样一分析,对于运行容器的节点,你是不是又觉得应该禁止使用Swap了呢?
|
||||
|
||||
我想提醒你,不能一刀切地下结论,我们总是说,具体情况要具体分析,我们落地到具体的场景里,就会发现情况又没有原先我们想得那么简单。
|
||||
|
||||
比如说,某一类程序就是需要Swap空间,才能防止因为偶尔的内存突然增加而被OOM Killer杀死。因为这类程序重新启动的初始化时间会很长,这样程序重启的代价就很大了,也就是说,打开Swap对这类程序是有意义的。
|
||||
|
||||
这一类程序一旦放到容器中运行,就意味着它会和“别的容器”在同一个宿主机上共同运行,那如果这个“别的容器” 如果不需要Swap,而是希望Memory Cgroup的严格内存限制。
|
||||
|
||||
这样一来,在这一个宿主机上的两个容器就会有冲突了,我们应该怎么解决这个问题呢?要解决这个问题,我们先来看看Linux里的Swappiness这个概念,后面它可以帮到我们。
|
||||
|
||||
## 如何正确理解swappiness参数?
|
||||
|
||||
在普通Linux系统上,如果你使用过Swap空间,那么你可能配置过proc文件系统下的swappiness 这个参数 (/proc/sys/vm/swappiness)。swappiness的定义在[Linux 内核文档](https://www.kernel.org/doc/Documentation/sysctl/vm.txt)中可以找到,就是下面这段话。
|
||||
|
||||
>
|
||||
swappiness
|
||||
|
||||
|
||||
>
|
||||
This control is used to define how aggressive the kernel will swap memory pages. Higher values will increase aggressiveness, lower values decrease the amount of swap. A value of 0 instructs the kernel not to initiate swap until the amount of free and file-backed pages is less than the high water mark in a zone.
|
||||
|
||||
|
||||
>
|
||||
The default value is 60.
|
||||
|
||||
|
||||
前面两句话大致翻译过来,意思就是 **swappiness可以决定系统将会有多频繁地使用交换分区。**
|
||||
|
||||
一个较高的值会使得内核更频繁地使用交换分区,而一个较低的取值,则代表着内核会尽量避免使用交换分区。swappiness的取值范围是0–100,缺省值60。
|
||||
|
||||
我第一次读到这个定义,再知道了这个取值范围后,我觉得这是一个百分比值,也就是定义了使用Swap空间的频率。
|
||||
|
||||
当这个值是100的时候,哪怕还有空闲内存,也会去做内存交换,尽量把内存数据写入到Swap空间里;值是0的时候,基本上就不做内存交换了,也就不写Swap空间了。
|
||||
|
||||
后来再回顾的时候,我发现这个想法不能说是完全错的,但是想得简单了些。那这段swappiness的定义,应该怎么正确地理解呢?
|
||||
|
||||
你还记得,我们在上一讲里说过的两种内存类型Page Cache 和RSS么?
|
||||
|
||||
在有磁盘文件访问的时候,Linux会尽量把系统的空闲内存用作Page Cache来提高文件的读写性能。在没有打开Swap空间的情况下,一旦内存不够,这种情况下就只能把Page Cache释放了,而RSS内存是不能释放的。
|
||||
|
||||
在RSS里的内存,大部分都是没有对应磁盘文件的内存,比如用malloc()申请得到的内存,这种内存也被称为**匿名内存(Anonymous memory)**。那么当Swap空间打开后,可以写入Swap空间的,就是这些匿名内存。
|
||||
|
||||
所以在Swap空间打开的时候,问题也就来了,在内存紧张的时候,Linux系统怎么决定是先释放Page Cache,还是先把匿名内存释放并写入到Swap空间里呢?
|
||||
|
||||
我们一起来分析分析,都可能发生怎样的情况。最可能发生的是下面两种情况:
|
||||
|
||||
第一种情况是,如果系统先把Page Cache都释放了,那么一旦节点里有频繁的文件读写操作,系统的性能就会下降。
|
||||
|
||||
还有另一种情况,如果Linux系统先把匿名内存都释放并写入到Swap,那么一旦这些被释放的匿名内存马上需要使用,又需要从Swap空间读回到内存中,这样又会让Swap(其实也是磁盘)的读写频繁,导致系统性能下降。
|
||||
|
||||
显然,我们在释放内存的时候,需要平衡Page Cache的释放和匿名内存的释放,而swappiness,就是用来定义这个平衡的参数。
|
||||
|
||||
那么swappiness具体是怎么来控制这个平衡的?我们看一下在Linux内核代码里是怎么用这个swappiness参数。
|
||||
|
||||
我们前面说了swappiness的这个值的范围是0到100,但是请你一定要注意,**它不是一个百分比,更像是一个权重**。它是用来定义Page Cache内存和匿名内存的释放的一个比例。
|
||||
|
||||
我结合下面的这段代码具体给你讲一讲。
|
||||
|
||||
我们可以看到,这个比例是anon_prio: file_prio,这里anon_prio的值就等于swappiness。下面我们分三个情况做讨论:
|
||||
|
||||
第一种情况,当swappiness的值是100的时候,匿名内存和Page Cache内存的释放比例就是100: 100,也就是等比例释放了。
|
||||
|
||||
第二种情况,就是swappiness缺省值是60的时候,匿名内存和Page Cache内存的释放比例就是60 : 140,Page Cache内存的释放要优先于匿名内存。
|
||||
|
||||
```
|
||||
/*
|
||||
* With swappiness at 100, anonymous and file have the same priority.
|
||||
* This scanning priority is essentially the inverse of IO cost.
|
||||
*/
|
||||
|
||||
anon_prio = swappiness;
|
||||
file_prio = 200 - anon_prio;
|
||||
|
||||
```
|
||||
|
||||
还有一种情况, 当swappiness的值是0的时候,会发生什么呢?这种情况下,Linux系统是不允许匿名内存写入Swap空间了吗?
|
||||
|
||||
我们可以回到前面,再看一下那段swappiness的英文定义,里面特别强调了swappiness为0的情况。
|
||||
|
||||
当空闲内存少于内存一个zone的"high water mark"中的值的时候,Linux还是会做内存交换,也就是把匿名内存写入到Swap空间后释放内存。
|
||||
|
||||
在这里zone是Linux划分物理内存的一个区域,里面有3个水位线(water mark),水位线可以用来警示空闲内存的紧张程度。
|
||||
|
||||
这里我们可以再做个试验来验证一下,先运行 `echo 0 > /proc/sys/vm/swappiness` 命令把swappiness设置为0, 然后用我们之前例子里的mem_alloc程序来申请内存。
|
||||
|
||||
比如我们的这个节点上内存有12GB,同时有2GB的Swap,用mem_alloc申请12GB的内存,我们可以看到Swap空间在mem_alloc调用之前,used=0,输出结果如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/35/50/35b806bd26e089506d909d31f1e87550.png" alt="">
|
||||
|
||||
接下来,调用mem_alloc之后,Swap空间就被使用了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e2/ec/e245d137131b1yyc1e169a24fd10b5ec.png" alt="">
|
||||
|
||||
因为mem_alloc申请12GB内存已经和节点最大内存差不多了,我们如果查看 `cat /proc/zoneinfo` ,也可以看到normal zone里high (water mark)的值和free的值差不多,这样在free<high的时候,系统就会回收匿名内存页面并写入Swap空间。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/21/1d/212f4dde0982f656610cd8a3b293051d.png" alt=""><br>
|
||||
好了,在这里我们介绍了Linux系统里swappiness的概念,它是用来决定在内存紧张时候,回收匿名内存和Page Cache内存的比例。
|
||||
|
||||
**swappiness的取值范围在0到100,值为100的时候系统平等回收匿名内存和Page Cache内存;一般缺省值为60,就是优先回收Page Cache;即使swappiness为0,也不能完全禁止Swap分区的使用,就是说在内存紧张的时候,也会使用Swap来回收匿名内存。**
|
||||
|
||||
## 解决问题
|
||||
|
||||
那么运行了容器,使用了Memory Cgroup之后,swappiness怎么工作呢?
|
||||
|
||||
如果你查看一下Memory Cgroup控制组下面的参数,你会看到有一个memory.swappiness参数。这个参数是干啥的呢?
|
||||
|
||||
memory.swappiness可以控制这个Memroy Cgroup控制组下面匿名内存和page cache的回收,取值的范围和工作方式和全局的swappiness差不多。这里有一个优先顺序,在Memory Cgorup的控制组里,如果你设置了memory.swappiness参数,它就会覆盖全局的swappiness,让全局的swappiness在这个控制组里不起作用。
|
||||
|
||||
不过,这里有一点不同,需要你留意:**当memory.swappiness = 0的时候,对匿名页的回收是始终禁止的,也就是始终都不会使用Swap空间。**
|
||||
|
||||
这时Linux系统不会再去比较free内存和zone里的high water mark的值,再决定一个Memory Cgroup中的匿名内存要不要回收了。
|
||||
|
||||
请你注意,当我们设置了"memory.swappiness=0时,在Memory Cgroup中的进程,就不会再使用Swap空间,知道这一点很重要。
|
||||
|
||||
我们可以跑个容器试一试,还是在一个有Swap空间的节点上运行,运行和这一讲开始一样的容器,唯一不同的是把容器对应Memory Cgroup里的memory.swappiness设置为0。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/46/a7/46a1a06abfa2f817570c8cyy5faa62a7.png" alt="">
|
||||
|
||||
这次我们在容器中申请内存之后,Swap空间就没有被使用了,而当容器申请的内存超过memory.limit_in_bytes之后,就发生了OOM Kill。
|
||||
|
||||
好了,有了"memory.swappiness = 0"的配置和功能,就可以解决我们在这一讲里最开始提出的问题了。
|
||||
|
||||
在同一个宿主机上,假设同时存在容器A和其他容器,容器A上运行着需要使用Swap空间的应用,而别的容器不需要使用Swap空间。
|
||||
|
||||
那么,我们还是可以在宿主机节点上打开Swap空间,同时在其他容器对应的Memory Cgroups控制组里,把memory.swappiness这个参数设置为0。这样一来,我们不但满足了容器A的需求,而且别的容器也不会受到影响,仍然可以严格按照Memory Cgroups里的memory.limit_in_bytes来限制内存的使用。
|
||||
|
||||
总之,memory.swappiness这个参数很有用,通过它可以让需要使用Swap空间的容器和不需要Swap的容器,同时运行在同一个宿主机上。
|
||||
|
||||
## 重点总结
|
||||
|
||||
这一讲,我们主要讨论的问题是在容器中是否可以使用Swap?
|
||||
|
||||
这个问题没有看起来那么简单。当然了,只要在宿主机节点上打开Swap空间,在容器中就是可以用到Swap的。但出现的问题是在同一个宿主机上,对于不需要使用swap的容器, 它的Memory Cgroups的限制也失去了作用。
|
||||
|
||||
针对这个问题,我们学习了Linux中的swappiness这个参数。swappiness参数值的作用是,在系统里有Swap空间之后,当系统需要回收内存的时候,是优先释放Page Cache中的内存,还是优先释放匿名内存(也就是写入Swap)。
|
||||
|
||||
swappiness的取值范围在0到100之间,我们可以记住下面三个值:
|
||||
|
||||
- 值为100的时候, 释放Page Cache和匿名内存是同等优先级的。
|
||||
- 值为60,这是大多数Linux系统的缺省值,这时候Page Cache的释放优先级高于匿名内存的释放。
|
||||
- 值为0的时候,当系统中空闲内存低于一个临界值的时候,仍然会释放匿名内存并把页面写入Swap空间。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6a/11/6aa89d2b88493ddeb7d37ab9db275811.jpeg" alt="">
|
||||
|
||||
swappiness参数除了在proc文件系统下有个全局的值外,在每个Memory Cgroup控制组里也有一个memory.swappiness,那它们有什么不同呢?
|
||||
|
||||
不同就是每个Memory Cgroup控制组里的swappiness参数值为0的时候,就可以让控制组里的内存停止写入Swap。这样一来,有了memory.swappiness这个参数后,需要使用Swap和不需要Swap的容器就可以在同一个宿主机上同时运行了,这样对于硬件资源的利用率也就更高了。
|
||||
|
||||
## 思考题
|
||||
|
||||
在一个有Swap分区的节点上用Docker启动一个容器,对它的Memory Cgroup控制组设置一个内存上限N,并且将memory.swappiness设置为0。这时,如果在容器中启动一个不断读写文件的程序,同时这个程序再申请1/2N的内存,请你判断一下,Swap分区中会有数据写入吗?
|
||||
|
||||
欢迎在留言区分享你的收获和疑问。如果这篇文章让你有所收获,也欢迎分享给你的朋友,一起交流和学习。
|
||||
Reference in New Issue
Block a user