Spark内核之美(一):RDD的原理与源码分析

Spark 是加州大学伯克利分校A岛。实验室( Algorithms 、Machines and People Lab )开发的通用大数据处理框架。Spark 生态系统也称为BDAS , 是伯克利APM 实验室所开发的,力图在算法( Algorithms )、机器( Machines )和人( People ) 三者之间通过大规模集成来展现大数据应用的一个开源平台。以下内容是对加州大学伯克利分校论文《Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing》翻译及自己收集资料的注解,个人能力有限,如有错误地方,麻烦批评纠正!

摘要

本文提出了一种弹性分布式数据集RDD(Resilient Distributed Datasets),这是一种分布式内存抽象,它允许程序员以容错的方式在大型集群上进行内存计算。 RDD由当前计算框架无效的两种类型的应用程序驱动:迭代算法和交互式数据挖掘工具。 在这两种情况下,将数据保存在内存中可以将性能提高一个数量级。 为了有效地实现容错,RDD基于粗粒度转换而不是对共享状态的细粒度更新提供受限形式的共享内存—— 即RDD是只读的,并且只能通过其他RDD上的批量操作来创建,同时,其还可以由外部存储系数据集创建,例如HDFS。为了高效和容错,RDD通过共享内存的方式来实现,但是,我们表明RDD足以捕获大量计算,包括最近专门的编程模型,如Pregel,以及这些模型无法捕获的新应用程序。 我们在名为Spark的系统中实现了RDD,我们通过各种用户应用程序和基准进行评估。目前,在实际的工程中,我们实现的RDD在迭代计算方面比Hadoop快20至50倍,甚至更快,可以在秒级实现交互式地查询1TB数据集。

Spark内核之美(一):RDD的原理与源码分析_第1张图片

1 引言

诸如mapreduce[10]和dryad[19]等集群计算框架已被广泛用于大规模数据分析。这些系统允许用户使用一组高级操作员编写并行计算,而不必担心工作分布和容错性。尽管当前的框架为访问集群的计算资源提供了许多AB策略,但它们缺乏利用分布式内存的抽象。这使得它们不适合于一类重要的新兴应用程序:那些跨多个计算重用中间结果的应用程序。数据重用在许多迭代机器学习和图形算法中很常见,包括pagerank、k-均值聚类和logistic回归。另一个引人注目的用例是交互式数据挖掘,用户在同一数据子集上运行多个即席查询。不幸的是,在大多数当前框架中,在计算之间(例如,两个mapreduce作业之间)重用数据的唯一方法是将数据写入外部稳定存储系统,例如,分布式文件系统。由于数据复制、磁盘I/O和串行化,这会导致大量开销,而串行化可能会控制应用程序的执行时间。认识到这个问题,研究人员为一些需要数据重用的应用程序开发了专门的框架。例如,pregel[22]是一个用于迭代图形计算的系统,它将中间数据保存在内存中,而haloop[7]提供了一个迭代映射接口。然而,这些框架只支持特定的计算模式(例如,循环一系列mapreduce步骤),并隐式地为这些模式执行数据共享。它们不为更一般的重用提供抽象,例如,让用户将几个数据集加载到内存中,并跨它们运行特别查询。

在本文中,我们提出了一种新的抽象,称为弹性分布式数据集(RDD),它可以在广泛的应用程序中实现高效的数据重用。RDD是一种容错的并行数据结构,允许用户将中间结果持久保存在内存中,控制其分区以优化数据放置,并使用一组丰富的运算符对其进行管理。设计RDD的主要挑战是定义一个能够有效地提供容错功能的编程接口。集群中内存存储的现有抽象,如分布式共享内存[24]、键值存储[25]、数据库和Piccolo[27],提供了一个基于可变状态(如表中的单元格)的细粒度更新的接口。使用此接口,提供容错的唯一方法是跨机器复制数据或跨机器记录更新。对于数据密集型的工作负载,这两种AP都是昂贵的,因为它们需要在clus ter网络上复制大量的数据,而clus ter网络的带宽远低于RAM,并且它们会产生大量的存储开销。与这些系统不同,RDD提供了一个基于粗粒度转换(例如,映射、过滤和连接)的界面,这些转换将相同的操作应用于许多数据项。这使他们能够通过记录用于构建数据集(其沿袭)而不是实际数据的转换来有效地提供容错能力。1如果RDD的一部分丢失,RDD就有足够的信息来说明它是如何从其他RDD派生的,从而仅重新计算该分区。因此,可以恢复丢失的数据,通常很快,而不需要昂贵的复制。

尽管基于粗粒度转换的接口一开始可能看起来有限,但RDD对于许多并行应用程序来说是一个很好的选择,因为这些应用程序自然会对多个数据项应用相同的操作。事实上,我们表明,RDD能够有效地表达许多到目前为止作为独立系统提出的集群编程模型,包括mapre-duce、dryadlinq、sql、pregel和haloop,以及这些系统无法捕获的新应用程序,例如交互式数据挖掘。我们相信,RDD能够满足以前只有通过引入新框架才能满足的计算需求,这是RDD抽象能力最可靠的证据。我们已经在一个名为spark的系统中实现了RDD,该系统正在加州大学伯克利分校和几家公司用于研究和生产应用。spark pro提供了一种方便的界面语言集成编程,类似于scala程序ming语言[2]中的dryadlinq[31]。此外,spark还可以在scala解释器中主动用于查询大数据集。我们相信Spark是第一个允许通用编程语言以超主动速度用于集群内存数据挖掘的系统。

我们通过微基准和用户应用程序来评估RDD。实验表明,在处理迭代式应用上Spark比Hadoop快高达20多倍,计算数据分析类报表的性能提高了50多倍,同时能够在5-7秒的延时内交互式扫描1TB数据集。此外,我们还在Spark之上实现了Pregel和HaLoop编程模型(包括其位置优化策略),以库的形式实现(分别使用了100和200行Scala代码)。最后,利用RDD内在的确定性特性,我们还创建了一种Spark调试工具rddbg,允许用户在任务期间利用Lineage重建RDD,然后像传统调试器那样重新执行任务。

本文首先在第2部分介绍了RDD的概念,然后第3部分描述Spark API,第4部分解释如何使用RDD表示几种并行应用(包括Pregel和HaLoop),第5部分讨论Spark中RDD的表示方法以及任务调度器,第6部分描述具体实现和rddbg。

2  Resilient Distributed Datasets (RDDs)

本部分描述RDD和编程模型。首先讨论RDD抽象(2.1),然后定义RDD编程模型(2.2),讨论示例:日志挖掘(2.3),并给出Scala的闭包相关概念(2.4),最后对比RDD与分布式共享内存(2.5)。

2.1  RDD抽象

