背景

memcg 按页 charge 的开销不低,per-cpu charge stock 用批量摊销来压住它:charge 1 页时直接批量 charge MEMCG_CHARGE_BATCH(64)页,多余的页数缓存进 per-cpu stock,后续的 charge 与 uncharge 都在 stock 内就地增减,只有 stock 不足或溢出才去触碰全局 page_counter。

per-cpu stock 的批量摊销
┌──────────────────────────────────────┐
│             charge 1 页              │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 批量 charge 64 页,63 页缓存进 stock │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 后续 charge 与 uncharge 在 stock 就  │
│ 地增减                               │  不碰全局 page_counter
└──────────────────────────────────────┘

memcg 还有另一个 percpu 缓存 obj_stock,缓存的是 kernel 内存记账用的 obj_cgroup 指针,与这里的 charge stock 是两个独立设施,互不相干。

问题

  • MEMCG_CHARGE_BATCH 既当高水位又当清空目标,stock 计数越界即整体清空。
  • write charge 1 页后 stock 缓存 63 页,read uncharge 2 页计数到 65 即越界,下次 charge 重新未命中。
  • request/response 型负载的 write/read 交错恰好踩中该模式,stock 持续清空重建,完全失效。

方案

把高水位与清空目标拆开,计数越界只修剪不清空。

做法借用页分配器 per-cpu cache 的水位分离:页分配器的 free 路径把计数维持在 batch 与 high 之间,链表上始终至少留 batch 页。charge stock 新增 MEMCG_STOCK_HIGH(64,与 MEMCG_CHARGE_BATCH 同值)与 MEMCG_STOCK_LOW(32)两档水位,计数超过 HIGH 才触发修剪,且只把超出 LOW 的部分回充 page_counter,stock 里始终留存 32 页余量继续服务后续 charge。

上限保持与 MEMCG_CHARGE_BATCH 同值,每个 memcg 的峰值缓存页数不增,作者注明将来再评估是否调大。每批 charge 仍是 64 页,批量摊销语义不变。

水位分离后的 stock 修剪
┌────────────────────────────────────┐
│ charge 批量充入,uncharge 计数回升 │
└─────────────────┬──────────────────┘
                  ▼
┌────────────────────────────────────┐
│  计数超过 MEMCG_STOCK_HIGH 才修剪  │  高水位 64
└─────────────────┬──────────────────┘
                  ▼
┌────────────────────────────────────┐
│ 只回充超出 MEMCG_STOCK_LOW 的部分  │  低水位 32,stock 留存余量
└────────────────────────────────────┘

同样的 write/read 交错流量下,uncharge 2 页把计数抬到 65,越过上限但只修剪到 32,stock 不清空;此后每轮净攒 1 页,要再攒 32 轮才修第二次,stock 全程保持命中。

收益

作者给出的修复前观测数据(request/response 型负载):

指标 问题消息尺寸区间 区间外
memcg charge/uncharge 路径 CPU 占比 44.6%-57.0% 0.27%-0.71%

从 root memcg 运行(跳过 socket 记账)性能恢复,问题由此定位到 charge/uncharge 路径。作者未提供修复后的实测数据;从代码逻辑推断,该流量模式下 stock 持续命中,不再每轮 write/read 都触碰全局 page_counter,charge 路径开销回到区间外水平。