10 资源限制:如何通过 Cgroups 机制实现资源限制?¶
上一课时,我们知道使用不同的 Namespace,可以实现容器中的进程看不到别的容器的资源,但是有一个问题你是否注意到?容器内的进程仍然可以任意使用主机的 CPU、内存等资源。如果某一个容器使用的主机资源过多,可能导致主机资源竞争,进而影响业务。那么,如果我们想限制一个容器的资源使用量(如 CPU、内存等),应该如何做呢?
这里就需要用到 Linux 内核的另一个核心技术 cgroups。那么究竟什么是 cgroups?我们应该如何使用 cgroups?Docker 又是如何使用 cgroups 的?下面我带你一一解密。
首先我们来学习下什么是 cgroups。
cgroups¶
cgroups(全称:control groups)是 Linux 内核的一个功能,它可以实现限制进程或者进程组的资源(如 CPU、内存、磁盘 IO 等)。
在 2006 年,Google 的工程师( Rohit Seth 和 Paul Menage 为主要发起人) 发起了这个项目,起初项目名称并不是 cgroups,而被称为进程容器(process containers)。在 2007 年 cgroups 代码计划合入 Linux 内核,但是当时在 Linux 内核中,容器(container)这个词被广泛使用,并且拥有不同的含义。为了避免命名混乱和歧义,进程容器被重名为 cgroups,并在 2008 年成功合入 Linux 2.6.24 版本中。cgroups 目前已经成为 systemd、Docker、Linux Containers(LXC) 等技术的基础。
cgroups 功能及核心概念¶
cgroups 主要提供了如下功能。
- 资源限制: 限制资源的使用量,例如我们可以通过限制某个业务的内存上限,从而保护主机其他业务的安全运行。
- 优先级控制:不同的组可以有不同的资源( CPU 、磁盘 IO 等)使用优先级。
- 审计:计算控制组的资源使用情况。
- 控制:控制进程的挂起或恢复。
了解了 cgroups 可以为我们提供什么功能,下面我来看下 cgroups 是如何实现这些功能的。
cgroups 功能的实现依赖于三个核心概念:子系统、控制组、层级树。
- 子系统(subsystem):是一个内核的组件,一个子系统代表一类资源调度控制器。例如内存子系统可以限制内存的使用量,CPU 子系统可以限制 CPU 的使用时间。
- 控制组(cgroup):表示一组进程和一组带有参数的子系统的关联关系。例如,一个进程使用了 CPU 子系统来限制 CPU 的使用时间,则这个进程和 CPU 子系统的关联关系称为控制组。
- 层级树(hierarchy):是由一系列的控制组按照树状结构排列组成的。这种排列方式可以使得控制组拥有父子关系,子控制组默认拥有父控制组的属性,也就是子控制组会继承于父控制组。比如,系统中定义了一个控制组 c1,限制了 CPU 可以使用 1 核,然后另外一个控制组 c2 想实现既限制 CPU 使用 1 核,同时限制内存使用 2G,那么 c2 就可以直接继承 c1,无须重复定义 CPU 限制。
cgroups 的三个核心概念中,子系统是最核心的概念,因为子系统是真正实现某类资源的限制的基础。
cgroups 子系统实例¶
下面我通过一个实例演示一下在 Linux 上默认都启动了哪些子系统。
我们先通过 mount 命令查看一下当前系统已经挂载的 cgroups 信息:
$ sudo mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,devices)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb)
我的操作系统版本为 CentOS 7.8,内核版本为 3.10.0-1127.el7.x86_64。不同内核版本的 cgroups 子系统和使用方式可能略有差异。如果你对 cgroups 不是很熟悉,请尽量使用相同的内核环境操作。
通过输出可以看到,当前系统已经挂载了常用的 cgroups 子系统,例如 CPU、memory 和 pids。CPU 和 memory 是容器环境中使用最多的两个子系统,下面分别介绍它们的用法。
CPU 子系统¶
首先以 CPU 子系统为例,演示 cgroups 如何限制进程的 CPU 使用时间。由于 cgroups 的许多操作需要 root 权限,以下命令默认在 root 用户下执行。
第一步:在 CPU 子系统下创建 cgroup¶
cgroups 的创建很简单,只需要在相应的子系统下创建目录即可。下面在 CPU 子系统下创建测试目录:
执行完上述命令后,我们查看一下我们新创建的目录下发生了什么?
# ls -l /sys/fs/cgroup/cpu/mydocker
total 0
-rw-r--r--. 1 root root 0 Sep 5 09:19 cgroup.clone_children
--w--w--w-. 1 root root 0 Sep 5 09:19 cgroup.event_control
-rw-r--r--. 1 root root 0 Sep 5 09:19 cgroup.procs
-rw-r--r--. 1 root root 0 Sep 5 09:19 cpu.cfs_period_us
-rw-r--r--. 1 root root 0 Sep 5 09:19 cpu.cfs_quota_us
-rw-r--r--. 1 root root 0 Sep 5 09:19 cpu.rt_period_us
-rw-r--r--. 1 root root 0 Sep 5 09:19 cpu.rt_runtime_us
-rw-r--r--. 1 root root 0 Sep 5 09:19 cpu.shares
-r--r--r--. 1 root root 0 Sep 5 09:19 cpu.stat
-r--r--r--. 1 root root 0 Sep 5 09:19 cpuacct.stat
-rw-r--r--. 1 root root 0 Sep 5 09:19 cpuacct.usage
-r--r--r--. 1 root root 0 Sep 5 09:19 cpuacct.usage_percpu
-rw-r--r--. 1 root root 0 Sep 5 09:19 notify_on_release
-rw-r--r--. 1 root root 0 Sep 5 09:19 tasks
可以看到,新目录下自动创建了许多控制文件。其中,cpu.cfs_quota_us 表示一个调度周期内允许使用的 CPU 时间,单位为微秒。例如,要限制进程最多使用 1 个 CPU 核,可以写入 100000(一个调度周期通常为 100000 微秒),然后将进程 ID 写入 tasks 文件;多个进程 ID 需要逐行写入。
此时,cgroup 就创建好了。
第二步:创建进程并加入 cgroup¶
为了方便演示,先把当前运行的 shell 进程加入 cgroup,然后在当前 shell 中运行 CPU 耗时任务。子进程会继承父进程的 cgroup。
使用以下命令将 shell 进程加入 cgroup 中:
查看一下 tasks 文件内容:
其中第一个进程 ID 是当前 shell 的主进程,也就是 3485。
第三步:执行 CPU 耗时任务并验证限制¶
下面使用死循环提高 CPU 使用率:
执行上述命令后,新打开一个 shell 窗口,使用 top -p 查看当前 CPU 使用率。-p 参数后面跟进程 ID,这里是 3485。
top -p 3485
top - 09:51:35 up 3 days, 22:00, 4 users, load average: 1.59, 0.58, 0.27
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
%Cpu(s): 9.7 us, 2.8 sy, 0.0 ni, 87.4 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 32779616 total, 31009780 free, 495988 used, 1273848 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 31852336 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3485 root 20 0 116336 2852 1688 S 99.7 0.0 2:10.71 bash
从输出可以看到,3485 进程的 CPU 使用率被限制在 100%,即约 1 个 CPU 核,说明 cgroup 的 CPU 限制已生效。按 Ctrl+C 可以退出死循环。
为了进一步验证限制效果,将 CPU 配额调整为 0.5 个核,命令如下:
同样使用上面的命令来制造死循环:
保持当前窗口,再打开一个 shell 窗口,使用 top -p 查看 CPU 使用率:
top -p 3485
top - 10:05:25 up 3 days, 22:14, 3 users, load average: 1.02, 0.43, 0.40
Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie
%Cpu(s): 5.0 us, 1.3 sy, 0.0 ni, 93.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 32779616 total, 31055676 free, 450224 used, 1273716 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 31898216 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3485 root 20 0 115544 2116 1664 R 50.0 0.0 0:23.39 bash
从输出可以看到,CPU 使用率已被限制在 50%,即约 0.5 个 CPU 核。CPU 验证完成后,继续验证 memory 子系统。
Memory 子系统¶
第一步:在 memory 子系统下创建 cgroup¶
同样,我们查看一下新创建的目录下发生了什么?
total 0
-rw-r--r--. 1 root root 0 Sep 5 10:18 cgroup.clone_children
--w--w--w-. 1 root root 0 Sep 5 10:18 cgroup.event_control
-rw-r--r--. 1 root root 0 Sep 5 10:18 cgroup.procs
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.failcnt
--w-------. 1 root root 0 Sep 5 10:18 memory.force_empty
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.failcnt
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.limit_in_bytes
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.max_usage_in_bytes
-r--r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.slabinfo
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.tcp.failcnt
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.tcp.limit_in_bytes
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.tcp.max_usage_in_bytes
-r--r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.tcp.usage_in_bytes
-r--r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.usage_in_bytes
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.limit_in_bytes
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.max_usage_in_bytes
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.memsw.failcnt
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.memsw.limit_in_bytes
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.memsw.max_usage_in_bytes
-r--r--r--. 1 root root 0 Sep 5 10:18 memory.memsw.usage_in_bytes
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.move_charge_at_immigrate
-r--r--r--. 1 root root 0 Sep 5 10:18 memory.numa_stat
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.oom_control
----------. 1 root root 0 Sep 5 10:18 memory.pressure_level
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.soft_limit_in_bytes
-r--r--r--. 1 root root 0 Sep 5 10:18 memory.stat
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.swappiness
-r--r--r--. 1 root root 0 Sep 5 10:18 memory.usage_in_bytes
-rw-r--r--. 1 root root 0 Sep 5 10:18 memory.use_hierarchy
-rw-r--r--. 1 root root 0 Sep 5 10:18 notify_on_release
-rw-r--r--. 1 root root 0 Sep 5 10:18 tasks
memory.limit_in_bytes 表示内存上限,单位为字节。这里将内存限制为 1 GiB,向该文件写入 1073741824:
第二步:创建进程并加入 cgroup¶
同样,将当前 shell 进程 ID 写入 tasks 文件:
第三步:执行内存测试并申请内存¶
这里使用 memtester 工具验证内存限制。安装方法可参考这篇文档。安装完成后执行:
# memtester 1500M 1
memtester version 4.2.2 (64-bit)
Copyright (C) 2010 Charles Cazabon.
Licensed under the GNU General Public License version 2 (only).
pagesize is 4096
pagesizemask is 0xfffffffffffff000
want 1500MB (1572864000 bytes)
got 1500MB (1572864000 bytes), trying mlock ...Killed
该命令申请 1500 MB 并执行内存测试。由于当前 shell 被限制为 1 GiB,达到上限后 cgroup 会终止 memtester。输出中的 Killed 表示进程触及内存上限,验证了限制已生效。
再将申请量调整为 500 MB:
# memtester 500M 1
memtester version 4.2.2 (64-bit)
Copyright (C) 2010 Charles Cazabon.
Licensed under the GNU General Public License version 2 (only).
pagesize is 4096
pagesizemask is 0xfffffffffffff000
want 500MB (524288000 bytes)
got 500MB (524288000 bytes), trying mlock ...locked.
Loop 1/1:
Stuck Address : ok
Random Value : ok
Compare XOR : ok
Compare SUB : ok
Compare MUL : ok
Compare DIV : ok
Compare OR : ok
Compare AND : ok
Sequential Increment: ok
Solid Bits : ok
Block Sequential : ok
Checkerboard : ok
Bit Spread : ok
Bit Flip : ok
Walking Ones : ok
Walking Zeroes : ok
8-bit Writes : ok
16-bit Writes : ok
Done.
可以看到,memtester 已成功申请 500 MB 并完成测试。至此,CPU 和 memory 子系统的基本实验完成;更多 cgroups 用法可参考 Red Hat 官方文档。
删除 cgroups¶
不再使用的 cgroup 可以删除,但目录必须为空。下面删除 memory 子系统下的 mydocker:
学习了 cgroups 的使用方式,下面我带你了解一下 Docker 是如何使用 cgroups 的。
Docker 是如何使用 cgroups 的?¶
首先,我们使用以下命令创建一个 nginx 容器:
上述命令创建并启动了一个 nginx 容器,并将内存限制为 1 GiB。进入 cgroups 的 memory 子系统目录,查看其中的内容:
# ls -l /sys/fs/cgroup/memory
total 0
-rw-r--r--. 1 root root 0 Sep 1 11:50 cgroup.clone_children
--w--w--w-. 1 root root 0 Sep 1 11:50 cgroup.event_control
-rw-r--r--. 1 root root 0 Sep 1 11:50 cgroup.procs
-r--r--r--. 1 root root 0 Sep 1 11:50 cgroup.sane_behavior
drwxr-xr-x. 3 root root 0 Sep 5 10:50 docker
... 省略部分输出
可以看到,该目录下有一个 docker 目录,这是 Docker 在 memory 子系统下创建的。进入该目录查看相关内容:
# cd /sys/fs/cgroup/memory/docker
# ls -l
total 0
drwxr-xr-x. 2 root root 0 Sep 5 10:49 cb5c5391177b44ad87636bf3840ecdda83529e51b76a6406d6742f56a2535d5e
-rw-r--r--. 1 root root 0 Sep 4 10:40 cgroup.clone_children
--w--w--w-. 1 root root 0 Sep 4 10:40 cgroup.event_control
-rw-r--r--. 1 root root 0 Sep 4 10:40 cgroup.procs
... 省略部分输出
-rw-r--r--. 1 root root 0 Sep 4 10:40 tasks
可以看到,docker 目录下有一个随机 ID 命名的子目录,它就是上面创建的 nginx 容器目录。进入该目录,查看容器的 memory.limit_in_bytes:
# cd cb5c5391177b44ad87636bf3840ecdda83529e51b76a6406d6742f56a2535d5e
# cat memory.limit_in_bytes
1073741824
可以看到,内存限制值正好为 1 GiB。Docker 创建容器时,会根据启动参数在对应的 cgroups 子系统下创建以容器 ID 命名的目录,并将资源限制写入相应的控制文件。
小结¶
本课时介绍了 cgroups 的核心概念、资源限制能力,以及 CPU 和 memory 子系统的基本操作。cgroups 不仅可以限制资源,还可以统计资源使用情况,容器监控系统也常以此作为数据来源。
需要注意,cgroups 可以限制资源上限,但不保证容器始终获得该配额。例如,限制容器最多使用 1 个 CPU 核,并不意味着它在 CPU 资源竞争时一定能持续使用满 1 个核。