从形式上讲,RDD是记录的只读分区集合。RDD只能通过对(1)稳定存储中的数据或(2)其他RDD进行确定操作来创建。我们称这些操作转换为RDD上的其他操作。转换的示例包括map、filter和join。RDD不需要一直实现。相反,RDD有足够的信息来说明它是如何从其他数据集(它的沿袭)派生出来的,从而可以从稳定存储中的数据计算其分区(即为RDD的血缘关系Lineage)。这是一个强大的属性:本质上,程序不能引用一个在失败后无法重建的RDD。最后,用户可以控制RDD的另外两个方面:持久性和分区。用户可以指示将重用哪些RDD,并为其选择存储策略(例如,内存存储)。他们还可以要求RDD的元素基于每个记录中的一个键跨机器进行分区。这对于布局优化很有用,例如确保将连接在一起的两个数据集以相同的方式进行哈希分区。

2.2   RDD编程模型

Spark通过类似于DryadLINQ [31]和FlumeJava [8]的语言集成API公开RDD,其中每个数据集都表示为一个对象,并使用这些对象上的方法调用转换。程序员首先通过对稳定存储(例如,映射和过滤器)中的数据的变换来定义一个或多个RDD。然后,他们可以在操作中使用这些RDD,这些操作将值返回给应用程序或将数据导出到存储系统。操作示例包括count(返回数据集中元素的数量),collect(返回元素本身)和save(将数据集输出到存储系统)。与DryadLINQ一样,Spark在动作中第一次使用它们时会懒得计算RDD,因此它可以进行流水线转换。

此外,程序员可以调用持久化方法来指示他们希望在将来的操作中重用哪些RDD。 Spark通过默认将持久性RDD保留在内存中,但如果没有足够的RAM,它可以将它们溢出到磁盘。用户还可以请求其他持久性策略,例如仅将RDD存储在磁盘上或通过机器复制它,通过flags来保持。最后,用户可以在每个RDD上设置持久性优先级,以指定哪些内存数据应首先溢出到磁盘。

另一方面,RDD还允许用户根据关键字(key)指定分区顺序,这是一个可选的功能。目前支持哈希分区和范围分区。例如,应用程序请求将两个RDD按照同样的哈希分区方式进行分区(将同一机器上具有相同关键字的记录放在一个分区),以加速它们之间的join操作。在Pregel和HaLoop中,多次迭代之间采用一致性的分区置换策略进行优化,我们同样也允许用户指定这种优化。

Spark内核之美(一):RDD的原理与源码分析_第2张图片

2.3  示例:日志挖掘

假设Web服务遇到错误,并且程序员希望在Hadoop文件系统(HDFS)中搜索TB级的日志以找出原因,一般是比较耗时且麻烦的工作。但是,使用Spark可以以一种较为简便的方式实习。程序员可以通过一组节点将日志中的错误消息加载到RAM中,并以交互方式查询它们。 示例的Scala代码如下:

    lines = spark.textFile("hdfs://...")
    errors = lines.filter(_.startsWith("ERROR")) errors.persist()

第1行从HDFS文件定义了一个RDD(即一个文本行集合),第2行获得一个过滤后的RDD,第3行请求将errors缓存起来。注意在Scala语法中filter的参数是一个闭包,Scala闭包的相关概念如下文2.2.2所述。

这时集群还没有开始执行任何任务。但是,用户已经可以在这个RDD上执行对应的动作,例如统计错误消息的数目:

errors.count()

用户还可以在RDD上执行更多的转换操作,并使用转换结果,如:

// Count errors mentioning MySQL:
errors.filter(_.contains("MySQL")).count()
// Return the time fields of errors mentioning
// HDFS as an array (assuming time is field
// number 3 in a tab-separated format):
errors.filter(_.contains("HDFS"))
    .map(_.split('\t')(3))
    .collect()

整个流程如下图所示:

Spark内核之美(一):RDD的原理与源码分析_第3张图片

图1 示例中第三个查询的Lineage图。(方框表示RDD,箭头表示转换)

在涉及错误的第一个操作运行后,spark将把错误分区存储在内存中,大大加快了随后对其进行计算的速度。请注意,基本RDD(行)没有加载到RAM中,因为错误消息可能只是数据的一小部分(小到足以插入内存)。

最后,为了说明我们的模型如何实现故障冗余,我们在上图中的第三个查询中显示了RDD的血缘关系流程图。在这个查询中,我们从错误开始,即一个过滤器在行上的结果,并在运行collect之前应用了一个过滤器和映射。Spark调度程序将对后两个转换进行流水线处理,并发送一组任务来计算这些转换,并将其发送到保存错误缓存分区的节点。此外,如果一个错误分区丢失,spark只在相应的行分区上应用过滤器来重建它。

2.4  Scala的闭包相关概念

闭包是一个函数,返回值依赖于声明在函数外部的一个或多个变量。闭包通常来讲可以简单的认为是可以访问一个函数里面局部变量的另外一个函数。如下面这段匿名的函数:

val multiplier = (i:Int) => i * 10  

函数体内有一个变量 i,它作为函数的一个参数。如下面的另一段代码:

val multiplier = (i:Int) => i * factor

在 multiplier 中有两个变量:i 和 factor。其中的一个 i 是函数的形式参数,在 multiplier 函数被调用时,i 被赋予一个新的值。然而,factor不是形式参数,而是自由变量,考虑下面代码:

var factor = 3  
val multiplier = (i:Int) => i * factor  

这里我们引入一个自由变量 factor,这个变量定义在函数外面。

这样定义的函数变量 multiplier 成为一个"闭包",因为它引用到函数外面定义的变量,定义这个函数的过程是将这个自由变量捕获而构成一个封闭的函数。

完整实例

object Test {  
   def main(args: Array[String]) {  
      println( "muliplier(1) value = " +  multiplier(1) )  
      println( "muliplier(2) value = " +  multiplier(2) )  
   }  
   var factor = 3  
   val multiplier = (i:Int) => i * factor  
}  

执行以上代码,输出结果为:

$ scalac Test.scala  
$  scala Test  
muliplier(1) value = 3  
muliplier(2) value = 6  

 

2.5  RDD与分布式共享内存

为了进一步理解RDD是一种分布式的内存抽象,表1列出了RDD与分布式共享内存(DSM,Distributed Shared Memory)[24]的对比。在DSM系统中,应用可以向全局地址空间的任意位置进行读写操作。(注意这里的DSM,不仅指传统的共享内存系统,还包括那些通过分布式哈希表或分布式文件系统进行数据共享的系统,比如Piccolo[28])DSM是一种通用的抽象,但这种通用性同时也使得在商用集群上实现有效的容错性更加困难。

RDD与DSM主要区别在于,不仅可以通过批量转换创建(即“写”)RDD,还可以对任意内存位置读写。也就是说,RDD限制应用执行批量写操作,这样有利于实现有效的容错。特别地,RDD没有检查点开销,因为可以使用Lineage来恢复RDD。而且,失效时只需要重新计算丢失的那些RDD分区,可以在不同节点上并行执行,而不需要回滚整个程序。

