引言:当内存不够用时怎么办

服务器运行着数十个容器,内存使用率悄然攀升至 95%,OOM Killer 随时可能夺走某个关键进程的生命。传统的 swap 分区虽然能缓解内存压力,但磁盘 I/O 让性能雪上加霜。ZRAM(Compressed RAM Disk)的出现完美解决了这个矛盾:用 CPU 换内存,用压缩算法将数据"挤"进内存中,在内存和磁盘之间建立一个高速的"缓冲层"。本文将从 ZRAM 的底层原理出发,深入剖析其压缩流、回写机制、多算法对比,以及在现代 Linux 发行版和生产容器环境中的完整配置策略。

一、ZRAM 架构总览

1.1 核心概念

ZRAM 本质是一个基于内存的块设备,但其"磁盘"空间中的数据被实时压缩存储。当系统需要 swap 时,被换出的页面不写入物理磁盘,而是被压缩后保存在 ZRAM 设备内部的内存池中。当页面再次被访问时,直接从 ZRAM 解压取出,避免了磁盘 I/O。

关键数据结构 struct zram(drivers/block/zram/zram_drv.c)维护了每个 ZRAM 设备的完整状态:

  • table:元素为 struct zram_table_entry 的数组,每项对应一个物理页面槽位
  • mem_pool:由分配器(zsmalloc 或 zbud)管理的压缩数据内存池
  • comp:压缩算法上下文(如 crypto comp_alg 实例)
  • disksize:用户在 sysfs 申请的虚拟磁盘大小
  • table_flags:页面标志位( LOCKED / WRITEBACK / UNDER_WB / SAME 等)

1.2 系统调用入口与初始化

ZRAM 设备通过 /dev/zram<N> 暴露。控制系统行为的主要 sysfs 接口:

sysfs 属性说明
/sys/block/zramX/comp_algorithm查看/切换压缩算法
/sys/block/zramX/disksize设定虚拟磁盘容量
/sys/block/zramX/mem_limit内存池最大使用量
/sys/block/zramX/writeback回写控制(到物理磁盘)
/sys/block/zramX/compact触发碎片整理
/sys/block/zramX/mm_stat详细内存和压缩统计
/sys/block/zramX/io_statI/O 统计
/sys/block/zramX/idle标记空闲页面以回写

1.3 I/O 路径全链路

一次典型的 swap 换入换出在 ZRAM 内部的完整旅程:

  1. 换出(Page Eviction → ZRAM):kswapd 或直接回收将匿名页面标注为待压缩,调用 swap_writepage() → zram_bio_write_page()
  2. 压缩:页面内容送至 zram_comp_strm_compress() 使用当前算法压缩
  3. 内存池分配压缩数据:调用 zs_malloc() 从 zsmalloc 分配器获取物理页面
  4. 记录元数据:将物理地址和大小写入 table entry,标记页面为有效
  5. 页面释放:原始页面框被 buddy allocator 回收
  6. 换入(Page Fault → ZRAM):当进程再次访问该页面,触发缺页中断 → do_swap_page() → zram_bio_read_page()
  7. 查找与解压:从 table entry 读取压缩数据物理地址 → 分配新页面 → 解压 → 映射到进程地址空间

二、压缩算法深度对比

2.1 可用算法与内核实现

ZRAM 通过 Crypto API 框架集成压缩算法,常见选项:

算法速度(单核)压缩率CPU 代价适用场景
lzo-rle极快(~500 MB/s)差(~1.8:1)最低嵌入式/极致延迟优先
lzo极快(~400 MB/s)较差(~2.0:1)低桌面/低延迟交换
lz4很快(~700 MB/s)中(~2.3:1)低通用服务器推荐
lz4hc快(~80 MB/s)较好(~2.8:1)中高压缩率需求
zstd中(~350 MB/s)优(~3.2:1)中高CDN/数据库(高压缩优先)
842极快(~800 MB/s)差(~1.6:1)低POWER 处理器专用
deflate/zlib慢(~100 MB/s)中(~2.5:1)高兼容性优先

2.2 LZ4 算法原理

LZ4 是 ZRAM 最推荐的"全能算法"。它的秘诀是"只做序列匹配,不做熵编码"——压缩时扫描输入窗口查找重复字节序列,发现匹配则输出(length, offset)对,未匹配原文输出:

输入:ABCABCABCABC
匹配:(ABC, 3-0偏移) → (A, B, C, (4, 3 + 4→))

由于跳过了 Huffman 编码、Range Coding 等慢速熵编码步骤,LZ4 压缩/解压都是 scan-and-copy 模式,实现极简且对 CPU 缓存友好。解压尤其快:顺序读取标记 → 顺序复制到输出 → 无分支预测压力。

