您知道吗......? LWN.net 是一份由订阅者支持的出版物;我们依靠订阅者来维持整个运作。 请通过订阅来帮助我们,让 LWN 继续在网络上运行。
|
2013年2月12日
本文由 Seth Jennings 投稿
交换是性能的最大威胁之一。即使在快速固态硬盘上,RAM 和交换之间的延迟差距也可能达到四个数量级。 吞吐量差距为两个数量级。 除了速度上的差距,交换区所在的存储也越来越多地共享和虚拟化,这可能会导致额外的 I/O 延迟和不确定的工作负载性能。 zswap 子系统的存在就是为了通过减少 I/O 活动来减轻交换带来的这些不良影响。
Zswap 是一种轻量级的交换页写后压缩缓存。 它接收正在被交换的页面,并尝试将其压缩到基于 RAM 的动态分配内存池中。 如果这一过程成功,向交换设备的回写就会推迟,在很多情况下甚至可以完全避免。 这就大大减少了 I/O,并提高了交换系统的性能。
Zswap 基础知识
Zswap 在交换回写过程中拦截页面,并使用前置交换 API 对其进行缓存。 内核自 3.5 版起就加入了前置交换功能,LWN也曾对其进行过报道。 它允许后端驱动程序(如 zswap)拦截交换页面回写和被交换页面的页面故障。Zswap 还利用 "zsmalloc "分配器(下文将讨论)来进行压缩页面存储。
Zswap 在结构和操作上力求简单,主要有两种数据结构。 第一个是zswap_entry结构,它包含了存储在 zswap 中的单个压缩页面的信息:
struct zswap_entry { struct rb_node rbnode; int refcount; pgoff_t offset; unsigned long handle; /* zsmalloc allocation */ unsigned int length; /* ...*/ };
第二个是zswap_tree结构,它包含一棵由偏移值索引的 zswap 条目组成的红黑树:
struct zswap_tree { struct rb_root rbroot; struct list_head lru; spinlock_t lock; struct zs_pool *pool; };
在最高层,有一个以交换设备编号为索引的zswap_tree结构数组。
每个zswap_tree有一个锁,用于在查找和修改时保护树结构。 上层交换代码提供了某些保护措施,从而简化了 zswap 的实现,无需对同一交换条目进行并发存储、加载和失效操作。 虽然这种单锁设计看似可能会引起争用,但实际执行情况表明,交换路径的瓶颈主要来自更高层次的其他锁,如anon_vmamutex 或swap_lock。 相比之下,zswap_tree锁的竞争程度很低。 下一节将介绍的回写支持也促成了这种单锁设计。
在页面压缩方面,zswap 使用内核加密 API 提供的压缩器模块。 这允许用户在启动时动态选择压缩器,并方便访问硬件压缩加速器或其他未来的压缩引擎。
当一个页面被回收系统选中进行交换,并且 frontswap 在swap_writepage()中截获该页面时,zswap 存储操作就会发生。 操作开始时,会将页面压缩到每个 CPU 的临时缓冲区中。 之所以需要将页面压缩到临时缓冲区,是因为在实际压缩之前,无法知道压缩后的页面大小,也就无法知道保存页面所需的永久分配大小。 一旦知道压缩后的大小,就会分配一个对象,并将临时缓冲区复制到该对象中。 最后,一个zswap_entry结构会被分配、填充并插入该交换设备的树中。
如果存储因故失败,最可能的原因是对象分配失败,zswap 会返回一个错误,该错误会通过frontswap 上传到swap_writepage()。然后,页面会像往常一样被交换到交换设备上。
当程序页面在包含交换条目的页表项(PTE)上发生错误,并被swap_readpage() 中的 frontswap 拦截时,就会发生加载操作。 交换条目包含在相应的树中查找 zswap 条目所需的设备和偏移量信息,一旦找到该条目,数据就会被直接解压缩到由页面故障代码分配的页面中。 在加载过程中,条目不会从数据树中移除;在条目失效之前,它一直保持最新状态。
当特定交换偏移的引用计数在swap_entry_free()中变为零时,就会发生无效操作。 在这种情况下,zswap 条目会从相应的树中移除,并且该条目及其引用的 zsmalloc 分配会被释放。
为了方便抢占,中断永远不会被禁用。 只有在访问每 CPU 临时缓冲页的压缩过程中,以及访问映射的 zsmalloc 分配的解压缩过程中,才会禁用抢占。
Zswap 回写
为了以最佳方式作为缓存运行,zswap 应保存最近使用的页面。 遗憾的是,使用前置交换时,确实有可能出现最近最少使用(LRU)的反向情况,即缓存中充满了较旧的页面,而较新的页面则被强制写入速度较慢的交换设备。 为了解决这个问题,zswap 在设计时考虑到了 "恢复 "回写。
作为背景,交换页面的过程遵循以下步骤:
- 首先,选择一个匿名内存页进行交换,并在交换设备中分配一个插槽。
- 然后,从使用该页的所有进程中取消该页的映射。 在引用该页面的 PTE 中填入交换条目,其中包括交换类型和可以找到该页面的偏移量。
- 最后,页面会被安排写回交换设备。
如果swap_writepage()中的frontswap_store()操作成功,则不执行回写步骤。 不过,交换设备中的插槽已分配完毕,并仍为该页面保留,即使该页面只存在于 frontswap 后端。 zswap 中的恢复回写会强制页面从压缩缓存中移出,进入交换设备中先前预留的交换槽。 目前,该策略是基本的,会在两种情况下强制将页面从缓存中移出:(1) 当缓存已达到根据max_pool_percentsysfs 可调变量设定的最大大小时,或 (2) 当 zswap 无法为压缩池分配新空间时。
在恢复写回过程中,zswap 会解压缩页面,将其添加回交换缓存,并安排写回之前预留的交换槽。 通过在调用frontswap _store()后将swap_writepage()分成两个函数,zswap 可以从 frontswap 中初始回写终止的位置恢复回写。 新函数名为 __swap_writepage()。
有了回写,释放 zswap 条目就变得更加复杂。 如果没有回写,只有在无效操作(zswap_frontswap_invalidate page())时才会释放页面。 使用回写后,也可以在zswap_writeback_pages() 中释放页面。 这些失效和回写函数可以针对同一个 zswap 条目同时运行。 为了保证条目在被其他线程访问时不会被释放,zswap_entry 结构中使用了一个引用计数字段(称为refcount)。
Zsmalloc 的基本原理
说到zswap,就不能不提到zsmalloc,它是zswap用于压缩页面存储的分配器,目前位于Linux暂存树中。
Zsmalloc 是 zswap 使用的一种基于板块的分配器;与内核板块分配器相比,它能在内存受限的环境中为大型对象提供更可靠的分配。 LWN 上已经讨论过 Zsmalloc,因此本节将重点讨论在内核板块分配器存在的情况下使用 zsmalloc 的必要性。
zswap 存储的对象是压缩页面。 默认的压缩器是 lzo1x-1,它以速度著称,但压缩率并不高。 因此,zswap 对象经常会比典型的板块对象大(>1/8PAGE_SIZE)。 在内存压力下,这对内核板块分配器来说是个问题。
内核板块分配器需要高阶页面分配来支持大对象的板块。 例如,在一个页面大小为 4K 的系统上,kmalloc-512缓存的板块需要两个连续页面的支持,而kmalloc-2048每个板块需要八个连续页面。 当系统面临内存压力时,这些高阶页面分配很可能会失败。
Zsmalloc 解决这个问题的方法是,允许支持板块的页面(或用 zsmalloc 术语来说的 "大小类")是非连续的,而且在数量上是可变的。它们在数量上是可变的,因为 zsmalloc 允许一个板块由少于目标数量的后备页组成。 使用struct page的字段将一组非连续页面拼接在一起,以创建一个 "zspage"。 这样,zsmalloc 就可以在不需要高阶页面分配的情况下,为高达PAGE_SIZE 的大型对象分配提供服务。
此外,内核板块分配器不允许大小小于一个页面的对象跨越页面边界。 这意味着,如果一个对象的大小是PAGE_SIZE/2 + 1字节,那么它实际上使用了整个页面,造成了 ~50% 的浪费。 因此,在PAGE_SIZE/2和PAGE_SIZE 之间没有kmalloc()缓存大小。 不过,Zswap 经常需要在这个范围内进行分配。 使用内核板块分配器会导致通过压缩节省的内存在碎片化中丢失。
为了满足这些较大的分配需求,同时又不浪费整个页面,zsmalloc 允许对象跨越页面边界,但代价是在访问它们之前必须映射分配。 之所以需要这种映射,是因为对象可能包含在两个不连续的页面中。 例如,在一个对象大小为 PAGE_SIZE 2/3 的 zsmalloc 大小类中,可以在一个有两个非连续后备页的 zspage 中存储三个对象,而不会造成浪费。 存储在 zspage 中三个对象中第二个位置的对象将被分割到两个不同的页面中。
Zsmalloc 非常适合 zswap。 我们使用内核板块分配器对 Zswap 进行了评估,这些问题确实对frontswap_store()的成功率产生了重大影响。 这是由于kmalloc()分配失败,以及需要拒绝压缩到大于PAGE_SIZE/2 大小的页面。
性能
为了进行性能比较,我们在内存容量恒定且受限的情况下,通过增加每次运行的线程数进行了内核构建。 结果表明,与普通交换相比,zswap 的运行时间减少了 53%,I/O 减少了 76%。 测试系统配置如下
- 运行 v3.7-rc7 的 Gentoo
- 四核 i5-2500 @ 3.3GHz
- 512MB DDR3 1600MHz
- (启动时受 mem=512m 限制)
- 文件系统和交换在 80GB HDD 上(使用 hdparm -t 时速度约为 58MB/s)。
下表总结了测试运行情况。
|
基准 |
zswap |
变化 |
N |
pswpin |
pswpout |
majflt |
输入/输出总和 |
pswpin |
pswpout |
I/O 和 |
I/O 总和 |
%I/O |
MB |
8 |
1 |
335 |
291 |
627 |
0 |
0 |
249 |
249 |
-60% |
1 |
12 |
3688 |
14315 |
5290 |
23293 |
123 |
860 |
5954 |
6937 |
-70% |
64 |
16 |
12711 |
46179 |
16803 |
75693 |
2936 |
7390 |
46092 |
56418 |
-25% |
75 |
20 |
42178 |
133781 |
49898 |
225857 |
9460 |
28382 |
92951 |
130793 |
-42% |
371 |
24 |
96079 |
357280 |
105242 |
558601 |
7719 |
18484 |
109309 |
135512 |
-76% |
1653 |
N "列表示每次运行时内核构建(make -jN)的最大并发线程数。 接下来的四列是不使用 zswap 的基准运行的统计信息,然后是使用 zswap 运行的统计信息。 每次运行的 I/O 总和列是 pswpin(换入页面)、pswpout(换出页面)和 majflt(主要页面故障)的总和。 基线运行和 zswap 运行之间的差异既以 I/O 减少百分比的相对形式显示,也以与交换活动相关的 I/O 减少 X 兆字节的绝对形式显示。
压缩交换缓存降低了页面回收过程的效率。 对于任何存储操作,缓存都可能分配一些页面来存储压缩页面。 这就降低了整体页面回收效率。 效率的降低会给页面缓存带来额外的收缩压力,导致必须从磁盘重新读取页面的重大页面故障增加。 为了全面了解 I/O 的影响,必须在 I/O 总量中考虑主要页面故障。
下表显示了内核构建的总运行时间:
运行时间(秒) |
N |
基数 |
zswap |
%change |
8 |
107 |
107 |
0% |
12 |
128 |
110 |
-14% |
16 |
191 |
179 |
-6% |
20 |
371 |
240 |
-35% |
24 |
570 |
267 |
-53% |
在比较相同线程数的运行时,交换活动对运行时间的影响有所降低。 在比较基准线和 zswap 时,对于限制越来越多的运行,性能下降率有所降低。
构建过程中 CPU 平均利用率的测量结果如下
CPU 利用率百分比(4 个 CPU,满分 400) |
N |
基准 |
zswap |
%变化 |
8 |
317 |
319 |
1% |
12 |
267 |
311 |
16% |
16 |
179 |
191 |
7% |
20 |
94 |
143 |
52% |
24 |
60 |
128 |
113% |
CPU 利用率表显示,使用 zswap 后,内核构建能更有效地利用 CPU,这与运行时的结果相符。
其他性能测试使用 SPECjbb 进行。 有关在 x86 和 Power7+(使用或不使用硬件压缩加速)上使用 zswap 所能实现的性能提升和 I/O 减少的指标,可在本页找到。
结论
Zswap 是一种压缩交换缓存,能够在压缩池达到大小限制或无法从好友分配器获取额外页面时,以 LRU 为基础将页面从压缩缓存中驱逐到后备交换设备。 这种权衡可以显著提高性能,因为读取和写入压缩缓存的速度几乎总是快于从交换设备读取数据的速度,因为从交换设备读取数据会产生异步块 I/O 读取的延迟。
(登录发表评论)