Что такое Spark? Давайте заглянем под капот

В моем последнем посте мы представили проблему: обильные, нескончаемые потоки данных, и ее решение: Apache Spark. Во второй части мы сосредоточимся на внутренней архитектуре Spark и структурах данных.

В первые дни они использовали волов для тяжелой тяги, и когда один вол не мог сдвинуть с места бревно, они не пытались вырастить более крупного быка. Мы должны стремиться не к большим компьютерам, а к большему количеству компьютерных систем, - Грейс Хоппер

Поскольку объем данных растет быстрыми и угрожающими темпами, нам нужен был способ быстро обрабатывать потенциальные петабайты данных, и мы просто не могли заставить один компьютер обрабатывать такой объем данных с разумной скоростью. Эта проблема решается путем создания кластера машин, которые будут выполнять работу за вас, но как эти машины работают вместе для решения общей проблемы?

Знакомьтесь, Спарк

Spark - это кластерная вычислительная среда для крупномасштабной обработки данных. Spark предлагает набор библиотек на трех языках (Java, Scala, Python) для своего единого вычислительного движка. Что на самом деле означает это определение?

Унифицированный - со Spark нет необходимости собирать приложение из нескольких API или систем. Spark предоставляет вам достаточно встроенных API-интерфейсов для выполнения работы.

Computing Engine - Spark обрабатывает загрузку данных из различных файловых систем и выполняет на них вычисления, но не сохраняет данные на постоянной основе. Spark полностью работает в памяти, что обеспечивает беспрецедентную производительность и скорость.

Библиотеки. Spark состоит из ряда библиотек, созданных для задач анализа данных. Spark включает библиотеки для SQL (Spark SQL), машинного обучения (MLlib), потоковой обработки (потоковая передача Spark и структурированная потоковая передача) и аналитики графов (GraphX).

Приложение Spark

Каждое приложение Spark состоит из драйвера и набора распределенных рабочих процессов (Executors).

Драйвер искры

Драйвер запускает main() метод нашего приложения, и именно там создается SparkContext. Драйвер Spark выполняет следующие обязанности:

  • Выполняется на узле в нашем кластере или на клиенте и планирует выполнение задания с помощью диспетчера кластера.
  • Реагирует на пользовательскую программу или ввод.
  • Анализирует, составляет график и распределяет работу между исполнителями.
  • Хранит метаданные о запущенном приложении и удобно предоставляет их в веб-интерфейсе.

Искры исполнители

Исполнитель - это распределенный процесс, отвечающий за выполнение задач. Каждое приложение Spark имеет свой собственный набор исполнителей, которые остаются в живых в течение жизненного цикла одного приложения Spark.

  • Исполнители выполняют всю обработку данных задания Spark.
  • Сохраняет результаты в памяти, сохраняя их на диске только в том случае, если это специально указано программой драйвера.
  • Возвращает результаты драйверу после их завершения.
  • На каждом узле может быть от одного исполнителя на узел до одного исполнителя на ядро.

Рабочий процесс приложения Spark

Когда вы отправляете задание в Spark для обработки, многое происходит за кулисами.

  1. Наше автономное приложение запускается и инициализирует свой SparkContext. Только после наличия SparkContext приложение может называться драйвером.
  2. Наша программа-драйвер запрашивает у менеджера кластера ресурсы для запуска своих исполнителей.
  3. Менеджер кластера запускает исполнителей.
  4. Наш драйвер запускает наш фактический код Spark.
  5. Исполнители запускают задачи и отправляют свои результаты обратно драйверу.
  6. SparkContext останавливается, и все исполнители закрываются, возвращая ресурсы обратно в кластер.

MaxTemperature, новый взгляд

Давайте подробнее рассмотрим работу Spark, которую мы написали в Части I, чтобы найти максимальную температуру по странам. Эта абстракция скрывала много кода установки, включая инициализацию нашего SparkContext. Давайте восполним пробелы:

Помните, что Spark - это фреймворк, в данном случае реализованный на Java. Только в строке 16 Spark нужно будет выполнять какую-либо работу. Конечно, мы инициализировали наш SparkContext, однако загрузка данных в RDD - это первый фрагмент кода, который требует отправки работы нашим исполнителям.

К настоящему времени вы, возможно, видели, что термин «RDD» встречается несколько раз, пора дать ему определение.

Обзор архитектуры Spark

Spark имеет четко определенную многоуровневую архитектуру со слабосвязанными компонентами, основанную на двух основных абстракциях:

  • Устойчивые распределенные наборы данных (RDD)
  • Направленный ациклический граф (DAG)

Устойчивые распределенные наборы данных

RDD - это, по сути, строительные блоки Spark - все состоит из них. Даже высокоуровневые API Spark (DataFrames, Datasets) состоят из RDD под капотом. Что значит быть устойчивым распределенным набором данных?

  • Устойчивость - поскольку Spark работает на кластере машин, потеря данных из-за сбоя оборудования является очень серьезной проблемой, поэтому RDD отказоустойчивы и могут восстанавливать себя в случае сбоя.
  • Распределенный - один RDD хранится на серии различных узлов в кластере, не принадлежащих ни к одному источнику (и ни к одной точке отказа). Таким образом, наш кластер может работать с нашим RDD параллельно.
  • Набор данных - набор значений (вы, вероятно, уже должны это знать).

Все данные, с которыми мы работаем в Spark, будут храниться в той или иной форме RDD - поэтому крайне важно их полностью понимать.

Spark предлагает множество API более высокого уровня, построенных на основе RDD, разработанных для абстрагирования сложности, а именно DataFrame и Dataset. С сильным упором на циклы чтения-оценки-печати (REPL), spark-submit и Spark shell в Scala и Python нацелены на специалистов по данным, которым часто требуется повторный анализ набора данных. RDD по-прежнему необходимо понимать, поскольку это основная структура всех данных в Spark.

RDD в просторечии эквивалентен «Распределенной структуре данных». По сути, JavaRDD<String> - это просто List<String>, рассредоточенные по каждому узлу в нашем кластере, причем каждый узел получает несколько разных фрагментов нашего списка. Со Spark нам всегда нужно мыслить в распределенном контексте.

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

RDD Операции

RDD неизменяемы, что означает, что после создания они не могут быть изменены каким-либо образом; их можно только преобразовать. Идея преобразования RDD лежит в основе Spark, и задания Spark можно рассматривать как не что иное, как любую комбинацию этих шагов:

  • Загрузка данных в RDD.
  • Преобразование RDD.
  • Выполнение действия над RDD.

Фактически, каждое задание Spark, которое я написал, состоит исключительно из задач этого типа, с добавлением ванильной Java для вкуса.

Spark определяет набор API-интерфейсов для работы с RDD, которые можно разбить на две большие группы - преобразования и действия.

  • Преобразования создают новый RDD из существующего.
  • Действия возвращают значение или значения программе драйвера после выполнения вычисления в ее RDD.

Например, функция карты weatherData.map() - это преобразование, которое передает каждый элемент RDD через функцию.

«Уменьшить» - это действие RDD, которое объединяет все элементы RDD с помощью некоторой функции и возвращает окончательный результат программе драйвера.

Ленивая оценка

«Я выбираю ленивого человека для тяжелой работы. Потому что ленивый человек найдет простой способ сделать это. - Билл Гейтс"

Все преобразования в Spark ленивы. Это означает, что, когда мы говорим Spark создать RDD с помощью преобразований существующего RDD, он не будет генерировать этот набор данных до тех пор, пока над ним или одним из его дочерних элементов не будет выполнено определенное действие. Затем Spark выполнит преобразование и действие, которое его вызвало. Это позволяет Spark работать намного эффективнее.

Давайте еще раз рассмотрим объявления функций из нашего предыдущего примера Spark, чтобы определить, какие функции являются действиями, а какие - преобразованиями:

16: JavaRDD<String> weatherData = sc.textFile(inputPath);

