任务不可序列化:java.io.NotSerializableException仅在类而不是对象上调用闭包外的函数时

时间:2014-03-23 15:22:49

标签: scala serialization apache-spark typesafe

在闭包之外调用函数时出现奇怪的行为:

  • 当一个对象中的函数一切正常时
  • 当函数在类get:
  

任务不可序列化:java.io.NotSerializableException:testing

问题是我需要在类中使用代码而不是对象。知道为什么会这样吗? Scala对象是否已序列化(默认?)?

这是一个有效的代码示例:

object working extends App {
    val list = List(1,2,3)

    val rddList = Spark.ctx.parallelize(list)
    //calling function outside closure 
    val after = rddList.map(someFunc(_))

    def someFunc(a:Int)  = a+1

    after.collect().map(println(_))
}

这是一个不起作用的例子:

object NOTworking extends App {
  new testing().doIT
}

//adding extends Serializable wont help
class testing {  
  val list = List(1,2,3)  
  val rddList = Spark.ctx.parallelize(list)

  def doIT =  {
    //again calling the fucntion someFunc 
    val after = rddList.map(someFunc(_))
    //this will crash (spark lazy)
    after.collect().map(println(_))
  }

  def someFunc(a:Int) = a+1
}

11 个答案:

答案 0 :(得分:291)

我不认为另一个答案是完全正确的。 RDDs are indeed serializable,所以这不是导致您的任务失败的原因。

Spark是一个分布式计算引擎,它的主要抽象是弹性分布式数据集( RDD ),可以将其视为分布式集合。基本上,RDD的元素在集群的节点之间进行分区,但Spark将其抽象远离用户,让用户与RDD(集合)进行交互,就好像它是本地的一样。

不要涉及太多细节,但是当您在RDD(mapflatMapfilter和其他人)上运行不同的转换时,您的转换代码(闭包)是:

  1. 在驱动程序节点上序列化,
  2. 发送到群集中的相应节点,
  3. 反序列化,
  4. 最后在节点上执行
  5. 您当然可以在本地运行(如您的示例所示),但所有这些阶段(除了通过网络传送)仍然会发生。 [这使您甚至可以在部署到生产之前捕获任何错误]

    在第二种情况下,您正在调用一个方法,该方法在map testing中从map函数中定义。 Spark看到了这一点,并且由于方法无法自行序列化,因此Spark会尝试序列化整个 testing类,以便代码在另一个JVM中执行时仍然有效。你有两种可能性:

    要么使类测试可序列化,所以整个类可以通过Spark序列化:

    import org.apache.spark.{SparkContext,SparkConf}
    
    object Spark {
      val ctx = new SparkContext(new SparkConf().setAppName("test").setMaster("local[*]"))
    }
    
    object NOTworking extends App {
      new Test().doIT
    }
    
    class Test extends java.io.Serializable {
      val rddList = Spark.ctx.parallelize(List(1,2,3))
    
      def doIT() =  {
        val after = rddList.map(someFunc)
        after.collect().foreach(println)
      }
    
      def someFunc(a: Int) = a + 1
    }
    

    或者你使someFunc函数而不是方法(函数是Scala中的对象),以便Spark能够序列化它:

    import org.apache.spark.{SparkContext,SparkConf}
    
    object Spark {
      val ctx = new SparkContext(new SparkConf().setAppName("test").setMaster("local[*]"))
    }
    
    object NOTworking extends App {
      new Test().doIT
    }
    
    class Test {
      val rddList = Spark.ctx.parallelize(List(1,2,3))
    
      def doIT() =  {
        val after = rddList.map(someFunc)
        after.collect().foreach(println)
      }
    
      val someFunc = (a: Int) => a + 1
    }
    

    类似,但类序列化的问题不同,您可能会感兴趣,并且可以阅读in this Spark Summit 2013 presentation

    作为旁注,您可以将rddList.map(someFunc(_))重写为rddList.map(someFunc),它们完全相同。通常,第二种是优选的,因为它阅读起来不那么冗长和清晰。

    EDIT(2015-03-15):SPARK-5307引入了 SerializationDebugger ,Spark 1.3.0是第一个使用它的版本。它为 NotSerializableException 添加了序列化路径。遇到NotSerializableException时,调试器访问对象图以找到无法序列化的对象的路径,并构造信息以帮助用户找到对象。

    在OP的情况下,这是打印到stdout的内容:

    Serialization stack:
        - object not serializable (class: testing, value: testing@2dfe2f00)
        - field (class: testing$$anonfun$1, name: $outer, type: class testing)
        - object (class testing$$anonfun$1, <function1>)
    

