背景

memcg 的对象计费(obj_cgroup)原本一个 memcg 只有一个。commit 01b9da291c49 把它拆成 per-NUMA-node,每个 node 一个 obj_cgroup,这样 reparenting LRU folios 时能取 per-node 的 lru 锁,减少锁竞争。

┌──────────────────────────────────────┐
│ 同一 cgroup 的线程跑在不同 NUMA 节点 │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│    per-CPU stock 只缓存一个 objcg    │  线程切到另一节点,缓存的 objcg 就失效
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│   反复 drain 与 refill,stock 颠簸   │  实测 stress-ng.switch 吞吐回归 67.7%
└──────────────────────────────────────┘

但这个拆分碰到了 per-CPU 缓存。memcg 给每个 CPU 维护一个 obj_stock_pcp,缓存当前 CPU 最近计费用的 obj_cgroup 指针,避免每次计费都走慢路径。拆分之前一个 memcg 只有一个 obj_cgroup,缓存它就够了;拆分之后一个 memcg 有多个(per-node 一个),而 stock 还是只缓存一个。于是当同一 memcg 的线程跑在不同 NUMA node 上、轮番在同一个 CPU 计费时,缓存的 obj_cgroup 不停被换:先 drain 旧的、再 refill 新的,stock 在 drain 与 refill 之间反复颠簸(thrash)。kernel test robot 的 stress-ng.switch 测出 ops_per_sec 回归 67.7%。

曾有一个临时修复(d0211878ce06)把同 memcg 的 sibling per-node obj_cgroup 当等价做缓存查找,但那只是权宜,本意是 per-node kmem accounting 落地后 revert。

问题

  • obj_cgroup 拆成 per-node,一个 memcg 变多个
  • per-CPU stock 仍只缓存一个 objcg
  • 同 memcg 线程跨 node 计费时 stock 反复 drain/refill
  • stress-ng.switch 吞吐回归 67.7%

方案

核心是让 per-CPU stock 能同时缓存多个 objcg。

镜像 memcg_stock_pcp 已有的 multi-slot 模式:把 cached_objcg 与 nr_bytes 转成 NR_OBJ_STOCK(5)个 slot 的数组,consume/refill/account 时扫所有 slot,插入优先空 slot、满了才 round-robin 驱逐。这样一颗 CPU 能同时握住一个 memcg 的各 node 变体外加几个 sibling,跨 node 切换不再强制 drain。

旧:单 slot,跨节点颠簸
┌────────────────────────────────┐
│ per-CPU stock 只缓存一个 objcg │
└───────────────┬────────────────┘
                ▼
┌────────────────────────────────┐
│  切节点就强制 drain 再 refill  │
└────────────────────────────────┘

新:多 slot 共存,热路径单缓存行
┌──────────────────────────────────────┐
│     per-CPU stock 缓存多个 objcg     │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐  缩字段腾空间,consume 与 refill 热路径
│ 同 cgroup 各节点变体并存,无需 drain │  各落在一个 64 字节缓存行
└──────────────────────────────────────┘

要塞进多 slot 又不撑大 per-CPU 缓存占用,前段先缩小现有字段腾地方(指针换成 node_id、计数字段收窄到 16 位),核心 patch 再把单 slot 转成 5 slot 数组,并重排 struct 布局:经 pahole 验证,consume_obj_stock、refill_obj_stock 与 slab 计费这三条热路径在非 debug 64-bit 构建上各自只触碰一个 64 字节 cache line,冷字段落到第二行。临时修复 d0211878ce06 随之不再需要。

收益

本系列消除了 stress-ng.switch 上 67.7% 的吞吐回归:

指标 回归幅度
stress-ng.switch.ops_per_sec -67.7%

(测试场景:stress-ng.switch,同一 memcg 的线程跨 NUMA node 运行。)

机制上,multi-slot 让同一 memcg 的 per-node objcg 变体在 per-CPU stock 中共存,消除了跨 node 的 drain/refill thrash,这正是 67.7% 回归的根因。结构上,pahole 验证 consume/refill/account 热路径各只落一个 64 字节 cache line,进一步降低缓存抖动。