背景
cgroup.memory=nokmem 让一个 memcg 不按 cgroup 记账内核内存(slab 等)。可 memcg-aware shrinker 的 per-memcg 数据仍然照常分配:每个 cgroup 都给每个 shrinker 建一份信息。
┌──────────────────────────────────────┐
│ 关闭内核内存按组记账时,shrinker 仍 │
│ 按组分配数据 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 可这些 shrinker 在组里无活可干 │ 白占内存,还以组数量级别持锁
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 大量superblock各带shrinker时浪费尤其 │
│ 明显 │
└──────────────────────────────────────┘
在 nokmem 下这些 memcg-aware shrinker 其实无活可干(内核内存不按 cgroup 记账,按组回收 slab 的路径本就被一道"内核内存记账未开启"的判定守护着跳过)。但 per-memcg 的 shrinker 信息还是照样分配着,白占内存,还在持 shrinker_mutex 时按 O(nr_memcgs) 增加、在按组回收 slab 时做无用功。容器宿主上尤其明显:大量 superblock 各带自己的 shrinker,nokmem 下 per-memcg 跟踪纯属浪费。
问题
- nokmem 时 memcg-aware shrinker 无活但仍分配 per-memcg 数据
- 白占内存、按组数量级别持锁
- 按组回收 slab 的路径里做无谓工作
- 大量superblock各带 shrinker 时浪费尤其明显
方案
核心是在 nokmem 时跳过 per-memcg shrinker 数据的分配。
旧:无活也按组分配 shrinker 数据
┌────────────────────────────┐
│ 每组都分配 shrinker 信息 │
└─────────────┬──────────────┘
▼
┌────────────────────────────┐
│ 组越多持锁越久、工作越浪费 │
└────────────────────────────┘
新:跳过分配,非内核内存 shrinker 除外
┌──────────────────────────────────────┐
│ 关闭内核内存记账则不按组分配 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 处理非内核内存的 shrinker 仍保留按组 │
│ 行为 │ 用启动时判定避免误伤早期注册的 shrinker
└──────────────────────────────────────┘
但有一类 shrinker 要保住:SHRINKER_NONSLAB(如 deferred_split_shrinker)处理的是非内核内存,nokmem 不该禁用它们的 per-memcg 行为,它们仍需要 per-memcg 数据。所以跳过只对普通的 memcg-aware shrinker 生效,SHRINKER_NONSLAB 不受影响。
判定依据启动时就确定的配置(cgroup.memory 是否设为 nokmem),而非运行时"当前是否有 memcg 开启了内核内存记账"的状态;若用运行时状态,第一个非 root memcg 创建之前就注册的 shrinker 会被误判为不需要 MEMCG_AWARE。
收益
作者未提供量化 benchmark。从机制推断的预期收益:
- nokmem 配置下不再为每个 memcg 分配 shrinker 信息,节省内存
- 不再按 O(nr_memcgs) 持 shrinker_mutex,大数量 memcg 下可扩展性改善
- 按组回收 slab 不再为无活的 memcg-aware shrinker 做无用功
- SHRINKER_NONSLAB 不受影响,非内核内存的 per-memcg 行为保留