Learning Platform
Глоссарий Troubleshooting
Урок 06.03 · 21 мин
Средний
distributed-executionsplitsconnectorlazy-generation

Splits: генерация и lazy split generation

В таксономии split — это кусок данных, над которым работает задача. Простое определение, но за ним стоит механика, которая прямо влияет на параллелизм запроса: что именно становится split, кто их создаёт, и почему движок не создаёт все splits сразу. Этот урок разбирает split как единицу работы и понятие lazy split generation.

Splits — это то, через что распределённость превращается из идеи в конкретное «вот эти данные читает этот воркер».


Split как единица параллельного чтения

Большая таблица не читается одним потоком — иначе распределённость бессмысленна. Таблицу надо разбить на части, и каждую часть отдать отдельной задаче. Split — это и есть такая часть: секция набора данных, единица работы для task.

Связь со стадиями прямая. У source-стадии (читающей таблицу) задачи получают splits и обрабатывают их. Число splits определяет потенциальный параллелизм чтения: 1000 splits можно разложить на 1000 параллельных порций работы; 1 split — это один поток, никакого параллелизма.

Split — порция работы для задачи
ТаблицаЛогическая таблица в источнике — может быть огромной, тысячи файлов.
разбивается на splits
SplitКусок данных. Достаётся одной задаче, читается одним driver.
SplitДругой кусок. Достаётся другой задаче — параллельно.
SplitЕщё кусок. Чем больше splits, тем выше потенциальный параллелизм.

Что физически становится split

Trino не решает сам, что такое split, — это решает коннектор. И решение зависит от природы источника, потому что у разных источников разная физическая структура данных.

ИсточникЧто коннектор делает split-ом
Hive / Iceberg / DeltaФайл целиком, либо часть файла (диапазон байт), либо отдельная row-group внутри файла
KafkaРаздел (partition) топика или диапазон offset-ов внутри раздела
PostgreSQL и другие JDBCОбычно один split на всю таблицу — JDBC-чтение по умолчанию не партиционируется
TPC-H / TPC-DSЛогический сегмент сгенерированного диапазона данных

Здесь видна важная закономерность. Для object storage коннектор может нарезать очень много splits — файлов и row-group-ов в таблице тысячи, и каждый становится отдельной порцией. Это даёт высокий параллелизм чтения. Для JDBC-источника коннектор по умолчанию делает один split на таблицу — реляционная база отдаёт результат одним потоком через одно соединение. Поэтому, как мы отмечали в модуле про коннекторы, fact-таблицы на больших объёмах держат на object storage: там много splits и читать их можно массово-параллельно.

Parquet: row groups как физическая основа splits Kafka: партиции топика как естественная граница splits

Размер split — это компромисс. Слишком крупные splits — мало порций, низкий параллелизм, неравномерная загрузка (одна задача доедает огромный split, остальные простаивают). Слишком мелкие splits — порций очень много, и накладные расходы на управление каждой порцией начинают съедать выгоду. Коннекторы стремятся к разумному размеру split — достаточно мелкому для параллелизма, достаточно крупному, чтобы накладные расходы были малы.

NOTE

Split — это не обязательно физический файл. Split это логическое описание порции данных: “файл X, байты с 0 по 128 МБ” или “топик T, раздел 3, offsets 1000-5000”. Коннектор создаёт такие описания, а задача потом по описанию читает реальные данные через PageSource. Один большой файл может породить несколько splits — по одному на row-group, — чтобы один файл читался параллельно несколькими задачами.


Откуда splits берутся: SplitManager и SplitSource

Splits создаёт сервис коннектора SplitManager — мы видели его в модуле про анатомию SPI. Когда движку нужно прочитать таблицу, он обращается к SplitManager этого коннектора.

Но SplitManager не возвращает сразу готовый список всех splits. Он возвращает SplitSource — источник splits. SplitSource — это объект, у которого движок запрашивает splits, забирая их батчами (порциями) по мере необходимости. Движок говорит SplitSource «дай следующую партию splits», получает её, раздаёт задачам, потом просит следующую.

Splits поступают от SplitSource батчами
SplitManagerСервис коннектора. На запрос движка отдаёт не список, а SplitSource.
отдаёт
SplitSourceИсточник splits. Движок забирает splits батчами, по мере надобности.
батч за батчем
ЗадачиЗадачи получают splits порциями и обрабатывают.

Почему именно батчами через SplitSource, а не одним готовым списком — это и есть суть следующего раздела.


Lazy split generation: зачем ленивость

Lazy split generation — ленивая генерация сплитов — означает, что splits создаются постепенно, по мере необходимости, а не все разом в начале запроса. Это сознательное проектное решение, и у него три веских причины.

Причина 1: память. Представьте таблицу-озеро из 500 000 файлов. Если коннектор создаст 500 000 объектов-splits разом и сложит в память — это заметный объём ещё до того, как прочитана хоть одна строка. Ленивая генерация держит в памяти только текущие батчи splits, а не весь миллион.

