Transcendent memory 技术

August 12, 2011
This article was contributed by Dan Magenheimer
Transcendent memory in a nutshell

翻译:李凯、曾怀东、王鑫、朱翊然、靳彬、林业、马少兵

  1. Linux 内核枚举并跟踪其所有的内存,且对于绝大部分的内存,它都可以单独的寻址到每一个字节。transcendent memory (“tmem”)为内核提供更好的利用内存的能力,使其不用再去枚举内存,在某些情况下甚至不用再去跟踪或者不再用去直接去寻址了。对于内核开发者来说,这听起来可能跟他们的直觉相违背甚至是愚蠢的,但是我们将看到其确实是很有用处的;实际上它给内核增加了一个提高灵活性的中间层,这样基于一些微小的内核变化之上,就能够实现一些较复杂的功能。最终的目标是使得内核可以更有效的利用内存,多个内核之间(包括虚拟化环境和非虚拟化环境)实现负载均衡,以期使得单个系统(甚至跨数据中心)可以达到更好的性能更低的RAM开销。这篇文章对transcendent memory 进行概述以及说明这种技术是怎样在Linux内核中运用的。

    【编译者注:如果仅有“地址枚举并直接访问”这一种内存使用模式,那么显然多个虚拟机使用内存就太不灵活了】

    内核与tmem是怎样展开对话的,将在第二部分进行详细的描述。内核管理着适用于 tmem 的若干不同类别的数据。其中"clean pagecache pages" 和 "swap pages"是内核开发者所熟知的两种。处理前者的补丁叫做“cleancache”它已经被并入3.0kernel里面了。处理交换页的补丁是frontswap,现在linux 内核邮件列表中仍然有对其进行审查。可能还有其它类别的数据与tmem能够更好的协同工作。这些对于tmem来说合适的数据源统称为“frontends”,我们将在第三部分详细的介绍。

    tmem使用不同的方法存储数据。对于tmem来说这些实现方式称为“backends”,任一前端都可以应用任一后端(可能使用shim连接它们)。最初的tmem应用是我们所熟知的“Xen tmem”,它允许 Xen hypervisor 的内存为一个或者多个支持tmem的 guest 使用。Xen tmem已经应用在Xen中两年了,从Xen4.0开始的;内核里的shim已经并入3.0版本里了。另一个Xen驱动部件,Xen self-ballooning 驱动,用来支持guest内核更有效的使用tmem,已经并入3.1版本里了,同时也包括“frontswap-selfshrinker”,查看本文最后的附录以了解这部分更多的内容。

    Tmem的第二个应用并不是虚拟化,而是“zcache”,它是内核里面的一种驱动,用来存储压缩页。这其实就是“倍增内存”的方法,对任何能通过tmem前端(例如cleancache, frontswap)使用的内存都能应用,从而减少内存的需求,例如嵌入式内核。Zchache作为staging驱动被合并到在2.6.39版本中。

    Tmem的第三个应用正在实施,称为“RAMster”.在RAMster中,一些“closely-connected”(【编译者注:极高带宽、极低延时的机器之间的互连】)的内核可以有效地共用它们的RAM,这样的话对于一台缺少RAM利用的机器来说可以利用另一台机器上闲置的RAM。RAMster也被称为“peer-to-peer transcendent memory”,通常应用在物理系统之间,但是也可以虚拟机内核进行测试。RAMster最适合用于高速‘exofabric’连接的多系统环境里,在这个环境里,一个系统可以直接寻址到另一个系统内存,其最初的原型是建立在以太网的连接上。

    还有一些其他的tmem实现也被提出:例如,在tmem可能会在何种程度上帮助KVM和/或容器(container)方面发生了一些讨论,随着最近zcache的更改被合并入3.1,部署必要的shims并尝试这些变得很容易;但还没有人加紧做这件事情。另一个例子是,tmem协议可能适用于某些种类的RAM的技术,如“"phase-change”内存(PRAM);这些技术大多具有某种特性,例如有限的写周期,它们可以通过tmem一类的软件接口被有效管理。在一些RAM技术厂商之间已经开始了对此的讨论。还有一个例子是 RAMster的变种:集群中的单台主机作为“内存服务器”并且只在这台主机上增加内存;这些内存可能是RAM,或者是类 RAM 的东东,比如 fast SSD。

    现有的tmem实现和一些未来实现的设想将在第四部分进行介绍。
  2. 内核是如何同transcendent memory通信的

    内核通过一个精心定义的接口同tmem进行通信,它被设计成向tmem的部署提供最大限度灵活性同时把对内核的影响降到最低。tmem接口可能会显得奇怪,但也有其特殊性很好的理由。请注意在某些情况下tmem接口完全是和内核交互,因此它是“API”;在另一些情况下它定义了两个独立软件组件之间的边界(例如,Xen和一个guest Linux kernel),所以它被称为“ABI”更合适。

    (泛泛而读的读者不妨跳过这节)

    Tmem应当被视作“拥有”一些内存的“实体”。这个实体可能是 in-kernel driver,其他内核,或者hypervisor/host。前面已经提到过,tmem不能被内核枚举;内核不知道tmem的大小,tmem可能动态改变,也可能在任意时刻变“满”。因此,对每一个页面请求内核必须“询问”tmem来接受数据或检索数据。

    Tmem不是字节寻址的 —— 只是大段的数据(准确或大约为一个页面的大小)在内核内存和tmem之间进行拷贝。由于内核无法“看到”tmem,API/ABI的tmem侧负责将数据拷贝进/出内核内存。tmem把相关的数据块放入一个池来管理;在池中,内核选择一个唯一的“句柄”来代表数据块的地址。当内核请求创建池的时候,它会指定一些特定属性(这些属性下面会讲到)。如果池创建成功,tmem提供一个“池id”。句柄在同一个池内是唯一的,但是在不同的池之间不是唯一,句柄由192位“对象id”和32位的“索引”组成。大致上,一个对象相当于一个“文件“,索引相当于文件中页的偏移量。

    tmem两个基本操作是”put”和“get”。如果内核希望在tmem中保存数据,它应该使用“put”操作,提供一个池id,一个句柄以及数据的位置;如果put操作返回成功,tmem就复制了数据。如果内核希望获取数据,它应该使用“get”操作,提供池id,句柄,以及位置用于tmem放置数据;如果get成功,返回的数据会被放置在指定的位置。请注意,不像I/O,tmem的拷贝操作是完全同步的。As a result, arbitrary locks can (and, to avoid races, often should!) be held by the caller.

    有两种基本池类型:短暂型和持久型。成功存入短暂型池的页面在内核使用对应的句柄进行后续get操作后,可能会存在也可能消失。成功存入持久型池的页面能够保证在后续get操作后依然存在。(此外,池可能是“共享的”或是“私有的”)。

    内核负责维护tmem数据及内核自己的数据之间的一致性,tmem有两种“刷新”操作来协助保持一致性:为了分离一个句柄和其他tmem数据,内核使用“刷新”操作。为了分离一个对象和对象中的所有数据块,内核使用“刷新对象”操作。在刷新之后,后续的get操作会失败。在(非共享的)短暂型池上的get操作是破坏性的,例如,暗含了刷新操作;否则,get是不具有破坏性的并且需要明确的刷新。(附录B中描述了两个附加的一致性保证)
  3. Transcendent memory frontends: frontswap and cleancache

    尽管还有其它的前端,frontswap和cleancache这两个现有的tmem前端包含了内核内存的主要类型中的两种,这两种对内存压力都很敏感。这两个前端属于互补的:cleancache处理(clean)那些已映射的页面(编译者注:VFS disk cache page),否则那些页面将会被内核回收;frontswap处理(dirty)那些匿名页,否则这些页面将被内核换出。当一个成功的cleancache_get发生时,会避免一次硬盘读。当一个成功的frontswap_put(或get)发生时,则会避免一次swap设备写(或读)。同时,假设tmem比硬盘的paging/swapping都要快得多,那么将会从一个限制在内存中的环境中获得客观的性能提升。

    • Frontswap

      在一个Linux系统中,“虚拟内存”的总量是物理RAM加上所有已配置的swap设备的量之和。当有一个工作量超过物理RAM的大小的“工作集”时,就会发生swapping —— swap设备本质上是用来模仿物理RAM的。但是通常情况下,一个swap设备要比RAM慢上好几阶,因此swapping已经变成了和可怕的性能相同的词语了。结果,聪明的系统管理员会增加物理RAM或者重新分配工作量来保证尽可能地避免swapping。但是如果swapping并不总是那么慢又会怎么样呢?

      Frontswap允许Linux的swap子系统使用 transcendent memory,当有可用的内存的时候,会取代往一个swap设备发送数据或从一个swap设备获得数据。Frontswap本身并不是一个swap设备,也因此它并不要求像swap设备那种的配置。它并没有改变系统中总的虚拟内存的数量,它只是导致了更快的swapping...一些或大多数或几乎所有时候,但并不总是如此。记住, transcendent memory的数量是不可知的和动态变化的。使用frontswap,当一个页面需要被换出,swap子系统会问tmem它是否愿意接收这个页的数据。如果tmem拒绝,swap子系统会像往常一样向swap设备中写这个页。如果tmem接受,swap子系统可以在任何时候请求此数据页,并且tmem保证了数据的可取回。过一会儿,如果swap子系统确信数据不再有效时(比如持有此数据的进程已经退出),它能够将数据页从tmem中flush掉。

      注意,tmem可以拒绝任何frontswap的“put”。为什么这样呢?一个例子就是如果tmem是多个内核的共享资源(又称之为“clients”),就好像Xen tmem或者RAMster的情况;另一个内核可能已经声明了这个空间,或者可能这个内核超过了tmem所管理的限额。另一个例子是,如果tmem在压缩数据,就像zcache中做的那样,并且它认为压缩的数据页太大了,在这种情况下,tmem可能会拒绝掉没有充分压缩的或者甚至如果平均压缩率增大到不可接受的的页面。

      Frontswap的补丁集是一种非侵略性的,当frontswap被禁止掉的时候,它一点都不会影响swap子系统的行为。事实上,一个关键的内核维护者观察到,frontswap看起来是同swap子系统紧密结合在一起的。这是一件好事,因为现在的swap子系统的代码是非常稳定的,而且并不经常使用(因为swapping是如此之慢),但仍然是系统正确性的关键;对swap子系统的巨大改变可能并不明智,frontswap只是修改了它的边缘。

      一些实现的注意点:Frontswap要求开启swap的情况下每页1 bit的metadata。(Linux swap子系统直到最近要求16bits,现在要求每页8bits的metadata,因此frontswap增加了12.5%)这每页的1 bit记录了这个页面是在tmem还是在物理swap设备。因为,任何时候,可能一些页在frontswap中,另一些在物理设备上,swap子系统“swapoff”代码也要求做一些修改。并且因为使用tmem比swap设备空间更有价值,frontswap提供了一些额外的修改来达到可以执行“临时的swapoff”。当然,hooks在read-page和write-page中传输数据到tmem中,并且添加了一个hook用来当数据不再需要时可以被flush掉。补丁部分影响核心的内核组件合计少于100行。
    • Cleancache

      在大部分的工作负载下,内核从慢速的磁盘中取到页,当RAM还有足够的空间时,内核会在内存中保持大部分页的备份,因为假设磁盘中的页使用过一次就很可能会再次使用。当发生两次磁盘读写而又有足够的空闲RAM没有使用的情况是没有意义的。如果在它们当中的一页中写入任何数据,这么变化必须要写回磁盘,但是预期到未来的变化,页(清理过的)会继续保持在内存中。其结果是,在“页缓存”中的清洁页的数量频繁的增长而填满了绝大多数的内存。最终,当内存几乎被填满,或者如果工作量增大而需要更多的内存的时候,内核会“回收”一部分这样的清洁页,数据会被丢弃,页面可以被别的东西利用。没有数据会丢失,因为内存中的清洁页与磁盘中的相同的页面是一致的。然而,如果内核随后决定它确实需要那个页面的数据,它必须再次从磁盘中获取,这种情况叫做“refault”。由于内核不能够预期未来,一些被保持的页再也不会被用到,而一些被回收的页会很快的被“refault”。

      cleancache 允许 tmem 存储少数 rerault 时产生的清洁页缓存页面。当内核回收一个页,而不是丢弃该页面的数据,它把数据放入 tmem 中,标记为 “ephemeral”,这意味着当 tmem 关闭时页的数据可能被丢弃。随后,如果内核决定需要该页的数据,它会要求 tmem 返还此数据。如果 tmem 保留该页,它会返回此数据,否则,内核继续 refault 操作,像平常一样从磁盘中取回数据。

      为了正确的运行,cleancache的“hooks”被放置在页面被回收以及refault操作发生的地方。内核也必须要确保页缓存、磁盘以及tmem间的相干性,所以该hook也会出现在内核使数据无效的地方。由于cleancache会影响内核的VFS层,并且不是所有的文件系统使用到VSF的全部特性,文件系统必须选择是否使用cleancache当它挂载一个文件系统时。

      cleancache有一个有趣的地方:clean pages 可能会保存在tmem中但是在内核的页缓存中已经没有空闲的页面。因此内核必须为页面提供一个在整个文件系统中独一无二的名字(“句柄”)。在一些文件系统中,inode 节点号已经足够了,但是在现代的文件系统中,使用的是192位的“exportfs”的句柄。
    • Other tmem frontends

      一个常见的问题是:用户空间的代码是否可以使用 tmem ?例如,规避页缓存的企业应用是否使用 tmem。当前,答案是否定的,但我们可以通过实现“超内存”系统调用,来实现它。这样可能会产生一致性问题,能否在用户空间中解决这些一致性问题,还有待于研究。

      其它的内核应用是怎样的呢?有人提出,内核的 dcache 可以会为 tmem 提供一个有用的数据源。这也需要进一步的研究。
  4. Transcendent memory backends

    tmem 技术的接口允许多个前端运行在不同的后端之上。尽管将来可能会允许某种形式的层次结构的出现,然而当前,只可以设定一个后端。tmem 技术的后端拥有一些共同特征:一个 tmem 技术后端类似于一块阻塞设备,但它不执行任何I/O,也不调用任何阻塞I/O子系统。事实上,tmem 技术的后端的功能完全是同步的,即在它运行期间,不能进入休眠状态,也不能调用调度程序。当内核中一页的数据被复制后,一个“PUT”完成。当一页数据被复制到内核的数据页时,一个“GET”成功完成。当这些限制对 tmem 技术后端造成一些困难时,他们仍然会保证后端满足“超内存”技术接口的要求,同时对核心内核做出最少的修改。

    • Zcache

      尽管 tmem 被认为是一种在一组内存需求经常变化的客户机之间共享固定资源的方式,同时,在被一个单独内核用于存储N页数据的RAM容量小于N*PAGESIZE,并且这些数据只能以页的粒度进行访问时,也可以很好的工作。Zcache 同时包括了 tmem 实现和内存压缩,以减少 tmem 前端数据的空间需求。因此,在内存用量紧张时,Zcache 可以充分增加RAM中清理缓存与交换缓存的页数,以显著减少磁盘I/O的次数。

      Zcache 当前还是一个开发中的驱动,因此它是可变的;Zcache 可以同时处理持久页(来自于frontswap)与临时页(来自于cleancache),且这两种情况下,它都使用内核中的lzo1x程序压缩/解压缩页中的数据。用于持久页的空间是通过xvmalloc 片,开发中的ZRAM驱动中一个用于存储压缩页的内存分配器获得的。用于临时页的空间是通过内核调用get_free_page()获得的,然后使用一种称之为“compression buddies”的算法对压缩的临时页进行配对。这个算法保证了包含两个压缩的临时页的物理页框,在必要时能够顺利的回收。Zcache 提供了一种收缩策略,以便于那些能够被回收的页框,在需要回收时,由内核使用已存在的内核收缩机制。

      Zcache 很好的证明了 tmem 技术的可伸缩性特征之一:回想下,通常情况下数据可以很好的压缩(两种或多种情况之一),但仍然可能会产生没法很好地压缩的长序列数据。由于 tmem 技术在“put”的时候,可以拒绝任何页,因此Zcache策略可以避免存储这种压缩的不好的数据,而把它交给原始交换设备进行存储,以动态优化存储在RAM中的页的密度。

    • RAMster

      RAMster 仍然在开发中,但概念已经被验证。RAMster 假定系统之间拥有高速互联,专业说法是 "exofabric"。每台机器中都有部分内存被收集起来通过 tmem 做共享资源使用。集群中的每个节点既是 tmem 客户端也是服务端,每个节点可独立配置拿出多少 RAM 做共享。Thus RAMster is a "peer-to-peer" implementation of tmem

      理论上这有点像远程同步DMA,让一个系统去读/写另一个系统的RAM。最初的RAMster概念验证中使用的是一个标准的以太网连接。exofabric 传输速率远高于磁盘读写的速度,肯定能从中得到额外好处。

      有趣的是,RAMster-POC(RAMster概念验证)展示了tmem的一个有用的因素:一旦页面被置入tmem,在需要的时候,只要页面可以被重组,数据就可以以多种多样的方式转换。当页面被应用于RAMster-POC,他们首先被压缩并且使用一个zcache-like tmem backend来本地缓存。随着本地内存限制增加,一个异步的进程会尝试“remotify”页面到集群中另一个节点;即使该节点拒绝了这次尝试,只要本地节点能够跟踪到远程数据所在的位置,还可以尝试其它节点。尽管当前的RAMster-POC 无法实现这一点,但可以remotify很多份从而实现系统更高的可用性(例如,从节点故障中还原)。

      虽然RAMster中这种多层次机制在puts方面很好用,但当前针对gets并没有同样好用的机制。当一个tmem前端需要一个稳定的get时,数据必须被迅速且异步的获取到;请求数据的线程必须处于忙等待状态,并且调度程序没有被调用。因此现有的RAMster-POC对多核处理器是最合适的,其不寻常的机制可以使所有内核都同时处于激活状态。
    • Transcendent memory for Xen
      tmem最初就是为了Xen而构造的,所以其Xen实现是目前最为成熟的,在Xen中,tmem后端利用空闲虚拟层内存来存储数据,支持大量的 guest,并且可随意实现压缩与删除(一个 guest 内或者跨多个 guest)来扩大数据存储容量。tmem前端使用一个shim转换为Xen超级调用。单独用户可能会被使用“self-ballooning”和"frontswap-self-shrinking"组装(linux3.1下两个都存在)以优化其与Xen tmem的交互。Xen tmem同样支持共享短暂池,所以在同一个物理服务器上同地协作的 guest 共享一个cluster fs 时,只需要在tmem中保留一份cleancache page的拷贝。Xen控制层同样完全实现了tmem:一个大量的统计信息集;完全支持tmem-using用户数据动态迁移以及存储恢复,配额/“权重”或许会被应用与tmem用户以避免 DoS。
    • Transcendent memory for kvm
      在linux3.1中,包含在zcache中的in-kernel tmem代码已经更新以支持多重tmem客户端。有了这些代码,一个对tmem的KVM实现应该很容易就可以完成,至少是原型方式。像在Xen中那样,必须有一个shim置入guest以将cleancache和frontswap frontend calls转换为KVM的超级调用。在主机方面,这些超级调用将会需要协同in-kernel tmem的后端代码一起工作。想要在一个KVM发行版中使用这些,一些额外的控制层支持也是必要的。
    • Future tmem backends
      tmem的适应性和动态性表明它对于其它的存储需求和其它的后端计划可能是很有用的。一些RAM扩展的技术的特质,比如SSD和相变(PRAM)已经被认为可能是合适的。since page-size quantities are always used, writes can be carefully controlled and accounted, and user code never writes to tmem, memory ,以前这类内存技术只能被用作一个快速I/O的设备,现在可以作缓慢RAM。这些想法有的已经在调查研究中了。
