cgroup v2使用与测试

1. 配置cgroup v2的环境

  1. 判断内核使用的cgroup版本
$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
tmpfs on /usr/local/aegis/cgroup type tmpfs (rw,relatime,size=51200k)
cgroup on /usr/local/aegis/cgroup/cpu type cgroup (rw,relatime,cpu)

如果输出只有cgroup,说明内核还未挂载cgroup v2或者内核不支持cgroup v2

  1. 判断内核是否支持cgroup v2
$ cat /proc/filesystems | grep

出现了cgroup v2说明内核是支持的,可以继续接下来的操作

  1. 挂载cgroup v2

对于使用 systemd 引导的系统,可以在引导文件 ​​/etc/default/grub​​​ 的​​GRUB_CMDLINE_LINUX_DEFAULT​​中添加如下一行,启用 v2 版本

“systemd.unified_cgroup_hierarchy=yes”

$ sudo vim /etc/default/grub
添加:GRUB_CMDLINE_LINUX_DEFAULT="systemd.unified_cgroup_hierarchy=yes"
$ sudo grub-mkconfig -o /boot/grub/grub.cfg
$ reboot

可以使用​​cgroup_no_v1 = allows​​防止cgroup v1抢占所有controller,体验纯cgroup v2环境

最后再进行重启,就可以使用cgroup v2了

2. 初探cgroup v2

查看cgroup2的目录树结构

$ ls

相比于cgroup v1,v2的目录则显得直接很多,毕竟如果将cgroup v1比作森林的话,cgroup v2就只是一颗参天大树

查看cgroup2可管理的系统资源类型

$ cat

查看cgroup2开启的控制器

$ cat

在root cgroup下创建一个cgroup

$ mkdir cgrp2test
$ ls

查看cgrp2test可用的控制器

$ cat

每个child cgroup不会继承其父母的控制器,只有在父节点​​cgroup.subtree_control​​显式配置开启的控制器才能在child cgroup中使用。目前cgrp2test可以对cpuset, cpu, io, memroy, pids这些资源进行限制

对cgroup添加cpu资源限制

$ echo 5000 10000 >

含义是在10000的CPU时间周期内,有5000是分配给本cgroup的,也就是本cgroup管理的进程在单核CPU上的使用率不会超过50%

测试一下

$ vim while.sh
while :
do
:
done

$ ./while.sh &
[1] 4139

$ top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4139 root 20 0 226356 3100 1372 R 99.7 0.0 0:36.51 bash
4143 root 20 0 227880 5636 3928 R 0.3 0.0 0:00.07 top
1 root 20 0 168812 11240 8392 S 0.0 0.0 0:02.03 systemd
...

$ cd /sys/fs/cgroup
$ mkdir cputest
$ echo 5000 10000 > cputest/cpu.max
$ echo 4139 > cputest/cgroup.procs

$ top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4139 root 20 0 223708 2888 1328 R 50.0 0.0 25:27.62 bash
3288 root 10 -10 119312 28720 16716 S 0.7 0.0 0:11.19 AliYunD+
4149 root 20 0 227880 5612 3904 R 0.3 0.0 0:00.04 top
1 root 20 0 168700 11208 8440 S 0.0 0.0 0:01.88 systemd

为了对直系child cgroup进行资源限制,cgrp2test也需要开启特定的控制器,查看cgrp2test已经开启的控制器

$ cat

此时cgrp2test没有开启任何控制器,那么cgrp2test对其直系child cgroup将无法进行资源分配的限制。

$ mkdir -p cgrp2test/cg1
$ ls cgrp2test/cg1 #没有控制器接口可用,cg1对资源自由竞争

我们现在开启cpu, memory控制器

$ echo "+cpu +memory" > cgrp2test/cgroup.subtree_control
$ ls cgrp2test/cg1 #出现了控制器的接口

以下图为例:

(cpu,memory) - B(memory) - C()
\ D()

A开启了cpu和memory,那么A可以控制B的CPU周期和内存的分配。B开启了memory,但没有开启cpu controller,那么C和D可以CPU资源进行自由竞争,但是他们对B可用内存的划分则是可控制的。

