ภารกิจไม่สามารถทำให้เป็นอนุกรมได้: java.io.NotSerializableException เมื่อเรียกใช้ฟังก์ชันภายนอกที่ปิดเฉพาะคลาสที่ไม่ใช่วัตถุเท่านั้น


224

รับพฤติกรรมที่ผิดปกติเมื่อเรียกใช้ฟังก์ชันนอกการปิด:

  • เมื่อฟังก์ชั่นอยู่ในวัตถุทุกอย่างทำงานได้
  • เมื่อฟังก์ชั่นอยู่ในคลาสจะได้รับ:

ภารกิจไม่สามารถทำให้เป็นอนุกรมได้: 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
}

Spark.ctx คืออะไร ไม่มีวัตถุ Spark ด้วยวิธี ctx AFAICT
javadba

คำตอบ:


334

RDD ขยายส่วนต่อประสาน Serialisableดังนั้นนี่ไม่ใช่สิ่งที่ทำให้งานของคุณล้มเหลว ตอนนี้ไม่ได้หมายความว่าคุณสามารถซีเรียลRDDกับ Spark และหลีกเลี่ยงNotSerializableException

Spark เป็นเอ็นจิ้นการคำนวณแบบกระจายและนามธรรมหลักคือชุดข้อมูลแบบกระจายที่ยืดหยุ่น ( RDD ) ซึ่งสามารถดูได้เป็นคอลเลกชันแบบกระจาย โดยทั่วไปองค์ประกอบของ RDD จะถูกแบ่งพาร์ติชันข้ามโหนดของคลัสเตอร์ แต่ Spark จะแยกสิ่งนี้ออกจากผู้ใช้ทำให้ผู้ใช้โต้ตอบกับ RDD (คอลเลกชัน) ราวกับว่ามันเป็นโลคัล

จะไม่ได้รับเข้าไปในรายละเอียดมากเกินไป แต่เมื่อคุณเรียกใช้การเปลี่ยนแปลงที่แตกต่างกันใน RDD ( map, flatMap, filterและอื่น ๆ ) รหัสการเปลี่ยนแปลงของคุณ (ปิด) เป็น:

  1. ต่อเนื่องบนโหนดไดรเวอร์
  2. ส่งไปยังโหนดที่เหมาะสมในคลัสเตอร์
  3. deserialized,
  4. และในที่สุดก็ดำเนินการบนโหนด

แน่นอนคุณสามารถเรียกใช้สิ่งนี้ในพื้นที่ (เช่นในตัวอย่างของคุณ) แต่ขั้นตอนเหล่านั้นทั้งหมด (นอกเหนือจากการจัดส่งผ่านเครือข่าย) ยังคงเกิดขึ้น [สิ่งนี้ช่วยให้คุณสามารถดักจับข้อบกพร่องก่อนที่จะปรับใช้กับการผลิต]

สิ่งที่เกิดขึ้นในกรณีที่สองของคุณคือคุณกำลังเรียกวิธีการที่กำหนดไว้ในชั้นเรียนtestingจากภายในฟังก์ชั่นแผนที่ 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

35
ในกรณีที่คุณไม่สามารถควบคุมชั้นเรียนได้คุณจะต้องต่อเนื่องกันได้ ... ถ้าคุณใช้ Scala คุณสามารถสร้างอินสแตนซ์ของมันได้ด้วย Serializable:val test = new Test with Serializable
Mark S

4
"rddList.map (someFunc (_)) ไปยัง rddList.map (someFunc) พวกเขาเหมือนกันทุกประการ" ไม่พวกเขาไม่เหมือนกันทุกประการและในความเป็นจริงการใช้หลังอาจทำให้เกิดข้อยกเว้นต่อเนื่องกัน
samthebest

1
@samthebest คุณช่วยอธิบายได้ไหมว่าเหตุใดแผนที่ (someFunc (_)) จะไม่ทำให้เกิดข้อยกเว้นต่อเนื่องในขณะที่แผนที่ (someFunc) จะเป็นอย่างไร
Alon

31

คำตอบของ Gregaนั้นยอดเยี่ยมในการอธิบายว่าทำไมรหัสต้นฉบับไม่ทำงานและสองวิธีในการแก้ไขปัญหา อย่างไรก็ตามวิธีนี้ไม่ยืดหยุ่นมาก พิจารณากรณีที่การปิดของคุณรวมถึงการเรียกใช้เมธอดบน non- Serializableclass ที่คุณไม่สามารถควบคุมได้ คุณไม่สามารถเพิ่มSerializableแท็กในคลาสนี้หรือเปลี่ยนการใช้งานพื้นฐานเพื่อเปลี่ยนวิธีการในฟังก์ชั่น

Nileshนำเสนอวิธีแก้ปัญหาที่ยอดเยี่ยมสำหรับสิ่งนี้ แต่วิธีแก้ปัญหาสามารถทำให้ทั้งกระชับและทั่วไปมากขึ้น:

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

ฟังก์ชั่น serializer นี้สามารถใช้ในการตัดการปิดและการเรียกเมธอดโดยอัตโนมัติ:

rdd map genMapper(someFunc)

เทคนิคนี้ยังมีประโยชน์ในการไม่ต้องใช้การพึ่งพาฉลามเพิ่มเติมเพื่อที่จะเข้าถึงKryoSerializationWrapperเนื่องจาก Chill ของ Twitter ถูกดึงเข้ามาโดยแกนหลักแล้ว


