2.CGroup

概述

参考:

Control Groups(控制组,简称 CGroups) 是一个 Linux 内核特性,用于限制、隔离一组进程集合的资源使用,资源包括 CPU、内存、磁盘 IO、网络 等。CGroups 由 Google 的两位工程师开发,自 2008 年 1 月发布的 Linux 2.6.24 版本的内核中提供此能力。到目前为止,CGroups 分 v1 和 v2 两个版本,v1 实现较早,功能比较多,但是由于它里面的功能都是零零散散的实现的,所以规划的不是很好,导致了一些使用和维护上的不便,v2 的出现就是为了解决 v1 中这方面的问题,在最新的 4.5 内核中,cgroup v2 声称已经可以用于生产环境了,但它所支持的功能还很有限,随着 v2 一起引入内核的还有 cgroup namespace。v1 和 v2 可以混合使用,但是这样会更复杂,所以一般没人会这样用。

在 Linux 里,一直以来就有对进程进行分组的概念和需求,比如 session group, progress group 等,后来随着人们对这方面的需求越来越多,比如需要追踪一组进程的内存和 IO 使用情况等,于是出现了 cgroup,用来统一将进程进行分组,并在分组的基础上对进程进行监控和资源控制管理等。

Cgroup 是 Linux kernel 的一项功能:它是在一个系统中运行的层级制进程组,你可对其进行资源分配(如 CPU 时间、系统内存、网络带宽或者这些资源的组合)。通过使用 cgroup,系统管理员在分配、排序、拒绝、管理和监控系统资源等方面,可以进行精细化控制。硬件资源可以在应用程序和用户间智能分配,从而增加整体效率。

cgroup 和 namespace 类似,也是将进程进行分组,但它的目的和 namespace 不一样,namespace 是为了隔离进程组之间的资源,而 cgroup 是为了对一组进程进行统一的资源监控和限制。CGroup 还能对进程进行优先级设置、审计、以及将进程挂起和恢复等操作

术语

cgroup 在不同的上下文中代表不同的意思,可以指整个 Linux 的 cgroup 技术,也可以指一个具体进程组。

cgroup 是 Linux 下的一种将进程按组进行管理的机制,在用户层看来,cgroup 技术就是把系统中的所有进程组织成一颗一颗独立的树,每棵树都包含系统的所有进程,树的每个节点是一个进程组,而每颗树又和一个或者多个 subsystem 关联,树的作用是将进程分组,而 subsystem 的作用就是对这些组进行操作。

cgroup 主要包括下面两部分:

  • subsystem(子系统) # 一个 subsystem 就是一个内核模块,他被关联到一颗 cgroup 树之后,就会 在树的每个节点(进程组)上做具体的操作。subsystem 经常被称作 resource controller,因为它主要被用来调度或者限制每个进程组的资源,但是这个说法不完全准确,因为有时我们将进程分组只是为了做一些监控,观察一下他们的状态,比如 perf_event subsystem。到目前为止,Linux 支持 12 种 subsystem,比如限制 CPU 的使用时间,限制使用的内存,统计 CPU 的使用情况,冻结和恢复一组进程等,后续会对它们一一进行介绍。
  • hierarchy(层次结构) # 一个 hierarchy 可以理解为一棵 cgroup 树,树的每个节点就是一个进程组,每棵树都会与零到多个 subsystem 关联。在一颗树里面,会包含 Linux 系统中的所有进程,但每个进程只能属于一个节点(进程组)。系统中可以有很多颗 cgroup 树,每棵树都和不同的 subsystem 关联,一个进程可以属于多颗树,即一个进程可以属于多个进程组,只是这些进程组和不同的 subsystem 关联。目前 Linux 支持 12 种 subsystem,如果不考虑不与任何 subsystem 关联的情况(systemd 就属于这种情况),Linux 里面最多可以建 12 颗 cgroup 树,每棵树关联一个 subsystem,当然也可以只建一棵树,然后让这棵树关联所有的 subsystem。当一颗 cgroup 树不和任何 subsystem 关联的时候,意味着这棵树只是将进程进行分组,至于要在分组的基础上做些什么,将由应用程序自己决定,systemd 就是一个这样的例子。

