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


224

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

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

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

问题是我需要在类而不是对象中的代码。知道为什么会这样吗?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
}

什么是Spark.ctx?没有使用ctx AFAICT方法的Spark对象
javadba 2014年

Answers:


334

RDD扩展了Serialisable接口,所以这不是导致您的任务失败的原因。现在,这并不意味着您可以RDD使用Spark 序列化并避免NotSerializableException

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

不要让太多细节,但是当你在一个RDD(运行不同的变换mapflatMapfilter等),您的转换代码包(closure)是:

  1. 在驱动程序节点上序列化,
  2. 运送到群集中的适当节点,
  3. 反序列化
  4. 最后在节点上执行

您当然可以在本地运行此程序(如您的示例中所示),但是所有这些阶段(除了通过网络传输外)仍然会发生。[这使您甚至可以在部署到生产之前捕获任何错误]

在第二种情况下,您正在调用的是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
}

类序列化的相似但不相同的问题可能引起您的兴趣,您可以在此Spark Summit 2013演示文稿中进行阅读

另外,您可以重写rddList.map(someFunc(_))rddList.map(someFunc),它们完全相同。通常,第二个是首选,因为它不那么冗长和清晰。

编辑(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
嗯,您所解释的内容肯定是有道理的,并说明了为什么整个类都被序列化(我还没有完全理解的东西)。尽管如此,我仍然认为rdd不能序列化(它们扩展了Serializable,但这并不意味着它们不会引起NotSerializableException,请尝试一下)。这就是为什么如果将它们放在类之外,则可以修复错误。我将稍微修改我的答案,以更精确地理解我的意思-即它们会导致异常,而不是它们会扩展接口。
samthebest 2014年

35
如果您无法控制该类,则需要将其序列化...如果您使用的是Scala,则可以使用Serializable实例化它:val test = new Test with Serializable
Mark S

4
“ rddList.map(someFunc(_))与rddList.map(someFunc)完全相同”不,它们并不完全相同,实际上,使用后者会导致序列化异常,而前者则不会。
samthebest,2016年

1
@samthebest您能解释一下为什么map(someFunc(_))不会导致序列化异常,而map(someFunc)会导致序列化异常吗?
阿隆

31

Grega的答案很好地解释了原始代码为何不起作用以及解决该问题的两种方法。但是,这种解决方案不是很灵活。考虑您的闭包包括对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 引入


嗨,我想知道我是否需要使用您的代码进行注册?我试着从kryo获取了Unable find class异常。THX
G_cy

25

完整的演讲完全解释了这个问题,提出了一种避免这些序列化问题的绝佳范式转换方法:https : //github.com/samthebest/dump/blob/master/sams-scala-tutorial/serialization-exceptions-and-memory-泄漏无ws.md

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

作为在这种特定情况下的快速解决方案,您可以只使用@transient批注来告诉它不要尝试序列化有问题的值(此处Spark.ctx是自定义类,不是OP命名后的Spark的自定义类):

@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似乎仍会引发异常类。”

向Kryo注册课程的策略

当然,这只会为您提供类型级别的控件,而不是值级别的控件。

...更多的想法。


9

我用另一种方法解决了这个问题。您只需要在通过闭包之前对对象进行序列化,然后再进行反序列化即可。即使您的课程不是可序列化的,这种方法也行得通,因为它在后台使用了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


这实际上是序列化实例还是创建静态实例并序列化引用(请参阅我的答案)。
samthebest 2014年

2
@samthebest您能详细说明吗?如果进行调查,KryoSerializationWrapper您会发现它使Spark认为确实如此java.io.Serializable-它只是使用Kryo在内部对对象进行了序列化-更快,更简单。而且我不认为它处理静态实例-只是在调用value.apply()时反序列化该值。
Nilesh 2014年

8

我遇到了类似的问题,从Grega的回答中我了解到

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(_))
}

并且,如果有多个功能出现,那么所有这些功能都应可用于父级上下文。


7

我不确定这是否适用于Scala,但是在Java中,我NotSerializableException通过重构代码来解决该问题,以便闭包不会访问不可序列化的final字段。


我在Java中遇到了同样的问题,我试图在RDD foreach方法中使用Java IO包中的FileWriter类。您能否让我知道我们如何解决这个问题。
香卡2015年

1
@Shankar好吧,如果the FileWriterfinal外部类的一个字段,则不能这样做。但是FileWriter可以从a String或a 构造File,两者都是Serializable。因此,重构代码以FileWriter根据外部类的文件名构造一个本地。
Trebor Rude 2015年

0

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

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

spark.kryo.unsafe="false"

要么

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序列化。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.