重学容器06: 容器资源限制背后的技术cgroups
2021-05-09
本节我们一起来了解一下容器的资源限制技术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:
1cat /proc/cgroups
2#subsys_name hierarchy num_cgroups enabled
3cpuset 2 4 1
4cpu 6 66 1
5cpuacct 6 66 1
6memory 8 64 1
7devices 9 65 1
8freezer 7 4 1
9net_cls 3 4 1
10blkio 5 64 1
11perf_event 10 4 1
12hugetlb 11 4 1
13pids 4 4 1
14net_prio 3 4 1
2.查看cgroup hierarchy层级树 #
在使用cgroups时需要先挂载,例如在centos下使用df -h | grep cgroup
也可以查看:
1tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
被挂载到了/sys/fs/cgroup
。
cgroup是一种文件系统类型,可以使用mount --type cgroup
命令查看当前系统挂载了哪些cgroup。
1mount --type cgroup
2cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
3cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
4cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
5cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
6cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
7cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
8cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
9cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
10cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
11cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
12cgroup 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子系统的根目录/
。
1ll /sys/fs/cgroup/memory/
2total 0
3drwxr-xr-x buildkit
4-rw-r--r-- cgroup.clone_children
5--w--w--w- cgroup.event_control
6-rw-r--r-- cgroup.procs
7-r--r--r-- cgroup.sane_behavior
8drwxr-xr-x default
9-rw-r--r-- memory.failcnt
10--w------- memory.force_empty
11-rw-r--r-- memory.kmem.failcnt
12-rw-r--r-- memory.kmem.limit_in_bytes
13-rw-r--r-- memory.kmem.max_usage_in_bytes
14-r--r--r-- memory.kmem.slabinfo
15-rw-r--r-- memory.kmem.tcp.failcnt
16-rw-r--r-- memory.kmem.tcp.limit_in_bytes
17-rw-r--r-- memory.kmem.tcp.max_usage_in_bytes
18-r--r--r-- memory.kmem.tcp.usage_in_bytes
19-r--r--r-- memory.kmem.usage_in_bytes
20-rw-r--r-- memory.limit_in_bytes
21-rw-r--r-- memory.max_usage_in_bytes
22-rw-r--r-- memory.memsw.failcnt
23-rw-r--r-- memory.memsw.limit_in_bytes
24-rw-r--r-- memory.memsw.max_usage_in_bytes
25-r--r--r-- memory.memsw.usage_in_bytes
26-rw-r--r-- memory.move_charge_at_immigrate
27-r--r--r-- memory.numa_stat
28-rw-r--r-- memory.oom_control
29---------- memory.pressure_level
30-rw-r--r-- memory.soft_limit_in_bytes
31-r--r--r-- memory.stat
32-rw-r--r-- memory.swappiness
33-r--r--r-- memory.usage_in_bytes
34-rw-r--r-- memory.use_hierarchy
35-rw-r--r-- notify_on_release
36-rw-r--r-- release_agent
37drwxr-xr-x system.slice
38-rw-r--r-- tasks
39drwxr-xr-x user.slice
上面包含buildkit
,default
, system.slice
, user.slice
等目录,这些目录下可能还会有子目录,相当于组织为如下的cgroup hierarchy层级树:
1/
2├── buildkit
3├── default
4├── system.slice
5├── user.slice
6└── miniflux-db
例如一台部署了mysql的机器,使用systemctl status mysqld
可以看出mysqld进程所在的cgroup为/system.slice/mysqld.service
:
1systemctl status mysqld
2● mysqld.service - MySQL Server
3 ......
4 Main PID: 5662 (mysqld)
5 Memory: 598.6M
6 CGroup: /system.slice/mysqld.service
7 └─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
的目录:
1cd /sys/fs/cgroup/cpu
2mkdir foo
3
4cd foo && ls
5cgroup.clone_children cpuacct.stat cpu.cfs_period_us cpu.rt_runtime_us notify_on_release
6cgroup.event_control cpuacct.usage cpu.cfs_quota_us cpu.shares tasks
7cgroup.procs cpuacct.usage_percpu cpu.rt_period_us cpu.stat
可以看出foo目录创建完成后,自动在其里面创建了cgroup相关的文件。重点关注cpu.cfs_period_us
和cpu.cfs_quota_us
,前者用来配置时间周期长度,默认值是100000us,后者用来设置在此时间周期长度内所能使用的cpu时间数,默认-1表示不受时间限制。
1cat cpu.cfs_period_us
2100000
3cat cpu.cfs_quota_us
4-1
写一个死循环且空耗的python脚本loop.py,后台执行将测试机cpu打满:
1while True:
2 pass
1python loop.py &
2[1] 30342
可以使用top命令查看30342进程cpu使用率确实达到了100%。下面将进程ID 30342写入/sys/fs/cgroup/cpu/foo/tasks
1echo 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%.
1echo 10000 > /sys/fs/cgroup/cpu/foo/cpu.cfs_quota_us
此时再次top命令查看30342进程cpu使用率被限制到了10%。
测试完成后如果要删除cpu:foo这个cgroup需要借助libcgroup工具:
1yum install libcgroup libcgroup-tools
2
3cgdelete cpu:foo
4.在Containerd容器中使用cgroup #
接下来使用nerdctl启动一个redis容器,并限制其使用内存100Mb:
1nerdctl run -d -m 100m --name redis redis:alpine3.13
2
3CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4241060f564f7 docker.io/library/redis:alpine3.13 "docker-entrypoint.s…" 5 minutes ago Up redis
查看该容器的cgroup:
1ll /sys/fs/cgroup/memory/default/
2241060f564f7f696c0aa3a9e75a0403d2b8e1e7bfe3744bcabbe7a1b417d7c90
3......
可以看到/sys/fs/cgroup/memory/default/
下出现了一个与容器ID同名的文件夹,这个文件夹下有很多文件:
1ll /sys/fs/cgroup/memory/default/241060f564f7f696c0aa3a9e75a0403d2b8e1e7bfe3744bcabbe7a1b417d7c90/
2......
3
4cat memory.limit_in_bytes
5104857600
6
7cat tasks
826377
926462
1026463
1126464
1226465
文件memory.limit_in_bytes
中的内容是我们设置的100Mb的内存限制。文件task
中第一行是redis进程在主机上的进程id,下面几行是这个进程下的线程。可以使用下面的命令查看:
1ps -ef | grep 26377
2polkitd 26377 26347 0 18:41 ? 00:00:01 redis-server *:6379
3
4ps -T -p 26377
5PID SPID TTY TIME CMD
626377 26377 ? 00:00:01 redis-server
726377 26462 ? 00:00:00 bio_close_file
826377 26463 ? 00:00:00 bio_aof_fsync
926377 26464 ? 00:00:00 bio_lazy_free
1026377 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。