"Бессмертный" Spark Streaming Job?

Хорошо, поэтому я спросил несколько похожий question, связанный с тем, как Spark обрабатывает исключения внутри, но пример, который у меня был тогда, на самом деле не был ясно или полно. Ответ там указал мне в каком-то направлении, но я не могу объяснить некоторые вещи.

Я установил фиктивное искрообразование, и на этапе трансформации у меня есть выражение русско-рулетка, которое может или не должно вызывать исключение. Если возникает исключение, я останавливаю контекст потока искрообразования. То, что это, никакая другая логика, не преобразование RDD.

object ImmortalStreamingJob extends App {
  val conf = new SparkConf().setAppName("fun-spark").setMaster("local[*]")
  val ssc  = new StreamingContext(conf, Seconds(1))

  val elems = (1 to 1000).grouped(10)
    .map(seq => ssc.sparkContext.parallelize(seq))
    .toSeq
  val stream = ssc.queueStream(mutable.Queue[RDD[Int]](elems: _*))

  val transformed = stream.transform { rdd =>
    try {
      if (Random.nextInt(6) == 5) throw new RuntimeException("boom")
      else println("lucky bastard")
      rdd
    } catch {
      case e: Throwable =>
        println("stopping streaming context", e)
        ssc.stop(stopSparkContext = true, stopGracefully = false)
        throw e
    }
  }

  transformed.foreachRDD { rdd =>
    println(rdd.collect().mkString(","))
  }

  ssc.start()
  ssc.awaitTermination()
}

Запуск этого в IntelliJ в какой-то момент вызовет исключение. Веселая часть:

  • Если исключение выбрано в первом преобразовании (когда обрабатывается первый RDD), контекст искры останавливается и приложение умирает, что я хочу
  • если исключение генерируется после обработки хотя бы одного RDD, приложение зависает после печати сообщения об ошибке и никогда не останавливается, что не то, что я хочу

Почему приложение во время замены висит, а не умирает?


Я запускаю Spark 2.1.0 на Scala 2.11.8. Выход из try-catch решает проблему (Spark останавливается сам по себе). Кроме того, устранение проблемы с помощью try-catch внутри foreachRDD решает проблему.

Однако я ищу ответ, который поможет мне понять, что происходит в этом конкретном примере.

Ответ 1

Вы увидите только исключения в действии (например, foreachRDD в данном случае), а не преобразования (например, transform в этом случае), потому что действия выполняют преобразования лениво. Это означает, что ваши преобразования даже не будут выполняться до действия. Причина, по которой это необходимо, требует изменения вашей ментальной модели работы распределенной обработки.

Рассмотрим обычную однопоточную программу. Код поступает по очереди, и если исключение выбрано и не обрабатывается, последующие строки кода просто не выполняются. В распределенной системе, где одни и те же преобразования Spark работают параллельно на нескольких машинах (и в разных шагах), что должно произойти, когда генерируется исключение? Это не так просто, поскольку исключение на одной машине не зависит от кода, идущего на других машинах, как вы этого хотите. Чтобы все независимые задачи, распространяемые по всему кластеру, просто закрывались при исключении, это просто одномашинное мышление, которое не переводится в распределенную парадигму. Как водитель должен иметь дело с этим?

Согласно Matei Zaharia, теперь из Databricks и одного из создателей Spark в Беркли, "Исключения должны быть отправлены обратно в программу драйвера и занесены в систему там (с помощью SparkException, если задача не работает более 4 раз )". (Кстати, это количество попыток по умолчанию можно изменить с помощью spark.task.maxFailures.). Поэтому, если Log4J правильно настроен для исполнителей, там будет зарегистрировано исключение; то он будет сериализован и отправлен обратно в драйвер, который по умолчанию повторит еще 3 раза.

В вашей конкретной ситуации, я бы предположил, что у вас есть пара вещей. Во-первых, вы работаете на одной машине, что приведет к ошибочному представлению о том, как обработка исключений работает в распределенной модели. Во-вторых, вы преждевременно останавливаете контекст. Остановка контекста - чрезвычайно разрушительная операция, которая включает в себя остановку всех ваших слушателей и DAGScheduler. Честно говоря, я не знаю, как вы можете ожидать, что Spark полностью обернет все, когда вы в основном погасте свет.

Наконец, я бы сказал, что более элегантная модель обработки исключений может выполнять ваши преобразования внутри Try. В итоге вы получите потенциально более громоздкий код, в котором ваши преобразования вернут RDD[Try[T]] или DStream[Try[T]], что означает, что вам придется обрабатывать случаи Success и Failure для каждого элемента. Но вы сможете распространять информацию об успехах и ошибках вниз по течению со всеми преимуществами, предоставляемыми монадой, включая отображение RDD[Try[A]] => RDD[Try[B]] и даже использование for понятий (в силу flatMap).

Ответ 2

Во-первых, причина, по которой он работает, когда обработка исключений перемещается в foreachRDD, заключается в том, что код в теле foreachRDD появляется там, где возникает исключение. Трансформации оцениваются лениво, когда вызывается действие (собирают, например,). Таким образом, поймав ошибку, вызов ssc.stop корректно отменяет задание, ssc находится в области видимости, и все хорошо и детерминировано.

Теперь, когда вы вызываете ssc.stop в преобразовании, все становится немного более запутанным. Контекст фактически не находится в сфере видимости от тела преобразования, потому что преобразование выполняется рабочим, а не драйвером. На данный момент это работает для вас, потому что вы управляете всеми красивыми и локальными. Как только вы перейдете в распределенную среду, вы обнаружите, что ваша работа не будет запущена, потому что вы не можете распространять контекст для исполнителей.

Когда исключение выбрасывается из преобразования, трассировка стека отправляется в драйвер, который будет повторять задачу, но в вашем случае вы остановили контекст, поэтому кто знает, что произойдет? Я предполагаю, что если исключение произойдет на первом RDD, то как-то работа будет убита, потому что это так скоро, будь то случайность или по дизайну. И я бы предположил, что это не все убивают, если исключение не входит в первый преобразование RDD, потому что вы останавливаете контекст и каким-то образом создаете условие, что драйвер и/или задача /s ждут бесконечно.

Ответ 3

Я выкопал эту проблему в течение последних нескольких недель, поэтому я понял, что было бы неплохо сообщить вам, что я узнал в прошлые дни с помощью сообщества StackOverflow и сообщества Apache Spark:

  • Никогда не останавливайте StreamingContext внутри преобразования/действия искры, как я делаю в этом примере. Если вы хотите сделать это, просто сделайте это в отдельной ветке . В противном случае вы можете наткнуться на тупики или какое-то поведение undefined. Спасибо за @Shinzong Xhu для этого.
  • Если вы хотите, чтобы ваше потоковое задание завершилось неудачно (ваши задачи не выполняются), вы можете явно выбросить SparkException, в котором ваше собственное исключение может быть обернуто. Ваше потоковое приложение не умрет, но, по крайней мере, потоковые задачи потерпят неудачу. Спасибо @Видья за вдохновение в этом.

ОБНОВЛЕНИЕ [Не забудьте закрыть внешние ресурсы]

Обязательно проверьте, нужно ли останавливать любые другие ресурсы, не связанные с Spark. Например, если вы остановите выполнение потоковой передачи, но драйвер использует ActorSystem из akka для выполнения некоторых HTTP-вызовов, не забудьте завершить работу системы или приложение будет зависать. Возможно, это не похоже на Spark, но не забывайте об этом.

Еще раз спасибо за другие ответы, и надеюсь, что вы найдете эту информацию полезной.