背景
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 路径开销回到区间外水平。