答案 1 :(得分:31)

Grega's answer非常适合解释原始代码无效的原因以及解决问题的两种方法。但是,这个解决方案不是很灵活;考虑一下你的闭包包含一个你无法控制的非Serializable类的方法调用的情况。您既不能将Serializable标记添加到此类,也不能更改底层实现以将方法更改为函数。

Nilesh为此提供了一个很好的解决方法,但解决方案可以更加简洁和通用:

def genMapper[A, B](f: A => B): A => B = {
  val locker = com.twitter.chill.MeatLocker(f)
  x => locker.get.apply(x)
}

然后,可以使用此函数序列化程序自动包装闭包和方法调用:

rdd map genMapper(someFunc)

这项技术的好处是不需要额外的Shark依赖关系来访问KryoSerializationWrapper,因为Twitter的Chill已经被核心Spark所吸引

答案 2 :(得分:25)

完整的讲话充分解释了这个问题,提出了一个很好的范式转换方法来避免这些序列化问题:https://github.com/samthebest/dump/blob/master/sams-scala-tutorial/serialization-exceptions-and-memory-leaks-no-ws.md

最高投票答案基本上建议丢掉整个语言功能 - 不再使用方法而只使用功能。实际上,在函数式编程中应该避免类中的方法,但是将它们转换为函数并不能解决这里的设计问题(参见上面的链接)。

作为这种特殊情况下的快速修复,您可以使用@transient注释告诉它不要尝试序列化有问题的值(这里,Spark.ctx是一个自定义类而不是Spark的一个跟随OP的命名):

@transient
val rddList = Spark.ctx.parallelize(list)

您还可以重新构建代码,以便rddList存在于其他地方,但这也是令人讨厌的。

未来可能是孢子

将来,Scala将包含这些被称为“孢子”的东西,这些东西应该允许我们精细控制粒子控制什么做和不完全被闭合拉入。此外,这应该将所有错误意外地将非可序列化类型(或任何不需要的值)引入编译错误,而不是现在可怕的运行时异常/内存泄漏。

http://docs.scala-lang.org/sips/pending/spores.html

关于Kryo序列化的提示

使用kyro时,请将其设置为必须注册,这意味着您会收到错误而不是内存泄漏:

“最后,我知道kryo有kryo.setRegistrationOptional(true)但是我很难找到如何使用它。当这个选项打开时,kryo似乎仍然会抛出异常,如果我没有注册课程。“

Strategy for registering classes with kryo

当然,这只会为您提供类型级别控制而非值级别控制。

......更多想法来。

答案 3 :(得分:8)

我使用不同的方法解决了这个问题。您只需要在通过闭包之前序列化对象,然后进行反序列化。即使您的类不是Serializable,这种方法也可以正常工作,因为它在幕后使用Kryo。你需要的只是一些咖喱。 ;)

以下是我如何做到的一个例子:

def genMapper(kryoWrapper: KryoSerializationWrapper[(Foo => Bar)])
               (foo: Foo) : Bar = {
    kryoWrapper.value.apply(foo)
}
val mapper = genMapper(KryoSerializationWrapper(new Blah(abc))) _
rdd.flatMap(mapper).collectAsMap()

object Blah(abc: ABC) extends (Foo => Bar) {
    def apply(foo: Foo) : Bar = { //This is the real function }
}

随意使Blah变得如你所愿,类,伴侣对象,嵌套类,对多个第三方库的引用。

KryoSerializationWrapper引用:https://github.com/amplab/shark/blob/master/src/main/scala/shark/execution/serialization/KryoSerializationWrapper.scala

答案 4 :(得分:7)

我不完全确定这适用于Scala,但是在Java中,我通过重构我的代码来解决NotSerializableException,因此闭包不会访问不可序列化的final字段。 / p>

答案 5 :(得分:7)

我遇到了类似的问题,我从Grega's answer了解到的是

object NOTworking extends App {
 new testing().doIT
}
//adding extends Serializable wont help
class testing {

val list = List(1,2,3)

val rddList = Spark.ctx.parallelize(list)

def doIT =  {
  //again calling the fucntion someFunc 
  val after = rddList.map(someFunc(_))
  //this will crash (spark lazy)
  after.collect().map(println(_))
}

def someFunc(a:Int) = a+1

}

您的 doIT 方法正在尝试序列化 someFunc(_)方法,但由于方法不可序列化,它会尝试序列化类测试这也是不可序列化的。

因此,要使代码正常工作,您应该在 doIT 方法中定义 someFunc 。例如:

def doIT =  {
 def someFunc(a:Int) = a+1
  //function definition
 }
 val after = rddList.map(someFunc(_))
 after.collect().map(println(_))
}

