使用cgroups限制MongoDB的内存使用

cgroups,其名称源自控制组群(control groups)的简写,是Linux内核的一个功能,用来限制,控制与分离一个进程组群的资源(如CPU、内存、磁盘输入输出等)。

这个项目最早是由Google的工程师在2006年发起(主要是Paul Menage和Rohit Seth),最早的名称为进程容器(process containers)。在2007年时,因为在Linux内核中,容器(container)这个名词有许多不同的意义,为避免混乱,被重命名为cgroup,并且被合并到2.6.24版的内核中去。自那以后,又添加了很多功能。

使​​​用​​​ cgroup,系​​​统​​​管​​​理​​​员​​​可​​​更​​​具​​​体​​​地​​​控​​​制​​​对​​​系​​​统​​​资​​​源​​​的​​​分​​​配​​​、​​​优​​​先​​​顺​​​序​​​、​​​拒​​​绝​​​、​​​管​​​理​​​和​​​监​​​控​​​。​​​可​​​更​​​好​​​地​​​根​​​据​​​任​​​务​​​和​​​用​​​户​​​分​​​配​​​硬​​​件​​​资​​​源​​​,提​​​高​​​总​​​体​​​效​​​率​​​。
在实践中,系统管理员一般会利用cgroup做下面这些事:

  • 隔离一个进程组(比如:nginx的所有进程),并限制他们所消费的资源,比如绑定CPU的核。
  • 为这组进程 分配其足够使用的内存
  • 为这组进程分配相应的网络带宽和磁盘存储限制
  • 限制访问某些设备(通过设置设备的白名单)

cgroups相关概念

  1. 任务(task)。在cgroups中,任务就是系统的一个进程。
  2. 控制组群(control group)。控制组群就是一组按照某种标准划分的进程。cgroups中的资源控制都是以控制组群为单位实现。一个进程可以加入到某个控制组群,也从一个进程组迁移到另一个控制组群。一个进程组的进程可以使用cgroups以控制组群为单位分配的资源,同时受到cgroups以控制组群为单位设定的限制。
  3. 层级(hierarchy)。控制组群可以组织成hierarchical的形式,既一颗控制组群树。控制组群树上的子节点控制组群是父节点控制组群的孩子,继承父控制组群的特定的属性。
  4. 子系统(subsystem)。一个子系统就是一个资源控制器,比如cpu子系统就是控制cpu时间分配的一个控制器。子系统必须附加(attach)到一个层级上才能起作用,一个子系统附加到某个层级以后,这个层级上的所有控制组群都受到这个子系统的控制。

当前的cgroup有一下规则:
1.每次在系统中创建新层级时,该系统中的所有任务都是那个层级的默认 cgroup(我们称之为 root cgroup ,此cgroup在创建层级时自动创建,后面在该层级中创建的cgroup都是此cgroup的后代)的初始成员。
2.一个子系统最多只能附加到一个层级。 (一个层级不会附加两个同样的子系统)
3.一个层级可以附加多个子系统
4.一个任务可以是多个cgroup的成员,但是这些cgroup必须在不同的层级。
5.系统中的进程(任务)创建子进程(任务)时,该子任务自动成为其父进程所在 cgroup 的成员。然后可根据需要将该子任务移动到不同的 cgroup 中,但开始时它总是继承其父任务的cgroup。

cgroup子系统

cgroups为每种可以控制的资源定义了一个子系统。典型的子系统介绍如下:

  1. cpu 子系统,主要限制进程的 cpu 使用率。
  2. cpuacct 子系统,可以统计 cgroups 中的进程的 cpu 使用报告。
  3. cpuset 子系统,可以为 cgroups 中的进程分配单独的 cpu 节点或者内存节点。
  4. memory 子系统,可以限制进程的 memory 使用量。
  5. blkio 子系统,可以限制进程的块设备 io。
  6. devices 子系统,可以控制进程能够访问某些设备。
  7. net_cls 子系统,可以标记 cgroups 中进程的网络数据包,然后可以使用 tc 模块(traffic control)对数据包进行控制。
  8. freezer 子系统,可以挂起或者恢复 cgroups 中的进程。
  9. ns 子系统,可以使不同 cgroups 下面的进程使用不同的 namespace。

cgroups安装

如果系统还没有安装cgroups,可以通过下面的命令进行安装

1
yum install libcgroup

启动和查看服务状态:

1
2
service cgconfig start
service cgconfig status

Linux把cgroups实现成一个文件系统,各个子系统的挂载点配置在/etc/cgconfig.conf文件中:

1
2
3
4
5
6
7
8
9
10
mount {
cpuset = /cgroup/cpuset;
cpu = /cgroup/cpu;
cpuacct = /cgroup/cpuacct;
memory = /cgroup/memory;
devices = /cgroup/devices;
freezer = /cgroup/freezer;
net_cls = /cgroup/net_cls;
blkio = /cgroup/blkio;
}

