systemd.resource-control : CPU control 详析与实践
1. First | 序
systemd.resource-control 的目标,是与 Linux 内核的控制组(Control Groups, cgroups)机制紧密集成,为服务(services)、切片(slices)、作用域(scopes)等多种 systemd 单元类型,提供一个在层级化树状结构中进行资源管理的标准化框架。这个框架不仅覆盖了传统的 CPU、内存、I/O 资源,还延伸至任务数量、设备访问权限乃至基于 BPF 的高级网络策略。通过这种方式,systemd 声明式的方式将复杂的资源隔离与分配问题,转化为清晰、可维护的配置文件。
接下来,我们将深入探讨 systemd 资源控制的总体架构,以理解其设计哲学和核心概念。
2. 资源控制总体架构
systemd 的资源控制模型围绕三个核心概念构建:
- 计量与限制的分离
- 层级化的资源分配
- 动态的控制器激活机制
2.1 计量 (Accounting) vs. 限制 (Limiting)
systemd 明确区分了对资源使用的 计量和限制 ,这为系统监控和策略制定提供了极大的灵活性。管理员可以先开启计量以收集服务运行的基线数据,再根据实际情况设定合理的限制。
| 计量 (Accounting) | 限制 (Limiting) | |
|---|---|---|
| 核心目的 | 追踪与统计指定单元的资源使用情况,而不施加任何限制。 | 为资源使用设定硬性或软性的边界,并由内核强制执行。 |
| 典型参数 | MemoryAccounting=yes TasksAccounting=yes IOAccounting=yes |
MemoryMax=1G TasksMax=1000 IOWriteBandwidthMax=/dev/sda 10M |
| 对性能的影响 | 开启计量会带来轻微的内核开销,用于维护统计计数器。 | 只有当单元的资源使用触及设定的阈值时,才会触发限制行为(如节流、OOM终止等)。 |
| 主要用途 | - 性能分析与容量规划 - 故障排查 - 设定资源限制前的数据收集 |
- 防止资源滥用 - 确保服务质量(QoS) - 在多租户环境中实现资源隔离 |
2.2 切片 (Slice) 与单元 (Unit) 的层级关系
systemd 通过 切片(Slice) 单元构建了一个资源控制的层级树,这与 cgroup 的原生树状结构完全对应。所有需要进行资源控制的单元(如服务或作用域)都必须被放置在某个切片中。资源配额沿着这个层级树自上而下进行分配。
- 顶层切片:系统默认包含几个顶层切片,如 system.slice(用于系统服务)、user.slice(用于用户会话)和 -.slice(根切片)。
- 资源分配:父切片获得的资源会在其子切片和其中的单元之间进行分配。例如,在 user.slice 下,每个用户会话会拥有一个独立的子切片(如 user-1000.slice),user.slice 上的资源限制将成为其下所有用户会话共享的总额度。
- 配置方式:通过在单元文件中设置 Slice= 参数,可以明确地将一个单元放置到资源树的指定位置。例如,Slice=user-1000.slice 会将单元放入用户 1000 的切片中。
这种层级结构使得实现复杂的资源分配策略成为可能,例如为整个用户群体设置总资源上限,同时为其中的特定高优先级应用分配更多的相对权重。
2.3 控制器启用机制
systemd 采用了一种高效的隐式控制器启用机制。管理员无需手动启用某个 cgroup 控制器(如 cpu 或 memory),systemd 会根据单元的配置自动完成。
其工作原理是:当为一个单元设置了与特定资源相关的限制参数时,systemd 会自动为该单元、其所有父级单元(直至根切片)以及所有同级单元(siblings)启用对应的 cgroup 控制器。
例如,在一个服务文件 a.service 中设置了 CPUWeight=200,systemd 会确保 cpu 控制器在以下路径上被激活: -.slice → system.slice → a.service
更重要的是,a.service 的所有同级单元(例如 b.service、c.service)也会被自动启用 cpu 控制器。这是一个 cgroup v2 资源模型的根本特性,它确保了在同一层级的所有单元都能参与相对权重的分配。如果同级单元不被激活,a.service 的权重设置将失去比较的基准,从而变得毫无意义。这个机制大大简化了配置,因为管理员只需关注具体单元的资源需求,systemd 会负责处理所有必要的底层 cgroup 控制器激活操作。
在理解了 systemd 资源控制的总体架构后,让我们首先详细分析最核心的资源——CPU的控制方法。
3. CPU Control OPTIONS
3.1. CPUAccounting=
- 目的与作用:这是一个布尔值开关,用于启用或关闭对单元内进程的 CPU 使用时间统计。
- 值与格式:yes 或 no。
- 底层机制:在 cgroup v2 统一层级架构下,CPU 使用统计功能是默认且始终可用的。
- 特别说明:根据 systemd 的历史记录,自 版本 258 起,此参数已被弃用。因为它在 cgroup v2 环境中已无实际作用,设置与否都不会影响 CPU 统计的可用性。
3.2. CPUWeight= 与 StartupCPUWeight=
- 目的与作用:控制单元之间 CPU 时间的相对权重。这是一个“工作保持(work-conserving)”的机制,即只有在 CPU 资源出现竞争时,权重才会生效。权重越高的单元能获得越多的 CPU 时间。
- 值与格式:接受一个 1 到 10000 之间的整数,内核默认为 100。此外,还支持特殊值 "idle",用于标记该 cgroup 采用“空闲调度”。这意味着该单元仅在系统没有其他非空闲标记的进程需要执行时,才会获得 CPU 资源。
- 底层机制:此参数直接映射到 cgroup v2 的 cpu.weight 属性(或 cpu.idle 属性)。
- 应用阶段:CPUWeight= 应用于系统正常运行阶段。StartupCPUWeight= 则专门应用于系统的启动和关闭阶段,允许管理员为服务的启动过程赋予不同的 CPU 优先级。如果未设置 StartupCPUWeight=,则启动阶段也会使用 CPUWeight= 的值。
3.3. CPUQuota=
- 目的与作用:设定一个单元可以使用的 CPU 时间的绝对上限。这是一种硬性限制,无论系统是否繁忙,该单元的 CPU 使用率都不会超过此配额。
- 值与格式:一个以 % 结尾的百分比值。该百分比是相对于单个 CPU 核心的总计算能力。例如,CPUQuota=20% 表示该单元最多使用一个 CPU 核心 20% 的时间。若要允许使用超过一个核心,可以设置大于 100% 的值,如 CPUQuota=150% 表示可以使用 1.5 个 CPU 核心的计算能力。
- 底层机制:控制 cgroup v2 的 cpu.max 属性。在传统的 cgroup v1 系统上,它对应于 cpu.cfs_quota_us。
3.4. CPUQuotaPeriodSec=
- 目的与作用:定义 CPUQuota= 的计算周期。它决定了内核以多长的时间窗口来衡量和强制执行 CPU 配额。
- 值与格式:一个时间值,单位可以是 s (秒)、ms (毫秒) 等。内核支持的范围是 1ms 到 1000ms,systemd 的默认值为 100ms。
- 底层机制:控制 cpu.max 属性的第二个字段。在 cgroup v1 上,它对应于 cpu.cfs_period_us。
3.5. AllowedCPUs= 与 StartupAllowedCPUs=
- 目的与作用:将单元内的进程绑定到指定的 CPU 核心上运行,即设置 CPU 亲和性。
- 值与格式:一个由逗号或空格分隔的 CPU 核心列表或范围。例如,0,1,5-7 表示进程只能在 CPU 0, 1, 5, 6, 7 上运行。
- 底层机制:通过 cgroup v2 的 cpuset 控制器实现。
- 应用阶段:与 CPUWeight 类似,AllowedCPUs= 用于正常运行阶段,而 StartupAllowedCPUs= 用于启动和关闭阶段,提供了在不同阶段设置不同 CPU 亲和性的能力。
4. 实践
测试使用服务配置如下所示

4.1. CPUQuota & CPUQuotaPeriodSec
PrivateTmp= & WorkingDirectory=.service 文件中另有参数 PrivateTmp= & WorkingDirectory= 设置
这是因为 openSUSE MicroOS 系统默认根分区只读
其他非不可变系统可以忽略
4.2. CPUWeight
4.3. AllowedCPUs
示例最后部分做了一些延申并以实验解释了当我们同时设置以下内容时
CPU 使用情况会如何
# /etc/systemd/system/quota-test.service
[Service]
PrivateTmp=yes
WorkingDirectory=/tmp
CPUQuota=300%
CPUAccounting=yes
CPUQuotaPeriodSec=100ms
ExecStart=/usr/bin/stress-ng --cpu 6
# /etc/systemd/system/quota-test.service.d/override.conf
[Service]
AllowedCPUs=0