Tác vụ không tuần tự hóa: java.io.NotSerializableException khi gọi hàm ngoài đóng chỉ trên các lớp không phải đối tượng


224

Nhận hành vi lạ khi gọi chức năng bên ngoài đóng cửa:

  • Khi chức năng ở trong một đối tượng, mọi thứ đều hoạt động.
  • khi hàm ở trong một lớp có được:

Tác vụ không tuần tự hóa: java.io.NotSerializableException: thử nghiệm

Vấn đề là tôi cần mã của mình trong một lớp chứ không phải một đối tượng. Bất cứ ý tưởng tại sao điều này đang xảy ra? Là một đối tượng Scala được tuần tự hóa (mặc định?)?

Đây là một ví dụ mã làm việc:

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

Đây là ví dụ không hoạt động:

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 là gì? Không có đối tượng Spark với phương thức ctx
AFAICT

Câu trả lời:


334

RDD mở rộng giao diện Nối tiếp , vì vậy đây không phải là nguyên nhân khiến nhiệm vụ của bạn thất bại. Bây giờ điều này không có nghĩa là bạn có thể tuần tự hóa RDDvới Spark và tránhNotSerializableException

Spark là một công cụ tính toán phân tán và sự trừu tượng chính của nó là một bộ dữ liệu phân tán ( RDD ) có khả năng phục hồi , có thể được xem như là một bộ sưu tập phân tán. Về cơ bản, các phần tử của RDD được phân vùng trên các nút của cụm, nhưng Spark trừu tượng hóa điều này khỏi người dùng, cho phép người dùng tương tác với RDD (bộ sưu tập) như thể nó là một cục bộ.

Không để có được vào quá nhiều chi tiết, nhưng khi bạn chạy biến đổi khác nhau trên một RDD ( map, flatMap, filtervà những người khác), mã chuyển đổi của bạn (đóng cửa) là:

  1. tuần tự hóa trên nút trình điều khiển,
  2. vận chuyển đến các nút thích hợp trong cụm,
  3. khử lưu huỳnh,
  4. và cuối cùng được thực hiện trên các nút

Tất nhiên bạn có thể chạy cái này cục bộ (như trong ví dụ của bạn), nhưng tất cả các giai đoạn đó (ngoài vận chuyển qua mạng) vẫn xảy ra. [Điều này cho phép bạn bắt bất kỳ lỗi nào ngay cả trước khi triển khai vào sản xuất]

Điều xảy ra trong trường hợp thứ hai của bạn là bạn đang gọi một phương thức, được định nghĩa trong lớp testingtừ bên trong hàm ánh xạ. Spark thấy rằng và vì các phương thức không thể tự được tuần tự hóa, Spark cố gắng tuần tự hóa toàn bộ testing lớp, để mã vẫn hoạt động khi được thực thi trong một JVM khác. Bạn có hai khả năng:

Hoặc là bạn thực hiện kiểm tra tuần tự hóa lớp, vì vậy toàn bộ lớp có thể được tuần tự hóa bởi 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
}

hoặc bạn tạo someFunchàm thay vì một phương thức (các hàm là các đối tượng trong Scala), do đó Spark sẽ có thể tuần tự hóa nó:

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
}

Tương tự, nhưng không phải vấn đề tương tự với việc xê-ri hóa lớp có thể khiến bạn quan tâm và bạn có thể đọc nó trong bài trình bày Spark Summit 2013 này .

Là một mặt lưu ý, bạn có thể viết lại rddList.map(someFunc(_))để rddList.map(someFunc), họ là giống hệt nhau. Thông thường, thứ hai được ưa thích vì nó ít dài dòng và dễ đọc hơn.

EDIT (2015-03-15): SPARK-5307 đã giới thiệu SerializationDebugger và Spark 1.3.0 là phiên bản đầu tiên sử dụng nó. Nó thêm đường dẫn tuần tự hóa vào một NotSerializableException . Khi gặp NotSerializableException, trình gỡ lỗi truy cập vào biểu đồ đối tượng để tìm đường dẫn đến đối tượng không thể được tuần tự hóa và xây dựng thông tin để giúp người dùng tìm thấy đối tượng.

Trong trường hợp của OP, đây là nội dung được in ra thiết bị xuất chuẩn:

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
Hmm, những gì bạn đã giải thích chắc chắn có ý nghĩa và giải thích lý do tại sao toàn bộ lớp học được nối tiếp (điều mà tôi không hiểu đầy đủ). Tuy nhiên, tôi vẫn cho rằng các rdd không thể tuần tự hóa (chúng cũng mở rộng Nối tiếp, nhưng điều đó không có nghĩa là chúng không gây ra NotSerializableException, hãy thử nó). Đây là lý do tại sao nếu bạn đặt chúng bên ngoài các lớp, nó sẽ sửa lỗi. Tôi sẽ chỉnh sửa câu trả lời của mình một chút để chính xác hơn về ý tôi - nghĩa là chúng gây ra ngoại lệ, không phải là chúng mở rộng giao diện.
samthebest

