Linux cgroup - memory子系统讲解

11 minute read

背景

Linux是一个很好的多用户平台,但是当我们在Linux中运行多个资源耗费很大的应用(比如数据库)时,应用间的资源争抢可能就比较严重。

那么有什么好的方法可以隔离不同应用之间的资源使用呢?cgroup是一个不错的选择。

cgroup目前已支持 cpu, 网卡, memory, io, 硬件设备 的隔离,详见kernel文档例如 :

/usr/share/doc/kernel-doc-2.6.32/Documentation/cgroups/

本文将详细介绍一下memory子系统的使用,memory子系统包括控制和状态报告两个部分的功能。

正文

大部分内容从 http://liwei.life/2016/01/22/cgroup_memory/ 提炼

一、Linux内存管理基础知识

free命令

在Linux系统中,我们经常用free命令来查看系统内存的使用状态。在一个RHEL6的系统上,free命令的显示内容大概是这样一个状态:

[root@tencent64 ~]# free  
             total       used       free     shared    buffers     cached  
Mem:     132256952   72571772   59685180          0    1762632   53034704  
-/+ buffers/cache:   17774436  114482516  
Swap:      2101192        508    2100684  

这里的默认显示单位是kb,我的服务器是128G内存,所以数字显得比较大。这个命令几乎是每一个使用过Linux的人必会的命令,但越是这样的命令,似乎真正明白的人越少(我是说比例越少)。

一般情况下,对此命令输出的理解可以分这几个层次:

1. 不了解。这样的人的第一反应是:天啊,内存用了好多,70个多G,可是我几乎没有运行什么大程序啊?为什么会这样?Linux好占内存!

2. 自以为很了解。这样的人一般评估过会说:嗯,根据我专业的眼光看的出来,内存才用了17G左右,还有很多剩余内存可用。buffers/cache占用的较多,说明系统中有进程曾经读写过文件,但是不要紧,这部分内存是当空闲来用的。

3. 真的很了解。这种人的反应反而让人感觉最不懂Linux,他们的反应是:free显示的是这样,好吧我知道了。神马?你问我这些内存够不够,我当然不知道啦!我特么怎么知道你程序怎么写的?

根据目前网络上技术文档的内容,我相信绝大多数了解一点Linux的人应该处在第二种层次。

大家普遍认为,buffers和cached所占用的内存空间是可以在内存压力较大的时候被释放当做空闲空间用的。

但真的是这样么?

无论如何,free命令确实给我门透露了一些有用的信息,比如内存总量,剩余多少,多少用在了buffers/cache上,Swap用了多少,如果你用了其它参数还能看到一些其它内容,这里不做一一列举。

那么这里又引申出另一些概念,什么是buffer?什么是cache?什么是swap?由此我们就直接引出另一个命令:

[root@zorrozou-pc ~]# cat /proc/meminfo  
MemTotal: 131904480 kB  
MemFree: 125226660 kB  
Buffers: 478504 kB  
Cached: 4966796 kB  
SwapCached: 0 kB  
Active: 1774428 kB  
Inactive: 3770380 kB  
Active(anon): 116500 kB  
Inactive(anon): 3404 kB  
Active(file): 1657928 kB  
Inactive(file): 3766976 kB  
Unevictable: 0 kB  
Mlocked: 0 kB  
SwapTotal: 2088956 kB  
SwapFree: 2088956 kB  
Dirty: 336 kB  
Writeback: 0 kB  
AnonPages: 99504 kB  
Mapped: 20760 kB  
Shmem: 20604 kB  
Slab: 301292 kB  
SReclaimable: 229852 kB  
SUnreclaim: 71440 kB  
KernelStack: 3272 kB  
PageTables: 3320 kB  
NFS_Unstable: 0 kB  
Bounce: 0 kB  
WritebackTmp: 0 kB  
CommitLimit: 68041196 kB  
Committed_AS: 352412 kB  
VmallocTotal: 34359738367 kB  
VmallocUsed: 493196 kB  
VmallocChunk: 34291062284 kB  
HardwareCorrupted: 0 kB  
AnonHugePages: 49152 kB  
HugePages_Total: 0  
HugePages_Free: 0  
HugePages_Rsvd: 0  
HugePages_Surp: 0  
Hugepagesize: 2048 kB  
DirectMap4k: 194816 kB  
DirectMap2M: 3872768 kB  
DirectMap1G: 132120576 kB  

以上显示的内容都是些什么鬼?

其实这个问题的答案也是另一个问题的答案

Buffers/Cached

buffer和cache是两个在计算机技术中被用滥的名词,放在不通语境下会有不同的意义。

在内存管理中,我们需要特别澄清一下,

这里的buffer指Linux内存中的:Buffer cache(缓冲区缓存)。

这里的cache指Linux内存中的:Page cache(页面缓存)。

翻译成中文可以叫做缓冲区缓存和页面缓存。

在历史上,它们一个(buffer)被用来当成对io设备写的缓存,而另一个(cache)被用来当作对io设备的读缓存,这里的io设备,主要指的是块设备文件和文件系统上的普通文件。

但是现在,它们的意义已经不一样了。在当前的内核中,page cache顾名思义就是针对内存页的缓存,说白了就是,如果有内存是以page进行分配管理的,都可以使用page cache作为其缓存来使用。

当然,不是所有的内存都是以页(page)进行管理的,也有很多是针对块(block)进行管理的,这部分内存使用如果要用到cache功能,则都集中到buffer cache中来使用。(从这个角度出发,是不是buffer cache改名叫做block cache更好?)然而,也不是所有块(block)都有固定长度,系统上块的长度主要是根据所使用的块设备决定的,而页长度在X86上无论是32位还是64位都是4k。

而明白了这两套缓存系统的区别,也就基本可以理解它们究竟都可以用来做什么了。

什么是page cache

Page cache主要用来作为文件系统上的文件数据的缓存来用,尤其是针对当进程对文件有read/write操作的时候。

如果你仔细想想的话,作为可以映射文件到内存的系统调用:

mmap是不是很自然的也应该用到page cache?如果你再仔细想想的话,malloc会不会用到page cache?

man map  
  
  mmap, mmap64, munmap - map or unmap files or devices into memory  
  
man malloc  
  
  calloc, malloc, free, realloc - Allocate and free dynamic memory  

以上提出的问题都请自己思考,本文档不会给出标准答案。

在当前的实现里,page cache也被作为其它文件类型的缓存设备来用,所以事实上page cache也负责了大部分的块设备文件的缓存工作。

什么是buffer cache

Buffer cache的主要功能:在系统对块设备进行读写时,对块进行数据缓存。但是由于page cache也负责块设备文件读写的缓存工作,于是,当前的buffer cache实际上要负责的工作比较少。这意味着仅某些对块的操作会使用buffer cache进行缓存,比如我们在格式化文件系统的时候。

一般情况下buffer cache/page cache两个缓存系统是一起配合使用的,比如当我们对一个文件进行写操作的时候,page cache的内容会被改变,而buffer cache则可以用来将page标记为不同的缓冲区,并记录是哪一个缓冲区被修改了。这样,内核在后续执行脏数据的回写(writeback)时,就不用将整个page写回,而只需要写回修改的部分即可。

