zRAM内存压缩技术分析及优化方向

1,262 阅读9分钟

1. zRAM出现的背景

阿姆达尔定律告诉我们,任何计算机操作系统总是存在瓶颈。从历史上看,许多系统上工作负载的瓶颈是 CPU,因此系统设计人员致力于 CPU 运行更快、更高效,不断增加 CPU 内核的数量。 慢慢地,RAM 越来越成为瓶颈。当数据从磁盘加载到 RAM时,CPU会闲置地等待。增大 RAM 并不总是一个具有成本效益的选择,有时甚至根本不是一个选择。更快的 I/O 总线和固态磁盘SSD减少了瓶颈,但并没有消除它。于是工程师们想,假如可以增加存储在 RAM 中的有效数据量,那不是很好吗?而且,由于这些 CPU 无论如何都在等待,也许我们可以使用这些空闲的 CPU 周期来做一些事情实现这一目标?这是内核压缩的目标:我们在 RAM 中保留更多压缩数据,并使用空闲的 CPU 周期来执行压缩和解压缩算法。

zRAM于2014 年进入 Linux 3.14 内核主线,但由于 Linux 用途十分广泛,这一技术并非没有默认启用,只有 Android 移动终端设备和少部分的 Linux 桌面发行版如 Fedora 默认启用了这一技术,以保证多任务场景下内存的合理分层存储。

举个具体的例子,你在android手机上连续打开了10个应用,系统空闲内存持续下降,通过zRAM内存压缩会把最近最少使用的页面压缩起来, 如淘宝的页面已最近最少使用的,原来占用100M的匿名页会压缩起来存放,压缩后占用40M,通过这个压缩就节省了60M内存。从而过到内存压缩保留更多数据在RAM中的目的, 让用户体验更佳。

2. zRAM软件架构

zRAM本质是一个块设备驱动,它使用内存模拟block device的做法。它把内存回收的策略交给内存管理,把压缩和解压缩交给压缩库,把自身内存分配交给zsmalloc, zRAM自身就是一个简单的驱动。

zRAM的软件架构主要包含3部分:

3. zRAM实现分析

3.1 zRAM驱动模块

  • zram_init

zram_init注册了zram块设备驱动,流程如下:

  • disksize_store

创建zram设备驱动后,通过用户态节点配置zram块设备大小, 对应disksize_store函数。

流程如下:

  • zram_make_request

所有zram的块设备请求都是通过zram_make_request进行的。

zram_make_request流程如下:

3.2 数据流模块

内核使用zcomp_strm结构描述压缩数据流,buffer用于存放压缩后的临时数据。

struct zcomp_strm {
	/* compression/decompression buffer */
	void *buffer;
	struct crypto_comp *tfm;
};

以前的3.15老版本内核提供了接口给上层设置最大压缩流。笔者从4.19内核中已经看到,尽管还是保留这个接口,但其实设置已经没有意义了,因为底层内核会针对各个CPU都配置一个压缩数据流。内核zram.txt有描述:

Set max number of compression streams Regardless the value passed to this attribute, ZRAM will always allocate multiple compression streams - one per online CPUs - thus allowing several concurrent compression operations. The number of allocated compression streams goes down when some of the CPUs become offline. There is no single-compression-stream mode anymore, unless you are running a UP system or has only 1 CPU online.

我们看下设置下来的值内核怎么处理,代码如下,它直接什么都不做,直接给返回了,所以新版本这里即便设置了也是设置了个寂寞,哈哈。

static ssize_t max_comp_streams_store(struct device *dev,
		struct device_attribute *attr, const char *buf, size_t len)
{
	return len;
}

创建压缩数据流程如下:

之所以要针对每个在线CPU都创建一个压缩数据流,主要是优化系统中并发的情况。每个写压缩操作都独享一个压缩数据流,通过多个压缩数据流,使得系统允许多个并发压缩操作。

3.3 压缩算法模块

zRAM默认使用lzo压缩算法,但其实 zRAM 支持的压缩算法有很多,我们可以通过 cat /sys/block/zram0/comp_algorithm 获取支持的算法,当前启用的算法被[]括起来。

