容器(Docker/K8s)的资源隔离、systemd 的服务限额、多租户机器上的公平调度,底座都是 cgroup。cgroup v2 在 4.5 内核后逐步取代 v1,用单一层级树取代了 v1 混乱的多层级设计。本文用一个 Python 模拟器量化演示 cpu.weight 份额分配、memory.max 硬限制、io.max 限速与层级继承的全部数值行为,并剖析 v1 到 v2 的设计演进。

一、从 v1 到 v2:为什么要重做

cgroup v1 的每个控制器(cpu、memory、blkio、devices…)可以各自挂载一棵独立的层级树。听上去灵活,实际是灾难:同一进程在 cpu 树、memory 树、blkio 树上可能处于完全不同位置,资源策略互相打架;控制器间的协调(比如 io 要参考 memory 的回写状态)没有统一视图几乎不可能。

v2 的核心决策就一条:整个系统只有一棵 cgroup 树,所有控制器挂在这同一棵树上。一个 cgroup 目录同时配置 cpu.weight、memory.max、io.max,进程归属一目了然。这条决策代价是失去了多树的灵活性,换来的是语义清晰和控制器协作的可能——事后被证明是压倒性的正确。

二、三大核心控制器的语义

三、完整可运行模拟器(Python 3,约 90 行)

#!/usr/bin/env python3
"""cgroup v2 模拟:cpu.weight 份额、memory.max 硬限制、
   io.max 限速、层级继承"""

# ---------- cpu.weight: 按权重瓜分 CPU ----------
def cpu_share(cgroups):
    """模拟一个 CPU 在多个 cgroup 间的份额分配"""
    total_w = sum(w for _, w in cgroups)
    return {name: 100.0 * w / total_w for name, w in cgroups}

print('=== cgroup v2 模拟 ===')
print('--- cpu.weight 份额分配 ---')
top = [('system.slice', 100), ('user.slice', 100),
       ('myapp.slice', 100)]
for n, s in cpu_share(top).items():
    print('  %s: weight=100 -> CPU 份额 %.1f%%' % (n, s))

print('调整 myapp.slice weight=200 后:')
top2 = [('system.slice', 100), ('user.slice', 100),
        ('myapp.slice', 200)]
for n, s in cpu_share(top2).items():
    print('  %s: CPU 份额 %.1f%%' % (n, s))

print()
print('--- 层级继承:myapp.slice 内两个子组 ---')
sub = [('db.service', 100), ('web.service', 300)]
sub_shares = cpu_share(sub)
myapp_total = cpu_share(top2)['myapp.slice']
for n, s in sub_shares.items():
    print('  myapp.slice/%s: 组内份额 %.0f%% -> 实际占整机 %.1f%%'
          % (n, s, myapp_total * s / 100))

# ---------- memory.max: 硬限制触发 OOM ----------
print()
print('--- memory.max 硬限制模拟(限额 64MB) ---')
LIMIT = 64
used = 0
for i in range(10):
    if used + 8 > LIMIT:
        print('  第 %d 次申请 8MB: 超出 memory.max=64MB'
              ' -> 分配失败, cgroup OOM-killer 介入' % (i + 1))
        used -= 24
        print('    OOM killer 选中占用最高的进程,'
              '回收 24MB,当前用量 %dMB' % used)
    else:
        used += 8
        print('  第 %d 次申请 8MB: 成功, 累计 %dMB' % (i + 1, used))

# ---------- io.max: 限速 ----------
print()
print('--- io.max 限速模拟(8:0 设备 rbps=100MB/s) ---')
rleft = 100
requests = [30, 50, 80, 120, 150]
print('  需求: %s MB/s, 上限 100MB/s' % requests)
for i, want in enumerate(requests):
    got = min(want, rleft)
    rleft -= got
    status = '全额满足' if got == want else '被限流(throttled)'
    print('  进程%d 需求 %3dMB/s -> 实际 %3dMB/s  %s'
          % (i + 1, want, got, status))

print()
print('--- 关键设计:v2 统一层级 ---')
print('v1: cpu、memory、io 各自独立层级,'
      '同一进程可能在三棵树的不同位置')
