用zram的writeback来节省内存

总结摘要
本文介绍了如何使用zram的writeback来节省内存。相比以前的zram,writeback可以把不活跃的zram页面回写到硬盘,从而释放zram占用的内存。实现了在享受zram swap速度的同时,还能拥有更大的可用内存空间。

我有几台虚拟机,都是很便宜的那种,内存只有1-2G。因为只是用来挂一些网站或者在线服务的,所以其实不需要很高的性能。但是随着服务越来越多,有些机子内存开始感觉不够用了。升级内存是最后迫不得已才会选的,因为得加钱,是下策。如果能不费一分钱就能提高这个容量,那才是上策。

最简单的办法就是加swap。一般来说如果不是同一时间需要用到大量内存的话,swap出去其实是很划算的,因为有些内存就是占着坑但是很久才会访问到一次。服务器开久了之后会发现,swap分区几乎一直都有几百M的东西,这些就完全是用不到的。

既然是虚拟机,硬盘一般都是限制了很低的带宽和IOPS,毕竟买的是便宜货。如果内存不足,且需要经常swap的话,一个慢硬盘就会让系统变得很卡。

有一个办法,就是zram。也就是压缩内存。这是一个块存储设备,可以被格式化成swap分区,然后拿去用。也就是说,当系统内存不足的时候,会把一些页面交换到zram里面,就等于把不用的内存压缩了一下放回内存里。这样相比硬盘swap的好处就是,速度更快,延迟更低。只是需要消耗一点CPU进行压缩和解压而已,实际用起来还是比硬盘swap快的,体感上好很多。这个方法已经被使用很多年了,连路由器和手机都有zram的身影。一般我自己的服务器都会开这个,比如2G内存的服务器就开一个2G的zram做swap,然后外加1G的硬盘swap,并且把zram的swap优先级调高。这样就会优先使用zram,不够用了再去用硬盘。

我在我的服务器上测试,zram用zstd压缩算法,内存压缩率一般在2.3左右。也就是说2.3G的zram swap实际会占用1G的内存空间。如果是2G内存加2G的zram swap,大约等同于3G的内存(2G zram压缩后约占1G内存,剩余1G可用内存,加起来就是1G可用+2G swap=3G)。

但是,如果用了zram加swap,另外再加了一个硬盘swap,当zram的swap写满了,开始用到硬盘swap的时候,系统就会变得很卡。并且zram里可能留了很多用不上的东西,这些还是占了一些内存。所以有没有什么办法让这些长期不活跃的内存写到硬盘里,而不是在内存里呢?

Linux 4.14 (2017年)给zram加了一个功能,writeback。也就是可以把zram的内容写进硬盘里。既然有了这个功能,我就再也不需要用硬盘swap了。可以说非常符合我的心意:既能把长期不活跃的内存放到硬盘里,还能大大提高平时的swap性能。如果是之前那个例子,2G内存加2G的zram swap,如果用了writeback,极限情况就是等于4G的内存,因为zram被回写到硬盘,释放了占用的内存空间。

writeback除了需要配置backing设备外,还需要手动触发回写。可以先用echo all > /sys/block/zram0/idle标记当前所有页面为idle,然后过一段时间再执行echo idle > /sys/block/zram0/writeback把这段时间内没被用到的页面写入到后端。就是说,不是配了backing就会自动回写了,而是需要手动触发,这样你可以根据自己的需要实现什么时候writeback,可以避免在业务高峰期writeback导致系统变慢。

关于这个配置,可以看Linux内核文档: zram: Compressed RAM-based block devices

我最近就给手上的几台服务器都上了这个功能。感觉挺不错的。手上的服务器都是Debian系统,并且我装了zram-config,这个会自动帮你开机配置zram(其实就是一个脚本而已,也可以手动搞,不过apt装一下比手动配置更方便)。装了zram-config之后,需要改一下配置文件/usr/bin/init-zram-swapping

#!/bin/sh

modprobe zram

LOOP_DEV=$(losetup -f --show /zram_backend.img)
echo "$LOOP_DEV" > /sys/block/zram0/backing_dev
echo zstd > /sys/block/zram0/comp_algorithm
echo 4096M > /sys/block/zram0/disksize

