web-dev-qa-db-ja.com

タスクが直列化できない:オブジェクトではなくクラスに対してのみクロージャ外部関数を呼び出すときのJava.io.NotSerializableException

クロージャの外側でfunctionを呼び出すときに奇妙な振る舞いをする:

  • 関数がオブジェクトの中にあるとき、すべてが働いています
  • 関数がクラス内にある場合

タスクが直列化できません: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
}
199
Nimrod007

私は他の答えが完全に正しいとは思わない。 RDDは確かにシリアライズ可能です したがって、これはあなたのタスクが失敗する原因になっているものではありません。

Sparkは分散コンピューティングエンジンであり、その主な抽象化は分散型コレクションと見なすことができる回復力のある分散型データセット(RDD)です。基本的に、RDDの要素はクラスタのノードにまたがって分割されますが、Sparkはこれをユーザーから切り離して、ユーザーがRDD(コレクション)と対話するようにします。

あまり多くの詳細に入らないようにしますが、RDDでさまざまな変換(mapflatMapfilterなど)を実行すると、変換コード(クロージャ)は次のようになります。

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

もちろん、これをローカルで(あなたの例のように)実行することはできますが、(ネットワークを介した出荷を除く)これらすべてのフェーズは依然として発生します。 [これにより、運用環境にデプロイする前でもバグを発見できます]

2番目のケースで起こることは、マップ関数の内側からクラスtestingで定義されたメソッドを呼び出しているということです。 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番目は読みやすく、冗長ではないため推奨されます。

EDIT(2015-03-15): SPARK-5307 が導入されたSerializationDebuggerとSpark 1.3.0がそれを使用する最初のバージョンです。NotSerializableExceptionにシリアル化パスを追加します。 NotSerializableExceptionが発生すると、デバッガはオブジェクトグラフを参照して、シリアル化できないオブジェクトへのパスを見つけ、ユーザーがそのオブジェクトを見つけるのに役立つ情報を構成します。

OPの場合、これは標準出力に出力されるものです。

Serialization stack:
    - object not serializable (class: testing, value: [email protected])
    - field (class: testing$$anonfun$1, name: $outer, type: class testing)
    - object (class testing$$anonfun$1, <function1>)
293
Grega Kešpret

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)

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

31
Ben Sidhom

これらのシリアル化問題を回避するための偉大なパラダイムシフト方法を提案する問題を完全に説明した完全な話: 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は例外をスローします。クラス。"

クラスをkryoに登録するための戦略

もちろん、これはあなたに値レベルの制御ではなくタイプレベルの制御を与えるだけです。

...もっとアイデアがあります。

25
samthebest

私はこの問題を別の方法で解決しました。クロージャを通過する前にオブジェクトをシリアライズし、その後デシリアライズするだけです。このアプローチは、クラスが直列化可能でなくても、正しく機能します。これは、舞台裏で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を、クラス、コンパニオンオブジェクト、ネストしたクラス、複数のサードパーティ製ライブラリへの参照など、必要なだけ複雑にしないでください。

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

8
Nilesh

これがScalaに当てはまるかどうかは完全にはわかりませんが、Javaでは、クロージャが直列化不可能なNotSerializableExceptionフィールドにアクセスしないようにコードをリファクタリングすることによってfinalを解決しました。

7
Trebor Rude

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

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(_)メソッドをシリアライズしようとしていますが、methodはシリアライズできないのでクラスをシリアライズしようとしていますtestingこれもシリアライズ可能ではありません。

それであなたのコードを動かして、あなたはdoITメソッドの中にsomeFuncを定義するべきです。例えば:

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

そして、構想に入る複数の関数があるなら、それらのすべての関数は親コンテキストに利用可能であるべきです。

7
Tarang Bhalodia