背景

内核把刚分配或刚解除映射的页先攒在 per-CPU 的 lru_add 批量里,等攒够或需要时一次性排空(drain)进 LRU。这个批量里有时混着"死 folio":它的 refcount 只剩下批量自己持有的那一份引用,换言之它已经没人用、迟早要被释放。

┌──────────────────────────────────┐
│ 排空时把批量里的 folio 加入 LRU  │
└────────────────┬─────────────────┘
                 ▼
┌──────────────────────────────────┐
│ 其中有死 folio,只剩批量引用计数 │  加入只为随即被移除,白拿两次锁
└────────────────┬─────────────────┘
                 ▼
┌──────────────────────────────────┐
│   占去 lruvec 锁竞争约四分之一   │
└──────────────────────────────────┘

按旧逻辑,死 folio 照样被排进 LRU,紧接着 folios_put_refs 发现它已是死页,又把它从 LRU 摘下释放。一来一回,一个本就不该进 LRU 的页白白占用了两次 lruvec 锁。Meta 在生产环境观测到,约 24% 的 lruvec 锁竞争就出在这种"加入只为移除"的死 folio 身上。

问题

  • lru_add 批量里混着只剩批量引用的死 folio
  • 死 folio 照样被排进 LRU
  • 紧接着又被摘下释放,白拿两次锁
  • 占 lruvec 锁竞争约 24%

方案

核心是在 drain 之前就把死 folio 拣出来,让它根本不进 LRU。

旧:死 folio 进 LRU 再移除
┌─────────────────────────────────┐
│       批量里混着死 folio        │
└────────────────┬────────────────┘
                 ▼
┌─────────────────────────────────┐
│ 加入 LRU 又立即移除,白拿两次锁 │
└─────────────────────────────────┘

新:排空前先过滤死 folio
┌──────────────────────────────┐
│ 冻结判定只剩批量引用的 folio │
└──────────────┬───────────────┘
               ▼
┌──────────────────────────────┐  过滤的 folio 绕过常规清理,须先清活跃与不可回
│  移出批量直接释放,不进 LRU  │  收标志
└──────────────────────────────┘

folio_ref_freeze 判定一个 folio 是否"只剩批量引用",是的话就判定它已死。把这些死 folio 移出 add 批量、放到一个临时批量里直接释放,省掉"加入 LRU 再摘除"的往返,也就省掉了那两次锁获取。

一处细节要小心:被过滤的死 folio 绕过了常规 lru_add 的清理路径,而它们身上可能带着 PG_active(或经 migration 路径设上的 PG_unevictable)。这些 flag 必须在释放前手动清掉,否则会带着脏标志回到页分配器。

收益

作者在 Instagram 生产 workload(高频短生命周期请求)上做 A/B(classic LRU,60 hosts each side,3 × 60s,95% CPU load),用 mm_lru_insertion tracepoint 采集 per-host LRU add:

指标 unpatched patched
dead folios/min 1,297,785(6.71%) 14(0.0001%)
total folios/min 19,341,986 19,039,996

patch 截获了几乎全部死 folio,每主机每分钟省下约 2.6M 次锁获取。系统级内存指标(patched 侧,95% CPU)也连带改善:

指标 变化
direct reclaim scanning -7%
allocation stalls -5.2%
compaction stalls -12.3%
page frees -4.9%

requests served/sec 与 p99 尾延迟无回归,更高 CPU 利用率(85%→95%)下两者均呈方向性改进。

需说明:测试用的是 classic LRU(非 MGLRU),结果反映的是 classic LRU 下的 lru_add drain 路径。