Switch-Router

理解 Cgroup

Published at 2019-05-08 | Last Update 2019-05-08

简单的类比

CgroupLinux内核的一项特性,它可以对进程进行一些系统资源(比如CPU、内存、IO等)限制。为什么要做这样的限制呢? 因为供需关系不平衡!

举个例子,假设小明家里有一台台式电脑,爸爸需要用它处理公务,妈妈想用它看电视剧,小明想用它来玩游戏。 于是小明一家人决定,每天爸爸处理一个小时公务,妈妈再看一个小时电视剧,最后小明再玩一个小时游戏。

这样一家人都可以得到有限的满足。 如果把上面的人都换成内核中的进程,而将台式电脑换成CPU。那么上面的例子就变成了三个进程都希望获取CPU的控制权从而运行,通常来说,这是调度器的工作,与Cgroup扯不上关系。

但如果小明家里又买了一台笔记本电脑,并且爸爸说它的工作比较重要,需要单独使用一台电脑。于是小明一家人决定爸爸单独使用笔记本电脑,而妈妈和小明分享台式电脑的使用时间。这种分配类比到内核,就是Cgroup可以完成的工作。

我们可以创建两个组(分别称为cg0cg1),然后将Father进程加入到cg0,将Mother进程和Ming进程加入到cg1。最后再设置cg0中的进程只能使用cpu0,cg1中的进程只能使用cpu1。这样的结果就是Father进程将独占cpu0

Mother进程和Ming进程将共享cpu1

Cgroup管理进程资源

子系统(subsystem)

Cgroup限制的是每个进程的资源,这是通过将进程加入cgroup组实现的。每个cgroup组都关联(bind)了一个或多个subsystem,属于同一个cgroup组的进程在绑定的subsystem资源上拥有同样的限制

随着内核的发展,越来越多的subsystem加入到内核,而最常见的subsystem有:

  • cpuset:在多核系统中,为cgroup进程划分特定的cpu范围和NUMA内存范围(也就是上文的例子)
  • cpu:当cpu繁忙时,为cgroup中的进程保证最小的CPU使用率
  • memory: 限制cgroup中的进程设置使用内存的上限
  • devices:限制cgroup的进程可以访问的设备

其他的子系统还有blkiocpuacctfreezer等等。

一个cgroup可以包含多个进程,相反的,一个进程也可以加入多个cgroup组,只要这些cgroup组关联的subsystem不同就行。

也可以说是线程,这里我不打算区分进程与线程,本文中所说的进程,其实就是内核中的task_struct

层级(hierarchy)

hierarchy这个概念在Cgroup中是比较容易引起困惑的,其实说白了hierarchy就是树状结构,它将cgroup组串起来。前面提到cgroup组关联subsystem,更准确的说法是hierarchy在创建的时候就关联subsystem了。

默认情况下,内核启动后会创建多个hierarchy,它们都关联了一个或两个subsystem,这可以通过查看/sys/fs/cgroup目录发现,该目录下每个子目录都是一个hierarchy,并绑定了对应目录名的subsystem

root@ubuntu:/sys/fs/cgroup# ls -l
total 0
dr-xr-xr-x 5 root root  0 Jul 22 13:15 blkio
lrwxrwxrwx 1 root root 11 Jul 22 13:15 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 Jul 22 13:15 cpuacct -> cpu,cpuacct
dr-xr-xr-x 5 root root  0 Jul 22 13:15 cpu,cpuacct
dr-xr-xr-x 3 root root  0 Jul 22 13:15 cpuset
dr-xr-xr-x 5 root root  0 Jul 22 13:15 devices
dr-xr-xr-x 2 root root  0 Jul 22 13:15 freezer
dr-xr-xr-x 2 root root  0 Jul 22 13:15 hugetlb
dr-xr-xr-x 5 root root  0 Jul 22 13:15 memory
lrwxrwxrwx 1 root root 16 Jul 22 13:15 net_cls -> net_cls,net_prio
dr-xr-xr-x 2 root root  0 Jul 22 13:15 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Jul 22 13:15 net_prio -> net_cls,net_prio
dr-xr-xr-x 2 root root  0 Jul 22 13:15 perf_event
dr-xr-xr-x 5 root root  0 Jul 22 13:15 pids
dr-xr-xr-x 5 root root  0 Jul 22 13:15 systemd

