背景
zswap 的 shrinker 用 zswap_shrinker_count() 估算该回收多少 zswap 页。它内部调 mem_cgroup_flush_stats(),同步取全局 cgroup rstat 锁、flush 整个 cgroup 层级。
┌──────────────────────────────────────┐
│ 多 NUMA 多 kswapd 并发跑 reclaim │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 每个 shrinker pass 查 zswap 回收量 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 每次同步取全局 rstat 锁 flush 整个层 │
│ 级 │ 锁竞争爆炸 84% kswapd cycles 耗在此
└──────────────────────────────────────┘
在多 NUMA 大机上这成了瓶颈:每个 NUMA 各跑 kswapd 并发做回收,回收框架对每个 memcg-aware shrinker 轮次都要估算一次 zswap 回收量,每次都同步取全局锁、flush 整棵 cgroup 层级。锁竞争随之爆炸:Cloudflare 的 AMD EPYC 9684X(96 核 192 线程 12 NUMA)生产负载上,perf 看到 2.88% 的 kernel cycles 花在 rstat 锁(osq_lock)竞争上,而 84% 的 kswapd kernel cycles 耗在估算回收量并同步 flush 层级的路径上,根本没在做实际的页回收。
问题
- 回收量估算同步取全局 cgroup rstat 锁 flush 整个层级
- 多 NUMA 多 kswapd 并发时每个 shrinker pass 都全局锁 flush
- 2.88% kernel cycles 在 osq_lock 竞争、84% kswapd cycles 在该路径而非实际 reclaim
- 锁等待膨胀、memory PSI 升高
方案
回收量估算本身只是一个启发式近似:按压缩比把已用 pool 规模缩放出目标值,并不要求精确;真正把 zswap 页写回 swap 发生在随后的扫描阶段,所以这里用到的 stats 稍有陈旧完全可接受。
于是把同步 flush 换成限速 flush:只有当周期 2 秒的后台 flusher 落后满一个周期时才补刷一次。同一条回收路径上,扫描控制的准备阶段早已采用同样的限速策略,这里只是与之对齐。
旧:同步 flush 整个层级
┌─────────────────────────────────────┐
│ zswap 回收量估算同步取全局 rstat 锁 │
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ flush 整个 cgroup 层级 │
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 多 kswapd 并发锁竞争 │
└─────────────────────────────────────┘
新:ratelimited flush
┌──────────────────────────┐
│ 回收量估算只需 heuristic │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 改用 ratelimited flush │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 锁竞争回到 baseline │
└──────────────────────────┘
改动后同步锁竞争随之消失:rstat flush 的延迟与锁等待回到不开 zswap shrinker 时的 baseline,而 zswap shrinker 照常工作(pool size 仍受 max_pool_percent 约束)。
收益
平台:AMD EPYC 9684X(96 核/192 线程/12 NUMA)生产 zswap 负载;另 8 台生产 metal 的 eBPF 测量。下表为同硬件同负载下开/关 zswap shrinker 的 A/B 对照(展示同步 flush 造成的锁竞争):
| 指标 | shrinker=Y(开) | shrinker=N(关,baseline) |
|---|---|---|
| osq_lock kernel cycles(EPYC perf) | 2.88% | 0.00% |
| memory PSI | 1.58% | 0.57% |
| rstat 锁获取(eBPF,8 metal) | 248/s | 1.1/s(约 50-250x 差距) |
| rstat 锁等待(eBPF) | 1.04 s/s | 0.0017 s/s |
EPYC perf 另测得 84% 的 kswapd kernel cycles 耗在估算回收量并同步 flush 层级的路径上(而非实际的页回收)。打补丁后,shrinker=Y 机器的 rstat flush 延迟与锁等待降到 shrinker=N 的 baseline 水平,zswap shrinker 仍正常工作(pool size 受 max_pool_percent 约束)。