背景

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 约束)。