hierarchy化组织的cgroup组的一个特性是我们可以为已有的cgroup组创建子cgroup组,并且子cgroup组可以使用的subsystem资源不能超过父cgroup组.

每个hierarchy在创建时,都会创建一个root cgroup组,并将系统中的所有进程加入到这个根cgroup组。

举个例子,在一台有8个cpu(编号0~7)的系统中,假设我们在根cgroup组下创建了一个子cgroupcg1,它限制其中的进程能使用的cpu编号为0~3,那么如果我们为cg1创建一个子cgroupcg2,则cg2中的进程最多也只能使用

编号为0~3的cpu。

前面说过,一个进程可以加入到多个cgroup组,但有一个限制,这些cgroup组不能属于同一个hierarchy,否则,系统也不知道该进程应该遵循哪个cgroup组的资源..

比如上面的httpd进程,它加入了两个cgroup组:/cpu_mem_cg/cg1/net/cg3, 这两个cgroup组分属不同的hierarchy。此时,它就不能再加入同属hierarchy A/cpu_mem_cg/cg2了。

当一个进程fork出子进程后,子进程将继承父进程加入的所有cgroup组。

虚拟文件系统(VFS)

我们可以通过内核为Cgroup提供的虚拟文件系统来轻松地进行Cgroup操作. 这个虚拟文件系统在内核启动时被挂载在/sys/fs/cgroup路径。 ··· root@ubuntu:/ # df -h …… tmpfs 992M 0 992M 0% /sys/fs/cgroup …… ···

使用 Cgroup

创建/删除子cgroup

我们可以为已有的cgroup组创建子cgroup组,以绑定cpusethierarchy为例,路径/sys/fs/cgroup/cpu_set下的内容就是前文提到的一个root cgroup

root@ubuntu:/sys/fs/cgroup/cpuset# ls
cgroup.clone_children  cpuset.cpus            cpuset.mem_hardwall             cpuset.memory_spread_page  cpuset.sched_relax_domain_level
cgroup.procs           cpuset.effective_cpus  cpuset.memory_migrate           cpuset.memory_spread_slab  notify_on_release
cgroup.sane_behavior   cpuset.effective_mems  cpuset.memory_pressure          cpuset.mems                release_agent
cpuset.cpu_exclusive   cpuset.mem_exclusive   cpuset.memory_pressure_enabled  cpuset.sched_load_balance  tasks

使用mkdir创建子目录,我们可以创建名为cg1子cgroup

root@ubuntu:/sys/fs/cgroup/cpuset# mkdir cg1
root@ubuntu:/sys/fs/cgroup/cpuset# ls
cg1                    cpuset.cpu_exclusive   cpuset.mem_exclusive    cpuset.memory_pressure_enabled  cpuset.sched_load_balance        tasks
cgroup.clone_children  cpuset.cpus            cpuset.mem_hardwall     cpuset.memory_spread_page       cpuset.sched_relax_domain_level
cgroup.procs           cpuset.effective_cpus  cpuset.memory_migrate   cpuset.memory_spread_slab       notify_on_release
cgroup.sane_behavior   cpuset.effective_mems  cpuset.memory_pressure  cpuset.mems                     release_agent
root@ubuntu:/sys/fs/cgroup/cpuset# ls cg1
cgroup.clone_children  cpuset.cpus            cpuset.mem_exclusive   cpuset.memory_pressure     cpuset.mems                      notify_on_release
cgroup.procs           cpuset.effective_cpus  cpuset.mem_hardwall    cpuset.memory_spread_page  cpuset.sched_load_balance        tasks
cpuset.cpu_exclusive   cpuset.effective_mems  cpuset.memory_migrate  cpuset.memory_spread_slab  cpuset.sched_relax_domain_level

可以看到,内核会自动填充这个子文件夹。

与创建cgroup组相对应的,我们可以通过rmdir删除子cgroup

root@ubuntu:/sys/fs/cgroup/cpuset# rmdir cg1
root@ubuntu:/sys/fs/cgroup/cpuset# ls
cgroup.clone_children  cpuset.cpus            cpuset.mem_hardwall             cpuset.memory_spread_page  cpuset.sched_relax_domain_level
cgroup.procs           cpuset.effective_cpus  cpuset.memory_migrate           cpuset.memory_spread_slab  notify_on_release
cgroup.sane_behavior   cpuset.effective_mems  cpuset.memory_pressure          cpuset.mems                release_agent
cpuset.cpu_exclusive   cpuset.mem_exclusive   cpuset.memory_pressure_enabled  cpuset.sched_load_balance  tasks