问题:(page的部分? or 部分page? , 是否还涉及到块设备的最小写单元?)

有搞大型系统经验的人都知道,缓存就像万金油,只要哪里有速度差异产生的瓶颈,就可以在哪里抹。但其成本之一:需要维护数据的一致性。内存缓存也不例外,内核需要维持其一致性。在脏数据产生较快或数据量较大的时候,缓存系统整体的效率一样会下降,因为毕竟脏数据写回也是要消耗IO的。

这个现象也会表现在这样一种情况下,当你发现free的时候,内存使用量较大,而且大部分是buffer/cache占用的。

以一般的理解,都会认为此时进程如果申请内存,内核会将buffer/cache占用的内存当成空闲的内存分给进程,这是没错的。

但是其成本是,在分配这部分已经被buffer/cache占用的内存的时候,内核会先对其上面的脏数据进行写回操作,保证数据一致后才会清空并分给进程使用。

如果此时你的进程是突然申请大量内存,而且你的业务是一直在产生很多脏数据(比如日志),并且系统没有及时写回的时候,此时系统给进程分配内存的效率会很慢,系统IO也会很高。那么此时你还以为buffer/cache可以当空闲内存使用么?

比如数据库应用,大量的导入数据,同时有业务发起了一个需要申请较大内存的读请求(比如用于HASH JOIN或者HASH 聚合,又或者排序等,用到较多的work_mem),此时就可能发生以上悲剧的事情。

如何回收cache?

Linux内核会在内存将要耗尽的时候,触发内存回收的工作,以便释放出内存给急需内存的进程使用。一般情况下,这个操作中主要的内存释放都来自于对buffer/cache的释放。尤其是cache空间,它主要用来做缓存,只是在内存够用的时候加快进程对文件的读写速度,那么在内存压力较大的情况下,当然有必要清空释放cache,作为free空间分给相关进程使用。所以一般情况下,我们认为buffer/cache空间可以被释放,这个理解是正确的。

但是这种清缓存的工作也并不是没有成本。清缓存必须保证cache中的数据跟对应文件中的数据一致,才能对cache进行释放。所以伴随着cache清除的行为,一般都是系统IO飙高。因为内核要对比cache中的数据和对应硬盘文件上的数据是否一致,如果不一致需要写回,之后才能回收。

在系统中除了内存将被耗尽的时候可以清缓存以外,我们还可以使用下面这个文件来人工触发缓存清除的操作:

echo 1 > /proc/sys/vm/drop_caches: 表示清除pagecache。  
  
echo 2 > /proc/sys/vm/drop_caches: 表示清除回收slab分配器中的对象(包括目录项缓存和inode缓存)。slab分配器是内核中管理内存的一种机制,其中很多缓存数据实现都是用的pagecache。  
  
echo 3 > /proc/sys/vm/drop_caches: 表示清除pagecache和slab分配器中的缓存对象。  

cache都能被回收么?

我们分析了cache能被回收的情况,那么有没有不能被回收的cache呢?当然有。

1. tmpfs

大家知道Linux提供一种“临时”文件系统叫做tmpfs,它可以将内存的一部分空间拿来当做文件系统使用,使内存空间可以当做目录文件来用。现在绝大多数Linux系统都有一个叫做/dev/shm的tmpfs目录,就是这样一种存在。

当然,我们也可以手工创建一个自己的tmpfs,方法如下:

[root@tencent64 ~]# mkdir /tmp/tmpfs  
[root@tencent64 ~]# mount -t tmpfs -o size=20G none /tmp/tmpfs/  
  
[root@tencent64 ~]# df  
Filesystem           1K-blocks      Used Available Use% Mounted on  
/dev/sda1             10325000   3529604   6270916  37% /  
/dev/sda3             20646064   9595940  10001360  49% /usr/local  
/dev/mapper/vg-data  103212320  26244284  71725156  27% /data  
tmpfs                 66128476  14709004  51419472  23% /dev/shm  
none                  20971520         0  20971520   0% /tmp/tmpfs  

于是我们就创建了一个新的tmpfs,空间是20G,我们可以在/tmp/tmpfs中创建一个20G以内的文件。如果我们创建的文件实际占用的空间是内存的话,那么这些数据应该占用内存空间的什么部分呢?

根据pagecache的实现功能可以理解,既然是某种文件系统,那么自然该使用pagecache的空间来管理。我们试试是不是这样?

