在任务之间有效地交换临时数据对于许多数据处理框架和应用程序的端到端性能至关重要。不幸的是,临时数据的多样性造成的存储需求往往落在传统存储平台(如文件系统或键值存储)之间。
本文提出了一种新的分布式存储架构NodeKernel,它通过将文件系统和键值语义融合到通用存储内核中,同时利用现代网络和存储硬件来实现高性能和低成本,在存储系统设计空间中提供了一个新的落脚点。NodeKernel提供了分层命名,高可伸缩性和接近裸机的性能,可提供临时数据特有的各种数据大小和访问模式。Crail是我们的NodeKernel架构的具体实现,该架构使用RDMA网络和分层DRAM / NVMe-Flash存储。我们证明了将临时数据存储在Crail中可将NoSQL工作负载性能提高多达4.8倍,将Spark应用程序性能提高了3.4倍。此外,通过在NVMe Flash和DRAM存储层之间存储数据,与仅DRAM的存储系统相比,Crail可以将存储成本降低多达8倍。

1.简介

有效地管理临时数据是提高集群计算工作负载性能的关键。例如,应用程序框架经常缓存输入数据或共享中间数据,既在作业内(例如,在map-reduce作业中shuffle数据),又在作业之间(例如,机器学习训练工作流中的预处理图像)。在无服务器计算中,为了在任务的不同阶段之间交换数据,临时数据存储也越来越重要[17]。

有效存储临时数据具有挑战性,因为其特征通常位于现有存储平台(例如分布式文件系统和键值存储)的设计点之间。例如,map-reduce作业中的随机数据可能由大量文件组成,这些文件按层次结构组织,大小变化很大,被随机写入,然后顺序读取。文件系统(例如HDFS)提供了方便的分层命名空间并有效地存储了大数据集以进行顺序访问,而分布式键值存储则针对可伸缩访问大量小对象进行了优化。类似地,基于DRAM的键值存储(例如Redis)提供了所需的低延迟,但是持久性存储平台(例如S3)更适合以低成本实现高容量。总体而言,我们发现现有存储平台无法满足分布式数据处理工作负载中临时数据存储和共享的所有不同要求。

在本文中,我们介绍NodeKernel,这是一种从头开始设计的新分布式存储体系结构,可支持快速有效地存储临时数据。作为其最显着的特性,NodeKernel体系结构融合了文件系统和键值语义,同时利用现代的网络和存储硬件来实现高性能。NodeKernel基于两个关键的观察。首先,长期存储平台提供的许多功能(例如耐用性和容错性)在存储临时数据时并不重要。我们观察到在这种情况下,文件系统和键值存储的软件体系结构开始看起来惊人地相似。基本区别在于,文件系统需要额外的间接级别才能将文件流中的偏移量映射到分布式存储资源,而键值存储将整个键值对映射到存储资源。第二个观察结果是,低延迟网络硬件和多CPU多核服务器通过在几ms的延迟中启用可扩展的RPC通信,从而在分布式环境中显着降低了这种间接成本。

基于这些见解,我们通过将文件系统和键值语义实现为通用存储内核之上的薄层,来开发NodeKernel架构。存储内核对统一名称空间中称为“Nodes”的不透明数据对象进行操作。应用程序可以在节点中存储任意大小的数据,在分层名称空间中排列节点,并在需要时通过专用节点类型获得文件系统或键值语义。例如,键值节点和表节点允许并发创建具有相同名称的节点,并提供最后提交为准的语义。另一方面,文件和目录节点允许在存储层次结构中的给定级别上高效枚举数据集。通过将通用存储内核和自定义节点类型中的功能分开,NodeKernel使应用程序可以使用单个平台来存储可能需要不同语义的数据,同时通常为各种数据大小和访问模式提供良好的性能。

NodeKernel在设计时明确考虑了现代硬件。经过严格考虑后的分离,存储内核由为低延迟网络硬件量身定制的轻量级和可伸缩的元数据平面以及可访问多层网络连接的存储资源的高效数据平面组成。精简的元数据平面能够在低开销下提供最关键的功能。数据平面运行一个轻量级的软件堆栈,并利用现代的网络和存储硬件来实现对任意大小数据集的快速访问,同时还提高了成本效率。例如,取决于性能和成本要求,附加到“Nodes”的数据可以固定到特定的存储技术层,也可以从一个存储层溢出到另一个存储层。

Crail是我们对NodeKernel体系结构的具体实现,它使用RDMA网络和分别基于DRAM和NVMe SSD的两个存储层。我们使用原始存储微基准以及NoSQL YCSB基准测试和不同的Spark工作负载,在配备Intel Optane NVMe SSD的100Gb/s RoCE集群上评估了Crail。我们的结果表明,Crail在最佳位置运行时,可以与当前最新文件系统和键值存储的性能相匹配,并且在文件系统和最佳位置之外进行数据访问时,其性能要比现有系统高出3.4倍。键值存储。此外,Crail通过在DRAM之外使用NVMe Flash,创造了降低成本和获得灵活性的新机会,并且几乎没有性能损失。例如,我们在Spark中使用Crail存储shuffle数据时调整DRAM和Flash之间的比率,而作业的运行时间增加很小。

总而言之,本文做出了以下贡献:
•我们提出了NodeKernel,这是一种融合了文件系统和键值语义的新存储架构,可以最好地满足数据处理工作负载中临时数据存储的需求。
•我们介绍了Crail,它是使用RDMA,DRAM和NVMe Flash的NodeKernel架构的具体实现。
•我们证明了将临时数据存储在Crail中可以减少运行时间并减少数据处理工作负载的成本。例如,Crail将NoSQL工作负载的性能提高了4.8倍。集成到Spark的shuffle和broadcast服务中后,Crail可以将应用程序性能提高3.4倍,并将成本降低8倍。

Crail是一个开源Apache项目[2,3],其代码可从项目网站下载,也可直接从GitHub的https://github.com/apache/incubator-crail下载。此外,本论文中使用的所有基准都是开源的。

2.背景和动机

临时数据代表了分析框架中一大类重要的处理中数据。例如,Zhang等人的报告指出,在Facebook执行的超过50%的Spark作业包含至少一项shuffle操作,从而产生了大量的临时数据[37]。

