Linux cgroups

cgroups(Control Groups)最初叫 Process Container,由 Google 工程师(Paul Menage 和 Rohit Seth)于 2006 年提出,后来因为 Container 有多重含义容易引起误解,就在 2007 年更名为 Control Groups,并被整合进 Linux 内核。 顾名思义就是把进程放到一个组里面统一加以控制.
cgroup 是一种以 hierarchical(树形层级)方式组织进程的机制(a mechanism to organize processes hierarchically),以及在层级中以受控和 可配置的方式(controlled and configurable manner)分发系统资源 (distribute system resources)
基本概念
- 任务(task): 在cgroup中,任务相当于是一个进程,可以属于不同的cgroup组,但是所属的cgroup不能同属一层级
- 任务/控制组: 资源控制是以控制组的方式实现的,进程可以加入到指定的控制组中,类似于Linux中user和group的关系。控制组为树状结构的上下父子关系,子节点控制组会继承父节点控制组的属性,如资源配额等
- 层级(hierarchy): 一个大的控制组群树,归属于一个层级中,不同的控制组以层级区分开
- 子系统(subsystem): 一个的资源控制器,比如cpu子系统可以控制进程的cpu使用率,子系统需要附加(attach)到某个层级,然后该层级的所有控制组,均受到该子系统的控制
组成
cgroup 主要由两部分组成:
- 核心(core):主要负责层级化地组织进程;
- 控制器(controllers):大部分控制器负责 cgroup 层级中 特定类型的系统资源的分配,少部分 utility 控制器用于其他目的。
常见的子系统(subsystem)
ubuntu@VM-16-12-ubuntu:/sys/fs/cgroup$ ls
blkio cpu cpuacct cpu,cpuacct cpuset devices freezer hugetlb memory net_cls net_cls,net_prio net_prio perf_event pids rdma systemd unified
- cpu 子系统: 主要限制进程的 cpu 使用率。
- cpuacct 子系统: 可以统计 cgroups 中的进程的 cpu 使用报告。
- cpuset 子系统: 为cgroups中的进程分配单独的cpu节点或者内存节点。
- memory 子系统: 可以限制进程的 memory 使用量。
- blkio 子系统: 可以限制进程的块设备 io。比如物理驱动设备(包括磁盘、固态硬盘、USB 等)
- devices 子系统: 可以控制进程能够访问某些设备。
- net_cls 子系统: 可以标记cgroups 中进程的网络数据包,然后可以使用 tc 模块(traffic control)对数据包进行控制。
- freezer 子系统: 可以挂起或者恢复 cgroups 中的进程。
- ns 子系统: 可以使不同 cgroups 下面的进程使用不同的 namespace。
- freezer subsystem: 可以挂起或恢复 cgroup 中的 task
- perf_event subsystem :使用后使得 cgroup 中的 task 可以进行统一的性能测试
四大功能
1)资源限制:cgroups可以对进程组使用的资源总额进行限制。如设定应用运行时使用内存的上限,一旦超过这个配额就发出OOM(Out of Memory)。
cgroup 主要限制的资源是:
- CPU
- 内存
- 网络
- 磁盘 I/O
2)优先级分配:通过分配的CPU时间片数量及硬盘IO带宽大小,实际上就相当于控制了进程运行的优先级。
3)资源统计:cgroups可以统计系统的资源使用量,如CPU使用时长、内存用量等等,这个功能非常适用于计费。
4)进程控制:cgroups可以对进程组执行挂起、恢复等操作
cgroup 子资源参数详解
blkio: 限制设备 IO 访问