3. 详解cgroup v2

top-down constraint

资源是自顶向下(top-down)分配的,只有当一个 cgroup 从 parent 获得了某种资源,它才可以继续向下分发。这意味着

  • 只有父节点启用了某个控制器,子节点才能启用;
  • 对应到实现上,所有非根节点(non-root)的​​cgroup.subtree_control​​​ 文件中, 只能包含它的父节点的​​cgroup.subtree_control​​ 中有的控制器;
  • 另一方面,只要有子节点还在使用某个控制器,父节点就无法禁用之。

no internal process

只有当一个 non-root cgroup 中没有任何进程时,才能将其 domain resource 分配给它的 children。换句话说,只有那些没有任何进程的 domain cgroup, 才能将它们的 domain controllers写到 ​​cgroup.subtree_control​​ 文件中。

这种方式保证了在一个启动的 domain controller的视野范围内,所有进程都位于叶子节点上, 因而避免了 child cgroup 内的进程与 parent 内的进程竞争的情况,便于 domain controller 扫描 hierarchy。

但root cgroup 不受此限制。

  • 对大部分类型的控制器来说,root 中包含了一些没有与任何 cgroup 相关联的进程和匿名资源占用 (anonymous resource consumption),需要特殊对待。
  • root cgroup 的资源占用是如何管理的,因控制器而异。

注意,cgroup.subtree_control 启用某个控制器之前,no internal process限制不会生效。 这非常重要,因为它决定了创建 populated cgroup children 的方式。 要控制一个 cgroup 的资源分配,这个 cgroup 需要创建 children cgroup然后在​​cgroup.subtree_control​​启动控制器之前,将自己所有的进程转移到 children cgroup 中

测试一下:

$ cd /sys/fs/cgroup
$ mkdir -p test test/cg1 test/cg2 test/cg1/cg1_1 test/cg2/cg2_1
# test __ cg1 -- cg1_1
# \_ cg2 -- cg2_1
$ echo "+memroy" > test/cgroup.subtree_control #开启test的memory控制器
$ echo "+memory" > test/cg1/cgroup.subtree_control #开启cg1的memory控制器
# test(*) __ cg1(*) -- cg1_1
# \_ cg2 -- cg2_1
# 打*号表示开启了控制器
$ sleep 10000 &
[1] 6768

$ echo 6768 > test/cg1/cg1_1/cgroup.procs #成功
$ echo 6768 > test/cg1/cgroup.procs #-bash: echo: 写错误: 设备或资源忙
$ echo 6768 > test/cgroup.procs #-bash: echo: 写错误: 设备或资源忙
$ echo 6768 > test/cg2/cg2_1/cgroup.procs #成功
$ echo 6768 > test/cg2/cgroup.procs #成功
$ echo 6768 > cgroup.procs #成功

资源分配模型

1. Weights

这种模型的一个例子是 ​​cpu.weight​​,负责在 active children 之间按比例分配 CPU cycle 资源。

这种模型中,parent 会根据所有 active children 的权重来计算它们各自的占比(ratio)。

  • 由于只有那些能使用这些资源的 children 会参与到资源分配,因此这种模型能实现资源的充分利用(work-conserving)。
  • 这种分配模型本质上是动态的(the dynamic nature), 因此常用于无状态资源
  • 权重值范围是 ​[1, 10000]​​,默认​​100​​。这使得能以 足够细的粒度增大或缩小权重

2. Limits

这种模型的一个例子是 ​​io.max​​,负责在 IO device 上限制 cgroup 的最大 BPS 或 IOPS。

  • 这种模型给 child 配置的资源使用量上限(limit)。
  • 资源是可以超分的(over-committed),即所有 children 的份额加起来可以大于 parent 的总可用量。
  • Limits 值范围是​​[0, max]​​​,默认​​max​​,也就是没做限制。
  • 由于 limits 是可以超分的,因此所有配置组合都是合法的。

3. Protections