我们将临时数据定义为在处理过程中创建,处理或使用的所有所有应用程序数据,不包括原始输入和最终输出数据。具体来说,我们确定了三种不同的临时数据类别:作业内,作业间和缓存的输入/输出数据集。当执行诸如页面排名或SQL查询之类的单个作业时,将在框架内生成作业内临时数据。常见的示例是在Spark,Hadoop或Flink等框架中的shuffle或broadcast操作期间生成的数据集。通常,此类数据是由同一作业生成和使用的,这限制了数据的生存期。作业间临时数据是多作业流水线中的中间结果。例如,在典型的机器学习流水线中[35]有许多预处理和后处理作业,其中一个作业的输出成为另一作业的输入。最后,缓存的输入/输出数据的示例大部分是只读数据集,这些数据集被拉到缓存中以进行快速重复处理。例如,用户可能会在短时间内在同一个表(或视图)上运行许多SQL查询。在这种情况下,输入表的副本可能会缓存在快速存储介质上。

为不同类型的临时数据构建有效的存储平台需要仔细考虑应用程序需求,数据特征和硬件机会。在以下部分中,我们将讨论对临时存储平台的若干要求,并提供当前最新解决方案的概述。

2.1需求与挑战

图片.png
大小,API和抽象多样性:数据处理工作负载中的临时数据可能会因数据大小而有很大差异。在图1中,我们显示了在执行以下任务过程中生成的临时数据大小分布(CDF):(a)在Twitter graph上执行PageRank;(b)在TPC-DS数据集上进行SQL查询;(c)在稀疏矩阵数据集上执行Cocoa机器学习[24]。如图所示,每个任务的数据大小范围从几个字节(用于机器学习)到GB(用于TPC-DS)。由于历史原因,通常使用不同的存储系统来处理此频谱的两端。分布式键值存储区(例如,RAMCloud,memcached等)具有对象API,并进行了优化,可有效存储较小的值以进行快速随机查找[20,33]。相反,诸如HDFS或Ceph之类的文件系统可以通过对数据集进行分区并维护查找索引来有效地存储大型数据集(GB)。此外文件系统的抽象,可附加文件,可枚举的层次名称空间以及用于I/O的流字节接口为将临时数据集(如all-to-all-shuffle)轻松映射到底层存储提供了额外的支持。
临时存储平台应该能够有效地存储较小和较大的值,并在单个系统中具有文件和键值抽象的统一优势。


性能:临时数据通常位于数据处理的关键路径中,因此必须快速访问临时数据。与大小一样,访问模式也相差很大。例如,由于没有全局顺序,所以shuffle数据经常是随机写入的[11],而SQL表则是在大型顺序扫描中读取的[32]。因此,用于临时数据的存储平台必须满足的一个要求是,对于任何访问模式,它都应该能够在整个数据大小范围内表现良好。
幸运的是,在过去的十年中,I/O设备发展迅速,可支持具有高带宽(100 Gbps),超低延迟(小于10 usc)和数百万IOPS。为了满足数据处理需求,这些设备现在部署在云(AWS,Azure)中,并在数据处理框架内部使用。因此,理想的临时存储系统应该能够在现代网络和存储硬件上高效运行,同时提供接近裸机的性能。

超越内存存储:数据处理工作负载所生成或消耗的临时数据总量可能很大。例如,在图1中显示了临时数据对象大小CDF的工作负载之间,数据的总容量相差100 GB(输入数据集大小的10-100%,未在图中显示)。在提供良好的数据访问性能的同时,很难有效地存储大量数据。例如,从性能的角度来看,最好将所有数据存储在DRAM中,但是这样做通常会太昂贵。值得庆幸的是,在过去的几年中,出现了不同的媒体类型,例如NAND闪存和PCM存储,以不同的成本,性能和能耗价格来存储数据。因此,一个有效的临时数据存储平台应集成多种存储技术,这些技术可提供不同的性能成本权衡,并允许应用程序在权衡空间的不同点之间进行选择。表1给出了不同存储技术在价格和性能方面的比较。
图片.png
没有必要的需求:我们观察到在临时数据存储的特定情况下,许多传统存储功能(例如持久性和容错性)不是优先选项。例如,由于临时数据的生存期短,因此持久的重要性较低。虽然容错通常对于短期数据很有用,但是对于临时数据存储,容错仍然不是一个高优先级。如今,容错通常是在计算框架级别以粗粒度方式实现的。例如,在重新启动任务导致数据丢失的情况下,Spark [36]和Ray [25]使用沿袭跟踪来重新计算数据。

2.2现有方法的局限性

关于上一节中列出的设计目标,我们将回顾当前的最新存储系统。我们将讨论的系统分为以下三类。
键值存储:Memcached [4]和Redis [5]是设计用于将数据存储在DRAM中的两种最流行的键值存储。经过网络优化的KV,例如MICA [21],Hedd [14],FaRM [12],KVDirect [19]和RAMCloud [26]使用RDMA操作来提供高性能的数据访问,但是无法轻松地将数据存储集成到除DRAM之外的其他层上 。Redis进行了扩展以将数据溢出到Flash,但是键索引仍存储在DRAM中,因此受到DRAM容量的限制。针对存储优化的KV(例如Aerospike [30]或BlueCache [34])使用NAND闪存进行存储。因此,它们的性能受限于Flash的性能,并且它们并未针对下一代NVM存储设备(如Optane)进行优化(请参阅第5.1节)。其他系统,例如HiKV [33],具有混合DRAM-Flash索引,但仅针对单个节点部署,因此将它们的应用程序限制在更广泛的操作范围内,例如改组。此外,这些KV存储的设计是针对数百字节至最大MB的非常小的数据集量身定制的,因此将它们的操作窗口限制为这些对象大小。

