Learning Platform
Глоссарий Troubleshooting
Урок 02.01 · 22 мин
Продвинутый
PhilosophyLeaky abstractionsProduction engineeringMental modelSpark 4.0

Apache Spark — одна из самых успешных высокоуровневых абстракций в истории распределённых вычислений. DataFrame API позволяет писать SQL-подобный код, который работает на кластере из тысяч машин, не думая о том, как данные перемещаются по сети, как организована память, как процессор исполняет скомпилированный байткод. Абстракция действительно хорошая — до определённого момента.

Этот момент наступает в production.

Один запрос, два варианта

Представьте: ваш pipeline на Spark читает таблицу транзакций (500 GB, Parquet, секционирована по дате), джойнит с таблицей пользователей (10 GB), фильтрует нужный диапазон дат, агрегирует по user_id. Это стандартный ETL, который выполняется сотнями команд каждый день.

На staging-кластере с теми же данными (семпл 10%) запрос работает за 4 минуты. Вы проводите capacity planning: на полных данных будет 40 минут. Деплоите в production. Первый запуск на реальных данных занимает 2 часа 20 минут.

Что пошло не так? Если вы не знаете internals, вы видите: Job succeeded, result correct. Просто медленно. Значит нужно больше ресурсов. Добавляете executor’ы. Становится 1 час 40 минут. Добавляете ещё. 1 час 20 минут. Деньги заканчиваются быстрее, чем производительность растёт.

Если вы знаете internals, вы видите в Spark UI: Stage 2 имеет 200 задач, 195 завершились за 3 минуты, 5 задач выполняются уже 70 минут. Один executor держит 90% нагрузки. Это data skew — несколько значений user_id доминируют в данных (боты, тестовые аккаунты), и вся их обработка падает на одну партицию. Решение: salting с broadcast join или AQE с spark.sql.adaptive.skewJoin.enabled=true. 15 минут рефакторинга. Запрос работает за 18 минут.

Разница не в ресурсах. Разница в понимании движка.

Теория протекающих абстракций

Joel Spolsky сформулировал Закон протекающих абстракций (Law of Leaky Abstractions, 2002): все нетривиальные абстракции в той или иной мере протекают. Абстракция скрывает детали реализации, но не скрывает их полностью. Когда нижний слой ведёт себя неожиданно, абстракция “протекает” — верхний слой обязан знать о нижнем.

Spark — это многослойная абстракция. Каждый слой скрывает сложность от вышележащего. DataFrame API не требует знать о Catalyst. Catalyst не требует знать о RDD. RDD не требует знать о DAGScheduler. DAGScheduler не требует знать о shuffle-буферах на уровне OS.

Слои абстракции Spark и точки протечек
DataFrame / Dataset / SQL APIВерхний слой: декларативные трансформации. Вы пишете 'что', не 'как'. Протечка: почему один и тот же SQL работает 2 минуты в одном случае и 2 часа в другом?
Catalyst OptimizerЛогический и физический планировщик. Протечка: почему Catalyst выбрал SortMergeJoin вместо BroadcastHashJoin? Почему predicate не pushdown-ился через CASE WHEN?
Tungsten EngineVectorized execution, code generation. Протечка: почему WSCG (whole-stage codegen) деградирует на конкретных типах данных? Почему один UDF убивает codegen для всего stage?
RDD LayerУстойчивые распределённые датасеты, партиционирование, трансформации. Протечка: почему repartition(200) на 500 GB дороже, чем coalesce(200)?
DAGScheduler / TaskSchedulerПланирование stages и задач, data locality, retry. Протечка: почему stage перезапускается целиком при failure одной задачи?
Shuffle ServiceПеремещение данных между stages. Протечка: почему shuffle spill на диск в 100x медленнее in-memory shuffle?
Executor Memory / JVM GCУправление памятью, execution vs storage regions. Протечка: почему GC паузы коррелируют с размером broadcast переменных? Почему heap dump не объясняет OOM?

Каждый слой протекает в определённых ситуациях. В 95% случаев абстракция держит, и вы можете работать не думая о реализации. В 5% случаев — production-инцидент, дедлайн, дорогостоящий кластер — она протекает. И именно для этих 5% нужны internals.

Пять классов проблем, которые нельзя решить без знания движка

1. Data skew и неравномерная нагрузка

