一、Cgroups简介
Cgroups(control groups)是 Linux 内核的一个功能,它可以实现限制进程或者进程组的资源(如 CPU、内存、磁盘 IO 等)。
cgroups 主要提供了如下功能:
- 资源限制: 限制资源的使用量,例如可以通过限制某个业务的内存上限,从而保护主机其他业务的安全运行。
- 优先级控制:不同的组可以有不同的资源( CPU 、磁盘 IO 等)使用优先级。
- 审计:计算控制组的资源使用情况。
- 控制:控制进程的挂起或恢复。
cgroups功能的实现依赖于三个核心概念:子系统、控制组、层级树:
- 子系统(subsystem):是一个内核的组件,一个子系统代表一类资源调度控制器。例如内存子系统可以限制内存的使用量,CPU 子系统可以限制 CPU 的使用时间。
- 控制组(cgroup):表示一组进程和一组带有参数的子系统的关联关系。例如,一个进程使用了 CPU 子系统来限制 CPU 的使用时间,则这个进程和 CPU 子系统的关联关系称为控制组。
- 层级树(hierarchy):是由一系列的控制组按照树状结构排列组成的。这种排列方式可以使得控制组拥有父子关系,子控制组默认拥有父控制组的属性,也就是子控制组会继承于父控制组。比如,系统中定义了一个控制组 c1,限制了 CPU 可以使用 1 核,然后另外一个控制组 c2 想实现既限制 CPU 使用 1 核,同时限制内存使用 2G,那么 c2 就可以直接继承 c1,无须重复定义 CPU 限制。
cgroups 的三个核心概念中,子系统是最核心的概念,因为子系统是真正实现某类资源的限制的基础。
二、Cgroups子系统
查看Linux上默认开启的子系统:
$ sudo mount -t cgroup
# mount 命令查看当前系统(操作系统版本为 CentOS7.8)已经挂载的cgroups信息
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)
以上结果可以看出,当前系统已经挂载了常用的cgroups子系统,例如 cpu、memory、pids 等子系统。这些子系统中,cpu 和 memory 子系统是容器环境中使用最多的子系统
1. cpu 子系统
由于cgroups的操作很多需要用到 root 权限,因此在执行命令前要确保已经切换到了 root 用户。
- 第一步:在 cpu 子系统下创建 cgroup
# cgroups的创建很简单,只需要在相应的子系统下创建目录即可
# mkdir /sys/fs/cgroup/cpu/mydocker
# 查看一下新创建的目录
# 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 代表限制 1 个核) ,tasks 文件中写入进程的 ID 即可(如果要限制多个进程 ID,在 tasks 文件中用换行符分隔即可)
- 第二步:创建进程,加入 cgroup
# 当前运行的 shell 进程加入 cgroup
# cd /sys/fs/cgroup/cpu/mydocker
# echo $$ > tasks
# 查看一下 tasks 文件
# cat tasks
3485
3543
# 第一个进程 ID 为当前 shell 的主进程
- 第三步:执行 CPU 耗时任务,验证 cgroup 是否可以限制 cpu 使用时间
# 制造一个死循环,提升 cpu 使用率
# while true;do echo;done;
# 新打开一个 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 这个进程被限制到了只能使用 100 % 的 cpu,也就是 1 个核
# Ctrl+c 退出循环,修改 cpu 限制时间为 0.5 核
# cd /sys/fs/cgroup/cpu/mydocker
# echo 50000 > cpu.cfs_quota_us
# 再次执行死循环
# while true;do echo;done;
# 新打开一个 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 个核
2. memory 子系统
- 第一步:在 memory 子系统下创建 cgroup
# 在相应的子系统下创建目录
# mkdir /sys/fs/cgroup/memory/mydocker
# 查看一下新创建的目录下
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 文件代表内存使用总量,单位为 byte
# 对内存使用限制为 1G
# cd /sys/fs/cgroup/memory/mydocker
# echo 1073741824 > memory.limit_in_bytes
- 第二步:创建进程,加入 cgroup
# 将当前 shell 进程 ID 写入 tasks 文件内
# cd /sys/fs/cgroup/memory/mydocker
# echo $$ > tasks
- 第三步:执行内存测试工具,申请内存
需要提前安装工具 memtester
# 申请 1500 M 内存,并且做内存测试
# 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
# 由于对当前 shell 进程内存限制为 1 G,当 memtester 使用的内存达到 1G 时,cgroup 便将 memtester 杀死
# 将内存申请调整为 500M
# 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.
# 申请成功
- 第四步:删除 cgroups。当创建的cgroups如果不想使用了,直接删除创建的文件夹
# rmdir /sys/fs/cgroup/memory/mydocker/
三、Docker如何使用Cgroups?
# 创建一个 nginx 容器,并且限制内存为 1G
docker run -it -m=1g nginx
# 进入cgroups内存子系统的目录,使用 ls 命令查看一下该目录下的内容
# 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 在内存子系统下创建的
# 进入到 docker 目录下查看
# 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 容器的 ID
# 查看一下该容器的 memory.limit_in_bytes 文件的内容
# cd cb5c5391177b44ad87636bf3840ecdda83529e51b76a6406d6742f56a2535d5e
# cat memory.limit_in_bytes
1073741824
# 内存限制值正好为 1G
总结一下:Docker 创建容器时,Docker 会根据启动容器的参数,在对应的 cgroups 子系统下创建以容器 ID 为名称的目录, 然后根据容器启动时设置的资源限制参数, 修改对应的 cgroups 子系统资源限制文件, 从而达到资源限制的效果。