背景

超级块 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 感知。