7. Архитектура Apache Spark
• RDD – низкоуровневый API без оптимизаций
• DataSet – RDD с организацией данных в поименованные колонки
• DataFrame – DataSet[Row]
• Tungsten Execution Backend
Всем добрый день, меня зовут Вова Штанько, я – математик-разработчик в компании Рамблер. Сегодня я и мой коллега Костя Колоколов постараемся рассказать вам про то, как поднять свой скилл программирования на Спарк. Для этого мы разбили наш рассказ на две части. Я кратко напомню вам про то, как устроен Спарк. Костя же уделит внимание всяческим «фишкам», ускоряющим работу и «подводным камням», которые стоит обходить.
Вот на этой картинке показана схема работы на Spark. Предположим, у нас есть код, который мы хотим исполнить. Для этого в первую очередь нам нужен драйвер. На драйвере (это процесс, где запускается main() существующий на какой—то машине в сети, пользовательской или не очень), исходя из конфигурации кластера создается SparkContext, который является точкой входа в систему. Через spark context код попадает на Cluster Manager, который может быть разный, например, Yarn, Mezos или standalone. На рабочих нодах запускаются отдельные процессы, которые исполняют код на выделенных им кусках данных. Потом результаты вычислений собираются обратно на драйвер, если есть такая нужда, или сохраняются в HDFS. Я думаю, здесь стоит упомянуть, что Спарк исполняется на JVM со всеми вытекающими последствиями, а именно настройкой JVM на самих нодах и драйвере, настройкой GC для оптимальной работы, а также возможностью использовать такие инструменты как jstack или visualvm для мониторинга, оптимизации и дебага.
Основной абстракцией, или интерфейсом при работе со Спарком является RDD, или resilient distributed dataset. Почему именно так называется? Dataset – это коллекция, которая может содержать в меру произвольные элементы, но одного типа. Distributed говорит о том, что элементы одной RDD могут находиться на разных машинах. Resilient – о том, что каждая RDD содержит информацию о том, как она может быть пересчитана заново из исходных данных, если что-то пойдет не так. Кроме того, у RDD есть еще ряд свойств, которые частично связаны с тем, что Spark использует элементы функциональной парадигмы программирования.
Первая из них - immutability, или неизменяемость РДД. Нельзя внести изменения в существующую РДД – можно только создать новую. Это облегчает пересчет РДД в случае, если что-то пойдет не так.
Второе свойство – lazy evaluation, или отложенные вычисления. Вычисления РДД будут производиться только тогда, когда они понадобятся, и только в том количестве, которое потребуется. Впереди еще будет несколько примеров, поэтому если кто-то не знаком с этим понятием, дальше станет понятнее.
Третье свойство – partitioned, или партицирование. Как уже говорилось ранее, РДД являются распределенными. Партицирование – это принцип их распределения. Он может быть банальным – случайным или по близости к данным, или более хитрым – например RDD имен можен быть партицирован по первой букве имени. Это может помочь оптимизировать скорость вычислений и об этом вам еще подробнее расскажет Костя.
Кроме RDD, современный Спарк (от версии 1.6 и выше) содержит еще два API, более высокоуровневых. Это DataSet и DataFrame. Они доступны из библиотеки SparkSql. Их использование позволяет добиться повышения производительности за счет встроенных оптимизаторов, плюс интерфейс DataFrame пытается максимально эмулировать интерфейс привычных DataFrame из Pandas или R. Как правило, для работы с машинным обучением и аналитикой используются в основном DataFrame. Кроме того, из PySpark в принципе не доступен DataSet. Когда работа идет со Spark Sql, к процессу старта задачи, который я показывал на одном из первых слайдов, добавляется еще вот этот пайплайн. Здесь происходит трансформация запроса в Sql в RDD, и его оптимизация.
Теперь давайте перейдем к специфике программирования на Спарк. В ней есть две большие разницы: первая разница – это разница между трансформациями и действиями, а вторая – разница между узкими и широкими трансформациями. Про первую. Как проявление парадигмы lazy valuation вычисления в Spark начинаются только тогда, когда непосредственно вызывается какой-либо action. До этого момента мы можем заявить произвольное количество трансформаций, они встанут в условную «очередь», но выполнены не будут.
Shuffle – это перемещение данных между executor’ами. В лучшем случае – это межпроцессная коммуникация, в редком случае когда мы работаем на одной машине. Чаще – это одновременно сетевая и дисковая операция. Она дорогая, поэтому их число необходимо минимизировать, а по возможности – избегать.
Вот примеры широких и узких трансформаций. Об оптимизации shuffle вам еще расскажет Костя. А я постараюсь показать вам нагляднее, как все это выглядит с точки зрения исполнения задачи.