Spark编程指引(二)---------------RDD介绍,闭包对RDD的影响,如何打印RDD。

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)
我们从外部文件定义了一个RDD,这个数据集并没有加载到内存,仅仅定义了一个指向这个文件的变量lines.

然后我们定义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) }
}
上面的例子中,我们定义了一个类MyClass.doStuff方法内部,rdd的map方法引用了Myclass的实例方法func1.所以我们必须把整个对象发送给集群。这与写成rdd.map(this.func1)是一样的。

相似的,访问方法外部的对象变量也会引用整个对象。

class MyClass {
  val field = "Hello"
  def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(x => field + x) }
}
上面的写法和rdd.map(x=>this.field+x)是等价的。为了避免发送整个对象这种问题,最简单的方法是定义一个局部变量保存field的副本,而不是访问外部的。

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

你可能感兴趣的:(hadoop,spark,spark,大数据,分布式,闭包)