1. Архитектура Spark Internals: Глубокое погружение в Catalyst Optimizer и движок Tungsten
Архитектура Spark Internals: Глубокое погружение в Catalyst Optimizer и движок Tungsten
Когда разработчик пишет df.select("name").filter("age > 25"), он видит лаконичный декларативный код. Однако за этой строкой скрывается сложнейшая иерархия трансформаций: Spark не просто выполняет ваш код, он переписывает его, оптимизирует структуру логических связей и, в конечном итоге, генерирует низкоуровневый байт-код на лету, минуя стандартные механизмы управления объектами Java. Если в Spark 1.x производительность упиралась в сетевые задержки и дисковый ввод-вывод, то современные версии Spark сталкиваются с «бутылочным горлышком» в виде эффективности CPU и работы с памятью. Именно для решения этих задач были созданы Catalyst и Tungsten.
Жизненный цикл запроса: от Unresolved Logical Plan до RDD
Понимание внутренних механизмов Spark начинается с осознания того, что любой код на DataFrame или Spark SQL проходит через многоступенчатый конвейер трансформаций. Этот процесс превращает высокоуровневое описание «что мы хотим получить» в низкоуровневую инструкцию «как это сделать эффективно».
Анализ и построение логического плана
Первый этап — создание Unresolved Logical Plan. На этом шаге Spark знает только структуру вашего запроса, но не знает, существуют ли таблицы и колонки, к которым вы обращаетесь.
Analyzer и системный Catalog, чтобы сопоставить имена колонок и таблиц с реальными метаданными. После этого план становится Resolved Logical Plan.Физическое планирование
Логический план описывает операции, но не говорит, какой алгоритм использовать. Например, операцию Join можно выполнить как SortMergeJoin, BroadcastHashJoin или ShuffleHashJoin.
Итогом становится Physical Plan, который представляет собой дерево операторов, готовых к генерации кода. Финальный этап — превращение этого плана в цепочку RDD (Resilient Distributed Datasets), которые и будут выполняться на экзекуторах.
Catalyst Optimizer: Магия функционального программирования
Catalyst — это расширяемый оптимизатор, построенный на языке Scala с использованием паттерн-матчинга и квазицитат. Его уникальность в том, что он позволяет разработчикам (и контрибьюторам Spark) легко добавлять новые правила оптимизации.
Деревья и правила
В основе Catalyst лежат две сущности: Trees (деревья) и Rules (правила). Дерево — это структура данных, где каждый узел представляет собой оператор (например, Filter) или выражение (Attribute). Оптимизация — это процесс трансформации одного дерева в другое.
Правила работают итеративно. Spark применяет набор правил к узлам дерева до тех пор, пока не будет достигнута «точка насыщения» (fixed point), когда план перестает меняться, или пока не будет превышено максимальное количество итераций.
Основные типы оптимизаций Catalyst
filter(col("price") * 0 > 100), Catalyst поймет, что условие всегда ложно, и вообще уберет этот узел.WHERE максимально близко к источнику.> «Catalyst — это не просто оптимизатор запросов, это фреймворк для манипуляции деревьями выражений, который позволяет Spark эволюционировать быстрее любого традиционного движка СУБД». > > Deep Dive into Spark SQL's Catalyst Optimizer
Движок Tungsten: Преодолевая ограничения JVM
Если Catalyst отвечает за логику, то Tungsten — за «железо». До появления Tungsten (Spark 1.4+) основной проблемой была неэффективность Java Virtual Machine (JVM) при обработке огромных объемов данных.
Проблемы стандартной JVM
java.lang.String "abcd" занимает в памяти гораздо больше 4 байт из-за заголовков объектов, указателей и использования UTF-16. В масштабах терабайт данных это ведет к колоссальному перерасходу памяти.Управление памятью Off-Heap
Tungsten уходит от использования стандартных объектов Java в пользу прямого управления памятью через sun.misc.Unsafe. Данные хранятся в виде компактных байтовых массивов (бинарный формат Tungsten).
Whole-Stage Code Generation
Это, пожалуй, самая впечатляющая часть Tungsten. Вместо того чтобы интерпретировать каждый узел физического плана для каждой строки данных (что создает огромные накладные расходы на вызовы функций и виртуальные таблицы), Spark генерирует Java-код «на лету».
Представьте запрос:
Вместо итерации: Plan -> Filter -> Project -> Aggregate, Tungsten генерирует один монолитный цикл на языке Java, который выглядит примерно так:
Этот код компилируется с помощью Janino (быстрого компилятора Java) в байт-код. Процессор видит простой цикл, который идеально ложится в его кэш и позволяет использовать SIMD-инструкции (Single Instruction, Multiple Data) для параллельной обработки данных на уровне регистров.
Эффективность кэша и алгоритмы Cache-Aware
Tungsten оптимизирует не только хранение, но и алгоритмы. В классических алгоритмах сортировки или агрегации Spark раньше хранил указатели на объекты. Tungsten использует Cache-aware computation. Например, при сортировке Spark теперь хранит в памяти непрерывный массив, состоящий из 8-байтового префикса ключа и указателя. При сравнении строк процессор сначала сравнивает префиксы. В большинстве случаев этого достаточно, чтобы определить порядок, не обращаясь к основной памяти за полной строкой. Это минимизирует Cache Misses и ускоряет сортировку в разы.
Взаимодействие Catalyst и Tungsten на практике
Рассмотрим сложный случай: соединение двух таблиц (Join) с последующей агрегацией.
SortMergeJoin, Tungsten обеспечит максимально быструю сортировку во внешней памяти (External Sort), используя свои механизмы управления страницами памяти.Пример: Борьба с виртуальными вызовами
В традиционном движке каждый оператор — это объект с методом next(). Вызов next() — это виртуальный вызов функции. Если у вас миллиард строк и 10 операторов, вы делаете 10 миллиардов виртуальных вызовов. В современных CPU это дорого из-за невозможности точного предсказания переходов (branch prediction). Whole-Stage Code Generation превращает эти 10 миллиардов вызовов в один плоский цикл.
Ограничения и нюансы
Несмотря на мощь этих движков, существуют ситуации, где магия не срабатывает:
pyspark.sql.functions всегда предпочтительнее.Глубинное понимание памяти: Страницы и указатели
Tungsten оперирует понятием Page. Память разбивается на логические страницы. Указатель в Tungsten — это 64-битное значение, где часть бит отвечает за номер страницы, а часть — за смещение (offset) внутри страницы. Это позволяет Spark работать с памятью объемом более 8 ГБ (лимит для 32-битных смещений) и легко перемещать данные между RAM и диском (spilling), просто сбрасывая страницы.
Где позволяет адресовать конкретный блок памяти, выделенный через Unsafe. Такая архитектура делает Spark похожим на операционную систему или высокопроизводительную БД, которая сама управляет своими ресурсами в обход абстракций виртуальной машины.
Влияние на проектирование систем
Для эксперта понимание Catalyst и Tungsten означает смену парадигмы:
Архитектура современных Internals делает Spark не просто инструментом обработки данных, а мощным компилятором распределенных запросов. Catalyst строит стратегию, а Tungsten обеспечивает тактическое превосходство на уровне машинных инструкций. В следующих главах мы разберем, как эта архитектура управляет памятью в условиях реальных нагрузок и как настраивать эти механизмы для достижения максимальной производительности.