或者也可以通过命令lssubsys -m或者mount -t cgroup挂载。

1
2
3
4
5
6
7
8
9
# lssubsys -m
cpuset /cgroup/cpuset
cpu /cgroup/cpu
cpuacct /cgroup/cpuacct
memory /cgroup/memory
devices /cgroup/devices
freezer /cgroup/freezer
net_cls /cgroup/net_cls
blkio /cgroup/blkio

或者你单独挂载某几个子系统:

1
mount -t cgroup -o remount,cpu,cpuset,memory cpu_and_mem /cgroup/cpu_and_mem

cgroups使用

挂载某一个 cgroups 子系统到挂载点之后,就可以通过在挂载点下面建立文件夹或者使用cgcreate命令的方法创建 cgroups 层级结构中的节点。比如通过命令cgcreate -g cpu:test就可以在 cpu 子系统下建立一个名为 test 的节点。结果如下所示:

1
2
3
4
5
# cgcreate -g cpu:test
# ls /cgroup/cpu
cgroup.event_control cpu.cfs_quota_us cpu.shares release_agent
cgroup.procs cpu.rt_period_us cpu.stat tasks
cpu.cfs_period_us cpu.rt_runtime_us notify_on_release test

然后可以通过写入需要的值到 test 下面的不同文件,来配置需要限制的资源。每个子系统下面都可以进行多种不同的配置,需要配置的参数各不相同,详细的参数设置需要参考 cgroups 手册。使用 cgset 命令也可以设置 cgroups 子系统的参数,格式为 cgset -r parameter=value path_to_cgroup
比如:cgset -r cfs_quota_us=50000 test限制进程组 test 使用50%的CPU。
或者直接写文件:

1
echo 50000 > /cgroup/cpu/test/cpu.cfs_quota_us

命令可以参考redhat的文档: Setting Parameters

当需要删除某一个 cgroups 节点的时候,可以使用 cgdelete 命令,比如要删除上述的 test 节点,可以使用 cgdelete -r cpu:test命令进行删除。

把进程加入到 cgroups 子节点也有多种方法,可以直接把 pid 写入到子节点下面的 task 文件中。也可以通过 cgclassify 添加进程,格式为 cgclassify -g subsystems:path_to_cgroup pidlist,也可以直接使用 cgexec 在某一个 cgroups 下启动进程,格式为cgexec -g subsystems:path_to_cgroup command arguments.

也可以在/etc/cgconfig.conf文件中定义group,格式如下:

1
2
3
4
5
6
7
8
group <name> {
[<permissions>]
<controller> {
<param name> = <param value>;
}
}

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
mount {
cpuset = /cgroup/cpuset;
cpu = /cgroup/cpu;
cpuacct = /cgroup/cpuacct;
memory = /cgroup/memory;
devices = /cgroup/devices;
freezer = /cgroup/freezer;
net_cls = /cgroup/net_cls;
blkio = /cgroup/blkio;
}
group mysql_g1 {
cpu {
cpu.cfs_quota_us = 50000;
cpu.cfs_period_us = 100000;
}
cpuset {
cpuset.cpus = "3";
cpuset.mems = "0";
}
cpuacct{
}
memory {
memory.limit_in_bytes=104857600;
memory.swappiness=0;
# memory.max_usage_in_bytes=104857600;
# memory.oom_control=0;
}
blkio {
blkio.throttle.read_bps_device="8:0 524288";
blkio.throttle.write_bps_device="8:0 524288";
}
}

还可以让一个服务Service启动的时候加入进程组,具体文档请参考: Starting_a_Service

Redhat的文档详细的介绍了cgroups的配置和使用方法,是很好的一个参考资料。

实践,限制MongoDB的内存使用

MongoDB是个吃内存的大户,它会尽可能的使用服务器的内存。在数据量巨大的时候,内存很快会被吃光,导致服务器上其它进程无法分配内存。
我们可以使用cgroups来限制MongoDB的内存使用。实际上,在参考文档2中 Vadim Tkachenko 就介绍了他的实际方法。

配置有几个步骤:

  1. 创建一个控制组群:cgcreate -g memory:DBLimitedGroup
  2. 指定可用的最大内存16G: echo 16G > /sys/fs/cgroup/memory/DBLimitedGroup/memory.limit_in_bytes
  3. 将缓存页丢掉 (flush and drop): sync; echo 3 > /proc/sys/vm/drop_caches
  4. 将mongodb的进程加入控制组:cgclassify -g memory:DBLimitedGrouppid of mongod

基本上就完成了任务,这样此MongoDB最多可以使用16G的内存。
为了处理机器重启还得手工添加的问题,你可以按照上面的文档将Mongo服务加入到控制组中。

