背景

vm.defrag_mode=1 下,non-movable 分配只从 non-movable pageblock 取页,不侵占 movable 块。当 non-movable pageblock 耗尽时,请求进入 allocator slowpath,reclaim 和 compaction 被提升到 pageblock_order 运行,以生产整块 non-movable pageblock。

但 slowpath 的重试决策仍按请求 order 判定。order-0 请求的 reclaim 在 pageblock_order 运行时,一旦发现 zone 看起来 compaction_ready() 就提前返回,报告了进展(返回 1)却什么都没回收,还跳过了本应对 memory.low 保护的 cgroup 做回收的重试。compaction 在 pageblock_order 失败或被 defer。然后 should_reclaim_retry() 按 order-0 判定,把这个空进展当作真实进展,重置 no_progress_loops,请求继续重试。compaction 重试检查也按请求 order 判定,与 promoted order 的实际结果脱节。

promoted order 与重试决策脱节导致死循环
┌───────────────────────────────────────────┐
│   order-0 non-movable 请求进入 slowpath   │
└─────────────────────┬─────────────────────┘
                      ▼
┌───────────────────────────────────────────┐
│ reclaim/compaction 提升到 pageblock_order │
│ 运行                                      │
└─────────────────────┬─────────────────────┘
                      ▼
┌───────────────────────────────────────────┐
│ reclaim 发现 compaction_ready 提前返      │
│ 回,报告进展但没回收                      │
└─────────────────────┬─────────────────────┘
                      ▼
┌───────────────────────────────────────────┐
│ should_reclaim_retry 按 order-0 判定,    │
│ 进展重置 no_progress_loops                │
└─────────────────────┬─────────────────────┘
                      ▼
┌───────────────────────────────────────────┐
│ 无限重试,永远到不了 ALLOC_NOFRAGMENT     │
│ fallback                                  │
└───────────────────────────────────────────┘

Meta 生产机上(64G、defrag_mode、memory.low 覆盖大部分负载),95% 的 direct reclaim 是无产出的 order-9 运行,峰值 60k 次/秒。全天约 2 亿次 should_reclaim_retry() 调用中 no_progress_loops 始终为 0。空转的是 SLUB inode/dentry refill,表现为内存压力并触发 PSI OOM 杀掉业务与系统服务。

问题

  • reclaim 提升到 pageblock_order 后空进展被当作 order-0 的真实进展
  • no_progress_loops 被反复重置,order-0 请求无限重试
  • compaction 重试检查按请求 order 判定,与 promoted order 结果脱节
  • 请求永远到不了 ALLOC_NOFRAGMENT fallback,形成死循环

方案

让重试决策跟随 promoted order。新增 nofrag_promote_order() 在 slowpath 每次迭代统一计算 promoted order,传给 direct reclaim、direct compaction 和两个重试 helper。

重试决策跟随 promoted order
┌────────────────────────────────────────┐
│ nofrag_promote_order 统一计算 promoted │
│ order                                  │
└───────────────────┬────────────────────┘
                    ▼
┌────────────────────────────────────────┐
│ reclaim 进展不再重置 no_progress_loops │
└───────────────────┬────────────────────┘
                    ▼
┌────────────────────────────────────────┐
│ compaction 按 promoted order 检查结    │
│ 果                                     │
└───────────────────┬────────────────────┘
                    ▼
┌────────────────────────────────────────┐
│ COMPACT_SKIPPED 不再重试,可 fallback  │
└───────────────────┬────────────────────┘
                    ▼
┌────────────────────────────────────────┐
│ fallback 时重置重试计数器,获得完整    │
│ 预算                                   │
└────────────────────────────────────────┘

reclaim 方面:promoted 请求的进展不再重置 no_progress_loops,与 costly order 的处理方式一致。这样空进展会累积计数,最终触发 fallback 而不是无限重试。

compaction 方面:should_compact_retry() 按 promoted order 检查 compaction 结果。COMPACT_SKIPPED 时 promoted 请求不再重试,因为可以走 fallback。但 compaction 优先级下限和 COMPACT_SUCCESS 的重试上限仍按请求 order 判定,保证非 costly 请求在 fallback 之前仍有机会走到 COMPACT_PRIO_SYNC_FULL。

fallback 方面:走 ALLOC_NOFRAGMENT fallback 时重置 no_progress_loops 和 compaction_retries,让 fallback 尝试获得完整的重试预算。

收益

32G VM,defrag_mode,memory.low 下 inode churn 复现器:

指标 Before After 变化
should_reclaim_retry() 调用次数 63M 293k -99.5%
峰值内存压力(PSI some avg10) 99% 12% -87pp
文件创建速度 — — 提升 5.7 倍