RDD Operations
RDD支持两种操作类型:
转换,从现有数据集创建一个新的数据集。
动作:在数据集上进行计算,并对驱动程序返回一个值。
比如,map是一个转换,它对数据集中的每个元素执行一个函数,然后返回一个新的RDD代表执行结果。reduce是一个动作,它通过一些函数聚合RDD中的所有元素,然后对驱动程序返回最后的结果。(虽然也有一个并行的reduceByKey函数返回一个分布式数据集)
所有的转换操作在Spark中都是懒操作,这意味着转换并不立即计算结果。它仅仅是记录应用到数据集(比如文件)上的转换。当在需要在数据集上执行动作操作返回值给驱动程序时,转换操作才真正被计算。这种设计是为了使Spark运行起来更高效---------比如,我们意识到用map操作创建的数据集最终将被reduce操作使用并返回结果给驱动程序,而不需要保留中间的更大的map数据集。
默认情况下,当在每个转换RDD上执行动作操作时,RDD将会被重新计算。然而,你也许需要持久化RDD在内存中,可以通过执行persist(或者cache)方法。这样,Spark将在集群上保持RDD,这样就会加快下一次的访问速度。你也可以持久化RDD到磁盘上,或者复制到多个节点上。
基础知识
为了说明RDD的基础知识,我们看下面的简单例子:
val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLengths.reduce((a, b) => a + b)
然后我们定义lineLegnths为map转换的返回结果。因为惰性计算的缘故,lineLengths并没有直接计算。最后我们执行一个动作操作reduce.这促使Spark将计算划分为不同的任务在分隔的集群机器上执行。每台机器运行map,reduce,最后返回结果给驱动程序。
如果我们想在以后使用lineLengths,我们可以在reduce之前执行
lineLengths.persist()
向Spark传递函数
Spark的API很大程度上依懒于向运行于集群上的驱动程序传递函数。有两种建议的方式做到这一点:
匿名函数:当代码量很少时
全局单例对象中的静态方法:比如你可以向下面这样,定义object MyFunctions然后传递MyFunctions.fun1.
object MyFunctions {
def func1(s: String): String = { ... }
}
myRdd.map(MyFunctions.func1)
注意,也有可能传递RDD引用给一个类的实例的方法(而不是一个单例对象),这要求发送包含该类随着方法的对象。例如:
class MyClass {
def func1(s: String): String = { ... }
def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(func1) }
}
相似的,访问方法外部的对象变量也会引用整个对象。
class MyClass {
val field = "Hello"
def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(x => field + x) }
}
def doStuff(rdd: RDD[String]): RDD[String] = {
val field_ = this.field
rdd.map(x => field_ + x)
}
理解闭包
当在集群上执行代码时,理解变量和方法的作用域和生存期是一个难点。RDD操作可以修改作用域之外的变量是造成困惑的常见原因之一。
下面,我们以通过foreach方法增加counter变量的值来说明这个问题,对于其它操作也存在同样的问题。
考虑下面的rdd,执行结果将会完全不同,取决于执行的虚拟机是否一样。另外使用--master = local[n]与部署应用在集群上的结果也会不一样。
var counter = 0
var rdd = sc.parallelize(data)
// Wrong: Don't do this!!
rdd.foreach(x => counter += x)
println("Counter value: " + counter)
本地VS集群模式
主要的挑战是,上面代码的执行结果是不确定的。在单一虚拟机的本地环境中,上面的代码会计算RDD所有变量的和并存储到counter变量中。这是因为RDD和变量counter存储在驱动节点的同一内存空间中。
然而,在集群模式中,会发生什么就比较复杂了,上面的代码甚至都不能按预期工作。为了执行工作,Spark将RDD的操作划分为不同的任务,每个任务被一个执行体执行。在执行之前,Spark会计算闭包。由于闭包的存在,为了执行RDD上的计算,这些变量和方法必须对所有的执行体可见。这个闭包被序列化并发送到每个执行体。在本地模式中,只有一个执行体,所以所有的东西都共享同一个闭包。然而在其它的模式中,情况就不同了。执行体在不同的节点上执行,每个执行体都有一个闭包的拷贝。
这种情况下,包含变量的闭包的拷贝会发送到每个执行体,这样,在foreach里引用的counter就不是驱动节点中的counter了。在驱动节点中的内存里仍存在这一个counter,但是它对所有的执行体都是不可见的。执行体只会看到拷贝中的变量。这样,counter的最终结果就会是0.因为所有的操作都是在序列化了的闭包上的变量上进行的。
为了确保在各种场景下行为的确定性,我们需要使用累加器。累加器是Spark专门提供的一种机制,用来保证当执行被划分在集群上的不同节点上时,安全地更新一个变量。累加器部分将会更详细地讨论。
通常情况下,由循环或局部方法构成的闭包,不应该改动一些全局的状态。Spark没有定义和保证修改从闭包外引用的对象的行为。一些这样做的代码在本地模式可能可以工作,但这也是偶然的。这样的代码在分布模式下将会不能按预期那样工作。如果需要一些全局的聚合计算,使用累加器。
打印RDD的元素
打印RDD中的所有元素的通常作法是使用rdd.foreach(println)
或者 rdd.map(println)
.在单一的机器上,这样做会产生期望的结果。然而,在集群模式下,输出将会在每个执行体的标准输出上,而不是在驱动节点上,所以驱动节点的标准输出将不会有结果。为了在驱动节点上打印所有元素,可以先使用collect方法将RDD带到驱动节点上,rdd.collect().foreach(println)
。这样做可能会造成驱动节点内存溢出,因为collect方法将整个RDD收集到一台机器上。如果你只是想打印RDD的部分元素,可以使用较为安全的做法:rdd.take(100).foreach(println)
这一节主要讲述了RDD的两种基本操作,以及函数和闭包的概念。以及在集群模式中需要注意的闭包和打印所有元素的问题。下一节继续介绍RDD的相关内容。
翻译整理自:http://spark.apache.org/docs/latest/programming-guide.html#AccumLink