Строка 16 не является ни действием, ни преобразованием - это функция sc, нашего JavaSparkContext.

17: JavaPairRDD<String, Integer> tempsByCountry = weatherData.mapToPair(new Func.....

Строка 17 представляет собой преобразование weatherData RDD. В нем мы сопоставляем каждую строку weatherData с парой, состоящей из (Город, Температура).

26: JavaPairRDD<String, Integer> maxTempByCountry = tempsByCountry.reduce(new Func....

Строка 26 также является преобразованием, потому что мы перебираем пары ключ-значение. Это преобразование tempsByCountry, при котором мы снижаем температуру в каждом городе до максимальной зарегистрированной температуры.

31: maxTempByCountry.saveAsHadoopFile(destPath, String.class, Integer.class, TextOutputFormat.class);

Наконец, в строке 31 мы запускаем действие Spark; сохранение нашего RDD в нашей файловой системе. Поскольку Spark подписывается на модель отложенного выполнения, только в этой строке Spark сгенерирует weatherData, tempsByCountry и maxTempsByCountry, прежде чем окончательно сохранить наш результат.

Направленный ациклический граф

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

Помните, что граф - это не что иное, как серия связанных вершин и ребер, и этот граф ничем не отличается. Каждая вершина в DAG - это функция Spark; некоторая операция, выполняемая над RDD (map, mapToPair, reduceByKey и т. д.).

В MapReduce DAG состоит из двух вершин: Карта → Уменьшить.

В приведенном выше примере MaxTemperatureByCountry группа DAG задействована немного больше:

parallelize → map → mapToPair → reduce → saveAsHadoopFile

DAG позволяет Spark оптимизировать план выполнения и минимизировать перетасовку. Мы обсудим DAG более подробно в следующих статьях, так как это выходит за рамки этого обзора Spark.

Циклы оценки

С помощью нашего нового словаря давайте еще раз рассмотрим проблему с MapReduce, как я определил в Части I, цитируемой ниже:

MapReduce отлично справляется с пакетной обработкой данных, но отстает в том, что касается повторного анализа и небольших циклов обратной связи. Единственный способ повторно использовать данные между вычислениями - это записать их во внешнюю систему хранения (а-ля HDFS) »

Повторно использовать данные между вычислениями? Похоже на RDD, над которым можно выполнять несколько действий! Предположим, у нас есть файл data.txt, и мы хотим выполнить два вычисления:

  • Общая длина всех строк в файле.
  • Длина самой длинной строки в файле.

В MapReduce для каждой задачи требовалось отдельное задание или необычная MulitpleOutputFormat реализация. Spark упрощает работу всего за четыре простых шага:

  1. Загрузите содержимое data.txt в RDD.
JavaRDD<String> lines = sc.textFile("data.txt");

2. Отобразите каждую строку строк по ее длине (для краткости используются лямбда-функции).

JavaRDD<Integer> lineLengths = lines.map(s -> s.length());

3. Чтобы найти общую длину: уменьшите lineLengths , чтобы найти общую сумму длины строки, в данном случае сумму каждого элемента в СДР.

int totalLength = lineLengths.reduce((a, b) -> a + b);

4. Чтобы найти максимальную длину: уменьшите lineLengths , чтобы найти максимальную длину строки.

int maxLength = lineLengths.reduce((a, b) -> Math.max(a,b));

Обратите внимание, что шаги третий и четвертый - это действия RDD, поэтому они возвращают результат нашей программе драйвера, в данном случае Java int. Также помните, что Spark ленив и отказывается выполнять какую-либо работу, пока не увидит действие. В этом случае реальная работа не начнется до третьего шага.

Следующие шаги

Пока что мы представили нашу проблему с данными и ее решение - Apache Spark. Мы рассмотрели архитектуру и рабочий процесс Spark, его флагманскую внутреннюю абстракцию (RDD) и модель выполнения.

Далее мы рассмотрим функции и синтаксис в Java, которые будут становиться все более техническими по мере углубления в структуру.