Зачем нужен Spark: когда данные перестают влезать
Урок объясняет фундаментальную проблему, ради которой создан Spark: данные больше не помещаются в память и на одну машину.
Apache Spark — это унифицированный движок для распределённой обработки данных: он раскладывает один большой набор данных на множество машин кластера и считает части параллельно, держа промежуточные результаты в памяти.
Стена, в которую упирается одна машина
Представьте лог веб-сервера за год: 4 ТБ строк. Вы хотите посчитать, сколько запросов пришло с каждого IP. На ноутбуке с 16 ГБ оперативной памяти pandas.read_csv() упадёт на первой же сотне гигабайт — данные просто не влезут в RAM. Можно читать частями (chunking), но тогда логика усложняется, а время растёт линейно: один поток, одно ядро, один диск.
У одной машины есть три жёстких потолка, и масштабировать их вверх (scale up, «вертикальное масштабирование») быстро становится непомерно дорого:
- Память (RAM). Сервер с 1 ТБ RAM существует, но стоит как небольшая квартира, и 4 ТБ в него всё равно не влезут.
- Процессор. Даже 64 ядра обрабатывают данные последовательными порциями. Удвоить число ядер на одной плате сложно и дорого.
- Диск. Один NVMe читает условно 3–5 ГБ/с. Прочитать 4 ТБ — это минимум 15 минут только на чтение, не считая вычислений.
Идея больших данных проста: вместо одной огромной дорогой машины взять много обычных (scale out, «горизонтальное масштабирование») и поделить работу между ними. Десять машин читают по 400 ГБ каждая параллельно — и те же 4 ТБ прочитаны за полторы минуты. Сто машин — за девять секунд. Вот это «деление работы между машинами» и есть распределённые вычисления.
Что значит «обработать данные распределённо»
Ключевая абстракция распределённой обработки — разбить данные на куски (их в Spark называют партициями, partitions), разослать куски по машинам и применить одну и ту же функцию к каждому куску независимо. Классический пример — подсчёт слов. Идею легко показать на обычном Python, без всякого кластера: «партиции» — это просто несколько списков, а «параллельная обработка» имитируется циклом.
from collections import Counter
# Большой текст «поделили» на 3 куска — это наши партиции,
# которые в реальном кластере лежали бы на 3 разных машинах.
partitions = [
"spark делит данные на партиции",
"партиции считаются параллельно на машинах",
"spark собирает результат с машин",
]
# ЭТАП MAP: каждую партицию обрабатываем независимо (это и есть параллелизм).
local_counts = []
for part in partitions:
c = Counter(part.split())
local_counts.append(c)
print("частичный результат:", dict(c))
# ЭТАП REDUCE: сводим частичные результаты в один (это и есть shuffle + агрегация).
total = Counter()
for c in local_counts:
total += c
print("итог:", dict(total))
Вывод:
частичный результат: {'spark': 1, 'делит': 1, 'данные': 1, 'на': 1, 'партиции': 1}
частичный результат: {'партиции': 1, 'считаются': 1, 'параллельно': 1, 'на': 1, 'машинах': 1}
частичный результат: {'spark': 1, 'собирает': 1, 'результат': 1, 'с': 1, 'машин': 1}
итог: {'spark': 2, 'делит': 1, 'данные': 1, 'на': 2, 'партиции': 2, 'считаются': 1, 'параллельно': 1, 'машинах': 1, 'собирает': 1, 'результат': 1, 'с': 1, 'машин': 1}
Это весь смысл Spark в миниатюре: map (применить функцию к каждой партиции независимо) и reduce (свести частичные результаты). Реальный Spark делает то же самое, но партиции лежат на разных машинах, обработка идёт по-настоящему параллельно, а свод частичных результатов (shuffle) — самая дорогая операция, которой посвящён отдельный раздел курса.
Почему не Hadoop MapReduce: цена диска
Spark — не первый движок для распределённых вычислений. До него стандартом был Hadoop MapReduce (2006). Его фатальная для многих задач черта: между каждым шагом map и reduce промежуточный результат записывался на диск (в распределённую файловую систему HDFS). Для одношаговой задачи это терпимо, но реальная аналитика — это цепочка из десятков шагов: отфильтровать, сгруппировать, соединить, снова сгруппировать. Каждый шаг MapReduce — это полный цикл «прочитать с диска → посчитать → записать на диск». Итеративные алгоритмы (а машинное обучение почти всё итеративное) на MapReduce работали мучительно медленно: 90% времени уходило на ввод-вывод, а не на вычисления.
Прорыв Spark (2010, проект AMPLab в Беркли) — держать промежуточные данные в оперативной памяти между шагами. Цепочка из десяти трансформаций считается, не касаясь диска. На итеративных нагрузках Spark оказался в десятки раз быстрее MapReduce. Память дешевела, и идея «считать в RAM» стала практичной — отсюда и взлёт Spark.
Spark, pandas и MapReduce: когда что
| Инструмент | Где живёт | Когда выбирать |
| pandas | одна машина, в памяти | данные влезают в RAM (до ~единиц ГБ), быстрый интерактивный анализ, прототипы |
| Spark | кластер, в памяти + спилл на диск | данные больше памяти одной машины (десятки ГБ — петабайты), нужен параллелизм и отказоустойчивость |
| Hadoop MapReduce | кластер, через диск | почти не выбирают для нового кода; легаси-пайплайны, экстремально большие одношаговые задачи, где память дороже времени |
Важная честность: Spark — не «быстрый pandas». Для гигабайта данных pandas почти всегда быстрее: у Spark есть фиксированные накладные расходы (запуск JVM, планирование задач, сериализация, сетевой обмен). Spark начинает выигрывать там, где данные перестают влезать в одну машину или где нужна устойчивость к падению узлов. Запускать кластер ради файла на 200 МБ — это стрелять из пушки по воробьям.
Отказоустойчивость как отдельная причина
Есть второй, менее очевидный мотив брать Spark, помимо объёма: устойчивость к сбоям. Когда вычисление идёт минуты, шанс падения одной машины невелик. Но когда оно длится часы на сотне машин, отказ хотя бы одной за это время почти неизбежен — таков закон больших чисел. Если бы падение узла рушило всю многочасовую задачу, большие данные были бы непригодны для работы.
Spark спроектирован вокруг этой реальности. Он не хранит резервных копий данных (это было бы расточительно), а помнит как они были получены — последовательность операций. Потеряв кусок данных из-за падения узла, Spark пересчитывает только этот кусок, заново применив записанные шаги, и продолжает. Вся многочасовая работа не теряется. Этот механизм (lineage) мы детально разберём в разделе про RDD; пока важно понять: отказоустойчивость — не бонус, а необходимое условие самой возможности считать на больших кластерах долго.
Подводные камни
- «Возьму Spark, он быстрый». На малых данных Spark медленнее pandas из-за накладных расходов. Сначала оцените реальный объём.
- Распределённость не бесплатна. Как только данные ходят между машинами по сети (shuffle), появляется главный источник тормозов. Цель инженера — минимизировать обмен между узлами.
- Не вся работа параллелится. Если алгоритм требует видеть все данные сразу (например, глобальная сортировка или соединение «все со всеми»), полностью избежать дорогого обмена нельзя — можно лишь его уменьшить.
Best practices
- Прежде чем тянуться к Spark, честно оцените объём. Влезает в память — берите pandas/Polars/DuckDB.
- Думайте «партициями»: любая операция в Spark — это «что произойдёт независимо с каждым куском данных».
- Запомните главный критерий стоимости: дёшево — то, что считается внутри партиции; дорого — то, что заставляет данные перемещаться между партициями по сети.
Итог
- Одна машина упирается в память, процессор и диск; масштабировать её вверх дорого.
- Решение — scale out: много обычных машин, данные поделены на партиции, обработка идёт параллельно.
- Базовая модель — map (независимо по партициям) + reduce (свод результатов).
- Spark обогнал Hadoop MapReduce, потому что держит промежуточные данные в памяти, а не пишет их на диск.
- Spark нужен, когда данные не влезают в одну машину; для малых данных pandas обычно быстрее.