LWN 上有关内存子系统现代化的话题每次都能牵动不少人的神经。这次围绕“虚拟交换空间”的讨论我理解下来其实是在回答一个老问题内存不够的时候我们除了把页面搬到磁盘还有没有更快、更聪明的路答案不仅有而且已经在内核里长住了很多年——zswap、zram 这两类机制就是典型的虚拟交换空间形态。它们把压缩和内存调度两条技术线捏在一起让 swap 不再是“慢”的代名词。这篇文章我会结合这段时间调参和排查线上机器的经历把机制讲透把配置路径写清楚把容易踩的坑也一并列出来。1. 先把问题摆清楚内存不足时系统在怎么挣扎1.1 换出页面的传统路径和它真正的成本老牌 Linux 的交换逻辑并不复杂当物理内存吃紧内核通过回收机制把一批不活跃的匿名页写入磁盘上的 swap 分区或者 swap 文件这个过程叫 swap out进程再次访问这些页面时缺页异常触发 swap in数据从磁盘读回内存。问题在于磁盘和内存之间存在数量级上的差距——DDR5 内存的访问延迟以几十纳秒计而普通 SSD 的随机读写延迟是以几十微秒甚至毫秒计的。中间差了上千倍哪怕挂的是 NVMe随机访问也远追不上内存。传统交换还有一个容易被忽略的细节它会把磁盘I/O的延迟直接引入进程的关键路径。应用访问已换出的内存时必须等待一次真实的磁盘读取完成。这个等待意味着整条线程阻塞在线服务场景下会直接体现在尾延迟上。对很多现代应用来说宁可减少物理内存占用也不愿意接受这种不稳定的长尾延迟。所以内核维护者一直在找一个平衡点既要保命在内存耗尽时释放空间又要降低伤害尽量减少阻塞和磁盘挫伤。虚拟交换空间就是在这一背景下被反复打磨的。1.2 虚拟交换空间的两种主流形态目前内核中被称为虚拟交换空间的主流方案主要有两个zswap 和 zram。它们思路的出发点不同但都指向同一个目标——把压缩带进交换路径。zswap 是一个透明的后端缓存。它挂在内存回收路径上当一个页面即将被换出时先不急着写磁盘而是尝试压缩后放进内存里的一个池子。只有池子满了或者内存压力继续增大才把页面真正回写到磁盘上的交换设备。对进程来说它拿到了空闲内存对系统来说很多“瞬时换出”其实只是从一份未压缩页变成一份压缩页根本没碰磁盘。zram 则是另一条路它把一段内存模拟成一块块设备上层 mount 或 swapon 的设备号就是它。数据写入这个块设备时会被压缩保存。zram 充当交换设备时swap in/out 都在内存里完成磁盘直接缺席。把这两者放在一起很多人会觉得重复但实际在调度语义上并不一样这一点我在后面选型部分会展开讲。2. 内核怎么把“换出”这件事做快的2.1 zswap 在回收路径上横插一脚在经典回收流程里匿名页要换出时直接交给块层写盘。有了 zswap 后回收路径多了一个检查点先把页压缩、存进 zpool内存分配池同时保留该页对应的 swap slot 但不立即写入磁盘。这样当系统后续要读回这个页面时如果它还在 zswap 池里就直接从压缩内存解压返回完全绕过块设备和 I/O 栈。这个过程的关键在于 zswap 是“机会主义”的。它不会等到 swap 写盘才决定是否保留而是在写盘之前先抢占一次。如果遇到同一页多次被换出换入zswap 可以把往返磁盘的代价去掉只付出压缩和解压的 CPU 开销。压缩算法选得好CPU 开销相当有限却能把磁盘访问彻底消掉。内核维护者还做了几个限制条件来防止 zswap 变成内存黑洞zswap 池有容量上限默认是总内存的 20%超过阈值后会触发淘汰页面压缩效果太差比如压缩后几乎没缩小会直接拒绝存储按传统路径写盘分配失败或内存紧迫时也会拒绝避免回收路径越搞越复杂。2.2 folio 与大块映射带来的新交换实现最近这些年的内核内存管理出现了一个重要基础改动就是 folio。以前内核处理内存映射的单位是一个 4KB 的页一个 64KB 的映射就要拆成 16 个独立页来管理folio 允许把这些物理上连续的页捆绑成一个逻辑单元处理。这个改动不仅影响文件页匿名页的处理和交换路径也一并被重写了。在旧模型里交换一页只要一个 swap entry 就能定位在大 folio 模型下一个 folio 可能对应多个连续的 swap entry交换逻辑必须保证这些 entry 是连续的换出换入时需要整块处理。好处是分摊了元数据操作批量申请和释放 swap slot 的成本更低坏处是碎片化管理更复杂。LWN 上那几篇文章里反复讨论的正是这种“按块交换”如何影响压缩缓存的效率——zswap 可以一次压缩一个大对象压缩率通常比单独压缩多个小页更友好节省的空间也更可观。这也是我最近在 6.x 内核上观察到的一个趋势换出路径越来越像一个流水线每一步都尽量批量化和对象化而不是碎片化地逐页处理。2.3 zpool、压缩器与可调参数zswap 的实现里zpool 是一个值得细看的组件。它解决的是压缩后页面的存放问题不同页面压缩后大小不一如果按固定槽位分配内存利用率会非常难看。zpool 下面的 zsmalloc 分配器允许存放任意大小接近实际压缩后大小的对象把碎片浪费控制在很低的水平。而早期用的 zbud 只能存 1 到 2 个压缩对象容量有限现在已经不是默认选择。压缩器的选型也直接影响效果。内核里 zswap 支持的压缩器有 lzo、lz4、lz4hc、zstd、deflate 等。zstd 压缩率高但 CPU 开销略大lz4 吞吐极高、压缩率一般lzo 是中间派。在大多数服务器场景我建议优先 zstd——它带来的解压速度足够快而压缩率折算出来的内存节省往往更值钱。毕竟换一个可存储页的收益要让给压缩率。内核模块参数里还有 max_pool_percent它限制 zswap 最多占用多少比例的可用内存。默认 20% 通常够用但如果你的机器内存很大而且 swap 设备很慢可以适度调高到 30~40%。调高意味着你愿意给压缩缓存更多内存来换取磁盘几乎不出现写 I/O。3. 从零启用虚拟交换空间的完整操作3.1 先确认你的内核和模块状态动手之前第一件事是确认内核编译时有没有把 zswap 编进去。现在的发行版内核大多默认开启 zswap 支持但有些裁剪过的内核会去掉它。判断方法很直接ls /sys/module/zswap cat /sys/module/zswap/parameters/enabled如果第一行能看到目录说明模块在或者已经预留参数空间如果enabled文件存在且输出为N说明支持但没启用。若目录完全不存在就得确认内核配置选项CONFIG_ZSWAP是否打开。zram 的判断则是看有没有对应的块设备驱动modprobe zram ls /sys/class/zram-control/能加载且出现zram-control说明内核支持。3.2 用引导参数或运行时开关启用 zswapzswap 的启用方式很灵活既可以在内核引导参数里指定也可以运行时修改参数。推荐生产环境用引导参数固定行为避免启动过程中发生意外回退。常用写法zswap.enabled1 zswap.compressorzstd zswap.zpoolzsmalloc zswap.max_pool_percent30改完引导参数后需要更新 grub 配置并重启这个不用我多说。如果只是临时验证或者测试用 runtime 开关echo 1 /sys/module/zswap/parameters/enabled echo zstd /sys/module/zswap/parameters/compressor echo zsmalloc /sys/module/zswap/parameters/zpool echo 30 /sys/module/zswap/parameters/max_pool_percent需要注意一点运行时改 zpool 和 compressor 只对之后进入的页面生效已经缓存住的旧页不受影响。对了zswap 本身不是交换设备它是在 swap 之上的缓存层所以依然需要一个已经激活的真交换设备否则 zswap 的写回无处可去。3.3 创建并激活一个 swap 文件或分区如果你的机器还没配置任何 swap最省事的方式是建一个 swap 文件。Linux 从 2.6 开始就支持 swap 文件现代内核配合 Btrfs 也支持在带 CoW 的文件系统上创建交换文件但稳妥起见建议还是放在 ext4/XFS 这种常规文件系统上。创建 8GB swap 文件的流程fallocate -l 8G /swapfile # 如果文件系统不支持 fallocate用 dd 替代 chmod 600 /swapfile mkswap /swapfile swapon /swapfile注意 fallocate 创建的文件本质上是稀疏的最好检查一下实际物理块是否真的分配了。稳妥派的做法是直接dd if/dev/zero of/swapfile bs1M count8192成本就是要等一会儿。然后建议把 swappiness 设在一个适合 zswap 工作的区间。zswap 希望我们愿意让匿名页进交换路径否则匿名页一直攒在内存里回收压力全压在文件页上反而体会不到 zswap 的效果。我的通常起点是 30~60具体看负载类型。3.4 zram 的创建方式用 zram 当交换设备需要先确定给 zram 分配多大内存。它不是一开始就占满的而是按需从内存里预留一部分动态增长。modprobe zram num_devices1 zramctl /dev/zram0 --algorithm zstd --size 4G mkswap /dev/zram0 swapon /dev/zram0这里--size 4G指 zram 能提供的最大可用空间。实际物理内存占用取决于数据的可压缩性比如压缩率 2:1按 4G 容量计算占用约 2G 内存。之前有读者问过我 zram 会不会把内存撑爆——只要你设的容量小于物理内存且在内存压力大的时候设置好 mem_limit风险就可控。zram 也可以通过 sysfs 限制实际内存占用上限echo 2G /sys/block/zram0/mem_limit3.5 开机持久化配置光在命令行里操作重启就白配了。两种方式我推荐 systemd 管理的做法把交换配置和模块参数全部写进 systemd unit 里。下面是一个简单的 swapfile 配置[Unit] DescriptionTurn on swapfile [Service] Typeoneshot RemainAfterExityes ExecStart/sbin/swapon /swapfile ExecStop/sbin/swapoff /swapfile [Install] WantedBymulti-user.targetzram 的持久化建议用发行版自带的 zram-generator 服务Fedora 上默认就有配置文件放在/etc/systemd/zram-generator.conf[zram0] zram-size min(ram / 2, 4096) compression-algorithm zstd这个工具的语法不同发行版有些差异但思路都是让 systemd 在开机时自动建 zram 设备、mkswap、swapon。比自己在 rc.local 里写脚本要稳得多。4. 观测、判断与调参它到底帮我省了多少4.1 从内核指标里读出真相zswap 和 zram 都提供了很细的观测接口问题是你得知道看哪儿。zswap 的调试数据一般在 debugfsmount -t debugfs none /sys/kernel/debug ls /sys/kernel/debug/zswap cat /sys/kernel/debug/zswap/stored_pages cat /sys/kernel/debug/zswap/written_back_pages cat /sys/kernel/debug/zswap/reject_compress_poor cat /sys/kernel/debug/zswap/pool_total_sizewritten_back_pages是真正落盘的数量它越大说明 zswap 池扛不住压力有大量数据被回写磁盘。reject_compress_poor是压缩率太低而被拒绝的页面数如果这个数字持续增长说明你的数据压缩收益不高有没有 zswap 差别有限。pool_total_size可以看池实际占用的内存和max_pool_percent对应起来换算基本上就能判断还留有多少余量。zram 的观测更明确看/sys/block/zram0/mm_stat或者用工具zramctl cat /sys/block/zram0/mm_statmm_stat里关键是原始数据总量、压缩后数据总量和实际内存占用。压缩率 原始总字节 / 压缩后总字节这个比值就能告诉你选对压缩算法没有。4.2 swappiness 到底该怎么设讲虚拟交换空间时swappiness 是绕不开的一项。它控制回收路径在匿名页和文件页之间的偏好。/proc/sys/vm/swappiness默认 60但这个值在 zswap 时代的意义已经被改写过。当 zswap 已经把交换路径压缩缓存化匿名页 swap out 的代价显著下降你不必害怕让匿名页参与回收。这也是为什么很多带 zswap 的部署里推荐把 swappiness 降到 10 或 30——不是为了让匿名页少换出而是为了避免文件页被过度回收导致 page cache 命中率下降。实际上这里有个更精细的取舍如果负载以读为主文件页回收过多应用重新读盘的成本很高匿名页换出到 zswap 的成本反而低所以 swappiness 要降如果负载以匿名内存为主且 zswap 池总是写满那继续降低 swappiness 也救不了你需要检查分配上限和压缩率。我建议先保持默认 60 观察看written_back_pages和缺页统计数据再决定是降 swappiness 还是提高 max_pool_percent。4.3 三个容易碰壁的场景第一类问题明明打开了 zswapwritten_back_pages还在疯涨。这通常说明 pool 上限设得太低或者当前可用内存太少。解决办法是先看pool_total_size是否接近max_pool_percent设定的上限如果是调高 max_pool_percent。如果池远没满却大量写回可能是 swap 设备本身经常被唤醒做 active 页回写这往往和 cgroup 的内存限制有关需要检查对应 cgroup 的 memory.max。第二类问题启用 zswap 后 CPU 占用升高。压缩毕竟要花 CPU。如果你的机器 CPU 核数少、主频低压缩开销可能超过磁盘 I/O 的收益。遇到这种场景换个更轻的压缩器比如 lz4 代替 zstd或者干脆把 zswap 关掉。注意观察一定周期内的用户态 CPU 和 si软中断时间不要盲目守着指标不放。第三类问题内存统计的困惑。启用 zswap 后free 命令显示的内存可能还是偏低。因为压缩缓存占据的内存是内核模块使用的不在普通进程 RSS 里体现。很多人因此误以为内存泄漏其实只要 pool_total_size 不持续增长就不是泄漏它只是把你“换出”的数据搬了个地方换了个形态存放。5. 选型、踩坑与我的实战建议5.1 zswap 和 zram 到底谁更合适一个场景一句话总结如果你靠真 swap 设备兜底用 zswap如果你希望彻底绕开磁盘用 zram。zswap 的优势是保留了真正的 swap 设备作为后盾极端内存压力下可以把冷页写盘内存不会被换出数据无限占据。它适合传统服务器上内存偶尔吃紧、但仍要保证有最后防线的场景。zram 的优势是快而且可控但它截断了换出数据落盘的路径一旦 zram 本身接近满系统只能靠 OOM 来释放内存风险阈值更陡峭。所以纯技术选型上没有绝对赢家。我见过嵌入式场景大量用 zram因为 RAM 固定且没有磁盘或 SSD也见过云主机上用 zswap因为云盘 I/O 性能和配额都需要精打细算。5.2 我看过的几个真实场景有一个线上服务内存 64G数据访问突发性强swap 设备是云盘性能一般。高峰期出现明显的 swap in 阻塞。启用 zswapzstd 压缩pool 上限 30%swappiness 从默认 60 降到 30 后磁盘写 I/O 几乎消失高峰期 P99 延迟显著改善。这个场景里的关键是所有进程共享同一个 cgroup内存回收是全局的zswap 能有效兜住突发波动。另一个场景是低内存小机器2G 内存跑容器集群。管事的人直接用 zram 把 1G 空间当 swap 用lz4 压缩减少 CPU 压力。效果是容器不频繁 OOM 了但代价是 zram 满时直接触发 OOM kill运维上必须盯着水位这也是这类方案适合“小范围测试”而非直接上生产的原因。5.3 我最后想给的调参顺序先把 zswap 打开观察一个完整业务周期至少包含一次内存高峰的指标。用written_back_pages判断写盘压力用reject_compress_poor判断压缩收益。再决定调 swappiness 还是调 pool 上限。整个过程记录变更一次只动一个参数别同时改三四个变量否则根本定位不到是谁起了作用。这是我在调优上反复吃过亏以后总结出来的纪律。最后提一个小技巧很多发行版默认内核参数里已经把 zswap 功能性打开但没调参数。你其实不需要重编内核只要在/etc/default/grub的GRUB_CMDLINE_LINUX里加入启动参数再grub2-mkconfig重新生成引导配置即可。改完重启的时间成本值得用来换之后几个月的稳定。像我实际排障遇到的很多问题最后都不是机制错了而是参数没给到合适的位置。把观测和调参放在一起做虚拟交换空间这口锅才能发挥出真正的价值。
阅读完成 · 觉得有帮助?