池满了,谁来腾地方?
应用进程
swap out 匿名页
zswap 内存池
压缩后的 swap 页
池满 → 触发回收
shrink_worker()
global shrinker
shrink_memcg() 逐个 node 回收
每个 NUMA node 一份 zswap LRU
node
zswap LRU entry
node
zswap LRU entry
node
zswap LRU entry
write back 到 swap 设备
腾出池空间
池满 → shrink_worker 唤醒 → shrink_memcg 逐 node 回写
list_lru_walk_one(..., nr_to_walk = 1)
shrink_memcg() 每 node 每轮的上限
每个 node 独立一份 LRU,一个周期里各前进 1 个 entry
node
zswap LRU
每轮 1 个
node
zswap LRU
每轮 1 个
node
zswap LRU
每轮 1 个
一轮每个 node 只走 1 个
LRU 上还排着大量 entry
反复 re-enter
shrink_worker()
唤醒 → 再进入
shrink_memcg()
每次只推进 1 个 entry
代价
海量重复的 worker 唤醒与函数调用
writeback 进度远远落后于消耗
因果链
回收太慢
每轮每 node 1 个
池持续满
refault 又占满
zswap_store() 失败
池里没有空间
页绕过 zswap
直落 swap 设备
热页本该留在池里
冷页
热页
期望
先被 write back
留在 zswap 池
实际
仍占着池
绕过 zswap 直落盘
该先走的冷页留下来,热页却直落盘
这就是
LRU inversion
bug 1 · memcg 禁用时 global shrinker 空转
mem_cgroup_iter() → NULL
走 !memcg 分支
重试 MAX_RECLAIM_RETRIES 次后放弃
一个 entry 也没 write back
修复 · 加一个条件
if (!memcg && !mem_cgroup_disabled())
memcg 未禁用才走重试,否则往下走
fall through → shrink root memcg
禁用时也能回收
池满不再只能等 swapin 或自然排空
bug 2 · -ENOENT 返回路径
旧:continue 直接重试
修复 · 先做 reschedule 检查
重并发 store 下及时让出 CPU
两个坑都在边角,不修就会「池满了,却回收不动」
每 node 每轮扫描上限
1
个 entry
→
32
个 entry
= SWAP_CLUSTER_MAX
一次调用批量 write back
旧 · nr_to_walk = 1
1 个
新 · nr_to_walk = SWAP_CLUSTER_MAX
32 个
32 倍:一次调用批量回写 32 个
每个 node 各有自己的上限
node
32 个
node
32 个
node
32 个
不共享全局配额 → 多节点下更公平
LRU 迭代沿用 second-chance
referenced 的 entry 旋转到队尾,单轮至多扫一次
referenced
entry
entry
entry
扫满 32 即停
两条回收路径同样批量
shrink_worker()
zswap_store()
都按 SWAP_CLUSTER_MAX 批量回收
每 node 独立配额 + second-chance:公平,且每个 entry 不重复扫
Case 1 · 池限反复触发 shrink_memcg()
32GB 单 NUMA · max_pool_percent=1 · 运行 120s
shrink_worker wakeups
5,363
169
−96.8%
shrink_memcg calls
11,373,201
350,703
−96.9%
pswpout
98,659
86,811
−12.0%
written_back pages
40,212
→
40,241
基本持平
zswap_store 拒绝率
~36%
→
~28%
written_back 基本持平
批量只消除重复的唤醒与调用,回收总量不变
省下的 96%+ 是重复的唤醒与调用
Case 2a · 全局池限 → 异步 shrink_worker()
stress-ng · memory.max=1G · max_pool_percent=1
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%
回收终于跟上了 refault
written_back 从 260 升到 468,216,不再是「几乎没回收」
拒绝率与 pswpin 同步下降
Case 2b · cgroup 限额 → 同步 zswap_store() 回收
zswap.max=320M · max_pool_percent=50 · memory.max=1G
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%
批量回收
每 node 每轮最多 32 个
worker 不再反复 re-enter
跟上 refault
压力下 written_back 上升
Case 2a 260 → 468,216
减少落盘
store 拒绝率最低 ~2%
pswpin −15.2% / −28.7%
批量只消除重复调用;Case 1 回收总量基本持平
压力更大时 written_back 随 refault 上升,回收终于跟上
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x