cgroup
1. cgroups 简介
1.1 功能和定位
Cgroups 全称 Control Groups,是 Linux 内核提供的物理资源隔离机制,通过这种机制,可以实现对 Linux 进程或者进程组的资源限制、隔离和统计功能。
比如可以通过 cgroup 限制特定进程的资源使用,比如使用特定数目的 cpu 核数和特定大小的内存,如果资源超限的情况下,会被暂停或者杀掉。
Cgroup 是于 2.6 内核由 Google 公司主导引入的,它是 Linux 内核实现资源虚拟化的技术基石,LXC(Linux Containers)和 docker 容器所用到的资源隔离技术,正是 Cgroup。
1.2 相关概念介绍
-
任务(task): 在 cgroup 中,任务就是一个进程。
-
控制组(control group): cgroup 的资源控制是以控制组的方式实现,控制组指明了资源的配额限制。进程可以加入到某个控制组,也可以迁移到另一个控制组。
-
层级(hierarchy): 控制组有层级关系,类似树的结构,子节点的控制组继承父控制组的属性(资源配额、限制等)。
-
子系统(subsystem): 一个子系统其实就是一种资源的控制器,比如 memory 子系统可以控制进程内存的使用。子系统需要加入到某个层级,然后该层级的所有控制组,均受到这个子系统的控制。
概念间的关系:
-
子系统可以依附多个层级,当且仅当这些层级没有其他的子系统,比如两个层级同时只有一个 cpu 子系统,是可以的。
-
一个层级可以附加多个子系统。
-
一个任务可以是多个 cgroup 的成员,但这些 cgroup 必须位于不同的层级。
-
子进程自动成为父进程 cgroup 的成员,可按需求将子进程移到不同的 cgroup 中。
cgroup 关系图如下:

两个任务组成了一个 Task Group,并使用了 CPU 和 Memory 两个子系统的 cgroup,用于控制 CPU 和 MEM 的资源隔离。
1.3 子系统
-
cpu: 限制进程的 cpu 使用率。
-
cpuacct 子系统,可以统计 cgroups 中的进程的 cpu 使用报告。
-
cpuset: 为 cgroups 中的进程分配单独的 cpu 节点或者内存节点。
-
memory: 限制进程的 memory 使用量。
-
blkio: 限制进程的块设备 io。
-
devices: 控制进程能够访问某些设备。
-
net_cls: 标记 cgroups 中进程的网络数据包,然后可以使用 tc 模块(traffic control)对数据包进行控制。
-
net_prio: 限制进程网络流量的优先级。
-
huge_tlb: 限制 HugeTLB 的使用。
-
freezer:挂起或者恢复 cgroups 中的进程。
-
ns: 控制 cgroups 中的进程使用不同的 namespace。
1.4 cgroups 文件系统
Linux 通过文件的方式,将 cgroups 的功能和配置暴露给用户,这得益于 Linux 的虚拟文件系统(VFS)。VFS 将具体文件系统的细节隐藏起来,给用户态提供一个统一的文件系统 API 接口,cgroups 和 VFS 之间的链接部分,称之为 cgroups 文件系统。
比如挂在 cpu、cpuset、memory 三个子系统到 /cgroups/cpu_mem 目录下
关于虚拟文件系统机制,见浅谈Linux虚拟文件系统机制
2. cgroups 子系统
这里简单介绍几个常见子系统的概念和用法,包括 cpu、cpuacct、cpuset、memory、blkio。
2.1 cpu 子系统
cpu 子系统限制对 CPU 的访问,每个参数独立存在于 cgroups 虚拟文件系统的伪文件中,参数解释如下:
-
cpu.shares: cgroup 对时间的分配。比如 cgroup A 设置的是 1,cgroup B 设置的是 2,那么 B 中的任务获取 cpu 的时间,是 A 中任务的 2 倍。
-
cpu.cfs_period_us: 完全公平调度器的调整时间配额的周期。
-
cpu.cfs_quota_us: 完全公平调度器的周期当中可以占用的时间。
-
cpu.stat 统计值 nr_periods 进入周期的次数 nr_throttled 运行时间被调整的次数 throttled_time 用于调整的时间
2.2 cpuacct 子系统
子系统生成 cgroup 任务所使用的 CPU 资源报告,不做资源限制功能。
-
cpuacct.usage: 该 cgroup 中所有任务总共使用的 CPU 时间(ns 纳秒)
-
cpuacct.stat: 该 cgroup 中所有任务总共使用的 CPU 时间,区分 user 和 system 时间。
-
cpuacct.usage_percpu: 该 cgroup 中所有任务使用各个 CPU 核数的时间。
通过 cpuacct 如何计算 CPU 利用率呢?可以通过cpuacct.usage
来计算整体的 CPU 利用率,计算如下:
2.3 cpuset 子系统
适用于分配独立的 CPU 节点和 Mem 节点,比如将进程绑定在指定的 CPU 或者内存节点上运行,各参数解释如下:
-
cpuset.cpus: 可以使用的 cpu 节点
-
cpuset.mems: 可以使用的 mem 节点
-
cpuset.memory_migrate: 内存节点改变是否要迁移?
-
cpuset.cpu_exclusive: 此 cgroup 里的任务是否独享 cpu?
-
cpuset.mem_exclusive: 此 cgroup 里的任务是否独享 mem 节点?
-
cpuset.mem_hardwall: 限制内核内存分配的节点(mems 是用户态的分配)
-
cpuset.memory_pressure: 计算换页的压力。
-
cpuset.memory_spread_page: 将 page cache 分配到各个节点中,而不是当前内存节点。
-
cpuset.memory_spread_slab: 将 slab 对象(inode 和 dentry)分散到节点中。
-
cpuset.sched_load_balance: 打开 cpu set 中的 cpu 的负载均衡。
-
cpuset.sched_relax_domain_level: the searching range when migrating tasks
-
cpuset.memory_pressure_enabled: 是否需要计算 memory_pressure?
2.4 memory 子系统
memory 子系统主要涉及内存一些的限制和操作,主要有以下参数:
-
memory.usage_in_bytes # 当前内存中的使用量
-
memory.memsw.usage_in_bytes # 当前内存和交换空间中的使用量
-
memory.limit_in_bytes # 设置 or 查看内存使用量
-
memory.memsw.limit_in_bytes # 设置 or 查看 内存加交换空间使用量
-
memory.failcnt # 查看内存使用量被限制的次数
-
memory.memsw.failcnt # - 查看内存和交换空间使用量被限制的次数
-
memory.max_usage_in_bytes # 查看内存最大使用量
-
memory.memsw.max_usage_in_bytes # 查看最大内存和交换空间使用量
-
memory.soft_limit_in_bytes # 设置 or 查看内存的 soft limit
-
memory.stat # 统计信息
-
memory.use_hierarchy # 设置 or 查看层级统计的功能
-
memory.force_empty # 触发强制 page 回收
-
memory.pressure_level # 设置内存压力通知
-
memory.swappiness # 设置 or 查看 vmscan swappiness 参数
-
memory.move_charge_at_immigrate # 设置 or 查看 controls of moving charges?
-
memory.oom_control # 设置 or 查看内存超限控制信息(OOM killer)
-
memory.numa_stat # 每个 numa 节点的内存使用数量
-
memory.kmem.limit_in_bytes # 设置 or 查看 内核内存限制的硬限
-
memory.kmem.usage_in_bytes # 读取当前内核内存的分配
-
memory.kmem.failcnt # 读取当前内核内存分配受限的次数
-
memory.kmem.max_usage_in_bytes # 读取最大内核内存使用量
-
memory.kmem.tcp.limit_in_bytes # 设置 tcp 缓存内存的 hard limit
-
memory.kmem.tcp.usage_in_bytes # 读取 tcp 缓存内存的使用量
-
memory.kmem.tcp.failcnt # tcp 缓存内存分配的受限次数
-
memory.kmem.tcp.max_usage_in_bytes # tcp 缓存内存的最大使用量
2.5 blkio 子系统 - block io
主要用于控制设备 IO 的访问。有两种限制方式:权重和上限,权重是给不同的应用一个权重值,按百分比使用 IO 资源,上限是控制应用读写速率的最大值。按权重分配 IO 资源:
-
blkio.weight:填写 100-1000 的一个整数值,作为相对权重比率,作为通用的设备分配比。
-
blkio.weight_device: 针对特定设备的权重比,写入格式为
device_types:node_numbers weight
,空格前的参数段指定设备,weight 参数与 blkio.weight 相同并覆盖原有的通用分配比。
按上限限制读写速度:
-
blkio.throttle.read_bps_device:按每秒读取块设备的数据量设定上限,格式 device_types:node_numbers bytes_per_second。
-
blkio.throttle.write_bps_device:按每秒写入块设备的数据量设定上限,格式 device_types:node_numbers bytes_per_second。
-
blkio.throttle.read_iops_device:按每秒读操作次数设定上限,格式 device_types:node_numbers operations_per_second。
-
blkio.throttle.write_iops_device:按每秒写操作次数设定上限,格式 device_types:node_numbers operations_per_second
针对特定操作 (read, write, sync, 或 async) 设定读写速度上限
-
blkio.throttle.io_serviced:针对特定操作按每秒操作次数设定上限,格式 device_types:node_numbers operation operations_per_second
-
blkio.throttle.io_service_bytes:针对特定操作按每秒数据量设定上限,格式 device_types:node_numbers operation bytes_per_second
3. cgroups 的安装和使用
测试环境为 ubuntu 18.10
3.1 cgroups 的安装
-
安装 cgroups
安装完成后,系统会出现该目录/sys/fs/cgroup
。
创建 cpu 资源控制组,限制 cpu 使用率最大为 50%
$ cd /sys/fs/cgroup/cpu $ sudo mkdir test_cpu $ sudo echo '10000' > test_cpu/cpu.cfs_period_us $ sudo echo '5000' > test_cpu/cpu.cfs_quota_us
创建 mem 资源控制组,限制内存最大使用为 100MB
$ cd /sys/fs/cgroup/memory $ sudo mkdir test_mem $ sudo echo '104857600' > test_mem/memory.limit_in_bytes
将进程加入到资源限制组
测试代码test.cc
如下:
#include <unistd.h> #include <stdio.h> #include <cstring> #include <thread> void test_cpu() { printf("thread: test_cpu start\n"); int total = 0; while (1) { ++total; } } void test_mem() { printf("thread: test_mem start\n"); int step = 20; int size = 10 * 1024 * 1024; // 10Mb for (int i = 0; i < step; ++i) { char* tmp = new char[size]; memset(tmp, i, size); sleep(1); } printf("thread: test_mem done\n"); } int main(int argc, char** argv) { std::thread t1(test_cpu); std::thread t2(test_mem); t1.join(); t2.join(); return 0; }
观察限制之前的运行状态
测试 cpu 的限制
cgexec -g cpu:test_cpu ./test
cpu 使用率降低了一半。
go 中例子:
用 Go 代码来实现一个简单的进程内存限制以及守护(被 Kill 能够自动重启)
- 进程测试代码 每隔一秒申请 1MB 存储空间,并且不释放,然后再打印下 Go 的内存申请情况
package main
import (
"fmt"
"os"
"runtime"
"time"
)
const (
MB = 1024 * 1024
)
func main() {
blocks := make([][MB]byte, 0)
fmt.Println("Child pid is", os.Getpid())
for range time.Tick(time.Second) {
blocks = append(blocks, [MB]byte{})
printMemUsage()
}
}
func printMemUsage() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc))
fmt.Printf("\tSys = %v MiB \n", bToMb(m.Sys))
}
func bToMb(b uint64) uint64 {
return b / MB
}
- 进程守护程序 该守护程序主要实现进程内存限制和进程守护(自动重启)
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"syscall"
)
var (
rssLimit int
cgroupRoot string
)
const (
procsFile = "cgroup.procs"
memoryLimitFile = "memory.limit_in_bytes"
swapLimitFile = "memory.swappiness"
)
func init() {
flag.IntVar(&rssLimit, "memory", 10, "memory limit with MB.")
flag.StringVar(&cgroupRoot, "root", "/sys/fs/cgroup/memory/climits", "cgroup root path")
}
func main() {
flag.Parse()
// set memory limit
mPath := filepath.Join(cgroupRoot, memoryLimitFile)
// 修改 memory.limit_in_bytes 限制内存使用量,默认10MB
whiteFile(mPath, rssLimit*1024*1024)
// set swap memory limit to zero
sPath := filepath.Join(cgroupRoot, swapLimitFile)
// 修改 memory.swappiness 限制交换分区大小,默认0
whiteFile(sPath, 0)
go startCmd("./simpleapp")
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
s := <-c
fmt.Println("Got signal:", s)
}
func whiteFile(path string, value int) {
if err := ioutil.WriteFile(path, []byte(fmt.Sprintf("%d", value)), 0755); err != nil {
log.Panic(err)
}
}
type ExitStatus struct {
Signal os.Signal
Code int
}
func startCmd(command string) {
restart := make(chan ExitStatus, 1)
runner := func() {
cmd := exec.Cmd{
Path: command,
}
cmd.Stdout = os.Stdout
// start app 启动一个进程
if err := cmd.Start(); err != nil {
log.Panic(err)
}
fmt.Println("add pid", cmd.Process.Pid, "to file cgroup.procs")
// set cgroup procs id
pPath := filepath.Join(cgroupRoot, procsFile)
// 修改 cgroup.procs
// 将新生成的进程号 cmd.Process.Pid 写到 cgroup.procs
whiteFile(pPath, cmd.Process.Pid)
// 通过 cmd.Wait() 接收命令输出结果
if err := cmd.Wait(); err != nil {
fmt.Println("cmd return with error:", err)
}
// 接收状态
status := cmd.ProcessState.Sys().(syscall.WaitStatus)
options := ExitStatus{
Code: status.ExitStatus(),
}
if status.Signaled() {
options.Signal = status.Signal()
}
cmd.Process.Kill()
restart <- options
}
go runner()
for {
status := <-restart
// 结果为 Kill 信号的时候,能够重启任务
switch status.Signal {
case os.Kill:
fmt.Println("app is killed by system")
default:
fmt.Println("app exit with code:", status.Code)
}
fmt.Println("restart app..")
// 重新启动一个进程
go runner()
}
}
控制磁盘IO:
使用cgroup限制磁盘io读写速率
在MySQL innobackupex全备期间,磁盘io基本被打满,设置过 --throttle效果不明显,
1
2
3
4
|
- - throttle = # This option specifies a number of I/O operations (pairs of read + write) per second. It accepts an integer argument. It is passed directly to xtrabackup's - - throttle option. |
以下是使用cgroup方式进行io读写的限制测试
1. 安装和启动cgroup
1
2
3
4
5
6
7
|
【centos6】 yum install libcgroup service cgconfig start 【centos7】 yum install - y libcgroup - tools.x86_64 systemctl start cgconfig |
说明:
centos6的cgroup挂载在/cgroup对应的路径下
centos7的cgroup挂载在/sys/fs/cgroup对应的路径下
可使用lssubsys -M 或者lssubsys -am命令查看
2. 设置测试的磁盘盘符和读写速率限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
io_read_limit = 1048576 io_write_limit = 1024 #比如我测试的读写文件都发生在/data文件系统里 filesystem_mounted = '/data' lsblk - d - n | awk '{print $1}' >all_disks while read line do aaa = $(df - h | grep - w ${filesystem_mounted} | grep $line) if [[ ! - z aaa ]];then disk_name = $line fi done < all_disks #echo $disk_name |
说明:以上主要是为了获取到裸盘的盘符,而非分区后的盘符,例如/data 文件系统对应的盘符是vdb1,这里获取的是vdb
3. 创建读写控制组
1
2
|
cgcreate - g blkio:test_read cgcreate - g blkio:test_write |
4. 将io读写限制策略绑定在指定的磁盘驱动号上
1
2
3
|
disk_id = $(ls - l ${disk_name} | awk '{print $5,$6}' | sed 's/ //g' | tr ',' ':' ) cgset - r blkio.throttle.read_bps_device = "${disk_id} ${io_read_limit}" test_read cgset - r blkio.throttle.write_bps_device = "${disk_id} ${io_write_limit}" test_write |
5. 确认配置的限制策略
1
2
|
cgget - r blkio.throttle.read_bps_device test_read cgget - r blkio.throttle.write_bps_device test_write |
6. 读测试对比
1
2
|
dd if = / dev / vdb of = / dev / null cgexec - g blkio:test_read dd if = / dev / vdb of = / dev / null |
7. 写测试对比
1
2
|
dd if = / dev / zero of = / data / testfile bs = 512 count = 100000 oflag = dsync cgexec - g blkio:test_write dd if = / dev / zero of = / data / testfile bs = 1024 count = 1000 oflag = dsync |
8. 将正在执行的进程添加到限制策略里
1
2
|
#比如正在执行的dd命令对应的pid是5306 cgclassify - g blkio:test_write 5306 |
==================================================
Cgroups功能的实现依赖于三个核心概念:子系统、控制组、层级树。
- 子系统(subsystem)
一个内核的组件,一个子系统代表一类资源调度控制器。例如内存子系统可以限制内存的使用量,CPU 子系统可以限制 CPU 的使用时间。
子系统是真正实现某类资源的限制的基础。
- 控制组(cgroup)
表示一组进程和一组带有参数的子系统的关联关系。例如,一个进程使用了 CPU 子系统来限制 CPU 的使用时间,则这个进程和 CPU 子系统的关联关系称为控制组。
- 层级树(hierarchy):
由一系列的控制组按照树状结构排列组成的。这种排列方式可以使得控制组拥有父子关系,子控制组默认拥有父控制组的属性,也就是子控制组会继承于父控制组。比如,系统中定义了一个控制组 c1,限制了 CPU 可以使用 1 核,然后另外一个控制组 c2 想实现既限制 CPU 使用 1 核,同时限制内存使用 2G,那么 c2 就可以直接继承 c1,无须重复定义 CPU 限制。
Docker是如何使用cgroups的?
Linux Cgroups 的设计还是比较易用的,简单粗暴地理解呢,它就是一个子系统目录加上一组 资源限制文件的组合。而对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下 面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程 的 PID 填写到对应控制组的 tasks 文件中就可以了。
而至于在这些控制组下面的资源文件里填上什么值,就靠用户执行 docker run 时的参数指定了。
首先,我们使用以下命令创建一个 nginx 容器:
[root@master ~]# docker run -it -m=1g nginx
===========================================================
参考:
https://xie.infoq.cn/article/f95fc587419278dea1b192e3a
https://www.cnblogs.com/imdba/p/14010458.html
https://piaohua.github.io/post/golang/20210123-golang-cgroups-memory-limit/
https://www.cnblogs.com/huiyichanmian/p/15636063.html 讲的不错,明白
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!