有人可能有疑问,zRAM使用哪种压缩算法最好?其实之所以同时存在这么多压缩算法是因是这些压缩算法并不存在绝对优劣, 各在优劣,有的压缩率高CPU消耗高、有的压缩率低CPU消耗低……, 在不同的硬件不同的场景也会有表现的差异,因此只有进行真实的测试,才能最合理地选择压缩算法。

工程师们也在持续优化压缩算法。截至当前,最新版本LZ v1.9.4(2022 年 8 月 16 日更新)在ARM64上最高解压缩性能优化了20%,最近的apple M1 芯片尤其如此,在 macbook 笔记本电脑和 nucs 中都有使用。

下面截取了几种算法在x86机器上的表现:

就上表而言, lz4压缩率与LZO很接近,但压缩和解压缩速度是远优于LZO压缩算法的,尤其是解压缩性能,是碾压性的。

近几年合入的zstd压缩算法,压缩率更高, 查压缩和解压缩会慢一些。

网上看到有大神写过脚本实测试过(这里就无耻地引用一下吧), 测试脚本[1]

3.4 zRAM读写流程

zram最典型的操作就是读写行为, 下面针对读写流程进行分析。

  • 写(压缩)流程

  • 读(解压缩)流程

3.5 zRAM writeback功能

zRAM是将内存压缩后存放起来,仍然是在在RAM中。如果有大量一次性访问页面被压缩后很长时间没有被再次被访问, 虽然经过压缩但仍然占内存。zRAM支持设置外部存储分区作为zRAM的backing_dev,对不可压缩内存(ZRAM_HUGE)和长时间没有被访问过的内存(ZRAM_IDLE)回写到外部存储中。

以Android手机为例, 如果配置开启了zram writeback有idle回写功能,流程如下:

4. zRAM性能调优

zRAM提供了非常多的可配置选项,调优方式也多种多样。本文针对常见原生配置及社区最新进展做调优思路讨论,不涉及任何具体实施的建议及方案,也不一定完全适用于各生产环境。

4.1 zram大小

【调优节点】: /sys/block/zram0/disksize

内核提供了/sys/block/zram0/disksize节点供配置zram大小。可以实际使用场景实测,一般有场景、重载的场景zram等典型场景观察内存相关指标。根据需求配置合理的zram大小。有人可能问:" 反正这个是最大值,不是设置越大越好吗?”

我们在上面disksize_store分析的时候提到过,zram大小是需要为其分配zram table的,是需要占用一定内存空间的。如果配置很大的zram空间但平时有用不着,这个就浪费内存的。

4.2 内存压缩算法

【调优节点】:/sys/block/zram0/comp_algorithm

在上面讲解压缩算法的时候,我们提到过各种压缩算法,这些压缩算法在不同平台,不同场景都有差异性的表现。可以根据需求选择最适用的。评估的考量主要是压缩/解压缩消耗的CPU cycle和压缩率的平衡。

4.3 zram的簇预读

【调优节点】:/proc/sys/vm/page-cluster

page-cluster的作用就是每次从交换设备读取数据时多读 2^n 页,一些块设备或文件系统有簇的概念,读取数据也是按簇读取,假设内存页大小为 4KiB,page-cluster为3的情况每次读取的一簇数据为 32KiB

这是swap时利用局部性原理,把附近的几个页“顺便”也读出来,减少频繁读取磁盘的次数。而其实使用zRAM时,并不会有磁盘相关操作,这里多读一些甚至是浪费内存的。

而且压缩后在zram中的数据也并不一定具有局部性原理。这里是可以通过实际测试来判断的。

void __init swap_setup(void)
{
	unsigned long megs = totalram_pages >> (20 - PAGE_SHIFT);

	/* Use a smaller cluster for small-memory machines */
	if (megs < 16)
		page_cluster = 2;
	else
		page_cluster = 3;
	/*
	 * Right now other parts of the system means that we
	 * _really_ don't want to cluster much more
	 */
}

以这份[2]提供的测试数据来看,对zRAM设备把page_cluster调小,平均延迟是改善了。

4.4 zram的vma预读

【调优节点】:/sys/kernel/mm/swap/vma_ra_enabled

预读始终是swap机制的优化方向之一。上面提到的基于page-cluster的预读可能不一定适合zram。内核工程师们提出了基于VMA地址的预读, 对于zRAM可能更符合局部性原理。