Appendix A: Self-ballooning and frontswap-selfshrinking

当一个系统运行一段时间后,通常内存会被 clean pagecache 所占用。而那些使用tmem机制的系统,尤其是 xen,把页面放在 tmem 而不是 guest 内存中会很有意义。为了实现这个功能,xen实现了具有破坏性的“自我膨胀”。通过操作 xen ballon driver 来人工地创建内存压力,以回收页帧,从而强迫内核将页面发送到 tmem 。The algorithm essentially uses control theory to drive towards a memory target that approximates the current "working set" of the workload using the "Committed_AS" kernel variable. Since Committed_AS doesn't account for clean, mapped pages, these pages end up residing in Xen tmem where, queueing theory assures us, Xen can manage the pages more efficiently. 【编译者注:不太理解意义何在,可能和避免 OOM 有关系?】

如果工作集的增长速度出乎意料的快,超过了 self-balloon driver ,就需要提供可用的ram,交换页面。但是在大多数情况下,frontswap尽可能将那些交换页面进入到xen的tmem。然而实际上,内核交换子系统保证所有的交换在磁盘上发生,交换页面将保存在磁盘上好长时间,尽管内核知道这些页面可能不会在被使用。这样做的原因是因为磁盘成本低廉且可以重复写。假设这些页面放在frontswap上,将会占据很多有用的空间。

