Linux Control Group(简称 cgroup)是一个用于限制、统计和隔离进程的资源的特性,它于 2006 年由 Google 的两位工程师开发,之后合入 2.6.24 版本的内核。那时 docker 正在 Google 内部兴起,本人推测正是前者催生了 cgroup。本文重点介绍如何用 cgroup 限制进程的资源。

在虚拟化领域,如 qemu-kvm 和 linux container,cgroup 用常用来限制以下类型的资源:

  • CPU time:进程占用的 CPU 时间
  • Memory:进程占用的物理内存
  • Block IO:进程访问块设备的 bandwidth 或 IOPS
  • Network IO:进程访问网络的 bandwidth 或 packages 的数量

以 ubuntu 14.04 为例,安装 cgroup:

$ apt-get install cgroup-bin cgroup-lite libcgroup1

安装完成后,cgroup 默认挂载在 /sys/fs/cgroup 上,该目录下共有 11 个 subsystem,关于它们的介绍请见官网文档,更为详细的介绍请见 redhat resource management guide,本文将用到 blkio, cpu, memory 和 net_cls 这四个 subsystem。

$ ls /sys/fs/cgroup/
blkio  cpu  cpuacct  cpuset  devices  freezer  hugetlb  memory  net_cls
net_prio  perf_event  systemd

Limit CPU Time

void main(){
    unsigned int i, end;

    end = 1024 * 1024 * 1024;
    for(i = 0; i < end; )
        i ++;
}

未限制 CPU 使用率前,上述代码的执行时间为:

$ time ./a.out

real	0m3.317s
user	0m3.312s
sys  	0m0.000s

我们在 /sys/fs/cgroup 下新建一个名为 cpu_limit 的 cgroup,并设置该 cgroup 下的进程只能占用单个 CPU 10% 的使用率。

# cfs_period_us 表示 CPU 总时间片段,cfs_quota_us 表示分配给该 cgroup 的时间片段。
# 10000/100000 = 10%

$ mkdir /sys/fs/cgroup/cpu_limit
$ echo 100000 > /sys/fs/cgroup/cpu_limit/cpu.cfs_period_us
$ echo 10000 > /sys/fs/cgroup/cpu_limit/cpu.cfs_quota_us

限制后上述代码的执行时间如下,约为前者的 10 倍:

$ time cgexec -g cpu:cpu_limit  ./a.out

real	0m31.904s
user	0m3.192s
sys 	0m0.000s

某个时间点 top 的输出为:

$ top
......
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
28280 root      20   0    4196    668    592 R  10.0  0.0   0:01.28 a.out

Limit Memory

首先在 /sys/fs/cgroup/memory 下新建一个名为 limit_memory 的 cgroup:

$ mkdir /sys/fs/cgroup/memory/limit_memory

限制该 cgroup 的内存使用量为 300 MB:

# 物理内存 + SWAP <= 300 MB
$ echo 314572800 > /sys/fs/cgroup/memory/limit_memory/memory.limit_in_bytes
$ echo 0 > /sys/fs/cgroup/memory/limit_memory/memory.swappiness

下面是测试代码,它分十次申请内存,每次申请 100 MB:

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

#define CHUNK_SIZE 1024 * 1024 * 100

void main(){
    char *p;
    int i;

    for(i = 0; i < 10; i ++)
    {
        p = malloc(sizeof(char) * CHUNK_SIZE);
        if(p == NULL){
            printf("fail to malloc!");
            return ;
        }
        memset(p, 0, CHUNK_SIZE);
        printf("malloc memory %d MB\n", (i + 1) * 100);
    }
}

执行结果如下,当进程占用的内存超过限制时,将被 kill。

$ cgexec -g memory:limit_memory ./a.out
malloc memory 100 MB
malloc memory 200 MB
Killed

Limit Block IO

我们采用 blkio 限制进程访问块设备的速率,以磁盘为例,未限制前,其读的带宽为:

$ dd if=in.file of=/dev/null count=1000 bs=1M
1000+0 records in
1000+0 records out
1048576000 bytes (1.0 GB) copied, 1.4419 s, 727 MB/s

采用以下方式配置 cgroup,限制磁盘的读取速率为 10MB/s

# 获取所读文件所在的磁盘编号,本文的编号为 252:0
$ df  -m
Filesystem                  1M-blocks  Used Available Use% Mounted on
/dev/mapper/ubuntu--vg-root     17755  6288     10543  38% /
...
$ ls -l /dev/mapper/ubuntu--vg-root
lrwxrwxrwx 1 root root 7 Jul 10 21:20 /dev/mapper/ubuntu--vg-root -> ../dm-0
$ ls -l /dev/dm-0
brw-rw---- 1 root disk 252, 0 Jul 10 21:20 /dev/dm-0

# 在 /sys/fs/cgroup 目录下新建一个 cgroup,名为 limit_blkio
$ mkdir /sys/fs/cgroup/limit_blkio

# 设置读速率为 10MB/s,其中 252:0 表示所读文件在的磁盘
$ echo "252:0 10485760" > /sys/fs/cgroup/blkio/limit_blkio/blkio.throttle.read_bps_device

再次执行 dd,其平均读速率为 10.5MB/s。

# 清楚内存的缓存数据
$ echo 3 > /proc/sys/vm/drop_caches

$ cgexec -g blkio:limit_blkio dd if=in.file of=/dev/null count=1000 bs=1M
1000+0 records in
1000+0 records out
1048576000 bytes (1.0 GB) copied, 100.03 s, 10.5 MB/s

其中某个时刻 iotop 的输出如下:

Total DISK READ :       9.80 M/s | Total DISK WRITE :       0.00 B/s
Actual DISK READ:       9.80 M/s | Actual DISK WRITE:       0.00 B/s
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
19987 be/4 root        9.80 M/s    0.00 B/s  0.00 % 96.99 % dd if=in.file of=/dev/null count=1000 bs=1M

test_limit 目录下有多个 blkio 相关的文件,较为常用的是以下四个:

  • blkio.throttle.read_bps_device:读取块设备的带宽
  • blkio.throttle.read_iops_device:读取块设备的 IOPS
  • blkio.throttle.write_bps_device:写块设备的带宽
  • blkio.throttle.write_iops_device:写块设备的 IOPS

Limit Network IO

未限速时,采用 scp 测试的网络速度为:

$ scp test.file root@10.10.1.180:~/
in.file                                            100% 1000MB  71.4MB/s   00:14

我们用 net_cls 标记某个 cgroup 下的包,借助 tc 来限制被标记的包的量,从而限制网络带宽:

$ mkdir /sys/fs/cgroup/net_cls/net_limit
$ echo 0x001000001 > net_cls.classid

# 采用 tc 限制 classid 为 10:1 网络带宽为 40Mbit/s
$ tc qdisc add dev eth0 root handle 10: htb
$ tc class add dev eth0 parent 10: classid 10:1 htb rate 40mbit
$ tc filter add dev eth0 parent 10: protocol ip prio 10 handle 1: cgroup

限速后,采用 scp 测试的网络速度为 3.6 MB/s,注意到 3.6 MB/s 和 40 Mbit/s(5MB/s) 有较大差距,而 IP 和 TCP 头部额外的开销(共 40 字节头部,每个包的平均大小为 1448 字节)不可能造成如此大的差距,所以本人也、对此深感疑惑,但未能查明原因。

$ scp test.file root@10.10.1.180:~/
in.file                                         100% 1000MB   3.6MB/s   04:39