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
nodev	cgroup
nodev	cgroup2

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

  1. 挂载cgroup v2

对于使用 systemd 引导的系统,可以在引导文件 /etc/default/grubGRUB_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 /sys/fs/cgroup
cgroup.controllers      cpuset.mems.effective   memory.idle_page_stats.local
cgroup.max.depth        cpu.stat                memory.numa_stat
cgroup.max.descendants  init.scope              memory.pressure
cgroup.procs            io.cost.model           memory.reap_background
cgroup.stat             io.cost.qos             memory.stat
cgroup.subtree_control  io.pressure             memory.use_priority_oom
cgroup.threads          io.stat                 memory.use_priority_swap
cpu.pressure            memory.fast_copy_mm     system.slice
cpuset.cpus.effective   memory.idle_page_stats  user.slice

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

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

$ cat cgroup.controllers
cpuset cpu io memory hugetlb pids rdma

查看cgroup2开启的控制器

$ cat cgroup.subtree_control
cpuset cpu io memory pids

在root cgroup下创建一个cgroup

$ mkdir cgrp2test
$ ls cgrp2test
cgroup.controllers      cpu.weight.nice               memory.pressure
cgroup.events           io.bfq.weight                 memory.priority
cgroup.freeze           io.latency                    memory.reap_background
cgroup.max.depth        io.max                        memory.stat
cgroup.max.descendants  io.pressure                   memory.swap.current
cgroup.procs            io.stat                       memory.swap.events
cgroup.stat             io.weight                     memory.swap.high
cgroup.subtree_control  memory.current                memory.swap.max
cgroup.threads          memory.events                 memory.use_priority_oom
cgroup.type             memory.events.local           memory.use_priority_swap
cpu.max                 memory.fast_copy_mm           memory.wmark_high
cpu.pressure            memory.high                   memory.wmark_low
cpuset.cpus             memory.idle_page_stats        memory.wmark_ratio
cpuset.cpus.effective   memory.idle_page_stats.local  memory.wmark_scale_factor
cpuset.cpus.partition   memory.low                    pids.current
cpuset.mems             memory.max                    pids.events
cpuset.mems.effective   memory.min                    pids.max
cpu.stat                memory.numa_stat
cpu.weight              memory.oom.group

查看cgrp2test可用的控制器

$ cat cgroup.controllers 
cpuset cpu io memory pids 

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

对cgroup添加cpu资源限制

$ echo 5000 10000 > cpu.max

含义是在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 cgroup.subtree_control

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

$ mkdir -p cgrp2test/cg1
$ ls cgrp2test/cg1 #没有控制器接口可用,cg1对资源自由竞争
cgroup.controllers  cgroup.max.descendants  cgroup.threads  io.pressure
cgroup.events       cgroup.procs            cgroup.type     memory.pressure
cgroup.freeze       cgroup.stat             cpu.pressure
cgroup.max.depth    cgroup.subtree_control  cpu.stat

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

$ echo "+cpu +memory" > cgrp2test/cgroup.subtree_control
$ ls cgrp2test/cg1 #出现了控制器的接口
cgroup.controllers      cpu.weight.nice               memory.pressure
cgroup.events           io.pressure                   memory.priority
cgroup.freeze           memory.current                memory.reap_background
cgroup.max.depth        memory.events                 memory.stat
cgroup.max.descendants  memory.events.local           memory.swap.current
cgroup.procs            memory.fast_copy_mm           memory.swap.events
cgroup.stat             memory.high                   memory.swap.high
cgroup.subtree_control  memory.idle_page_stats        memory.swap.max
cgroup.threads          memory.idle_page_stats.local  memory.use_priority_oom
cgroup.type             memory.low                    memory.use_priority_swap
cpu.max                 memory.max                    memory.wmark_high
cpu.pressure            memory.min                    memory.wmark_low
cpu.stat                memory.numa_st

以下图为例:

 A(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 > cgroup.type
-bash: echo: 写错误: 无效的参数

同样的,如果想让一个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.

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