表1 RDD与DSM对比

对比项目 RDD 分布式共享内存(DSM)
批量或细粒度操作 细粒度操作
批量转换操作 细粒度操作
一致性 不重要(RDD是不可更改的) 取决于应用程序或运行时
容错性 细粒度,低开销(使用Lineage) 需要检查点操作和程序回滚
落后任务的处理 任务备份 很难处理
任务安排 基于数据存放的位置自动实现 取决于应用程序(通过运行时实现透明性)
如果内存不够 与已有的数据流系统类似 性能较差

注意,通过备份任务的拷贝,RDD还可以处理落后任务(即运行很慢的节点),这点与MapReduce[12]类似。而DSM则难以实现备份任务,因为任务及其副本都需要读写同一个内存位置。与DSM相比,RDD模型有两个好处。第一,对于RDD中的批量操作,运行时将根据数据存放的位置来调度任务,从而提高性能。第二,对于基于扫描的操作,如果内存不足以缓存整个RDD,就进行部分缓存。把内存放不下的分区存储到磁盘上,此时性能与现有的数据流系统差不多。最后看一下读操作的粒度。RDD上的很多动作(如count和collect)都是批量读操作,即扫描整个数据集,可以将任务分配到距离数据最近的节点上。同时,RDD也支持细粒度操作,即在哈希或范围分区的RDD上执行关键字查找。

正如在介绍中所讨论的,RDD最适合于对数据集的所有元素应用相同操作的批处理应用程序。在这些情况下,RDD可以有效地将每个转换记忆为沿袭图中的一个步骤,并且可以在不记录大量数据的情况下恢复丢失的分区。RDD不太适合对共享状态进行异步细粒度更新的应用程序,例如Web应用程序的存储系统或增量Web爬虫程序。对于这些应用程序,更有效地使用执行传统更新日志记录和数据检查点的系统,如数据库、Ramcloud[25]、Percolator[26]和Piccolo[27]。我们的目标是为批分析提供有效的编程模型,并将这些异步应用程序留给专门的系统。

3  Spark接口编程

Spark通过类似于Scala [2]中的DryadLINQ [31]的语言集成API提供RDD抽象,Scala [2]是Java VM的静态类型函数编程语言。 我们之所以选择Scala,是因为它结合了简洁(便于交互使用)和效率(由于静态类型)。 但是,关于RDD抽象的任何内容都不需要函数式语言。要使用Spark,开发人员编写一个连接到一组worker的驱动程序,如图2所示。驱动程序定义一个或多个RDD并调用它们的动作。 驱动程序上的Spark代码也跟踪RDD的沿袭。 这些工作程序是长期存在的进程,可以跨操作在RAM中存储RDD分区。

Spark内核之美(一):RDD的原理与源码分析_第4张图片

图2 Spark的运行时。用户的driver程序启动多个worker,worker从分布式文件系统中读取数据块,并将计算后的RDD分区缓存在内存中。

再看看2.2中的例子,用户执行RDD操作时会提供参数,比如map传递一个闭包(closure,函数式编程中的概念)。Scala将闭包表示为Java对象,如果传递的参数是闭包,则这些对象被序列化,通过网络传输到其他节点上进行装载。Scala将闭包内的变量保存为Java对象的字段。例如,var x = 5; rdd.map(_ + x) 这段代码将RDD中的每个元素加5。总的来说,Spark的语言集成类似于DryadLINQ。

RDD本身是静态类型对象,由参数指定其元素类型。例如,RDD[int]是一个整型RDD。不过,我们举的例子几乎都省略了这个类型参数,因为Scala支持类型推断。虽然在概念上使用Scala实现RDD很简单,但还是要处理一些Scala闭包对象的反射问题。如何通过Scala解释器来使用Spark还需要更多工作,这点我们将在第6部分讨论。不管怎样,我们都不需要修改Scala编译器。

3.1 Spark中的RDD操作

表2列出了Spark中的RDD转换和动作。每个操作都给出了标识,其中方括号表示类型参数。前面说过转换是延迟操作,用于定义新的RDD;而动作启动计算操作,并向用户程序返回值或向外部存储写数据。

表2 Spark中支持的RDD转换和动作

转换 map(f : T ) U) : RDD[T] ) RDD[U]
filter(f : T ) Bool) : RDD[T] ) RDD[T]
flatMap(f : T ) Seq[U]) : RDD[T] ) RDD[U]
sample(fraction : Float) : RDD[T] ) RDD[T] (Deterministic sampling)
groupByKey() : RDD[(K, V)] ) RDD[(K, Seq[V])]
reduceByKey(f : (V; V) ) V) : RDD[(K, V)] ) RDD[(K, V)]
union() : (RDD[T]; RDD[T]) ) RDD[T]
join() : (RDD[(K, V)]; RDD[(K, W)]) ) RDD[(K, (V, W))]
cogroup() : (RDD[(K, V)]; RDD[(K, W)]) ) RDD[(K, (Seq[V], Seq[W]))]
crossProduct() : (RDD[T]; RDD[U]) ) RDD[(T, U)]
mapValues(f : V ) W) : RDD[(K, V)] ) RDD[(K, W)] (Preserves partitioning)
sort(c : Comparator[K]) : RDD[(K, V)] ) RDD[(K, V)]
partitionBy(p : Partitioner[K]) : RDD[(K, V)] ) RDD[(K, V)]
动作 count() : RDD[T] ) Long
collect() : RDD[T] ) Seq[T]
reduce(f : (T; T) ) T) : RDD[T] ) T
lookup(k : K) : RDD[(K, V)] ) Seq[V] (On hash/range partitioned RDDs)
save(path : String) : Outputs RDD to a storage system, e.g., HDFS

注意,有些操作只对键值对可用,比如join。另外,函数名与Scala及其他函数式语言中的API匹配,例如map是一对一的映射,而flatMap是将每个输入映射为一个或多个输出(与MapReduce中的map类似)。除了这些操作以外,用户还可以请求将RDD缓存起来。而且,用户还可以通过Partitioner类获取RDD的分区顺序,然后将另一个RDD按照同样的方式分区。有些操作会自动产生一个哈希或范围分区的RDD,像groupByKey,reduceByKey和sort等。

4. 应用程序示例

现在我们讲述如何使用RDD表示几种基于数据并行的应用。首先讨论一些迭代式机器学习应用(4.1),然后看看如何使用RDD描述几种已有的集群编程模型,即MapReduce(4.2),Pregel(4.3),和Hadoop(4.4)。最后讨论一下RDD不适合哪些应用(4.5)。

4.1 迭代式机器学习

很多机器学习算法都具有迭代特性,运行迭代优化方法来优化某个目标函数,例如梯度下降方法。如果这些算法的工作集能够放入内存,将极大地加速程序运行。而且,这些算法通常采用批量操作,例如映射和求和,这样更容易使用RDD来表示。

