通过 cgroup 对 Linux 进程的 CPU、内存等核心资源进行精细化配额管控,从根源避免业务进程资源抢占导致的系统雪崩。
cgroup 概述
cgroup(Control Group)是 Linux 内核提供的一种资源管理机制,它将进程组织成层次结构,并对每个层次结构中的进程分配资源限制。通过 cgroup,可以实现资源限制、调整资源访问优先级、监控和报告资源使用情况、批量控制所有进程的状态等功能。
cgroup 通过层级结构组织在一起,然后对进程进行资源控制,形成多对多的关系,如下图所示。

cgroup 子系统(最上层):针对每种可以控制的资源,如 cpu、memory、devices 子系统等。每个 cgroup 子系统只能 attach 到一个层级结构:cgroup 子系统针对特定资源进行控制和监视,如果被 attach 到多个层级结构,会导致资源分配和限制的冲突,从而影响系统的稳定性和可靠性。
cgroup 层级结构(中间层):可以 attach 一个或者几个 cgroup 子系统,并对其 attach 的 cgroup 子系统进行资源的限制。
进程与 css_set(最底层):每一个进程指向一个 css_set(cgroups subsystem set),且只能隶属于一个 css_set;一个 css_set 可以包含多个进程,隶属于同一 css_set 的进程受到该 css_set 所关联的资源限制。
环境默认设置
cgroup 版本
默认提供 cgroup 能力。您可以通过如下命令查询是否支持:
检查内核是否启用 cgroup 基础框架
grep CONFIG_CGROUPS=y /boot/config-$(uname -r)
返回结果如下:
CONFIG_CGROUPS=y
检查内核是否启用 cgroup 调度器支持。
grep CONFIG_CGROUP_SCHED=y /boot/config-$(uname -r)
返回结果如下:
CONFIG_CGROUP_SCHED=y
TencentOS Server 4 默认使用 cgroup v2,TencentOS Server 3 默认使用 cgroup v1。
您可通过以下命令查询目前 cgroup 版本:
stat -fc %T /sys/fs/cgroup/
不同 cgroup 版本的返回结果如下:
cgroup2fs # 当前使用 cgroup v2tmpfs # 当前使用 cgroup v1
cgroup v2 的目录结构如下:
ls /sys/fs/cgroup/
返回结果如下:
cgroup.controllers cgroup.threads init.scope memory.numa_stat sys-kernel-debug.mountcgroup.max.depth cpu.pressure io.cost.model memory.pressure sys-kernel-tracing.mountcgroup.max.descendants cpuset.cpus.effective io.cost.qos memory.reclaim system.slicecgroup.pressure cpuset.mems.effective io.pressure memory.stat user.slicecgroup.procs cpu.stat io.prio.class misc.capacitycgroup.stat dev-hugepages.mount io.stat sys-fs-fuse-connections.mountcgroup.subtree_control dev-mqueue.mount irq.pressure sys-kernel-config.mount
cgroup v1 的目录结构如下:
ls /sys/fs/cgroup/
返回结果如下:
blkio cpuacct cpuset freezer memory net_cls net_prio pids systemdcpu cpu,cpuacct devices hugetlb misc net_cls,net_prio perf_event rdma unified
警告:切换 cgroup 版本需要修改内核启动参数并重启系统,属于高风险操作。切换前请确认业务对 cgroup 版本的兼容性,并做好回退预案。
cgroup 子系统
查询
/proc/cgroups 确认当前 cgroup 支持的子系统:cat /proc/cgroups
返回结果如下,目前默认支持的子系统如下所示:
#subsys_name hierarchy num_cgroups enabledcpuset 0 132 1cpu 0 132 1cpuacct 0 132 1blkio 0 132 1memory 0 132 1devices 0 132 1freezer 0 132 1net_cls 0 132 1perf_event 0 132 1net_prio 0 132 1hugetlb 0 132 1pids 0 132 1rdma 0 132 1misc 0 132 1
查询
/sys/fs/cgroup/cgroup.controllers 确认当前 cgroup 层次结构中已启用的子系统:cat /sys/fs/cgroup/cgroup.controllers
返回结果如下:
cpuset cpu io memory hugetlb pids rdma misc
通过
mount 命令向指定挂载点添加系统支持且未启用的子系统:mkdir -p /mnt/cgroups/freezermount -t cgroup -o freezer freezer /mnt/cgroups/freezerls /mnt/cgroups/freezer/
返回结果如下:
cgroup.clone_children cgroup.procs cgroup.sane_behavior notify_on_release release_agent tasks
通过
lscgroup 确认子系统已添加:lscgroup | grep freezer
返回结果如下:
freezer:/
如果不再需要可以通过
umount 命令卸载:umount /mnt/cgroups/freezer
cgroup 使用说明
使用 systemd 操作 cgroup
systemd 通过将 cgroup 层级结构与 systemd unit 树绑定,将资源管理简化到应用程序级别。用户可以直接通过 systemctl 指令或修改 systemd unit 的配置文件,来管理 unit 相关的资源。
在资源管控方面,systemd 提供了三种 unit 类型:
service:一个或一组进程,由 systemd 依据 unit 配置文件启动。service 对指定进程进行封装,使进程可以作为一个整体被启动或终止。
scope:一组外部创建的进程。这些进程通过 fork() 函数启动和终止,之后由 systemd 在运行时注册并封装。例如,用户会话、容器和虚拟机通常被视为 scope。
slice:一组按层级排列的 unit。slice 本身不包含进程,而是组建一个层级结构,将 scope 和 service 放置其中。真正的进程包含在 scope 或 service 中。在这一层级树中,每一个 slice 单元的名称对应通向层级中某个位置的路径。
查看 cgroup 的层级结构
通过
systemd-cgls 命令可以查看 cgroup 的层级结构:systemd-cgls
返回结果如下:
Control group /:-.slice├─user.slice (#1203)│ → user.invocation_id: f7d3942d34cd4635a9a1567f35cf247a│ → trusted.invocation_id: f7d3942d34cd4635a9a1567f35cf247a│ └─user-0.slice (#41553)│ → user.invocation_id: d1ff03802e014648a616ac2bdafbb73a...├─init.scope (#25)│ └─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 31└─system.slice (#65)├─rngd.service (#2701)│ → user.invocation_id: eb2c44177aec4afc8095560dc6269402│ → trusted.invocation_id: eb2c44177aec4afc8095560dc6269402│ └─674 /usr/sbin/rngd -f -x pkcs11 -x nist...
启动 service
通过
systemd-run 命令可以创建、启动临时 service 或 scope 单位,并在此单位中运行自定义指令,格式如下:systemd-run --unit=name --scope --slice=<slice_name> command
如果未指定
slice 则默认属于 system.slice。设置 service 属性
服务启动后,就可以使用
systemctl set-property 来修改 cgroup:systemctl set-property name parameter=value
与 cgroup v1 相比,v2 的 property 有许多调整,可通过如下命令来查询当前支持的 property 具体类型:
man 5 systemd.resource-control
libcgroup 工具
libcgroup 是当前版本管理 cgroup 的主要工具之一,您可以通过如下命令安装,tools 子包会把主包也依赖进来安装到环境上:
dnf install -y libcgroup-tools
显示 cgroup 层次结构
libcgroup 提供了
lscgroup 来显示 cgroup 层次结构,可以直接列出当前全量的已启用 cgroup 子系统信息:lscgroup
返回结果如下:
cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/sys-fs-fuse-connections.mountcpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/sys-kernel-config.mountcpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/sys-kernel-debug.mountcpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/dev-mqueue.mount...
也可以指定一个子系统以及其路径:
lscgroup -g cpuset:/sys-fs-fuse-connections.mount
返回结果如下:
cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/sys-fs-fuse-connections.mount/
创建/删除控制群组
libcgroup 提供了
cgcreate/cgdelete 这一对命令用来创建/删除控制群组。使用前,需要先挂载子系统,如:
mkdir -p /mnt/cgroups/freezermount -t cgroup -o freezer freezer /mnt/cgroups/freezer
使用
cgcreate 创建控制群组:cgcreate -g freezer:/cgcreate_child
完成后,可以通过
lscgroup 命令确认:lscgroup | grep freezer
返回结果如下:
freezer:/freezer:/freezerfreezer:/cgcreate_child
此时,
/mnt/cgroups/freezer 目录下就多了 cgcreate_child 子目录,下面有 freezer 相关的文件:ls /mnt/cgroups/freezer/cgcreate_child/
返回结果如下:
cgroup.clone_children freezer.parent_freezing freezer.state taskscgroup.procs freezer.self_freezing notify_on_release
另外,也可以直接通过
mkdir 的方式替代 cgcreate:mkdir /mnt/cgroups/freezer/childgroup
通过
lscgroup 确认创建结果:lscgroup | grep freezer
返回结果如下:
freezer:/freezer:/freezerfreezer:/childgroupfreezer:/cgcreate_child
查看新创建的子目录内容:
ls /mnt/cgroups/freezer/childgroup
返回结果如下:
cgroup.clone_children freezer.parent_freezing freezer.state taskscgroup.procs freezer.self_freezing notify_on_release
如果不再需要,可以通过
cgdelete 命令删除子群组:cgdelete freezer:/cgcreate_childcgdelete freezer:/childgroup
确认删除结果:
lscgroup | grep freezer
返回结果如下:
freezer:/freezer:/freezer
添加进程到控制群组
可以通过运行
cgexec 指令在手动创建的 cgroup 中启动进程。cgexec 的语法为:cgexec -g controllers:path_to_cgroup command arguments
如果进程已启动,可以运行
cgclassify 指令将进程移动到 cgroup 中:cgclassify -g controllers:path_to_cgroup pidlist
以将进程 15729 移动到
childgroup 中示例:cgclassify -g freezer:/childgroup 15729
查看
childgroup 中的进程列表:cat /mnt/cgroups/freezer/childgroup/tasks
返回结果如下:
15729
参考示例
使用 systemd-run 将服务加入 cgroup
这里通过 cgroup 的 memory 系统来举例说明。
警告:以下示例会构造内存泄漏场景并触发 OOM,仅用于演示 cgroup 内存限制的效果,请勿在生产环境执行。
1.
编辑 demo
如下,此处构造一个内存缓慢泄漏的场景,正常情况下,这个进程运行很久才会由于 OOM 被 kill 掉:#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#define SIZE 1024 * 10 // 10kBint main() {char *ptr;while (1) {ptr = (char *) malloc(SIZE);memset(ptr, 0, SIZE);sleep (1);}return 0;}
2. 通过
systemd-run 命令拉起服务:systemd-run --unit=my_malloc --slice=test ./my_malloc
服务拉起后可以通过
systemd-cgls 命令查到服务:systemd-cgls
返回结果如下:
Control group /:-.slice...├─test.slice (#5581)│ → user.invocation_id: 41e6db3f95094ae99a7f4d7ce471d3a1│ → trusted.invocation_id: 41e6db3f95094ae99a7f4d7ce471d3a1│ └─my_malloc.service (#5637)│ → user.invocation_id: 02d4d4b8d38b4101b5f6ef5af2addfb3│ → trusted.invocation_id: 02d4d4b8d38b4101b5f6ef5af2addfb3│ └─2124 /home/./my_malloc
如果未指定
slice 则默认属于 system.slice:systemd-run --unit=my_malloc ./my_malloc
查看 cgroup 层级结构:
systemd-cgls
返回结果如下:
Control group /:-.slice├─user.slice (#1123)│ → user.invocation_id: 17274220636d448c8b14dca5d4ce1d45│ → trusted.invocation_id: 17274220636d448c8b14dca5d4ce1d45...└─system.slice (#65)...├─my_malloc.service (#4501)│ → user.invocation_id: 7d25225645844d228f300b1f6dc21905│ → trusted.invocation_id: 7d25225645844d228f300b1f6dc21905│ └─2146 /home/./my_malloc
3. 服务拉起后,cgroup 根目录下可以查到 my_malloc 这个服务的信息:
ls /sys/fs/cgroup/test.slice/my_malloc.service
返回结果如下:
cgroup.controllers cgroup.threads memory.low memory.swap.eventscgroup.events cgroup.type memory.max memory.swap.highcgroup.freeze cpu.pressure memory.min memory.swap.maxcgroup.kill cpu.stat memory.numa_stat memory.zswap.currentcgroup.max.depth io.pressure memory.oom.group memory.zswap.maxcgroup.max.descendants irq.pressure memory.peak pids.currentcgroup.pressure memory.current memory.pressure pids.eventscgroup.procs memory.events memory.reclaim pids.maxcgroup.stat memory.events.local memory.stat pids.peakcgroup.subtree_control memory.high memory.swap.current
4.
memory.max 默认是 max,我们通过 systemctl 设置其为 10k。先查看当前值:cat memory.max
返回结果如下:
max
设置内存限制为 10K:
systemctl set-property my_malloc.service MemoryMax=10K
5. 可以发现服务由于 oom 提前退出:
systemctl status my_malloc
返回结果如下:
× my_malloc.service - /home/./my_mallocLoaded: loaded (/run/systemd/transient/my_malloc.service; transient)Transient: yesDrop-In: /run/systemd/transient/my_malloc.service.d└─50-MemoryMax.confActive: failed (Result: oom-kill) since Tue 2023-05-23 17:20:46 CST; 12s agoDuration: 6min 15.691sProcess: 2124 ExecStart=/home/./my_malloc (code=killed, signal=KILL)Main PID: 2124 (code=killed, signal=KILLCPU: 16msMay 23 17:14:30 controller systemd[1]: Started my_malloc.service - /home/./my_malloc.May 23 17:20:46 controller systemd[1]: my_malloc.service: A process of this unit has been killed by the OO>May 23 17:20:46 controller systemd[1]: my_malloc.service: Main process exited, code=killed, status=9/KILLMay 23 17:20:46 controller systemd[1]: my_malloc.service: Failed with result 'oom-kill'.
使用 /sys/fs/cgroup 管理 task
1. 列出 controllers 支持的子系统:
cat /sys/fs/cgroup/cgroup.subtree_control
返回结果如下:
cpuset cpu io memory pids
2. 启用 memory 子系统:
echo "+memory" >> /sys/fs/cgroup/cgroup.subtree_control
您可以通过
man 命令来查询 cgroup.subtree_control 的具体使用方法:man 7 cgroups
3. 创建
/sys/fs/cgroup/Example/ 目录:mkdir /sys/fs/cgroup/Example/
此时,子系统文件也会在目录中自动创建:
ls /sys/fs/cgroup/Example/
返回结果如下:
cgroup.controllers cgroup.type cpuset.mems memory.events memory.statcgroup.events cpu.idle cpuset.mems.effective memory.events.local memory.swap.currentcgroup.freeze cpu.max io.bfq.weight memory.high memory.swap.eventscgroup.kill cpu.max.burst io.latency memory.low memory.swap.highcgroup.max.depth cpu.pressure io.max memory.max memory.swap.maxcgroup.max.descendants cpu.stat io.pressure memory.min memory.zswap.currentcgroup.pressure cpu.weight io.prio.class memory.numa_stat memory.zswap.maxcgroup.procs cpu.weight.nice io.stat memory.oom.group pids.currentcgroup.stat cpuset.cpus io.weight memory.peak pids.eventscgroup.subtree_control cpuset.cpus.effective irq.pressure memory.pressure pids.maxcgroup.threads cpuset.cpus.partition memory.current memory.reclaim pids.peak
此时,
Example/cgroup.subtree_control 需要使能,刚创建的时候为空:cat /sys/fs/cgroup/Example/cgroup.subtree_control
4. 启用 Example 的 memory 子系统:
echo "+memory" >> /sys/fs/cgroup/Example/cgroup.subtree_control
确认已启用:
cat /sys/fs/cgroup/Example/cgroup.subtree_control
返回结果如下:
memory
5. 创建
/sys/fs/cgroup/Example/tasks/ 目录:mkdir /sys/fs/cgroup/Example/tasks/
此时 tasks 目录下就只有 memory 子系统相关的文件:
ls /sys/fs/cgroup/Example/tasks/
返回结果如下:
cgroup.controllers cgroup.procs io.pressure memory.max memory.statcgroup.events cgroup.stat irq.pressure memory.min memory.swap.currentcgroup.freeze cgroup.subtree_control memory.current memory.numa_stat memory.swap.eventscgroup.kill cgroup.threads memory.events.local memory.peak memory.swap.highcgroup.max.depth cgroup.type memory.high memory.pressure memory.swap.maxcgroup.max.descendants cpu.pressure memory.low memory.reclaim memory.zswap.current
6. 查看 memory 最大值,默认为 max:
cat /sys/fs/cgroup/Example/tasks/memory.max
返回结果如下:
max
设置 memory 最大值为 10K:
echo "10K" > /sys/fs/cgroup/Example/tasks/memory.max
确认设置结果:
cat /sys/fs/cgroup/Example/tasks/memory.max
返回结果如下:
8192
需要注意的是,
echo 的是 10k,但是显示的是 8k,说明是按照 4k 对齐的。7. 获取 demo 的 pid:
./my_malloc &
返回结果如下:
[1] 2475
将该进程加入 cgroup 中:
echo "2475" > /sys/fs/cgroup/Example/tasks/cgroup.procs
8. 执行后 demo 将被系统
kill :[1]+ Killed ./my_malloc
使用 cgconfig.conf 管理子系统
libcgroup-tools 提供了 cgconfig.service 这个服务帮助用户简便地添加和删除层级,该服务默认关闭,用户在编辑
/etc/cgconfig.conf 后使能服务即可完成对层级的操作,下面举例说明。1. 显示当前支持的子系统:
cat /proc/cgroups
返回结果如下:
#subsys_name hierarchy num_cgroups enabledcpuset 0 90 1cpu 0 90 1cpuacct 0 90 1blkio 0 90 1memory 0 90 1devices 0 90 1freezer 0 90 1net_cls 0 90 1perf_event 0 90 1net_prio 0 90 1hugetlb 0 90 1pids 0 90 1rdma 0 90 1misc 0 90 1
2. 确定需要挂载的子系统没有被占用,以 freezer 为例,执行如下命令,确保子系统未被占用:
lscgroup | grep freezer
子系统被占用时 cgconfig 服务会起不来,已使用被占用的 memory 为例,报错如下:
systemctl status cgconfig
返回结果如下:
× cgconfig.service - Control Group configuration serviceLoaded: loaded (/usr/lib/systemd/system/cgconfig.service; disabled; vendor preset: disabled)Active: failed (Result: exit-code) since Tue 2023-05-23 20:53:49 CST; 1min 10s agoProcess: 2420 ExecStart=/usr/sbin/cgconfigparser -l /etc/cgconfig.conf -s 1664 (code=exited, status=101)Main PID: 2420 (code=exited, status=101)CPU: 3msMay 23 20:53:49 controller systemd[1]: Starting cgconfig.service - Control Group configuration service...May 23 20:53:49 controller cgconfigparser[2420]: /usr/sbin/cgconfigparser; error loading /etc/cgconfig.conf: Cgroup mounting failedMay 23 20:53:49 controller cgconfigparser[2420]: Error: cannot mount memory to /mnt/cgroups/memory: Device or resource busyMay 23 20:53:49 controller systemd[1]: cgconfig.service: Main process exited, code=exited, status=101/n/aMay 23 20:53:49 controller systemd[1]: cgconfig.service: Failed with result 'exit-code'.May 23 20:53:49 controller systemd[1]: Failed to start cgconfig.service - Control Group configuration service.
3. 创建挂载条目,编辑
/etc/cgconfig.conf 输入如下内容:mount {freezer = /mnt/cgroups/freezer;}
重启 cgconfig 服务:
systemctl restart cgconfig
需要注意的是,重启会失效,如果需要持久化,可以通过 enable 服务实现:
systemctl enable cgconfig
此时指定的
/mnt/cgroups/freezer 目录下就会有子系统文件:ls /mnt/cgroups/freezer/
返回结果如下:
cgroup.clone_children cgroup.procs cgroup.sane_behavior notify_on_release release_agent tasks
上述步骤的配置等效于如下操作:
mkdir -p /mnt/cgroups/freezermount -t cgroup -o freezer freezer /mnt/cgroups/freezer
完成后您可以参考上文中 cgroup 的使用方法来控制进程的资源。