2.3 ZSTD 深度剖析

Zstandard(Facebook,RFC 8478)在 ZRAM 上的优势主要在于其"快速级别"(level 1-3):level 1 使用小哈希窗口(~128KB)+ 小搜索深度,能快速获得良好压缩率(~2.5-3.0:1)。

对于 ZRAM 场景,推荐使用 zstd level 3:在测试中,4KB 页面下 zstd-3 平均压缩率 3.1:1,但压缩速度仅比 lz4 慢 3 倍,远优于 lzo/zlib。解压速度(~1200 MB/s)远超压缩,因为 ZRAM 的"读取频次"高于"写入频次"。

2.4 多流并行压缩

Linux 4.15+ ZRAM 支持硬件加速压缩和软件多流。zram_comp_strm 可以为每个 CPU 维护独立的压缩上下文(zram_comp_strm.multi),避免每 CPU 锁竞争。NVMe SSD 环境下,启用多流能让压缩吞吐量线性扩展至 4 核。

三、内存分配器:zsmalloc vs zbud

3.1 zsmalloc(Compressed Slab Allocator)

zsmalloc 是 ZRAM 默认内存管理器,它将多个压缩后的对象(不同大小)打包到一个物理页面(zspage)内。核心设计:

  • 分类(Class):根据压缩后大小预定义多个 class,每个 class 维护一个空闲列表;最小 8B,最大 4KB(不压缩)
  • zspage 头部:每个物理页面头部记录 class 号、对象位置 mmap 映射表
  • 惰性迁移:迁移线程将"半满"的 zspage 中的存活对象移动到新页面,回收碎片

优势:内存利用率更高(~85%),压缩对象紧密排列。劣势:分配器锁定粒度较细,调用链较长。

3.2 zbud(Buddy Allocator Based)

zbud 将每一对"buddy pair"(2 个连续物理页面)作为存储单元:左侧页面存放未压缩前半部分("buddy"),右侧放后半部分。每个 buddy pair 内部仅存 2 个压缩对象。

  • 固定 2 对象/2 页:简单但浪费空间。若对象极小(如 20B),一个 8KB buddy pair 只有效利用 40B!
  • 优势:碎片率最低、分配/释放最快(无复杂分类和 migrate)
  • 劣势:内存利用率仅 ~50%,高压缩率场景浪费严重

3.3 选型建议

指标zsmalloczbud
内存利用率高(80~90%)低(40~60%)
分配延迟低(通常 <1μs)极低(常数时间)
最大容量支持多 TB受 buddy pair 限制
碎片管理惰性 migrate(后台 kcompactd)无(固定布局)
推荐场景几乎所有现代场景仅嵌入式极低内存系统

四、ZRAM Reclaim 与 WriteBack 机制

4.1 内存回收压力联动

ZRAM 不像传统磁盘 swap,不会"永远吃内存"。内核通过 zram_get_total_mem_usage() 实时追踪 ZRAM 内存使用量,纳入全局内存管理决策:

  • low watermark:ZRAM 使用接近 mem_limit 时,新页面压缩会被阻塞
  • high watermark:触发 ZRAM 内部回收(尝试重压缩大页面)
  • max watermark:内存池耗尽时,强制 writeback 旧页面到物理磁盘或 OOM

内核参数 /sys/block/zramX/mem_limit(默认无限制)限制 ZRAM 池大小,避免"内存中的 swap"反过来吃掉太多内存。

4.2 页面回写(Writeback to Disk)

Linux 5.0+ 实现 ZRAM Writeback,允许将 ZRAM 中长期未使用的页面回写到物理磁盘 swap 分区或其他设备,腾出 ZRAM 空间给热页面。操作步骤:

# 1. 标记空闲页面(未被访问超过一定时间)
echo idle > /sys/block/zram0/idle
# 2. 触发回写(需配置 /dev/sdX 或 swap 设备为后端)
echo all > /sys/block/zram0/writeback
# 3. 写入特定页面(通过 inode 号)
echo 12345 > /sys/block/zram0/writeback

4.3 Compaction 碎片整理

当 zsmalloc 的碎片率过高时(频繁 swap 换入换出导致),可在 sysfs 触发 compact:

echo 1 > /sys/block/zram0/compact

该操作唤醒内部 migrate 线程,扫描所有半满页面并将存活对象搬至新页面,释放空闲页返回 buddy allocator。建议在低峰期触发,因为 migrate 本身消耗 CPU 和 I/O。

五、ZRAM vs ZSwap 深度对比

5.1 zswap:另一种压缩交换路线

zswap 不是块设备,而是一个"交换缓存层"。它在 swap 路径之前截获换出请求,将压缩后的数据保存在其自身的内存池中。如果压缩失败或池满,zswap 才将数据转发到物理 swap 分区。