例如下面的程序是逻辑回归[15]的实现。逻辑回归是一种常见的分类算法,即寻找一个最佳分割两组点(即垃圾邮件和非垃圾邮件)的超平面w。算法采用梯度下降的方法:开始时w为随机值,在每一次迭代的过程中,对w的函数求和,然后朝着优化的方向移动w。

val points = spark.textFile(...)
     .map(parsePoint).persist()
var w = // random initial vector
for (i <- 1 to ITERATIONS) {
     val gradient = points.map{ p =>
          p.x * (1/(1+exp(-p.y*(w dot p.x)))-1)*p.y
     }.reduce((a,b) => a+b)
     w -= gradient
}

首先定义一个名为points的缓存RDD,这是在文本文件上执行map转换之后得到的,即将每个文本行解析为一个Point对象。然后在points上反复执行map和reduce操作,每次迭代时通过对当前w的函数进行求和来计算梯度。7.1小节我们将看到这种在内存中缓存points的方式,比每次迭代都从磁盘文件装载数据并进行解析要快得多。

已经在Spark中实现的迭代式机器学习算法还有:kmeans(像逻辑回归一样每次迭代时执行一对map和reduce操作),期望最大化算法(EM,两个不同的map/reduce步骤交替执行),交替最小二乘矩阵分解和协同过滤算法。Chu等人提出迭代式MapReduce也可以用来实现常用的学习算法[11]。

4.2 使用RDD实现MapReduce

MapReduce模型[12]很容易使用RDD进行描述。假设有一个输入数据集(其元素类型为T),和两个函数myMap: T => List[(Ki, Vi)] 和 myReduce: (Ki; List[Vi]) ) List[R],代码如下:

data.flatMap(myMap)
    .groupByKey()
    .map((k, vs) => myReduce(k, vs))

如果任务包含combiner,则相应的代码为:

data.flatMap(myMap)
    .reduceByKey(myCombiner)
    .map((k, v) => myReduce(k, v))

ReduceByKey操作在mapper节点上执行部分聚集,与MapReduce的combiner类似。

4.3 使用RDD实现Pregel

regel[21]是面向图算法的基于BSP范式[32]的编程模型。程序由一系列超步(Superstep)协调迭代运行。在每个超步中,各个顶点执行用户函数,并更新相应的顶点状态,变异图拓扑,然后向下一个超步的顶点集发送消息。这种模型能够描述很多图算法,包括最短路径,双边匹配和PageRank等。

以PageRank为例介绍一下Pregel的实现。当前PageRank[7]记为r,顶点表示状态。在每个超步中,各个顶点向其所有邻居发送贡献值r/n,这里n是邻居的数目。下一个超步开始时,每个顶点将其分值(rank)更新为 α/N + (1 - α) * Σci,这里的求和是各个顶点收到的所有贡献值的和,N是顶点的总数。Pregel将输入的图划分到各个worker上,并存储在其内存中。在每个超步中,各个worker通过一种类似MapReduce的Shuffle操作交换消息。

Pregel的通信模式可以用RDD来描述,如图3。主要思想是:将每个超步中的顶点状态和要发送的消息存储为RDD,然后根据顶点ID分组,进行Shuffle通信(即cogroup操作)。然后对每个顶点ID上的状态和消息应用用户函数(即mapValues操作),产生一个新的RDD,即(VertexID, (NewState, OutgoingMessages))。然后执行map操作分离出下一次迭代的顶点状态和消息(即mapValues和flatMap操作)。代码如下:

val vertices = // RDD of (ID, State) pairs
val messages = // RDD of (ID, Message) pairs
val grouped = vertices.cogroup(messages)
val newData = grouped.mapValues {
    (vert, msgs) => userFunc(vert, msgs)
    // returns (newState, outgoingMsgs)
}.cache()
val newVerts = newData.mapValues((v,ms) => v)
val newMsgs = newData.flatMap((id,(v,ms)) => ms)

需要注意的是,这种实现方法中,RDD grouped,newData和newVerts的分区方法与输入RDD vertices一样。所以,顶点状态一直存在于它们开始执行的机器上,这跟原Pregel一样,这样就减少了通信成本。因为cogroup和mapValues保持了与输入RDD相同的分区方法,所以分区是自动进行的。

完整的Pregel编程模型还包括其他工具,比如combiner,附录A讨论了它们的实现。下面将讨论Pregel的容错性,以及如何在实现相同容错性的同时减少需要执行检查点操作的数据量。我们差不多用了100行Scala代码在Spark上实现了一个类Pregel的API。

Spark内核之美(一):RDD的原理与源码分析_第5张图片

图3 使用RDD实现Pregel时,一步迭代的数据流。(方框表示RDD,箭头表示转换)

4.3.1 Pregel容错

当前,Pregel基于检查点机制来为顶点状态及其消息实现容错[21]。然而作者是这样描述的:通过在其它的节点上记录已发消息日志,然后单独重建丢失的分区,只需要恢复局部数据即可。上面提到这两种方式,RDD都能够很好地支持。

通过4.3小节的实现,Spark总是能够基于Lineage实现顶点和消息RDD的重建,但是由于过长的Lineage链,恢复可能会付出高昂的代价。因为迭代RDD依赖于上一个RDD,对于部分分区来说,节点故障可能会导致这些分区状态的所有迭代版本丢失,这就要求使用一种“级联-重新执行”[20]的方式去依次重建每一个丢失的分区。为了避免这个问题,用户可以周期性地在顶点和消息RDD上执行save操作,将状态信息保存到持久存储中。然后,Spark能够在失败的时候自动地重新计算这些丢失的分区(而不是回滚整个程序)。

最后,我们意识到,RDD也能够实现检查点数据的reduce操作,这要求通过一种高效的检查点方案来表达检查点数据。在很多Pregel作业中,顶点状态都包括可变与不可变的组件,例如,在PageRank中,与一个顶点相邻的顶点列表是不可变的,但是它们的排名是可变的,在这种情况下,我们可以使用一个来自可变数据的单独RDD来替换不可变RDD,基于这样一个较短的Lineage链,检查点仅仅是可变状态,图4解释了这种方式。

Spark内核之美(一):RDD的原理与源码分析_第6张图片

图4 经过优化的Pregel使用RDD的数据流。可变状态RDD必须设置检查点,不可变状态才可被快速重建。

在PageRank中,不可变状态(相邻顶点列表)远大于可变状态(浮点值),所以这种方式能够极大地降低开销。

4.4 使用RDD实现HaLoop

HaLoop[8]是Hadoop的一个扩展版本,它能够改善具有迭代特性的MapReduce程序的性能。基于HaLoop编程模型的应用,使用reduce阶段的输出作为map阶段下一轮迭代的输入。它的循环感知任务调度器能够保证,在每一轮迭代中处理同一个分区数据的连续map和reduce任务,一定能够在同一台物理机上执行。确保迭代间locality特性,reduce数据在物理节点之间传输,并且允许数据缓存在本地磁盘而能够被后续迭代重用。

