背景
memcg 逐页计费开销大,内核按批预收:try_charge_memcg() 一次向 page_counter 收 MEMCG_CHARGE_BATCH(64 页),用不完的余额由 refill_stock() 存进 per-CPU 的 memcg_stock_pcp,后续 charge 先由 consume_stock() 吃余额,吃中就不必碰全局计数器。
每个 CPU 的 stock 有 7 个 slot(NR_MEMCG_STOCK),选 7 是为让指针数组与余额数组挤进一条 cacheline。每个 slot 记一对信息:cached[] 指向 memcg,nr_pages[] 记余额页数。slot 的占用与释放都伴着一次 css 引用操作:
- refill_stock() 占 slot 前 css_get() 拿引用:同 memcg 的 slot 直接累加余额,否则找空 slot,找不到就按 drain_idx 轮转挑一个 drain 掉腾位
- drain_stock() 清 slot:余额退回 memcg 的计数器,css_put() 放引用,cached[] 置 NULL
- memcg offline 时 drain_all_stock() 扫过所有 CPU,把有余额的 slot 清掉,引用放完 memcg 才真正释放
判据只有一条:drain 路径只认 nr_pages[] 非零的 slot。
consume_stock() 把余额减到 0 就停手,cached[] 仍指着 memcg,slot 成了空壳:余额为零,引用还在。空壳不触发 drain,css 引用只剩两条偶然释放路径:被无关 memcg 的 charge 轮转挤出,或者 CPU 下线。
余额吃到 0,引用仍滞留
┌────────────────────────────────────────┐
│ refill_stock() 占 slot 前 css_get() │
│ 拿引用,slot 记下 memcg 与余额 │
└───────────────────┬────────────────────┘
▼
┌────────────────────────────────────────┐
│ consume_stock() 吃余额,减到 0 时 slot │
│ 仍指着 memcg │
└───────────────────┬────────────────────┘
▼
┌────────────────────────────────────────┐
│ offline drain 只清理有余额的 slot, │
│ 空壳 slot 滞留 css 引用 │
└────────────────────────────────────────┘
问题
- consume_stock() 把余额吃到 0 时 cached[] 仍指着 memcg,css 引用不放
- drain 只清有余额的 slot,空壳的引用没有常规释放路径
- 引用只靠无关 charge 挤出或 CPU 下线偶然释放,memcg 滞留 dying
- 容器频繁创建销毁时,每 CPU 最多滞留 7 个 dying memcg
- 空壳 slot 不能被 refill_stock() 复用,新 charge 只能驱逐有余额的 slot
方案
把引用随 slot 清空一并释放:consume_stock() 把余额减到 0 时顺手 css_put(),cached[] 置 NULL。
stock_pages -= nr_pages;
WRITE_ONCE(stock->nr_pages[i], stock_pages);
if (!stock_pages) {
css_put(&memcg->css);
WRITE_ONCE(stock->cached[i], NULL);
}
余额归零即释放引用
┌───────────────────────────────────────────┐
│ consume_stock() 把 slot 余额吃到 0 │
└─────────────────────┬─────────────────────┘
▼
┌───────────────────────────────────────────┐
│ 顺手 css_put() 放引用,slot 指针置 NULL, │
│ 空壳不复存在 │
└─────────────────────┬─────────────────────┘
▼
┌───────────────────────────────────────────┐
│ refill_stock() 直接复用空 slot,drain │
│ 路径保持原样 │
└───────────────────────────────────────────┘
空壳状态就此消失,slot 只剩两种形态:有主且有余额,或者完全空闲。drain 路径不用改,refill_stock() 找空 slot 时复用腾出的位置,有余额的 slot 不再被无关 charge 轮转挤出去。
留着空壳唯一换来的是下次同 memcg charge 省一对 css_get()/css_put(),代价却是引用滞留与 slot 浪费两头。修复后每清空一个 slot 多一对引用计数操作,最多每 MEMCG_CHARGE_BATCH 页一次。
收益
作者未提供性能数据,从代码逻辑推断的预期收益:
- memcg 的 css 引用随 slot 清空即放,offline 后不再滞留 dying 状态
- 空出的 slot 可被 refill_stock() 直接复用,有余额的 slot 不再被 drain_idx 轮转误逐
- drain 路径无需识别空壳 slot,判据保持「有余额」一条