内核支持可配置基于vma的预读还是page-cluster的预读。

struct page *swapin_readahead(swp_entry_t entry, gfp_t gfp_mask,
				struct vm_fault *vmf)
{
	return swap_use_vma_readahead() ?
			swap_vma_readahead(entry, gfp_mask, vmf) :
			swap_cluster_readahead(entry, gfp_mask, vmf);

社区描述[3]的测试结果会有明显效果:

4.5 zram使用程度倾向

【调优节点】:/proc/sys/vm/swappiness

swappiness参数用于表征更倾向于回收匿名页还是文件页。Linux5.8以下版本swapiness默认值是60,最大值是100, 如果swappiness设置为100表示匿名页和文件将用同样的优先级进行回收。由于zram设备非外部存储设备,其本质还是对RAM的访问,可以相对更激进地使用zram,即把swappiness设置得比100还大,也就是回收匿名页比文件页激进。

linux5.8支持把swappiness设置得比100还大, 最大值从100修改为了200, 提交[4]已经进入5.8版本主线。如下:

mm: allow swappiness that prefers reclaiming anon over the file workingset

文档同时也更新了说明, 对于zram设备建议swappiness大于100。

For in-memory swap, like zram or zswap, [...] values beyond 100 can be considered. For example, if the random IO against the swap device is on average 2x faster than IO from the filesystem, swappiness should be 133 (x + 2x = 200, 2x = 133.33).

4.6 zRAM去重

【调优节点】:/sys/block/zram0/ dedup_enable

Android 是使用 zram 作为swap device的最大用户之一,节省zram内存使用量非常重要。有研究论文《MemScope: Analyzing Memory Duplication on Android Systems》[5]表明Android的内存内容的重复率是相当高的。

Android 应用进程和系统进程可能表现出不同的特点,因为 Android 应用程序运行 dalvik VM 但非应用程序进程不运行。应用进程的内存内容重复率平均约14%,系统进程的内存内容重复率平均约3%。

论文调查了哪些内存区域包含最多重复页面,结果是超过 80% 的重复页面来自来自匿名页、gralloc-buffer、dalvik-heap、dalvik-zygote和 libdvm.so 库的 BSS 段。如下图:

于是内核工程师们提出了zram支持去重功能以节省功能,提交集[6]如下:

zram: implement deduplication in zram

可以通过/sys/block/zram/io_stat/中zram->dedup->dedups查看重复的page量。

4.7 多种压缩算法组合压缩

社区在讨论一个zram多重压缩的优化。可以在压缩后再次变更压缩算法对idle和huge的page再次压缩。基本思想是使用一种更快但压缩率较低的默认算法率和可以使用更高压缩率的辅助算法。以较慢的压缩/解压缩为代价。替代压缩算法可以提供更好的压缩比,以减少 zsmalloc 内存使用。当然这个改动[7]还没有合入到主线中。

zram: Support multiple compression streams

5. 参考资料

lwn.net/Articles/54…

segmentfault.com/a/119000004…

tjtech.me/analyze-zra…

kernel.meizu.com/zram-introd…

www.dazhuanlan.com/songlipeng2…

github.com/lz4/lz4/rel…

blog.csdn.net/Death__Azre…

docs.kernel.org/admin-guide…

lore.kernel.org/linux-mm/20…

dl.acm.org/doi/10.1145…

www.mail-archive.com/linux-kerne…

参考

  1. [^](zhuanlan.zhihu.com/p/565651762…)gist.github.com/LuRenJiasWo…
  2. [^](zhuanlan.zhihu.com/p/565651762…)segmentfault.com/a/119000004…
  3. [^](zhuanlan.zhihu.com/p/565651762…)lore.kernel.org/linux-mm/20…
  4. [^](zhuanlan.zhihu.com/p/565651762…)git.kernel.org/pub/scm/lin…
  5. [^](zhuanlan.zhihu.com/p/565651762…)dl.acm.org/doi/10.1145…
  6. [^](zhuanlan.zhihu.com/p/565651762…)lore.kernel.org/lkml/149455…
  7. [^](zhuanlan.zhihu.com/p/565651762…)lore.kernel.org/linux-mm/20…