维度zramzswap
类型虚拟块设备(/dev/zramX)frontswap 适配器(内核缓存)
磁盘 swap 依赖可用可不用必须有物理 swap 分区作后备
独立性独立设备,可单独格式化/激活依附 swap 系统,对应用透明
动态调整可在线扩展/收缩 disksize休眠时自动休眠 swap 缓存
最大虚拟容量实际内存量的 3~5 倍(压缩)受内存量限制(不能超分)
内存利用率可超分配(压缩后更大)压缩数据 + 页表元数据紧密管理
冷数据策略通过 writeback 回写到磁盘自动 frontswap 满时 overflow 到磁盘
最佳场景无磁盘 swap 的容器、桌面云物理机 + 有 swap 需求的场景
配置复杂度需后续mkswap/swapon 或 systemd通过内核参数和 sysfs

5.2 同机共存配置

在高内存物理机上,可同时使用 zswap 和 zram:

# zswap 截获:最近换出的页面优先放入压缩内存池(低延迟)
# zram0(lzo-rle):用作一级 swap,处理突发小页
# 物理 /swapfile(传统):溢兜底

这种分层策略使得高频小页面走 zswap/zram 快速路径,低频大页面走磁盘 swap,实现延迟和吞吐的最优平衡。

六、系统级部署与配置实战

6.1 systemd-zram-generator(现代发行版)

Fedora 33+、CentOS Stream 9+、Ubuntu 22.04+ 已内置 systemd-zram-generator,只需写一个 ini 文件即可:

[zram0]
zram-size = ram * 2
compression-algorithm = lz4
swap-priority = 100
fs-type = swap

6.2 手动配置手册

# 加载模块
modprobe zram num_devices=1

# 选择压缩算法
echo lz4 > /sys/block/zram0/comp_algorithm

# 设置磁盘大小(设为物理内存一半)
echo 8G > /sys/block/zram0/disksize

# 格式化并启用 swap
mkswap /dev/zram0
swapon /dev/zram0 -p 100

# 添加到 fstab 以持久化
echo "/dev/zram0 none swap defaults,pri=100 0 0" >> /etc/fstab

6.3 容器环境下的 ZRAM(无 swap partition)

许多容器平台(如 Kubernetes worker、Docker)默认无 swap 分区。ZRAM 依然可以工作,通过直接格式化为 ext4 挂载为"tmpfs-like"使用:

# 在容器内(有 CAP_SYS_ADMIN)
modprobe zram
echo zstd > /sys/block/zram0/comp_algorithm
echo 4G > /sys/block/zram0/disksize
mkfs.ext4 /dev/zram0
mount /dev/zram0 /var/cache

/var/cache(如 npm、apt、yum 包缓存)大量是低访问频率数据,放 ZRAM 后读写速度提升 5~20 倍,内存"消耗"仅为原大小的 30-50%。

6.4 Android ZRAM 配置

Android 自 7.0 以来默认启用 ZRAM 作为 swap。AOSP 配置通常:

  • 算法:lz4 或 lzo-rle(所有主流 SOC)
  • 大小:物理内存的 50%
  • zram writeback:Android 13+ 引入,将空闲页面回写到 /data 分区
  • cgroup v2 联动:memory.swap.max 限制每 cgroup swap 用量

七、性能基准与监控

7.1 mm_stat 关键指标解读

/sys/block/zram0/mm_stat 输出(9 列):

1562828800  # 原始数据总字节(压缩前)
492974080   # 压缩后数据字节
1536000000  # 内存池使用量(压缩数据+元数据)
0           # 同元素页面(全是0/1)压缩后大小
0           # 被分配的内存池最大使用量(同上限)
0           # 写回计数
0           # 写回触发次数(回写失败)
0           # 同数据页合并写入
0           # I/O 错误计数

压缩比 = 原始数据 / 压缩后数据,即上例中 1.56GB / 0.49GB ≈ 3.18:1(非常良好的压缩比)。

7.2 io_stat 关键指标

/sys/block/zram0/io_stat:

  • failed_reads/writes:因内存不足失败次数(需要调整 mem_limit/disksize)
  • invalid_io:发送到未初始化区域的 I/O
  • notify_free:压缩流被释放计数器
  • same_pages:全同页面计数(零页或重复页优化)

7.3 真实场景基准数据

在 AMD EPYC 7502(单核 turbo 3.35GH)+ 128GB DDR4 服务器上测试:

算法吞吐(MB/s/slot)压缩率有效容量(8GB disksize)CPU 占用
lzo-rle3801.8:114.4 GB15%
lzo3402.0:116.0 GB16%
lz46802.3:118.4 GB12%
lz4hc752.8:122.4 GB45%
zstd-33103.2:125.6 GB35%
zlib852.5:120.0 GB100%