cgroups组进行设置

绑定了不同的subsystemcgroups组有不同的参数,这些参数都可以通过写入对应cgroups组文件系统路径下的文件进行设置。

cpuset为例,目录下的所有文件都是cgroups组的参数。其中最基础的是cpuset.cpuscpuset.memstasks,

  • cpuset.cpus表示加入该cgroups组中的进程可以运行的cpu编号
  • cpuset.mems表示可以申请的内存所属的cpu节点(对非NUMA架构,这个值只能为0)。
  • tasks表示加入到该cgroup中的进程号。
root@ubuntu:/sys/fs/cgroup/cpuset# cat cpuset.cpus
0-3
root@ubuntu:/sys/fs/cgroup/cpuset# cat cpuset.mems
0
root@ubuntu:/sys/fs/cgroup/cpuset# cat tasks
1
2
3
5
.....省略....
5937
5938
5939
5955
8113
8227

root cgroups组的参数如上,可以看出,该cgroups组限制该cgroups组可用的cpu编号是0~3,包含所有进程

我们可以创建一个子cgroup组,并设置它只能使用cpu0~1

root@ubuntu:/sys/fs/cgroup/cpuset# mkdir cg1
root@ubuntu:/sys/fs/cgroup/cpuset# cd cg1
root@ubuntu:/sys/fs/cgroup/cpuset/cg1# echo 0-1 > cpuset.cpus
root@ubuntu:/sys/fs/cgroup/cpuset/cg1# cat cpuset.cpus
0-1
root@ubuntu:/sys/fs/cgroup/cpuset/cg1# echo 0 > cpuset.mems
root@ubuntu:/sys/fs/cgroup/cpuset/cg1# cat cpuset.mems
0
root@ubuntu:/sys/fs/cgroup/cpuset/cg1# cat tasks
root@ubuntu:/sys/fs/cgroup/cpuset/cg1#

用户新创建的cgroup组的tasks文件是空的,说明没有进程受到cgroups的限制。

将进程加入cgroup

每个进程在创建时都是继承其父进程的cgroups组,默认情况下,它们都会加入root cgroups组。而我们通过写入子cgroups下的tasks文件将指定的进程移动到该cgroups

root@ubuntu:/sys/fs/cgroup/cpuset/cg1# echo $$     
5956
root@ubuntu:/sys/fs/cgroup/cpuset/cg1# echo 5956 > tasks

上面的命令可以将当前进程(bash) 加入到cg1 (如果是追加写入,就将上面的>切换为>>)

查看tasks文件可以发现已经写入了5956 (8383这一个是cat产生的临时进程,并不重要)

root@ubuntu:/sys/fs/cgroup/cpuset/cg1# cat tasks
5956
8383

我们使用stress工具来测试,(没有安装的话可以通过apt-get install stress进行安装)

通过以下命令创建4个计算密集型的子进程,并让它们工作在后台,这些子进程同样会受到cg1的限制

root@ubuntu:/sys/fs/cgroup/cpuset/cg1# stress -c 4 &

通过top命令可以看到这些进程都跑在了cpu0cpu1上 (启动top后按1切换cpu面板)

root@ubuntu:/sys/fs/cgroup/cpuset/cg1# top
top - 02:22:13 up 12:43,  2 users,  load average: 1.77, 0.79, 1.64
Tasks: 286 total,   5 running, 281 sleeping,   0 stopped,   0 zombie
%Cpu0  :100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  0.3 us,  0.0 sy,  0.0 ni, 98.3 id,  0.0 wa,  0.0 hi,  1.3 si,  0.0 st
KiB Mem :  2029760 total,   281172 free,   671696 used,  1076892 buff/cache
KiB Swap:  1003516 total,  1003516 free,        0 used.  1082568 avail Mem

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
  8425 root      20   0    7480     92      0 R  99.7  0.0   0:24.44 stress
  8427 root      20   0    7480     92      0 R  96.7  0.0   0:28.36 stress
  8428 root      20   0    7480     92      0 R  53.2  0.0   0:30.85 stress
  8426 root      20   0    7480     92      0 R  49.8  0.0   0:30.22 stress