Learning Platform
Глоссарий Troubleshooting
Урок 14.01 · 18 мин
Средний
Native ExecutionJVM LimitationsArrowVeloxDataFusionSIMD

Основы нативного исполнения

Почему нативное исполнение?

Apache Spark исторически работает на JVM (Java Virtual Machine). Это дало Spark огромную экосистему библиотек, межплатформенность и garbage collection «из коробки». Но для data engineering workloads JVM имеет фундаментальные ограничения, которые становятся критичными на масштабе.

GC pauses

Garbage Collector — главный враг предсказуемой производительности. При длительных batch jobs JVM накапливает объекты в heap, и периодически запускается stop-the-world GC pause. На heap размером 32-64 GB паузы могут достигать секунд — за это время все executor threads заморожены.

Для диагностики GC-проблем включите расширенное логирование:

# Spark executor GC diagnostics
spark.executor.extraJavaOptions=-verbose:gc -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps \
  -Xloggc:/tmp/spark-gc-%p.log
spark.executor.memory=16g
spark.executor.memoryOverhead=4g

# G1GC — рекомендуется для Spark executor heap > 8 GB
spark.executor.extraJavaOptions=-XX:+UseG1GC \
  -XX:InitiatingHeapOccupancyPercent=35 \
  -XX:G1HeapRegionSize=16m

Tungsten (М02) частично решил проблему через off-heap memory, но значительная часть Spark internals по-прежнему аллоцирует объекты на JVM heap: catalyst tree nodes, UDF вычисления, shuffle metadata.

Memory overhead

Каждый Java-объект имеет 16-байтный заголовок (mark word + class pointer). Integer в Java занимает 16 байт, хотя само значение — 4 байта. Для массива строк overhead ещё больше: каждая String — объект с указателем на char[], который сам — объект.

В row-oriented layout Tungsten (UnsafeRow) Spark хранит данные компактнее, но при переходе к пользовательскому коду (UDF, custom aggregators) данные конвертируются обратно в Java-объекты — и overhead возвращается.

Serialization cost

Между стадиями (stages) данные сериализуются для shuffle. Java serialization медленна, Kryo быстрее, но всё равно требует преобразования объектов в байты и обратно. На shuffle-heavy workloads сериализация может занимать 15-30% общего времени.

Отсутствие SIMD

Современные CPU поддерживают SIMD (Single Instruction, Multiple Data) — инструкции, обрабатывающие 4-8-16 значений за один такт (AVX-256, AVX-512). Нативный C++/Rust код использует SIMD напрямую. JVM JIT-компилятор теоретически умеет автовекторизацию, но на практике это работает непредсказуемо — только для простейших циклов и только с определёнными паттернами доступа к памяти.

Для колоночных операций (фильтрация, агрегация, проекция) SIMD даёт 2-8x ускорение по сравнению с скалярным кодом. Нативные движки используют это напрямую.

Проверка знанийKnowledge check
ОтветAnswer

Arrow Columnar как общий субстрат

Все нативные движки, ускоряющие Spark, используют Apache Arrow columnar format как общий язык между JVM и нативным кодом. Подробно мы разбирали Arrow в модуле М11 (Apache Arrow: колоночный формат данных). Здесь — ключевые моменты для контекста нативного исполнения:

  • Колоночный in-memory формат: данные одного столбца хранятся непрерывно в памяти, что идеально для SIMD-обработки и cache locality
  • Zero-copy transfer между JVM и нативным кодом через Arrow FFI (Foreign Function Interface) / JNI — данные не копируются, передаётся только указатель на память
  • Lingua franca: и Comet (Rust/DataFusion), и Gluten (C++/Velox) работают с Arrow-совместимыми колоночными batch-ами. Результаты возвращаются в JVM через Arrow, без десериализации

Пример конфигурации off-heap для Arrow-based нативного исполнения:

# Off-heap memory обязательна для всех нативных движков
spark.memory.offHeap.enabled=true
spark.memory.offHeap.size=8g

# Arrow-based columnar operations
spark.sql.execution.arrow.pyspark.enabled=true
spark.sql.execution.arrow.maxRecordsPerBatch=10000

Это означает, что нативные движки не заменяют Spark целиком — они заменяют execution engine внутри Spark, получая данные в Arrow-формате и возвращая результаты в том же формате.

Meta Velox

Velox — C++ библиотека для векторизованной обработки данных, созданная в Meta (Facebook). Это не standalone query engine, а переиспользуемая execution library, которую можно встроить в любую систему.

Архитектура

  • Arrow-compatible columnar memory: данные хранятся в колоночных vectors (Flat, Dictionary, Constant, RLE-encoded)
  • Lazy materialization: промежуточные результаты не материализуются полностью — вычисляются по требованию
  • Vectorized batch processing: операции применяются к целым batch-ам (1024-4096 строк), а не к отдельным строкам
  • Типы: scalar (INT, VARCHAR, TIMESTAMP), complex (STRUCT, MAP, ARRAY), nested

Операторы

Velox реализует полный набор реляционных операторов: scans, writes, projections, filtering, grouping (aggregations), ordering, shuffle/exchange, hash/merge/nested loop joins, unnest. Каждый оператор оптимизирован под SIMD и cache-friendly memory access.

Использование

