cgroups(Control Groups)是linux内核提供的一种机制,这种机制可以根据需求把一系列系统任务整合到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架,,简单的说,cgroups可以限制,记录任务组所使用的物理资源,本质上,cgroups是系统内核附加在程序上的一系列钩子(hook),通过程序运行时对资源的调度触发相应的钩子以达到资源限制和追踪的目的。
cgroups是容器化时代必须了解的技术,使用cgroups我们可以很方便的限制容器可以使用的cpu,内存等资源。
cgroup是的主要作用
资源限制:可以对任务使用的资源总额进行限制。
资源统计:cgroups可以统计资源的使用量,比如cpu使用时长,内存用量等,这个功能非常适合在云端产品中按量计费的方式。
相关概念解释
任务(task):在 linux 系统中,内核本身的调度和管理并不对进程和线程进行区分,只是根据 clone 时传入的参数的不同来从概念上区分进程和线程。这里使用 task 来表示系统的一个进程或线程
子系统(Subsystem):cgroups中的子系统就是一个资源调度控制器(controllers),比如cpu子系统可以控制cpu的时间分配,内存子系统可以限制内存的使用量,简单点说,其实就是用来控制我们各种资源使用的,支持的子系统可以在文件(cat /etc/cgroups)中查看,支持的 subsystem 如下:
blkio 对块设备的 IO 进行限制。
cpu 限制 CPU 时间片的分配,与 cpuacct 挂载在同一目录。
cpuacct 生成 cgroup 中的任务占用 CPU 资源的报告,与 cpu 挂载在同一目录。
cpuset 给 cgroup 中的任务分配独立的 CPU(多处理器系统) 和内存节点。
devices 允许或禁止 cgroup 中的任务访问设备。
freezer 暂停/恢复 cgroup 中的任务。
hugetlb 限制使用的内存页数量。
memory 对 cgroup 中的任务的可用内存进行限制,并自动生成资源占用报告。
net_cls 使用等级识别符(classid)标记网络数据包,这让 Linux 流量控制器(tc 指令)可以识别来自特定 cgroup 任务的数据包,并进行网络限制。
net_prio 允许基于 cgroup 设置网络流量(netowork traffic)的优先级。
perf_event 允许使用 perf 工具来监控 cgroup。
pids 限制任务的数量。
控制组(Control group) :可以理解为一组资源控制策略,比如cpu控制组,memory控制组,不同类型的控制组都有一个根控制组,cpu类型的跟控制组的控制文件就在/sys/fs/cgroup/cpu目录下,控制组是一种层级结构的,比如我们可以在这个目录下再创建一层文件夹表示这个根控制组的子控制组,子节点继承父节点挂载的子系统,下面我们会通过例子解释。
cgroups 的文件系统接口
cgroups 以文件的方式提供应用接口,我们可以通过 mount 命令来查看 cgroups 默认的挂载点:
$ mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
第一行的tmpfs说明/sys/fs/group目录中的文件都是存在于内存中的临时文件
其余的挂载点则是内核支持的各个子系统的根级层级结构,就像我们再控制组概念中描述的一样。
下面让我们来探索一下 /sys/fs/cgroup 目录及其子目录下都是些什么:
ll /sys/fs/cgroup/
total 0
dr-xr-xr-x 5 root root 0 Jan 15 02:24 blkio
lrwxrwxrwx 1 root root 11 Jan 15 02:24 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 Jan 15 02:24 cpuacct -> cpu,cpuacct
dr-xr-xr-x 3 root root 0 Jan 15 02:24 cpu,cpuacct
dr-xr-xr-x 2 root root 0 Jan 15 02:24 cpuset
dr-xr-xr-x 5 root root 0 Jan 15 02:24 devices
dr-xr-xr-x 2 root root 0 Jan 15 02:24 freezer
dr-xr-xr-x 2 root root 0 Jan 15 02:24 hugetlb
dr-xr-xr-x 6 root root 0 Jan 15 02:24 memory
lrwxrwxrwx 1 root root 16 Jan 15 02:24 net_cls -> net_cls,net_prio
dr-xr-xr-x 2 root root 0 Jan 15 02:24 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Jan 15 02:24 net_prio -> net_cls,net_prio
dr-xr-xr-x 2 root root 0 Jan 15 02:24 perf_event
dr-xr-xr-x 5 root root 0 Jan 15 02:24 pids
dr-xr-xr-x 2 root root 0 Jan 15 02:24 rdma
dr-xr-xr-x 5 root root 0 Jan 15 02:24 systemd
这个目录/sys/fs/cgroup目录下是各个子系统的根控制组目录,下面看下memory目录下的文件:
ls /sys/fs/cgroup/memory/
cgroup.clone_children memory.kmem.failcnt memory.kmem.tcp.usage_in_bytes memory.memsw.usage_in_bytes memory.swappiness tasks
cgroup.event_control memory.kmem.limit_in_bytes memory.kmem.usage_in_bytes memory.move_charge_at_immigrate memory.usage_in_bytes user.slice
cgroup.procs memory.kmem.max_usage_in_bytes memory.limit_in_bytes memory.numa_stat memory.use_hierarchy
cgroup.sane_behavior memory.kmem.slabinfo memory.max_usage_in_bytes memory.oom_control nick_memory
init.scope memory.kmem.tcp.failcnt memory.memsw.failcnt memory.pressure_level notify_on_release
memory.failcnt memory.kmem.tcp.limit_in_bytes memory.memsw.limit_in_bytes memory.soft_limit_in_bytes release_agent
memory.force_empty memory.kmem.tcp.max_usage_in_bytes memory.memsw.max_usage_in_bytes memory.stat system.slice
这些文件都是memory子系统根控制组的设置文件,比如,memory.limit_in_bytes中的数字用来限制进程的最大可用内存,memory.swappiness 中保存着使用 swap 的权重等等。
下来我们就通过简单的 demo 来演示如何使用 cgroups 限制进程可以使用的资源
查看进程所属的 cgroups
以通过 /proc/[pid]/cgroup 来查看指定进程属于哪些 cgroup:
$ cat /proc/889/cgroup
12:cpuset:/
11:hugetlb:/
10:rdma:/
9:freezer:/
8:memory:/system.slice/crond.service
7:perf_event:/
6:pids:/system.slice/crond.service
5:blkio:/system.slice/crond.service
4:net_cls,net_prio:/
3:cpu,cpuacct:/
2:devices:/system.slice/crond.service
1:name=systemd:/system.slice/crond.service
每一行包含使用冒号分割开的三列,他们的含义分别是:
- cgroup树的ID,或者可以理解为控制组类型的ID,比如cpu控制组的ID就是3
- cgroup 树绑定的所有 subsystem,多个 subsystem 之间用逗号隔开。这里 name=systemd 表示没有和任何 subsystem 绑定,只是给他起了个名字叫 systemd。
- 进程在 cgroup 树中的路径,即进程所属的 cgroup,这个路径是相对于挂载点的相对路径。
使用工具演示cgroup使用demo
我们需要使用libcgroup-tools工具中的cgexec来演示demo,通过下面命令安装cgroup-tools
$ yum install libcgroup-tools
demo1: 限制进程可用的cpu
在我们使用 cgroups 时,最好不要直接在各个子系统的根目录下直接修改其配置文件。推荐的方式是为不同的需求在子系统树中定义不同的节点。比如我们可以在 /sys/fs/cgroup/cpu 目录下新建一个名称为 nick_cpu 的目录:
$ cd /sys/fs/cgroup/cpu
$ sudo mkdir nick_cpu
然后查看新建的目录下的内容:
$ ls nick_cpu/
cgroup.clone_children cpuacct.stat cpuacct.usage_all cpuacct.usage_percpu_sys cpuacct.usage_sys cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.procs cpuacct.usage cpuacct.usage_percpu cpuacct.usage_percpu_user cpuacct.usage_user cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
cgroups 的文件系统会在创建文件目录的时候自动创建这些配置文件!
让我们通过下面的设置把 CPU 周期限制为总量的十分之一:
$ sudo su
$ echo 100000 > nick_cpu/cpu.cfs_period_us
$ echo 10000 > nick_cpu/cpu.cfs_quota_us
上面的100000和10000是微秒 100000 表示 100 毫秒, 1000表示10毫秒,它们的含义是:在每 100 毫秒的时间里,运行进程使用的 CPU 时间最多为 10 毫秒
然后创建一个 CPU 密集型的程序:
void main()
{
unsigned int i, end;
end = 1024 * 1024 * 1024;
for(i = 0; i < end; )
{
i ++;
}
}
保存为文件 cputime.c 编译并通过不同的方式执行:
$ gcc cputime.c -o cputime
$ time ./cputime
real 0m2.437s
user 0m2.423s
sys 0m0.000s
$ time cgexec -g cpu:nick_cpu ./cputime
real 0m23.762s
user 0m2.389s
sys 0m0.000s
time 命令可以为我们报告程序执行消耗的时间,其中的 real 就是我们真实感受到的时间。使用 cgexec 能够把我们添加的 cgroup 配置 nick_cpu 应用到运行 cputime 程序的进程上。 上图显示,默认的执行只需要 2s 左右。通过 cgroups 限制 CPU 资源后需要运行 23s。
demo2:限制进程可用的内存
这次我们来限制进程可用的最大内存,在 /sys/fs/cgroup/memory 下创建目录nick_memory:
$ cd /sys/fs/cgroup/memory
$ sudo mkdir nick_memory
下面的设置把进程的可用内存限制在最大 300M,并且不使用 swap:
# 物理内存 + SWAP <= 300 MB;1024*1024*300 = 314572800
$ sudo su
$ echo 314572800 > nick_memory/memory.limit_in_bytes
$ echo 0 > nick_memory/memory.swappiness
然后创建一个不断分配内存的程序,它分五次分配内存,每次申请 100M:
#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 < 5; i ++)
{
p = malloc(sizeof(char) * CHUNK_SIZE);
if(p == NULL)
{
printf("fail to malloc!");
return ;
}
// memset() 函数用来将指定内存的前 n 个字节设置为特定的值
memset(p, 0, CHUNK_SIZE);
printf("malloc memory %d MB\n", (i + 1) * 100);
}
}
把上面的代码保存为 mem.c 文件,然后编译:
$ gcc mem.c -o mem
执行生成的 mem 程序:
$ ./mem
malloc memory 100 MB
malloc memory 200 MB
malloc memory 300 MB
malloc memory 400 MB
malloc memory 500 MB
此时一切顺利,然后加上刚才的约束试试:
$ cgexec -g memory:nick_memory ./mem
malloc memory 100 MB
malloc memory 200 MB
Killed
由于内存不足且禁止使用 swap,所以被限制资源的进程在申请内存时被强制杀死了。
总结
cgroups 是 linux 内核提供的功能,由于牵涉的概念比较多,所以不太容易理解。本文试图在介绍概念性内容的同时,用最简单的 demo 演示 cgroups 的用法。