mkswap /dev/zram0
swapon -p 5 /dev/zram0

这台机是2G内存,但是用了writeback后,我直接给这台机配了4G的zram。此外,还需要创建这个/zram_backend.img文件:

dd if=/dev/zero of=zram_backend.img bs=1M count=4096

再配一个crontab,每10分钟标记一次idle,9分钟后回写(这个可以很灵活,我这样做是因为我每10分钟有一个定时任务,所以打算每十分钟结尾的时候做一次标记和回写操作):

9,19,29,39,49,59 * * * * echo all > /sys/block/zram0/idle
8,18,28,38,48,58 * * * * echo idle > /sys/block/zram0/writeback

然后重启服务器,就搞定了。不重启也行,手动配一下zram就可以了。

我还用AI写了个查看zram当前状态的脚本zram_stat.sh

#!/bin/bash

ZRAM="/sys/block/zram0"

# 检查 zram 模块是否存在
if [ ! -d "$ZRAM" ]; then
    echo "未检测到 $ZRAM,请确认 ZRAM 已启用。"
    exit 1
fi

# 字节转可读格式函数
human() {
    awk -v x="$1" 'BEGIN {
        split("B KB MB GB TB", u);
        i = 1;
        while (x >= 1024 && i < 5) {
            x /= 1024;
            i++;
        }
        printf "%.2f %s", x, u[i];
    }'
}

echo "== 内存状态 (mm_stat) =="
read -a mm < "$ZRAM/mm_stat"

orig_data_size=${mm[0]}
compr_data_size=${mm[1]}
mem_used_total=${mm[2]}
large_pages_bytes=$(( mm[7] * 4096 ))   # 大页/不可压缩数据的字节数

echo "原始数据总大小:    $(human ${orig_data_size})"
echo "压缩后数据大小:    $(human ${compr_data_size})"
echo "ZRAM总占用内存:    $(human ${mem_used_total})"
echo "历史最大使用内存:  $(human ${mm[4]})"
echo "大页/不可压缩数据:  $(human ${large_pages_bytes})"

echo ""
echo "== 后端设备状态 (bd_stat) =="
read -a bd < "$ZRAM/bd_stat"

bd_count=${bd[0]}
bd_bytes=$(( bd_count * 4096 ))         # 当前常驻后端的数据大小
bd_reads_bytes=$(( bd[1] * 4096 ))     # 累计读取大小
bd_writes_bytes=$(( bd[2] * 4096 ))    # 累计写入大小

echo "当前回写数据大小:  $(human ${bd_bytes})"
echo "后端累计读取数据:  $(human ${bd_reads_bytes})"
echo "后端累计写入数据:  $(human ${bd_writes_bytes})"

echo ""
if [ "$compr_data_size" -gt 0 ]; then
    ratio=$(awk -v orig="$orig_data_size" -v bd="$bd_bytes" -v compr="$compr_data_size" '
    BEGIN {
        net_orig = orig - bd;
        if (net_orig < 0) net_orig = 0; # 防御性打底
        printf "%.2f", net_orig / compr
    }')
    echo "内存实际压缩比:      ${ratio} : 1"
else
    echo "内存实际压缩比:      N/A (当前无内存压缩数据)"
fi

最后效果大概是这样子:

== 内存状态 (mm_stat) ==
原始数据总大小:    1.87 GB
压缩后数据大小:    55.52 MB
ZRAM总占用内存:    91.09 MB
历史最大使用内存:  775.47 MB
大页/不可压缩数据:  200.00 KB

== 后端设备状态 (bd_stat) ==
当前回写数据大小:  1.74 GB
后端累计读取数据:  16.66 GB
后端累计写入数据:  16.78 GB

内存实际压缩比:      2.52 : 1

好用,非常好用。服务内存又空了起来。

我觉得最近手机出的营销概念,所谓“内存扩展”,其实就是用zram来实现的。安卓手机老早就上这个功能了,大部分手机默认都是开启的。只是以前没人宣传。不知道谁带起的这个风气,换了个新词来宣传老功能,只是因为没有被宣传过。真无语。