分布式数据存储:Ceph-over-Accelio [8]和Octopus [23]等存储系统是用于快速网络和NVM设备的高性能分布式文件系统。但是,由于专注于提供容错的,持久的存储,它们在小对象上的性能很差(请参见第5节)。最近提出的Regions系统提供了对远程内存的文件抽象[6]。但是,与其他内存存储系统一样,Regions不是存储大型数据集的经济有效的解决方案。诸如Alluxio [1]和Pocket [17]之类的系统提供对多种存储技术类型的支持。但是,Alluxio无法提供高端硬件的性能,而是旨在构建本地缓存。Pocket拥有我们的目标,即专用于存储临时数据的存储系统,其设计与NodeKernel架构相似。但是,Pocket的重点是在云中的商品硬件上提供高效且灵活的临时存储,而NodeKernel是为低延迟高带宽网络和存储硬件而设计的。此外,Pocket仅具有基于对象的I/O接口,非常适合在无服务器工作负载中进行数据共享。相比之下,NodeKernel的统一API提供了诸如“append”和“bag”之类的语义,以支持在不同的工作负载中存储各种临时数据。

临时数据特定操作:许多工作可加速数据处理工作负载中的特定存储操作。例如,Riffle [37]是一种优化的shuffle服务器,旨在减少与大型扇出相关的开销。Sailfish [27]是一个引入I-files的框架,这些I-files是经过随机优化的数据容器。ThemisMR [28]还旨在优化shuffle并针对小型机架规模的部署。通常,这些系统的目标是针对map-reduce型工作负载优化基于磁盘的,面向文件的shuffle数据管理。尚不清楚它们的设计如何支持其他通信模式,例如broadcast和multicast,或如何集成不同的存储类型以针对不同的访问模式进行优化。并行数据库[7,22]对数据库操作符使用RDMA优化的Shuffle操作。但是,这些工作是高度特定于数据库,不能自然地扩展到其他数据处理工作负载或其他形式的临时数据。

3.NodeKernel体系结构

我们介绍了NodeKernel,这是一种新的存储体系结构,旨在满足数据处理工作负载中临时数据存储的各种复杂需求。NodeKernel通过融合存储语义来解决这一挑战,这些语义在文件系统和键值存储中是可以单独使用的,例如分层命名,可伸缩性,多个存储层,数据集的快速枚举以及对小数据和大数据大小的支持(图2)。
图片.png
NodeKernel体系结构遵循以下三个设计原则:
1.将高级存储语义提取到通用存储内核之上的薄层中。
2.将数据管理问题分为轻量级的元数据平面和为现代网络和存储硬件优化的“哑”数据平面。
3.利用多种存储技术来高效存储大型数据集。
在第4节中描述Crail(NodeKernel架构的具体实现)之前,我们将在下面更详细地讨论每种设计原则。

3.1 存储内核和节点类型

在NodeKernel体系结构中,更高级别的存储语义在共享存储内核之上实现为薄层(或更确切地说,作为专用数据类型),并导出不透明数据“节点”的分层命名空间。节点是抽象类型Node的对象,如以下代码片段所示。
图片.png
内核负责代表节点分配存储资源,操纵分层名称空间以及实现诸如read,append和update之类的基本数据访问操作。应用程序与存储内核交互,以在层次结构中的给定位置创建数据节点,将任意大小的数据附加到节点,查找节点,并从节点获取关联的数据集。节点使用编码存储层次结构中位置的路径名来标识,类似于文件系统中的文件和目录。

应用程序不会直接创建原始Node对象,而是会创建提供特殊功能的派生类型的对象。这些所谓的自定义类型通过两种方式扩展Node来实现更高级别的存储语义。首先,自定义类型为抽象操作addChild和removeChild提供实现。每当在存储层次结构中插入新节点或从中删除新节点时,内核就会调用这些操作。其次,自定义数据类型提供了特殊的数据访问操作,这些操作使用Node中可用的read和append实现。

NodeKernel定义了五种自定义节点类型,每种类型都提供了稍微不同的语义和操作:
(1)File和KeyValue:这两种节点类型都通过在Node中公开相应的操作来提供read和append接口。但是,这两种类型在创建和插入新节点的过程中提供了不同的语义,这些语义是通过addChild和removeChild的实现来控制的。对于文件节点,针对给定路径名的第一个创建操作成功,而对相同路径名的后续创建操作失败。对于KeyValue节点,将在表示现有节点的路径名上进行后续的创建操作,从而替换现有节点。如第5节所述,KeyValue节点对于在NoSQL工作负载中缓存输入数据集非常有用,允许并发更新数据,而File节点更适合在Spark工作负载中缓存只读输入数据。
(2)Directory和Table:这些节点类型分别是File和KeyValue的容器。Directory和Table节点将其所有子项的名称成分存储为数据的一部分(使用Node中可用的append和update操作来实现)。例如,路径名称为“/a/b”的目录的数据段存储了两个路径名称为“/a/b/c”和“/a/b/c2”的文件,其中包括两个名称成分“c1” 和“c2”。 Directory和Table节点均提供操作以枚举其所有子节点的名称(使用Node中可用的read来实现)。可以选择将表配置为“不可枚举”,在这种情况下,不存储名称组件,枚举返回空集。在不可枚举的表中创建KeyValue节点通常更快,因为它省去了更新名称组件的步骤(如第4.1节所述)。 
(3)Bag:Bag节点类型旨在支持有效地顺序读取分布在许多数据节点上的数据。 Bag的行为类似于目录,因此它充当File节点的容器。应用程序在Bag中创建和写入文件的方式与在Directory中创建和写入文件的方式相同。但是,在读取Bag时,Bag像单个文件一样出现在应用程序中。使用Bag的读取操作,应用程序可以依次读取Bag中的全部文件。通常,由于在文件边界处更有效的元数据访问,因此Bag的读取操作比单独读取每个File节点提供更好的性能。正如我们将在第5节中看到的那样,Spark应用程序可以使用Bags存储转换数据,从而允许简化任务有效地获取由map任务编写的数据。 NodeKernel中的Bag类型在精神上与Hurricane中的bag类似,这是最近在抑制数据处理工作负载中的偏斜上的一项工作[9]。