Data skew — когда данные распределены неравномерно по партициям. В большинстве реальных датасетов 20% значений ключей дают 80% строк: боты, системные пользователи, null-значения, популярные категории. При shuffle Spark распределяет данные по хэшу ключа, и несколько задач получают несравнимо больше данных, чем остальные.

Симптомы в Spark UI: почти все задачи stage завершены, 3-5 задач продолжают выполняться. Duration stage определяется самой медленной задачей.

Решение зависит от типа skew:

  • Join skew: salting ключа + explode + join, или AQE auto-skew handling (spark.sql.adaptive.skewJoin.enabled).
  • GroupBy skew: двухфазная агрегация (partial aggregate -> shuffle -> final aggregate), или рекомпозиция партиций.
  • Null skew: фильтрация null перед join, затем union.

Ни одно из этих решений не видно на уровне DataFrame API. Чтобы применить правильное, нужно понимать, что происходит на уровне shuffle partitioner и task execution.

2. Shuffle spill и нехватка памяти

Shuffle в Spark состоит из двух фаз: map side (write данных в shuffle files) и reduce side (read + sort + aggregate). Если данных в reduce-фазе больше, чем выделено памяти для shuffle (определяется spark.executor.memory и spark.memory.fraction), Spark начинает spill — сбрасывает данные на диск.

Spill сам по себе не фатален, но катастрофически медленен. SSD: 200-500 MB/s последовательная запись. Сеть: 10-25 Gbps = 1.25-3.1 GB/s. Spill на диск в 3-10x медленнее чтения из сети. Если у вас spill 50 GB — это десятки минут потеряно.

Спил не бросает исключений. Он просто делает задачу медленной. Без знания internals вы видите “задача медленная” и добавляете cores. С internals — видите в Spark UI метрику “Spill (Disk)” в деталях задачи и понимаете: нужно либо увеличить executor memory, либо уменьшить data per executor, либо настроить AQE coalescing.

3. Broadcast join OOM

Broadcast join — когда одна из сторон join маленькая, Spark копирует её целиком на каждый executor и делает локальный hash lookup. Это в разы быстрее SortMergeJoin, который требует shuffle обеих сторон.

Catalyst автоматически применяет BroadcastHashJoin, если размер таблицы ниже порога spark.sql.autoBroadcastJoinThreshold (по умолчанию 10 MB в Spark 4.0). Проблема: порог определяется по статистике из каталога, которая может быть устаревшей или отсутствующей. Если реальный размер таблицы 2 GB, а статистика говорит 5 MB (после ANALYZE TABLE не запускался несколько месяцев), Catalyst выберет broadcast.

Результат: 2 GB данных отправляются через SparkContext.broadcast(), материализуются в memory каждого executor-а. При 50 executor-ах — 100 GB RAM на broadcast. Driver может упасть с OOM ещё при сериализации. Executor-ы падают при десериализации.

Решение: обновить статистики (ANALYZE TABLE), явно отключить broadcast hint, или настроить порог. Но диагностировать можно только зная, что broadcast — это не “магия оптимизатора”, а конкретный план, который можно увидеть в explain() и который можно явно контролировать.

4. Codegen-регрессии

Tungsten Whole-Stage Code Generation (WSCG) — одна из ключевых оптимизаций Spark 2.0+. Вместо интерпретированного исполнения операторов, Catalyst генерирует Java-код, который JIT-компилирует в нативный машинный код. Это даёт 2-10x ускорение на типичных аналитических запросах.

WSCG ломается в нескольких случаях:

  • Python UDF: любой Python UDF в плане отключает codegen для всего stage, потому что Python-процесс работает вне JVM. Stage деградирует до интерпретированного режима.
  • Слишком сложный план: codegen имеет лимит на размер генерируемого метода (64 KB bytecode). Широкие SELECT с сотнями колонок или deep expression trees превышают лимит и fallback-ятся на интерпретированный режим.
  • Нестандартные типы: complex nested types (map of arrays of structs) плохо поддаются vectorization.

Symtom: запрос работает нормально, вы рефакторите один WHERE clause или добавляете UDF, производительность падает в 3 раза без очевидной причины. explain() не показывает ничего подозрительного. Только df.queryExecution.debug.codegen() покажет, что сгенерированный код вернулся к interpreted path.

5. Speculative execution и retry-шторм

