Skip to content

02 资源使用听指挥 - cgroup 与资源限制

什么是 cgroup(Control Groups)

  • cgroup 是 Linux 内核中的一种机制,用于将进程划分到不同的组(称为 "控制组"),从而对这些组应用各种资源的限制、监控和隔离。
  • cgroup 可以限制和跟踪 CPU、内存、I/O、网络带宽等资源的使用,广泛应用于容器(如 Docker、LXC)和虚拟化环境中,以确保每个应用或服务不会占用过多的系统资源。

cgroup 的基本功能

  • 资源限制:可以为进程组设置资源使用的上限,例如限制 CPU 使用率、内存使用量、磁盘 I/O 带宽等。
  • 优先级设置:为不同的 cgroup 设置优先级,确保关键任务优先获得资源。
  • 资源隔离:将不同进程组的资源进行隔离,互不干扰。
  • 资源审计:可以监控 cgroup 的资源使用情况,统计进程组的资源使用情况,例如 CPU 时间、内存使用量等,便于监控和分析,方便管理员跟踪资源消耗。
  • 进程控制:允许动态地添加、移除进程到 cgroup 中。

cgroup 的层次结构

cgroup 采用树状结构来组织进程组。每个 cgroup 可以包含多个子 cgroup,形成一个层次结构。每个 cgroup 节点可以设置不同的资源限制和优先级。

  • 根 cgroup:位于层次结构的顶端,包含所有进程。
  • 子 cgroup:根 cgroup 下的子节点,可以进一步细分资源管理。

cgroup 的子系统

  • cgroup 是分层结构,每个层级称为一个 subsystemcontroller。每个子系统管理某一种资源的使用。

  • cgroup 通过不同的子系统(subsystem)来管理不同的资源类型。

  • 常见的 cgroup 子系统包括:

    • CPU 子系统(cpu、cpuacct):控制 CPU 资源的分配。
    • 内存子系统(memory):控制内存的使用,包含物理内存和交换内存的限制。
    • 块设备 I/O 子系统(blkio):控制块设备(如硬盘)的 I/O 读写带宽。
    • 设备访问控制子系统(devices):控制进程是否可以访问某些设备。
    • 网络子系统(net_cls, net_prio):控制进程的网络带宽和优先级。
    • 挂载命名空间(namespace):隔离系统的名字空间,使每个组的进程感知到的系统环境不同。

cgroup 版本

目前 Linux 内核支持两种 cgroup 版本,分别是 cgroup v1cgroup v2cgroup v2 是对 cgroup v1 的改进版本,设计更简洁,功能更强大,但一些控制器在 cgroup v2 中进行了简化和调整。

  • cgroup v1:每个控制器(如 CPU、内存等)可以独立挂载,并且对每个子系统(controller)都有自己的层级,导致管理较为复杂。
  • cgroup v2:所有控制器在一个统一的层次结构中进行管理,简化了管理操作。

cgroup 和 taskset 的区别

  • taskset 控制对象是进程的 CPU;cgroup 控制对象可以是 CPU,也可以是内存和 IO;:
  • taskset 的作用对象是单个进程;cgroup 作用对象可以是单个进程也可以是进程组;
  • cgroup 可能存在层级关系;taskset 的作用对象无层级关系;
  • 在某些场景下,taskset 的作用可由 cgroup 替代;

cgroup 的应用场景

  • 容器化:Docker 和 Kubernetes 等容器技术广泛使用 cgroup 来隔离和限制容器的资源使用。
  • 虚拟化:在虚拟化环境中,cgroup 可以用于限制虚拟机的资源使用。
  • 资源调度:在多租户环境中,cgroup 可以用于公平分配资源,防止某个租户占用过多资源。

使用 cgroup v1