NodeKernel限制了可以堆叠节点类型的方式。
例如,KeyValue节点只能附加到Table节点,而File节点只能附加到Bag和Directory节点。此外,Directory可以任意嵌套,而bag和table实现平面名称空间。允许的节点类型组合与临时数据存储的特定用例匹配。同时,防止节点类型的任意组合可确保体系结构不会过度设计且易于实现。例如,Bag要用于存储按单位存储桶组织的随机数据,因此,启用任意嵌套的Bag似乎是不必要的。当时,对扁平Bag的读取操作比对任意嵌套的Bag的读取操作更容易实现。

为什么要提供一个统一的存储名称空间?
通过在通用存储内核和一组自定义数据类型中拆分功能,NodeKernel实现了两件事。首先,它允许由单个存储平台管理需要不同语义的不同类型的临时数据。例如,稍后我们将看到,Spark应用程序可以使用Crail来存储broadcast和shuffle数据,以及缓存RDD。其次,将核心数据访问与存储语义脱钩,使应用程序可以根据所需的语义(键/值与文件)而不是根据数据的大小或访问模式来选择特定的节点类型。如第2.1节所述,即使在单个工作负载内,临时数据通常也会在数据大小和访问模式方面有所不同,从而难以将数据有效地存储在文件系统或键值存储之类的存储平台中。相比之下,NodeKernel中的节点类型没有大小限制,并且可以为不同的访问模式提供有效的数据访问,我们将在后面的第5节中看到。

3.2 系统架构

图2说明了NodeKernel系统架构。在数据管理级别,NodeKernel的体系结构类似于HDFS或GFS之类的分布式文件系统的体系结构,该体系结构由跨集群部署的一组元数据和存储服务器组成。附加到存储层次结构中“Node”的数据在客户端以流的形式出现,但内部数据是由一系列块组成的。块是指存储在其中一台存储服务器中的固定字节序列。元数据服务器维护分层存储名称空间以及块元数据,即存储块和存储服务器之间的映射。存储服务器在启动时会分配大量存储块,并向其中一个元数据服务器注册。元数据服务器维护一个空闲列表,其中包含未分配给特定节点的块,并在数据写入和追加期间将块从空闲列表移至每个节点列表。在访问数据时,客户端首先联系元数据服务器之一,并请求相应块的元数据。然后,基于此信息,客户端将与给定的存储服务器联系以读取或写入数据。

Node抽象数据类型导出两个抽象操作,addChild和removeChild,将由第3.1节中描述的派生类型实现。每当创建或删除新节点时,这些操作就会在元数据服务器上执行。节点进一步导出功能来操纵数据,例如read,append和update。这些功能被实现为客户端库的一部分,并且需要与元数据和存储服务器进行交互。最后,基本的元数据操作(如getPath或size)也由客户端库(如果可能)返回缓存的值来实现。

部署目标:NodeKernel以临时数据为目标,这些数据是短期的并且易于重新生成。此外,NodeKernel的目标是由少量计算机架组成的中小型部署。考虑到目标部署和要存储的数据的性质,NodeKernel优先考虑性能而不是容错能力。但是,如果认为有必要,可以添加其他的容错机制,例如复制,擦除编码等。缺少这些功能不会在根本上影响设计。

性能挑战:NodeKernel的主要挑战是提出一种能够满足第2.1节中讨论的全部需求的系统体系结构。特别是,尽管提供了方便的分层名称空间,该体系结构仍应该像键值存储一样可伸缩。此外,该体系结构应支持低延迟键值样式访问以及高带宽文件系统(如数据访问)。接下来,我们讨论如何在NodeKernel体系结构中满足这些要求。

3.2.1低延迟元数据操作

将文件系统和键值语义融合到单个存储内核中,首先需要快速的元数据访问。在第2.1节中,我们观察到分布式文件系统的软件体系结构与键值存储的根本区别仅在于将文件流中的偏移量映射到存储资源(例如存储服务器上的块)所需的额外元数据操作。因此,将元数据操作的开销保持在较低水平将使NodeKernel的体系结构适合小型数据集上的键值形式的操作,同时还可以提高大型数据访问的效率。

如今,现代的低延迟网络硬件能够以几ms的延迟实现RPC通信[13,16]。NodeKernel具有轻量级的元数据平面,可与低延迟网络硬件很好地匹配。首先,将元数据平面缩减为仅提供由六个关键RPC操作组成的最关键功能:create以创建新节点,lookup以检索节点的元数据,remove以删除现有节点,move将节点移动到存储层次结构中的其他位置,map以将节点数据流中的逻辑偏移量映射到存储资源,register以供存储服务器在元数据服务器中注册存储资源。所有这些操作都具有较低的计算和I/O强度,并且可以在高性能网络结构上以几ms的延迟来实现。我们特意将诸如enumeration之类的数据密集型元数据操作移至数据平面,以避免产生干扰(请参阅第3.1和4节)。

3.2.2元数据分区

NodeKernel的可伸缩性在很大程度上取决于其元数据RPC子系统的吞吐量和可伸缩性。最近的工作表明,一方面可以使用高性能网络硬件将RPC系统的规模扩展至每秒上百万次操作[15];另一方面,还可以在商品硬件上使用高效的软件堆栈[13]。NodeKernel中的轻量级RPC接口旨在实现高吞吐量,并且使用单个元数据服务器每秒可驱动多达1000万个元数据操作,如第5节中稍后所述。到目前为止,在我们所有的部署中,我们从未有过单个元数据服务器将达到其极限的情况。实际上,在我们最大的128个节点部署中,元数据每秒可达到450万次操作,仅大约达到单个元数据服务器可支持的一半。

但是,对于仅一个元数据服务器不足的情况,NodeKernel允许在多个服务器上划分元数据空间。从而,顶级根名称空间在元数据服务器的有序列表中进行了哈希分区。使用元数据分区,可以假设足够大的顶级扇出水平地水平缩放元数据平面。

基于顶级名称空间进行静态分区的一个缺点是,取决于子树的大小和子树内的活动,负载可能会在元数据服务器之间不均匀地分布。一种替代方法是在更细粒度的级别进行动态分区。例如,过去的工作提出了一种针对文件系统的分区方案,其中在目录级别实施元数据分区,并且可以选择在大型目录过大时按需拆分大型目录,并将拆分内容分布在多个元数据服务器上[29]。尽管这种方法创建了更均匀的负载分配,但由于在路径查找和遍历期间需要多次RPC调用,因此付出了可观的性能代价。给定NodeKernel中5-10 µs的性能目标,我们最终决定采用更简单的分区方案,其中节点路径始终位于元数据服务器本地。

