容器技术原理(三):使用 Cgroups 实现资源限制

cgroups(control groups)是由 Linux 内核提供的一种特性,它能够限制、核算和隔离一组进程所使用的系统资源(如 CPU、内存、磁盘 I/O、网络等)。

在上一篇文章中我们已了解 Namespace 在容器技术中扮演的角色,如果说 Namespace 控制了容器中的进程能看到什么,那么 cgroups 则控制了容器中的进程能使用多少资源。Namespace 实现了进程的隔离,cgroups 则实现了资源的限制,后者同样是构建容器的基础。

本文将沿袭 Namespace 文章的行文思路,实际创建一个容器,观察宿主机中 cgroups 的变化,来实际展示 cgroups 如何工作,然后了解如何自行配置 cgroups。

cgroup 在何时创建

Linux 内核通过一个叫做 cgroupfs 的伪文件系统来提供管理 cgroup 的接口,我们可以通过 lscgroup 命令来列出系统中已有的 cgroup,该命令实际上遍历了 /sys/fs/cgroup/ 目录中的文件:

1
$ lscgroup | tee cgroup.a

如果你使用的 Linux 发行版没有 lscgroup 命令,可通过 command-not-found.com 提供的指令下载安装。

我们将输出结果保存到 cgroup.a 文件中。接着在另一窗口中根据 Namespace 文章中的步骤启动一个容器:

1
2
$ cd /mycontainer
$ runc run mybox

回到原来的窗口再次执行 lsgroup 命令:

1
$ lscgroup | tee group.b

现在对比两次 lscgroup 命令的输出结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ diff group.a group.b

> perf_event:/mybox
> freezer:/mybox
> net_cls,net_prio:/mybox
> cpu,cpuacct:/user.slice/mybox
> blkio:/user.slice/mybox
> cpuset:/mybox
> hugetlb:/mybox
> pids:/user.slice/user-0.slice/session-5.scope/mybox
> memory:/user.slice/user-0.slice/session-5.scope/mybox
> devices:/user.slice/mybox

从结果中可看到,mybox 容器创建后,系统中专门为其创建了所有类型的新的 cgroup。

cgroup 如何控制容器的资源

cgroup 所控制的对象是进程,它控制一个或一组进程所能使用多少内存/CPU/网络等等。一个 cgroup 的 tasks 列表中记录了其所控制进程的 PID,该 tasks 实际上也是 cgroupfs 中的一个文件。

init 进程

我们首先在宿主机中打印出容器中的进程信息,找到容器的 init 进程:

1
2
3
4
$ runc ps mybox

UID          PID    PPID  C STIME TTY          TIME CMD
root        2250    2240  0 15:28 pts/0    00:00:00 sh

任意打印一些类型的 cgroup 的 tasks 列表:

1
2
3
4
$ cat /sys/fs/cgroup/memory/user.slice/user-0.slice/session-5.scope/mybox/tasks
2250
$ cat /sys/fs/cgroup/blkio/user.slice/mybox/tasks
2250

这一过程简单明了:容器创建之后,容器的 init 进程会被加入到为该容器所创建的 cgroups 之中,我们可以通过 /proc/$PID/cgroup 得到更肯定的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ cat /proc/2250/cgroup
12:devices:/user.slice/mybox
11:memory:/user.slice/user-0.slice/session-5.scope/mybox
10:pids:/user.slice/user-0.slice/session-5.scope/mybox
9:hugetlb:/mybox
8:cpuset:/mybox
7:rdma:/
6:blkio:/user.slice/mybox
5:cpu,cpuacct:/user.slice/mybox
4:net_cls,net_prio:/mybox
3:freezer:/mybox
2:perf_event:/mybox
1:name=systemd:/user.slice/user-0.slice/session-5.scope/mybox
0::/user.slice/user-0.slice/session-5.scope

容器中的其他进程

接下来我们在 mybox 容器中运行一个新的进程:

1
2
# 在 mybox 容器中运行
$ top -b

看看是否会创建新的 cgroup:

1
2
$ lscgroup | tee group.c
$ diff group.b group.c

没有输出任何结果,说明没有创建新的 cgroup。既然 cgroup 可以控制一组进程,我们猜测在已运行容器中新建的进程,也都会加入到 init 进程所属的 cgroups 中。

下面开始验证,首先找到新建进程的 PID:

1
2
3
4
$ runc ps mybox
UID          PID    PPID  C STIME TTY          TIME CMD
root        2250    2240  0 15:28 pts/0    00:00:00 sh
root        2576    2250  0 15:59 pts/0    00:00:00 top -b

新进程的 PID 是 2576,然后打印该进程的 cgroups 信息:

1
cat /proc/2576/cgroup

输出和 PID 2250 进程的输出完全一致,我们也可以打印其中一个 cgroup 的 tasks 列表:

1
2
3
cat /sys/fs/cgroup/blkio/user.slice/mybox/tasks
2250
2576

完全符合预期。实际上向 tasks 文件直接写入进程的 PID 就实现了将进程加入到该 cgroup 中。当一个容器被创建时,将为每种类型的资源创建一个新的 cgroup,在容器中运行的所有进程都将加入到这些 cgroup 中。

通过控制容器中运行的所有进程,cgroups 实现了对容器的资源限制。

如何配置 cgroup

下面我们将以内存 cgroup 为例,了解如何配置 cgroup 以实现对 mybox 容器的内存限制。

配置 cgroup 有两种方式,一种是直接修改 cgroupfs 中的指定文件,另一种是通过 runcdocker 等高阶工具实现。

文件系统方式

通过 cgroupfs 的方式,查看/修改该 cgroup 目录下的特定文件即可查看/设置该 cgroup 的限额:

1
2
cat /sys/fs/cgroup/memory/user.slice/user-0.slice/session-5.scope/mybox/memory.limit_in_bytes
9223372036854771712

修改 memory.limit_in_bytes 文件即可设置最大可用内存,现在我们并未对该容器设置任何限制,因此内存限制的当前值是一个无意义的特别大的值,现在我们向该文件直接写入新的值:

1
echo "100000000" > /sys/fs/cgroup/memory/user.slice/user-0.slice/session-5.scope/mybox/memory.limit_in_bytes

这样就设置了新的内存限制。写入新的限制值后,容器中的所有进程不能使用总共超过 100M 的内存,超过后将根据 memory.oom_control 文件中设置的 OOM 策略 killsleep 容器中的进程。

高阶工具方式

通过高阶工具提供的途径来配置 cgroup 是一种更友好的方式,虽然这些工具背后的实现也是如上所述更改 cgroupfs

对于 runc 来说,需要修改 filesystem bundle 中的 config.json 文件来配置 cgroup。设置内存限制需要如下修改 JSON 对象中的 linux.resources 字段:

1
2
3
4
5
6
7
"resources": {
    "memory": {
    "limit": 100000,
    "reservation": 200000
    },
    ...
}

对于 docker 来说更为简单,它本身就是一个面向用户的封装好的工具,执行 docker run 命令时通过 --memory 选项即可指定内存限制。实际上该参数会被写入到 config.json 由运行时实现 runc 使用,再由 runc 去更改 cgroupfs

参考链接

updatedupdated2022-08-032022-08-03