背景
MGLRU 用多代 LRU 管理回收,它的回收循环要把"老化"(aging,推进代)、"要扫描多少 folio"和"实际回收"几件事捏在一起做。这几件事相互耦合,加上脏页(dirty folio)的处理逻辑又和回收主循环差异较大,使得整条回收循环既难读,又让脏页冲刷(dirty flush)难以在合适的时机生效。其中一些问题是在生产环境暴露的。
┌────────────────────────────────────┐
│ MGLRU 回收循环复杂 │
└─────────────────┬──────────────────┘
▼
┌────────────────────────────────────┐
│ 老化、扫描量计算、回收循环互相耦合 │ 扫描量每次迭代重算,与老化、轮转纠缠
└─────────────────┬──────────────────┘
▼
┌────────────────────────────────────┐
│ 脏页冲刷游离在循环之外 │ 回收循环难跟随,脏页冲刷难以生效
└────────────────────────────────────┘
具体地,旧循环每次迭代都重新计算要扫描的 folio 数,而这个计算只在"不需要老化"或"默认优先级"时才按回收优先级移位,把扫描量计算和老化、轮转搅在一起。结果老化在某些本该立即推进的时刻被跳过,回收方白费一轮迭代直到优先级升级,过度回收 slab、破坏多 cgroup 场景下的回收平衡。
问题
- 老化、扫描量计算、回收循环相互耦合
- 扫描量每次迭代重算,缠着老化与轮转
- 脏页冲刷游离在回收循环之外,难以及时生效
- 部分问题在生产环境暴露,偶发意外 OOM
方案
系列用三条主线收拾这个循环。
旧:耦合,脏页冲刷游离
┌──────────────────────────────┐
│ 扫描量每次迭代重算,缠着老化 │
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ 脏页冲刷在循环外,难以生效 │
└──────────────────────────────┘
新:起始定扫描预算,脏页冲刷进循环
┌──────────────────────────────────────┐
│ 循环起始一次算清扫描预算 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐ 老化需要时立即跑,脏页冲刷在循环内及时
│ 老化与回收计算解耦,脏页冲刷移入循环 │ 触发
└──────────────────────────────────────┘
- 引入 scan budget:在回收循环起始就一次性算清这次要扫描多少 folio,不再每轮重算。
- 把老化从回收计算里解耦出来:扫描量始终尊重回收优先级,老化在该推进时立即推进,原先 DEF_PRIORITY 下即使已经无法 eviction 也跳过老化、空耗迭代,现在只在 eviction 仍可行时才允许这次跳过。
- 把脏页冲刷逻辑移进回收循环内部,让它在回收进行中就能及时触发,而不是游离在外、错过时机。
这三处相互关联,合起来让回收循环更清晰,脏页冲刷更有效,也为后续 MGLRU 改进铺路。
收益
作者在 48c96t、NUMA 2 节点、128G 内存、NVME 存储的机器上测试。
MongoDB YCSB workloadb(recordcount 20M、operationcount 6M、threads 32,95% read/5% update;10G cgroup、WiredTiger cache 4.5G、NVME、不用 SWAP 以隔离文件 LRU 回写路径):
| 指标 | MGLRU Before | MGLRU After | 变化 |
|---|---|---|---|
| Throughput (ops/sec) | 60,653.5 | 82,384.35 | +35.8% |
| workingset_refault_file | 12,904,916 | 7,128,285 | -44.7% |
| pgpgin | 165,366,622 | 113,170,693 | -31.5% |
MongoDB 吞吐显著提升,文件 refault 大幅下降,全程无 swap 介入。 在慢 IO(HDD、网络存储)上增益更大,部分 workload 超过 100%。其他常见 benchmark 无回归,LOC 减少,并减少了意外 OOM。