3.2.3硬件加速的存储

NodeKernel的数据平面旨在与现代网络和存储硬件完美配合。目的是使存储接口保持简单,以避免过多的软件开销,并允许在硬件中实现尽可能多的数据访问功能。NodeKernel中的客户端通过两个接口与本地和远程存储服务器进行交互:read(blockid, offset, length, buffer)以从块中获取一定数量的字节,以及write(blockid, offset, buffer, length)以进行写入存储在块中的缓冲区中的数据。在第4节的后面,我们将展示Crail中的read和write操作几乎可以完全转移到DRAM和Flash的网络和存储硬件上。还要注意,存储接口通过在字节级别(而不是块级别)定义访问粒度,显式支持字节可调整的存储硬件。

3.2.4分层存储

NodeKernel采用简单的分层存储设计来容纳无法以经济高效的方式存储在DRAM中的大型数据集。存储服务器分为不同的类别(请参见图2),通常是每种存储技术(DRAM,NVMe SSD,HDD等)的类别。存储服务器是一个逻辑实体,即一台物理或虚拟机可以托管多个不同类型的存储服务器。例如,一种常见的部署是每个主机运行两台存储服务器,一台导出一些本地DRAM,一台导出本地NVMe SSD上的存储空间。原则上,存储类是用户定义的存储服务器集。一台存储服务器始终完全属于一个存储类。在我们的评估中,我们配置了两种存储类别,一种用于DRAM服务器,另一种用于NVMe SSD服务器。

传统的存储分层方法是随着更快的存储类别填满,将数据迁移到更具成本效益的存储类别。由于数据的生命周期短,我们发现该策略对临时数据无效。相反,我们选择了一种更简单的方法,即根据用户定义的顺序填充存储类(通常先是DRAM,然后是闪存和硬盘),而无需在层之间迁移数据。特别是在数据写入操作期间,元数据服务器首先尝试分配DRAM块,只有在整个集群中没有更高优先级的存储块可用时,才转向较低优先级的存储层。

4. Crail

Crail是NodeKernel体系结构的具体实现。我们用大约1万行Java和C ++代码实现了Crail。表2显示了Crail的应用程序界面。Crail中的顶级数据类型是CrailStore。应用程序使用CrailStore来create,lookup和delete数据节点,或将数据节点move到存储层次结构中的其他位置。使用类似于文件系统的路径名来标识节点。使用create创建新节点时,应用程序可以为要存储的数据选择首选的存储类(表2中的参数“ sc”/storage class)。我们也将存储类首选项称为存储亲和力,因为它允许用户为特定的一组数据指定对一组特定的存储服务器或存储介质的亲和力。
图片.png
Crail实现了第3.1节中讨论的完整节点类型集。请注意,Crail中的所有操作都是非阻塞且异步的(返回future对象)。Crail的异步API与用于现代网络和存储硬件的异步软件接口非常匹配。实际上,几乎所有Crail的高级操作都可以直接映射到一组非阻塞的异步网络和存储操作。通过无效的futures或异常(表2中未显示)来传达Crail API调用的失败。例如,尝试lookup不存在的节点将导致无效的future (nullpointer),而尝试从超出节点容量的节点read数据将导致异常。

Crail既可以作为共享存储服务使用,也可以按用户或按应用程序部署的形式进行操作。但是,Crail的当前实现没有提供任何工具来虚拟化,保护和隔离多个租户。

4.1 元数据平面

Crail元数据服务器在内存中表示以下内容:(a)存储层次结构,(b)空闲块集,以及(c)块对“节点”的分配。具体而言,每个元数据服务器维护每个存储类的可用块列表。存储类根据用户定义的优先级进行排序。如第3.2节所述,如果在写操作期间当前写位置尚未指向已分配的块,则客户端将通过调用元数据map RPC操作来请求一个新的新块。元数据服务器根据所选的存储关联性(表2中的“ sc”)选择一个空闲块。如果所选存储类别中没有空闲块,则元数据服务器将尝试从优先级列表中的下一个存储类别分配一个块。在存储类中选择块时,元数据服务器在给定存储类中的所有存储服务器上使用循环,以确保在集群中均匀地分发数据。如果任何存储类中没有可用块,则客户端的写操作将失败。

在3.2.1节中讨论的Crail跨元数据服务器阵列对元数据进行分区,这意味着,每个元数据服务器负责存储层次结构的分区。每个服务器都使用DaRPC [31]实现为轻量级RPC服务,该服务是基于RDMA发送/接收的异步低延迟RPC库。为了实现高吞吐量,客户端连接在不同的CPU内核之间进行了分区。每个内核在单个流程上下文中就地管理客户端连接的子集,以避免上下文切换开销。RPC缓冲区的所有内存均分配给与负责特定连接的给定CPU内核关联的NUMA节点本地。图3说明了Crail中元数据处理的不同方面。
图片.png
Enumeration:在第3.1节中,我们讨论了容器节点(Table, Directory, Bag)如何维护所有子节点的名称列表作为其数据的一部分。该设计背后的理由是,它允许我们在数据平面中有效地实现容器枚举。我们已经看到一些用例,其中在Spark中存储Spark shuffle数据会在Bag中生成近十万个File节点。在元数据服务器上实现枚举将导致客户端和元数据服务器之间的大量数据传输,并且在许多情况下将需要进行多轮RPC枚举所有节点。
Crail用于容器节点的文件格式由固定大小的记录组成,这些记录由子项的名称组成部分和有效标志组成。创建新节点后,元数据服务器会在数组内分配一个唯一的偏移量,客户端将根据该偏移量写入相应的记录。在删除操作期间,在元数据服务器删除了节点条目之后,客户端将清除相应记录的有效标志(通过将记录中的有效位清零)。请注意,容器数据中的节点记录仅被视为补充信息。元数据服务器始终充当验证节点是否存在的权限。因此,与删除操作同时运行的枚举操作可能会导致以下情况:目录文件中的节点记录仍然有效,但是该节点在元数据服务器上的元数据状态已被删除。在这种情况下,该节点被视为已删除,并且不允许进行任何读或写操作。

