本节我们一起来了解一下容器的资源限制技术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_uscpu.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_us10000us,为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选择使用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。

参考