回收生产者
每轮回收更新代价
lru_lock
2 个标量都在锁下
anon_cost / file_cost
均衡启发式的输入
get_scan_count()
决定 anon/file 扫描量
更新本身不大,却和真正的 LRU 操作争同一把锁
lru_lock 是内存压力工作负载上的常见瓶颈
扫描量由代价驱动,代价的每次更新却要回到锁里
这就是要搬走记账的起点
代价记录散落在回收路径的多个出口
回收不活跃页之后
活跃页轮转之后
缺页重新引用之后
每个出口都要重新取 lru_lock,只为记 2 个标量
记账还沿 memcg 层级向上传播
leaf lruvec
取锁
parent lruvec
再取锁
… 祖先 …
逐层取锁
开销按深度成倍放大
记账本身不贵,贵在每次都要碰 lru_lock
扫描前还要锁下快照 → 竞争被进一步放大
代价不再由生产者写,而由消费端从 vmstat 读
旧:写侧承担全部记账
生产者累计代价
每轮回收更新
lru_lock 下记账
每个出口重新取锁
逐层向上同步
按 memcg 深度放大
新:读侧从 vmstat 派生
回收事件
记成 per-LRU vmstat
扫描前无锁读取
折算成均衡代价
cost_lock 序列化累加与检查
不再与 LRU 操作争锁
生产者不再为记代价获取任何锁
换出
NR_VMSCAN_WRITE
writeout() 成功时累加
经 lruvec_stat_mod_folio 计入 lruvec 与 memcg
只贡献匿名代价
轮转
PGROTATE_ANON
PGROTATE_FILE
新增计数器
每次轮转累加
重引用
WORKINGSET_RESTORE
原本就存在
refault 时累加
三者按 per-LRU vmstat 计数使用;换出热路径新增 memcg stat 记账,node 级总量保留
PGROTATE_* 在 classic LRU 与 MGLRU 中都会更新
MGLRU 走 generation 老化,本身不消费这个 cost 信号
每轮扫描开始前,对每个 LRU 求 2 个单调计数的增量
上次快照
本次计数
Δ
io Δ × SWAP_CLUSTER_MAX + rotate Δ
anon / file 累加器
作为该侧均衡代价
轮转增量与加权后的 io 增量相加,折入对应一侧;生产者不取锁
差分必须用非钳位读法
旧:signed long 越 LONG_MAX 会被钳到 0
32 位无符号减法下溢,吐出约 2^31 伪差
新:返回 unsigned,增长不超 unsigned long 时模减法正确
衰减也搬到回收侧
观察两次回收之间的完整差
按比例衰减 anon 与 file
修正空闲期事件互相挤占
per-lruvec cost_lock
序列化累加器 + lrusize/4 减半检查
隔离 / 搬移 / 新增 LRU 页不碰它
持续回收时也不与真正的 LRU 操作抢锁
层级聚合交给 rstat
vmstat 沿 memcg 层级自然传播
不再逐层取锁,也不用 reparent 拼接
读到的值有 flush 延迟,但与周围启发式共用 rstat 视图
固定开销
每侧新增 1 个 struct lru_cost(count、last_rotated、last_io)加 cost_lock
per-lruvec 净增 4 个 unsigned long 加 1 个 spinlock · 小额开销,与机制正交
更新代价不再回到 lru_lock,记账路径上的竞争随之消失
层级传播交给 rstat;逐层取锁与 reparent 拼接都不需要
176 核 · 256 GB · usemem -n 16(256 MB × 16 线程)· memory.max=512M · 16 GB swap
4 GB 匿名工作集挤进 512 MB,持续触发不活跃页回收与重引用 · 30 秒 perf lock record -a
速率不变:pgscan_direct 约 172K/s · pgsteal_direct 约 67K/s · workingset_refault_anon 约 40K/s · 基准版与补丁版均约 0% 偏差
Before
After
shrink_lruvec
(lru_note_cost_unlock_irq)
0
722.84 ms → 0 · -100%
workingset_refault
(lru_note_cost_refault)
0
385.26 ms → 0 · -100%
shrink_node
689.43 ms → 26.95 ms · -96%
shrink_active_list
208.34 ms → 15.97 ms · -92%
lru_add_drain_cpu
1.96 s → 917.71 ms · -53%
Total LRU lock wait
~4.23 s → ~1.66 s · -61%
2 个竞争点归零;其余为连带下降 · 剩余 ~1.66 s 主要在 per-CPU pagevec 排放与 shrink_lruvec 主路径
锁下记账
生产者更新
出口散落 / 层级放大
memcg 逐层取锁
vmstat 计数器
事件变 per-LRU 计数
读侧无锁 + cost_lock
锁竞争下降
总等待 ~4.23 s → ~1.66 s · -61%
2 个竞争点直接归零;其余为连带下降
把记账从写侧搬到读侧,锁竞争随之下降
剩余瓶颈回到 per-CPU pagevec 排放与 shrink_lruvec 主路径
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x