博客 > 硬件&操作系统&网络&DevOps > Linux
# 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 作为最终回退。