容器技术原理(三):使用 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 中的指定文件,另一种是通过 runc 或 docker 等高阶工具实现。

文件系统方式

通过 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 策略 kill 或 sleep 容器中的进程。

高阶工具方式

通过高阶工具提供的途径来配置 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。

参考链接

updatedupdated2025-02-262025-02-26