Причина 2: часть splits может не понадобиться. Запрос с LIMIT 100 может остановиться, прочитав несколько первых splits, — остальные просто не нужны. Ещё ярче — dynamic filtering: движок на лету понимает, что часть партиций таблицы заведомо не подходит под join-условие, и просит SplitSource эти splits вообще не генерировать. Если бы все splits создавались заранее, эта экономия была бы невозможна — нельзя «не создавать» то, что уже создано.

Причина 3: ранний старт. Движку не нужно ждать, пока коннектор перечислит все файлы таблицы, чтобы начать работу. Как только готов первый батч splits — задачи могут начинать читать, пока коннектор параллельно генерирует следующие батчи. Запрос стартует быстрее, перечисление splits и их обработка идут внахлёст.

Ленивая генерация против полного списка
Полный список заранееВсе splits созданы разом: много памяти, нельзя ничего отменить, старт после полного перечисления.
МинусыПамять на весь миллион splits, лишняя работа, поздний старт.
Trino выбирает
Lazy generationSplits генерируются батчами по мере надобности через SplitSource.
ПлюсыМало памяти, можно не создавать ненужное, ранний старт.
TIP

Связь с производительностью: ленивая генерация splits — это то, что делает dynamic filtering по-настоящему полезным. Динамический фильтр урезает множество splits ещё на этапе их генерации — задачи даже не получают splits для отсеянных партиций. Если бы splits материализовались заранее, фильтр мог бы только пропускать уже созданные splits, но не экономить на их создании и не сокращать работу планировщика. Подробнее dynamic filtering разбирается в модуле про cost-based optimizer.


Splits, задачи и параллелизм — цельная картина

Сведём split в общую механику исполнения. Distributed planning дал дерево стадий. У source-стадии движок запрашивает у коннектора splits — получает SplitSource. Из SplitSource движок батчами забирает splits и раздаёт их задачам этой стадии на разных воркерах. Задача получает свой набор splits и обрабатывает их силами своих драйверов.

Число splits задаёт верхнюю границу параллелизма чтения: больше splits — больше порций, которые можно раздать параллельно работающим задачам и драйверам. Но это именно потенциал: реализуется он, если воркеров и драйверов достаточно, чтобы splits разобрать. Если splits 1000, а драйверов в сумме 50, splits будут обрабатываться волнами по 50 за раз.

Поэтому split — это связующее звено таксономии: он соединяет «данные в источнике» с «задачами и драйверами, которые их читают». Коннектор решает, что такое split; SplitManager через SplitSource отдаёт их лениво; движок раздаёт их задачам; драйверы их переваривают.


Попробуй сам

Поведение splits видно в EXPLAIN ANALYZE и в Web UI:

  1. Выполните EXPLAIN ANALYZE SELECT count(*) FROM tpch.sf10.lineitem. В деталях source-стадии найдите число обработанных splits. Сопоставьте его с числом строк.
  2. Запустите тот же запрос на tpch.sf1 и tpch.sf100. Сравните число splits — оно должно расти с объёмом данных.
  3. Откройте Web UI, страницу запроса, и посмотрите, как splits источника распределились по задачам — равномерно ли.
  4. Подумайте над JDBC-источником: если бы вы делали count(*) для большой таблицы PostgreSQL, сколько splits ожидать по умолчанию и почему такой запрос не распараллелится по чтению.
  5. Сформулируйте письменно три причины, по которым генерация splits ленивая, и какую из них особенно усиливает dynamic filtering.

Проверка знанийKnowledge check
Что такое lazy split generation, через какой механизм splits поступают в движок, и какие три причины делают ленивую генерацию правильным решением?
ОтветAnswer
Lazy split generation — ленивая генерация сплитов — означает, что splits создаются постепенно, батчами по мере необходимости, а не все разом в начале запроса. Механизм такой: splits создаёт сервис коннектора SplitManager, но он не возвращает готовый список — он возвращает SplitSource, источник splits, у которого движок батчами запрашивает splits по мере надобности, раздаёт их задачам и просит следующую партию. Три причины, по которым ленивая генерация правильна. Первая — память: таблица-озеро может состоять из сотен тысяч файлов, и материализовать столько объектов-splits разом — заметный расход памяти ещё до чтения данных; ленивая генерация держит в памяти только текущие батчи. Вторая — часть splits может не понадобиться: запрос с LIMIT может остановиться на первых splits, а dynamic filtering на лету понимает, что часть партиций не подходит под join-условие, и просит вообще не генерировать эти splits — нельзя не создавать то, что уже создано заранее. Третья — ранний старт: движку не нужно ждать перечисления всех файлов таблицы, как только готов первый батч splits, задачи начинают читать, пока коннектор генерирует следующие батчи внахлёст. Особенно ленивую генерацию усиливает dynamic filtering: фильтр урезает множество splits прямо на этапе их генерации, и задачи даже не получают splits для отсеянных партиций.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Кто решает, что физически становится split для конкретной таблицы?

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

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

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

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