cgroup.memory=nokmem
关闭按 cgroup 的内核内存记账
slab 等不再记到具体 memcg
memcg-aware shrinker 失去按组回收对象
nokmem 是启动参数,整机生命周期稳定
启动时确定
cgroup_memory_nokmem = true;
mem_cgroup_kmem_disabled()
-> true
不是运行时“当前有没有 memcg 记账”
但 shrinker 注册路径还没看这个开关
nokmem 下仍给每个 memcg 分配 per-memcg shrinker 数据
浪费内存,还增加注册与扫描开销
问题不在回收算法,而在分配时机
一旦启动时选了 nokmem,就不会再打开按组内核内存记账
所以可以用 boottime 判据做决定
per-memcg shrinker 数据
每个 memcg/node 的 shrinker_info 按 shrinker 建槽
nokmem 下没有按组记账的内核 slab
这些数据从创建起就没有活可干
容器越多,白占的内存越多
注册时持 shrinker_mutex
idr_alloc(&shrinker_idr, ...)
expand_shrinker_info(id)
扩容时逐个 memcg 扩 shrinker_info
memcg 越多,持锁与扩展越重
回收时也白跑
shrink_slab_memcg() 已在 !memcg_kmem_online() 时跳过普通 shrinker
但 per-memcg bitmap 和数据已经分配,仍要维护
扫描守卫只省 CPU,省不掉内存与注册开销
容器宿主:大量 superblock 各带 shrinker
每个 superblock 的 per-memcg 跟踪都纯属浪费
shrinker_alloc(SHRINKER_MEMCG_AWARE)
注册一个 memcg-aware shrinker
准备分配 per-memcg 数据
-> shrinker_memcg_alloc()
旧代码(节选):只问 memcg 是否整体关闭
if (mem_cgroup_disabled())
return -ENOSYS;
nokmem 下 mem_cgroup_disabled() 仍为 false
所以继续往下分配
旧路的实际工作
shrinker_memcg_alloc()
idr_alloc()
expand_shrinker_info()
per-memcg 数据
nokmem 不会触发跳过;expand 只在 id 超出上限时执行
已有回退:-ENOSYS -> 清 SHRINKER_MEMCG_AWARE -> 非 memcg 路径
新判据只要也返回 -ENOSYS,就能复用这条回退
补丁(节选):加在 shrinker_memcg_alloc() 开头
if (mem_cgroup_disabled())
return -ENOSYS;
+ if (mem_cgroup_kmem_disabled() &&
+ !(shrinker->flags & SHRINKER_NONSLAB))
+ return -ENOSYS;
只多 3 行;不改 idr_alloc 或扫描逻辑
调用者已有回退
-ENOSYS
-> 清 MEMCG_AWARE
-> 非 memcg 路径
无需新错误处理
回退路径不变
跳过分配与扩展
idr_alloc() / expand_shrinker_info() 不再执行
不再建立 per-memcg shrinker_info;只保留 per-shrinker nr_deferred
三行补丁,复用的是已有的 -ENOSYS 回退路径
不是新增一条旁路
SHRINKER_NONSLAB
SHRINKER_MEMCG_AWARE + SHRINKER_NONSLAB
处理的是非内核内存
nokmem 只是关闭“内核内存”按组记账
这类 shrinker 的 per-memcg 行为必须保留
例子:deferred_split_shrinker
THP deferred split
flags = SHRINKER_NUMA_AWARE | SHRINKER_MEMCG_AWARE
| SHRINKER_NONSLAB
管理非内核内存,per-memcg 数据仍需要
nokmem 不能把它降级
跳过条件
nokmem && !NONSLAB -> 跳过
NONSLAB -> 保留 per-memcg 数据
一个例外,避免把非内核内存也一起关掉
shrink_slab_memcg() 已有同款例外
!memcg_kmem_online() 时仍调用 NONSLAB shrinker;分配与扫描口径一致
mem_cgroup_kmem_disabled()
return cgroup_memory_nokmem;
启动时就确定
不会随 memcg 创建顺序改变
memcg_kmem_online()
static key:未设 nokmem 时,第一个非 root memcg online 后置真
这是运行时状态
注册时刻可能还是 false
如果拿运行时状态做分配判据
先注册 shrinker(此时 memcg_kmem_online() == false)
再 mkdir 第一个非 root memcg(未设 nokmem 时 key 才变 true)
早注册的 shrinker 会被误判为不需要 MEMCG_AWARE
之后想补 per-memcg 数据已经晚
boottime 判据与 memcg 创建顺序解耦
nokmem 是全局启动配置,适合在注册时直接决定
省内存
不再分配 per-memcg
shrinker_info
省锁
注册时不再按 memcg
扩展 shrinker_info
省扫描
无活的 shrinker
不进 per-memcg bitmap
例外仍然保留
SHRINKER_NONSLAB:deferred_split_shrinker 等继续按 memcg 工作
非内核内存的 per-memcg 回收不受 nokmem 影响
作者未提供量化 benchmark
收益来自机制:少分配、少持锁、少维护 bitmap
容器宿主上 superblock 越多,节省越明显
这是可扩展性修复,不是吞吐数字的胜利
旧
if (mem_cgroup_disabled())
return -ENOSYS;
nokmem 不触发
继续分配 per-memcg 数据
新
mem_cgroup_disabled() ||
(mem_cgroup_kmem_disabled() &&
!(shrinker->flags & SHRINKER_NONSLAB))
nokmem 下直接 -ENOSYS
复用非 memcg 回退
一个 boottime 判据,改变三件事
分配:不再建 per-memcg shrinker_info
锁:注册时不再按 memcg 扩 shrinker_info
扫描:普通 shrinker 不进 per-memcg bitmap
NONSLAB 例外保留;其余 nokmem shrinker 走非 memcg 回退
判据在启动时确定,与 memcg 创建顺序无关
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x