在介绍干货之前,先来运行一个小测试程序。这段小程序通过输入的参数数量不断申请内存资源:

#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

#define BLOCK_SIZE (1024*1024)

int main(int argc, char **argv)
{

        int thr, i;
        char *p1;

        if (argc != 2) {
                printf("Usage: mem_alloc <num (MB)>\n");
                exit(0);
        }

        thr = atoi(argv[1]);

        printf("Allocating," "set to %d Mbytes\n", thr);

        sleep(30);

        for (i = 0; i < thr; i++) {
                p1 = malloc(BLOCK_SIZE);
                memset(p1, 0x00, BLOCK_SIZE);
        }

        sleep(600);

        return 0;
}

Dockerfile:

FROM centos:8.1.1911

COPY ./mem_alloc /

CMD ["/mem_alloc", "2000"]

Makefile:

all: image

mem_alloc: mem_alloc.c
        gcc -o mem_alloc mem_alloc.c
image: mem_alloc
        docker build -t registry/mem_alloc:v1 .
clean:
        rm mem_alloc -f
        docker stop mem_alloc;
        docker rm mem_alloc;
        docker rmi registry/mem_alloc:v1

通过如下脚本启动容器:

#!/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

脚本输出如下:

容器放在哪个目录_物理内存

这时,我们通过docker ps查看运行着的docker容器,发现我们创建的容器mem_alloc存在并运行,稍等片刻,再次执行docker ps,则发现我们的容器不见了!

容器放在哪个目录_物理内存_02

执行docker ps -a查看所有容器状态,可以看到我们的容器mem_alloc已经退出,退出码为137。

容器放在哪个目录_Memory_03

通过docker inspect查看容器退出原因,是被OOM killed了。

容器放在哪个目录_容器放在哪个目录_04

好了,进入正题。

OOM Kill

OOM是Out of Memory的缩写,OOM Killer会在Linux系统中内存不足时,会杀死一个正在运行的进程来释放内存。

为什么内存不足的时候,不是malloc()函数返回失败,而是杀死正在运行的进程呢?这和Linux进程的内存申请策略有关。Linux进程申请内存时,允许申请超过实际物理内存上限的大小(称为overcommit),也就是说如果实际空闲物理内存只有512M了,但一个进程仍然可以申请600M的内存大小,因为malloc()申请的是内存的虚拟地址,系统只会给一个地址范围,由于没有写入数据,所以程序并没有得到真正的物理内存。只有在真正往申请的内存地址写入数据的时候,才会分配物理内存。这种策略可以有效提高系统的内存利用率,但一旦物理内存真的不够了,Linux采取的措施就是杀死某个正在运行的进程。

那么,在发生OOM时,Linux是根据什么标准来选择要杀死的进程呢?这就涉及到Linux内核中的oom_badness()函数,它定义了选择进程的标准。它的计算方式是这样的:用系统总的可用页面数,去乘以OOM校准值oom_score_adj,再加上进程已经使用的物理页面数,计算出来的值越大,这个进程被OOM Kill的几率也就越大。其中,oom_score_adj是进程的OOM校准值,每个进程都有一个/proc/<pid>/oom_score_adj的接口文件,我们可以在这个文件中输入-1000~1000之间的任一数值(默认值是0),来调整被OOM kill的几率。

Memory Cgroup

容器发生OOM Kill大多是因为Memory Cgroup的限制所导致。Momory Cgrou是Linux Cgroup的子系统之一,它的作用是对一组进程的Memory使用做限制。

Memory Cgroup的虚拟文件系统挂载点一般在/sys/fs/cgroup/memory目录下。和CPU Cgroup一样,Memory Cgroup也是树状结构,每一个分支都是一个控制组,每个控制组下有很多参数,其中跟OOM相关的3个重要参数为:memory.limit_in_bytes,memory.oom_control,memory.usage_in_bytes,如下图所示:

容器放在哪个目录_物理内存_05

memory.limit_in_bytes:这是每个Memory Cgroup控制组里最重要的一个参数了,控制组里所有进程可使用的内存最大值,就是由这个值决定的。由于是树状结构,父节点的memory.limit_in_bytes值会限制它的子节点中所有进程的内存使用,即使子节点的memory.limit_in_bytes值比父节点的大。

在我的系统里,这个值很大,远远超过了物理内存的大小(我的物理内存是32G):

cat memory.limit_in_bytes 
9223372036854771712

memory.oom_control:这个参数决定了当控制组中的内存使用量达到了最大值,会不会触发OOM Killer。缺省值会触发OOM Killer,试了修改我的系统中该文件中的值,发现是Permission denied的。

memory.usage_in_bytes:该值表示了当前控制组里所有进程实际使用的内存总和。该数值和memory.limit_in_bytes中的值约接近,OOM的风险越高。

好了,在有了以上的认识之后,我们就可以针对进程被OOM kill掉做一些处理,一种情况是进程本身的确需要很大的内存,但memory.limit_in_bytes的值设小了,那么就需要增大这个上限值;另一种情况就是代码bug,导致内存泄露,这就需要去解决代码中的问题了。