在 cgroup v1 中,每个资源控制器有自己独立的挂载点,可以通过在文件系统中写入特定文件来配置资源限制。

  1. 创建新的 cgroup:

    bash
    mkdir /sys/fs/cgroup/cpu/my_cgroup # 用于限制 CPU 资源
    mkdir /sys/fs/cgroup/memory/my_cgroup # 用于限制内存资源
    mkdir /sys/fs/cgroup/blkio/my_cgroup # 用于限制块设备 I/O
  2. 设置 CPU 限制:

    bash
    echo "50000" | tee /sys/fs/cgroup/cpu/my_cgroup/cpu.cfs_quota_us # 限制为 50% 的 CPU 资源
    echo "100000" > /sys/fs/cgroup/cpu/my_cgroup/cpu.cfs_quota_us # 限制为 100% 的 CPU 资源
  3. 设置内存限制:

    bash
    echo "524288000" > /sys/fs/cgroup/memory/my_cgroup/memory.limit_in_bytes # 限制为 500MB
    echo "104857600" | tee /sys/fs/cgroup/memory/mygroup/memory.limit_in_bytes # 限制为 100MB
  4. 限制块设备 I/O 速率:

    bash
    echo "8:0 52428800" | tee /sys/fs/cgroup/blkio/my_cgroup/blkio.throttle.read_bps_device # 限制读 50MB/s
    echo "8:0 20971520" > /sys/fs/cgroup/blkio/my_cgroup/blkio.throttle.write_bps_device # 限制写 20MB/s
    • 8:0 是块设备的主次设备号,5242880020971520 分别是读写速率(字节为单位)。
  5. 将进程加入到 cgroup:

    bash
    echo <PID> > /sys/fs/cgroup/cpu/my_cgroup/tasks # 将进程加入到 限制 CPU 的 cgroup 中
    echo <PID> | sudo tee /sys/fs/cgroup/memory/mygroup/tasks # 将进程加入到 限制内存的 cgroup 中
    echo <PID> | sudo tee /sys/fs/cgroup/blkio/my_cgroup/tasks # 将进程加入到 限制块设备 I/O 的 cgroup 中

使用 cgroup v2

  • cgroup v2 的配置方式与 cgroup v1 略有不同,所有控制器在同一个层次结构中进行管理。
  • 通过将控制器激活在 cgroup v2 中,我们可以一次性管理多个资源。
  1. 创建新的 cgroup:

    bash
    mkdir /sys/fs/cgroup/my_cgroup
  2. 设置资源限制:

    bash
    # 设置 CPU 限制
    echo 20000 > /sys/fs/cgroup/my_cgroup/cpu.max # 限制为 20% 的 CPU 资源
    
    # 设置内存限制
    echo 524288000 > /sys/fs/cgroup/my_cgroup/memory.max # 限制为 500MB
    
    # 限制 I/O 速率
    echo "8:0 rbps=52428800 wbps=20971520" > /sys/fs/cgroup/my_cgroup/io.max # 限制读 50MB/s,写 20MB/s
  3. 将进程加入到该 cgroup:

    bash
    echo <PID> > /sys/fs/cgroup/my_cgroup/cgroup.procs

cgroup 示例

cgroup 是内核提供的资源管理的接口,其位置一般位于 /sys/fs/cgroup 可通过 mount | grep cgroup 进行确定。

bash
  ~ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

限制进程的 CPU 调度

bash
# 进入到 cgroup 文件系统中的 cpu,cpuacct 目录。
# cgroup 是 Linux 提供的一种资源管理机制,这里的 cpu,cpuacct 是用于管理 CPU 资源使用和统计的控制组。
# 通过进入这个目录,可以对系统中进程的 CPU 资源使用进行控制和监控。
cd /sys/fs/cgroup/cpu,cpuacct

# 设置当前 cgroup 的 clone_children 属性为 1。
# clone_children 属性控制是否允许子 cgroup 继承父 cgroup 的资源限制和配置。
# 将此值设置为 1,意味着新创建的子 cgroup 将自动继承父 cgroup 的限制和属性。
# 例如,如果父 cgroup 限制了 CPU 使用,子 cgroup 也会自动受到相同的限制。
echo 1 > cgroup.clone_children

# 创建一个名为 quota_limit 的子目录。
# 该子目录实际上是一个新的 cgroup,这个 cgroup 可以为其包含的进程设置独立的资源限制。
# 创建该目录后,进入这个新的 cgroup 目录,后续操作将在这个 cgroup 下进行。
mkdir quota_limit && cd quota_limit

