一两千块一年的小机器,内存只有 1G 或 2G,这是很多个人站长的真实配置。跑着 Nginx、PHP-FPM、MySQL,再挂个 Redis,内存永远是紧的。每次内存紧张,第一反应是加 swap 文件,结果加了以后发现:磁盘灯狂闪,网站响应从 80ms 掉到 800ms,比不加还难受。
这篇文章讲的是一台 2G 内存机器上的实测经验:用 zram 把 swap 换成内存里的压缩块设备以后,同样的负载下页面生成时间从 900ms 降到 210ms,磁盘 IO 等待几乎消失。下面把原理、配置、调参和踩过的坑一次讲清楚,所有命令都是可以直接复制执行的。
一、为什么 swap 文件会让小站变慢
先说清楚 swap 到底在做什么。当物理内存不够用时,内核把不常用的匿名页(进程堆、栈、匿名 mmap)写到 swap 设备上,腾出物理内存给正在用的页。这个过程叫换出(swap out),读回来叫换入(swap in)。
传统 swap 分区或 swap 文件放在磁盘上,哪怕是最便宜的 SSD,随机读的延迟也在 0.1ms 量级,而内存是 0.1 微秒量级,差三个数量级。更糟的是小站用的往往是共享型 VPS,磁盘 IO 本身就是被限流的,swap 一开,IOPS 瞬间打满,其他所有读文件的请求(PHP 加载、MySQL 读页)都排在后面等。
实测数据最能说明问题。一台 2G 内存的机器,MySQL 占 500M,PHP-FPM 四个 worker 各 60M,Nginx 40M,还挂着系统服务和 Redis。网站有个功能会临时聚合一份报表,峰值要额外 300M 内存。没有 swap 时直接 OOM,被内核杀掉;有 swap 文件时能活下来,但换入换出期间 iostat 的 %util 打满,首页 TTFB 从 90ms 涨到 900ms 以上。
二、zram 是什么:拿 CPU 换内存的设备
zram 是内核自带的一个块设备驱动,它不落盘,而是在内存里划一块区域当压缩后的交换空间。写进去的页会被 zstd 或 lz4 压缩,读出来时自动解压。对文本类、稀疏数据,压缩比通常在 2~4 倍之间,也就是说你给它 500M 内存,实际能装 1G~2G 的交换内容。
它换来的代价是 CPU:压缩和解压都要占 CPU 时间。但在小站这个场景里,CPU 通常是有富余的(网站峰值时 CPU 使用率 30%~50%),而磁盘 IO 是瓶颈。用富余的 CPU 换掉紧张的 IO,正好是划算的交易。
需要明确的边界:zram 不能替代物理内存。它的作用是「让不该被杀掉的进程活下来,并且活得不太难受」。如果工作集本身就长期超过物理内存,压缩再多也只是把问题推迟,最终还是得加内存或减服务。
三、加载模块与创建 zram 设备
现代内核(4.x 以上)都内置 zram,大部分发行版编成了模块。先确认一下:
modinfo zram | head -5
ls /sys/class/zram-control/如果 /sys/class/zram-control 存在,说明模块已经加载。没有的话手动加载一次,并写入开机自动加载:
modprobe zram
echo zram > /etc/modules-load.d/zram.conf
echo 'options zram num_devices=1' > /etc/modprobe.d/zram.conf创建一个 zram 设备并设置大小。这里有个关键选择:disksize 是「未压缩的逻辑容量」,不是占用多少物理内存。设成 1G 是安全的,物理内存只在真正写入压缩页时才被占用。
zramctl --find --size 1G --algorithm zstd
# 或者手动指定:
echo 1G > /sys/block/zram0/disksize注意顺序:必须先设算法再设大小。zram 设备的算法只能在初始化阶段指定,一旦 disksize 写入非零值,再改 comp_algorithm 会报 Device or resource busy,只能 reset 重来。这个坑第一次配 zram 的人基本都会踩。
四、把 swap 指到 zram 上
zram 设备创建好后,它就是一个普通的块设备,可以用 mkswap 和 swapon 处理:
mkswap /dev/zram0
swapon --priority 100 /dev/zram0
swapon --show
free -h--priority 100 这一项不能漏。如果机器上还留着原来的 swap 文件,两个 swap 设备优先级相同(默认都是 -2)时,内核会轮流使用,一半数据落到磁盘上,zram 的好处被稀释掉一半。给 zram 高优先级,内核就会优先换到 zram,只有 zram 用满了才溢出到磁盘 swap。这个「分层」是设计的重点,不是可有可无的细节。
persistent 配置(Debian/Ubuntu 用 systemd 的话直接写 unit 更稳):
# /etc/systemd/system/zram-swap.service
[Unit]
Description=zram swap
After=local-fs.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/sbin/modprobe zram
ExecStartPre=/bin/sh -c 'echo 1G > /sys/block/zram0/disksize'
ExecStartPre=/sbin/mkswap /dev/zram0
ExecStart=/sbin/swapon --priority 100 /dev/zram0
ExecStop=/sbin/swapoff /dev/zram0
[Install]
WantedBy=multi-user.target然后 systemctl daemon-reload && systemctl enable --now zram-swap。这里有个顺序细节:ExecStartPre 是按声明顺序执行的,所以设大小必须在 mkswap 之前。写成 ExecStart 里的多条命令用 && 串联也行,但出问题时的报错定位不如分开写清楚。
五、调参:三个内核参数决定它好不好用
zram 开起来只是第一步,真正决定体验的是这三个参数。
第一个是 swappiness。很多教程让你把它调大(比如 60 或 100),理由是「反正压缩很快」。这个建议在 zram 场景下部分正确,但要看你的内存构成。swappiness 控制的是「匿名页 vs 页缓存」的换出倾向。zram 场景下页缓存(文件缓存)也是宝贵的,因为 MySQL 的 buffer pool 之外的读、PHP 文件的读取都靠它。把 swappiness 设成 100,内核会疯狂往 zram 里塞匿名页,把页缓存挤掉,结果文件读变多。实测在 MySQL + PHP 的机器上,swappiness=60 比 100 稳,比 10 又能多腾出 200M 左右的可用内存。
sysctl -w vm.swappiness=60
echo 'vm.swappiness=60' > /etc/sysctl.d/99-zram.conf第二个是 zram 的压缩算法。zstd 压缩比高(实测文本数据 3.5 倍左右),解压速度慢一些;lz4 压缩比 2.5 倍左右,压缩解压都快。老 CPU(不支持 AVX2 的)上 lz4 的优势更明显。判断方法很简单:
zramctl
# NAME ALGORITHM DISKSIZE DATA COMPR USED STREAMS看 COMPR 这一列,就是当前的实际压缩比。如果 COMPR 长期低于 1.5,说明你的数据压缩不动(已经压缩过的数据、加密数据、随机数据),zram 只是在白烧 CPU,这时候要么换算法,要么干脆关掉。这个数字是最直接的判据,比任何理论分析都准。
第三个是 min_free_kbytes。小内存机器上内核预留的空闲内存如果太小,内存分配路径会进入频繁的直接回收(direct reclaim),表现是「没有报 OOM,但什么都慢」。2G 内存的机器设成 32M~64M 比较合适:
sysctl -w vm.min_free_kbytes=49152
cat /proc/vmstat | grep -E 'pgscan_direct|pgsteal_direct'看 pgscan_direct 的增长率。如果每分钟涨几万,说明直接回收很频繁,该加大 min_free_kbytes 或者减服务了。这个指标比 free 的输出更能反映「内存是否真的紧」。
六、实测对比:同机同负载,三组数据
在 2G 内存的机器上跑同一份压测(wrk 20 并发,测一个会聚合数据的页面,持续三分钟),三组配置的结果:
- 无 swap:跑到第 90 秒时 OOM,PHP-FPM 被杀,502 一片,压测失败
- 磁盘 swap 文件 1G:全程不挂,但 TTFB P95 从 110ms 涨到 940ms,
iostat -x 1显示 %util 长期 95% 以上,svctm 明显偏高 - zram 1G(zstd)+ swappiness 60:全程不挂,TTFB P95 为 210ms,%util 峰值 40%,PHP-FPM 无重启
关键是第三组还要看 CPU:压测期间 CPU 使用率从 45% 涨到 62%,多出来的部分主要在 system 时间。这就是「拿 CPU 换 IO」的账,在小站上完全还得出。
还有一个不在表格里但很真实的体会:zram 开起来以后,dmesg 里的 OOM 记录彻底消失,SSH 登录也变快了。SSH 登录变快不是错觉 —— 登录本身要 fork 进程、加载 PAM 模块和 shell,这些都要内存,在 swap 抖动严重的机器上,光是登上去看看就会卡十几秒。
七、五个容易踩的坑
坑一:zram 设备已经用过,改参数报 busy。解决办法是先 swapoff,然后 echo 1 > /sys/block/zram0/reset,再重新设算法和大小。reset 会把设备恢复到未初始化状态,所有数据丢失,所以只能在 swapoff 之后做。
坑二:以为 zram 是实打实的内存扩容。不少人把 disksize 设成 4G、8G,觉得「解压能到这么多就行」。但压缩不是无限的,物理占用 = 写入量 / 压缩比,如果压缩比只有 2 倍,写进去 2G 就吃掉 1G 物理内存,直接触发 OOM。合理的设法是物理内存的 25%~50%,并且盯着 zramctl 的 USED 列。
坑三:同时保留大量磁盘 swap 且不给优先级。前面已经说过,没设 priority 会轮流用,效果减半。要么给 zram 高优先级,要么直接把磁盘 swap 关掉(推荐后者,小站没必要留两个 swap,留一个磁盘 swap 反而在紧急时把人骗过去以为还有余量)。
坑四:用监控工具看 zram 的 USED 当成真实内存占用。不同工具的口径不一样,free 里 zram 占用的物理内存不计入 used 也不计入 buff/cache,容易让人以为「内存很宽裕」。真实判断看 /proc/meminfo 里的 AnonPages 和 zramctl 的 USED 两个数相加。
坑五:把 zram 当成解决内存泄漏的手段。进程内存缓慢爬升是代码问题,zram 只是让它爬得久一点才爆。判断方法:看 AnonPages 是否单调上升且不回落,同时 zram 的 USED 同步上涨。这种情况该做的是重启计划或者查泄漏,不是加大 zram。
八、什么时候不该用 zram
有两种情况不建议开。一是机器本身 CPU 就紧张(常年 70% 以上),压缩解压会进一步占 CPU,可能让原本的瓶颈更严重。二是磁盘本身就是 NVMe 且没有 IOPS 限流,磁盘 swap 的代价没那么大,zram 带来的收益有限,反而多了一层复杂度。
反过来,最适合 zram 的场景很明确:共享型 VPS、机械盘或者限流的云盘、内存 1~2G、跑着多个小服务、CPU 有余量。这种机器上一开 zram,体感变化非常明显。
九、小结
zram 不是什么高级技巧,它只是把 swap 从慢设备搬到了快设备上,顺带做了压缩。真正需要想清楚的只有三件事:物理内存给多少、算法选哪个、swappiness 调到多少。三个数字都能从 zramctl 和 /proc/vmstat 里读出来,不需要猜。
如果你的小机器还在因为一次流量高峰就 OOM,先别急着加内存配置,花十分钟把它配上,再用同一份压测跑一遍对比数据。多数情况下这一步就够把这台机器的生命周期再延长一年。等 zram 也压不住的时候,那时候再升级配置,得到的判断是有数据支撑的,而不是「感觉不够用了」。