zswap 的 shrinker 用 zswap_shrinker_count() 估算该回收多少页
每个 shrinker 轮次
zswap_shrinker_count()
mem_cgroup_flush_stats()
同步取全局 cgroup rstat 锁,flush 整个 cgroup 层级
只想估个回收量,却要把整棵层级刷一遍
多 NUMA 大机上,这一步会变成瓶颈
下面看它怎么把锁竞争放大
多 NUMA 大机:每个 NUMA 各跑 kswapd,并发做回收
NUMA node · kswapd
NUMA node · kswapd
…
NUMA node · kswapd
全局 cgroup rstat 锁:每个 shrinker pass 都要抢一次
每个 NUMA 的回收轮次都走这条同步 flush
kswapd kernel cycles 的构成(EPYC perf)
84% 停在估算与同步 flush
84% 的 kswapd 周期没在做实际的页回收
锁等待膨胀,memory PSI 跟着升高
回收量估算本身只要一个启发式近似
按压缩比缩放出目标值
把已用 pool 规模按压缩比缩放
只是一个估计,不要求精确
真正写回发生在随后的扫描
zswap 页真正写回 swap 在扫描阶段
这里 stats 稍有陈旧完全可接受
既然只要近似,就没有理由每轮同步精确 flush
陈旧一点的 stats 只会让估算略偏,不影响正确性
方案:同步 flush 换成限速 flush
旧 · 每次 pass 同步 flush
抢全局锁,刷整棵 cgroup 层级
新 · ratelimited flush
落后满一个周期才补刷一次
后台 flusher 每 2 秒刷一轮
估算只在落后满一个周期时补刷,其余时候直接用现有 stats
同一条回收路径上,扫描控制的准备阶段早已用同样的限速策略
flush 频率有了上限,估算不再每轮抢锁
这次改动只是与扫描控制阶段的做法对齐
A/B 对照:同机同负载,只差 zswap shrinker 开关
AMD EPYC 9684X:96 核 / 192 线程 / 12 NUMA 的生产 zswap 负载
osq_lock kernel cycles(EPYC perf,同一量程:满格 = 2.88%)
shrinker=Y
2.88%
shrinker=N
0.00%
memory PSI
shrinker=Y
1.58%
shrinker=N
0.57%
同硬件同负载,唯一差别是 shrinker 开关
2.88% 到 0.00% 的差距,就是同步 flush 带来的锁竞争
eBPF 侧的对照:8 台生产 metal 上的锁获取与锁等待
rstat 锁获取(eBPF)
248/s
→
1.1/s
约 50–250x 差距
rstat 锁等待(eBPF)
1.04 s/s
→
0.0017 s/s
锁等待几乎消失
rstat flush 的延迟与锁等待回到 baseline
zswap shrinker 照常工作,pool size 仍受 max_pool_percent 约束
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x