背景

zswap 把压缩后的 swap 页放在内存池里,池满时由 global shrinker(shrink_worker())把较冷的压缩页 write back 到 swap 设备、腾出池空间。真正干活的是 shrink_memcg():它遍历某个 memcg 在各节点 zswap LRU 上的 entry,把可回收的 write back 出去。但旧实现里 shrink_memcg() 每个节点每轮最多只 write back 1 个 entry(list_lru_walk_onenr_to_walk 固定为 1)。于是 shrink_worker() 每被唤醒一次,只能挤牙膏式地把各节点推进一步,要取得实质进展必须反复重新进入 shrink_memcg(),期间伴随大量 worker 唤醒与函数调用开销。

shrink_memcg 单 entry,shrink_worker 反复 re-enter
┌──────────────────────────────────────┐
│ shrink_memcg 每节点每轮最多 write    │
│ back 1 个 entry                      │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ shrink_worker 必须反复 re-enter 才有 │
│ 实质进展                             │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 海量重复调用与唤醒,writeback 效率低 │
└──────────────────────────────────────┘

问题

  • shrink_memcg() 每节点每轮最多 write back 1 个 entry。
  • shrink_worker() 必须反复 re-enter 才有实质进展。
  • 大量重复的 worker 唤醒与函数调用,writeback 效率低下。
  • writeback 跟不上 refault 时 zswap store 失败,页绕过 zswap 直落盘,造成 LRU inversion。
  • memcg 禁用时 global shrinker 完全空转,池满只能靠 swapin 或释放自然排空。

方案

放宽 shrink_memcg() 的每节点单轮扫描上限,从 1 个 entry 到 SWAP_CLUSTER_MAX(32 页),单次调用批量 write back。

实现不新增参数也不新增宏:per-node LRU 的 nr_to_walk 直接取 SWAP_CLUSTER_MAX,shrink_worker()zswap_store() 两条回收路径同样批量。每个节点独立扫自己的上限,节点间不共享一份全局扫描配额,避免多 NUMA 节点下各节点抢同一份预算造成 writeback 不公平。

LRU 迭代沿用既有 second-chance 逻辑,referenced 的 entry 旋转到队尾,保证每个 entry 单轮至多被扫一次,扫满上限即停。

系列同时修了相关 bug:mem_cgroup_disabled()mem_cgroup_iter() 总返回 NULL,shrink_worker() 一直走 !memcg 分支、重试 MAX_RECLAIM_RETRIES 次后放弃,根本 write back 不了;修复是在该分支加 !mem_cgroup_disabled() 条件,让 memcg 禁用时 fall through 去 shrink root memcg。-ENOENT 返回路径同时由 continue 改为先做 reschedule 检查,避免重并发 store 下长时间不让出 CPU。

批量 writeback 消除反复 re-enter
┌────────────────────────────────────────────────┐
│ 每节点单轮扫描上限从 1 放宽到 SWAP_CLUSTER_MAX │
└───────────────────────┬────────────────────────┘
                        ▼
┌────────────────────────────────────────────────┐
│ shrink_worker 与 zswap_store 同样批            │
│ 量                                             │
└───────────────────────┬────────────────────────┘
                        ▼
┌────────────────────────────────────────────────┐
│      单次调用批量 write back 不再反复进入      │
└────────────────────────────────────────────────┘

收益

作者在 32GB 单 NUMA 节点机器上评估,zswap 配置 accept_threshold_percent=50shrinker_enabled=N,运行 120s。Case 1 设 max_pool_percent=1,先分配 512MB 随机数据匿名页(避免压缩)用 memory.reclaim 压进 zswap,再每 2ms 分配 4K 匿名页并触发回收,让池达到阈值反复触发 shrink_memcg()

指标 Baseline Patched
shrink_worker wakeups 5,363 169(-96.8%)
shrink_memcg calls 11,373,201 350,703(-96.9%)
written_back pages 40,212 40,241
zswap_store 拒绝率 ~36% ~28%
pswpout 98,659 86,811(-12.0%)

Case 2 用 stress-ng 在 memory.max=1G 的 cgroup 内持续施压:2a 设 max_pool_percent=1 触发全局池限,唤醒 shrink_worker() 异步回收;2b 设 zswap.max=320Mmax_pool_percent=50 触发 cgroup 限额,走 zswap_store() 同步回收。

Case 2a:

指标 Baseline Patched
shrink_worker wakeups 5,640 1,308(-76.8%)
shrink_memcg calls 8,481,500 3,140,972(-63.0%)
written_back pages 260 468,216
zswap_store 拒绝率 ~66% ~52%
pswpin 4,288,497 3,635,365(-15.2%)

Case 2b:

指标 Baseline Patched
shrink_memcg calls 687,608 54,002(-92.1%)
written_back pages 639,176 846,663(+32.5%)
zswap_store 拒绝率 ~19% ~2%
pswpin 1,707,823 1,216,814(-28.7%)

Case 1 的 written_back 基本持平,说明批量只消除重复调用、不改变回收总量;压力更大的 2a 与 2b 里 written_back 明显上升、store 拒绝率与 pswpin 下降,说明批量回收跟上了 refault,页不再绕过 zswap 直落盘,LRU inversion 得到缓解。