[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         36         89          0          1         19  
-/+ buffers/cache:         15        111  
Swap:            2          0          2  
[root@tencent64 ~]# dd if=/dev/zero of=/tmp/tmpfs/testfile bs=1G count=13  
13+0 records in  
13+0 records out  
13958643712 bytes (14 GB) copied, 9.49858 s, 1.5 GB/s  
[root@tencent64 ~]#   
[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         49         76          0          1         32  
-/+ buffers/cache:         15        110  
Swap:            2          0          2  

我们在tmpfs目录下创建了一个13G的文件,并通过前后free命令的对比发现,cached增长了13G,说明这个文件确实放在了内存里并且内核使用的是cache作为存储。

再看看我们关心的指标: -/+ buffers/cache那一行。

我们发现,在这种情况下free命令仍然提示我们有110G内存可用,但是真的有这么多么?

我们可以人工触发内存回收看看现在到底能回收多少内存:

[root@tencent64 ~]# echo 3 > /proc/sys/vm/drop_caches  
[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         43         82          0          0         29  
-/+ buffers/cache:         14        111  
Swap:            2          0          2  

可以看到,cached占用的空间并没有像我们想象的那样完全被释放,其中13G的空间仍然被/tmp/tmpfs中的文件占用的。

当然,我的系统中还有其他不可释放的cache占用着其余16G内存空间。

那么tmpfs占用的cache空间什么时候会被释放呢?

是在其文件被删除的时候,如果不删除文件,无论内存耗尽到什么程度,内核都不会自动帮你把tmpfs中的文件删除来释放cache空间。

[root@tencent64 ~]# rm /tmp/tmpfs/testfile   
[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         30         95          0          0         16  
-/+ buffers/cache:         14        111  
Swap:            2          0          2  

这是我们分析的第一种cache不能被回收的情况。

2. 共享内存

共享内存是系统提供给我们的一种常用的进程间通信(IPC)方式,但是这种通信方式不能在shell中申请和使用,所以我们需要一个简单的测试程序,代码如下:

[root@tencent64 ~]# cat shm.c   
  
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/ipc.h>  
#include <sys/shm.h>  
#include <string.h>  
  
#define MEMSIZE 2048*1024*1023  
  
int  
main()  
{  
    int shmid;  
    char *ptr;  
    pid_t pid;  
    struct shmid_ds buf;  
    int ret;  
  
    shmid = shmget(IPC_PRIVATE, MEMSIZE, 0600);  
    if (shmid<0) {  
        perror("shmget()");  
        exit(1);  
    }  
  
    ret = shmctl(shmid, IPC_STAT, &buf);  
    if (ret < 0) {  
        perror("shmctl()");  
        exit(1);  
    }  
  
    printf("shmid: %d\n", shmid);  
    printf("shmsize: %d\n", buf.shm_segsz);  
  
    buf.shm_segsz *= 2;  
  
    ret = shmctl(shmid, IPC_SET, &buf);  
    if (ret < 0) {  
        perror("shmctl()");  
        exit(1);  
    }  
  
    ret = shmctl(shmid, IPC_SET, &buf);  
    if (ret < 0) {  
        perror("shmctl()");  
        exit(1);  
    }  
  
    printf("shmid: %d\n", shmid);  
    printf("shmsize: %d\n", buf.shm_segsz);  
  
  
    pid = fork();  
    if (pid<0) {  
        perror("fork()");  
        exit(1);  
    }  
    if (pid==0) {  
        ptr = shmat(shmid, NULL, 0);  
        if (ptr==(void*)-1) {  
            perror("shmat()");  
            exit(1);  
        }  
        bzero(ptr, MEMSIZE);  
        strcpy(ptr, "Hello!");  
        exit(0);  
    } else {  
        wait(NULL);  
        ptr = shmat(shmid, NULL, 0);  
        if (ptr==(void*)-1) {  
            perror("shmat()");  
            exit(1);  
        }  
        puts(ptr);  
        exit(0);  
    }  
}  

程序功能很简单,就是申请一段不到2G共享内存,然后打开一个子进程对这段共享内存做一个初始化操作,父进程等子进程初始化完之后输出一下共享内存的内容,然后退出。

但是退出之前并没有删除这段共享内存。我们来看看这个程序执行前后的内存使用:

[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         30         95          0          0         16  
-/+ buffers/cache:         14        111  
Swap:            2          0          2  
[root@tencent64 ~]# ./shm   
shmid: 294918  
shmsize: 2145386496  
shmid: 294918  
shmsize: -4194304  
Hello!  
[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         32         93          0          0         18  
-/+ buffers/cache:         14        111  
Swap:            2          0          2  

cached空间由16G涨到了18G。那么这段cache能被回收么?继续测试:

[root@tencent64 ~]# echo 3 > /proc/sys/vm/drop_caches  
[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         32         93          0          0         18  
-/+ buffers/cache:         14        111  
Swap:            2          0          2  

结果是仍然不可回收。大家可以观察到,这段共享内存即使没人使用,仍然会长期存放在cache中,直到其被删除。

删除方法有两种,一种是程序中使用shmctl()去IPC_RMID,另一种是使用ipcrm命令。我们来删除试试:

[root@tencent64 ~]# ipcs -m  
  
------ Shared Memory Segments --------  
key        shmid      owner      perms      bytes      nattch     status        
0x00005feb 0          root       666        12000      4                         
0x00005fe7 32769      root       666        524288     2                         
0x00005fe8 65538      root       666        2097152    2                         
0x00038c0e 131075     root       777        2072       1                         
0x00038c14 163844     root       777        5603392    0                         
0x00038c09 196613     root       777        221248     0                         
0x00000000 294918     root       600        2145386496 0                         
  
[root@tencent64 ~]# ipcrm -m 294918  
[root@tencent64 ~]# ipcs -m  
  
------ Shared Memory Segments --------  
key        shmid      owner      perms      bytes      nattch     status        
0x00005feb 0          root       666        12000      4                         
0x00005fe7 32769      root       666        524288     2                         
0x00005fe8 65538      root       666        2097152    2                         
0x00038c0e 131075     root       777        2072       1                         
0x00038c14 163844     root       777        5603392    0                         
0x00038c09 196613     root       777        221248     0                         
  
[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         30         95          0          0         16  
-/+ buffers/cache:         14        111  
Swap:            2          0          2  

删除共享内存后,cache被正常释放了。这个行为与tmpfs的逻辑类似。

内核底层在实现共享内存(shm)、消息队列(msg)和信号量数组(sem)这些POSIX:XSI的IPC机制的内存存储时,使用的都是tmpfs。

这也是为什么共享内存的操作逻辑与tmpfs类似的原因。当然,一般情况下是shm占用的内存更多,所以我们在此重点强调共享内存的使用。

3. mmap

mmap()是一个非常重要的系统调用,这仅从mmap本身的功能描述上是看不出来的。

从字面上看,mmap就是将一个文件映射进进程的虚拟内存地址,之后就可以通过操作内存的方式对文件的内容进行操作。但是实际上这个调用的用途是很广泛的。

当malloc申请内存时,小段内存内核使用sbrk处理,而大段内存就会使用mmap。

当系统调用exec族函数执行时,因为其本质上是将一个可执行文件加载到内存执行,所以内核很自然的就可以使用mmap方式进行处理。

我们在此仅仅考虑一种情况,就是使用mmap进行共享内存的申请时,会不会跟shmget()一样也使用cache?

同样,我们也需要一个简单的测试程序:

[root@tencent64 ~]# cat mmap.c   
#include <stdlib.h>  
#include <stdio.h>  
#include <strings.h>  
#include <sys/mman.h>  
#include <sys/stat.h>  
#include <sys/types.h>  
#include <fcntl.h>  
#include <unistd.h>  
  
#define MEMSIZE 1024*1024*1023*2  
#define MPFILE "./mmapfile"  
  
int main()  
{  
    void *ptr;  
    int fd;  
  
    fd = open(MPFILE, O_RDWR);  
    if (fd < 0) {  
        perror("open()");  
        exit(1);  
    }  
  
    ptr = mmap(NULL, MEMSIZE, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, fd, 0);  
    if (ptr == NULL) {  
        perror("malloc()");  
        exit(1);  
    }  
  
    printf("%p\n", ptr);  
    bzero(ptr, MEMSIZE);  
  
    sleep(100);  
  
    munmap(ptr, MEMSIZE);  
    close(fd);  
  
    exit(1);  
}  

这次我们干脆不用什么父子进程的方式了,就一个进程,申请一段2G的mmap共享内存,然后初始化这段空间之后等待100秒,再解除映射,所以我们需要在它sleep这100秒内检查我们的系统内存使用,看看它用的是什么空间?

当然在这之前要先创建一个2G的文件 ./mmapfile。结果如下:

[root@tencent64 ~]# dd if=/dev/zero of=mmapfile bs=1G count=2  
[root@tencent64 ~]# echo 3 > /proc/sys/vm/drop_caches  
[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         30         95          0          0         16  
-/+ buffers/cache:         14        111  
Swap:            2          0          2  

然后执行测试程序:

[root@tencent64 ~]# ./mmap &  
[1] 19157  
0x7f1ae3635000  
  
[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         32         93          0          0         18  
-/+ buffers/cache:         14        111  
Swap:            2          0          2  
  
[root@tencent64 ~]# echo 3 > /proc/sys/vm/drop_caches  
[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         32         93          0          0         18  
-/+ buffers/cache:         14        111  
Swap:            2          0          2  

我们可以看到,在程序执行期间,cached一直为18G,比之前涨了2G,并且此时这段cache仍然无法被回收。然后我们等待100秒之后程序结束。

[root@tencent64 ~]#   
[1]+  Exit 1                  ./mmap  
[root@tencent64 ~]#   
[root@tencent64 ~]# free -g  
             total       used       free     shared    buffers     cached  
Mem:           126         30         95          0          0         16  
-/+ buffers/cache:         14        111  
Swap:            2          0          2  

程序退出之后,cached占用的空间被释放。这样我们可以看到,使用mmap申请标志状态为MAP_SHARED的内存,内核也是使用的cache进行存储的。

在进程对相关内存没有释放之前,这段cache也是不能被正常释放的。实际上,mmap的MAP_SHARED方式申请的内存,在内核中也是由tmpfs实现的。

由此我们也可以推测,由于共享库的只读部分在内存中都是以mmap的MAP_SHARED方式进行管理,实际上它们也都是要占用cache且无法被释放的。

小结

我们通过三个测试例子,发现Linux系统内存中的cache并不是在所有情况下都能被释放当做空闲空间用的。并且也也明确了,即使可以释放cache,也并不是对系统来说没有成本的。

总结一下要点,我们应该记得这样几点:

1. 当cache作为文件缓存被释放的时候会引发IO变高,这是cache加快文件访问速度所要付出的成本。

2. tmpfs中存储的文件会占用cache空间,除非文件删除否则这个cache不会被自动释放。

3. 使用shmget方式申请的共享内存会占用cache空间,除非共享内存被ipcrm或者使用shmctl去IPC_RMID,否则相关的cache空间都不会被自动释放。

4. 使用mmap方法申请的MAP_SHARED标志的内存会占用cache空间,除非进程将这段内存munmap,否则相关的cache空间都不会被自动释放。

5. 实际上shmget、mmap的共享内存,在内核层都是通过tmpfs实现的,tmpfs实现的存储用的都是cache。

当理解了这些的时候,希望大家对free命令的理解可以达到我们说的第三个层次。

我们应该明白,内存的使用并不是简单的概念,cache也并不是真的可以当成空闲空间用的。

如果我们要真正深刻理解你的系统上的内存到底使用的是否合理,是需要理解清楚很多更细节知识,并且对相关业务的实现做更细节判断的。

我们当前实验场景是Centos 6的环境,不同版本的Linux的free现实的状态可能不一样,大家可以自己去找出不同的原因。

当然,本文所述的也不是所有的cache不能被释放的情形。那么,在你的应用场景下,还有那些cache不能被释放的场景呢?

Linux是如何使用内存的?

了解清楚这个问题是很有必要的,因为只有先知道了Linux如何使用内存,我们在能知道内存可以如何限制,以及做了限制之后会有什么问题?我们在此先例举出几个常用概念的意义:

内存,作为一种相对比较有限的资源,内核在考虑其管理时,无非应该主要从以下出发点考虑:

1. 内存够用时怎么办?

2. 内存不够用时怎么办?

当内存够用时

当内存够用时,内核的思路是,如何尽量提高资源的利用效率,以加快系统整体响应速度和吞吐量?于是内存作为一个CPU和I/O之间的大buffer的功能就呼之欲出了。

为此,内核设计了Buffers/Cached系统来做这个功能:

思考题:

Linux什么时候会将脏数据写回到外部设备(如块设备 磁盘)?这个过程如何进行人为干预?

这足可以证明一点,以内存管理的复杂度,我们必须结合系统上的应用状态来评估系统监控命令所给出的数据,才是做评估的正确途径。如果你不这样做,那么你就可以轻而易举的得出”Linux系统好烂啊!”这样的结论。也许此时,其实是你在这个系统上跑的应用很烂的缘故导致的问题。

当内存不够用时

我们好像已经分析了一种内存不够用的状态,就是上述的大量buffer/cache把内存几乎占满的情况。但是基于Linux对内存的使用原则,这不算是不够用,但是这种状态导致IO变高了。我们进一步思考,假设系统已经清理了足够多的buffer/cache分给了内存,而进程还在嚷嚷着要内存咋办?

此时内核就要启动一系列手段来让进程尽量在此时能够正常的运行下去。

请注意我在这说的是一种异常状态!我之所以要这样强调是因为,很多人把内存用满了当成一种正常状态。他们认为,当我的业务进程在内存使用到压力边界的情况下,系统仍然需要保证让业务进程有正常的状态!这种想法显然是缘木求鱼了。另外我还要强调一点,系统提供的是内存管理的机制和手段,而内存用的好不好,主要是业务进程的事情,责任不能本末倒置。

谁该被SWAP?

首先是Swap机制。Swap是交换技术,当内存不够用的时候,我们可以选择性的将一块磁盘、分区或者一个文件当成交换空间,将内存上一些临时用不到的数据放到交换空间上,以释放内存资源给急用的进程。

哪些数据可能会被交换出去呢?

从概念上判断,如果一段内存中的数据被经常访问,那么就不应该被交换到外部设备上,因为这样的数据如果交换出去的话会导致系统响应速度严重下降。

内存管理需要将内存区分为活跃的(Active)和不活跃的(Inactive),再加上一个进程使用的 用户空间内存映射 包括文件映射(file)和匿名映射(anon),所以就包括了Active(anon)、Inactive(anon)、Active(file)和Inactive(file)。

你说神马?啥是文件映射(file)和匿名映射(anon)?

匿名映射举例:进程使用malloc申请内存,或者使用mmap(MAP_ANONYMOUS的方式)申请的内存。

文件映射举例:进程使用mmap映射文件系统上的文件,包括普通的文件,也包括临时文件系统(tmpfs)。另外,Sys V的IPC 和 POSIX的IPC (IPC是进程间通信机制,在这里主要指共享内存,信号量数组和消息队列)也都是通过文件映射方式体现在用户空间内存中的。

匿名映射和文件映射的内存都会被算成进程的RSS。同时在cgroup的统计方法中,共享内存(通过文件映射方式为IPC而申请的内存) 和 文件缓存(file cache)都会被算成是cgroup的cache使用的总量。

共享内存不计算在RSS中。

[root@zorrozou-pc ~]# cat /cgroup/memory/memory.stat  
cache 94429184  
rss 102973440  
rss_huge 50331648  
mapped_file 21512192  
swap 0  
pgpgin 656572990  
pgpgout 663474908  
pgfault 2871515381  
pgmajfault 1187  
inactive_anon 3497984  
active_anon 120524800  
inactive_file 39059456  
active_file 34484224  
unevictable 0  
hierarchical_memory_limit 9223372036854775807  
hierarchical_memsw_limit 9223372036854775807  
total_cache 94429184  
total_rss 102969344  
total_rss_huge 50331648  
total_mapped_file 21520384  
total_swap 0  
total_pgpgin 656572990  
total_pgpgout 663474908  
total_pgfault 2871515388  
total_pgmajfault 1187  
total_inactive_anon 3497984  
total_active_anon 120524800  
total_inactive_file 39059456  
total_active_file 34484224  
total_unevictable 0  

字段解释

Statistic Description
cache page cache, including tmpfs (shmem), in bytes
rss anonymous and swap cache, not including tmpfs (shmem), in bytes
mapped_file size of memory-mapped mapped files, including tmpfs (shmem), in bytes
pgpgin number of pages paged into memory
pgpgout number of pages paged out of memory
swap swap usage, in bytes
active_anon anonymous and swap cache on active least-recently-used (LRU) list, including tmpfs (shmem), in bytes
inactive_anon anonymous and swap cache on inactive LRU list, including tmpfs (shmem), in bytes
active_file file-backed memory on active LRU list, in bytes
inactive_file file-backed memory on inactive LRU list, in bytes
unevictable memory that cannot be reclaimed, in bytes
hierarchical_memory_limit memory limit for the hierarchy that contains the memory cgroup, in bytes
hierarchical_memsw_limit memory plus swap limit for the hierarchy that contains the memory cgroup, in bytes
When you interpret the values reported by memory.stat, note how the various statistics inter-relate:  
  
active_anon + inactive_anon = anonymous memory + file cache for tmpfs + swap cache  
  
Therefore, active_anon + inactive_anon ≠ rss, because rss does not include tmpfs.  
  
active_file + inactive_file = cache - size of tmpfs  

这些值跟Swap有什么关系?还是刚才的问题,什么内容该被从内存中交换出去呢?

文件cache是一定不需要swap的,因为是cache,就意味着它本身就是硬盘上的文件(当然你现在应该知道了,它也不仅仅只有文件),那么如果是硬盘上的文件,就不用swap交换出去,只要写回脏数据,保持数据一致之后清除就可以了,这就是刚才说过的缓存清楚机制。但并不是所有被标记为cache的空间都能被写回硬盘的 (比如共享内存,但是共享内存能被Swap)。

能交换出去的内存主要包括:

Inactive(anon 匿名映射)这部分内存。需要注意的是,内核也将共享内存作为计数统计进了Inactive(anon)中去了(是的,共享内存也可以被Swap)。

还要补充一点,如果内存被mlock标记加锁了,则也不会交换,这是对内存加mlock锁的唯一作用。

另外再说明一下,HugePages也是不会交换的。

刚才我们讨论的这些cgroup计数,很可能会随着Linux内核的版本改变而产生变化,但是在比较长的一段时间内,我们可以这样理解。

我们基本搞清了swap这个机制的作用效果,那么既然swap是内部设备和外部设备的数据拷贝,加一个缓存就显得很有必要,这个缓存就是swapcache,在cgroup memory.stat文件中,swapcache跟anon page被一起记录到rss中,但是不包含共享内存。

当前的swap空间用了多少,总共多少,这些我们也可以在相关的数据中找到答案。

以上概念中还有一些名词大家可能并不清楚其含义,比如RSS或HugePages。请自行查资料补上这些知识。为了让大家真的理解什么是RSS,请思考 (ps aux命令中显示的VSZ;RSS;cat /proc/pid/smaps中显示的:PSS) 这三个进程占用内存指标的差别?

参考:

《精确度量Linux下进程占用多少内存的方法》

何时SWAP?

搞清楚了谁该swap,那么还要知道什么时候该swap。这看起来比较简单,内存耗尽而且cache也没什么可以回收的时候就应该触发swap。其实现实情况也没这么简单,实际上系统在内存压力可能不大的情况下也会swap,这种情况并不是我们今天要讨论的范围。

思考题:

除了内存被耗尽的时候要swap,还有什么时候会swap?如何调整内核swap的行为?如何查看当前系统的swap空间有哪些?都是什么类型?什么是swap权重?swap权重有什么意义?

绝大多数场景下,什么时候swap并不重要,而swap之后的事情相对却更重要。

大多数的内存不够用,只是临时不够用,比如并发突增等突发情况,这种情况的特点是时间持续短,此时swap机制作为一种临时的中转措施,可以起到对业务进程的保护作用。

如果没有swap,内存耗尽的结果一般都是触发oom killer,会杀掉此时积分比较高的进程。

更严重的话,内存不够用还会触发进程D状态死锁,死锁怎么发生的呢?

当多个进程同时要申请内存的时候,需要被干掉的积分比较高的进程很可能就是需要申请内存的进程,而这个进程本身因为正在争抢内存而导致陷入D状态,那么此时kill就可能是对它无效的。从而导致进程hang住的状态。此时既然不能KILL它,如果系统还有足够的内存,而且只是对应cgroup组的内存限制导致的,建议放大memory限制来解决。

但是swap也不是任何时候都有很好的保护效果。如果内存申请是长期并大量的,那么交换出去的数据就会因为长时间驻留在外部设备上,导致进程调用这段内存的几率大大增加,当进程很频繁的使用它已经被交换出去的内存时,就会让整个系统处在io繁忙的状态,此时进程的响应速度会严重下降,导致整个系统夯死。对于系统管理员来说,这种情况是完全不能接受的,因为故障之后的第一要务是赶紧恢复服务,但是swap频繁使用的IO繁忙状态会导致系统除了断电重启之外,没有其它可靠手段可以让系统从这种状态中恢复回来,所以这种情况是要尽力避免的。此时,如果有必要,我们甚至可以考虑不用swap,哪怕内存过量使用被oom,或者进程D状态都是比swap导致系统卡死的情况更好处理的状态。如果你的环境需求是这样的,那么可以考虑关闭swap。

进程申请内存的时候究竟会发生什么?

刚才我们从系统宏观的角度简要说明了一下什么是buffer/cache以及swap。下面我们从一个更加微观的角度来把一个内存申请的过程以及触发机制给串联起来。本文描述的过程是基于Linux 3.10内核版本的,Linux 4.1基本过程变化不大。如果你想确认在你的系统上究竟是什么样子,请自行翻阅相关内核代码。

进程申请内存可能用到很多种方法,最常见的就是malloc和mmap。但是这对于我们并不重要,因为无论是malloc还是mmap,或是其他的申请内存的方法,都不会真正的让内核去给进程分配一个实际的物理内存空间。真正会触发分配物理内存的行为是 缺页异常(page fault)

缺页异常就是我们可以在memory.stat中看到的total_pgfault,这种异常一般分两种,一种叫major fault,另一种叫minor fault。这两种异常的主要区别是,进程所请求的内存数据是否会引发磁盘io?如果会引发,就是一个major fault,如果不引发,那就是minor fault。就是说如果产生了major fault,这个数据基本上就意味着已经被交换到了swap空间上。

缺页异常的处理过程大概可以整理为以下几个路径:

首先检查要访问的虚拟地址是否合法,如果合法则继续查找和分配一个物理页,

步骤如下:

检查发生异常的虚拟地址在物理页表中是不是不存在?

1. 如果虚拟地址在物理页表中不存在,那么

1.1 如果请求是匿名映射,则申请置零的匿名映射内存,此时也有可能是映射了某种虚拟文件系统,比如共享内存,那么就去映射相关的内存区,或者发生COW(写时复制)申请新内存。

1.2 如果是文件映射,则有两种可能,一种是这个映射区是一个page cache,直接将相关page cache区映射过来即可,或者COW新内存存放需要映射的文件内容。如果page cache中不存在,则说明这个区域已经被交换到swap空间上,应该去处理swap。

2. 如果页表中已经存在需要映射的内存,那么

2.1 检查是否要对内存进行写操作,如果不写,那就直接复用,如果要写,就发生COW写时复制,此时的COW跟上面的处理过程不完全相同,在内核中,这里主要是通过do_wp_page方法实现的。

2.2 如果需要申请新内存,则都会通过alloc_page_vma申请新内存,而这个函数的核心方法是__alloc_pages_nodemask,也就是Linux内核著名的内存管理系统伙伴系统的实现。

分配过程先会检查空闲页表中有没有页可以申请,实现方法是:get_page_from_freelist,我们并不关心正常情况,分到了当然一切ok。

更重要的是异常处理,如果空闲中没有,则会进入__alloc_pages_slowpath方法进行处理。这个处理过程的主逻辑大概这样:

2.2.1 唤醒kswapd进程,把能换出的内存换出,让系统有内存可用。

2.2.2 继续检查看看空闲中是否有内存。有了就ok,没有继续下一步;

2.2.3 尝试清理page cache,清理的时候会将进程置为D状态。如果还申请不到内存则:

2.2.4 启动oom killer干掉一些进程释放内存,如果这样还不行则:

2.2.5 回到步骤1再来一次!

当然以上逻辑要符合一些条件,但是这一般都是系统默认的状态,(比如,你必须启用oom killer机制等。另外这个逻辑中有很多其它状态与本文无关,比如检查内存水印、检查是否是高优先级内存申请等等,当然还有关于numa节点状态的判断处理,我没有一一列出)。

另外,以上逻辑中,不仅仅只有清理cache的时候会使进程进入D状态,还有其它逻辑也会这样做。这就是为什么在内存不够用的情况下,oom killer有时也不生效,因为可能要干掉的进程正好陷入这个逻辑中的D状态了。

以上就是内存申请中,大概会发生什么的过程。

本文的重点cgroup内存限制进行说明,当我们处理限制的时候,更多需要关心的是当内存超限了会发生什么?对边界条件的处理才是我们这次的主题。

所以我并没有对正常申请到的情况做细节说明,也没有对用户态使用malloc什么时候使用sbrk还是mmap来申请内存做出细节说明,毕竟那是程序正常状态的时候的事情,后续可以另写一个内存优化的文章主要讲解那部分。

下面我们该进入正题了:

二、Cgroup内存限制的配置

其实最简单的莫过于如何进行限制了,我们的系统环境还是沿用上一次讲解CPU内存隔离的环境,使用cgconfig和cgred服务进行cgroup的配置管理。还是创建一个zorro用户,对这个用户产生的进程进行内存限制。基础配置方法不再多说,如果不知道的请参考 这个文档

环境配置好之后,我们就可以来检查相关文件了。内存限制的相关目录根据cgconfig.config的配置放在了/cgroup/memory 目录中,如果你跟我做了一样的配置,那么这个目录下的内容应该是这样的:

[root@zorrozou-pc ~]# ls /cgroup/memory/  
cgroup.clone_children memory.failcnt memory.kmem.slabinfo memory.kmem.usage_in_bytes memory.memsw.limit_in_bytes memory.oom_control memory.usage_in_bytes shrek  
cgroup.event_control memory.force_empty memory.kmem.tcp.failcnt memory.limit_in_bytes memory.memsw.max_usage_in_bytes memory.pressure_level memory.use_hierarchy tasks  
cgroup.procs memory.kmem.failcnt memory.kmem.tcp.limit_in_bytes memory.max_usage_in_bytes memory.memsw.usage_in_bytes memory.soft_limit_in_bytes zorro  
cgroup.sane_behavior memory.kmem.limit_in_bytes memory.kmem.tcp.max_usage_in_bytes memory.meminfo memory.move_charge_at_immigrate memory.stat notify_on_release  
jerry memory.kmem.max_usage_in_bytes memory.kmem.tcp.usage_in_bytes memory.memsw.failcnt memory.numa_stat memory.swappiness release_agent  

其中,zorro、jerry、shrek都是目录概念跟cpu隔离的目录树结构类似。

相关配置文件内容:

[root@zorrozou-pc ~]# cat /etc/cgconfig.conf   
mount {  
  cpu = /cgroup/cpu;  
  cpuset = /cgroup/cpuset;  
  cpuacct = /cgroup/cpuacct;  
  memory = /cgroup/memory;  
  devices = /cgroup/devices;  
  freezer = /cgroup/freezer;  
  net_cls = /cgroup/net_cls;  
  blkio = /cgroup/blkio;  
}  
  
group zorro {  
  cpu {  
    cpu.shares = 6000;  
    cpu.cfs_quota_us = "600000";  
  }  
    
  cpuset {  
    cpuset.cpus = "0-7,12-19";  
    cpuset.mems = "0-1";  
  }  
    
  memory {  
      
  }  
}  

配置中添加了一个真对memory的空配置项,我们稍等下再给里面添加配置。

[root@zorrozou-pc ~]# cat /etc/cgrules.conf  
zorro cpu,cpuset,cpuacct,memory zorro  
jerry cpu,cpuset,cpuacct,memory jerry  
shrek cpu,cpuset,cpuacct,memory shrek  

文件修改完之后记得重启相关服务:

[root@zorrozou-pc ~]# service cgconfig restart  
[root@zorrozou-pc ~]# service cgred restart  

让我们继续来看看真对内存都有哪些配置参数:

[root@zorrozou-pc ~]# ls /cgroup/memory/zorro/  
cgroup.clone_children memory.kmem.failcnt memory.kmem.tcp.limit_in_bytes memory.max_usage_in_bytes memory.memsw.usage_in_bytes memory.soft_limit_in_bytes  
cgroup.event_control memory.kmem.limit_in_bytes memory.kmem.tcp.max_usage_in_bytes memory.meminfo memory.move_charge_at_immigrate memory.stat notify_on_release  
cgroup.procs memory.kmem.max_usage_in_bytes memory.kmem.tcp.usage_in_bytes memory.memsw.failcnt memory.numa_stat memory.swappiness tasks  
memory.failcnt memory.kmem.slabinfo memory.kmem.usage_in_bytes memory.memsw.limit_in_bytes memory.oom_control memory.usage_in_bytes  
memory.force_empty memory.kmem.tcp.failcnt memory.limit_in_bytes memory.memsw.max_usage_in_bytes memory.pressure_level memory.use_hierarchy  

首先我们已经认识了memory.stat文件了,这个文件内容不能修改,它实际上是输出当前cgroup相关内存使用信息的。常见的数据及其含义我们刚才也已经说过了,在此不再复述。

1. cgroup内存限制

1. memory.memsw.limit_in_bytes: 内存+swap空间使用的总量限制。

2. memory.limit_in_bytes:内存使用量限制。

这两项的意义很清楚了,如果你决定在你的cgroup中关闭swap功能,可以把两个文件的内容设置为同样的值即可。

至于为什么相信大家都能想清楚。

注意:

在调整memsw.limit_in_bytes或limit_in_bytes时,请保证任何时刻 “memsw.limit_in_bytes 都 >= limit_in_bytes”,否则可能修改失败。

比如现在

memsw.limit_in_bytes=1G

limit_in_bytes=1G

要缩小到800MB,那么应该先缩小limit_in_bytes再缩小memsw.limit_in_bytes

2. OOM控制

memory.oom_control: 内存超限之后的oom行为控制。

这个文件中有两个值:

1. oom_kill_disable 0

默认为0表示打开oom killer,就是说当内存超限时会触发干掉进程。

如果设置为1表示关闭oom killer,此时内存超限不会触发内核杀掉进程。而是将进程夯住(hang/sleep),实际上内核中就是将进程设置为D状态,并且将相关进程放到一个叫做OOM-waitqueue的队列中。这时的进程可以kill杀掉。如果你想继续让这些进程执行,可以选择这样几个方法:

1.1 增加该cgroup组的内存限制,让进程有内存可以继续申请。

1.2 杀掉该cgroup组内的其他一些进程,让本组内有内存可用。

1.3 把一些进程移到别的cgroup组中,让本cgroup内有内存可用。

1.4 删除一些tmpfs的文件,就是占用内存的文件,比如共享内存或者其它会占用内存的文件。

说白了,此时只有当cgroup中有更多内存可以用了,在OOM-waitqueue队列中被挂起的进程就可以继续运行了。

2. under_oom 0

这个值只是用来看的,它表示当前的cgroup的状态是不是已经oom了,如果是,这个值将显示为1。

我们就是通过设置和监测这个文件中的这两个值来管理cgroup内存超限之后的行为的。

在默认场景下,如果你使用了swap,那么你的cgroup限制内存之后最常见的异常效果是IO变高,如果业务不能接受,我们一般的做法是关闭swap,那么cgroup内存oom之后都会触发kill掉进程,如果我们用的是LXC或者Docker这样的容器,那么还可能干掉整个容器。

当然也经常会因为kill进程的时候因为进程处在D状态,而导致整个Docker或者LXC容器根本无法被杀掉。

至于原因,在前面已经说的很清楚了。当我们遇到这样的困境时该怎么办?一个好的办法是,关闭oom killer,让内存超限之后,进程挂起,毕竟这样的方式相对可控。

此时我们可以检查under_oom的值,去看容器是否处在超限状态,然后根据业务的特点决定如何处理业务。

我推荐的方法是关闭部分进程或者重启掉整个容器,因为可以想像,容器技术所承载的服务应该是在整体软件架构上有容错的业务,典型的场景是web服务。容器技术的特点就是生存周期短,在这样的场景下,杀掉几个进程或者几个容器,都应该对整体服务的稳定性影响不大,而且容器的启动速度是很快的,实际上我们应该认为,容器的启动速度应该是跟进程启动速度可以相媲美的。

你的业务会因为死掉几个进程而表现不稳定么?如果不会,请放心的干掉它们吧,大不了很快再启动起来就是了。但是如果你的业务不是这样,那么请根据自己的情况来制定后续处理的策略。

当我们进行了内存限制之后,内存超限的发生频率要比使用实体机更多了,因为限制的内存量一般都是小于实际物理内存的。所以,使用基于内存限制的容器技术的服务应该多考虑自己内存使用的情况,尤其是内存超限之后的业务异常处理应该如何让服务受影响的程度降到更低。在系统层次和应用层次一起努力,才能使内存隔离的效果达到最好。

3. 内存资源审计

1. memory.memsw.usage_in_bytes: 当前cgroup的内存+swap使用量。

2. memory.usage_in_bytes: 当前cgroup的内存使用量。

3. memory.max_usage_in_bytes: 当前cgroup的历史最大内存使用量。

4. memory.memsw.max_usage_in_bytes: 当前cgroup的历史最大内存+swap使用量。

这些文件都是只读的,用来查看相关状态信息,只能看不能改。

5. 如果你的内核配置打开了CONFIG_MEMCG_KMEM选项(getconf -a)的话,那么可以看到当前cgroup的内核内存使用的限制和状态统计信息,他们都是以memory.kmem开头的文件。你可以通过memory.kmem.limit_in_bytes来限制内核使用的内存大小,通过memory.kmem.slabinfo来查看内核slab分配器的状态。现在还能通过memory.kmem.tcp开头的文件来限制cgroup中使用tcp协议的内存资源使用和状态查看。

6. 所有名字中有failcnt的文件里面的值都是相关资源超限的次数的计数,可以通过echo 0将这些计数重置。

7. 如果你的服务器是NUMA架构的话,可以通过memory.numa_stat这个文件来查看cgroup中的NUMA相关状态。

8. memory.swappiness跟 /proc/sys/vm/swappiness 的概念一致,用来调整cgroup使用swap的状态,表示不使用交换分区。但是依旧可能会发生swapout,如果真的不想发生,建议使用mlock锁定内存,前面讲了使用mlock的内存不会被swapout。

but:

swap out might still happen when there is a shortage of system memory because the global virtual memory management logic does not read the cgroup value. To lock pages completely, use mlock() instead of cgroups.  
  
You cannot change the swappiness of the following groups:  
  
the root cgroup, which uses the swappiness set in /proc/sys/vm/swappiness.  
  
a cgroup that has child groups below it.  

9. memory.failcnt

reports the number of times that the memory limit has reached the value set in memory.limit_in_bytes.  

10. memory.memsw.failcnt

reports the number of times that the memory plus swap space limit has reached the value set in memory.memsw.limit_in_bytes.  

4. 内存软限制 以及 内存超卖

1. memory.soft_limit_in_bytes: 内存软限制。

如果超过了memory.limit_in_bytes所定义的限制,那么进程会被oom killer干掉或者被暂停,这相当于硬限制,因为进程无法申请超过自身cgroup限制的内存,但是软限制确是可以突破的。

我们假定一个场景,如果你的实体机上有四个cgroup,实体机的内存总量是64G,那么一般情况我们会考虑给每个cgroup限制到16G内存。

但是现实情况并不会这么理想,首先实体机上其他进程和内核会占用部分内存,这将导致实际上每个cgroup都不会真的有16G内存可用,如果四个cgroup都尽量占用内存的话,他们可能谁都不会到达内存的上限触发超限的行为,这可能将导致进程都抢不到内存而被饿死。

类似的情况还可能发上在内存超卖的环境中,比如,我们仍然只有64G内存,但是确开了8个cgroup,每个都限制了16G内存。

这样每个cgroup分配的内存之和达到了128G,但是实际内存量只有64G。

这种情况是出于绝大多数应用可能不会占用满所有的内存来考虑的,这样就可以把本来属于它的那份内存”借用”给其它cgroup。

如果全局内存已经耗尽了,但是某些cgroup还没达到他的内存使用上限,而它们此时如果要申请内存的话,此时该从哪里回收内存?

如果我们配置了memory.soft_limit_in_bytes,那么内核将去回收那些内存超过了这个软限制的cgroup的内存,尽量缩减它们的内存占用达到软限制的量以下 ,以便让没有达到软限制的cgroup有内存可以用。

在没有这样的内存竞争以及没有达到硬限制的情况下,软限制是不会生效的。还有,软限制的起作用时间可能会比较长,毕竟内核要平衡多个cgroup的内存使用。

根据软限制的这些特点,我们应该明白如果想要软限制生效,应该把它的值设置成小于硬限制。

5. 进程迁移时的内存charge

memory.move_charge_at_immigrate: 打开或者关闭进程迁移时的内存记账信息。

进程可以在多个cgroup之间切换,所以内存限制必须考虑当发生这样的切换时。

进程进入的新cgroup时,内存使用量是重新从0累计还是把原来cgroup中的信息迁移过来?

设置为0时,关闭这个功能,相当于不累计之前的信息.

默认是1,迁移的时候要在新的cgroup中累积(charge)原来信息,并把旧group中的信息给uncharge掉。

如果新cgroup中没有足够的空间容纳新来的进程,首先内核会在cgroup内部回收内存,如果还是不够,导致进程迁移cgroup失败。

6. 清空cgroup组的内存

memory.force_empty

仅仅当该组内没有任何进程时可以使用,用法 echo 0 > memory.force_empty

如果无法清除,则内存会移到父组(如果是根组,则不允许对其执行 echo 0 > memory.force_empty,除非没有进程在里面)

when set to 0, empties memory of all pages used by tasks in the cgroup.   
  
This interface can only be used when the cgroup has no tasks.   
  
If memory cannot be freed, it is moved to a parent cgroup if possible.   
  
Use the memory.force_empty parameter before removing a cgroup to avoid moving out-of-use page caches to its parent cgroup.  

7. 回收子进程内存

用于控制是否回收当前cgroup中pid的子进程的内存

memory.use_hierarchy

contains a flag (0 or 1) that specifies whether memory usage should be accounted for throughout a hierarchy of cgroups.   
  
If enabled (1), the memory subsystem reclaims memory from the children of and process that exceeds its memory limit.   
  
By default (0), the subsystem does not reclaim memory from a task's children.  

8. 内存压力 通知机制

内存的资源隔离还提供了一种压力通知机制。当cgoup内的内存使用量达到某种压力状态的时候,内核可以通过eventfd的机制来通知用户程序,通过cgroup.event_control和memory.pressure_level来实现。

使用方法:

与 https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide/sec-memory.html 有一些出入,可能是版本问题。

1. 使用eventfd()创建一个eventfd,假设叫做efd,

2. 然后open()打开memory.pressure_level的文件路径,产生一个另一个fd,我们暂且叫它cfd,

3. 然后将这两个fd和我们要关注的内存压力级别告诉内核,让内核帮我们判断条件是否成立,如果成立,内核会把以上信息按这样的格式: “event_fd efd” 写入cgroup.event_control。

4. 然后就可以去等着efd是否可读了,如果能读出信息,则代表内存使用已经触发相关压力条件。

压力级别的level有三个:

“low”:表示内存使用已经达到触发内存回收的压力级别。

“medium”:表示内存使用压力更大了,已经开始触发swap以及将活跃的cache写回文件等操作了。

“critical”:到这个级别,就意味着内存已经达到上限,内核已经触发oom killer了。

程序从efd读出的消息内容就是这三个级别的关键字。我们可以通过这个机制,建立一个内存压力管理系统,在内存达到相应级别的时候,触发响应的管理策略,来达到各种自动化管理的目的。

下面给出一个监控程序的例子:

#include <sys/eventfd.h>  
  
#define USAGE_STR "Usage: cgroup_event_listener "  
  
int main(int argc, char **argv)  
{  
int efd = -1;  
int cfd = -1;  
int event_control = -1;  
char event_control_path[PATH_MAX];  
char line[LINE_MAX];  
int ret;  
  
if (argc != 3)  
errx(1, "%s", USAGE_STR);  
  
cfd = open(argv[1], O_RDONLY);  
if (cfd == -1)  
err(1, "Cannot open %s", argv[1]);  
  
ret = snprintf(event_control_path, PATH_MAX, "%s/cgroup.event_control",  
dirname(argv[1]));  
if (ret >= PATH_MAX)  
errx(1, "Path to cgroup.event_control is too long");  
  
event_control = open(event_control_path, O_WRONLY);  
if (event_control == -1)  
err(1, "Cannot open %s", event_control_path);  
  
efd = eventfd(0, 0);  
if (efd == -1)  
err(1, "eventfd() failed");  
  
ret = snprintf(line, LINE_MAX, "%d %d %s", efd, cfd, argv[2]);  
if (ret >= LINE_MAX)  
errx(1, "Arguments string is too long");  
  
ret = write(event_control, line, strlen(line) + 1);  
if (ret == -1)  
err(1, "Cannot write to cgroup.event_control");  
  
while (1) {  
uint64_t result;  
  
ret = read(efd, &result, sizeof(result));  
if (ret == -1) {  
if (errno == EINTR)  
continue;  
err(1, "Cannot read from eventfd");  
}  
assert(ret == sizeof(result));  
  
ret = access(event_control_path, W_OK);  
if ((ret == -1) && (errno == ENOENT)) {  
puts("The cgroup seems to have removed.");  
break;  
}  
  
if (ret == -1)  
err(1, "cgroup.event_control is not accessible any more");  
  
printf("%s %s: crossed\n", argv[1], argv[2]);  
}  
  
return 0;  
}  

三、最后

Linux的内存限制要说的就是这么多了,当我们限制了内存之后,相对于使用实体机,实际上对于应用来说可用内存更少了,所以业务会相对更经常地暴露在内存资源紧张的状态下。

相对于虚拟机(kvm,xen),多个cgroup之间是共享内核的,我们可以从内存限制的角度思考一些关于”容器”技术相对于虚拟机和实体机的很多特点:

1. 内存更紧张,应用的内存泄漏会导致相对更严重的问题。

2. 容器的生存周期时间更短,如果实体机的开机运行时间是以年计算的,那么虚拟机则是以月计算的,而容器应该跟进程的生存周期差不多,顶多以天为单位。所以,容器里面要跑的应用应该可以被经常重启。

3. 当有多个cgroup(容器)同时运行时,我们不能再以实体机或者虚拟机对资源的使用的理解来规划整体运营方式,我们需要更细节的理解什么是cache,什么是swap,什么是共享内存,它们会被统计到哪些资源计数中?在内核并不冲突的环境,这些资源都是独立给某一个业务使用的,在理解上即使不是很清晰,也不会造成歧义。但是在cgroup中,我们需要彻底理解这些细节,才能对遇到的情况进行预判,并规划不同的处理策略。

也许我们还可以从中得到更多的理解,大家一起来想喽?

参考

1. https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide/sec-memory.html

2. https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide/sec-Using_the_Notification_API.html

3. http://liwei.life/2016/01/22/cgroup_memory/

4. 《精确度量Linux下进程占用多少内存的方法》

5. 《Linux page allocation failure 的问题处理 - lowmem_reserve_ratio》

6. /usr/share/doc/kernel-doc-2.6.32/Documentation/filesystems/proc.txt

7. /usr/share/doc/kernel-doc-2.6.32/Documentation/cgroups/

Flag Counter

digoal’s 大量PostgreSQL文章入口