Spark序列化错误之谜

时间:2015-11-14 14:34:36

标签: scala serialization apache-spark jvm

假设我有以下代码:

class Context {
  def compute() = Array(1.0)
}
val ctx = new Context
val data = ctx.compute

现在我们在Spark中运行此代码:

val rdd = sc.parallelize(List(1,2,3))
rdd.map(_ + data(0)).count()

上面的代码抛出org.apache.spark.SparkException: Task not serializable。我不问如何解决它,通过扩展Serializable或创建一个case类,我想了解错误发生的原因。

我不理解的是为什么它抱怨Context类不是Serializable,尽管它不是lambda的一部分:rdd.map(_ + data(0))data这里是一个应该序列化的值数组,但似乎JVM也捕获了ctx引用,在我的理解中,它不应该发生。

据我了解,在shell中,Spark应该从repl上下文中清除lambda。如果我们在delambdafy阶段之后打印树,我们会看到这些部分:

object iw extends Object {
  ... 
  private[this] val ctx: $line11.iw$Context = _;
  <stable> <accessor> def ctx(): $line11.iw$Context = iw.this.ctx;
  private[this] val data: Array[Double] = _;
  <stable> <accessor> def data(): Array[Double] = iw.this.data; 
  ...
}

class anonfun$1 ... {
  final def apply(x$1: Int): Double = anonfun$1.this.apply$mcDI$sp(x$1);
  <specialized> def apply$mcDI$sp(x$1: Int): Double = x$1.+(iw.this.data().apply(0));
  ...
}

因此,发送到工作节点的反编译lambda代码为:x$1.+(iw.this.data().apply(0))。部分iw.this属于Spark-Shell会话,因此,据我所知,它应该被ClosureCleaner清除,因为它与逻辑无关,不应该被序列化。无论如何,调用iw.this.data()会返回Array[Double]变量的data值,该变量在构造函数中初始化:

def <init>(): type = {
  iw.super.<init>();
  iw.this.ctx = new $line11.iw$Context();
  iw.this.data = iw.this.ctx().compute(); // <== here
  iw.this.res4 = ...
  ()
}

在我的理解中ctx值与lambda无关,它不是闭包,因此不应该序列化。我错过了什么或误解了什么?

1 个答案:

答案 0 :(得分:1)

这与Spark认为可以安全地用作闭包的内容有关。这在某些情况下非常直观,因为Spark使用反射并且在许多情况下无法识别Scala的某些保证(不是完整的编译器或任何东西)或者同一对象中的某些变量无关紧要的事实。为了安全起见,Spark将尝试序列化引用的任何对象,在您的情况下包括iw,这是不可序列化的。

ClosureCleaner中的代码有一个很好的例子:

  

例如,以下需要传递性清洁   情形:

class SomethingNotSerializable {
  def someValue = 1
  def scope(name: String)(body: => Unit) = body
  def someMethod(): Unit = scope("one") {
    def x = someValue
    def y = 2
      scope("two") { println(y + 1) }
  }
}
     

在这个例子中,范围&#34;两个&#34;是不可序列化的,因为它引用了范围&#34;一个&#34;,它引用了SomethingNotSerializable。但请注意,范围的主体&#34; 2&#34;实际上并不依赖于SomethingNotSerializable。这意味着我们可以安全地清空克隆范围的父指针&#34;一个&#34;并将其设置为范围的父级&#34; 2&#34;,这样范围&#34; 2&#34;不再传递SomethingNotSerializable。

最简单的修复方法可能是在同一范围内创建一个局部变量,从对象中提取值,这样就不再对lambda中的封装对象进行任何引用了:

val rdd = sc.parallelize(List(1,2,3))
val data0 = data(0)
rdd.map(_ + data0).count()