这种模型的一个例子是 ​​memory.low​​,实现了 best-effort 内存保护

  • 在这种模型中,只要一个 cgroup 的所有祖先都处于各自的 protected level 以下,那么这个 cgroup 拿到的资源量就能达到配置值(有保障)。这里的保障可以是
  • hard guarantees
  • best effort soft boundaries
  • Protection 可以超分,在这种情况下,only upto the amount available to the parent is protected among children.
  • Protection 值范围是​​[0, max]​​​,默认是​​0​​,也就是没有特别限制。
  • 由于 protections 是可以超分的,因此所有配置组合都是合法的。

4. Allocations

这种模型的一个例子是 ​​cpu.rt.max​​,它 hard-allocates realtime slices。

  • 这种模型中,cgroup 会排他性地分配(exclusively allocated)资源量。
  • Allocation不可超分,即所有 children 的 allocations 之和不能超过 parent 的可用资源量。
  • Allocation 值范围是​​[0, max]​​​,默认是​​0​​,也就是不会排他性地分配资源。
  • 由于 allocation 不可超分,因此某些配置可能不合法,会被拒绝;如果强制迁移进程,可能会因配置不合法(资源达到上限)而失败。

核心接口文件

所有cgroup中的核心文件都以​​cgroup.​​开头

  1. cgroup.type
    可读写文件,只存在于非root cgroup中,有以下几个类型:
  • “domain": 正常的domain cgroup,默认类型
  • “domain threaded”:threaded domain cgroup,作为 threaded subtree 的 root,该状态仍然属于进程级别的管理,但是其child cgroup则既不是domain类型也不是domain threaded类型,而是threaded类型,进入线程粒度的管理
  • “threaded”:表示当前cgroup是某个threaded subtree的一个member
  • “domain invalid”:无效状态,此状态下无法被populate或者启用控制器

一个操作进程的cgroup被称为一个domain,是每个新创建的cgroup的默认类型。通过​​echo threaded > cgroup.type​​可以将默认的domain类型转为threaded类型,在一个cgroup转为threaded时会自动将其parent cgroup转换为domain threaded类型,且一旦转为threaded类型,该cgroup就再也不能再转为domain类型。

切换到threaded,需要满足以下条件:

  • 由于该cgroup需要加入父母的资源域,所以其父母必须是有效的,包括valid (threaded) domain或者threaded cgroup
  • 当其父母为unthreaded domain时,不能有enabled domain controllers以及populated domain children,root cgroup除外,root cgroup它既可以按照domain类型工作,也可以按照domain threaded类型工作,所以其child cgroup类型既可以为domain也可以为threaded

可在线程粒度管理资源的控制器称之为threaded controller [如cpu, perf_event, pids],只能在进程粒度管理资源的控制器称为domain controller,一个threaded subtree下只允许使用threaded controllers

关于domain invalid,以下面场景为例:

A(domain threaded)--> B(threaded) --> C(domain invalid, new created)
|--> C(domain invalid)
|--> D(domain invalid, new created)

当一个domain类型转为threaded,其所有兄弟节点和子节点都只能为threaded类型,所以其所有的unthreaded cgroup兄弟节点都变为domain invalid状态,新建的节点由于默认domain类型,也将变为invalid,可以通过​​echo threaded > cgroup.type​​新建节点变得可用

将一个cgroup类型设置为domain threaded的唯二方式,第一就是使其一个child cgroup变为threaded类型;第二就是在拥有进程时,cgroup.subtree_control中启动了threaded controller。

如果直接尝试改变为domain threaded,会报错

$ echo domain threaded >

同样的,如果想让一个domain threaded类型的cgroup变回domain类型,需要删除其threaded类型的child cgroup,直接尝试改变其状态也会报错。

由于一个threaded subtree不受no inter process限制,一个threaded controller必须有能力处理non-leaf cgroup中的线程与其child cgroup中的线程资源竞争问题。

  1. cgroup.procs

可读写文件,每行一个 PID,可用于所有 cgroups。

读时,返回这个 cgroup 内的所有进程 ID,每行一个。PID 列表没有排序,同一个 PID 可能会出现多次 —— 如果该进程先移除再移入该 cgroup,或 PID 循环利用了, 都可以回出现这种情况。