Frontswap-自我压缩技术用于解决这个问题:当frontswap活动比较稳健,以及客户机内核返回了一个状态,说明目前没有内存压力,根据这个压力删除在frontswap中的一些页面,使用一个叫”partial”的swapoff接口,返回信息给内核内存,来根据需要来释放tmem空间以供紧急需要。例如其他的客户机正处在内存饥渴态。

Appendix B: Subtle tmem implementation requirements

Although tmem places most coherency responsibility on its clients, a tmem backend itself must enforce two coherency requirements. These are called "get-get" coherency and "put-put-get" coherency. For the former, a tmem backend guarantees that if a get fails, a subsequent get to the same handle will also fail (unless, of course, there is an intermediate put). For the latter, if a put places data "A" into tmem and a subsequent put with the same handle places data "B" into tmem, a subsequent "get" must never return "A".

This second coherency requirement results in an unusual corner-case which affects the API/ABI specification: If a put with a handle "X" of data "A" is accepted, and then a subsequent put is done to handle "X" with data "B", this is referred to as a "duplicate put". In this case, the API/ABI allows the backend implementation two options, and the frontend must be prepared for either: (1) if the duplicate put is accepted, the backend replaces data "A" with data "B" and success is returned and (2) the duplicate put may be failed, and the backend must flush the data associated with "X" so that a subsequent get will fail. This is the only case where a persistent get of a previously accepted put may fail; fortunately in this case the frontend has the new data "B" which would have overwritten the old data "A" anyway.

Topic: sohulinux