Какво е Spark? Нека надникнем под капака

В последната ми публикация въведохме проблем: обилни, безкрайни потоци от данни и неговото решение: Apache Spark. Тук, във втора част, ще се съсредоточим върху вътрешната архитектура и структури от данни на Spark.

В дните на пионерите са използвали волове за тежко теглене и когато един вол не може да помръдне дънер, те не са се опитвали да отгледат по-голям вол. Не трябва да се опитваме за по-големи компютри, а за повече системи от компютри — Грейс Хопър

С мащаба на данните, нарастващ с бързи и зловещи темпове, имахме нужда от начин да обработваме бързо потенциални петабайти данни и просто не можехме да накараме нито един компютър да обработва това количество данни с разумно темпо. Този проблем се решава чрез създаване на клъстер от машини, които да изпълняват работата вместо вас, но как тези машини работят заедно, за да решат общия проблем?

Запознайте се със Спарк

Spark е клъстерна изчислителна рамка за широкомащабна обработка на данни. Spark предлага набор от библиотеки на три езика (Java, Scala, Python) за своя унифициран изчислителен двигател. Какво всъщност означава това определение?

Унифициран – със Spark няма нужда да сглобявате приложение от множество API или системи. Spark ви предоставя достатъчно вградени API, за да свършите работата.

Компютърна машина — Spark управлява зареждането на данни от различни файлови системи и извършва изчисления върху тях, но не съхранява никакви данни за постоянно. Spark работи изцяло в паметта, позволявайки несравнима производителност и скорост.

Библиотеки — Spark се състои от поредица от библиотеки, създадени за задачи в областта на науката за данни. Spark включва библиотеки за SQL (Spark SQL), Машинно обучение (MLlib), Поточна обработка (Spark Streaming и Structured Streaming) и Graph Analytics (GraphX).

Приложението Spark

Всяко приложение на Spark се състои от драйвер и набор от разпределени работни процеси (изпълнители).

Spark Driver

Драйверът изпълнява метода main() на нашето приложение и там се създава SparkContext. Водачът на Spark има следните задължения:

  • Работи на възел в нашия клъстер или на клиент и планира изпълнението на заданието с мениджър на клъстер.
  • Отговаря на програмата или въвеждането на потребителя.
  • Анализира, планира и разпределя работата между изпълнителите.
  • Съхранява метаданни за работещото приложение и го излага удобно в уеб интерфейс.

Spark Executors

Изпълнителят е разпределен процес, отговорен за изпълнението на задачите. Всяко приложение на Spark има свой собствен набор от изпълнители, които остават живи за жизнения цикъл на едно приложение на Spark.

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

Работен процес на приложението на Spark

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

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

Максимална температура, преразгледана

Нека да разгледаме по-задълбочено работата на Spark, която написахме в „Част I“, за да намерим максимална температура по държава. Тази абстракция скри много код за настройка, включително инициализацията на нашия SparkContext. Нека попълним празнините:

Не забравяйте, че Spark е рамка, в този случай реализирана в Java. Едва на ред 16 Spark изобщо трябва да върши някаква работа. Разбира се, инициализирахме нашия SparkContext, но зареждането на данни в RDD е първият код, който изисква работа да бъде изпратена на нашите изпълнители.

Досега може би сте виждали термина „RDD“ да се появява многократно, време е да го дефинираме.

Общ преглед на архитектурата на Spark

Spark има добре дефинирана слоеста архитектура със слабо свързани компоненти, базирани на две основни абстракции:

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

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

RDD са по същество градивните елементи на Spark - всичко се състои от тях. Дори API от по-високо ниво на Sparks (DataFrames, Datasets) са съставени от RDD под капака. Какво означава да бъдеш устойчив разпределен набор от данни?

  • Устойчив – тъй като Spark работи на клъстер от машини, загубата на данни от хардуерен срив е много реален проблем, така че RDD са устойчиви на грешки и могат да се възстановят в случай на срив
  • Разпределен — единичен RDD се съхранява на поредица от различни възли в клъстера, не принадлежащи към нито един източник (и нито една точка на отказ). По този начин нашият клъстер може да работи на нашия RDD паралелно.
  • Набор от данни — колекция от стойности (вероятно вече трябва да знаете това).

Всички данни, с които работим в Spark, ще се съхраняват в някаква форма на RDD — следователно е наложително да ги разберем напълно.

Spark предлага набор от API на „високо ниво“, изградени върху RDD, предназначени да абстрахират сложността, а именно DataFrame и Dataset. Със силен фокус върху циклите за четене-оценка-печат (REPLs), 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.

Например функцията map 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 превъзхожда пакетната обработка на данни, но изостава, когато става въпрос за повторен анализ и малки вериги за обратна връзка. Единственият начин за повторно използване на данните между изчисленията е да ги запишете във външна система за съхранение (a la HDFS)”

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

  • Обща дължина на всички редове във файла.
  • Дължината на най-дългия ред във файла.

В MapReduce всяка задача би изисквала отделна работа или фантастична MulitpleOutputFormat реализация. Spark прави това лесно само с четири прости стъпки:

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

2. Съпоставете всеки ред от редове с неговата дължина (функциите Lambda, използвани за краткост).

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

3. За да определите общата дължина: намалете lineLengthsза да намерите сумата от общата дължина на линията, в този случай сумата от всеки елемент в RDD.

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, като ставаме все по-технически, докато навлизаме по-дълбоко в рамката.