Завдання не серіалізується: java.io.NotSerializableException при виклику функції поза закриттям лише для класів, а не об'єктів


224

Отримання дивної поведінки під час виклику функції поза закриттям:

  • коли функція знаходиться в об'єкті, все працює
  • коли функція знаходиться в класі, отримуйте:

Завдання не серіалізується: 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? Немає об'єкта Spark методом ctx AFAICT
javadba

Відповіді:


334

RDD розширюють інтерфейс Serialisable , тому це не те, що спричиняє збій вашої задачі. Тепер це не означає, що ви можете серіалізувати RDDіскру і уникатиNotSerializableException

Spark - це двигун розподілених обчислень, а його основна абстракція - це стійкий розподілений набір даних ( RDD ), який можна розглядати як розподілену колекцію. В основному елементи RDD розподілені по вузлах кластера, але Spark відводить це від користувача, дозволяючи користувачеві взаємодіяти з RDD (колекцією) так, ніби це локальний.

Щоб не потрапити в занадто багато деталей, але при запуску різних перетворень на РДУ ( map, flatMap, filterта інші), код перетворення (закриття) є:

  1. серіалізовані на вузлі драйвера,
  2. відправляється до відповідних вузлів кластеру,
  3. десеріалізований,
  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), вони точно так же. Зазвичай, друге віддається перевазі, оскільки воно менш дослівне і більш чисте для читання.

EDIT (2015-03-15): SPARK-5307 представив SerializationDebugger, а Spark 1.3.0 - це перша версія, яка його використовує. Це додає шлях серіалізації до NotSerializableException . Коли зустрічається NotSerializableException, налагоджувач відвідує графік об'єкта, щоб знайти шлях до об'єкта, який неможливо серіалізувати, і створює інформацію, щоб допомогти користувачеві знайти об'єкт.

У випадку з ОП це друкується для 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
Позначити S

4
"rddList.map (someFunc (_)) для rddList.map (someFunc), вони абсолютно однакові." Ні, вони не є абсолютно однаковими, і фактично використання останнього може спричинити винятки серіалізації, коли перші не мали б.
samthebest

1
@samthebest, чи можете ви пояснити, чому карта (someFunc (_)) не спричинить винятки серіалізації, тоді як карта (someFunc) буде?
Алон

31

Відповідь Греги чудово пояснює, чому оригінальний код не працює, і два способи виправити проблему. Однак це рішення не дуже гнучко; розглянемо випадок, коли ваше закриття включає виклик методу на неклас, над Serializableяким ви не маєте контролю. Ви не можете ні додати Serializableтег до цього класу, ні змінити базову реалізацію, щоб змінити метод у функцію.

Нілеш представляє чудове рішення для цього, але рішення можна зробити як більш лаконічним, так і загальним:

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)

Ця методика також має перевагу в тому, що не потребує додаткових залежностей від Акули, щоб отримати доступ KryoSerializationWrapper, оскільки Twitter Chill вже втягнутий в основу Spark


Привіт, мені цікаво, чи потрібно мені щось зареєструвати, якщо я використовую ваш код? Я спробував отримати виняток класу Unable find від kryo. THX
G_cy

25

Повна розмова, що повністю пояснює проблему, яка пропонує чудовий спосіб зміни парадигми, щоб уникнути цих проблем серіалізації: https://github.com/samthebest/dump/blob/master/sams-scala-tutorial/serialization-exceptions-and-memory- витоки-но-ws.md

Відповідь, що голосує вгорі, в основному передбачає викидання цілої мовної функції - тобто вже не використання методів та використання лише функцій. Дійсно, методів функціонального програмування на заняттях слід уникати, але перетворення їх на функції не вирішує тут питання дизайну (див. Вище посилання).

Для швидкого виправлення в цій конкретній ситуації ви можете просто використовувати @transientанотацію, щоб сказати їй не намагатися серіалізувати правопорушнє значення (ось Spark.ctxспеціальний клас, а не Спарк, наступний за іменем ОП):

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

Ви також можете реструктурувати код, щоб rddList жив десь в іншому місці, але це теж неприємно.

Майбутнє - це, мабуть, спори

У майбутньому Scala включатиме в себе такі речі, які називаються "спорами", які повинні дозволяти нам тонко контролювати зерно, що робить, а що не точно втягується в закриття. Крім того, це повинно перетворити всі помилки випадкового втягування несеризованих типів (або будь-яких небажаних значень) в помилки компіляції, а не зараз, які є жахливими винятками виконання / витоками пам'яті.

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

Порада щодо серіалізації Кріо

Використовуючи kyro, зробіть так, що необхідна реєстрація, це означає, що ви отримаєте помилки замість витоку пам'яті:

"Нарешті, я знаю, що у kryo є kryo.setRegistrationOptions (правда), але мені дуже важко намагатися зрозуміти, як його використовувати. Коли цей параметр увімкнено, 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

2
@samthebest ви могли б детальніше розглянути? Якщо ви розслідуєте, KryoSerializationWrapperто виявите, що це змушує Іскру думати, що це дійсно java.io.Serializable- він просто серіалізує об'єкт внутрішньо за допомогою Kryo - швидше, простіше. І я не думаю, що він має справу зі статичним екземпляром - він просто де-серіалізує значення, коли викликається value.apply ().
Нілеш

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 (_) , але оскільки метод не є серіалізаційним, він намагається серіалізувати тестування класів, яке знову не є серіалізаційним.

Отож, щоб ваш код працював, ви повинні визначити деякий методFunc всередині 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 з пакета Java IO всередині методу RDD foreach. Чи можете ви, будь ласка, дайте мені знати, як ми можемо це вирішити.
Шанкар

1
Ну @Shankar, якщо FileWriterце finalполе зовнішнього класу, ви не можете це зробити. Але FileWriterможе бути побудовано з a Stringабо a File, які є обома Serializable. Тому рефактор вашого коду, щоб побудувати локальний FileWriterна основі імені файлу із зовнішнього класу.
Trebor Rude

0

FYI в Spark 2.4, мабуть, багато з вас, можливо, зіткнуться з цим питанням. Kryo серіалізація покращилася, але в багатьох випадках ви не можете використовувати spark.kryo.unsafe = true або наївний серіалізатор крио.

Для швидкого виправлення спробуйте змінити наступне в конфігурації Spark

spark.kryo.unsafe="false"

АБО

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

Я змінюю власні перетворення RDD, з якими стикаються або особисто пишу, використовуючи явні змінні трансляції та використовуючи нові вбудовані api twitter-chill, перетворюючи їх 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-серіалізацію.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.