4.2 数据平面

Crail实现了两种存储类别,一种用于DRAM,另一种用于基于NVMe的SSD。存储类的实现包括一个服务器部分,该服务器部分导出一个存储资源(请参阅第3.2.3节),以及一个客户端部分,用于实现有效的数据访问。

RDMA存储类:RDMA存储类中的存储服务器将RDMA注册内存的较大区域导出到元数据服务器。基于RDMA的存储块的元数据包含必要的RDMA凭证,例如地址,长度和标记,并允许客户端使用RDMA单边读/写直接读取或写入存储块。

NVMe-over-Fabrics存储类:NVMe-over-Fabrics(NVMf)是NVMe标准的最新扩展,该标准允许通过支持RDMA的网络访问远程NVMe设备。它消除了沿I/O路径到远程设备的不必要的协议转换,从而将NVMe的多个成对队列设计直接暴露给客户端。与RDMA一样,队列对可以直接映射到应用程序上下文中,以避免内核开销。
Crail NVMf存储服务器通过连接到控制器并将其凭据(如NVMe合格名称,大小等)报告到元数据服务器,充当NVMf控制器的控制平面。借助元数据服务器提供的凭据,客户端可以直接连接到NVMf控制器并执行块读取和写入操作。

4.3故障语义和持久性

Crail当前未实现容错机制(请参见第3.2节),因此无法防止机器或硬件故障。在存储服务器崩溃时,相应的数据块会丢失。在元数据服务器崩溃时,相应的元数据分区会丢失。元数据服务器根据保持活动消息从活动服务器列表中删除不可访问的存储服务器,并确保在块分配期间仅考虑活动服务器。
Crail提供了可选的机制来持久存储DRAM中存储的数据,关闭Crail部署并从先前的持久状态开始Crail部署。持久性是通过元数据服务器上的操作日志记录以及在存储服务器上使用内存映射的持久性存储来实现的。

4.4数据访问剖析

图4说明了Crail客户端如何代表应用程序从File或KeyValue节点读取数据的情况下与存储服务器和元数据服务器进行交互。应用程序首先调用lookup来检索节点句柄,从而使Crail通过RPC从元数据服务器中获取必要的元数据。元数据包含有关节点的信息,例如数据大小和第一个块的位置。成功执行lookup调用后,应用程序发出read操作以从节点读取一定数量的字节。请求的字节数可能少于一个块。在那种情况下,单个RDMA或NVMf操作将足以完成请求。如果所请求的字节数产生了多个块(如本例所示),则Crail立即为第一个块发出数据传输,同时并行请求下一个块的元数据。在正常情况下-由于客户端和元数据服务器之间的基于RDMA的低延迟协议-元数据请求将在当前块传输之前完成,并确保继续进行数据传输,而客户端不必等待丢失的元数据信息。
图片.png

5. 评估

在我们的评估中,我们评估Crail是否满足第2.1节中讨论的临时存储平台的要求。具体来说,我们回答以下问题:
1.Crail及其额外的间接层的统一抽象对于高性能设备上的各种数据大小是否性能良好? (第5.1节)
2.将更高级别的工作负载(及其临时数据访问)映射到Crail有多简单? (第5.2节)
3.用于数据处理框架的混合介质存储系统的性能和成本收益有多大? (第5.3节)

集群配置:我们使用八个x64节点的集群,以及两个Intel®Xeon®CPU E5-2690 v1 @ 2.90GHz CPU,96GB DDR3 DRAM和100 Gbit/s Mellanox ConnectX-5 RoCE RDMA 网卡。对于客户端服务器微基准测试,服务器配置了4个Intel Optanane 900P SSD,但IOPS实验除外,在IOPS实验中,每个服务器仅使用2个Optane驱动器。对于较大的集群实验,所有8个节点均配备4个Samsung 960 Pro SSD。这些节点运行Linux内核版本为4.10.0-33-generic的Ubuntu 16.04.3 LTS(Xenial Xerus)和Java 8。

5.1 微基准测试

小型和中型数据:我们首先要评估Crail存储小型和中型值的数据,这种情况通常是键值存储很好地满足的用例。因此,我们将Crail的性能(延迟和IOPS)与两个最先进的开源键值存储(RAMCloud(用于DRAM存储)和Aerospike(用于NVM Optane))进行了比较。图5显示了不同数据大小的get和put操作的性能。在Crail中,通过使用create API调用(请参见表2)创建KeyValue节点,然后执行append操作来实现put操作。get操作是使用lookup调用实现的,然后在KeyValue节点上执行read,与图4中所示的情况类似。对于小型数据(4字节),Crail在DRAM存储方面的性能比RAMCloud稍差(12 µs vs 6 µs),但在Optane NVM上的表现优于Aerospike,幅度为2-4x(Crail为23-40 µs,Aerospike为100 µs)。Aerospike和Crail之间的差异来自其I/O执行方面的差异。Aerospike使用同步I/O和多个I/O线程,这会导致占用时间并在同步功能中花费大量执行时间[18]。Crail使用异步I/O并在一个上下文中执行I/O请求,从而完全避免了上下文切换和同步。考虑到RAMCloud是为小型数据而优化的系统,Crail和RAMCloud之间的延迟差异是可以接受的。对于中等大小的数据(64KB-1MB),Crail优于RAM-Cloud和Aerospike,幅度为2-6.8倍。例如,一个1MB的put耗时约为590 µs,而Aerospike则为4 ms。这些性能提升来自有效使用RDMA单边操作(用于DRAM和NVM),它消除了客户端和服务器端的数据副本,并通常减少了在put/get操作期间执行的代码路径。尽管Crail本机支持任意大小的数据集(通过将块分布在多个存储服务器上),但是在RAMCloud或Aerospike这样的系统中存储如此大的值是困难的或被禁止的。例如,Aerospike将单个键/值对限制为1MB。RAMCloud没有严格的大小限制,但是无法存储大于4MB的值。
图片.png
为了完整起见,图5还显示了超大数据16MB和128MB的put/get延迟。如我们所见,在Crail的DRAM层中存储128MB值大约需要12ms,而在Crail的Optane层中存储相同的数据集大约需要20ms。在Crail中存储如此大的数据值完全取决于吞吐量。因此,远程DRAM延迟受100 Gb/s网络带宽的限制,而NVM延迟则由存储设备的带宽确定。4个Optane驱动器的总带宽约为10-12 GB/s。因此,存储在Crail的NVM层中的大型数据集的数据访问带宽为80-87 Gb/s。