Spark имеет механизм speculative execution: если задача выполняется медленно (медленнее median времени задач stage на 1.5x — настраивается через spark.speculation.multiplier), Spark запускает её копию на другом executor-е. Первая завершившаяся побеждает.

В нормальных условиях это полезно: медленный executor (сетевая проблема, GC pause) не блокирует весь stage. Но в патологических случаях speculation создаёт retry-шторм: задача медленная из-за data skew (данных реально много), запускается speculative copy, тоже медленная, запускается ещё одна — и так далее. При включённом spark.speculation и значимом skew вы можете получить в 3-5 раз больше задач, чем нужно, каждая читает одни и те же shuffle данные, кластер перегружен.

Эти пять классов проблем объединяет одно: симптомы видны на уровне API (медленно, OOM, неправильный результат), причины — на уровне internals.

Что меняется при переходе от middle к senior

Middle Spark-инженер знает API. Он может написать сложный multi-join pipeline, правильно настроить партиционирование для своего use case, использовать window functions, структурировать streaming job. Это ценные навыки.

Senior Spark-инженер знает, что API — это интерфейс к конкретному движку с конкретным поведением. Он работает на уровне двух слоёв:

Middle vs Senior: уровни понимания
Middle EngineerИспользует Spark API: DataFrame, Dataset, SQL. Знает паттерны оптимизации. Понимает что делать, но не всегда почему это работает именно так.
DataFrame APIПишет код на уровне трансформаций. Знает, когда cache, когда broadcast, когда repartition.
КонфигурацияЗнает ключевые параметры: spark.executor.memory, spark.sql.shuffle.partitions, autoBroadcastJoinThreshold.
Senior EngineerПонимает движок: как план трансформируется Catalyst, как задачи планируются и исполняются, почему конфиг ведёт себя именно так. Может предсказать поведение до запуска.
Execution planЧитает explain() как родной язык. Видит разницу между логическим и физическим планом. Понимает, почему оптимизатор выбрал именно эту стратегию.
Runtime metricsИнтерпретирует Spark UI: задачи, stages, память, spill, GC, shuffle bytes. Находит bottleneck по метрикам, а не по интуиции.
ДвижокЗнает, что происходит при shuffle: какие структуры данных, как буферизуется, когда spill. Знает, как codegen компилирует выражения.

Разница не в количестве знаний, а в уровне абстракции, на котором инженер работает. Senior не просто знает больше конфигов — он понимает, почему каждый конфиг работает именно так, может предсказать последствия изменения до запуска, и может решить проблему, которой ещё нет в Stack Overflow.

В 2026 году это различие ещё острее. Spark 4.0 принёс ANSI по умолчанию, улучшенный AQE, Spark Connect как основной путь к кластеру, тесную интеграцию с Apache Arrow для Python workloads. Новые возможности = новые точки протечек, новые слои абстракции, новые сценарии, где нужно знать движок.

Как устроен этот курс и как его проходить

Курс построен как путешествие от философии к железу и обратно. Каждый модуль — это один слой движка, разобранный до уровня, когда поведение становится предсказуемым.

Структура:

  • Модули 01-03 — фундамент: зачем internals, архитектурный mental model, RDD как базовая абстракция.
  • Модули 04-05 — данные в движении и в памяти: shuffle и memory management.
  • Модули 06-08 — оптимизационный стек: Catalyst, Tungsten, AQE.
  • Модули 09-10 — специальные рантаймы: Structured Streaming internals, Arrow/Spark Connect.
  • Модули 11-16 — расширение, дебаг, капстоун.

Каждый урок содержит:

  1. Концептуальный раздел — что происходит и почему именно так.
  2. Механику — конкретные классы, конфиги, метрики.
  3. Production-паттерн — как это знание применяется в реальных сценариях.
  4. Попробуй сам — практическое задание или эксперимент.
TIP

Наибольшая отдача от курса — если рядом есть реальный Spark-кластер или облачный Spark-as-a-service (Databricks, EMR, Dataproc). Многие разделы “Попробуй сам” рассчитаны на наблюдение за реальными метриками в Spark UI. Если кластера нет — Docker Compose с single-node Spark 4.0 достаточен для большинства экспериментов.

Предполагаемый фон: вы работаете с Spark в production (или активно работали), понимаете DataFrame API, умеете писать production pipelines. Этот курс не учит Spark с нуля — он учит понимать то, что вы уже используете.

