シリアライズ不可能なタスク:オブジェクトではなくクラスでのみ、クロージャーの外部で関数を呼び出すと、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

回答:


334

RDDはSerialisableインターフェースを拡張するため、これがタスクの失敗の原因ではありません。これはRDD、Sparkでシリアル化して回避できることを意味しませんNotSerializableException

Sparkは分散コンピューティングエンジンであり、その主な抽象概念は、分散コレクションと見なすことができる復元力のある分散データセット(RDD)です。基本的に、RDDの要素はクラスターのノード間で分割されますが、Sparkはユーザーからこれを抽象化し、ユーザーがローカルのRDD(コレクション)と同様にRDD(コレクション)を操作できるようにします。

あまりにも多くの詳細に入るが、あなたはRDD(上の異なる変換を実行するとしないmapflatMapfilterなど)、あなたの変換コード(クロージャ)は次のとおりです。

  1. ドライバノードでシリアル化され、
  2. クラスタ内の適切なノードに出荷され、
  3. 逆シリアル化、
  4. そして最後にノードで実行されます

もちろん、これをローカルで(例のように)実行することもできますが、それらすべてのフェーズ(ネットワーク経由の配送を除く)は引き続き発生します。[これにより、本番環境にデプロイする前でもバグをキャッチできます]

2番目のケースで何が起こるかはtesting、map関数内からクラスで定義されたメソッドを呼び出すことです。Sparkはそれを認識し、メソッドを単独でシリアル化することはできないため、Sparkはクラス全体 をシリアル化しようとしtestingます。そのため、コードは別のJVMで実行されても機能します。次の2つの可能性があります。

クラステストをシリアライズ可能にすると、クラス全体を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)、彼らはまったく同じです。通常、2番目の方が読みやすく、冗長ではないため、2番目の方法が推奨されます。

編集(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
場合は、あなたは、シリアライズする必要がクラスを制御することはできません...あなたはスカラ座を使用している場合、あなただけのシリアル化とそれをインスタンス化することができます:val test = new Test with Serializable
マーク・S

4
「rddList.map(someFunc(_))からrddList.map(someFunc)まではまったく同じです」いいえ、まったく同じではありません。実際、後者を使用すると、前者がそうでなかった場合でも直列化例外が発生する可能性があります。
samthebest 2016

1
@samthebestは、map(someFunc(_))が直列化例外を引き起こさないのに対し、map(someFunc)が引き起こさない理由を説明していただけますか?
Alon

31

Gregaの回答は、元のコードが機能しない理由と問題を解決する2つの方法を説明するのに最適です。ただし、このソリューションはあまり柔軟ではありません。クロージャに、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)

この手法には、KryoSerializationWrapperTwitterのChillがコアSparkによってすでに組み込まれているため、にアクセスするために追加のShark依存関係を必要としないという利点もあります。


こんにちは、私があなたのコードを使用する場合、何かを登録する必要があるのでしょうか?私はしようとしましたが、kryoからUnable findクラス例外を取得しました。THX
G_cy 2016

25

これらのシリアル化の問題を回避するための優れたパラダイムシフトの方法を提案する問題を完全に説明する完全なトーク:https : //github.com/samthebest/dump/blob/master/sams-scala-tutorial/serialization-exceptions-and-memory- Leaks-no-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

この問題を別の方法で解決しました。クロージャーを通過する前にオブジェクトをシリアル化し、後で逆シリアル化するだけです。このアプローチは、クラスが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


これは実際にインスタンスをシリアル化するか、静的インスタンスを作成して参照をシリアル化しますか(私の回答を参照)。
samthebest 2014

2
@samthebest詳しく説明していただけますか?調査KryoSerializationWrapperすると、Sparkが実際にそうであると考えるようになります。これはjava.io.Serializable、Kryoを使用して内部的にオブジェクトをシリアル化するだけであり、高速でシンプルです。そして、それは静的インスタンスを扱っているとは思いません-value.apply()が呼び出されたときに値を逆シリアル化するだけです。
Nilesh 14

8

私は同様の問題に直面し、グレガの答えから私が理解していることは

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(_)メソッドをシリアル化しようとしていますが、メソッドはシリアル化可能ではないため、クラステストをシリアル化しようとしますが、これもシリアル化可能ではありません。

だから、あなたが定義する必要があり、あなたのコードを動作させる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でも同じ問題に直面しています。RDDforeachメソッド内でJava IOパッケージのFileWriterクラスを使用しようとしています。これを解決する方法を教えてください。
シャンカール2015

1
場合まあ@Shankarは、FileWriterあるfinal外部クラスのフィールド、あなたはそれを行うことはできません。ただし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.map(row =>rdd.mapPartitions(partition => {関数に変換することで、遭遇したり個人的に記述したカスタムRDD変換を変更します。

古い(素晴らしい)方法

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

この新しい方法は、パーティションごとに1回だけブロードキャスト変数を呼び出すので、より優れています。クラスを登録しない場合でも、Javaシリアル化を使用する必要があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.