背景

memcg 被 offline 后,它 LRU 上的 folio 要过继给父级,两条 LRU 实现各有入口:

  • 传统 LRU 走 lruvec_reparent_lru(),把子级列表 splice 进父级
  • MGLRU 走 lru_gen_reparent_memcg()

列表搬家的同时记账也跟着走:每个 lruvec 各 LRU 列表上的 folio 数记在 per-zone 的 lru_zone_size[] 里,folio 上下 LRU 时由 lruvec_add_folio()/lruvec_del_folio() 增减,reparent 把子级的计数贷记给父级,父级接手的 folio 一个不少。

问题出在子级那一份:贷记之后从不清零。reparent 完成时 folio->memcg_data 已改指父级,这些 folio 此后的计数变动全记在父级,子级留下的陈旧值再没有任何路径修正。dying 的 memcg 不会立即释放,mem_cgroup_iter() 照旧遍历它,shrink_lruvec() 也就照旧被调用。

reparent 复制 size 留下幽灵计数器
┌──────────────────────────────────────┐
│    memcg offline,folio 过继父级     │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│  列表 splice 进父级,size 贷记父级   │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 子级副本不清零,幽灵计数器再无人修正 │
└──────────────────────────────────────┘

问题

  • reparent 把 lru_zone_size[] 贷记父级却不清零子级,size 是复制而非搬移
  • shrink_lruvec() 拿幽灵计数器对空列表按 SWAP_CLUSTER_MAX 步长空转
  • MGLRU 的 count_shadow_nodes() 同样误读,shadow node 限额被超配
  • 单机实测 380 个幽灵计数器横跨 57 个 dying memcg,虚报 476 GiB 达内存 1.89 倍

方案

把 size 在 reparent 时从子级搬走:父级贷记多少,子级减去多少。

size 搬移而非复制
                        ┌──────────────────────┐
                        │ reparent 时搬移 size │
                        └──────────┬───────────┘
                ┌──────────────────┴──────────────────┐
                ▼                                     ▼
┌────────────────────────────────┐      ┌───────────────────────────┐
│ 父级加上 size,接手 folio 记账 │      │ 子级减去 size,计数器归零 │
└────────────────────────────────┘      └───────────────────────────┘

传统 LRU 与 MGLRU 两条 reparent 路径同改:子级某 zone 的 size 为 0 直接跳过,否则父级加上 size、子级减去等量。LRU_UNEVICTABLE 的 size 同样要搬:它的列表故意不 splice,lruvec_init() 把表头 poison 掉了,unevictable LRU 是虚构列表,folio 从不串上去,但 size 由 lruvec_add_folio()/lruvec_del_folio() 照常维护,这些 folio 此后同样记账到父级,不搬就留下同款幽灵计数器。

清零子级有个前提:folio 的 objcg 与其 node 必须一致。否则已 splice 进父级列表的 folio 在 objcg 的 node 完成 reparent 之前仍解析到子级 lruvec,lruvec_del_folio() 会把子级计数器减出下溢。

收益

作者未提供性能数据,从代码逻辑推断的预期收益:

  • dying memcg 的计数器随 reparent 归零,扫描器不再对空列表空转
  • MGLRU 的 shadow node 限额恢复按真实 folio 数计算
  • 父子双份记账消除,lruvec_lru_size() 读数恢复真实