本节我们一起来了解一下容器的资源限制技术CGroups。CGroups即Linux Control Group其作用是限制一组进程可以使用的资源上限(CPU、内存等)。

1.CGroups的基本概念

学习CGroup需要先了解几个CGroup的基本概念:

  • Task: 在cgroup中,task可以理解为一个进程,但这里的进程和一般意义上的OS进程不太一样,实际上是进程ID和线程ID列表,后边再讨论
  • CGroup: 即控制组。一个控制组就是一组按照某种标准划分的Tasks。可以理解为资源限制是以进程组为单位实现的,一个进程加入到某个控制组后,就会受到相应配置的资源限制。
  • Hierarchy: cgroup的层级组织关系,cgroup以树形层级组织,每个cgroup子节点默认继承其父cgroup节点的配置属性。这样每个Hierarchy在初始化会有root cgroup
  • Subsystem: 即子系统,更准确的表述应该是资源控制器(Resource Controller)更合适一些。子系统表示具体的资源配置,如CPU使用,内存占用等。Subsystem附加到Hierarchy上后可用。

当前的cgroup有两个版本v1和v2,据说是随着越来越多的特性被加入导致cgroups v1变得难以维护,从linux kernel 3.1开始了cgroups v2的开发,v2到linux kernel4.5才正式稳定可用。

CGroups支持的子系统包含以下几类,即为每种可以控制的资源定义了一个子系统:

  • cpuset: 为cgroup中的进程分配单独的CPU节点,即可以绑定到特定的CPU
  • cpu: 限制cgroup中进程的CPU使用份额
  • cpuacct: 统计cgroup中进程的CPU使用情况
  • memory: 限制cgroup中进程的内存使用,并能报告内存使用情况
  • devices: 控制cgroup中进程能访问哪些文件设备(设备文件的创建、读写)
  • freezer: 挂起或恢复cgroup中的task
  • net_cls: 可以标记cgroups 中进程的网络数据包,然后可以使用tc模块(traffic contro)对数据包进行控制
  • blkio: 限制cgroup中进程的块设备IO
  • perf_event: 监控cgroup中进程的perf时间,可用于性能调优
  • hugetlb: hugetlb的资源控制功能
  • pids: 限制cgroup中可以创建的进程数
  • net_prio: 允许管理员动态的通过各种应用程序设置网络传输的优先级,类似于socket 选项的SO_PRIORITY

通过上面的各个子系统,可以看出,使用CGroups可以控制的资源有: CPU、内存、网络、IO、文件设备等。可以查看/proc/cgroups文件,查看当前系统支持的cgroups subsystem:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
cat /proc/cgroups
#subsys_name    hierarchy       num_cgroups     enabled
cpuset              2               4               1
cpu                 6               66              1
cpuacct             6               66              1
memory              8               64              1
devices             9               65              1
freezer             7               4               1
net_cls             3               4               1
blkio               5               64              1
perf_event          10              4               1
hugetlb             11              4               1
pids                4               4               1
net_prio            3               4               1

2.查看cgroup hierarchy层级树

在使用cgroups时需要先挂载,例如在centos下使用df -h | grep cgroup也可以查看:

1
tmpfs     3.9G     0  3.9G   0% /sys/fs/cgroup

被挂载到了/sys/fs/cgroup

cgroup是一种文件系统类型,可以使用mount --type cgroup命令查看当前系统挂载了哪些cgroup。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
mount --type cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)

/sys/fs/cgroup下的每个子目录对应一个subsystem,cgroup是以目录形式组织的,/是cgroup的根目录,cgroup的根目录可以被挂载到任意目录,例如cgroups的memory子系统的挂载点是/sys/fs/cgroup/memory, 则/sys/fs/cgroup/memory/对应memory子系统的根目录/

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
ll /sys/fs/cgroup/memory/
total 0
drwxr-xr-x  buildkit
-rw-r--r--  cgroup.clone_children
--w--w--w-  cgroup.event_control
-rw-r--r--  cgroup.procs
-r--r--r--  cgroup.sane_behavior
drwxr-xr-x  default
-rw-r--r--  memory.failcnt
--w-------  memory.force_empty
-rw-r--r--  memory.kmem.failcnt
-rw-r--r--  memory.kmem.limit_in_bytes
-rw-r--r--  memory.kmem.max_usage_in_bytes
-r--r--r--  memory.kmem.slabinfo
-rw-r--r--  memory.kmem.tcp.failcnt
-rw-r--r--  memory.kmem.tcp.limit_in_bytes
-rw-r--r--  memory.kmem.tcp.max_usage_in_bytes
-r--r--r--  memory.kmem.tcp.usage_in_bytes
-r--r--r--  memory.kmem.usage_in_bytes
-rw-r--r--  memory.limit_in_bytes
-rw-r--r--  memory.max_usage_in_bytes
-rw-r--r--  memory.memsw.failcnt
-rw-r--r--  memory.memsw.limit_in_bytes
-rw-r--r--  memory.memsw.max_usage_in_bytes
-r--r--r--  memory.memsw.usage_in_bytes
-rw-r--r--  memory.move_charge_at_immigrate
-r--r--r--  memory.numa_stat
-rw-r--r--  memory.oom_control
----------  memory.pressure_level
-rw-r--r--  memory.soft_limit_in_bytes
-r--r--r--  memory.stat
-rw-r--r--  memory.swappiness
-r--r--r--  memory.usage_in_bytes
-rw-r--r--  memory.use_hierarchy
-rw-r--r--  notify_on_release
-rw-r--r--  release_agent
drwxr-xr-x  system.slice
-rw-r--r--  tasks
drwxr-xr-x  user.slice