使用RDD来优化HaLoop,我们在Spark上实现了一个类似HaLoop的API,这个库只使用了200行Scala代码。通过partitionBy能够保证跨迭代的分区的一致性,每一个阶段的输入和输出被缓存以用于后续迭代。

4.5 不适合使用RDD的应用

在2.1节我们讨论过,RDD适用于具有批量转换需求的应用,并且相同的操作作用于数据集的每一个元素上。在这种情况下,RDD能够记住每个转换操作,对应于Lineage图中的一个步骤,恢复丢失分区数据时不需要写日志记录大量数据。RDD不适合那些通过异步细粒度地更新来共享状态的应用,例如Web应用中的存储系统,或者增量抓取和索引Web数据的系统,这样的应用更适合使用一些传统的方法,例如数据库、RAMCloud[26]、Percolator[27]和Piccolo[28]。我们的目标是,面向批量分析应用的这类特定系统,提供一种高效的编程模型,而不是一些异步应用程序。

5. RDD的描述及作业调度

我们希望在不修改调度器的前提下,支持RDD上的各种转换操作,同时能够从这些转换获取Lineage信息。为此,我们为RDD设计了一组小型通用的内部接口。简单地说,每个RDD都包含:(1)一组RDD分区(partition,即数据集的原子组成部分);(2)对父RDD的一组依赖,这些依赖描述了RDD的Lineage;(3)一个函数,即在父RDD上执行何种计算;(4)元数据,描述分区模式和数据存放的位置。例如,一个表示HDFS文件的RDD包含:各个数据块的一个分区,并知道各个数据块放在哪些节点上。而且这个RDD上的map操作结果也具有同样的分区,map函数是在父数据上执行的。表3总结了RDD的内部接口。

表3 Spark中RDD的内部接口

操作 含义
partitions() 返回一组Partition对象
preferredLocations(p) 根据数据存放的位置,返回分区p在哪些节点访问更快
dependencies() 返回一组依赖
iterator(p, parentIters) 按照父分区的迭代器,逐个计算分区p的元素
partitioner() 返回RDD是否hash/range分区的元数据信息

设计接口的一个关键问题就是,如何表示RDD之间的依赖。我们发现RDD之间的依赖关系可以分为两类,即:

(1)窄依赖(narrow dependencies):子RDD的每个分区依赖于常数个父分区(即与数据规模无关);注解:有些博客及书籍资料中写道,窄依赖是每个子RDD只对应一个父分区,但是,通过下图及Spark作者论文发现,该说法有一定的错误,而应为子RDD的每个分区依赖于常数个父分区(即与数据规模无关)。

(2)宽依赖(wide dependencies):子RDD的每个分区依赖于所有父RDD分区。例如,map产生窄依赖,而join则是宽依赖(除非父RDD被哈希分区)。另一个例子见图5。

Spark内核之美(一):RDD的原理与源码分析_第7张图片

图5 窄依赖和宽依赖的例子。(方框表示RDD,实心矩形表示分区)

窄依赖每个child RDD 的partition的生成操作都是可以并行的,而宽依赖则需要所有的parent partition shuffle结果得到后再进行。下面我们来看下,我们来看下org.apache.spark.Dependency.scala的源码。

抽象类Dependency:

abstract class Dependency[T] extends Serializable {
  def rdd: RDD[T]
}

Dependency有两个子类,一个子类为窄依赖:NarrowDependency;一个为宽依赖ShuffleDependency

NarrowDependency也是一个抽象类,它实现了getParents 重写了 rdd 函数,它有两个子类,一个是 OneToOneDependency,一个是 RangeDependency:

abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
  /**
   * Get the parent partitions for a child partition.
   * @param partitionId a partition of the child RDD
   * @return the partitions of the parent RDD that the child partition depends upon
   */
  def getParents(partitionId: Int): Seq[Int]

  override def rdd: RDD[T] = _rdd
}

OneToOneDependency,可以看到getParents实现很简单,就是传进一个partitionId: Int,再把partitionId放在List里面传出去,即去parent RDD 中取与该RDD 相同 partitionID的数据:

class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int): List[Int] = List(partitionId)
}

RangeDependency,用于union。与上面不同的是,这里我们要算出该位置,设某个parent RDD 从 inStart 开始的partition,逐个生成了 child RDD 从outStart 开始的partition,则计算方式为: partitionId - outStart + inStart:

class RangeDependency[T](rdd: RDD[T], inStart: Int, outStart: Int, length: Int)
  extends NarrowDependency[T](rdd) {

  override def getParents(partitionId: Int): List[Int] = {
    if (partitionId >= outStart && partitionId < outStart + length) {
      List(partitionId - outStart + inStart)
    } else {
      Nil
    }
  }
}

ShuffleDependency,需要进行shuffle

class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
    @transient private val _rdd: RDD[_ <: Product2[K, V]],
    val partitioner: Partitioner,
    val serializer: Serializer = SparkEnv.get.serializer,
    val keyOrdering: Option[Ordering[K]] = None,
    val aggregator: Option[Aggregator[K, V, C]] = None,
    val mapSideCombine: Boolean = false)
  extends Dependency[Product2[K, V]] {

  override def rdd: RDD[Product2[K, V]] = _rdd.asInstanceOf[RDD[Product2[K, V]]]

  private[spark] val keyClassName: String = reflect.classTag[K].runtimeClass.getName
  private[spark] val valueClassName: String = reflect.classTag[V].runtimeClass.getName
  // Note: It's possible that the combiner class tag is null, if the combineByKey
  // methods in PairRDDFunctions are used instead of combineByKeyWithClassTag.
  private[spark] val combinerClassName: Option[String] =
    Option(reflect.classTag[C]).map(_.runtimeClass.getName)
//获取shuffleID
  val shuffleId: Int = _rdd.context.newShuffleId()
//向注册shuffleManager注册Shuffle信息
  val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManager.registerShuffle(
    shuffleId, _rdd.partitions.length, this)

  _rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
}

区分这两种依赖很有用。首先,窄依赖允许在一个集群节点上以流水线的方式(pipeline)计算所有父分区。例如,逐个元素地执行map、然后filter操作;而宽依赖则需要首先计算好所有父分区数据,然后在节点之间进行Shuffle,这与MapReduce类似。第二,窄依赖能够更有效地进行失效节点的恢复,即只需重新计算丢失RDD分区的父分区,而且不同节点之间可以并行计算;而对于一个宽依赖关系的Lineage图,单个节点失效可能导致这个RDD的所有祖先丢失部分分区,因而需要整体重新计算。

