Apache Spark

Spark — движок распределённой обработки данных: терабайты данных обрабатываются кластером машин в памяти. DataFrame API похож на Pandas, но масштабируется.

Уровень: средний 19 разделов ⏱ 4–5 часов

1. Что такое Spark

Spark делит задачу на партиции, распределяет их по исполнителям (executors) и обрабатывает параллельно. Компоненты:

2. Установка и запуск

pip install pyspark
from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName('demo').master('local[*]') \
    .getOrCreate()

local[*] запускает на одном узле со всеми ядрами — для обучения достаточно.

3. Создание DataFrame

df = spark.read.csv('sales.csv', header=True, inferSchema=True)
df2 = spark.read.json('events/*.json')
df3 = spark.read.parquet('data.parquet')

df.show(5)
df.printSchema()

В отличие от Pandas, DataFrame ленивый: вычисления запускаются при действий (show, count, write).

4. Колонки и трансформации

from pyspark.sql import functions as F

df = df.withColumn('revenue', F.col('price') * F.col('qty'))
df = df.filter(F.col('revenue') > 100)
df = df.select('product', 'revenue')
df = df.dropDuplicates(['user_id'])
df.show()

5. Агрегации

total = df.groupBy('product').agg(
    F.sum('revenue').alias('total'),
    F.count('*').alias('n')
).orderBy(F.col('total').desc())
total.show()

Работает как GROUP BY из SQL, только по кластеру.

6. Spark SQL

df.createOrReplaceTempView('sales')

results = spark.sql('''
    SELECT product, SUM(revenue) AS total
    FROM sales WHERE revenue > 100
    GROUP BY product
''')

Пишите SQL прямо в Spark, если привычнее — движок тот же.

7. RDD vs DataFrame

RDD — низкоуровневое API (Java/Scala корни). DataFrame построен поверх и оптимизирован Catalyst-планировщиком: всегда выбирай DataFrame, если не нужна специфика RDD.

8. Интеграции и коннекторы

9. Потоковая обработка

stream_df = spark.readStream \
    .format('kafka') \
    .option('kafka.bootstrap.servers', 'localhost:9092') \
    .option('subscribe', 'events') \
    .load()

query = stream_df \
    .selectExpr('CAST(value AS STRING)') \
    .writeStream.outputMode('append').format('console') \
    .start()

query.awaitTermination(timeout=30)

10. Кластер

Проблема «shuffle-перекос» (перекос партиций) решается увеличением spark.sql.shuffle.partitions и ключей с солью.

11. Лучшие практики

df.collect() возвращает ВСЕ данные в драйвер: на больших объёмах это OOM. Считай агрегаты в кластере.

12. RDD, DataFrame и Datasets

Spark предоставляет три API: низкоуровневые RDD, современные DataFrame и типизированные Datasets. На практике 95% работы идёт через DataFrame — оно оптимизируется планировщиком (Catalyst) автоматически.

APIТипизацияОптимизацияКогда использовать
RDDнетручнаянизкоуровневые преобразования
DataFrameсхема, но не compile-timeCatalystаналитика и ETL по умолчанию
Datasetcompile-time (JVM)Catalystтипизированный код на Scala
df = spark.read.parquet("events.parquet")
(# планировщик сам сдвинет фильтры вниз и объединит чтения
df.filter(col("region") == "RU")
  .groupBy("product")
  .agg(sum("revenue").alias("total"))
  .show())
Не «дёргай» RDD ради простого агрегата: DataFrame сделает то же самое быстрее и короче. RDD нужен лишь для кастомных разделений и нестандартных источников.

13. Оптимизация задач: партиции и broadcast

Классические причины медленных джобов — неравномерные партиции (shuffle-перекос), слишком большие партиции и большие таблицы, которые копируются в каждый shuffle.

# добавить соль против перекоса ключа
df.withColumn("salt", (rand() * 10).cast("int"))
  .groupBy("key", "salt").agg(...)

# маленькая справочная таблица — в broadcast
from pyspark.sql.functions import broadcast
joined = df.join(broadcast(lookup), "id", "left")
Broadcast-join копирует таблицу на каждый executor. Для таблиц в десятки ГБ он приведёт к OOM — такой трюк только для небольших справочников.

14. Обработка ошибок и контрольные точки

Долгие ETL-джобы нужно делать устойчивыми к сбоям: правильно завершать работу, логировать и уметь возобновляться с места остановки.

try:
    (df.write.mode("overwrite")
       .partitionBy("date")
       .parquet("warehouse/events"))
except Exception as e:
    log.error(f"Запись упала: {e}")
    spark.stop()
    raise
Правило «staging → publish» уберегает читателей от частично записанных данных. Spark хранит служебные метки, но полагаться на них не стоит.

15. Spark в продакшене: практический сценарий

Соберём типовой конвейер «сырые события → витрина для BI» на одном кластере.

  1. чтение сырья из Kafka или S3;
  2. очистка: фильтры, дедупликация, приведение типов;
  3. партиционирование по дате и запись в Parquet;
  4. агрегация витрины: продуктовые метрики по дням;
  5. передача в ClickHouse/PostgreSQL для дашбордов;
  6. расписание через cron или Airflow с повторными попытками.
spark-submit \
  --master yarn \
  --num-executors 8 \
  --executor-memory 4g \
  --conf spark.sql.shuffle.partitions=400 \
  etl_events.py
Проверь идемпотентность: повторный запуск джобы не должен дублировать данные. mode("overwrite") по партиции даты — минимально достаточная защита.