print('v2: 单一层级树,一个 cgroup 同时挂所有控制器,'
      '挂载点统一 /sys/fs/cgroup')

四、真实运行输出与解读

以上代码在 Python 3 下原样运行,输出如下:

=== cgroup v2 模拟 ===
--- cpu.weight 份额分配 ---
  system.slice: weight=100 -> CPU 份额 33.3%
  user.slice: weight=100 -> CPU 份额 33.3%
  myapp.slice: weight=100 -> CPU 份额 33.3%
调整 myapp.slice weight=200 后:
  system.slice: CPU 份额 25.0%
  user.slice: CPU 份额 25.0%
  myapp.slice: CPU 份额 50.0%

--- 层级继承:myapp.slice 内两个子组 ---
  myapp.slice/db.service: 组内份额 25% -> 实际占整机 12.5%
  myapp.slice/web.service: 组内份额 75% -> 实际占整机 37.5%

--- memory.max 硬限制模拟(限额 64MB) ---
  第 1 次申请 8MB: 成功, 累计 8MB
  ...(第 2~8 次各 +8MB,累计到 64MB)...
  第 8 次申请 8MB: 成功, 累计 64MB
  第 9 次申请 8MB: 超出 memory.max=64MB -> 分配失败, cgroup OOM-killer 介入
    OOM killer 选中占用最高的进程,回收 24MB,当前用量 40MB
  第 10 次申请 8MB: 成功, 累计 48MB

--- io.max 限速模拟(8:0 设备 rbps=100MB/s) ---
  需求: [30, 50, 80, 120, 150] MB/s, 上限 100MB/s
  进程1 需求  30MB/s -> 实际  30MB/s  全额满足
  进程2 需求  50MB/s -> 实际  50MB/s  全额满足
  进程3 需求  80MB/s -> 实际  20MB/s  被限流(throttled)
  进程4 需求 120MB/s -> 实际   0MB/s  被限流(throttled)
  进程5 需求 150MB/s -> 实际   0MB/s  被限流(throttled)

逐条解读:

五、理论分析

5.1 份额制 vs 配额制

cpu.weight(份额)与 cpu.max(配额,带宽控制 cfs_quota 的 v2 化)是两种不同的公平:份额制在资源富余时不设限,争用时按比例分;配额制是绝对的周期上限(如每 100ms 最多 50ms)。份额制利用率更高,配额制隔离性更强。生产实践:K8s 的 requests 映射到 weight(份额)、limits 映射到 quota(配额),二者配合才能既高利用又防 noisy neighbor。

5.2 no internal process 约束

v2 有一条容易踩坑的规则:非叶子 cgroup 里不能有进程(开启了控制器的中间节点必须为空)。因为控制器需要明确资源竞争的双方是『子 cgroup 之间』,混入本级进程会让份额语义混乱。这解释了为什么 systemd 会给每个服务单独建叶子 slice,根 slice 只挂控制器不跑进程。

5.3 内存限制的层次语义

memory.max(硬墙)、memory.high(软压制)、memory.low(保护)三层设计对应三种 QoS 意图:max 是『绝对不能超』,high 是『超了就回收施压但不杀』,low 是『别人抢内存时保住我』。Meta 的内存调优实践(oomd、senpai)就是围绕 high/low 的动态调节,目标是把整机内存利用率推到 90% 以上而不触发硬 OOM。

六、工程应用

七、常见误区

八、参考文献

  1. Linux kernel documentation: Documentation/admin-guide/cgroup-v2.rst(v2 的权威设计文档,含 no internal process 等全部规则)。
  2. T. Heo (2015). Cgroup v2: unified hierarchy. LWN.net 及内核 cgroup-v2 主线系列补丁。
  3. systemd 文档: systemd.resource-control(5)(CPUWeight/MemoryMax/IOWeight 与 cgroup v2 的完整映射)。
  4. Meta Engineering (2019-2023). oomd、senpai 与内存调优实践 系列博客(engineering.fb.com)。
  5. W. Mauerer 著,郭旭译. 专业 Linux 内核架构 相关章节:控制组与资源管理。
← 返回文章列表