Velox используется в 12+ системах внутри Meta: Presto, Spark (через Gluten), PyTorch feature engineering, внутренние ETL-инструменты. Вне Meta его используют ByteDance, Intel, Microsoft и другие. Наследие Presto execution engine делает Velox зрелым для SQL-workloads.

Проверка знанийKnowledge check
ОтветAnswer

Apache DataFusion

Apache DataFusion — query engine на Rust, работающий нативно поверх Apache Arrow. В отличие от Velox, DataFusion — полноценный query engine с собственным SQL-парсером, оптимизатором и execution engine. Подробнее о standalone DataFusion — в модуле М14 урок 06 (Альтернативные движки и расширения Spark).

Для контекста этого модуля ключевое:

  • Arrow-native: DataFusion работает напрямую с Arrow RecordBatch — нет преобразований между форматами
  • Rust: безопасность памяти без GC, предсказуемая производительность, SIMD через Rust SIMD intrinsics
  • Версия 45.0+: активно развивающийся Apache TLP (Top-Level Project) с растущей экосистемой
  • Relevance to Spark: Apache DataFusion Comet использует DataFusion как backend для нативного исполнения Spark-запросов. Это центральная тема урока L02

Spark Plugin API: как нативные движки интегрируются

Оба нативных движка (Comet и Gluten) интегрируются с Spark через один и тот же механизм — Spark Plugin API, введённый в Spark 3.0.

Интерфейс SparkPlugin

public interface SparkPlugin {
    DriverPlugin driverPlugin();
    ExecutorPlugin executorPlugin();
}

driverPlugin() работает на driver: регистрирует правила оптимизации, перехватывает физический план. executorPlugin() работает на executor: инициализирует нативные библиотеки, управляет памятью.

Механизм перехвата

Плагин подключается через конфигурацию spark.plugins:

# Comet
spark.plugins=org.apache.spark.CometPlugin

# Gluten
spark.plugins=org.apache.gluten.GlutenPlugin

Полный пример spark-submit с нативным движком:

# Comet — single-JAR deployment (Spark 3.4/3.5)
spark-submit \
  --jars comet-spark-spark3.5_2.12-0.12.0.jar \
  --conf spark.plugins=org.apache.spark.CometPlugin \
  --conf spark.comet.enabled=true \
  --conf spark.comet.exec.enabled=true \
  --conf spark.comet.scan.enabled=true \
  --conf spark.memory.offHeap.enabled=true \
  --conf spark.memory.offHeap.size=8g \
  your_etl_app.py

# Gluten + Velox — native columnar execution (Spark 3.5)
spark-submit \
  --jars gluten-velox-bundle-spark3.5_2.12-1.4.0.jar \
  --conf spark.plugins=io.glutenproject.GlutenPlugin \
  --conf spark.gluten.enabled=true \
  --conf spark.shuffle.manager=org.apache.spark.shuffle.columnar.ColumnarShuffleManager \
  --conf spark.memory.offHeap.enabled=true \
  --conf spark.memory.offHeap.size=12g \
  your_etl_app.py

После подключения плагин перехватывает физический план после Catalyst-оптимизации. Catalyst по-прежнему делает всю логическую и физическую оптимизацию (predicate pushdown, join reordering, column pruning). Плагин работает на последнем этапе — заменяет конкретные физические операторы нативными эквивалентами.

Fallback: ключевая концепция

Не все операторы поддерживаются нативно. Если оператор, выражение или тип данных не поддержан — он остаётся на JVM execution. Это называется fallback. Между нативным и JVM-кодом данные конвертируются через ColumnarToRow (C2R) и RowToColumnar (R2C) — и эта конвертация имеет overhead.

Поэтому нативные движки стараются заменить максимально длинные цепочки операторов, чтобы минимизировать количество C2R/R2C переходов. Если в середине цепочки один оператор не поддержан — вся цепочка может быть невыгодна для нативного исполнения.

Проверка знанийKnowledge check
ОтветAnswer

Визуализация: Native Execution Pipeline

Интерактивная диаграмма ниже показывает, как Comet и Gluten перехватывают физический план Spark и направляют поддерживаемые операторы в нативное исполнение. Переключайте режимы, чтобы увидеть разницу в архитектуре.

Native Execution Pipeline: Comet vs Gluten
SQL / DataFrame API
Catalyst Optimizer

Logical Plan → Physical Plan

Physical Plan
CometPlugin Intercepts

CometScanRule + CometExecRule (bottom-up)

Operator tree
Supported operators
CometNativeExec

ProtoBuf → JNI

DataFusion (Rust)

Vectorized execution

Arrow RecordBatch

Arrow FFI → JVM

Unsupported operators
JVM Fallback

Tungsten execution

Reason stored on node

Results
LanguageRust
EngineDataFusion
Plan formatProtoBuf
TPC-H speedup~2.4x
Spark / CommonComet (native)Fallback (JVM)

Обратите внимание на ключевые отличия:

  • Comet использует Protocol Buffer для сериализации плана и DataFusion (Rust) для исполнения
  • Gluten использует Substrait (стандартизированный формат) и Velox (C++) или ClickHouse как backend
  • Оба движка имеют JVM fallback для неподдерживаемых операторов, но с разным overhead

В следующих уроках мы детально разберём каждый из этих движков: архитектуру, конфигурацию, бенчмарки и production adoption.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 8. Какой фактор JVM является главным врагом предсказуемой производительности на длительных batch jobs с heap 32-64 GB?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6