背景

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,判据保持「有余额」一条