IOPS缩放:到目前为止,我们已经讨论了卸载延迟。图6显示了加载的Crail系统针对不同介质类型的256字节值的延迟性能测试结果。在此设置中,我们将客户端的数量从1个增加到64个。客户端在16台物理计算机上运行,并在紧密的循环中发出put/获取操作。在此设置中,我们仅使用一台存储服务器和一台元数据服务器,它们被配置为服务于DRAM,Optane NVM或闪存。图6的第一行显示了队列深度为1的情况,这意味着每个客户端在运行中始终只有一个操作。如图所示,Crail可以提供稳定的延迟,并达到合理的高吞吐量。对于DRAM,高达4M IOPS的获取等待时间(图6右上方)保持在12-15 µs,这时元数据服务器成为瓶颈。我们对多个元数据服务器进行了相同的实验,并验证了系统吞吐量是线性扩展的(如上图7所示)。对于Optane NVM配置,等待时间一直保持20 µs,直到接近1M IOPS,这非常接近设备限制。闪存延迟较高,但三星驱动器也具有较高的吞吐量限制。实际上,队列深度为1的64个客户端无法使Samsung设备饱和。为了产生更高的负载,我们测量了每个客户端始终进行四个操作的情况下的吞吐量和延迟(队列深度4,图6中的底行)。如图所示,队列深度4通常达到较高的吞吐量,直到达到硬件极限,设备队列过载(例如,对于NVM Optane)和等待时间长。例如,在延迟呈指数增长之前,Crail在4.2M IOPS(DRAM)上获得30.1 µs的延迟(图6右下),对于110万IOPS(Optane)而言为60.7 µs,对于640.3K IOPS(闪存)的延迟为99.86 µs。放置情况类似,尽管通常性能较低。
图片.png
元数据性能:在图7(顶部)中,我们对用于在Crail中检索节点元数据的简单查找元数据操作的性能进行了基准测试,并将其与在Octopus中类似的元数据操作getattr的性能进行了比较[23]( 在DRAM中运行的经过RDMA优化的NVM文件系统)。这里有两个主要的观察。首先,对于单个名称节点,Crail的性能比Octopus高1.7-5.9倍。Crail中的单个namenode峰值约为9.3M lookup/s。其次,Crail可以非常有效地将单个名称节点的性能扩展到多个名称节点的设置。系统可以为2个和4个名称节点配置提供每秒高达16.7M和27.4M的查询速度。
图片.png
访问大型数据集:与在Octopus和Alluxio中进行文件读取操作相比,图7(底部)显示了在Crail中读取File节点的大型数据集时测得的带宽(y轴)。图7中的x轴表示客户端在读取操作期间正在使用的应用程序缓冲区的大小。Crail具有高效的数据和元数据平面,并且查找RPC和数据获取的重叠也很快达到了网络带宽限制,即使是相对较小的缓冲区大小(刚好超过1kB)。由于数据复制和网络堆栈实施效率低下,Alluxio的性能受到CPU的限制。Octopus的性能优于Alluxio,对于大型缓冲区(接近1MB),其线速度逐渐达到98 Gb/s。请注意,图7中的Crail峰值带宽(98 Gb/s)优于图5中的峰值带宽(87 Gb/s),因为对于KV实验,每个KeyValue节点都是打开,读取和关闭的,而对于文件 实验将这些访问权摊销。

总结:在本节中,我们证明了Crail可以有效地存储大型数据值,同时提供比其他针对特定数据范围进行了优化的最新系统更可比或更高的性能。

5.2 系统级基准测试

5.2.1 NoSQL工作负载

Yahoo! 云服务基准(YCSB)是一个开放标准,旨在比较NoSQL数据库的性能[10]。它带有五种强调不同属性的工作负载,例如 工作负载A的更新繁重,而工作负载C的读取繁重。我们选择工作负载B,将Crail与RAMCloud和Aerospike进行比较。工作负载B具有95%的读取和5%的更新操作,并且使用Zipfian分布选择记录。所有系统都在单个命名节点/数据节点配置中运行。本实验的目的是评估Crail的延迟配置文件,以了解超出上一节中介绍的微基准的实际工作负载。
图片.png
图8(顶部)显示了使用单个客户端默认设置10条字段(每条记录100字节,每KV对1K)的工作负载B的读取和更新延迟分布。图的左侧显示读取性能,右侧显示更新性能。如该默认设置所示,最大数量的读取操作观察到的延迟分别为14 µs(95%和99%的百分数分别为37µs和84 µs)和26 µs(95%和99%的百分数分别为47µs和81µs) 分别用于DRAM和Optane。Optane上的Crail的平均延迟为38 µs,因此仅比DRAM上的Crail慢15 µs。另一方面,带有Optane的Aerospike的平均延迟为108.7 µs,比Crail平均延迟(38.03 µs)差了2.84倍。将Crail的DRAM性能与RAMCloud进行比较,可以看出RAM-Cloud的速度比Crail快。但是,随着我们在图8(底部)中移到更大的10个字段(每个10KB,每个KV对100KB)的较大值,Crail分别比Aerospike和RAMCloud高出2.6-4.8倍。

总结:我们使用YCSB基准进行的实验表明,(a)Crail可以成功地将原始DRAM / NVMe的性能优势转化为工作负载级别的收益,并且(b)Crail可以有效地处理小型和大型数据,而RAMCloud和Aerospike可以在特定的操作范围内发挥最佳性能 。

5.2.2 Spark集成