通过RDD接口,Spark只需要不超过20行代码实现便可以实现大多数转换。5.1小节给出了例子,然后我们讨论了怎样使用RDD接口进行调度(5.2),最后讨论一下基于RDD的程序何时需要数据检查点操作(5.3)。

5.1 RDD实现举例

RDD公共接口能够让使用者在编写少于20行代码的情况下,让Spark实现大多数的转换操作。实际上,即使是新的Spark用户,在不知道Spark调度细节的情况下,也能够轻松实现相应的转换操作(例如,采样和各种类型的连接)。我们在下面概述一些RDD实现。

HDFS文件(HDFS files):我们的示例中的输入RDD的文件都存储在HDFS中。对于这些RDD,分区为文件的每个块返回一个分区(块的偏移存储在每个分区对象中),preferredLocations提供块所在的节点,迭代器(iterator)读取块。

map:在任何RDD上调用map都会返回mapped  RDD对象。此对象与其父对象具有相同的分区和首选位置,但将传递的函数应用于在其迭代器方法中映射到父对象的记录。

联合(union):对两个RDD调用联合返回一个RDD,该RDD的分区是父级的联合。每个子分区都是通过相应的父分区上的一个狭窄的非依赖性来计算的。

示例(sample):采样与映射类似,只是RDD为每个分区存储一个随机数生成器种子,以确定地对父记录进行采样。

联接(join):联接两个RDD可能会导致两个NAR行依赖项(如果它们都是用同一分区器分区的哈希/范围)、两个宽依赖项或混合(如果一个父级有一个分区器而另一个没有)。在这两种情况下,输出RDD都有一个分区器(从父级继承的分区器或默认的哈希分区器)。

5.2 Spark任务调度器

调度器根据RDD的结构信息为每个动作确定有效的执行计划。调度器的接口是runJob函数,参数为RDD及其分区集,和一个RDD分区上的函数。该接口足以表示Spark中的所有动作(即count、collect、save等)。

总的来说,我们的调度器跟Dryad类似,但我们还考虑了哪些RDD分区是缓存在内存中的。调度器根据目标RDD的Lineage图创建一个由stage构成的无回路有向图(DAG)。每个stage内部尽可能多地包含一组具有窄依赖关系的转换,并将它们流水线并行化(pipeline)。stage的边界有两种情况:一是宽依赖上的Shuffle操作;二是已缓存分区,它可以缩短父RDD的计算过程。例如图6。父RDD完成计算后,可以在stage内启动一组任务计算丢失的分区。

Spark内核之美(一):RDD的原理与源码分析_第8张图片

图6 Spark怎样划分任务阶段(stage)的例子。实线方框表示RDD,实心矩形表示分区(黑色表示该分区被缓存)。

要在RDD G上执行一个动作,调度器根据宽依赖创建一组stage,并在每个stage内部将具有窄依赖的转换流水线化(pipeline)。 本例不用再执行stage 1,因为B已经存在于缓存中了,所以只需要运行2和3。调度器根据数据存放的位置分配任务,以最小化通信开销。如果某个任务需要处理一个已缓存分区,则直接将任务分配给拥有这个分区的节点。否则,如果需要处理的分区位于多个可能的位置(例如,由HDFS的数据存放位置决定),则将任务分配给这一组节点。

对于宽依赖(例如需要Shuffle的依赖),目前的实现方式是,在拥有父分区的节点上将中间结果物化,简化容错处理,这跟MapReduce中物化map输出很像。如果某个任务失效,只要stage中的父RDD分区可用,则只需在另一个节点上重新运行这个任务即可。如果某些stage不可用(例如,Shuffle时某个map输出丢失),则需要重新提交这个stage中的所有任务来计算丢失的分区。

最后,lookup动作允许用户从一个哈希或范围分区的RDD上,根据关键字读取一个数据元素。这里有一个设计问题。Driver程序调用lookup时,只需要使用当前调度器接口计算关键字所在的那个分区。当然任务也可以在集群上调用lookup,这时可以将RDD视为一个大的分布式哈希表。这种情况下,任务和被查询的RDD之间的并没有明确的依赖关系(因为worker执行的是lookup),如果所有节点上都没有相应的缓存分区,那么任务需要告诉调度器计算哪些RDD来完成查找操作。

5.3 检查点

尽管RDD中的Lineage信息可以用来故障恢复,但对于那些Lineage链较长的RDD来说,这种恢复可能很耗时。例如4.3小节中的Pregel任务,每次迭代的顶点状态和消息都跟前一次迭代有关,所以Lineage链很长。如果将Lineage链存到物理存储中,再定期对RDD执行检查点操作就很有效。

一般来说,Lineage链较长、宽依赖的RDD需要采用检查点机制。这种情况下,集群的节点故障可能导致每个父RDD的数据块丢失,因此需要全部重新计算[20]。对比来看,对稳定存储中数据的具有窄依赖的RDD,例如,在我们4.1小节的逻辑回归的例子中的点集合,以及在Pregel优化变体中不可变的顶点状态,可能永远都不需要执行检查点操作。

当前Spark版本提供检查点API,但由用户决定是否需要执行检查点操作。今后我们将实现自动检查点,根据成本效益分析确定RDD Lineage图中的最佳检查点位置。

我们来阅读下org.apache.spark.rdd.ReliableCheckpointRDD中的def writePartitionToCheckpointFile 和 def writeRDDToCheckpointDirectory:

writePartitionToCheckpointFile:把RDD一个Partition文件里面的数据写到一个Checkpoint文件里面:

  def writePartitionToCheckpointFile[T: ClassTag](
      path: String,
      broadcastedConf: Broadcast[SerializableConfiguration],
      blockSize: Int = -1)(ctx: TaskContext, iterator: Iterator[T]) {
    val env = SparkEnv.get
    //获取Checkpoint文件输出路径
    val outputDir = new Path(path)
    val fs = outputDir.getFileSystem(broadcastedConf.value.value)

    //根据partitionId 生成 checkpointFileName
    val finalOutputName = ReliableCheckpointRDD.checkpointFileName(ctx.partitionId())
    //拼接路径与文件名
    val finalOutputPath = new Path(outputDir, finalOutputName)
    //生成临时输出路径
    val tempOutputPath =
      new Path(outputDir, s".$finalOutputName-attempt-${ctx.attemptNumber()}")

    if (fs.exists(tempOutputPath)) {
      throw new IOException(s"Checkpoint failed: temporary path $tempOutputPath already exists")
    }
    //得到块大小,默认为64MB
    val bufferSize = env.conf.getInt("spark.buffer.size", 65536)
    //得到文件输出流
    val fileOutputStream = if (blockSize < 0) {
      fs.create(tempOutputPath, false, bufferSize)
    } else {
      // This is mainly for testing purpose
      fs.create(tempOutputPath, false, bufferSize,
        fs.getDefaultReplication(fs.getWorkingDirectory), blockSize)
    }
    //序列化文件输出流
    val serializer = env.serializer.newInstance()
    val serializeStream = serializer.serializeStream(fileOutputStream)
    Utils.tryWithSafeFinally {
    //写数据
      serializeStream.writeAll(iterator)
    } {
      serializeStream.close()
    }

    if (!fs.rename(tempOutputPath, finalOutputPath)) {
      if (!fs.exists(finalOutputPath)) {
        logInfo(s"Deleting tempOutputPath $tempOutputPath")
        fs.delete(tempOutputPath, false)
        throw new IOException("Checkpoint failed: failed to save output of task: " +
          s"${ctx.attemptNumber()} and final output path does not exist: $finalOutputPath")
      } else {
        // Some other copy of this task must've finished before us and renamed it
        logInfo(s"Final output path $finalOutputPath already exists; not overwriting it")
        if (!fs.delete(tempOutputPath, false)) {
          logWarning(s"Error deleting ${tempOutputPath}")
        }
      }
    }
  }