35
Trong trường hợp bạn không có quyền kiểm soát lớp, bạn cần phải tuần tự hóa ... nếu bạn đang sử dụng Scala, bạn có thể khởi tạo nó bằng Nối tiếp:val test = new Test with Serializable
Đánh dấu S

4

1
@samthebest bạn có thể giải thích tại sao bản đồ (someFunc (_)) sẽ không gây ra ngoại lệ nối tiếp trong khi bản đồ (someFunc) sẽ không?
Alon

31

Câu trả lời của Grega rất hay trong việc giải thích tại sao mã gốc không hoạt động và hai cách để khắc phục vấn đề. Tuy nhiên, giải pháp này không linh hoạt lắm; hãy xem xét trường hợp đóng của bạn bao gồm một cuộc gọi phương thức trên một Serializablelớp không phải là lớp mà bạn không có quyền kiểm soát. Bạn không thể thêm Serializablethẻ vào lớp này cũng như không thay đổi triển khai cơ bản để thay đổi phương thức thành hàm.

Nilesh trình bày một cách giải quyết tuyệt vời cho việc này, nhưng giải pháp có thể được thực hiện cả ngắn gọn và tổng quát hơn:

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

Trình tuần tự hóa chức năng này sau đó có thể được sử dụng để tự động đóng các bao đóng và các cuộc gọi phương thức:

rdd map genMapper(someFunc)

Kỹ thuật này cũng có lợi ích là không yêu cầu các phụ thuộc Shark bổ sung để truy cập KryoSerializationWrapper, vì Chill của Twitter đã được rút ra bởi lõi Spark


Xin chào, tôi tự hỏi tôi có cần phải đăng ký một cái gì đó nếu tôi sử dụng mã của bạn? Tôi đã thử và nhận được một ngoại lệ tìm thấy Unable từ kryo. THX
G_cy

25

Nói chuyện hoàn toàn giải thích vấn đề, trong đó đề xuất một cách chuyển đổi mô hình tuyệt vời để tránh các vấn đề tuần tự hóa này: https://github.com/samthebest/dump/blob/master/sams-scala-tutorial/serialization-exceptions-and-memory- rò rỉ-no-ws.md

Câu trả lời được bình chọn hàng đầu về cơ bản là đề xuất loại bỏ toàn bộ tính năng ngôn ngữ - đó là không còn sử dụng các phương thức và chỉ sử dụng các chức năng. Thật vậy, trong các phương thức lập trình chức năng trong các lớp nên tránh, nhưng biến chúng thành các hàm không giải quyết được vấn đề thiết kế ở đây (xem liên kết ở trên).

Để khắc phục nhanh trong tình huống cụ thể này, bạn chỉ có thể sử dụng @transientchú thích để bảo nó không cố gắng tuần tự hóa giá trị vi phạm (ở đây, Spark.ctxlà một lớp tùy chỉnh không phải là Spark theo tên của OP):

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

Bạn cũng có thể cơ cấu lại mã để rddList sống ở một nơi khác, nhưng điều đó cũng khó chịu.

Tương lai có lẽ là bào tử

Trong tương lai Scala sẽ bao gồm những thứ gọi là "bào tử" cho phép chúng ta kiểm soát hạt tốt và không chính xác bị kéo vào bởi một bao đóng. Hơn nữa, điều này sẽ biến tất cả các lỗi vô tình kéo các loại không tuần tự hóa (hoặc bất kỳ giá trị không mong muốn nào) thành lỗi biên dịch thay vì bây giờ là ngoại lệ thời gian chạy / rò rỉ bộ nhớ khủng khiếp.

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

Một mẹo về tuần tự hóa Kryo

Khi sử dụng kyro, hãy thực hiện để đăng ký là cần thiết, điều này có nghĩa là bạn sẽ gặp lỗi thay vì rò rỉ bộ nhớ:

"Cuối cùng, tôi biết rằng kryo có kryo.setRegistrationOptional (đúng) nhưng tôi đang gặp khó khăn khi cố gắng tìm ra cách sử dụng nó. Khi tùy chọn này được bật, kryo dường như vẫn ném ngoại lệ nếu tôi chưa đăng ký các lớp học."

Chiến lược đăng ký lớp học với kryo