# 将目标进程的 PID 写入当前 cgroup 的 cgroup.procs 文件。
# cgroup.procs 文件用于将进程加入到当前 cgroup 中,这样该进程将受到当前 cgroup 的资源限制。
# [PID] 是占位符,表示进程的实际 PID(进程标识符)。例如,如果进程的 PID 是 1234,那么应该写入 echo 1234 > cgroup.procs。
echo [PID] > cgroup.procs

# 设置当前 cgroup 的 CPU 使用配额。
# cpu.cfs_quota_us 文件控制该 cgroup 内进程的 CPU 使用时间配额,以微秒为单位。
# 这里设置为 20000 微秒,表示该 cgroup 中的所有进程每 100000 微秒(默认周期)最多只能使用 20000 微秒的 CPU 时间。
# 默认情况下,cpu.cfs_period_us(CFS 调度周期)是 100000 微秒,即 100 毫秒。
# 因此,这样的设置相当于将进程的 CPU 使用限制在 20%(20000 / 100000 = 0.2)。
# 这意味着即使系统有多个 CPU 核心,这个进程也最多只能使用总 CPU 资源的 20%。
echo "20000" > cpu.cfs_quota_us # 此操作为设置进程最多使用20%的CPU资源,可通过 top -H -p [PID] 进行验证
# top -H 命令显示各线程的资源使用情况,而 -p 选项允许指定某个进程。

cpu/cpuacct 目录文件

/sys/fs/cgroup/cpu,cpuacct 目录下文件:

