Spark函数传递:闭包和单例模式

闭包例子

在Spark的集群模式中,每一个Spark应用由负责运行用户的main函数的driver program和并行运行在集群中的工作进程组成。主要的抽象数据结构是RDD,可以在集群中并行的被操作,其主要提供了两个操作:transformations以及actions。这些都是Spark的基本内容,稍微提及一下,由以下一个小例子引入正题:

val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLengths.reduce((a, b)=> a + b)

    Spark的API很大程度上依赖于集群中的驱动程序向集群传递函数。如上,

·        第一行通过一个外部文件定义了一个基本的RDD。这个数据集未被加载到内存,也未执行操作:lines仅仅指向这个文件。

·        第二行定义了lineLengths作为map转换结果。此外,由于惰性,不会立即计算lineLengths。

·        最后,我们运行reduce,这是一个动作。这时候,Spark才会将这个计算拆分成不同的task,并运行在独立的机器上,并且每台机器运行它自己的map部分和本地的reducatin,仅仅返回它的结果给驱动程序

将函数传入Spark

reduce作为actions操作,触发提交job的过程,lines.map中的匿名函数s=>s.length被封装成闭包,发送到worker节点上去执行。显然,闭包是有状态的,这主要是指它牵涉到的那些自由变量以及自由变量依赖到的其他变量,所以,在将一个简单的函数或者一段代码片段(就是闭包)传递给类似RDD.map这样的操作前,Spark需要检索闭包内所有的涉及到的变量(包括传递依赖的变量),正确地把这些变量序列化之后才能传递到worker节点并反序列化去执行。

官网提供了两种推荐的方法去实现:

1:匿名函数,用于较短的代码

2:单例模式

       至于第三种Class对象的方式,存在很多隐患,除非你明确需要它实现特殊的目的,否则不建议使用。

针对2单例模式,如下:

object MyFunctions{
  def func1(s:String):String={...}
}
myRdd.map(MyFunctions.func1)

稍后详细介绍单例模式,先看看Class对象方式如下:

class MyClass{
  val field="Hello"
  def doStuff(rdd:RDD[String]):RDD[String]={rdd.map(x=>field+x)}
}

       此时,如果我们新建了一个MyClass的对象,并且调用doStuff函数,传进去一个RDD参数,此时map将会引用整个MyClass对象,将其序列化,放入闭包中,如果MyClass无法正确的进行序列化,将会报错:

org.apache.spark.SparkException: Task not serializable

因为{rdd.map(x=>field+x)}部分是{rdd.map(x=>this.field+x)}的简写,this将会引用整个对象。为了避免这种情况,可以如下方式:

def doStuff(rdd:RDD[String]):RDD[String]={
  val field_=this.field
  rdd.map(x=>field_+x)
}

将对象的属性值赋值给一个临时变量,此时不会再引用整个对象。

理解闭包

关于Spark的一个更困难的问题是理解当在一个集群上执行代码的时候,变量和方法的范围以及生命周期。修改范围之外变量的RDD操作经常是造成混乱的源头。在下面的实例中我们看一下使用foreach()来增加一个计数器的代码,不过同样的问题也可能有其他的操作引起。

考虑下面的单纯的RDD元素求和,根据是否运行在同一个虚拟机上,它们表现的行为完全不同。一个简单的例子是在local模式(–master=local[n])下运行Spark对比将Spark程序部署到一个集群上(例如通过spark-submit提交到YARN):

varcounter=0
var rdd=sc.parallelize(data)
// Wrong: Don't do this!!
rdd.foreach(x=>counter+=x)
println("Counter value: "+counter)

本地模式 VS 集群模式

上述代码的行为是未定义的,不能按照预期执行。为了执行作业,Spark将RDD操作拆分成多个task,每个任务由一个执行器操作。在执行前,Spark计算闭包。闭包是指执行器要在RDD上进行计算时必须对执行节点可见的那些变量和方法(在这里是foreach())。这个闭包被序列化并发送到每一个执行器。

闭包中的变量被发送到每个执行器都是被拷贝的,因此,当计数器在foreach函数中引用时,它不再是驱动节点上的那个计数器了。在驱动节点的内存中仍然有一个计数器,但它对执行器来说不再是可见的了。执行器只能看到序列化闭包中的拷贝。因此,计数器最终的值仍然是0,因为所有在计数器上的操作都是引用的序列化闭包中的值。

单例模式

         但是,以上更多的是关于匿名函数的闭包方式的讨论,第二种单例模式的处理方式与以上所述存在一些区别。

         在一个Spark应用的执行过程中,Driver和Worker是两个重要角色。Driver程

序是应用逻辑执行的起点,负责作业的调度,即Task任务的分发,而多个Worker用来管理

计算节点和创建Executor并行处理任务。在执行阶段,Driver会将Task和Task所依赖的file和jar序列化后传递给对应的Worker机器,同时Executor对相应数据分区的任务进行处理。注意,所有的Executor上都会获取一份程序的jar包。

         单例模式是一种常用的设计模式,但是在集群模式下的 Spark 中使用单例模式会引发一些错误。我们用下面代码作例子,解读在 Spark 中使用单例模式遇到的问题。

object Example{
  var instance:Example = new Example("default_name");
  def getInstance():Example = {
    return instance
  }
  def init(name:String){
    instance = new Example(name)
  }
}
class Example private(name1:String) extends  Serializable{
  var name = name1
}
 
object Main{
  def main(args:Array[String]) = {
    Example.init("new_name")
    val sc =  new SparkContext(newSparkConf().setAppName("test"))
 
    val rdd = sc.parallelize(1 to 10, 3)
    rdd.map(x=>{
      x + "_"+ Example.getInstance().name
    }).collect.foreach(println)
  }
}

本地运行的结果是:

1_new_name
2_new_name
3_new_name
4_new_name
7_new_name
5_new_name
8_new_name
6_new_name
10_new_name
9_new_name
12_new_name
11_new_name

注释掉setMaster("local[5]")部分,提交到Spark集群运行,得到的结果是:

1_default_name
2_default_name
3_default_name
4_default_name
5_default_name
6_default_name
7_default_name
8_default_name
9_default_name
10_default_name
11_default_name
12_default_name

         注意:我们在rdd.map中使用了Exampleobject,但是并没有对Example可序列化做任何处理,但是程序并没有抛出不可序列化异常,显而易见,当Spark准备闭包的时候,并没有将Example整合对象序列化打包传递到worker端执行。

这是由什么原因导致的呢?Spark执行算子之前,会将算子需要东西准备好并打包(这就是闭包的概念),分发到不同的 executor,但这里不包括类。类存在 jar包中,随着 jar包分发到不同的 executors中。当不同的executors执行算子需要类时,直接从分发的 jar包取得。这时候在 driver上对类的静态变量进行改变,并不能影响executors中的类。拿上面的程序做例子,jar包存的 Example.instance = newExample("default_name"),分发到不同的 executors。这时候不同executors Example.getInstance().name等于"default_name"

参考:

http://www.tuicool.com/articles/jAbeuqJ

http://www.tuicool.com/articles/RJnqqmR

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值