OOM Killer
容器被杀掉只有一种情况,就是容器中的进程使用太多内存。即容器里所有进程使用的内存超过了容器所在 Memory Cgroup 里的内存限制。这时 Linux 就会主动杀死容器中的一个进程,往往这会导致整个容器的退出。
通过docker inspect可以看到 OOMKilled : true
。OOM Killer 就是在 Linux 里如果内存不足时,就需要杀死一个正在运行的进程来释放内存。
Linux 里的程序都是调用 malloc() 来申请内存,如果内存不足直接返回失败就可以,为什么还要去杀死正在运行的进程呢?
Linux 允许进程申请内存时是 overcommit 的,即允许进程申请超过实际物理内存上限的内存。
比如空闲物理内存只有 512MB,但是 malloc() 申请 600MB,这次申请还是被允许的。因为 malloc() 申请的是虚拟地址,系统只是给了地址范围,由于没有写入数据,并没有得到真正的物理内存。只有真的往这个地址写入数据时,才会分配。
这就像航空公司超售机票一样,overcommit 的申请模式可以有效提高系统的内存利用率。不过物理内存真的不够了又该怎么办呢?
Linux 采取的措施就是杀死某个正在运行的进程。
在 Linux 内核里有一个 oom_badness() 函数,它定义了选择进程的标准。函数中涉及两个条件:
- 进程已经使用的物理内存页面数。
- 每个进程的 OOM 校准值 oom_score_adj。在 /proc 文件系统中,每个进程都有一个 /proc//oom_score_adj 的接口文件。我们可以在这个文件中输入 -1000 到 1000 之间 的任意一个数值,调整进程被 OOM Kill 的几率
用系统总的可用页面数,去乘以 OOM 校准值 oom_score_adj,再加上进程已经使用的物理页面数,计算出来的值越大,这个进程被 OOM Kill 的几率也就越大。
Memory Cgroup
容器发生 OOM Kill 大多是因为 Memory Cgroup 的限制
Memory Cgroup 也是 Linux Cgroups 子系统之一,作用是对一组进程的 Memory 使用做限制。Memory Cgroup 的虚拟文件系统的挂载点一般在"/sys/fs/cgroup/memory"下。可以在挂载点目录下,创建一个子目录作为控制组。
控制组下有不少参数,这一讲只讲跟 OOM 相关的 3 个参数
memory.limit_in_bytes
,它限制一个控制组里所有进程可使用内存的最大值。
memory.oom_control
,当控制组中的进程内存使用达到上限时,这个参数能够决定会不会触发 OOM Killer。
default是触发控制组内的 OOM Killer,只要 echo 1 > memory.oom_control 就可以不触发了。
但这样操作后,就会影响到控制组中正在申请物理内存页面的进程。
这些进程会处于一个停止状态,不能往下运行了。
memory.usage_in_bytes
参数是只读的,是当前控制组里所有进程实际使用的内存总和。
控制组之间是树状的层级结构,父节点的控制组里的 memory.limit_in_bytes 可以限制子节点中所有进程的内存使用。
group1 里的 memory.limit_in_bytes 设置的值是 200MB,它的子控制组 group3 是 500MB。那么 group3 里所有进程使用的内存总值就不能超过 200MB,而不是 500MB。
解决问题
容器创建后,系统会为它建立一个 Memory Cgroup 的控制组,容器的所有进程都在这个控制组里。一旦容器中进程使用的内存达到了上限,OOM Killer 会杀死进程使容器退出。
怎样才能快速确定容器发生了 OOM 呢?可以通过查看内核日志发现。
使用 journal -k 命令,或者直接查看日志文件 /var/log/message,当容器发生 OOM Kill 时,内核会输出下面的这段信息:
- 容器里每一个进程使用的内存页面数量。在"rss"列里,"rss'是 Resident Set Size 的缩写,指进程真正在使用的物理内存页面数量。
比如下面的日志,init 进程的"rss"是 1 个页面,mem_alloc 进程的"rss"是 130801 个页面,内存页面的大小一般是 4KB,我们可以做个估算,130801 * 4KB 大致等 于 512MB。
-
"oom-kill:" 这行里列出了发生 OOM 的 Memroy Cgroup 的控制组,可以从控制组的信息中知道 OOM 是在哪个容器发生的。
-
"Killed process 7445 (mem_alloc)" 这行,它显示了最终被 OOM Killer 杀死的进程。
makefile
知道了哪个进程消耗了最大内存后就可以有针对性地对这个进程进行分析,一般有这两种情况:
- 进程的确需要很大的内存,我们给 memory.limit_in_bytes 里的内存上限设置小了
- 进程代码有 Bug,会导致内存泄漏。
总结
OOM Killer 是一种内存过载后的保护机制,通过牺牲个别的进程,来保证整个节点的内存不会被全部消耗掉。
Memory Cgroup 中每一个控制组可以对一组进程限制内存使用量,一旦所有进程使用内存的总量达到限制值,在default情况下,就会触发 OOM Killer, 控制组里的"某个进程"就会被杀死。
杀掉"某个进程"的选择标准,涉及到内核函数 oom_badness()。具体的计算方法是 :
系统总的可用页面数乘以进程的 OOM 校准值 oom_score_adj,再加上进程已经使用的物理页面数,计算出来的值越大,进程被 OOM Kill 的几率也就越大。