Tất nhiên điều này chỉ cung cấp cho bạn điều khiển mức loại chứ không phải điều khiển mức giá trị.

... Nhiều ý tưởng sắp tới.


9

Tôi đã giải quyết vấn đề này bằng cách sử dụng một cách tiếp cận khác. Bạn chỉ cần tuần tự hóa các đối tượng trước khi chuyển qua bao đóng và hủy tuần tự hóa sau đó. Cách tiếp cận này chỉ hoạt động, ngay cả khi các lớp của bạn không Nối tiếp, bởi vì nó sử dụng Kryo đằng sau hậu trường. Tất cả bạn cần là một số cà ri. ;)

Đây là một ví dụ về cách tôi đã làm nó:

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

Vui lòng làm cho Blah phức tạp như bạn muốn, lớp, đối tượng đồng hành, các lớp lồng nhau, tham chiếu đến nhiều lib của bên thứ 3.

KryoSerializationWrapper đề cập đến: https://github.com/amplab/shark/blob/master/src/main/scala/shark/execut/serialization/KryoSerializationWrapper.scala


Điều này thực sự tuần tự hóa cá thể hoặc tạo một thể hiện tĩnh và tuần tự hóa một tham chiếu (xem câu trả lời của tôi).
samthebest

2
@samthebest bạn có thể giải thích? Nếu bạn điều tra, KryoSerializationWrapperbạn sẽ thấy rằng điều đó khiến Spark nghĩ rằng đó thực sự là java.io.Serializable- nó chỉ đơn giản là tuần tự hóa đối tượng bên trong bằng Kryo - nhanh hơn, đơn giản hơn. Và tôi không nghĩ rằng nó xử lý một thể hiện tĩnh - nó chỉ khử tuần tự hóa giá trị khi giá trị.apply () được gọi.
Nilesh

8

Tôi đã đối mặt với vấn đề tương tự, và những gì tôi hiểu từ câu trả lời của 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

}

phương thức doIT của bạn đang cố gắng tuần tự hóa phương thức someFunc (_) , nhưng vì phương thức này không được tuần tự hóa, nó cố gắng tuần tự hóa kiểm tra lớp mà một lần nữa không được tuần tự hóa.

Vì vậy, làm cho mã của bạn hoạt động, bạn nên xác định someFunc bên trong phương thức doIT . Ví dụ:

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

Và nếu có nhiều hàm xuất hiện, thì tất cả các hàm đó sẽ có sẵn cho bối cảnh cha.


7

Tôi không hoàn toàn chắc chắn rằng điều này áp dụng cho Scala nhưng, trong Java, tôi đã giải quyết NotSerializableExceptionbằng cách tái cấu trúc mã của mình để việc đóng không truy cập vào trường không tuần tự hóa final.


Tôi đang đối mặt với cùng một vấn đề trong Java, tôi đang cố gắng sử dụng lớp FileWriter từ gói Java IO bên trong phương thức foreach RDD. Bạn có thể vui lòng cho tôi biết làm thế nào chúng ta có thể giải quyết điều này.
Shankar

1
Chà @Shankar, nếu FileWriterlà một finallĩnh vực của lớp bên ngoài, bạn không thể làm điều đó. Nhưng FileWritercó thể được xây dựng từ một Stringhoặc Filecả hai, cả hai đều được Serializable. Vì vậy, cấu trúc lại mã của bạn để xây dựng một cục bộ FileWriterdựa trên tên tệp từ lớp bên ngoài.
Trebor Rude

0

FYI trong Spark 2.4 rất nhiều bạn có thể sẽ gặp phải vấn đề này. Kryo serialization đã trở nên tốt hơn nhưng trong nhiều trường hợp bạn không thể sử dụng spark.kryo.unsafe = true hoặc serial serial kryo ngây thơ.

Để khắc phục nhanh, hãy thử thay đổi các mục sau trong cấu hình Spark của bạn

spark.kryo.unsafe="false"

HOẶC LÀ

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

Tôi thay đổi biến đổi tùy chỉnh RDD mà tôi gặp phải hoặc cá nhân viết bằng cách sử dụng các biến phát sóng rõ ràng và sử dụng các inbuilt twitter-lạnh api mới, chuyển đổi chúng từ rdd.map(row =>đến rdd.mapPartitions(partition => {chức năng.

Thí dụ

Cách cũ (không tuyệt vời)

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

Cách khác (tốt hơn)

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

Cách mới này sẽ chỉ gọi biến quảng bá một lần trên mỗi phân vùng sẽ tốt hơn. Bạn vẫn sẽ cần sử dụng Tuần tự hóa Java nếu bạn không đăng ký các lớp.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.