背景

swap 子系统里,同步 I/O 设备(如 ZRAM)的 swapin 路径有一段历史代码:它可以绕过 swap cache,直接换入。是否绕过由一道 swap count 检查门控,只有引用计数为 1 时才绕过。这道门控让绕过在多数场景下并不生效。更麻烦的是,它把"是否绕过 swap cache"和"是否禁用预读、是否启用大块 swapin"绑在了一起:想对同步 I/O 设备总禁用预读,就得同时绕过 swap cache,而后者的代价(重复 I/O 与内存开销)又让人下不去手。

swapin 同步又是另一个包袱。多个缺页路径可能同时换入同一个 swap entry,内核靠 swap_map 里的 SWAP_HAS_CACHE 位做同步:谁先置位,谁就做实际的换入工作。这是一个粗糙的位锁实现,竞争失败者不知道是谁占住了 slot,只能反复睡眠一拍再重试,带来长尾和其它性能问题。

┌───────────────────────────────────────────┐  绕过被引用计数检查门控,多数情况下并不
│  同步IO设备的 swapin 路径可绕过换页缓存   │  生效
└─────────────────────┬─────────────────────┘
                      ▼
┌───────────────────────────────────────────┐
│ swapin 同步靠缓存占用位,先占者做实际换   │
│ 入                                        │  竞争者不知谁占用,只能睡眠一拍后重试
└─────────────────────┬─────────────────────┘
                      ▼
┌───────────────────────────────────────────┐
│     持久化与共享内存工作负载长尾严重      │  后台保存类负载首当其冲
└───────────────────────────────────────────┘

这两件事(swap cache 绕过、SWAP_HAS_CACHE 同步)耦合在一起,长久以来难以清理。直到 swap table(phase I 引入的 per-cluster 结构)就位,swap cache 本身的开销已可忽略,统一 swapin 路径、换掉 SWAP_HAS_CACHE 同步才变得既可行又能带来收益。

问题

  • 同步 I/O 设备换入绕过 swap cache,被引用计数检查门控,多数场景不生效
  • 预读禁用与大块 swapin 被门控卡住,无法独立生效
  • swapin 同步靠 SWAP_HAS_CACHE 位,竞争者只能睡眠一拍重试
  • 持久化与共享内存工作负载长尾严重

方案

系列把 swapin 路径收敛到 swap cache 一条路,并改用 swap cache 配合页锁来做同步,随后移除 SWAP_HAS_CACHE 位。

旧:绕过换页缓存,占用位同步
┌──────────────────────────────────────┐
│ 同步IO换入绕过换页缓存,被引用计数检 │
│ 查门控                               │  预读禁用与大块 swapin 也被同一道门控卡住
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│  占用位竞争者睡眠一拍重试,产生长尾  │
└──────────────────────────────────────┘

新:统一走换页缓存,缓存层加页锁同步
┌──────────────────────────────────────┐  预读对同步IO设备总是禁用,大块 swapin 总是
│      swapin 路径统一走换页缓存       │  启用
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 换页缓存配合页锁做同步,占用位彻底移 │
│ 除                                   │  竞争者直接等待页锁,不再睡眠轮询
└──────────────────────────────────────┘

统一 swapin 路径,拆除绕过门控

核心改动让同步 I/O 设备也不再绕过 swap cache,所有换入都走 swap cache。一旦绕过路径消失,那道 swap count 门控就可以拆掉,原本被它绑在一起的两件事反而成为纯粹的收益:

  • 预读对同步 I/O 设备从此总是禁用
  • 大块 swapin 对所有 swap entry 总是启用

之前这两者因为和 swap cache 绕过耦合、受 swap count 检查限制而时常无法生效。

swapin 同步改用 swap cache 与 folio 锁

新机制用 swap cache 层配合 cluster 锁与 folio 锁来同步换入:谁先把 folio 插进 swap cache,谁就做换入;其它竞争者因为 folio 在换入期间加锁,直接等 folio 锁即可。这取代了 SWAP_HAS_CACHE 位那套睡眠轮询,长尾的根源被消除。swap cache 状态改由 swap table 直接维护后,SWAP_HAS_CACHE 位再无用户,顺势移除;swap_map 也回归纯粹的引用计数。

收益集中在引用计数大于 1 的场景。后台保存(BGSAVE)靠 fork 与 COW 产生大量共享 swap entry,正是 SWAP_HAS_CACHE 同步长尾最严重的地方,换成 folio 锁后改善最明显。系列前序补丁搭建 swap_cache_alloc_folio 等 helper 与若干循环抽取的基础设施,收尾补丁清理不再需要的旧路径与标志位。

收益

作者在多组 workload 上对比(封面信数据)。

核心收益在持久化与 COW 工作负载。Redis/Valkey(valkey-server --maxmemory 2560M,redis-benchmark -t get,-r/-n 3000000,-d 1024,-c 12,-P 32,ZRAM 作 swap):

平台 场景 Before (RPS) After (RPS) 变化
ARM64 VM 1.5G 无持久化 460,475.84 451,943.34 -1.9%
ARM64 VM 1.5G 后台保存 311,591.19 371,379.06 +19.2%
x86_64 VM 4G 无持久化 306,044.38 309,645.44 +1.2%
x86_64 VM 4G 后台保存 102,745.88 125,313.28 +22.0%

后台保存场景提升约 20%(ARM64 +19.2%、x86 +22.0%);无持久化场景基本持平。作者指出这类提升适用于一切涉及共享内存与 COW 的工作负载。

其余 workload 与基线几乎一致:

workload 指标 Before After
内核构建(ZRAM,x86_64 4G,make -j48,32 次均值) system time 1379.91s 1364.22s
内核构建(ZSWAP+NVME,x86_64 4G,make -j48,32 次均值) system time 1822.52s 1803.33s
MySQL(sysbench 只读,ZRAM,512M memcg,3 次均值) qps 318,162.18 318,512.01
vm-scalability(usemem,模拟 PMEM swap,6 次均值) 总吞吐 5677.35 MB/s 5688.78 MB/s

作者诚实指出 ARM64 无持久化场景略降 1.9%:此时仍用 swap_map 跟踪引用计数,对某些架构带来额外缓存与 CPU 开销,后续阶段把 swap count 并入 swap table 后会改善。

整体上,对引用计数大于 1 的同步 I/O 设备工作负载提升约 20%,其余场景持平或基本一致。