我们介绍了Spark(最流行的数据处理引擎之一)对Crail的评估。Spark通过一系列map-reduce的步骤来执行工作负载,同时在每个步骤中共享性能关键数据。Spark数据处理流水线中有多个点可以生成临时数据集。在本节中,我们将显示专门针对以下两个方面的性能指标:shuffle和broadcast。这两个子系统都可以轻松地实现为Spark的插件模块。
图片.png
broadcast:通过将broadcast数据存储为不可枚举的Table中的KeyValue节点,我们使用Crail实现了broadcast。broadcast写入器创建一个新的KeyValue节点,将broadcast数据附加到该节点,然后将节点“名称”传递给读取器。分布在Spark执行程序内部多台机器上的读取器对“名称”进行查找,并从Crail读取数据。图10a显示了结果。x轴显示在Spark作业中不同broadcast读取器观察到的延迟,而y轴显示读取器的百分比。垂直实线表示基线时延为12 µs,这在我们的微基准测试中得到了证明。如图所示,大多数Crail broadcast读取器观察到的延迟非常接近最小可能的延迟。少数读取器观察到低于12 µs的等待时间,因为其中一些读取器位于存储值的同一台物理机器上。对于这些节点,即使它们仍然使用本地网络接口读取数据,也没有发生实际的网络传输,因此,它们的读取性能不受网络的限制。总而言之,Crail broadcast性能比默认的Spark实施好1-2个数量级。

shuffle:在Spark中,shuffle写入器在映射阶段会不断生成shuffle数据,因为它会处理输入数据集并将数据分类到不同的存储桶中,这些存储桶随后将由reducer读取。由于大型的扇入和扇出访问模式,我们使用Crail Bag节点实现了shuffle。每个reducer有一个Bag节点,每个shuffle写入器将数据追加到一组私有File节点的数组中,每个bag每个写入器一个File节点。映射阶段之后,每个读取使用Bag节点类型中可用的优化读取接口读取其关联的bag(请参阅第3.1节)。我们生成了大量数据(512 GB),并使用Spark源代码中提供的GroupBy基准触发了shuffle操作。图10b和10c显示了各种配置的性能(x轴上的运行时)和观察到的网络吞吐量(y轴)。值1、4或8表示分配给每个Spark执行器的内核数。对这两个图进行快速比较可以看出,Crail加速的Spark观察到更高的网络吞吐量(对于相应的内核数),因此,运行时间更好(1个内核5倍,4个内核2.5倍和8个内核2倍)。

总结:在本节中,我们证明了Crail能够通过利用不同的节点类型(KeyValue和Bag)成功地针对较小的值(例如broadcast)和较大的值(例如shuffle)加速Spark中的临时数据访问。

5.3 混合DRAM / NVM配置的效率

在评估的最后一部分,我们将量化Crail的分层数据平面如何帮助实现性能和成本目标。我们考虑Terasort工作负载,它是Spark上最I/O密集型应用程序之一。我们分两个阶段将Terasort实现为外部范围-分区排序算法。第一阶段将传入的键值对(10个字节的键和90个字节的值)映射到外部存储桶中。然后,将这些存储桶按单独的reduce任务进行shuffle和分类。为了进行评估,我们使用了先前开发的加速shuffle和broadcast插件。

在图9中,我们探索了使用NVM而不是DRAM在200 GB Spark排序工作负载中存储随机数据的性能/成本折衷。为此,我们为Crail配置了DRAM和Flash存储层不同的存储限制。x轴表示在DRAM与Flash中存储的总shuffle数据中的哪一部分。请注意,在本实验中,我们使用的是基于Samsung Flash的SSD,而不是Optane设备。10/90的配置意味着10%的数据保存在DRAM中,而90%的数据保存在闪存中。该图还显示了完全在DRAM(使用tmpfs作为存储后端)中完全在其默认shuffle引擎上运行的原始版本Spark(该图的第一条)的性能。这里有两个主要观察结果。首先,与原始版本Spark相比,将Crail用于shuffle后端已经将运行时间减少了3.4倍。这种性能提升可归因于Crail中高效使用高性能网络和存储硬件。例如,在reduce阶段,我们测得了70 Gb/s/台机器的所有网络吞吐量。其次,随着我们减少Crail中DRAM的比例,转而使用Flash,Spark会优雅地自动将随机数据泄漏到Flash层中。在所有随机数据都存储在Flash中的极端配置中,性能下降到46.49秒(提高了48%),而总存储成本则从1,000美元减少到126美元(存储200 GB数据)减少了8倍。根据表1中的数字)。数据从DRAM到闪存的逐渐溢出是透明发生的。即使在全闪存配置中,Crail集成的Spark Terasort的性能也只有完全内置DRAM的Vanilla Spark性能的一半。这些结果验证了我们在Crail中所做的设计选择,这些选择允许以交易性能换取存储成本。
图片.png
总结:在本节中,我们证明了在Spark中使用Crail(i)会由于其高效的I/O路径而带来更好的性能;(ii)由于采用了DRAM-NVMe混合架构,因此降低了存储成本,并提高了性能。

6. 结论

在数据处理工作负载中有效地存储和访问临时数据对于性能至关重要,但由于复杂的存储需求介于文件系统或键值存储之类的现有存储系统之间,因此仍然具有挑战性。我们介绍了NodeKernel,这是一种新颖的存储体系结构,它通过将分层命名与可伸缩性和出色的性能相结合,为各种临时数据所特有的各种数据大小和访问模式提供了新的存储设计空间。NodeKernel体系结构受现代网络和存储硬件机会的推动,这使我们能够减少开销,而这种开销过去使这种设计变得不切实际。我们表明,将临时数据存储在Crail中,这是我们利用RDMA网络和NVMe存储对NodeKernel架构进行的具体实现,可以将NoSQL工作负载提高4.8倍,将Spark应用程序性能提高3.4倍。与仅使用DRAM的存储系统相比,Crail使用NVMe Flash可以进一步将存储成本降低多达8倍。

Reference:
[1]Patrick Stuedi, Animesh Trivedi, Jonas Pfefferle, Ana Klimovic, Adrian Schuepbach, and Bernard Metzler. 2019. Unification of Temporary Storage in the NodeKernel Architecture. In Proceedings of the 2019 USENIX Annual Technical Conference, 767–781.