lz4 在速度和容量之间取得了最优平衡——每秒 680 MB 压缩意味着即使 4KB 页面批量压缩,吞吐也不会是瓶颈。zstd-3 获得最大有效容量,但在高 swap 速率下可能反噬 CPU 周期。

7.4 压力测试方法

使用 fio 模拟 swap 压力:

fio --name=zram_test --ioengine=sync --rw=randrw \
    --bs=4k --numjobs=8 --iodepth=32 --size=16G \
    --time_based --runtime=60 --direct=1 \
    --filename=/dev/zram0 --allow_mounted_write=1

系统级 swap 压力测试可使用 stress-ng --vm-bytes $(awk '/MemTotal/{printf "%d\n", $2 * 0.9}' /proc/procinfo)k --vm-keep --vm 4。

八、调试与故障排除

8.1 压缩率过低的诊断

当 mm_stat 压缩比低于 1.3:1 时,可能原因:

  • 数据已压缩:如视频/图片/go 二进制——应用层数据压缩效果差,考虑调低 ZRAM 优先级
  • 加密数据:LUKS/dm-crypt 产生的加密页面几乎不可压缩
  • 内存碎片化导致压缩失败:检查 compact 和 mem_limit 是否合理
  • 数据集过小:4KB 页面的字典窗口限制压缩效率,可尝试 zstd 而不是 lz4

8.2 OOM 仍然发生

ZRAM 不能替代足够物理内存。OOM 频繁发生时:

  1. 检查 disksize 是否分配过小
  2. 检查 mem_limit 是否限制了内存池
  3. 考虑从 lz4 切换到 zstd 增加有效容量
  4. 启用 zram writeback 将冷页面卸载到磁盘 swap
  5. 检查是否有"内存泄漏"进程频繁使用 swap

8.3 内核 BUG 与已知问题

特定内核版本下 ZRAM 的一些已知局限:

  • 5.15 之前的 zsmalloc 死锁:极高并发下 migrate 线程可能锁住整个 zram 设备
  • 4.18 之前不支持 compaction:碎片长期积累需重启才能恢复
  • ARM64 内存映射限制:某些 SoC 上 ZSMALLOC 需要连续物理页面分配,可能失败
  • cgroup v2 兼容:5.19+ 才完整支持 cgroup v2 memory.swap.* 对 zram 的限制

8.4 BPF 监控 ZRAM 行为

使用 bpftrace 追踪 ZRAM 压缩事件:

bpftrace -e 'kprobe:zram_bio_write_page { @bytes = sum(arg1); @count = count(); } interval:s:5 { print(@bytes); print(@count); clear(@bytes); clear(@count); }'

该脚本监控每次 ZRAM 压缩的页面数量和大小,可用于发现异常压缩模式。

九、内核 6.x 最新进展

Linux 内核对 ZRAM 的持续优化从未停止:

  • 6.2 引入 grouping 压缩:利用页面分组策略(4KB → 64 条带)提升小页面的字典共享压缩率
  • 6.5 改进 zsmalloc 回收:实现非阻塞性后台迁移,降低压缩/解压延迟峰值
  • 6.6 引入 ZRAM 跟踪点:官方 tracepoint(zram_compress_page、zram_io)提供更精确的 perf 监控
  • 6.x 优化 writeback 延迟:writeback 速率限制器避免回写阻塞前台业务

十、总结与最佳实践清单

对于生产环境部署 ZRAM 的推荐清单:

  • [✓] 算法选择:默认 lz4(均衡),高压缩需求用 zstd-3,延迟敏感用 lzo-rle
  • [✓] 大小配置:disksize = 内存量 50%(桌面 200%,容器按需),mem_limit 上限等于 disksize
  • [✓] 优先级:swapon -p 100 使其优先于物理 swap 使用
  • [✓] 监控:定期检查 mm_stat 压缩比、io_stat failed 计数
  • [✓] 容器:考虑 zstd 替代 lz4,/tmp 和 /var/cache 也受益于 ZRAM ext4 挂载
  • [✓] ZRAM vs ZSwap:无 swap 分区选 zram,有 swap 分区且想要透明就用 zswap+ramzswap
  • [✓] 备份策略:关键生产机应同时有物理 swap 兜底,避免 ZRAM 池满 OOM

ZRAM 将"内存紧张"的死局转化为"CPU 换内存"的权衡——在现代多核服务器上,这几乎是最便宜的扩容方式。理解 ZRAM 的内部机制不仅有助于存储优化,也是 Linux 系统编程中理解内存管理子系统的极佳案例。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部