สวัสดีฉันสงสัยว่าฉันต้องลงทะเบียนอะไรถ้าฉันใช้รหัสของคุณหรือไม่ ฉันลองและได้รับข้อยกเว้นค้นหาชั้นไม่สามารถจาก kryo THX
G_cy

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 อาศัยอยู่ที่อื่น แต่ก็น่ารังเกียจเช่นกัน

อนาคตอาจเป็นสปอร์

ในอนาคตสกาล่าจะรวมสิ่งต่าง ๆ เหล่านี้เรียกว่า "สปอร์" ที่จะทำให้เราสามารถควบคุมเมล็ดพืชได้ดีสิ่งที่ทำและไม่ได้ถูกดึงเข้ามาอย่างแน่นอน นอกจากนี้ควรเปลี่ยนความผิดพลาดทั้งหมดของการดึงโดยไม่ตั้งใจในประเภทที่ไม่ต่อเนื่อง (หรือค่าที่ไม่พึงประสงค์) เป็นข้อผิดพลาดในการรวบรวมมากกว่าตอนนี้ซึ่งเป็นข้อยกเว้นรันไทม์ / การรั่วไหลของหน่วยความจำที่น่ากลัว

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

เคล็ดลับในการทำให้เป็นอนุกรมของ Kryo

เมื่อใช้ kyro ทำให้จำเป็นต้องลงทะเบียนซึ่งหมายความว่าคุณได้รับข้อผิดพลาดแทนการรั่วไหลของหน่วยความจำ:

"ในที่สุดฉันรู้ว่า kryo มี kryo.setRegistrationOptional (จริง) แต่ฉันมีช่วงเวลาที่ยากลำบากมากในการพยายามหาวิธีใช้งานเมื่อตัวเลือกนี้เปิดใช้งาน 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 ซับซ้อนเท่าที่คุณต้องการคลาสวัตถุร่วมคลาสที่ซ้อนอยู่อ้างอิงถึง libs ของบุคคลที่สามหลายรายการ

KryoSerializationWrapper อ้างถึง: https://github.com/amplab/shark/blob/master/src/main/scala/shark/execution/serialization/KryoSerializationWrapper.scala


สิ่งนี้ทำให้ซีเรียลอินสแตนซ์เป็นอนุกรมขึ้นหรือสร้างอินสแตนซ์แบบคงที่และทำให้เป็นอนุกรมอ้างอิง (ดูคำตอบของฉัน)
samthebest

2
@ Samthebest คุณช่วยอธิบายได้ไหม? หากคุณตรวจสอบKryoSerializationWrapperคุณจะพบว่ามันทำให้ Spark คิดว่าเป็นจริงjava.io.Serializable- เป็นเพียงแค่ทำให้วัตถุเป็นอนุกรมโดยใช้ Kryo - เร็วขึ้นง่ายขึ้น และฉันไม่คิดว่ามันจะเกี่ยวข้องกับอินสแตนซ์แบบคงที่ - เพียงแค่ยกเลิกการทำให้เป็นอนุกรมของค่าเมื่อเรียกว่า value.apply ()
Nilesh

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 (_)วิธี แต่เป็นวิธีที่ไม่ได้เป็นแบบอนุกรมมันพยายามที่จะทำให้เป็นอนุกรมการทดสอบระดับซึ่งเป็นอีกครั้งที่ไม่เป็นอนุกรม

เพื่อให้โค้ดทำงานได้คุณควรกำหนดsomeFuncภายในวิธีdoIT ตัวอย่างเช่น:

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 ฉันพยายามใช้ FileWriter class จากแพ็คเกจ Java IO ภายในวิธี RDD foreach คุณช่วยกรุณาบอกฉันด้วยว่าเราจะแก้ปัญหานี้ได้อย่างไร
Shankar

1
ดี @Shankar ถ้าFileWriterเป็นfinalเขตของชั้นนอกคุณจะไม่สามารถทำมันได้ แต่FileWriterสามารถสร้างจาก a Stringหรือ a Fileซึ่งทั้งสองSerializableอย่างนั้น ดังนั้น refactor รหัสของคุณเพื่อสร้างท้องถิ่นFileWriterตามชื่อไฟล์จากชั้นนอก
Trebor Rude

0

FYI ใน Spark 2.4 คุณหลายคนอาจจะประสบปัญหานี้ การทำให้เป็นอนุกรมของ Kryo นั้นดีขึ้น แต่ในหลาย ๆ กรณีคุณไม่สามารถใช้ spark.kryo.unsafe = true หรือ naive kryo serializer ที่ไร้เดียงสา

สำหรับการแก้ไขด่วนลองเปลี่ยนสิ่งต่อไปนี้ในการกำหนดค่า Spark ของคุณ

spark.kryo.unsafe="false"

หรือ

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

ฉันปรับเปลี่ยนการแปลง RDD แบบกำหนดเองที่ฉันพบหรือเขียนเป็นการส่วนตัวโดยใช้ตัวแปรบรอดคาสต์ที่ชัดเจนและใช้ประโยชน์จาก inbuilt twitter-chill api ใหม่การแปลงพวกมันจาก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 Serialization ถ้าคุณไม่ลงทะเบียนคลาส

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.