内存吃紧时,符合条件的冷页可能被回收到交换空间。作为交换设备的 zram 把页面压缩后放在 RAM 中,以处理器工作和内存开销换取潜在余量。收益依赖工作负载,不是一份固定增加的内存,也不保证每台机器都只有很小的处理器成本。
内存里的压缩块设备
zram 是驻留在 RAM 中的压缩块设备。配置为交换时,它可以保留符合条件的页面,避免普通磁盘交换输入输出。但可选写回功能能够使用另行配置的后备设备,因此“永不接触磁盘”不是通用性质。压缩可能降低延迟,具体仍取决于算法、处理器负载和数据。逻辑设备容量与真实 RAM 占用必须分开观察,二者也都不能直接替代系统剩余内存。
压缩就是这笔交易
「压缩」就是这笔交易赚头所在,也是「一台小机型显得能装下比物理内存更多内容」的根源。一页值不值得压要看内容:全零页、文本和有结构的数据都缩得不错,而图片、压缩包这类已经被压缩过的对象几乎缩不动。站在工程视角看,zram 等于把暂时闲置的 CPU 换算成「看似更多」的内存;当工作负载多半是可压缩的、又特别想避开真实磁盘换页的延迟时,这笔交易才划算。
适合服务器与低端设备
它最合身的地方是那些内存不多、换页压力又不过度的机器:小 VPS、容器宿主机,以及手机、路由器这类如今要把袖珍机器安排得明明白白的角色。反过来,内存本来就充裕、或者本来就要换到冷冷盘上的服务器,同一笔钱买一张真实磁盘往往能装下更多,因此未必轮到 zram。实用的判断方法是盯住压缩率和 swap 使用量:当驻留数据始终很小、又够可压缩、且 CPU 相对空闲,才是让 zram 出场的时刻。
坏掉的样子
压缩效果差可能消除预期收益。照片、视频、加密缓冲和已压缩归档可能几乎缩不动。应比较 orig_data_size 与 compr_data_size,同时查看包含分配器碎片和元数据的 mem_used_total。池占用、可用 RAM、负载需求与压力仍然重要。接近一的压缩比不能决定是否发生 OOM,也不能预测其时间;冷页驻留与回收行为同样影响结果。不能把一组字节比率当成故障倒计时。
昂贵的压缩算法可能增加处理器占用与交换延迟。可用算法及参数取决于内核构建,应在代表性压力下比较 lzo-rle、lz4 或 zstd 等候选,而不是断言某一个总适合交互机器。系统态占用高或 kswapd 忙碌,也不能单独证明压缩器就是原因。修改策略前,应把应用延迟、有效进展、压缩统计和回收活动放在同一观察窗口,保留原配置并验证实际变化。
同时使用 zswap 和 zram 可能增加压缩、解压与记账开销。zswap 逐出页面时,先重建页面,再提交给后备交换设备,并不是直接把已压缩的字节块交给 zram。作为后备设备的 zram 可能再次压缩重建后的页面。这种组合是否有收益需要负载证据,不能把两个名称当成同一机制,也不能声称每一种双层配置都有完全相同的成本。应核对实际交换路径与启用功能,再解释统计。
固定 disksize 可能在机器变配后不再合适,但它设置的是逻辑设备容量,不会立即预留同样多的 RAM。设备由 sysfs disksize 设置初始化,随后才执行 mkswap;mkswap 本身不决定 zram 块设备大小。mem_limit 则是压缩数据内存的另一项限制。容量政策应结合需求、开销与恢复余量,变配后重新评估,而不是自动按任意比例扩大。部署说明应分别记录逻辑大小、实际占用与限制,避免把配置额度当成已经分配的内存。
快速验证
保存为 zram_ratio.py,用未启用优化的 Python 运行 python3 zram_ratio.py。标准库自测只在普通临时文件中写入虚构的 mm_stat 快照,断言核对完整报告与退出判定,结束后清理这些文件。它不会发现设备,也不读取内核接口。若要检查已经保存的普通文本快照,可运行 python3 zram_ratio.py saved-mm-stat.txt,只读指定文件。不要把设备文件或虚拟内核文件作为输入;默认测试完全不需要真实 zram,也不会改变交换、内核参数或运行中的服务。
#!/usr/bin/env python3
"""Offline mm_stat parser/report demo; no device discovery or kernel access."""
from pathlib import Path
import sys
from tempfile import TemporaryDirectory
def parse_mm_stat(text):
parts = text.split()
if len(parts) < 3:
raise ValueError("need orig, compr and mem_used_total")
if any(not x.isascii() or not x.isdecimal() for x in parts):
raise ValueError("fields must be unsigned decimal integers")
values = tuple(int(x) for x in parts)
if any(x > (1 << 64) - 1 for x in values):
raise ValueError("field exceeds unsigned 64-bit range")
orig, compr, mem_used = values[:3]
if orig == 0 and compr != 0:
raise ValueError("inconsistent zero-orig snapshot")
return orig, compr, mem_used
def report(name, orig, compr, mem_used):
prefix = f"{name}: orig={orig} compr={compr} mem_used_total={mem_used}"
if orig == 0:
return prefix + " ratio=n/a (no stored data)"
if compr == 0:
return prefix + " ratio=n/a (nonempty; zero compressed payload)"
ratio = orig / compr
saved = 100.0 * (1.0 - compr / orig)
return prefix + f" ratio={ratio:.2f} payload_saved={saved:.1f}%"
def inspect_paths(paths):
lines = []
parsed = skipped = 0
for path in paths:
try:
values = parse_mm_stat(path.read_text(encoding="ascii"))
except (OSError, UnicodeError, ValueError) as exc:
lines.append(f"SKIP {path.name}: {type(exc).__name__}")
skipped += 1
continue
lines.append(report(path.name, *values))
parsed += 1
lines.append(f"SUMMARY: parsed={parsed} skipped={skipped}")
return (0 if parsed > 0 and skipped == 0 else 1), lines
def self_test():
if not __debug__:
raise RuntimeError("run without Python optimization")
with TemporaryDirectory(prefix="zram-fixture-", dir=".") as directory:
root = Path(directory)
cases = {
"ratio": ("314572800 104857600 125829120 0 125829120 0 0 0 0\n",
"ratio: orig=314572800 compr=104857600 mem_used_total=125829120 ratio=3.00 payload_saved=66.7%"),
"same": ("4096 0 0 0 0 1 0 0 0\n",
"same: orig=4096 compr=0 mem_used_total=0 ratio=n/a (nonempty; zero compressed payload)"),
"empty": ("0 0 0\n", "empty: orig=0 compr=0 mem_used_total=0 ratio=n/a (no stored data)"),
"overhead": ("0 0 4096\n", "overhead: orig=0 compr=0 mem_used_total=4096 ratio=n/a (no stored data)"),
"plain": ("4096 4096 8192\n", "plain: orig=4096 compr=4096 mem_used_total=8192 ratio=1.00 payload_saved=0.0%"),
}
for name, (text, expected) in cases.items():
path = root / name
path.write_text(text, encoding="ascii")
assert inspect_paths([path]) == (0, [expected, "SUMMARY: parsed=1 skipped=0"])
bad = ["", "1", "1 1", "x 1 1", "1 1 x", "-1 0 0",
"1 -1 0", "1 1 -1", "1 1 1 -1", "0 1 1", "1.0 1 1",
"+1 1 1", "1_000 1 1", "18446744073709551616 1 1"]
invalid_paths = []
for index, text in enumerate(bad):
path = root / f"bad-{index}"
path.write_text(text, encoding="ascii")
invalid_paths.append(path)
assert inspect_paths([path]) == (1, [f"SKIP {path.name}: ValueError", "SUMMARY: parsed=0 skipped=1"])
missing = root / "missing"
assert inspect_paths([missing]) == (1, ["SKIP missing: FileNotFoundError", "SUMMARY: parsed=0 skipped=1"])
assert inspect_paths([]) == (1, ["SUMMARY: parsed=0 skipped=0"])
code, lines = inspect_paths(invalid_paths + [missing])
assert code == 1 and lines[-1] == "SUMMARY: parsed=0 skipped=15"
assert len(lines) == 16 and all(line.startswith("SKIP ") for line in lines[:-1])
code, lines = inspect_paths([root / "ratio", missing])
assert code == 1 and lines == [cases["ratio"][1], "SKIP missing: FileNotFoundError", "SUMMARY: parsed=1 skipped=1"]
print("PASS: offline file fixtures; ratio, nonempty zero payload, empty, invalid/missing, all-skipped and partial results")
def main():
if len(sys.argv) == 1:
self_test()
return 0
code, lines = inspect_paths([Path(name) for name in sys.argv[1:]])
print("\n".join(lines))
return code
if __name__ == "__main__":
raise SystemExit(main())
2026-10-19,两份完整后继围栏在 Python 3.12.14 下通过离线运行,断言启用、退出码为零、标准错误为空。PASS 只表示这些普通文件夹具断言通过。虚构的原始 300 MiB、压缩 100 MiB 用例核对 3.00 载荷比例与 66.7% 载荷节省,不是主机测量,也不是总 RAM 收益。其他断言区分非空但零压缩载荷、没有已存数据与仍有分配开销的情况,并拒绝缺失字段、格式错误、负数、超出范围以及原始字节为零但压缩字节非零的快照。独立变异测试确认指定破坏会触发失败,不能据此声称所有错误都已覆盖。
解析器至少要求前三个字段,所有输入字段都须为无符号六十四位十进制整数,并单独报告 mem_used_total。同值填充页可以拥有非零原始字节而没有压缩载荷,因此比例不可算,不应标为空。没有已存数据也不代表没有分配 RAM。可选文件模式只打印报告和解析、跳过数量,绝不打印 PASS;未解析任何快照或跳过任意输入都会返回非零退出码。其余字段虽校验数字格式,却不解释为完整内核状态。程序不能推断 disksize、池满、趋势、处理器成本、OOM 时间或应当扩容、重压缩;这些结论需要额外观测与负载证据。本次没有测试真实设备行为。
延伸阅读
来源
kernel.org:zram 块设备,zram 的权威文档,已核验 2026-10-19。
Wikipedia:zram,zram 是什么、怎样运作,已核验 2026-10-19。
ArchWiki:Zram,保留的延伸参考;2026-10-19 尝试访问时返回反机器人页面,未能核验正文。
Linux v6.12 zswap source,修订依据,已核验 2026-10-18。
Linux zswap documentation,修订依据,已核验 2026-10-18。
Linux v6.12 zram source,同值填充页记账与 mm_stat 字段,已核验 2026-10-19。