JVM Garbage Collection и не только: 7 причин OOM-ошибки в Apache Spark

Автор Категория ,
JVM Garbage Collection и не только: 7 причин OOM-ошибки в Apache Spark

Обучая разработчиков Big Data, сегодня рассмотрим, почему в распределенных приложениях Apache Spark случаются OOM-ошибки. Читайте далее, как работает сборка мусора JVM в Spark-приложениях, почему из-за нее случаются утечки памяти и что можно сделать на уровне драйвера и исполнителя для предупреждения OutOfMemoryError.

Сборка мусора JVM и OOM-ошибки в Spark-приложениях

На практике сбои Spark-приложения случаются довольно часто из-за необработанного исключения OutOfMemoryError (OOM), связанного с нехваткой памяти кучи (heap) для виртуальных машин Java, которые запускаются как исполнители или драйверы. По умолчанию размер памяти для исполнителя (executor) Spark по умолчанию равен 1 ГБ. Если этого объема недостаточно, JVM запускает полную сборку мусора (Garbage Collection), чтобы высвободить избыточную память. Но если после 5 последовательных циклов полной сборки мусора высвобождается менее 2% памяти, JVM выдает соответствующее исключение – Out Of Memory Error, которое выглядит следующим образом [1]:

ERROR akka.ErrorMonitor: Uncaught fatal error from thread [sparkDriverakka.

actor.default-dispatcher-29] shutting down ActorSystem [sparkDriver]

java.lang.OutOfMemoryError: Java heap space

Exception in thread “task-result-getter-0” java.lang.OutOfMemoryError: Java heap space

Поскольку OOM-исключение вызывается в результате сборки мусора, то чрезмерная активность Garbage Collector, который занимает слишком много времени и ресурсов, может привести к Out Of Memory Error. Это случается из-за превышения предела накладных расходов памяти и в случае распределенных систем, что актуально для Spark-приложений, может быть решено с помощью G1GC – сборщика мусора серверного типа для многопроцессорных машин с большой памятью. G1GC соответствует целевым показателям времени паузы при сборке мусора, обеспечивая высокую пропускную способность и выполняя операции с кучей одновременно с потоками приложения. Это предотвращает прерывания, пропорциональные размеру кучи или оперативных данных. Включить G1GC в Apache Spark можно, задав конфигурацию executor.extraJavaOptions: -XX: + UseG1GC [2]. Подробнее о видах памяти фреймворка и конфигурации его JVM-настроек мы писали здесь.

Официальная документация Apache Spark отмечает, что сборка мусора JVM становится проблемой, при большом оттоке RDD, хранимых приложением. В этом случае Java пытается избавиться от старых объектов, чтобы освободить место для новых. Фактически, стоимость сборки мусора пропорциональна количеству объектов Java, поэтому использование структур данных с меньшим количеством объектов, например, массив Ints вместо LinkedList, снижает эти расходы. Также можно сохранить объекты в сериализованной форме, чтобы на каждый раздел RDD приходился только один объект (массив байтов), и попробовать сериализованное кэширование. Если рабочая память пользовательских задач накладывается на RDD, кэшированных на узлах Spark-кластера, сборка мусора тоже может провоцировать OOM.

Напомним, пространство кучи Java разделено на две области:

  • для хранения короткоживущих объектов, разделенное на регионы Eden, Survivor1, Survivor2;
  • для объектов с более длительным сроком службы.

Когда пространство Eden заполнено, в нем запускается второстепенный сборщик мусора, а объекты из Eden и Survivor1 копируются в Survivor2. Регионы Survivor2 и Survivor1 меняются местами. Если объект достаточно старый или Survivor2 заполнен, он перемещается в пространство для объектов с длительном сроком службы. Когда и оно заполняется, вызывается полный сборщик мусора. Если это происходит несколько раз до завершения задачи, то для ее выполнения недостаточно памяти. В этом случае следует задать нужный размер пространства Eden с учетом специфики Spark-приложения.

Например, если задача считывает данные из HDFS, то ее объем памяти можно оценить по размеру блока данных, прочитанного из HDFS. Однако, размер распакованного блока обычно в 2-3 раза больше исходного. Так, если нужно рабочее пространство на 3-4 таких задачи, а размер блока HDFS составляет 128 Мбайт, то размер Eden можно оценить в 4*3*128=1563 Мбайт. Флаги настройки GC для исполнителей можно указать в конфигурации задания, установив spark.executor.defaultJavaOptions или spark.executor.extraJavaOptions [3].

Однако, сборка мусора – не единственная причина OOM-ошибки в Спарк-приложениях, которая может случиться на уровне драйвера и/или на исполнителе из-за множества других факторов, которые мы рассмотрим далее.

Почему в Apache Spark случаются исключения OutOfMemoryError и как с ними бороться

На уровне драйвера Apache Spark ошибка OOM может случиться по следующим причинам [4]:

  • нехватка памяти на драйвере, который является основным элементом управления Spark-приложением;
  • слишком большой размер таблицы, которая должна транслироваться, например, для выполнения SQL-запросов.

На уровне исполнителя ошибка OOM происходит из-за следующих факторов [4]:

  • слишком большое количество ядер ЦП для исполнителя, заданное в свойстве executor.cores без учета требуемой памяти. На практике чаще всего на каждого исполнителя выделяется 3-5 ядер CPU. Подробнее про конфигурирование Спарк-исполнителей с настройкой памяти и ядер ЦП мы рассказывали здесь и здесь.
  • много столбцов в SQL-запросе, что устраняется включением в запрос только нужных столбцов и бакетированием данных;
  • недостаток служебной памяти YARN, нужной для служебных данных JVM, внутренних строк или других требований к метаданным. Эта проблема исправляется настройкой конфигурации yarn.executor.memoryOverhead. Обычно накладные расходы памяти YARN вне кучи составляют 10% от общей памяти исполнителя. При этом рекомендуется увеличивать не только память исполнителя, но и накладную память YARN для потоков JVM, внутренних метаданных и пр. через spark-submit [1]:

conf “spark.executor.memory=12g”

conf “spark.yarn.executor.memoryOverhead=2048”

Также это можно сделать в конфигурационном файле spark-defaults.conf:

 “executor-memory=12g”

Наконец, OOM-ошибки могут возникать из-за перекоса данных и выполнения операций перетасовки (group by, join и т.д.). Shuffle-операции выполняются на уровне исполнителя, а если он занят или на нем выполняется полная сборка мусора, о которой мы сказали выше, то будет выдано исключение OutOfMemoryError. Поэтому рекомендуется реже использовать shuffle-функции, которые включают в перетасовки. А избавиться от перекоса данных можно с помощью с помощью типового или пользовательского перераспределителя, а также готовых методов coalesce() и repartition(), разницу между которыми мы разбирали здесь. О том, как и где найти главную причину OutOfMemoryError в приложениях Apache Spark, читайте в этом материале.

А освоить тонкости этого фреймворка для разработки распределенных приложений и аналитики больших данных вам помогут специализированные курсы в нашем лицензированном учебном центре обучения и повышения квалификации для разработчиков, менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data в Москве:

Источники

  1. https://support.datafabric.hpe.com/s/article/Spark-Troubleshooting-guide-Memory-Management-How-to-troubleshooting-out-of-memory-OOM-issues-on-Spark-Executor
  2. https://medium.com/disney-streaming/a-step-by-step-guide-for-debugging-memory-leaks-in-spark-applications-e0dd05118958
  3. https://spark.apache.org/docs/latest/tuning.html
  4. https://goraidebashree7.medium.com/spark-out-of-memory-error-da89b242d435