「生产服务器要不要 swap」是后端社区经久不衰的争论题,一边是「swap 是性能杀手,全部关掉」,另一边是「没 swap 的机器 OOM 死得更快」。两边都有真实事故撑腰,却常常吵不出结论,因为争论双方说的其实是两件事:swap 作为慢速逃生通道的价值,与 swap 被动用后换页延迟的代价。这篇把这层争论拆开:先看 swap 在内核里的真实身份(它不只是「备用内存」,还是内存回收机制的组成部分),再看一笔清晰的延迟账单,然后讲 zram——用 CPU 压缩换内存空间的折中方案,它是 Android、ChromeOS、Fedora 工作站的默认配置,也是近年云服务器内存优化的新选项。最后给出几类典型服务的决策矩阵。目标不是替你选边,而是让每个选择都有可核对的数字。
一、swap 的内核身份:回收机制的组成件
很多教材把 swap 描述为「物理内存不够时的后备仓库」,这个说法有一个方向性误导:内核平时就会用 swap。上一篇讲过,内存回收的常规流程里,匿名页(堆、栈这些没有文件后备的内存)要被回收,唯一的去处就是 swap——先把页内容写到交换空间,页框腾出来给更需要的人;进程下次访问时缺页,再从交换空间读回。也就是说 swap 的存在让「内存回收」这个机制在匿名页上也能运转。没有 swap 的机器上,回收只能动文件页(丢弃干净页或写回脏页),匿名页一旦占据大头,回收空间迅速枯竭,kswapd 空转,最后只剩 OOM Killer 一条路。
free -h 的输出把这个身份写得很直白:
$ free -h
total used free shared buff/cache available
Mem: 62Gi 41Gi 2.1Gi 1.2Gi 19Gi 20Gi
Swap: 8.0Gi 1.2Gi 6.8Gi
注意 available(20Gi)与 free(2.1Gi)的巨大差距:available 的估算公式里包含了「还能再挤出的 buff/cache」与「swap 剩余空间的一部分」,内核认为这 20G 是可以安全分配的。这也解释了为什么 sre 手册里判断内存是否吃紧不看 free 看 available——swap 在 available 里占了一个份额,它是内核内存管理策略的一部分,而不是一个外挂。
交换空间有两种形态:swap 分区与 swap 文件。分区的好处是布局固定、没有文件系统元数据开销,坏处是尺寸调整要动分区表。swap 文件的好处是随时 dd 出来 swapon,坏处有三个历史坑:其一,文件必须无洞(sparse file 会让 swapon 报错 "swapfile has holes"),用 dd 或 fallocate(ext4/xfs 5.2+ 支持)而非 cp --sparse 创建;其二,在 CoW 文件系统(btrfs/ZFS)上,swap 文件必须禁用写时复制(btrfs 要 chattr +C 且不能有快照引用),否则第一次写触发 CoW 会直接破坏交换区;其三,swap 文件挂在文件系统上,依赖该文件系统正常工作——文件系统所在的盘坏掉时 swap 跟着失效。现代内核(2.6 之后)对 swap 文件的支持已经完备,性能与分区基本无差(内核通过块设备偏移直接访问,不经 VFS 缓存),「分区更快」是老黄历,今天的选型更多考虑运维便利性。
二、延迟账单:一次换页到底多贵
swap 的性能代价必须先有具体数字才能讨论。换页延迟由 swap 设备的介质决定,做一笔对比账:内存访问约 100ns;NVMe SSD 的随机读约 60~100μs(千倍于内存);SATA SSD 约 150~300μs;机械盘约 5~10ms(十万倍于内存)。再算上换页的单位粒度——4KB——以及连续访问同一「交换簇」(内核按 cluster 聚合写入)的摊销,一笔匿名页换出到 NVMe 的往返大致是:写出时脏页先入队列、由 kswapd 批量刷,成本被后台摊薄;读回时是同步的:进程访问一个被换出的页 → 缺页中断 → 从 swap 读 4KB → 恢复映射,整个路径上进程是睡着的。假设一次读回 80μs,一次 GC 扫过 10 万个被换出的页就是 8 秒的纯等待——这就是 Java 服务在 swap 开启后常见的「GC 停顿从 50ms 变成 5s」的机制。
vmstat 的 si/so 两列是这笔账单的流水:
$ vmstat 2
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 12582912 215040 34092 19466244 512 0 896 214 912 2140 12 3 84 1 0
4 1 12582912 188416 34092 19471880 3120 1024 42112 1024 2104 3512 15 5 62 18 0
第二行里 si=3120, so=1024:每秒有约 12MB 在换入换出,同时 wa(iowait)升到 18——CPU 在等换页 IO。健康的判读标准很简单:稳态下 si/so 长期为零(说明 swap 只是保险,从未被动用),或者极小且不再增长;si/so 持续非零、swpd 持续增长,说明物理内存已经不够,系统进入「用磁盘时间换内存空间」的抖动区,此时每一毫秒的换页 IO 都直接加在用户请求的延迟上,唯一的正确动作是加内存或降负载,而不是调 swap 参数。
三、swap 的回收去向:匿名页的完整旅程
把匿名页在「有 swap」「swap 上叠 zram」两种配置下的去向画出来,两种方案的差异就一目了然:
flowchart TD
A["匿名页被 kswapd 选中回收"] --> B{swap 配置形态}
B -- "无 swap" --> C["匿名页不可回收<br/>LRU 扫描空转,内存压力无处释放<br/>最终 OOM Killer"]
B -- "磁盘 swap" --> D["压缩/写出 → NVMe/HDD 交换区<br/>换入延迟 60μs~10ms(随介质)"]
B -- "zram" --> E["lz4/zstd 压缩后驻留 RAM<br/>典型压缩比 2.5~3.5x<br/>换入延迟 ~2~5μs(纯 CPU 解压)"]
D --> F["内存不足 + 持续换出"] --> G["抖动:si/so 狂飙,延迟崩塌"]
E --> H["压缩后空间仍不足"] --> G
四、zram:用 CPU 换内存的经济学
zram 的思路朴素而有效:与其把换出的页写到慢速磁盘,不如压缩后继续放在 RAM 里——腾地方靠压缩比,换回靠解压。内核里 zram 是一个块设备(/dev/zram0),你把它格式化成 swap 用,所有写入被 lz4 或 zstd 压缩后存进内存池。看一个真实设备的账本:
$ zramctl
NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
/dev/zram0 lz4 8.0G 2.1G 704.5M 728.6M 8 [SWAP]
$ cat /sys/block/zram0/mm_stat
2210437120 721584128 745695232 0 6176768 ...
(原始字节) (压缩后字节) (含元数据总占用)
DATA 2.1G / COMPR 704M ≈ 3 倍压缩比:本该占 2.1G 物理内存的匿名页现在只占 728M,净赚约 1.4G 有效内存,代价是压缩与解压的 CPU 时间。zram 的换入延迟是纯内存拷贝 + 解压,微秒级(lz4 解压速度约 3~4GB/s/核,4KB 页解压约 1μs 出头),比 NVMe swap 快两个数量级。这就是 Android、ChromeOS 把 zram 作为唯一交换形态的原因——移动设备没有可浪费的毫秒,也没有额外的存储空间可以牺牲。压缩算法的选择是个纯粹的 CPU/压缩比权衡:lz4 快(压缩约 200MB/s/核以上)但压缩比略低,zstd 压缩比高约 15~25% 但慢一倍以上;低配边缘设备选 lz4,核多内存紧的服务器可以选 zstd。Fedora 33 起桌面版默认 /dev/zram0 8GB(大小随内存比例调整),生产环境启用它只需几行:modprobe zram、echo lz4 > /sys/block/zram0/comp_algorithm、echo 8G > /sys/block/zram0/disksize、mkswap /dev/zram0、swapon -p 100 /dev/zram0(-p 设优先级,让 zram 排在磁盘 swap 之前)。
zram 不是银弹,两个边界必须清醒:第一,它救的是「匿名页占着 RAM 不干活」的浪费,若工作集本身真的需要那么多活跃内存,压缩腾出的空间会被再次填满,CPU 白烧——适合「有大量冷匿名页」的场景(多进程服务、JVM 堆的冷区域),不适合「内存全部活跃」的场景。第二,zram 的容量池仍在物理内存里,disksize 8G 不代表能真正吃下 8G 原始数据——超过压缩池的写入会失败或(较新内核上)回退到后备设备(writeback 模式可以把长期不动的页再转存到真实磁盘,兼顾两级),配置时要按「预期原始页量 / 压缩比」估算实际内存占用,并留出余量。
五、决策矩阵:三类服务的不同答案
把前面的机制翻译成场景决策。数据库服务器(MySQL/PostgreSQL):主流共识是配置少量磁盘 swap(1~4G)+ swappiness=1。配少量 swap 的理由不是让它被用,而是给 OOM 前的 kswapd 一条退路——内存回收在「彻底无路可走」之前能先换出少量冷页,配合监控尽早告警,而不是直接跳到 OOM 杀掉数据库主进程(数据库被 OOM 杀的恢复代价远高于几百毫秒换页)。swappiness=1 则保证正常时期内核几乎不碰匿名页,page cache(数据库的主要缓存层)优先被回收。MySQL 官方文档与 PostgreSQL wiki 长期持有这个立场。JVM 服务:swap 的危害点集中在 GC——收集器并发标记阶段要扫全堆,任何一页在 swap 里就是一次同步读盘,G1/ZGC 的停顿预算被轻易击穿。建议同样是小额 swap 兜底 + swappiness=1,并监控 si/so;一旦发现 JVM 进程的 RSS 与 swap 用量同步增长,先排查堆外内存泄漏(DirectByteBuffer、glibc malloc 的 arena 碎片),那才是把堆换出去的推手。边缘设备/小内存云主机(1~4G):无盘或盘慢、内存绝对不足,zram 是几乎必选——Android 的实践(压缩比 3x 左右,换入微秒级)证明了它在低配环境的有效性;此时 swappiness 保持较高值(100 甚至更大)让内核放心把冷页压进 zram。
最后一个必须澄清的迷思:swappiness=0 不等于关闭 swap。它的语义(3.5+ 内核)是「回收时尽量绕开匿名页」,但在文件页枯竭时内核仍会换出匿名页;更危险的是,0 值会让 kswapd 在「只剩匿名页」时陷入空转扫描(pgscan 暴涨、pgsteal 归零),回收机制半瘫痪。想要「几乎不用 swap」应设 1(保留最后通道),想要「绝对不用」应直接 swapoff——但那会让 OOM 更快到来。参数语义的精确性在这里不是学究问题,是把「内存紧张」这一事故档位从 OOM 提前到换页的关键阀门。
小结
swap 的工程决策取决于把它放在哪一层:作为回收机制的组成部分,它给了匿名页一条退路,让 kswapd 在内存紧张时有活可干、给监控留出预警窗口;作为慢速存储,它的换页延迟(NVMe 60μs 到机械盘 10ms)会在 GC、请求路径上兑现成灾难性停顿。成熟的做法从来不是「开或关」的二选一:数据库与 JVM 服务用「小额磁盘 swap + swappiness=1」把事故档位从 OOM 下调到换页,同时靠 si/so 监控确保稳态无人动用;低配与边缘环境用 zram 把换页延迟从微秒级进一步压到 CPU 解压的亚微秒级,用 2.5~3.5 倍压缩比凭空多出三分之一可用内存。所有配置的共同前提是理解 free/available/swpd 三组数字的真实含义、把 vmstat 的 si/so 当作抖动探测器——swap 像安全气囊,最好一辈子不弹出,但你绝不会因为「弹出时有点疼」就把气囊拆掉。
读者留言
COMMENTS 暂无还没有留言,来说第一句?