Оптимизация shuffle: практика

Что означает Spark 4.0 для senior-инженера

Spark 4.0 — первый major release с 2016 года. Изменения, которые напрямую касаются internals:

ANSI по умолчанию. Раньше 1 / 0 возвращал null, деление строки на int не бросало исключение. Теперь — стандартное ANSI SQL поведение: исключение при делении на ноль, строгая типизация. Это ломает части legacy-кода, но убирает класс silent data corruption bugs.

Spark Connect как основной транспорт. Spark Connect — gRPC-based протокол отсоединения клиента от драйвера. В 4.0 это не experimental: Python-клиенты по умолчанию работают через Spark Connect. Это меняет где и как строится план запроса — часть Catalyst-работы теперь происходит на клиентской стороне.

AQE зрелость. Adaptive Query Execution был введён в Spark 3.0. В 4.0 он включён по умолчанию (spark.sql.adaptive.enabled=true) и значительно расширен. Понимание AQE internals — не optional, это core knowledge для Spark 4.0.

Structured Streaming улучшения. Новые source/sink APIs, улучшенный watermark handling, better integration с Kafka.

Этот курс написан для Spark 4.0. Там, где поведение изменилось с 3.x, это отмечено явно.

Попробуй сам

Это задание — диагностика вашего текущего уровня. Не тест на правильные ответы, а карта того, где белые пятна.

Возьмите любой production Spark job, который вы запускали последние несколько месяцев. Откройте его Spark UI (или History Server). Попробуйте ответить на следующие вопросы:

  1. Сколько stages в job-е? Совпадает ли это число с вашей интуицией? Можете ли вы сказать, какая операция в коде соответствует каждому stage boundary?

  2. Какой stage является bottleneck? Посмотрите на timeline: какой stage занимает больше всего времени? Это ожидаемо?

  3. Есть ли spill? В деталях задач stage найдите колонки “Spill (Memory)” и “Spill (Disk)”. Если есть значительный spill — знаете ли вы, почему?

  4. Какой тип join выбран? В physical plan (explain("extended")) найдите операцию join. Это BroadcastHashJoin, SortMergeJoin или ShuffledHashJoin? Почему именно этот?

  5. Есть ли stragglers? В деталях stage посмотрите на distribution времени задач. Если медиана 30 секунд, а max — 15 минут, у вас straggler. Знаете ли вы причину?

Если вы смогли ответить на все пять — хорошая база, курс углубит понимание. Если на 2-3 — именно для этого мы здесь. Если ни на одного — идеальный момент начать с нуля, с правильной mental model.

Проверка знанийKnowledge check
В Spark UI вы видите: Stage 3 содержит 400 задач. Median duration = 45 секунд, Max duration = 38 минут. Spill (Disk) на 3 задачах = 28 GB, 29 GB, 31 GB суммарно. Input data per task: медиана 120 MB, у 3 медленных задач — 4.2 GB, 4.5 GB, 3.9 GB. Что произошло и как это исправить без увеличения кластера?
ОтветAnswer
Это data skew с последующим shuffle spill. Три задачи получили в 35 раз больше данных, чем медиана, — значит, три значения ключа dominируют в данных (null, боты, популярные категории). Размер превысил доступную execution memory, и Spark spill-ил 90 GB на диск, что и создало 38-минутное время. Исправление зависит от операции: если это groupBy/agg — включить AQE skew join (spark.sql.adaptive.skewJoin.enabled=true) и уменьшить spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes до 256 MB. Если null-ключи не нужны в результате — добавить filter before join. Если это join — рассмотреть salting: добавить к ключу случайный суффикс (0 до N), развернуть маленькую таблицу в N копий с каждым суффиксом, сделать join по составному ключу. Увеличение кластера здесь не поможет: три задачи будут так же медленны на любом числе executor-ов, потому что каждая задача — однопоточная.

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

Результат: 0 из 0
Аналитический
Вопрос 1 из 4. В Spark UI Stage 4 содержит 500 задач. Медиана времени выполнения — 40 секунд, максимальное — 52 минуты. Spill (Disk) на 4 задачах — суммарно 110 GB. Какова наиболее вероятная причина, и какое действие эффективнее всего исправит ситуацию?

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

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

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

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