# Linux zswap 与快慢盘分层交换实践
> 下面是我和AI反复沟通,最终的解决方案,通过AI进行的整理复盘文档。其中有不少设计是我自己的,但AI进行了细化、边界控制和总结。
>
> 整理日期:2026-08-04
## 1. 背景与目标
> 一台 CPU 富余、物理内存有限,同时必须长期运行内存泄漏程序的桌面工作站调优记录。
这台机器的典型矛盾不是 CPU 性能不足,而是:
- CPU 长期利用率很低,即使运行生产力工具也很少超过 15%。
- 物理内存约 15 GiB,桌面程序较多。
- 有若干个不能关闭、不能修改且必须持续运行的黑盒程序,存在不严重但长期累积的内存泄漏。
- 系统盘 `/` 是 NVMe,容量和延迟表现较好。
- `/home` 位于约 1 TiB 的机械硬盘上,适合容纳大量冷数据,但随机 I/O 延迟很高。
- 希望在发生大量交换的情况下仍能较舒适地使用桌面进行工作。
最终目标:
1. 尽量使用富余 CPU 压缩冷匿名页,减少真实磁盘 I/O。
2. 降低突发性全局卡顿的发生频率。
3. 降低单次卡顿持续时间和波及程序范围。
4. 允许泄漏程序长期占用冷内存,并把它逐层沉降到廉价存储。
5. 保留 NVMe swap 作为低延迟高速层,把机械盘作为最终容量层。
6. 不因短期内存压力提前杀死桌面程序或黑盒程序。
## 2. 硬件和存储布局
本机相关配置:
| 资源 | 配置 |
|---|---|
| CPU | Intel Core i7-12700,CPU 资源长期富余 |
| 物理内存 | 约 14.8 GiB 可见内存 |
| 系统盘 | NVMe,Linux 根分区约 100 GiB |
| 数据盘 | SATA 机械硬盘,约 1 TiB,挂载到 `/home` |
| 快 swap | `/swap1.img`,32 GiB,NVMe,优先级 10 |
| 慢 swap | `/home/swap2.img`,64 GiB,机械盘,优先级 5 |
| 总 swap | 约 96 GiB |
实际 swap 配置位于 `/etc/fstab`:
```fstab
/swap1.img none swap sw,pri=10 0 0
/home/swap2.img none swap sw,pri=5 0 0
```
注意:当前 `/etc/fstab` 中的注释还写着 SSD 16 GiB,但实际文件已经重建为 32 GiB。注释不影响行为。
## 3. 最初的卡顿现象与诊断
问题表现为:
- 从一个程序切换到另一个程序时出现极端卡顿。
- 打开 Chrome 时尤其明显。
- 有时整个桌面长时间失去响应,而 CPU 利用率并不高。
故障现场采样:
```text
物理内存:约 14 GiB
已用内存:约 11 GiB
当时 swap:约 12 GiB
内存 PSI full avg10:约 17%
I/O PSI full avg10:约 54%
```
`vmstat` 显示系统正在发生换入,进程状态中出现多个不可中断磁盘等待 `D` 状态。`jbd2/sda1`、`localsearch-3` 和若干应用进程都曾等待机械盘 I/O。
结论:
> 主要问题不是 Chrome 或 CPU,而是内存回收、交换换入与机械盘随机 I/O 共同造成的 I/O 堵塞。
Chrome 启动和切换窗口时需要读取程序代码、缓存、图形资源和被换出的匿名页。如果相关页面落在机械盘,随机换入会拖住整个桌面。
## 4. zswap 的定位
zswap 是真实 swap 的压缩 RAM 缓存:
```text
匿名页准备换出
↓
预先分配真实 swap 后备槽位
↓
尝试使用 CPU 压缩并存入 zswap RAM 池
↓
zswap 无法接收或需要淘汰时,写入此前绑定的真实 swap 槽位
```
它用 CPU 周期换取更少的磁盘交换 I/O。对于 CPU 富余、RAM 较小、磁盘延迟昂贵的机器,这是非常合适的交换。
### 4.1 逻辑 swap 与真实磁盘占用
`swapon --show` 的 `USED` 是逻辑 swap 槽位占用,不是真实磁盘写入量。
zswap 中的页面必须先预留真实 swap 槽位,因此即使数据仍压缩在 RAM 中,对应设备也会显示为已使用。
例如:
```text
快盘 swap USED:32 GiB
其中可能有:
28 GiB 原始页面仍压缩在 zswap RAM
4 GiB 页面真正写入快盘
```
快盘显示满并不代表快盘已经物理写满。它表示快盘的所有逻辑槽位已经承诺给相应页面。zswap 淘汰这些页面时,仍可写入它们预留的快盘槽位。
### 4.2 zswap 不增加逻辑 swap 容量
zswap 是真实 swap 的压缩缓存,不是额外的 swap 设备。每个保存在 zswap 中的页面仍必须占用一个真实 swap 后备槽位。
因此本机的逻辑可换出上限仍是:
```text
32 GiB 快 swap + 64 GiB 慢 swap = 96 GiB
```
而不是:
```text
96 GiB 真实 swap + zswap 压缩池
```
例如 30 GiB 原始页面压缩成 10 GiB 保存在 zswap:
```text
zswap 实际占用 RAM:10 GiB
逻辑 swap 已使用:30 GiB
剩余 swap 槽位:66 GiB
```
系统总容量也不能写成 `RAM + swap + zswap`,因为 zswap 同时占用 RAM 和真实 swap 槽位,不能重复计算。整体可承诺匿名内存规模大致仍受 RAM 与真实 swap 容量约束;zswap 改善的是存放效率、延迟和磁盘 I/O,而不是内核可分配的 swap 槽位数量。
一个重要边界:如果 96 GiB 真实 swap 槽位已经全部被分配,即使 zswap 压缩池尚未达到 50% RAM 上限,也无法继续换出新页面。因此大容量真实 swap 同时也是 zswap 的后备地址空间。
### 4.3 后备槽位绑定
swap 设备优先级是在页面分配 swap 槽位时决定的,而不是在 zswap 淘汰时重新决定的。
假设快盘 32 GiB 优先级高于慢盘:
1. 前 32 GiB 逻辑换出页面优先绑定快盘槽位。
2. 即使页面实际保存在 zswap,快盘槽位也已被占用。
3. 快盘逻辑槽位满后,新换出页面绑定慢盘槽位。
4. zswap 淘汰页面时,绑定快盘的写快盘,绑定慢盘的写慢盘。
5. 淘汰时不会重新按照设备优先级选择目标。
所以真实分层不是严格的:
```text
所有页面 → zswap → 快盘 → 慢盘
```
而是:
```text
先按 swap 优先级绑定后备槽位
↓
再尝试将页面压缩保存在 zswap
↓
淘汰时写回各自早已绑定的设备
```
### 4.4 页面被访问后的重新绑定
慢盘绑定的页面被访问时,会从 zswap 或 swap 换入普通 RAM。旧 zswap 条目和槽位在引用归零后被释放。
如果该页面以后再次变冷并被换出,就会重新参与 swap 槽位分配。此时快盘有空闲槽位,它便会优先绑定快盘。
长期效果:
```text
访问驱动热页上浮
定时倾倒驱动冷页下沉
```
共享页和 swap cache 可能让槽位释放延迟,但不改变长期趋势。
## 5. 最终 zswap 配置
`/etc/default/grub` 的相关启动参数:
```text
lru_gen=1
zswap.enabled=1
zswap.compressor=zstd
zswap.max_pool_percent=50
zswap.shrinker_enabled=0
zswap.accept_threshold_percent=100
```
完整相关行:
```bash
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash lru_gen=1 zswap.enabled=1 zswap.compressor=zstd zswap.max_pool_percent=50 zswap.shrinker_enabled=0 zswap.accept_threshold_percent=100"
```
修改后执行:
```bash
sudo update-grub
```
### 5.1 参数解释
`zswap.enabled=1`
- 启用 zswap。
`zswap.compressor=zstd`
- 使用 zstd 压缩。
- 相比优先追求压缩速度的算法,zstd 更充分利用富余 CPU,换取更高压缩率和更少磁盘 I/O。
`zswap.max_pool_percent=50`
- 压缩池最多使用物理 RAM 的 50%。
- 本机上限约 7.4 GiB 压缩后数据。
- 若长期压缩比约 3:1,理论上可承载二十多 GiB 原始页。
`zswap.shrinker_enabled=0`
- 禁止内核仅因普通内存压力而主动把冷 zswap 页提前写回磁盘。
- 压缩池未达硬上限时,优先把页面留在压缩 RAM。
- 这是偏向“CPU 和压缩 RAM 优先,磁盘最后”的激进策略。
`zswap.accept_threshold_percent=100`
- 关闭 zswap 池满后的默认滞回。
- 池内一旦出现空间,立即继续接收新页面。
- 代价是池满后的持续高压场景可能出现更频繁的一进一出。
### 5.2 冷页会不会永久堵住 zswap
不会永久堵住,但关闭 shrinker 后,未满时确实允许长期冷页留在压缩池中。
达到池上限后,zswap会按 LRU 淘汰较冷页面到其绑定的后备 swap,为新页面腾出空间。
这意味着:
- 未满时:宁可保留冷压缩页,避免磁盘访问。
- 满后:按 LRU 淘汰冷页,接纳新换出页。
- 已经在真实磁盘上的页面不会被 zswap主动晋升,必须先被访问换入,之后再次换出才会重新进入 zswap。
## 6. sysctl 内存策略
配置文件:`/etc/sysctl.d/99-swap-optimization.conf`
```conf
vm.swappiness = 150
vm.vfs_cache_pressure = 50
vm.page-cluster = 0
vm.min_free_kbytes = 262144
vm.watermark_scale_factor = 50
```
应用配置:
```bash
sudo sysctl -p /etc/sysctl.d/99-swap-optimization.conf
```
### 6.1 `vm.swappiness=150`
Linux 的 swappiness 范围是 0-200,不是最高 100。
设置为 150 的目的:
- 更积极地将冷匿名页送入 zswap。
- 更多利用空闲 CPU 进行 zstd 压缩。
- 保留 Chrome、VS Code 等应用需要的文件缓存。
- 提前、渐进处理内存压力,避免到临界点集中回收。
这不会为了提高 CPU 利用率而无意义空转。只有出现适合换出的冷页时,CPU 才执行压缩工作。
### 6.2 `vm.vfs_cache_pressure=50`
- 倾向保留目录项和 inode 等 VFS 元数据缓存。
- 减少启动和切换大型应用时重新读取文件系统元数据的需求。
### 6.3 `vm.page-cluster=0`
- 关闭传统 swap 换入预读放大。
- 交互式程序唤醒时只读取真正需要的页,避免从机械盘顺带读取无关页面。
- 适合降低单次卡顿的持续时间和影响范围。
### 6.4 空闲内存和水位
```conf
vm.min_free_kbytes = 262144
vm.watermark_scale_factor = 50
```
- 保留约 256 MiB 基础空闲内存缓冲。
- 提前让 kswapd 在后台回收,尽量避免前台进程进入直接回收。
## 7. 快慢 swap 文件
```text
/swap1.img 32 GiB priority 10 NVMe
/home/swap2.img 64 GiB priority 5 HDD
```
swap 文件均为 `600 root:root`,签名正确,可由 `/etc/fstab` 在启动时恢复。
## 8. 定时冷页下沉脚本
脚本位置:
```text
~/GreenSoftwares/drain-fast-swap.sh
```
软链接:
```text
~/bin/drain-fast-swap
```
权限:`755`。
当前脚本:
```bash
#!/usr/bin/env bash
set -e
read -r swap priority size used < <(swapon --show=NAME,PRIO,SIZE,USED --bytes --noheadings | awk '
NR == 1 || $2 > highest { swap = $1; highest = $2; size = $3; used = $4 }
END { print swap, highest, size, used }
')
if [[ $(TZ=Asia/Singapore date +%u) -ne 7 ]] && (( used * 4 <= size * 3 )); then
exit 0
fi
printf "Restarting swap %s with priority %s\n" "$swap" "$priority"
sudo swapoff "$swap"
sudo swapon --priority "$priority" "$swap"
```
逻辑:
- 自动选择当前最高优先级的第一个已启用 swap。
- 新加坡时区周日无条件倾倒。
- 其他日期仅当快 swap 逻辑使用量严格超过 75% 时倾倒。
- 未达到条件时静默退出。
- 倾倒后按原优先级重新启用。
root crontab:
```cron
0 9 * * * /home/rick/GreenSoftwares/drain-fast-swap.sh
```
每天 09:00 检查,但通常不会执行实际迁移。
### 8.1 为什么倾倒有意义
内核不会主动在多个 swap 设备之间根据冷热程度重新平衡已绑定槽位。
快盘槽位可能被长期不访问、但仍存在于 zswap 的冷页占用。关闭快盘 swap 会解除这些绑定;慢盘仍启用时,无法继续留在普通 RAM/zswap 的页面最终绑定慢盘。
重新启用快盘后,新的或再次变热后重新换出的页面可优先绑定快盘。
代价:
- `swapoff` 是主动的大规模迁移,会制造计划内 I/O 峰值和短暂卡顿。
- 这是主动接受一次阵痛,以换取后续较长时间快盘槽位充足。
## 9. 完整 swap 洗牌实验
为了让此前已经落盘、不会自动晋升的旧页重新经过 zswap,执行过一次完整洗牌:
1. 关闭慢盘 swap。
2. 等待其页面进入 RAM、zswap 或快盘。
3. 重新启用慢盘,优先级 5。
4. 关闭快盘 swap。
5. 等待页面重新经过 RAM/zswap,剩余冷页沉降到慢盘。
6. 重新启用快盘,优先级 10。
命令顺序:
```bash
sudo swapoff /home/swap2.img
sudo swapon --priority 5 /home/swap2.img
sudo swapoff /swap1.img
sudo swapon --priority 10 /swap1.img
```
迁移过程中观察到:
- 机械盘读取是最慢阶段,I/O PSI 一度明显升高。
- zswap 压缩池从约 2 GiB 增长到 5 GiB 以上。
- zswap 承载原始页面达到约 16 GiB。
- 内存仍保留约 1-2 GiB 可用空间,没有触发内核 OOM。
- 快盘最终接近清空,慢盘承接逻辑冷页。
洗牌后的一个最终快照:
```text
快盘 /swap1.img:约 6 MiB,优先级 10
慢盘 /home/swap2.img:约 19.5 GiB,优先级 5
zswap 压缩池:约 5.6 GiB
zswap 承载原始页:约 16.3 GiB
```
随后应用继续运行,数值会动态变化。当前文档生成时,快盘已有约 1.1 GiB 新逻辑页,说明重开后新的换出页面已重新优先绑定快盘。
## 10. 压力测试
### 10.1 轻量测试
先使用临时程序分配并保持约 3 GiB 匿名内存,验证新冷页是否优先进入 zswap。
结果:
- zswap 从约 1.39 GiB 增长至约 1.70 GiB。
- 承载原始页从约 3.94 GiB 增长至约 5.32 GiB。
- 测试期间磁盘 swap-out 基本为零。
- I/O wait 通常为 0-3%。
### 10.2 12 GiB 动态分配与随机读取测试
编写临时 C 程序:
- 每次增加 64 MiB。
- 最多分配 12 GiB 匿名内存。
- 每页实际写入,避免只保留虚拟地址。
- 完成分配后随机读取 200,000 个页面。
- 统计随机访问平均和最大延迟。
结果:
```text
成功分配:12 GiB
随机读取:200,000 次
平均延迟:4.8 us
最大延迟:8.46 ms
CPU 空闲:通常仍为 86-94%
I/O wait:通常为 0-4%
```
测试期间:
```text
zswap 压缩池:约 1.70 GiB → 2.13 GiB
zswap 承载原始页:约 5.19 GiB → 6.56 GiB
pool_limit_hit:0
压缩失败:0
压缩率差拒绝:0
written_back_pages:没有增加
```
结论:
- 新增冷页优先进入 zswap。
- 随机读取主要来自普通 RAM/zswap 解压路径,而非机械盘。
- CPU 仍有大量余量,zstd 压缩没有成为瓶颈。
- 关闭 shrinker 后,没有因普通内存压力主动写回磁盘。
## 11. systemd-oomd
系统原先启用了 `systemd-oomd`。日志确认它曾因用户会话内存压力超过 50% 持续 20 秒而杀掉:
- `xdg-desktop-portal`
- 一个 tmux scope
这与本机目标冲突:本机准备了 96 GiB swap,明确希望用交换空间维持泄漏程序和桌面,而不是因短期 PSI 提前杀进程。
最终处理:
```bash
sudo systemctl disable --now systemd-oomd.service systemd-oomd.socket
sudo systemctl stop systemd-oomd.socket systemd-oomd.service
sudo systemctl mask systemd-oomd.service systemd-oomd.socket
sudo systemctl daemon-reload
```
最终状态:
```text
systemd-oomd.service:masked, inactive
systemd-oomd.socket:masked, inactive
```
没有发现 `earlyoom` 或 `nohang` 等其他提前杀进程工具。
系统仍保留内核原生 OOM killer。只有 RAM 与 swap 真正无法满足分配时,内核才会介入。相关配置保持默认且合理:
```text
vm.overcommit_memory = 0
vm.overcommit_ratio = 50
vm.panic_on_oom = 0
vm.oom_kill_allocating_task = 0
```
风险:关闭 userspace OOMD 后,极端容量耗尽前系统更可能经历长时间 thrashing,而不是提前牺牲进程。这是本机明确接受的取舍。
## 12. 无用文件索引器 localsearch-3
`localsearch-3` 是 GNOME/Tracker 文件内容索引器。它通过 XFCE 自启动项运行,并曾在故障现场处于机械盘等待状态。
本机文件搜索主要使用 Thunar 或 `find`,不依赖 GNOME 全局全文搜索,因此停止并对当前用户禁用它。
用户覆盖文件:`~/.config/autostart/localsearch-3.desktop`
```ini
[Desktop Entry]
Type=Application
Name=Tracker File System Miner
Hidden=true
```
没有卸载软件包,避免破坏 Nautilus 和相关包依赖。
## 13. 图形监控工具
原有 `xfce4-taskmanager` 可以查看进程,但缺少网络和磁盘 I/O 总览。最终补充:
### Mission Center
- 通过 Flathub system-wide 安装。
- 可显示 CPU、内存、NVMe、机械盘、网络接口、应用和服务。
- 首次启动完成 `magpie` 辅助采集程序授权。
- 当前安装版本记录为 1.2.0。
安装命令:
```bash
sudo flatpak install --system flathub io.missioncenter.MissionCenter
```
启动:
```bash
flatpak run io.missioncenter.MissionCenter
```
### XFCE Disk Performance Monitor
安装:
```bash
sudo apt install xfce4-diskperf-plugin
```
已添加到包含 System Load Monitor 和 Network Monitor 的 XFCE Panel 2。可在面板首选项中选择具体设备和显示方式。
## 14. 重启前验证
在修改 GRUB、zswap 和 OOMD 后进行过完整启动前检查:
- `/boot/vmlinuz-7.0.0-28-generic` 存在。
- `/boot/initrd.img-7.0.0-28-generic` 存在且 `lsinitramfs` 可正常读取。
- `grub-script-check /boot/grub/grub.cfg` 通过。
- GRUB 默认首项为 Ubuntu 7.0.0-28。
- 生成后的 `.28` 启动行包含全部 zswap 参数。
- `.28` 内核启用 `CONFIG_ZSWAP=y`。
- 根分区、EFI 和 `/home` 的 UUID 与文件系统类型验证通过。
- 两个 swap 文件签名正确。
- 默认启动目标为 `graphical.target`。
- LightDM 已启用并处于图形目标依赖链。
- 没有失败的 systemd 单元。
- `systemd-oomd` 的 mask 不在图形启动依赖链,不会阻止图形界面。
- 旧的 7.0.0-27 内核仍保留,可从 GRUB 高级选项回退。
## 15. 常用观察命令
查看内存和逻辑 swap:
```bash
free -h
swapon --show=NAME,TYPE,SIZE,USED,PRIO
```
查看 zswap:
```bash
grep -E 'Zswap:|Zswapped:|SwapCached:' /proc/meminfo
grep -H . /sys/module/zswap/parameters/*
```
解释:
- `Zswap`:压缩后实际占用的 RAM。
- `Zswapped`:zswap 当前承载的原始未压缩页面总量。
- `SwapCached`:当前已回到普通 RAM,但仍保留 swap cache/槽位的页面。
估算压缩比:
```text
压缩比 ≈ Zswapped / Zswap
```
查看 PSI:
```bash
cat /proc/pressure/memory
cat /proc/pressure/io
```
查看实时换入换出和 I/O wait:
```bash
vmstat 1
```
查看 zswap 内核计数,需要 root:
```bash
sudo grep -H . /sys/kernel/debug/zswap/*
```
重点字段:
- `pool_total_size`
- `stored_pages`
- `pool_limit_hit`
- `written_back_pages`
- `reject_alloc_fail`
- `reject_compress_fail`
- `reject_compress_poor`
## 16. 风险边界
这套配置不是通用最佳实践,而是针对本机目标的有意识偏置。
### 优势
- 充分利用长期闲置 CPU。
- 高压缩率显著减少真实 swap I/O。
- 保留应用文件缓存,有利于桌面切换和启动速度。
- NVMe 和机械盘形成容量、延迟分层。
- 定时释放快盘逻辑槽位,实现近似冷页下沉。
- 避免 OOMD 因短时压力提前杀桌面程序。
### 代价
- zswap 最多可占据约一半物理 RAM,会与应用和文件缓存竞争。
- 关闭 shrinker 后,冷压缩页在池未满时可以长期存在。
- `accept_threshold_percent=100` 在池满后的持续高压下可能增加抖动。
- 每次 `swapoff` 都会制造明显的计划内 I/O 峰值。
- 机械盘上的页面一旦突然变热,首次换入仍可能很慢。
- 关闭 OOMD 后,极端内存耗尽更可能表现为长期 thrashing。
- 最终容量真的耗尽时,内核 OOM killer 仍必须杀进程。
## 17. 最终评价
| 部分 | 评分 |
|---|---:|
| zswap 与 CPU 利用 | 9.5/10 |
| swap 分层 | 9.5/10 |
| 容量容错 | 9.5/10 |
| 桌面交互保护 | 8.5/10 |
| 泄漏程序适配 | 9/10 |
| 整体 | 9.3/10 |
扣分主要来自物理 RAM 容量、机械盘随机访问的客观延迟、池满后的潜在抖动,以及 `swapoff` 的计划内阵痛,而不是配置逻辑。
## 18. 核心经验总结
1. CPU 利用率低不代表系统没有性能瓶颈。桌面卡死经常来自内存回收和磁盘 I/O,而不是 CPU。
2. zswap 很适合 CPU 富余、RAM 有限、真实 swap 延迟高的机器。
3. `swapon USED` 是逻辑槽位占用,不能直接当成真实磁盘写入量。
4. zswap 页面在压缩前已经绑定后备 swap 设备,淘汰时不会重新选择设备。
5. 页面被访问并在以后再次换出时,会重新参与 swap 优先级分配,形成热页上浮。
6. 内核不会主动在多个 swap 设备间完成理想的冷热重平衡,定时 `swapoff` 可以实现冷页下沉和快盘槽位回收。
7. userspace OOMD 的目标是保持系统响应并提前牺牲进程,不适合所有进程必须存活且拥有超大 swap 的场景。
8. 调优必须同时观察 RAM、zswap、逻辑 swap、PSI、I/O wait 和具体磁盘,而不能只看 `free` 或任务管理器。
9. 压测应包含实际逐页写入和随机重新访问,否则无法验证 zswap 与真实换入延迟。
10. 所有激进参数都应理解失效边界,并保留真实 swap 和内核 OOM 作为最终回退。