【注意】最后更新于 May 9, 2021,文中内容可能已过时,请谨慎使用。
本节我们一起来了解一下容器的资源限制技术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
cd 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_us
和cpu.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
|
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_us
为10000
us,为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选择使用cgroupfs
或systemd
作为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。
参考