writeRDDToCheckpointDirectoryWrite,将一个RDD写入到多个checkpoint文件,并返回一个ReliableCheckpointRDD来代表这个RDD:

 def writeRDDToCheckpointDirectory[T: ClassTag](
      originalRDD: RDD[T],
      checkpointDir: String,
      blockSize: Int = -1): ReliableCheckpointRDD[T] = {

    val sc = originalRDD.sparkContext

    // 生成 checkpoint文件 的输出路径
    val checkpointDirPath = new Path(checkpointDir)
    val fs = checkpointDirPath.getFileSystem(sc.hadoopConfiguration)
    if (!fs.mkdirs(checkpointDirPath)) {
      throw new SparkException(s"Failed to create checkpoint path $checkpointDirPath")
    }

    // 保存文件,并重新加载它作为一个RDD
    val broadcastedConf = sc.broadcast(
      new SerializableConfiguration(sc.hadoopConfiguration))
    sc.runJob(originalRDD,
      writePartitionToCheckpointFile[T](checkpointDirPath.toString, broadcastedConf) _)

    if (originalRDD.partitioner.nonEmpty) {
      writePartitionerToCheckpointDir(sc, originalRDD.partitioner.get, checkpointDirPath)
    }

    val newRDD = new ReliableCheckpointRDD[T](
      sc, checkpointDirPath.toString, originalRDD.partitioner)
    if (newRDD.partitions.length != originalRDD.partitions.length) {
      throw new SparkException(
        s"Checkpoint RDD $newRDD(${newRDD.partitions.length}) has different " +
          s"number of partitions from original RDD $originalRDD(${originalRDD.partitions.length})")
    }
    newRDD
  }

RDD checkpoint之后得到了一个新的RDD,那么child RDD 如何知道 parent RDD 有没有被checkpoint过呢? 看 RDD的源码,我们可以发现:

private var dependencies_ : Seq[Dependency[_]] = null

dependencies_ 用来存放checkpoint后的结果的,如为null,则就判断没checkpoint:

 final def dependencies: Seq[Dependency[_]] = {
    checkpointRDD.map(r => List(new OneToOneDependency(r))).getOrElse {
      if (dependencies_ == null) {
        dependencies_ = getDependencies
      }
      dependencies_
    }
  }

值得注意的是,因为RDD是只读的,所以不需要任何一致性维护(例如写复制策略,分布式快照或者程序暂停等)带来的开销,后台执行检查点操作。我们使用10000行Scala代码实现了Spark。系统可以使用任何Hadoop数据源(如HDFS,Hbase)作为输入,这样很容易与Hadoop环境集成。Spark以库的形式实现,不需要修改Scala编译器。

这里讨论关于实现的三方面问题:(1)修改Scala解释器,允许交互模式使用Spark(6.1);(2)缓存管理(6.2);(3)调试工具rddbg(6.3)。

6. 实现

6.1 解释器的集成

像Ruby和Python一样,Scala也有一个交互式shell。基于内存的数据可以实现低延时,我们希望允许用户从解释器交互式地运行Spark,从而在大数据集上实现大规模并行数据挖掘。

Scala解释器通常根据将用户输入的代码行,来对类进行编译,接着装载到JVM中,然后调用类的函数。这个类是一个包含输入行变量或函数的单例对象,并在一个初始化函数中运行这行代码。例如,如果用户输入代码var x = 5,接着又输入println(x),则解释器会定义一个包含x的Line1类,并将第2行编译为println(Line1.getInstance().x)。

在Spark中我们对解释器做了两点改动:

  1. 类传输:解释器能够支持基于HTTP传输类字节码,这样worker节点就能获取输入每行代码对应的类的字节码。
  2. 改进的代码生成逻辑:通常每行上创建的单态对象通过对应类上的静态方法进行访问。也就是说,如果要序列化一个闭包,它引用了前面代码行中变量,比如上面的例子Line1.x,Java不会根据对象关系传输包含x的Line1实例。所以worker节点不会收到x。我们将这种代码生成逻辑改为直接引用各个行对象的实例。图7说明了解释器如何将用户输入的一组代码行解释为Java对象。

Spark内核之美(一):RDD的原理与源码分析_第9张图片

图7 Spark解释器如何将用户输入的两行代码解释为Java对象

Spark解释器便于跟踪处理大量对象关系引用,并且便利了HDFS数据集的研究。我们计划以Spark解释器为基础,开发提供高级数据分析语言支持的交互式工具,比如类似SQL和Matlab。

 

6.2 缓存管理

Worker节点将RDD分区以Java对象的形式缓存在内存中。由于大部分操作是基于扫描的,采取RDD级的LRU(最近最少使用)替换策略(即不会为了装载一个RDD分区而将同一RDD的其他分区替换出去)。目前这种简单的策略适合大多数用户应用。另外,使用带参数的cache操作可以设定RDD的缓存优先级。

6.3 rddbg:RDD程序的调试工具

RDD的初衷是为了实现容错以能够再计算(re-computation),这个特性使得调试更容易。我们创建了一个名为rddbg的调试工具,它是通过基于程序记录的Lineage信息来实现的,允许用户:(1)重建任何由程序创建的RDD,并执行交互式查询;(2)使用一个单进程Java调试器(如jdb)传入计算好的RDD分区,能够重新运行作业中的任何任务。

我们强调一下,rddbg不是一个完全重放的调试器:特别是不对非确定性的代码或动作进行重放。但如果某个任务一直运行很慢(比如由于数据分布不均匀或者异常输入等原因),仍然可以用它来帮助找到其中的逻辑错误和性能问题。

举个例子,我们使用rddbg去解决用户Spam分类作业中的一个bug,这个作业中的每次迭代都产生0值。在调试器中重新执行reduce任务,很快就能发现,输入的权重向量(存储在一个用户自定义的向量类中)竟然是空值。由于从一个未初始化的稀疏向量中读取总是返回0,运行时也不会抛出异常。在这个向量类中设置一个断点,然后运行这个任务,引导程序很快就运行到设置的断点处,我们发现向量类的一个数组字段的值为空,我们诊断出了这个bug:稀疏向量类中的数据字段被错误地使用transient来修饰,导致序列化时忽略了该字段的数据。