文件名描述
cgroup.clone_children控制子 cgroup 是否继承父 cgroup 的设置
cgroup.event_control事件通知机制的配置文件,通常用于通知 cgroup 中发生的事件
cgroup.procs包含属于该 cgroup 的进程的 PID 列表
cgroup.sane_behavior控制 cgroup 的行为是否遵循更为严格的规则
cpuacct.statCPU 统计信息,包含进程在用户态和内核态下的 CPU 时间使用
cpuacct.usage该 cgroup 中所有进程累计使用的 CPU 时间(以纳秒 ns 为单位)
cpuacct.usage_percpu每个 CPU 核心上进程的累计使用时间(以纳秒 ns 为单位)
cpu.cfs_period_us完全公平调度器 (CFS) 的调度周期,控制时间片长度(以微秒 μs 为单位)。和 cpu.cfs_quota_us 配合使用
cpu.cfs_quota_usCFS 可以使用的 CPU 时间配额(以微秒 μs 为单位),用于限制 CPU 使用。cpu.cfs_quota_us/cpu.cfs_period_us 即为允许使用 CPU 上限。
cpu.rt_period_us实时调度器的调度周期(以微秒 μs 为单位)。和 cpu.rt_runtime_us 配合使用
cpu.rt_runtime_us实时调度器中允许的最大 CPU 运行时间(以微秒 μs 为单位),默认值为 950000,即 1s 内 RT 进程最多运行 0.95s。
cpu.sharesCPU 共享权重,决定该 cgroup 相对于其他 cgroup 的 CPU 资源分配比例,默认值为 1024,若为 2048,则表示该 cgroup 的 CPU 资源分配是其他 cgroup 的 2 倍(相比同级进程多 1 倍运行时间)。
cpu.stat包含关于任务调度和 CPU 使用的统计信息。
  1. cgroup.clone_children

    • 描述: 控制子 cgroup 是否继承父 cgroup 的资源限制和属性。
    • 默认值: 0,表示子 cgroup 不继承父 cgroup 的设置。
    • 用途: 设置为 1 后,当在当前 cgroup 目录下创建子 cgroup 时,子 cgroup 会继承父 cgroup 的资源限制。
    • 操作: echo 1 > cgroup.clone_children: 设置子 cgroup 继承父 cgroup 的参数配置,这样在创建子 cgroup 时,无需再次手动配置相同的资源限制。
  2. cgroup.event_control

    • 描述: 用于 cgroup 事件通知的配置,通常用于监控和响应 cgroup 中的资源使用事件。
    • 用途: 配置特定事件的通知机制,但在常规资源管理中较少直接操作。
  3. cgroup.procs

    • 描述: 包含属于当前 cgroup 的所有进程的 PID 列表。
    • 用途: 将进程的 PID 写入此文件,即可将该进程分配到当前 cgroup,并受到相应的资源限制。
    • 操作: echo [PID] > cgroup.procs[PID] 替换为实际进程的 ID,以对该进程进行资源限制或管理。
  4. cgroup.sane_behavior

    • 描述: 控制 cgroup 的行为是否遵循更为严格的规则,通常用于确保资源管理更加合理和一致。
    • 用途: 对于一些较新的内核版本和配置可能会使用到,但默认不一定启用。
  5. cpuacct.stat

    • 描述: 提供当前 cgroup 中所有进程的 CPU 使用统计信息,包括用户态和内核态的 CPU 时间。
    • 用途: 监控 CPU 使用情况,以了解该 cgroup 中进程的 CPU 时间消耗。
  6. cpuacct.usage

    • 描述: 显示当前 cgroup 中所有进程累计使用的 CPU 时间,以纳秒为单位。
    • 用途: 用于监控该 cgroup 总体的 CPU 使用量。
  7. cpuacct.usage_percpu

    • 描述: 显示该 cgroup 中所有进程在每个 CPU 核心上累计使用的 CPU 时间,以纳秒为单位。
    • 用途: 分析和监控不同 CPU 核心的使用情况,帮助在多核系统上进行负载平衡或调优。
  8. cpu.cfs_period_us:

    • 这个文件表示的是完全公平调度器 (CFS, Completely Fair Scheduler) 的调度周期(以微秒为单位)。它控制了一个时间片的长度。
    • 默认值通常为 100000 微秒 (即 100 毫秒)。
  9. cpu.cfs_quota_us:

    • 这个文件表示的是 CFS 可以使用的 CPU 时间配额(以微秒为单位)。当进程组消耗的 CPU 时间达到这个配额时,CFS 会将它的调度推迟到下一个调度周期。
    • 通过设置这个值,你可以限制进程组的 CPU 使用率。
  10. cpu.rt_period_us:

    • 这个文件定义了实时调度器的调度周期(以微秒为单位)。实时进程使用的是 RT (Real-Time) 调度策略。
    • 默认值通常为 1000000 微秒(即 1 秒)。
    • cfs_quota_us/cfs_period_us 即为允许使用 CPU 上限:通过 cpu.cfs_quota_uscpu.cfs_period_us 的比值,可以计算出进程组在某个时间段内允许使用的 CPU 的百分比。
    • 例如,cfs_quota_us 设置为 50000,cfs_period_us 设置为 100000,表示该进程组最多能使用 50% 的 CPU 时间。
  11. cpu.rt_runtime_us:

    • 这个文件表示在每个 rt_period_us 内,实时进程最多可以运行的时间(以微秒为单位)。
    • 默认值通常为 950000 微秒,这意味着在 1 秒的周期内,实时进程最多可以运行 0.95 秒。
  12. cpu.shares:

    • 这个文件表示相对 CPU 共享权重。默认值通常为 1024。
    • 权重值越高,进程组可以获得的 CPU 资源相对于其他进程组越多。如果一个进程组的权重设置为 2048,而另一个是 1024,那么前者相对于后者将获得两倍的 CPU 时间。
  13. cpu.stat:

    • 这个文件包含一些关于 CPU 使用的统计信息,比如任务的调度时间和等待时间等。

限制进程的 CPU 核心

bash
cd /sys/fs/cgroup/cpuset
# 切换到 cpuset 子系统的 cgroup 控制目录。
# cpuset 是一种 cgroup 子系统,用于控制进程在特定 CPU 核心上运行。

echo 1 > cgroup.clone_children
# 将 cgroup.clone_children 设置为 1。
# 启用子 cgroup 的设置继承,即当创建新的子 cgroup 时,
# 子 cgroup 会自动继承父 cgroup 的 CPU 集合和内存节点设置。

mkdir sched_limit && cd sched_limit
# 创建一个新的 cgroup 目录 sched_limit,并切换到该目录。
# 这个新目录将作为一个独立的 cgroup,允许我们对其中的进程应用特定的资源限制。

echo [PID] > cgroup.procs
# 将指定的进程 ID (PID) 添加到当前 cgroup (sched_limit) 中。
# 这将把该进程移动到 sched_limit cgroup 中,使其受这个 cgroup 的资源限制影响。
# 请将 [PID] 替换为实际的进程 ID。