16. Catalyst-оптимизация: планы и explain

Spark строит план выполнения, который проходит через оптимизатор Catalyst: правило переписывает ваш DataFrame в оптимальный набор операций. Важно уметь читать план и находить «дорогие» шаги.

df = spark.read.parquet("events/")
query = (df.filter(col("type") == "click")
          .groupBy("day").count())

query.explain(True)      # физический план
query.explain("cost")    # с оценкой стоимости
Перед оптимизацией: сначала explain, потом интуиция. Часто shuffle-стадия устраняется простым перестановочным фильтром или партиционированием.

17. Структурированный стриминг: детали

Структурированный стриминг обрабатывает данные в микро-батчах или continuously: события как бесконечная таблица, к которой применяются ваши трансформации. Ключ — надёжность и водяные знаки.

from pyspark.sql.functions import window

events = (spark.readStream
    .format("kafka")
    .option("kafka.bootstrap.servers", "kafka:9092")
    .option("subscribe", "clicks")
    .load())

windowed = (events
    .groupBy(window("timestamp", "1 minute"),
             "user_id")
    .count())

query = (windowed.writeStream
    .outputMode("update")
    .format("console")
    .start())
Забытый batch interval и рост воды — классическая деградация: окна копятся, память растёт. Всегда настраивайте watermark и следите за задержками.

18. Форматы файлов: Parquet, Avro, ORC

Выбор формата определяет скорость, объём и совместимость. Parquet — колоночный стандарт аналитики, Avro — строчный для потоков, ORC — «сорт» для Hive-хранилищ.

ФорматПлюсыМинусы
Parquetколоночный, отличное сжатиемедленный для deny-writes
Avroстрочный, схема в файле, простой для Kafkaмедленный для column-scan
ORCколоночный + скомпрессия, любят в Hiveменьше совместимости вне Hadoop
Одинаковая схема важнее формата: Schema Registry для Avro и совместимость версий уберегут от «битых» файлов на стыке команд.

19. Spark UI и дебаг джобов

Spark UI (порт 4040) — главный инструмент диагностики: стадии, задачи, shuffle, кэшированные RDD, SQL-планы. Научиться читать его — половина успеха в производительных ETL.

  1. откройте вкладку SQL: увидите реальный план и счётчики, где Spark тратит время;
  2. Jobs: сколько «лишних» действий запускается из-за повторных действий;
  3. Stage: длительность, число задач, распределение (скью на задачу);
  4. Storage: что в кэше и не «раздулся» ли;
  5. Executors: память и GC каждого воркера.
Признаки проблем:
⌛ Long Job → неэффективный план или мало узлов
⚙️ Large Shuffle → перекос по ключу
📈 High GC → мало памяти на кэш или партиции
Многие проблемы «плавают»: смотрите медиану, а не только максимум, и сравнивайте повторные прогоны. Разовый spike может быть следствием GC, а не логики.

20. Память и настройка исполнителей

Частые проблемы Spark — GC-паузы и вытеснение кэша. Управление ядрами, памятью и сериализацией — ключ к стабильной работе джобов на больших данных.

# пример конфигурации
spark.executor.memory      8g
spark.executor.cores       4
spark.memory.offHeap.enabled  true
spark.sql.adaptive.enabled    true
Смотрите Spark UI: если время GC превышает 10–15% общего, уменьшите память исполнителя или включите off-heap.
Слишком маленький исполнитель — постоянный shuffle между узлами. Меняйте память и ядра вместе, проверяя оба показателя UI.

21. Delta Lake: транзакции поверх данных

Delta Lake добавляет к Spark-таблицам свойства базы данных: транзакции, версии, обновления. Он устраняет главную боль озёр данных — частично записанные файлы и несогласованные чтения.

# upsert через MERGE
MERGE INTO events t USING updates s ON t.id = s.id
WHEN MATCHED THEN UPDATE SET t.value = s.value
WHEN NOT MATCHED THEN INSERT *
Delta-таблицы эффективны для «озера с гарантиями»: начинайте с одной ключевой таблицы, затем раскатывайте на соседние.
Vacuum удаляет старые версии — настраивайте retention, чтобы история не разрасталась бесконечно.

22. MLlib: машинное обучение на Spark

MLlib — библиотека машинного обучения для распределённых объёмов: подготовка признаков, обучение моделей и оценка выполняются на кластере. Это стандарт для ML с миллионами строк.

Пишите pipeline в виде единого объекта: загрузка, preprocess, model, save — повторные эксперименты станут проще.
Не оценивайте качество на обучающих данных: выведите holdout-набор и проверяйте на нём, иначе «успех» будет иллюзорным.

23. Мониторинг джобов и тонкая настройка

Spark UI и журналы дают полную картину каждого джоба: стадии, шuffle, память, время. Умение читать эти данные быстрее находит узкое место, чем догадки.

  1. смотрите Jobs: время каждой стадии и её длительность;
  2. проверяйте Stage Details: shuffle read/write и баланс партиций;
  3. анализируйте Executors: использование памяти и параллельность;
  4. ищите перекос: пара партиций «тянет» весь джоб.
Постоянные перекосы лечатся ключами-«соларами» и репартиционированием: распределение данных важнее сырой мощности.
Пишите метки конфигурации в журнале запуска: сравните версии джоба, чтобы понять, какая правка дала прирост.

Что дальше

Данные для Spark часто приходят из Kafka; храни и анализируй результаты в ClickHouse, смотри в Pandas на малых объёмах.

← Вернуться к курсам