CGroup 子系统类型

可以通过 /proc/cgroups 文件查看当前系统支持哪些 subsystem:

~]# cat /proc/cgroups
#subsys_name hierarchy num_cgroups enabled
cpuset 6 5 1
cpu 8 95 1
cpuacct 8 95 1
blkio 4 95 1
memory 12 236 1
devices 11 95 1
freezer 9 5 1
net_cls 10 5 1
perf_event 5 5 1
net_prio 10 5 1
hugetlb 2 5 1
pids 3 103 1
rdma 7 1 1

整理一下:

subsys_namehierarchynum_cgroupsenabled
cpuset651
cpu8951
cpuacct8951
blkio4951
memory122361
devices11951
freezer951
net_cls1051
perf_event551
net_prio1051
hugetlb251
pids31031
rdma711

从左到右,字段的含义分别是:

  • subsys_name # subsystem 的名字
    • blkio # 块设备 IO
    • cpu # 基于 CFS 对 CPU 时间配额进行限制的子系统,CFS 概念详见:CPU 管理 章节中的 CFS 调度器。该子系统是 cgroup 对进程使用 CPU 资源进行限制的主要手段
    • cpuacct # CPU 资源使用报告
    • cpuset # 多处理器平台上的 CPU 集合
    • devices # 设备访问
    • freezer # 挂载器或恢复任务
    • hungetlb #
    • memory # 内存用量及报告
    • net_cls # cgroup 中的任务创建的数据包的类别标识符
    • net_prio #
    • perf_event # 对 cgroup 中的任务进行统一性能测试
    • pids #
    • rdma #
  • hierarchy # subsystem 所关联到的 cgroup 树的 ID,如果多个 subsystem 关联到同一颗 cgroup 树,那么他们的这个字段将一样,比如这里的 cpu 和 cpuacct 就一样,表示他们绑定到了同一颗树。如果出现下面的情况,这个字段将为 0:
    • 当前 subsystem 没有和任何 cgroup 树绑定
    • 当前 subsystem 已经和 cgroup v2 的树绑定
    • 当前 subsystem 没有被内核开启
  • num_cgroups # subsystem 所关联的 cgroup 树中进程组的个数,也即树上节点的个数
  • enabled # 1 表示开启,0 表示没有被开启(可以通过设置内核的启动参数“cgroup_disable”来控制 subsystem 的开启).

CGroup 关联文件

/sys/fs/cgroup/ - CGroup 根目录

CGroup 的相关操作都是基于内核中的 CGroup Virtual Filesystem(控制组虚拟文件系统)。所以,使用 CGroup 首先需要挂载这个文件系统,通常,现代系统在启动时,都默认会挂载相关的 CGroup 文件系统:

  • CGroupV1 该目录下的每个目录都是 CGroup 子系统的名称。其中包含该子系统中所关联的进程的资源控制信息。
~]# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
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,cpu,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
...... 略
  • CGroupV2,则只会有一个 cgroup2 on /sys/fs/cgroup type cgroup2 (……) 的挂载项
~]# mount -t cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

这里面的 /sys/fs/cgroup/ 目录,就称为 CGroup 的根目录。CGroup 文件系统的 V1 与 V2 的根目录下的内容,各不相同,详见 CGroup FS 章节

/proc/PID/cgroup - 进程号为 PID 的进程所属的 cgroup 信息