blkio是cgroup中的一个子系统,可以用于限制及监控磁盘读写io blkio控制子系统可以限制进程读写的 IOPS 和吞吐量,但它只能对 Direct I/O 的文件读写进行限速,对 Buffered I/O 的文件读写无法限制 Buffered I/O 指会经过 PageCache 然后再写入到存储设备中。
限制磁盘 IO 有两种方式:权重(weight)和上限(limit)。权重是给不同的应用(或者 cgroup)一个权重值,各个应用按照百分比来使用 IO 资源;上限是直接写死应用读写速率的最大值。
- 设置 cgroup 访问设备的权重
- 设置 cgroup 访问设备的限制
root@node197:~# ll /sys/fs/cgroup/blkio/
-rw-r--r-- 1 root root 0 Mar 4 03:00 blkio.throttle.read_bps_device
-rw-r--r-- 1 root root 0 Mar 4 03:00 blkio.throttle.read_iops_device
-rw-r--r-- 1 root root 0 Mar 4 06:21 blkio.throttle.write_bps_device
-rw-r--r-- 1 root root 0 Mar 4 03:00 blkio.throttle.write_iops_device
# 通用格式
echo "<disk-number> <io-value>" > /sys/fs/cgroup/blkio/<io-type>
# 查看sdb磁盘对应编号为8:16
[root@node189 ~]# ll /dev/block/ | grep sdb
lrwxrwxrwx 1 root root 6 Mar 4 14:29 8:16 -> ../sdb
# 限制sdb磁盘写入带宽为1MB/s
echo "8:16 1048576" >> /sys/fs/cgroup/blkio/blkio.throttle.write_bps_device
# 解除sdb磁盘写入限制
echo "8:16 0" >> /sys/fs/cgroup/blkio/blkio.throttle.write_bps_device
- disk-number:指定需要限制的磁盘编号,可通过ll /dev/block/ | grep sdb查看对应磁盘编号,如8:16
- io-value:指定需要限制的io数值(数值为0表示不限制),当指定的文件名称为blkio.throttle.write_bps_device,则表示限制写入带宽,单位为B/s
- io-type:指定需要限制的io类型
- blkio.throttle.write_bps_device:磁盘写入带宽限制
- blkio.throttle.write_iops_device:磁盘写入IOPS限制
- blkio.throttle.read_bps_device:磁盘读取带宽限制
- blkio.throttle.read_iops_device:磁盘读取IOPS限制
// github.com/openebs/lib-csi@v0.8.2/pkg/device/iolimit/utils.go
func SetIOLimits(request *Request) error {
if !helpers.DirExists(baseCgroupPath) {
return errors.New(baseCgroupPath + " does not exist")
}
// 确认是 cgroup V2
if err := checkCgroupV2(); err != nil {
return err
}
validRequest, err := validate(request)
if err != nil {
return err
}
err = setIOLimits(validRequest)
return err
}
func getIOLimitsStr(deviceNumber *DeviceNumber, ioMax *IOMax) string {
line := strconv.FormatUint(deviceNumber.Major, 10) + ":" + strconv.FormatUint(deviceNumber.Minor, 10)
if ioMax.Riops != 0 {
line += " riops=" + strconv.FormatUint(ioMax.Riops, 10)
}
if ioMax.Wiops != 0 {
line += " wiops=" + strconv.FormatUint(ioMax.Wiops, 10)
}
if ioMax.Rbps != 0 {
line += " rbps=" + strconv.FormatUint(ioMax.Rbps, 10)
}
if ioMax.Wbps != 0 {
line += " wbps=" + strconv.FormatUint(ioMax.Wbps, 10)
}
return line
}
func setIOLimits(request *ValidRequest) error {
line := getIOLimitsStr(request.DeviceNumber, request.IOMax)
err := os.WriteFile(request.FilePath, []byte(line), 0600)
return err
}
func validate(request *Request) (*ValidRequest, error) {
if !helpers.IsValidUUID(request.PodUid) {
return nil, errors.New("Expected PodUid in UUID format, Got " + request.PodUid)
}
podCGPath, err := getPodCGroupPath(request.PodUid, request.ContainerRuntime)
if err != nil {
return nil, err
}
// io限制路径
ioMaxFile := podCGPath + "/io.max"
if !helpers.FileExists(ioMaxFile) {
return nil, errors.New("io.max file is not present in pod CGroup")
}
deviceNumber, err := getDeviceNumber(request.DeviceName)
if err != nil {
return nil, errors.New("Device Major:Minor numbers could not be obtained")
}
return &ValidRequest{
FilePath: ioMaxFile,
DeviceNumber: deviceNumber,
IOMax: request.IOLimit,
}, nil
}
cpu:限制进程组 CPU 使用
任务使用 CPU 资源有两种调度方式:完全公平调度(CFS,Completely Fair Scheduler)和 实时调度(RT,Real-Time Scheduler)。 前者可以根据权重为任务分配响应的 CPU 时间片,后者能够限制使用 CPU 的核数。
// https://github.com/koordinator-sh/koordinator/blob/632ef287e881ea0a2097e04cef108e46f290258e/pkg/koordlet/util/system/cgroup_resource.go
const (
CFSBasePeriodValue int64 = 100000
CFSQuotaMinValue int64 = 1000 // min value except `-1`
CPUSharesMinValue int64 = 2
CPUSharesMaxValue int64 = 262144
CPUWeightMinValue int64 = 1
CPUWeightMaxValue int64 = 10000
CPUStatName = "cpu.stat" // CPU 使用的统计数据
CPUSharesName = "cpu.shares" // cgroup 使用 CPU 时间的权重值
CPUCFSQuotaName = "cpu.cfs_quota_us"
CPUCFSPeriodName = "cpu.cfs_period_us"
CPUBVTWarpNsName = "cpu.bvt_warp_ns"
CPUBurstName = "cpu.cfs_burst_us"
CPUTasksName = "tasks"
CPUProcsName = "cgroup.procs"
CPUThreadsName = "cgroup.threads"
CPUMaxName = "cpu.max"
CPUMaxBurstName = "cpu.max.burst"
CPUWeightName = "cpu.weight"
CPUIdleName = "cpu.idle"
CPUSetCPUSName = "cpuset.cpus"
CPUSetCPUSEffectiveName = "cpuset.cpus.effective"
CPUAcctStatName = "cpuacct.stat"
CPUAcctUsageName = "cpuacct.usage"
CPUAcctCPUPressureName = "cpu.pressure"
CPUAcctMemoryPressureName = "memory.pressure"
CPUAcctIOPressureName = "io.pressure"
)
CFS 调优参数
设置 CPU 数字的单位都是微秒(microsecond),用 us 表示
- cpu.cfs_quota_us:每个周期 cgroup 中所有任务能使用的 CPU 时间,默认为 -1,表示不限制 CPU 使用。需要配合 cpu.cfs_period_us 一起使用,一般设置为 100000(docker 中设置的值)
- cpu.cfs_period_us:每个周期中 cgroup 任务可以使用的时间周期,如果想要限制 cgroup 任务每秒钟使用 0.5 秒 CPU,可以在 cpu.cfs_quota_us 为 100000 的情况下把它设置为 50000。如果它的值比 cfs_quota_us 大,表明进程可以使用多个核 CPU,比如 200000 表示进程能够使用 2.0 核
- cpu.stat:CPU 使用的统计数据,nr_periods 表示已经过去的时间周期;nr_throttled 表示 cgroup 中任务被限制使用 CPU 的次数(因为超过了规定的上限);throttled_time 表示被限制的总时间
- cpu.shares:cgroup 使用 CPU 时间的权重值。如果两个 cgroup 的权重都设置为 100,那么它们里面的任务同时运行时,使用 CPU 的时间应该是一样的;如果把其中一个权重改为 200,那么它能使用的 CPU 时间将是对方的两倍
RT 调度模式下的参数
- cpu.rt_period_us:设置一个周期时间,表示多久 cgroup 能够重新分配 CPU 资源
- cpu.rt_runtime_us:设置运行时间,表示在周期时间内 cgroup 中任务能访问 CPU 的时间。这个限制是针对单个 CPU 核数的,如果是多核,需要乘以对应的核数
memory:限制内存使用
- memory.limit_in_bytes:cgroup 能使用的内存上限值,默认为字节;也可以添加 k/K、m/M 和 g/G 单位后缀。往文件中写入 -1 来移除设置的上限,表示不对内存做限制
- memory.memsw.limit_in_bytes:cgroup 能使用的内存加 swap 上限,用法和上面一样。写入 -1 来移除上限
- memory.failcnt:任务使用内存量达到 limit_in_bytes 上限的次数
- memory.memsw.failcnt:任务使用内存加 swap 量达到 memsw.limit_in_bytes 上限的次数
- memory.soft_limit_in_bytes:设置内存软上限。如果内存充足, cgroup 中的任务可以用到 memory.limit_in_bytes 设定的内存上限;当时当内存资源不足时,内核会让任务使用的内存不超过 soft_limit_in_bytes 中的值。文件内容的格式和 limit_in_bytes 一样
- memory.swappiness:设置内核 swap out 进程内存(而不是从 page cache 中回收页) 的倾向。默认值为 60,低于 60 表示降低倾向,高于 60 表示增加倾向;如果值高于 100,表示允许内核 swap out 进程地址空间的页。如果值为 0 表示倾向很低,而不是禁止该行为
net_cls:为网络报文分类
net_cls 子资源能够给网络报文打上一个标记(classid),这样内核的 tc(traffic control)模块就能根据这个标记做流量控制。
net_cls.classid:包含一个整数值。从文件中读取是的十进制,写入的时候需要是十六进制。比如,0x100001 写入到文件中,读取的将是 1048577, ip 命令操作的形式为 10:1。
这个值的格式为 0xAAAABBBB,一共 32 位,分成前后两个部分,前置的 0 可以忽略,因此 0x10001 和 0x00010001 一样,表示为 1:1
容器中映射关系
docker 中资源的表示
➜ ~ docker run --rm -d --cpus=2 --memory=2g --name=2c2g redis:alpine
e420a97835d9692df5b90b47e7951bc3fad48269eb2c8b1fa782527e0ae91c8e
➜ ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/cpu.max
200000 100000
➜ ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/memory.max
2147483648
➜ ~
➜ ~ docker run --rm -d --cpus=0.5 --memory=0.5g --name=0.5c0.5g redis:alpine
8b82790fe0da9d00ab07aac7d6e4ef2f5871d5f3d7d06a5cdb56daaf9f5bc48e
➜ ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/cpu.max
50000 100000
➜ ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/memory.max
536870912
kubernetes 资源的表示
对于CPU
- resource.requests 经过转换之后会写入 cpu.share, 表示这个 cgroups最少可以使用的 CPU,在Kubernetes中一个CPU线程相当于1024 share
- resource.limits 则通过 cpu.cfs_quota_us和cpu.cfs_period_us 两个文件来控制,表示cgroups最多可以使用的 CPU。如果 cgroups 中任务在每 1 秒内有 0.2 秒,可对单独 CPU 进行存取,可以将 cpu.cfs_quota_us 设定为 200000,cpu.cfs_period_us 设定为 1000000。
对于内存
$ ls -l /sys/fs/cgroup/memory/kubepods/burstable/podfbc202d3-da21-11e8-ab5e-42010a80014b/0a1b22ec1361a97c3511db37a4bae932d41b22264e5b97611748f8b662312574
...
-rw-r--r-- 1 root root 0 Oct 27 19:53 memory.limit_in_bytes
-rw-r--r-- 1 root root 0 Oct 27 19:53 memory.soft_limit_in_bytes
cgroup v1 与 cgroup v2
最初 cgroups 的版本被称为 v1,这个版本的 cgroups 设计并不友好,理解起来非常困难。 后续的开发工作由 Tejun Heo 接管,他重新设计并重写了 cgroups,新版本被称为 v2,并首次出现在 kernel 4.5 版本。
# 系统同时挂载了 cgroup 和 cgroup2
ubuntu@VM-16-12-ubuntu:/sys/fs/cgroup$ mount |grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
# 只有 cpu/io/memory 等少量控制器(大部分还在 cgroup v1 中,系统默认使用 v1)
$ ls -ahlp /sys/fs/cgroup/unified/
total 0
-r--r--r-- 1 root root 0 cgroup.controllers
-rw-r--r-- 1 root root 0 cgroup.max.depth
-rw-r--r-- 1 root root 0 cgroup.max.descendants
-rw-r--r-- 1 root root 0 cgroup.procs
-r--r--r-- 1 root root 0 cgroup.stat
-rw-r--r-- 1 root root 0 cgroup.subtree_control
-rw-r--r-- 1 root root 0 cgroup.threads
-rw-r--r-- 1 root root 0 cpu.pressure
-r--r--r-- 1 root root 0 cpu.stat
drwxr-xr-x 2 root root 0 init.scope/
-rw-r--r-- 1 root root 0 io.pressure
-rw-r--r-- 1 root root 0 memory.pressure
drwxr-xr-x 121 root root 0 system.slice/
drwxr-xr-x 3 root root 0 user.slice/
- cgroup v2 是单一层级树,因此只有一个挂载点(第二行)/sys/fs/cgroup/unified
- cgroup v1 根据控制器类型(cpuset/cpu,cpuacct/hugetlb/…),挂载到不同位置
内核提供了 cgroup_no_v1=allows 配置, 可完全禁用 v1 控制器(强制使用 v2)
Kubernetes 自 v1.25 起 cgroup2 特性正式 stable.
工具
$ sudo apt-get install cgroup-tools
- 查看所有的 cgroup:lscgroup
- 查看所有支持的子系统:lssubsys -a
- 查看所有子系统挂载的位置: lssubsys –m
- 查看单个子系统(如 memory)挂载位置:lssubsys –m memory