RDD(转):一种基于内存的集群计算的容错性抽象方法(三)
2013-01-30 11:59 Polarisary 阅读(676) 评论(0) 编辑 收藏 举报
弹性分布式数据集:一种基于内存的集群计算的容(一):摘要,引言
3. Spark API
Spark用Scala语言实现了RDD的API。Scala是一种基于JVM的静态类型、函数式、面向对象的语言。我们选择Scala是因为它简 洁(特别适合交互式使用)、有效(因为是静态类型)。但是,RDD抽象并不局限于函数式语言,也可以使用其他语言来实现RDD,比如像Hadoop那样用 类表示用户函数。
要使用Spark,开发者需要编写一个driver程序,连接到集群以运行worker,如图2所示。Driver定义了一个或多个RDD,并调用RDD
上的行为(action)。Worker是长时间运行(long-lived)的进程,将RDD分区以Java对象的形式缓存在RAM中。
图2 Spark的运行时。用户的driver程序启动多个worker,worker从分布式文件系统中读取数据块(block),并将计算后的RDD分区(partition)缓存在内存中。
再看看2.4中的例子,用户执行RDD操作时会提供参数,比如map传递一个闭包(closure,函数式编程中的概念)。Scala将闭包表示为 Java对象,如果传递的参数是闭包,则这些对象被序列化,通过网络传输到其他节点上进行装载。Scala将闭包内的变量保存为Java对象的字段 (field)。例如,var x = 5; rdd.map(_ + x) 这段代码将RDD中的每个元素加5。总的来说,Spark的语言集成类似于DryadLINQ。
RDD本身是静态类型对象,由参数指定其元素类型。例如,RDD[int]是一个整型RDD。不过,我们举的例子几乎都省略了这个类型参数,因为Scala支持类型推断。
虽然使用Scala实现RDD概念上很简单,但还是要处理一些Scala闭包对象的反射(re?ection)问题。如何通过Scala解释器来使用Spark还需要更多工作,这点我们将在第6部分讨论。不管怎样,我们都不需要修改Scala编译器。
3.1 Spark中的RDD操作
表2(参见论文原文)列出了Spark中的RDD转换(transformation)和行为(action)。每个操作都给出了标识,其中方括号表示类型参数。前面说过转换是懒操作,用于定义新的RDD;而行为启动计算操作,并向用户程序返回值或向外部存储写数据。
注意,有些操作只对键值对(key-value pairs)可用,比如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 迭代式机器学习
很多机器学习算法都具有迭代特性,运行迭代优化方法来优化某个目标函数,例如梯度下降方法。如果这些算法的工作集(working set)能够放入RAM,将极大地加速程序运行。而且,这些算法通常采用批量操作,例如映射和求和,这样更容易使用RDD来表示。
例如下面的程序是逻辑回归的实现。逻辑回归是一种常见的分类算法,即寻找一个最佳分割两组点(即垃圾邮件和非垃圾邮件)的超平面w。算法采用梯度下降的方法:开始时w为随机值,在每一次迭代的过程中,对w的函数求和,然后朝着优化的方向移动w。
val points = spark.textFile(...).map(parsePoint).cache() 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也可以用来实现常 用的学习算法。
4.2 使用RDD实现MapReduce
MapReduce模型很容易使用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
Pregel是面向图算法的基于批量同步并行模型(Bulk Synchronous Parallel paradigm)的编程模型。程序由一系列超步(superstep)协调迭代运行。在每个超步中,各个顶点执行用户函数,并更新相应的顶点状态,变异 图拓扑,然后向下一个超步的顶点集发送消息。这种模型能够描述很多图算法,包括最短路径,双边匹配和PageRank等。
以PageRank为例介绍一下Pregel的实现。当前PageRank记为r,顶点表示状态。在每个超步中,各个顶点向其所有邻居发送贡献值 (contribution)r/n,这里n是邻居的数目。下一个超步开始时,每个顶点将其分值(rank)更新为 (公式无法显示,参见原文) ,这里的求和是各个顶点收到的所有贡献值的和,N是顶点的总数。
Pregel将输入的图划分(partition)到各个worker上,并存储在其内存中。在每个超步中,各个worker通过一种类似MapReduce的混排(shuffle)操作交换消息。
Pregel的通信模式可以用RDD来描述,如图3。主要思想是:将每个超步中的顶点状态和要发送的消息存储为RDD,然后根据顶点ID分组,进行 混排通信(即cogroup操作)。然后对每个顶点ID上的状态和消息应用(apply)用户函数(即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)
图3 使用RDD实现Pregel时,一步迭代的数据流。(方框表示RDD,箭头表示转换)
需要注意的是,这种实现方法中, RDD grouped,newData和newVerts的分区方法与输入RDD vertices一样。所以,顶点状态一直存在于它们开始执行的机器上,这跟原Pregel一样,这样就减少了通信成本。因为cogroup和 mapValues保持了与输入RDD相同的分区方法,所以分区是自动进行的。
完整的Pregel编程模型还包括其他工具,比如combiner,附录A讨论了它们的实现。下面将讨论Pregel的容错性,以及如何在实现相同容错性的同时减少需要执行检查点操作的数据量。
我们差不多用了100行Scala代码在Spark上实现了一个类Pregel的API。7.2小节将使用PageRank算法评估它的性能。
引自:http://www.ninqing.net/?p=74 原作者:甯青_