/proc/PID/cgroup 文件中会指定进程所使用的 CGropu 的相对路径。文件中每行都是进程所属的 CGroup 子系统,每行子系统信息由以 : 分割的三个字段组成

  • hierarchy-ID # Hierarchy 唯一标识符。与 /proc/cgroups 文件中的 Hierarchy ID 相同。
    • CGroup v2 版本该字段始终为 0
  • controller-list # 绑定到 Hierarchy ID 的控制器列表。也就是 CGroup 的子系统。
    • CGroup v2 版本该字段为空
  • cgroup-path # 进程所属 CGroup 子系统的信息的路径。这是一个相对路径。
    • 这里面的 / 就是指 CGroup 的根节点中对应子系统的目录
      • 对于 CGroupV1 来说通常是 /sys/fs/cgroup/SUBSYSTEM。所以,一个完整的 cgroup-path 应该是 /sys/fs/cgroup/SUBSYSTEM/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope/*

cgroup 的 v1 和 v2 版本显示的信息不同

CGroupV1

~]# cat /proc/1185/cgroup
12:memory:/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
11:devices:/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
10:net_cls,net_prio:/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
9:freezer:/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
8:cpu,cpuacct:/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
7:rdma:/
6:cpuset:/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
5:perf_event:/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
4:blkio:/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
3:pids:/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
2:hugetlb:/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
1:name=systemd:/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
0::/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope

比如,1185 进程的 cpu 子系统的 CGroup 信息,就在 /sys/fs/cgroup/cpu/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope/ 目录中:

~]# ls /sys/fs/cgroup/cpu/system.slice/docker-b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460.scope
cgroup.clone_children  cpuacct.usage         cpuacct.usage_percpu_sys   cpuacct.usage_user  cpu.shares      cpu.uclamp.min
cgroup.procs           cpuacct.usage_all     cpuacct.usage_percpu_user  cpu.cfs_period_us   cpu.stat        notify_on_release
cpuacct.stat           cpuacct.usage_percpu  cpuacct.usage_sys          cpu.cfs_quota_us    cpu.uclamp.max  tasks

CGropuV2

~]# cat /proc/1277/cgroup
0::/system.slice/docker-020cfdfbd4cd43981570f4fa7def9a2b600025b2e60e3150e742a5049562f30f.scope

比如,1277 进程的 CGroup 信息,就在 /sys/fs/cgroup/system.slice/docker-020cfdfbd4cd43981570f4fa7def9a2b600025b2e60e3150e742a5049562f30f.scope/ 目录中:

~]# ls /sys/fs/cgroup/system.slice/docker-020cfdfbd4cd43981570f4fa7def9a2b600025b2e60e3150e742a5049562f30f.scope/
cgroup.controllers      cgroup.procs            cpu.max                cpuset.mems            cpu.weight       io.weight            memory.low        memory.stat   rdma.max
cgroup.events           cgroup.stat             cpu.pressure           cpuset.mems.effective  cpu.weight.nice  memory.current       memory.max        pids.current
cgroup.freeze           cgroup.subtree_control  cpuset.cpus            cpu.stat               io.max           memory.events        memory.min        pids.events
cgroup.max.depth        cgroup.threads          cpuset.cpus.effective  cpu.uclamp.max         io.pressure      memory.events.local  memory.oom.group  pids.max
cgroup.max.descendants  cgroup.type             cpuset.cpus.partition  cpu.uclamp.min         io.stat          memory.high          memory.pressure   rdma.current

其他文件

/proc/cgroups # 当前系统支持的所有 CGroup 子系统

systemd 的 slice 单元

在 Systemd 作为 1 号进程的系统中,进程的 CGroup 都可以配置为由 Systemd 管理,其中 Slice 类型的单元就是用来控制 CGroup 的。默认会创建 3 个顶级 Slice

  • system.slice # 所有 Service Unit 的默认。
  • user.lice # 所有用户进程的默认。
  • machine.slice # 所有虚拟机和容器的默认。

systemd-cgls 命令可以查看 CGroup 的层次结构

~]# systemd-cgls
Control group /:
-.slice
├─931 bpfilter_umh
├─user.slice
│ └─user-1000.slice
│   ├─user@1000.service …
│   │ └─init.scope
│   │   ├─81271 /lib/systemd/systemd --user
│   │   └─81276 (sd-pam)
│   ├─session-431.scope
│   │ ├─81902 sshd: desistdaydream [priv]
│   │ ├─81998 sshd: desistdaydream@pts/1
│   │ ├─82001 -bash
│   │ ├─82100 su - root
│   │ ├─82101 -bash
│   │ ├─82697 systemd-cgls
│   │ └─82698 pager
│   └─session-432.scope
│     ├─82013 sshd: desistdaydream [priv]
│     ├─82097 sshd: desistdaydream@notty
│     └─82098 /usr/lib/openssh/sftp-server
├─init.scope
│ └─1 /sbin/init nospectre_v2 nopti noibrs noibpb
└─system.slice
  ├─irqbalance.service
  │ └─524 /usr/sbin/irqbalance --foreground
  ├─uniagent.service
  │ └─537 /usr/local/uniagent/bin/uniagent
  ├─containerd.service …
  │ ├─  714 /usr/bin/containerd
  │ ├─ 1140 /usr/bin/containerd-shim-runc-v2 -namespace moby -id b8f92f970f0d17377e7ad4c9b75f8316cdb15a6dd7dd81466f415e6fcaed6460 -address /run/containerd/containerd.sock
  │ ├─31778 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 4c5ec4bc9717bb9fd2a2ea7b507ac3c0e16da95fa87974152f0fe3b3a653cef9 -address /run/containerd/containerd.sock
......

systemd-cgtop 命令可以查看 CGroup 的动态信息。

Control Group                                                                       Tasks   %CPU   Memory  Input/s Output/s
/                                                                                     221    1.0     3.1G        -        -
user.slice                                                                             11    0.7     1.5G        -        -
system.slice                                                                          139    0.4     1.2G        -        -
system.slice/containerd.service                                                        46    0.2   276.0M        -        -
system.slice/cloudResetPwdUpdateAgent.service                                          18    0.2   102.4M        -        -
system.slice/docker-4c5ec4…d2a2ea7b507ac3c0e16da95fa87974152f0fe3b3a653cef9.scope       1    0.1     1.3M        -        -
system.slice/multipathd.service                                                         7    0.0    13.8M        -        -
init.scope                                                                              1      -     7.6M        -        -
system.slice/ModemManager.service                                                       3      -     6.8M        -        -
system.slice/NetworkManager.service                                                     3      -    13.6M        -        -
system.slice/accounts-daemon.service                                                    3      -     6.5M        -        -

CGroupV2

检查 cgroup v2 是否已启用

如果 /sys/fs/cgroup/cgroup.controllers 存在于系统上,则使用的是 v2,否则使用的是 v1。

已知以下发行版默认使用 cgroup v2:

  • Fedora(31 起)
  • Arch Linux(自 2021 年 4 月起)
  • openSUSE Tumbleweed(自 2021 年起)
  • Debian GNU/Linux(从 11 开始)
  • Ubuntu(自 21.10 起)

启用 cgroup v2

为容器启用 cgroup v2 需要内核 4.15 或更高版本。建议使用内核 5.2 或更高版本。

然而,将 cgroup v2 控制器委派给非 root 用户需要最新版本的 systemd。建议使用 systemd 244 或更高版本。

要使用 cgroup v2 引导主机,请将以下字符串添加到 GRUB_CMDLINE_LINUX 行/etc/default/grub,然后运行 sudo update-grub.

systemd.unified_cgroup_hierarchy=1

启用 CPU、CPUSET 和 I/O 委派

默认情况下,非 root 用户只能获取 memory 控制器和 pids 要委托的控制器。

$ cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers
memory pids

要允许委派其他控制器,例如 cpu、cpuset 和 io,请运行以下命令:

$ sudo mkdir -p /etc/systemd/system/user@.service.d
$ cat <<EOF | sudo tee /etc/systemd/system/user@.service.d/delegate.conf
[Service]
Delegate=cpu cpuset io memory pids
EOF
$ sudo systemctl daemon-reload

建议使用委派 cpuset 以及 cpu. 委派 cpuset 需要 systemd 244 或更高版本。

更改 systemd 配置后,您需要重新登录或重新启动主机。建议重启主机。