背景
超级块 shrinker 在注册时声明自己 memcg 感知,理由很实在:它管理的 dentry、inode LRU 本就逐 memcg 记账。但同一个 shrinker 在回收时还会顺带驱动一组可选的文件系统钩子,用来清点游离于 LRU 之外的缓存对象:btrfs 用它回收 extent map,xfs 用它做 inode 回收,shmem 用它处理闲置的大页。这些钩子操作的是整个文件系统或单个超级块共用的全局状态,完全不区分调用来自哪个 memcg。于是按组计费的 LRU 与无视 memcg 的全局钩子,就这样混进了同一次回收。
memcg 感知 shrinker 混入全局钩子
┌────────────────────────────────┐
│ shrinker 注册为 memcg 感知 │ dentry 与 inode LRU 按组计费
└───────────────┬────────────────┘
▼
┌────────────────────────────────┐
│ 文件系统钩子却无视 memcg │ 操作整个文件系统或单超级块状态
└───────────────┬────────────────┘
▼
┌────────────────────────────────┐
│ 逐组回收反复驱动同一份全局回收 │ 忙碌主机一轮数百次调用
└────────────────────────────────┘
这套混搭在 memcg 密集的 slab 回收下暴露出两类代价:
- CPU 白烧:逐组回收会对每个 memcg 与 NUMA 节点的组合各调用一次,忙碌主机上一轮回收能跑出数百次调用;可它们排队等待的,全是 root 路径早已踢起的同一份全局回收工作。
- 回收位清不掉(更隐蔽):btrfs、xfs 的全局缓存对象计数在任何正在使用的文件系统上几乎总不为零,即便某个 memcg 自己的 dentry、inode LRU 已经空空如也,回收接口返回的对象总数仍是正的。于是逐组回收路径始终以为该 memcg 还有东西可收,永远不去清掉它在超级块 shrinker 位图里占的那一位;下一轮回收又原路重入计数,再付一遍全局计数器的遍历开销。另外,xfs 的 inode 回收既然不按 memcg 过滤,理论上还可能替别的 memcg 回收掉它的 inode。
问题
- 文件系统全局钩子不感知 memcg,却在逐组回收路径被反复调用
- 全局缓存计数恒正,回收位始终清不掉,下一轮重入再付全局遍历
- 忙碌主机一轮回收数百次冗余调用,烧 CPU 走全局计数器、排重复工作
- xfs inode 回收不按 memcg 过滤,可能替他组回收 inode
方案
整套修复只立一条原则:让这些全局钩子回到它们真正起作用的全局回收路径。
做法是给钩子调用加一道 memcg 闸门:只有回收目标为空(即全局路径)或指向 root memcg 时才放行,其余一律跳过。无论清点计数还是实际扫描,调用钩子之前都先过这道闸门:凡是非 root memcg 的 slab 回收,一律绕开这些文件系统全局钩子。
加一道 memcg 闸门,按回收路径分流
┌────────────────────┐
│ 收到 slab 回收请求 │
└─────────┬──────────┘
┌────────────────┴────────────────┐
▼ ▼
┌────────────────────────────┐ ┌─────────────────────────┐
│ 非 root memcg 跳过全局钩子 │ │ root 或全局路径放行钩子 │
└────────────────────────────┘ └─────────────────────────┘
memcg 感知的 dentry、inode LRU 不受影响,仍按目标 memcg 逐组计数和扫描;被跳过的只有那些全局文件系统钩子。而全局路径(kswapd 或 root 直接回收)依旧驱动它们,本来真正产生回收的也只有这条路径。如此既免去逐组回收路径上对 per-cpu 计数器的无谓遍历和重复排队,也堵住了 xfs inode 回收替他组回收的副作用。
收益
作者未提供定量 benchmark,仅定性描述 memcg 密集 slab 回收下的开销(忙碌主机一轮回收数百次调用、反复遍历 per-cpu 计数器并排队重复工作)。从机制推断的预期收益:
- 消除逐组回收对全局钩子的冗余调用(原本每轮回收、每个 memcg 与 NUMA 节点组合各 1 次)
- 非 root memcg 下超级块 shrinker 位能正常清零,不再因全局计数恒正而重入计数、再付全局遍历
- 堵住 xfs inode 回收在非 root memcg 回收下替他组回收 inode 的副作用
- 全局路径行为不变,钩子照常驱动;memcg 感知的 dentry、inode LRU 照常逐组计费
这是权宜之计,未来 xfs、shmem 的回调应具备真正的 memcg 感知。