要将一个进程移动到该 cgroup,只需将 PID 写入这个文件。写入时必须满足:

  1. 必须有对改 cgroup 的 cgroup.procs 文件写权限。
  2. 必须对 source and destination cgroups 的共同祖先的 cgroup.procs 文件有写权限。
  3. cgroup.threads

A read-write new-line separated values file which exists on all cgroups.

When read, it lists the TIDs of all threads which belong to the cgroup one-per-line. The TIDs are not ordered and the same TID may show up more than once if the thread got moved to another cgroup and then back or the TID got recycled while reading.

A TID can be written to migrate the thread associated with the TID to the cgroup. The writer should match all of the following conditions.

  • It must have write access to the “cgroup.threads” file.
  • The cgroup that the thread is currently in must be in the same resource domain as the destination cgroup.
  • It must have write access to the “cgroup.procs” file of the common ancestor of the source and destination cgroups.

When delegating a sub-hierarchy, write access to this file should be granted along with the containing directory.

  1. cgroup.controllers

只读(read-only)文件,内容是空格隔开的值,可用于所有 cgroups。

读取这个文件,得到的是该 cgroup 的所有可用控制器,空格隔开。控制器列表未排序。

  1. cgroup.subtree_control

可读写,空格隔开的值,可用于所有控制器,初始时是空的。

读取时,返回这个 cgroup 已经启用的控制器,对其 children 做资源控制。

可通过 ​​+<controller>​​​ 和 ​​-<controller>​​ 来启用或禁用控制器。如果一个控制器在文件中出现多次,最后一次有效。 如果一次操作中指定了启用或禁用多个动作,那要么全部成功,要么全部失败。

  1. cgroup.events

只读,flat-keyed file,只可用于 non-root cgroups。

定义了下面两个配置项:

  • populated:1 if the cgroup or its descendants contains any live processes; otherwise, 0.
  • frozen:1 if the cgroup is frozen; otherwise, 0.

除非有特别设置,否则修改本文件会触发一次 file modified event.

  1. cgroup.max.descendants

可读写 single value files,默认值 ​​"max"​​。

允许的最大 descent cgroups 数量。如果实际的 descendants 数量等于或大于该值,在 hierarchy 中再创建新 cgroup 时会失败。

  1. cgroup.max.depth

可读写 single value files,默认值 ​​"max"​​。

当前 cgroup 内允许的最大 descent depth。如果实际的 depth 数量等于或大于该值,再创建新 child cgroup 时会失败。

  1. cgroup.stat

只读 flat-keyed file,定义了下列 entries:

  • nr_descendants:可见的 descendant cgroups 总数。
  • nr_dying_descendants
    Total number of dying descendant cgroups. A cgroup becomes dying after being deleted by a user. The cgroup will remain in dying state for some time undefined time (which can depend on system load) before being completely destroyed.
    A process can’t enter a dying cgroup under any circumstances, a dying cgroup can’t revive.
    A dying cgroup can consume system resources not exceeding limits, which were active at the moment of cgroup deletion
  1. cgroup.freeze

可读写 single value file,只能用于 non-root cgroups。 Allowed values are “0” and “1”. The default is “0”.

Writing “1” to the file causes freezing of the cgroup and all descendant cgroups. This means that all belonging processes will be stopped and will not run until the cgroup will be explicitly unfrozen. Freezing of the cgroup may take some time; when this action is completed, the “frozen” value in the cgroup.events control file will be updated to “1” and the corresponding notification will be issued.

A cgroup can be frozen either by its own settings, or by settings of any ancestor cgroups. If any of ancestor cgroups is frozen, the cgroup will remain frozen.

Processes in the frozen cgroup can be killed by a fatal signal. They also can enter and leave a frozen cgroup: either by an explicit move by a user, or if freezing of the cgroup races with fork(). If a process is moved to a frozen cgroup, it stops. If a process is moved out of a frozen cgroup, it becomes running.

Frozen status of a cgroup doesn’t affect any cgroup tree operations: it’s possible to delete a frozen (and empty) cgroup, as well as create new sub-cgroups.