上面包含buildkit,default, system.slice, user.slice等目录,这些目录下可能还会有子目录,相当于组织为如下的cgroup hierarchy层级树:

1
2
3
4
5
6
/
├── buildkit
├── default
├── system.slice
├── user.slice
└── miniflux-db

例如一台部署了mysql的机器,使用systemctl status mysqld可以看出mysqld进程所在的cgroup为/system.slice/mysqld.service:

1
2
3
4
5
6
7
systemctl status mysqld
● mysqld.service - MySQL Server
  ......
 Main PID: 5662 (mysqld)
   Memory: 598.6M
   CGroup: /system.slice/mysqld.service
           └─5662 /usr/local/mysql/bin/mysqld --daemonize --pid-file=/usr/local/mysql/mysql.pid

实际系统的文件系统目录有/sys/fs/cgroup/cpu/system.slice/mysqld.service/sys/fs/cgroup/memory/system.slice/mysqld.service,可以理解为cpu和memory子系统被附加到了cgroup /system.slice/mysqld.service上。

3.cgroup初体验

接下来以cgroups cpu子系统为例体验一下手动设置cgroup的,在/sys/fs/cgroup/cpu下创建一个名为foo的目录:

1
2
3
4
5
6
7
cd /sys/fs/cgroup/cpu
mkdir foo

foo && ls
cgroup.clone_children  cpuacct.stat          cpu.cfs_period_us  cpu.rt_runtime_us  notify_on_release
cgroup.event_control   cpuacct.usage         cpu.cfs_quota_us   cpu.shares         tasks
cgroup.procs           cpuacct.usage_percpu  cpu.rt_period_us   cpu.stat

可以看出foo目录创建完成后,自动在其里面创建了cgroup相关的文件。重点关注cpu.cfs_period_uscpu.cfs_quota_us,前置用来配置时间周期长度,默认值是100000us,后者用来设置在此时间周期长度内所能使用的cpu时间数,默认-1表示不受时间限制。

1
2
3
4
cat cpu.cfs_period_us
100000
cat cpu.cfs_quota_us
-1

写一个死循环且空耗的python脚本loop.py,后台执行将测试机cpu打满:

1
2
while True:
    pass
1
2
python loop.py &
[1] 30342

可以使用top命令查看30342进程cpu使用率确实达到了100%。下面将进程ID 30342写入/sys/fs/cgroup/cpu/foo/tasks

1
echo 30342 > /sys/fs/cgroup/cpu/foo/tasks

设置/sys/fs/cgroup/cpu/foo/cpu.cfs_quota_us10000us,为cpu.cfs_period_us默认值100000us,即表示我们要限制cpu使用率为10%.

1
echo 10000 > /sys/fs/cgroup/cpu/foo/cpu.cfs_quota_us

此时再次top命令查看30342进程cpu使用率被限制到了10%。

测试完成后如果要删除cpu:foo这个cgroup需要借助libcgroup工具:

1
2
3
yum install libcgroup libcgroup-tools

cgdelete cpu:foo

4.在Containerd容器汇总使用cgroup

接下来使用nerdctl启动一个redis容器,并限制其使用内存100Mb:

1
2
3
4
nerdctl run -d -m 100m --name redis redis:alpine3.13

CONTAINER ID    IMAGE                                 COMMAND                   CREATED          STATUS    PORTS   NAMES
241060f564f7    docker.io/library/redis:alpine3.13    "docker-entrypoint.s…"    5 minutes ago    Up                 redis

查看该容器的cgroup:

1
2
3
ll /sys/fs/cgroup/memory/default/
241060f564f7f696c0aa3a9e75a0403d2b8e1e7bfe3744bcabbe7a1b417d7c90
......

可以看到/sys/fs/cgroup/memory/default/下出现了一个与容器ID同名的文件夹,这个文件夹下有很多文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ll /sys/fs/cgroup/memory/default/241060f564f7f696c0aa3a9e75a0403d2b8e1e7bfe3744bcabbe7a1b417d7c90/
......

cat memory.limit_in_bytes
104857600

cat tasks
26377
26462
26463
26464
26465

文件memory.limit_in_bytes中的内容是我们设置的100Mb的内存限制。文件task中第一行是redis进程在主机上的进程id,下面几行是这个进程下的线程。可以使用下面的命令查看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ps -ef | grep 26377
polkitd  26377 26347  0 18:41 ?        00:00:01 redis-server *:6379

ps -T -p 26377
PID  SPID TTY          TIME CMD
26377 26377 ?        00:00:01 redis-server
26377 26462 ?        00:00:00 bio_close_file
26377 26463 ?        00:00:00 bio_aof_fsync
26377 26464 ?        00:00:00 bio_lazy_free
26377 26465 ?        00:00:00 jemalloc_bg_thd

删除这个redis容器后,/sys/fs/cgroup/memory/default/下的容器ID文件夹会自动删除。

5.cgroupfs和systemd

如果linux系统使用systemd初始化系统时,初始化进程会生成一个root cgroup,systemd与cgroup紧密联系,每个systemd unit都将会被分配一个cgroup。同样可以配置容器运行时如containerd选择使用cgroupfssystemd作为cgroup驱动。containerd默认使用的是cgroupfs,但对于使用了systemd的linux发行版来说就同时存在两个cgroup管理器,对于该服务器上启动的容器使用的是cgroupfs,而对于其他systemd管理的进程使用的是systemd,这样在服务器资源负载高的情况下可能会变的不稳定。因此对于使用了systemd的linux系统,推荐将容器运行时的cgroup驱动更改为systemd。

修改containerd的cgroup驱动方法(containerd 1.3及以后的版本)如下,在其配置文件/etc/containerd/config.toml中[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]下添加SystemdCgroup = true。修改后重启containerd。

参考