背景
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_one 的 nr_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=50、shrinker_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=320M、max_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 得到缓解。