如果有多个函数出现在图片中,那么所有这些函数都应该可用于父上下文。

答案 6 :(得分:0)

Spark 2.4中的仅供参考,很多人可能会遇到此问题。 Kryo序列化已经变得更好,但是在许多情况下,您不能使用spark.kryo.unsafe = true或朴素的kryo序列化器。

要快速修复,请尝试在Spark配置中更改以下内容

spark.kryo.unsafe="false"

OR

spark.serializer="org.apache.spark.serializer.JavaSerializer"

我使用显式广播变量并使用新的内置twitter-chill api修改遇到或亲自编写的自定义RDD转换,将其从rdd.map(row =>转换为rdd.mapPartitions(partition => {函数。

示例

旧(非伟大)方式

val sampleMap = Map("index1" -> 1234, "index2" -> 2345)
val outputRDD = rdd.map(row => {
    val value = sampleMap.get(row._1)
    value
})

替代(更好)方式

import com.twitter.chill.MeatLocker
val sampleMap = Map("index1" -> 1234, "index2" -> 2345)
val brdSerSampleMap = spark.sparkContext.broadcast(MeatLocker(sampleMap))

rdd.mapPartitions(partition => {
    val deSerSampleMap = brdSerSampleMap.value.get
    partition.map(row => {
        val value = sampleMap.get(row._1)
        value
    }).toIterator
})

此新方法将每个分区仅调用一次广播变量,这更好。如果不注册类,您仍然需要使用Java序列化。

答案 7 :(得分:0)

def upper(name: String) : String = { 
var uppper : String  =  name.toUpperCase()
uppper
}

val toUpperName = udf {(EmpName: String) => upper(EmpName)}
val emp_details = """[{"id": "1","name": "James Butt","country": "USA"},
{"id": "2", "name": "Josephine Darakjy","country": "USA"},
{"id": "3", "name": "Art Venere","country": "USA"},
{"id": "4", "name": "Lenna Paprocki","country": "USA"},
{"id": "5", "name": "Donette Foller","country": "USA"},
{"id": "6", "name": "Leota Dilliard","country": "USA"}]"""

val df_emp = spark.read.json(Seq(emp_details).toDS())
val df_name=df_emp.select($"id",$"name")
val df_upperName= df_name.withColumn("name",toUpperName($"name")).filter("id='5'")
display(df_upperName)

这将导致错误 org.apache.spark.SparkException:任务无法序列化 在org.apache.spark.util.ClosureCleaner $ .ensureSerializable(ClosureCleaner.scala:304)

解决方案-

import java.io.Serializable;

object obj_upper extends Serializable { 
  def upper(name: String) : String = 
  {
    var uppper : String  =  name.toUpperCase()
    uppper
  }
val toUpperName = udf {(EmpName: String) => upper(EmpName)}
}

val df_upperName= 
df_name.withColumn("name",obj_upper.toUpperName($"name")).filter("id='5'")
display(df_upperName)

答案 8 :(得分:0)

我也有类似的经历。

当我在驱动程序(主机)上初始化变量时触发了错误,但随后尝试在其中一个工作程序上使用它。 发生这种情况时,Spark Streaming将尝试序列化对象以将其发送给工作程序,如果该对象不可序列化,则会失败。

我通过将变量设为静态解决了该错误

先前的无效代码

  private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();

工作代码

  private static final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();

积分:

  1. https://docs.microsoft.com/en-us/answers/questions/35812/sparkexception-job-aborted-due-to-stage-failure-ta.html pradeepcheekatla-msft 的答案)
  2. https://databricks.gitbooks.io/databricks-spark-knowledge-base/content/troubleshooting/javaionotserializableexception.html

答案 9 :(得分:0)

我的解决方案是添加一个 Compagniion 类来处理类中所有不可序列化的方法。

答案 10 :(得分:0)

在类中定义的Scala方法是不可序列化的,方法可以转换成函数来解决序列化问题。

方法语法

def func_name (x String) : String = {
...
return x
}

函数语法

val func_name = { (x String) => 
...
x
}