В этом курсе мы много говорили о DWH, ETL, batch и streaming. Везде неявно предполагалось, что данные обрабатываются где-то — внутри Snowflake, в Spark-кластере, через Kafka. Но почему вообще нужны эти распределённые системы? Почему нельзя просто взять один большой сервер и считать на нём?
Этот урок — о фундаментальном ограничении одной машины и о трёх стратегиях, которые применяют, когда оно достигнуто. Без понимания этого вся остальная DE-инфраструктура (Spark, Kafka, Flink) кажется набором случайных инструментов.
Что такое single node и где его предел
Single node — это один сервер, на котором всё происходит: и CPU, и RAM, и диск. Самый понятный шаблон обработки: запустил Python-скрипт, прочитал CSV, посчитал агрегаты, сохранил результат.
Эта модель работает, пока данные помещаются на один узел. Под «помещаются» имеется в виду одно из трёх:
- Данные помещаются в RAM (десятки гигабайт на одной машине).
- Данные помещаются на диск, и обработка идёт стримом без полной загрузки в память.
- Compute (CPU) одного узла справляется с задачей за разумное время.
Когда хотя бы одно из этих условий перестаёт выполняться, начинается распределённая обработка. Разберём все три ограничения по очереди.
Ограничение 1: данные больше памяти
В современных серверах RAM составляет десятки или сотни гигабайт. Самые мощные одиночные машины (AWS u-24tb1.metal или подобные) имеют до 24 терабайт RAM, но стоят сотни тысяч долларов в месяц.
Когда у тебя датасет 10 ТБ событий — он не помещается в RAM почти любого реалистичного сервера. Можно работать стримом с диском (читать порциями), но многие операции (например, joins, sort, hash aggregation) требуют держать в памяти промежуточный state. На диске они становятся в десятки раз медленнее.
С ростом данных мы упираемся сначала в RAM одной машины, потом в её диск, потом в её пропускную способность.
При 10 ТБ дешевле и надёжнее взять 10 машин по 1 ТБ RAM и распределить нагрузку. Тогда у каждой машины свой кусок данных, помещающийся в её RAM, и обработка идёт параллельно.
Ограничение 2: CPU bottleneck
Даже если данные помещаются в RAM, может не хватать вычислительной мощности. CPU современного сервера — это десятки ядер (типично 32-128). Если задача — это серия независимых операций, она может загрузить все ядра. Но если объём вычислений настолько большой, что один сервер обрабатывает датасет 12 часов — это операционно неприемлемо.
Пример: ежедневный job, который считает рекомендации по 100 миллионам пользователей. Каждый пользователь требует расчёта похожести с 1000 кандидатов — это 100 миллиардов операций. На одной машине с 64 ядрами это часы. На 100 машинах по 64 ядра это минуты.
CPU bottleneck часто решается горизонтальным масштабированием: разбить данные на куски и обрабатывать параллельно на множестве машин. Это и есть распределённая обработка.
Ограничение 3: пропускная способность диска и сети
Третье ограничение менее очевидное. Современный SSD читает 1-5 ГБ/сек. NVMe — до 7-10 ГБ/сек. Это много, но 10 ТБ при скорости 5 ГБ/сек читаются 2000 секунд = 33 минуты. Если нужно прочитать датасет несколько раз (join по нескольким измерениям, агрегация по разным разрезам) — время растёт.
С распределённой обработкой эту проблему решают параллельным чтением: 100 машин одновременно читают свои куски данных с локальных дисков или из распределённой файловой системы (HDFS, S3). Эффективная пропускная способность складывается — 100 машин × 5 ГБ/сек = 500 ГБ/сек.
Аналогично сеть: одна машина 10 Gbit/sec прокачивает примерно 1 ГБ/сек. Между сотнями узлов в кластере — на порядки больше.
Три стратегии: sharding, partitioning, parallel execution
Когда мы решили, что обработка должна быть распределённой, нужно физически разнести данные и compute по узлам. Используются три фундаментальные техники:
Sharding (шардинг)
Шардинг — это разделение датасета на куски (shards), каждый из которых живёт на отдельном узле. Шардинг применяется обычно к persistent storage — операционным базам, key-value хранилищам, поисковым индексам.
Пользователи 1-100 000 000 разделены на 10 шардов:
shard 1: user_id [1, 10_000_000) -> node A
shard 2: user_id [10_000_000, 20_000_000) -> node B
...
shard 10: user_id [90_000_000, 100_000_000) -> node J
Когда приходит запрос «дай пользователя с id 35 678 901», router считает 35_678_901 div 10_000_000 = 3, и направляет запрос на shard 4 (node D). Это хороший подход для point queries — поиска по конкретному ключу.
Partitioning (партиционирование)
Партиционирование — это разделение датасета на куски (partitions), часто по диапазону или хэшу ключа. В отличие от шардинга, партиции могут жить на одном узле или распределены по нескольким — это про логическое разделение данных, не обязательно по машинам.
Партиционирование особенно важно для больших таблиц в DWH и data lake:
fact_orders партиционируется по order_date:
partition_2025_05_01/ -> файлы Parquet за 1 мая
partition_2025_05_02/ -> файлы Parquet за 2 мая
...
Когда аналитик пишет SELECT * FROM fact_orders WHERE order_date = '2025-05-15', engine читает только одну партицию — не весь датасет. Это называется partition pruning и даёт огромный прирост скорости.
Шардинг — физическое разделение по узлам, обычно для OLTP и точечных запросов. Партиционирование — логическое разделение для аналитических запросов с фильтрами.
В Spark/Hive обе техники используются одновременно: партиции по дате на storage-уровне + распределение compute по executor-ам кластера (это и есть «sharding на лету»).
Parallel execution
Параллельное выполнение — это разделение compute между множеством воркеров. У тебя есть Spark-кластер из 100 машин (executors). Когда приходит запрос, scheduler разбивает его на tasks — независимые единицы работы, каждая обрабатывает свою партицию данных.
# Псевдо-Spark
df.filter(col("country") == "RU") \
.groupBy("category") \
.agg(sum("amount").alias("revenue"))
Под капотом этот SQL превращается в DAG задач. 100 executors параллельно фильтруют свои партиции, группируют локально, потом обмениваются данными (shuffle) и считают финальный агрегат.
Параллельное выполнение — это причина, почему Spark может прогнать 10 ТБ за минуты, а Python-скрипт — за дни. Каждая машина делает свою маленькую часть, и результаты складываются. Если задача хорошо параллелится, эффективная скорость растёт линейно с числом узлов (с некоторым потолком из-за coordination overhead).
Когда single node всё ещё достаточно
Распределённая обработка имеет свою цену: настройка кластера, координация задач, network shuffle, отладка распределённых багов. Поэтому распределённый стек оправдан не всегда.
Хорошие признаки, что single node всё ещё подходит:
- Данные помещаются в RAM сервера (десятки ГБ).
- Обработка укладывается в разумное время (минуты-часы).
- Задача не критична к свежести (час задержки нормально).
В этих случаях обычный Python/pandas/DuckDB на одном сервере или ноутбуке работает быстрее, дешевле и проще, чем Spark-кластер. Многие DE-задачи 2026 года вообще не требуют распределённости — DuckDB и Polars прекрасно справляются с гигабайтами на ноутбуке.
Распределённый стек оправдан, когда:
- Данные не помещаются в RAM/диск одной машины (терабайты, петабайты).
- Обработка одной машиной заняла бы дни/недели.
- Параллелизм требуется для SLA (job должен укладываться в 30 минут).
- Нужна fault tolerance — задача должна продолжать работать при падении узлов.
Cloud DWH vs Spark: разные подходы к распределённости
Стоит уточнить: распределённость бывает разной. Snowflake и BigQuery — это распределённые системы, но они скрывают это от пользователя. Ты пишешь SQL, движок сам разбивает на задачи и распределяет по узлам. Никакой ручной настройки кластера.
Spark и Flink — это открытые распределённые системы. Ты явно конфигурируешь кластер, выбираешь количество executors, размер партиций, стратегии join’ов. Это даёт больше контроля и больше боли.
Оба подхода правильны для разных сценариев. SQL-аналитика в DWH — Snowflake/BigQuery. Программируемые пайплайны с custom-логикой — Spark. Streaming — Flink/Spark Structured Streaming.
Spark: архитектура distributed compute — driver, executors, partitionsПопробуй сам
Возьми любой датасет — например, NYC taxi trips (около 200 ГБ за год). Подумай, на чём ты его будешь обрабатывать. Один экземпляр DuckDB на сервере 64 ГБ RAM справится за часы? Snowflake X-Small warehouse за минуты? Spark-кластер из 10 узлов за минуты? Какие плюсы и минусы у каждого варианта? Это упражнение и есть мышление DE — каждая задача требует выбора правильного инструмента, а не самого мощного.