引言:当内存不够用时怎么办
服务器运行着数十个容器,内存使用率悄然攀升至 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_stat | I/O 统计 |
| /sys/block/zramX/idle | 标记空闲页面以回写 |
1.3 I/O 路径全链路
一次典型的 swap 换入换出在 ZRAM 内部的完整旅程:
- 换出(Page Eviction → ZRAM):kswapd 或直接回收将匿名页面标注为待压缩,调用
swap_writepage()→zram_bio_write_page() - 压缩:页面内容送至
zram_comp_strm_compress()使用当前算法压缩 - 内存池分配压缩数据:调用 zs_malloc() 从 zsmalloc 分配器获取物理页面
- 记录元数据:将物理地址和大小写入 table entry,标记页面为有效
- 页面释放:原始页面框被 buddy allocator 回收
- 换入(Page Fault → ZRAM):当进程再次访问该页面,触发缺页中断 →
do_swap_page()→zram_bio_read_page() - 查找与解压:从 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 选型建议
| 指标 | zsmalloc | zbud |
|---|---|---|
| 内存利用率 | 高(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 分区。
| 维度 | zram | zswap |
|---|---|---|
| 类型 | 虚拟块设备(/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-rle | 380 | 1.8:1 | 14.4 GB | 15% |
| lzo | 340 | 2.0:1 | 16.0 GB | 16% |
| lz4 | 680 | 2.3:1 | 18.4 GB | 12% |
| lz4hc | 75 | 2.8:1 | 22.4 GB | 45% |
| zstd-3 | 310 | 3.2:1 | 25.6 GB | 35% |
| zlib | 85 | 2.5:1 | 20.0 GB | 100% |
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 频繁发生时:
- 检查
disksize是否分配过小 - 检查
mem_limit是否限制了内存池 - 考虑从 lz4 切换到 zstd 增加有效容量
- 启用 zram writeback 将冷页面卸载到磁盘 swap
- 检查是否有"内存泄漏"进程频繁使用 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 系统编程中理解内存管理子系统的极佳案例。

发表评论 取消回复