背景
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,进一步降低缓存抖动。