cat cpuset.cpus
# 显示当前 sched_limit cgroup 中允许进程运行的 CPU 核心列表。
# cpuset.cpus 文件包含允许该 cgroup 中进程运行的 CPU 核心列表。

echo 1 > cpuset.cpus
# 将 sched_limit cgroup 的 cpuset.cpus 文件设置为 1。
# 这意味着该 cgroup 中的所有进程将被限制在 CPU 1 核心上运行。

# 此时,可以使用下面的命令验证进程的 CPU 核心限制:
# top -H -p [PID]
# 通过 top 命令查看指定进程的所有线程(-H 选项),
# 并验证这些线程是否只在 CPU 1 上运行。

cgroup 与 taskset 对比

在简单的单进程情境下,cgrouptaskset 的效果相似。然而,当涉及到多进程或多 cgroup 时,cgroup 的组调度机制使得它能够提供更复杂的 CPU 核心和资源控制,可能导致与 taskset 不同的行为。这种差异主要来源于进程调度和组调度的原理。

  • taskset:是一个用户空间工具,用于设置或获取进程的 CPU 亲和性。通过 taskset,可以将特定进程绑定到某些指定的 CPU 核心上。例如,taskset -c 0x2 [PID] 将进程绑定到 CPU 1 上(假设 CPU 核心从 0 开始编号)。
  • cgroupcpuset 子系统通过控制组(cgroup)来实现更复杂的 CPU 亲和性设置,可以控制一个或多个进程在指定 CPU 核心和内存节点上的运行。此外,它还提供了更细粒度的资源控制,比如限制一组进程只能使用指定的 CPU 核心。

  • 若仅存在单个 cgroup 文件夹且仅有单个进程,则其效果与 taskset -cp 0x2 [PID] 一致
  • 若存在多个 cgroup 文件夹或文件夹中存在多个进程,则可能与 taskset 的效果不同,相关原理涉及
    • 进程调度
    • 组调度

当只有单个 cgroup 文件夹且仅有一个进程时

  • 在这种情况下,cgroup 中设置的 CPU 亲和性实际上只影响到该单一进程,因此其效果与使用 taskset 的效果相同。无论是通过 taskset 绑定 CPU 亲和性,还是通过 cgroup 设置,结果都是该进程只能在指定的 CPU 核心上运行。

当存在多个 cgroup 文件夹或单个 cgroup 中存在多个进程时

  • 进程调度(Process Scheduling):

    • 在传统的进程调度中,如果不使用 cgroup,Linux 内核会根据优先级、负载等因素动态地在多个 CPU 核心之间分配进程。如果使用 taskset,单个进程的亲和性被设置后,它会被限制在指定的 CPU 核心上。
    • 但是,使用 cgroup 可以对一组进程进行调度控制。如果一个 cgroup 中存在多个进程,那么这些进程会受到 cgroup 设置的 CPU 限制,并且内核会根据组内的调度策略(例如公平调度)来决定这些进程如何在指定的 CPU 核心上运行。
  • 组调度(Group Scheduling):

    • 当多个 cgroup 文件夹存在时,每个 cgroup 可以独立地设置自己的 CPU 亲和性。这使得不同的 cgroup 中的进程可以被限制在不同的 CPU 核心上运行,从而实现更复杂的资源隔离和控制。
    • 如果一个 cgroup 中有多个进程,那么这些进程在被调度到指定的 CPU 核心时,会按照组调度策略进行竞争。在这种情况下,即使不同的进程被限制在同一组 CPU 核心上,它们的调度行为可能会有所不同,具体取决于内核的调度算法(如 CFS - 完全公平调度器)。

关键差异点

  • 单进程 vs 多进程:当只有一个进程时,tasksetcgroup 的效果一致。但当有多个进程时,cgroup 提供了对多个进程的更复杂的调度控制,并且可以在多个 CPU 核心之间分配进程组的资源。
  • cgroup 的组调度特性cgroup 不仅影响单个进程,还影响一组进程,这使得它能够在资源管理方面提供比 taskset 更高级的功能,特别是在需要对进程组进行资源隔离的场景中。