rddbg给程序执行带来的开销很小。程序本来就需要将各个RDD中的所有闭包序列化并通过网络传送,只不过使用rddbg同时还要将这些闭集记录到磁盘。

 

 

参考文献

[1] Apache Hive. http://hadoop.apache.org/hive.
[2] Scala. http://www.scala-lang.org.
[3] G. Ananthanarayanan, A. Ghodsi, S. Shenker, and I. Stoica. Disk-locality in datacenter computing considered irrelevant. In HotOS ’11, 2011.
[4] P. Bhatotia, A. Wieder, R. Rodrigues, U. A. Acar, and R.  Pasquin. Incoop: MapReduce for incremental computations. In ACM SOCC ’11, 2011.
[5] R. Bose and J. Frew. Lineage retrieval for scientific data processing: a survey. ACM Computing Surveys, 37:1–28, 2005.
[6] S. Brin and L. Page. The anatomy of a large-scale hypertextual web search engine. In WWW, 1998.
[7] Y. Bu, B. Howe, M. Balazinska, and M. D. Ernst. HaLoop: efficient iterative data processing on large clusters. Proc. VLDB Endow., 3:285–296, September 2010.
[8] C. Chambers, A. Raniwala, F. Perry, S. Adams, R. R. Henry, R.  Bradshaw, and N. Weizenbaum. FlumeJava: easy, efficient data-parallel pipelines. In PLDI ’10. ACM, 2010.
[9] J. Cheney, L. Chiticariu, and W.-C. Tan. Provenance in databases: Why, how, and where. Foundations and Trends in Databases, 1(4):379–474, 2009.
[10]    J. Dean and S. Ghemawat. MapReduce: Simplified data processing on large clusters. In OSDI, 2004.
[11]    J. Ekanayake, H. Li, B. Zhang, T. Gunarathne, S.-H. Bae, J. Qiu, and G. Fox. Twister: a runtime for iterative mapreduce. In HPDC ’10, 2010.
[12]    P. K. Gunda, L. Ravindranath, C. A. Thekkath, Y. Yu, and L.  Zhuang. Nectar: automatic management of data and computation in datacenters. In OSDI ’10, 2010.
[13]    Z. Guo, X. Wang, J. Tang, X. Liu, Z. Xu, M. Wu, M. F. Kaashoek, and Z. Zhang. R2: an application-level kernel for record and replay. OSDI’08, 2008.
[14]    T. Hastie, R. Tibshirani, and J. Friedman. The Elements of Statistical Learning: Data Mining, Inference, and Prediction. Springer Publishing Company, New York, NY  2009.
[15]    B. He, M. Yang, Z. Guo, R. Chen, B. Su, W. Lin, and L. Zhou. Comet: batched stream processing for data intensive distributed computing. In SoCC ’10.
[16]    A. Heydon, R. Levin, and Y. Yu. Caching function calls using precise dependencies. In ACM SIGPLAN Notices, pages
311–320, 2000.
[17]    B. Hindman, A. Konwinski, M. Zaharia, A. Ghodsi, A. D. Joseph, R. H. Katz, S. Shenker, and I. Stoica. Mesos: A platform for fine-grained resource sharing in the data center. In NSDI ’11.
[18]    T. Hunter, T. Moldovan, M. Zaharia, S. Merzgui, J. Ma, M. J. Franklin, P. Abbeel, and A. M. Bayen. Scaling the Mobile Millennium system in the cloud. In SOCC ’11, 2011.
[19]    M. Isard, M. Budiu, Y. Yu, A. Birrell, and D. Fetterly. Dryad: distributed data-parallel programs from sequential building blocks. In EuroSys ’07, 2007.
[20]    S. Y. Ko, I. Hoque, B. Cho, and I. Gupta. On availability of intermediate data in cloud computations. In HotOS ’09, 2009.
[21]    D. Logothetis, C. Olston, B. Reed, K. C. Webb, and K. Yocum. Stateful bulk processing for incremental analytics. SoCC ’10.
[22]    G. Malewicz, M. H. Austern, A. J. Bik, J. C. Dehnert, I. Horn, N.  Leiser, and G. Czajkowski. Pregel: a system for large-scale graph processing. In SIGMOD, 2010.
[23]    D. G. Murray, M. Schwarzkopf, C. Smowton, S. Smith, A.  Madhavapeddy, and S. Hand. Ciel: a universal execution engine for distributed data-flow computing. In NSDI, 2011.
[24]    B. Nitzberg and V. Lo. Distributed shared memory: a survey of issues and algorithms. Computer, 24(8):52 –60, Aug 1991.
[25]    J. Ousterhout, P. Agrawal, D. Erickson, C. Kozyrakis, J.  Leverich, D. Mazi`eres, S. Mitra, A. Narayanan, G. Parulkar,M. Rosenblum, S. M. Rumble, E. Stratmann, and R. Stutsman. The case for RAMClouds: scalable high-performance storage entirely in DRAM. SIGOPS Op. Sys. Rev., 43:92–105, Jan 2010.
[26]    D. Peng and F. Dabek. Large-scale incremental processing using distributed transactions and notifications. In OSDI 2010.
[27]    R. Power and J. Li. Piccolo: Building fast, distributed programs with partitioned tables. In Proc. OSDI 2010, 2010.
[28]    R. Ramakrishnan and J. Gehrke. Database Management Systems. McGraw-Hill, Inc., 3 edition, 2003.
[29]    K. Thomas, C. Grier, J. Ma, V. Paxson, and D. Song. Design and evaluation of a real-time URL spam filtering service. In IEEE Symposium on Security and Privacy, 2011.
[30]    J. W. Young. A first order approximation to the optimum checkpoint interval. Commun. ACM, 17:530–531, Sept 1974.
[31]    Y. Yu, M. Isard, D. Fetterly, M. Budiu, ´U. Erlingsson, P. K. Gunda, and J. Currey. DryadLINQ: A system for
general-purpose distributed data-parallel computing using a high-level language. In OSDI ’08, 2008.
[32]    M. Zaharia, D. Borthakur, J. Sen Sarma, K. Elmeleegy,S.  Shenker, and I. Stoica. Delay scheduling: A simple technique for achieving locality and fairness in cluster scheduling. In EuroSys ’10, 2010.
[33]    M. Zaharia, M. Chowdhury, T. Das, A. Dave, J. Ma,M.  McCauley, M. Franklin, S. Shenker, and I. Stoica. Resilient distributed datasets: A fault-tolerant abstraction for in-memory cluster computing. Technical Report UCB/EECS-2011-82, EECS Department, UC Berkeley, 2011.

你可能感兴趣的:(Spark,Spark内核之美)