除此之外,作者还提到了 dirty cache flush的问题, 注意两个参数:/proc/sys/vm/dirty_background_ratio/proc/sys/vm/dirty_ratio

这里有一篇关于调整磁盘缓冲参数的介绍:
1) /proc/sys/vm/dirty_ratio
这个参数控制文件系统的文件系统写缓冲区的大小,单位是百分比,表示系统内存的百分比,表示当写缓冲使用到系统内存多少的时候,开始向磁盘写出数据。增大之会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能。但是,当你需要持续、恒定的写入场合时,应该降低其数值,:
echo '1' > /proc/sys/vm/dirty_ratio

2) /proc/sys/vm/dirty_background_ratio
这个参数控制文件系统的pdflush进程,在何时刷新磁盘。单位是百分比,表示系统内存的百分比,意思是当写缓冲使用到系统内存多少的时候,pdflush开始向磁盘写出数据。增大之会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能。但是,当你需要持续、恒定的写入场合时,应该降低其数值,:

echo '1' > /proc/sys/vm/dirty_background_ratio

3) /proc/sys/vm/dirty_writeback_centisecs
这个参数控制内核的脏数据刷新进程pdflush的运行间隔。单位是 1/100 秒。缺省数值是500,也就是 5 秒。如果你的系统是持续地写入动作,那么实际上还是降低这个数值比较好,这样可以把尖峰的写操作削平成多次写操作。设置方法如下:

echo "100" > /proc/sys/vm/dirty_writeback_centisecs
如果你的系统是短期地尖峰式的写操作,并且写入数据不大(几十M/次)且内存有比较多富裕,那么应该增大此数值:

echo "1000" > /proc/sys/vm/dirty_writeback_centisecs

4) /proc/sys/vm/dirty_expire_centisecs
这个参数声明Linux内核写缓冲区里面的数据多“旧”了之后,pdflush进程就开始考虑写到磁盘中去。单位是 1/100秒。缺省是 30000,也就是 30 秒的数据就算旧了,将会刷新磁盘。对于特别重载的写操作来说,这个值适当缩小也是好的,但也不能缩小太多,因为缩小太多也会导致IO提高太快。

echo "100" > /proc/sys/vm/dirty_expire_centisecs
当然,如果你的系统内存比较大,并且写入模式是间歇式的,并且每次写入的数据不大(比如几十M),那么这个值还是大些的好。

5) /proc/sys/vm/vfs_cache_pressure
该文件表示内核回收用于directory和inode cache内存的倾向;缺省值100表示内核将根据pagecache和swapcache,把directory和inode cache保持在一个合理的百分比;降低该值低于100,将导致内核倾向于保留directory和inode cache;增加该值超过100,将导致内核倾向于回收directory和inode cache

缺省设置:100

6) /proc/sys/vm/min_free_kbytes
该文件表示强制Linux VM最低保留多少空闲内存(Kbytes)。
缺省设置:724(512M物理内存)

7) /proc/sys/vm/nr_pdflush_threads
该文件表示当前正在运行的pdflush进程数量,在I/O负载高的情况下,内核会自动增加更多的pdflush进程。
缺省设置:2(只读)

8) /proc/sys/vm/overcommit_memory
该文件指定了内核针对内存分配的策略,其值可以是0、1、2。
0, 表示内核将检查是否有足够的可用内存供应用进程使用;如果有足够的可用内存,内存申请允许;否则,内存申请失败,并把错误返回给应用进程。
1, 表示内核允许分配所有的物理内存,而不管当前的内存状态如何。
2, 表示内核允许分配超过所有物理内存和交换空间总和的内存(参照overcommit_ratio)。

缺省设置:0

9) /proc/sys/vm/overcommit_ratio
该文件表示,如果overcommit_memory=2,可以过载内存的百分比,通过以下公式来计算系统整体可用内存。
系统可分配内存=交换空间+物理内存*overcommit_ratio/100

10) /proc/sys/vm/page-cluster
该文件表示在写一次到swap区的时候写入的页面数量,0表示1页,1表示2页,2表示4页。
缺省设置:3(2的3次方,8页)

11) /proc/sys/vm/swapiness
该文件表示系统进行交换行为的程度,数值(0-100)越高,越可能发生磁盘交换。

参考文档

  1. https://www.kernel.org/doc/Documentation/cgroups/cgroups.txt
  2. Using Cgroups to Limit MySQL and MongoDB memory usage
  3. Red Hat Enterprise Linux 6 Resource Management Guide
  4. cgroups介绍及安装配置使用详解
  5. 美团 Linux资源管理之cgroups简介
  6. Docker基础技术:Linux CGroup
  7. linux集群之--设置磁盘缓冲参数
  8. http://centaurea.io/blog?name=mongodb-memory-allocation-and-cache-management