Learning Platform
Глоссарий Troubleshooting
Урок 04.03 · 23 мин
Средний
SPIconnectormetadatasplits

Анатомия Connector SPI

В прошлом уроке коннектор был «чёрным ящиком», реализующим SPI. Теперь вскроем ящик. Connector SPI это не один интерфейс, а набор сервисов, каждый со своей зоной ответственности. Понимание этих сервисов — не академическое упражнение: оно объясняет, в каком порядке Trino обращается к источнику при выполнении запроса, почему чтение распараллеливается и где именно происходит pushdown.

Этот урок разбирает четыре главных сервиса коннектора: Metadata, SplitManager, PageSource и PageSink.


Коннектор как набор сервисов

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

Четыре сервиса коннектора
MetadataОписывает структуру источника: схемы, таблицы, столбцы, типы, статистику. Используется при планировании.
SplitManagerРазбивает данные таблицы на splits — куски, которые можно читать параллельно.
PageSourceЧитает данные одного split и отдаёт их движку страницами (Page) в колоночном формате.
PageSinkПринимает страницы от движка и записывает их в источник. Только если коннектор поддерживает запись.

Эти четыре сервиса выстраиваются в конвейер по фазам жизни запроса. Сначала движок планирует запрос — здесь работает Metadata. Затем нужно понять, как распараллелить чтение — это SplitManager. На исполнении worker-ы читают данные — PageSource. Если запрос пишет (CREATE TABLE AS, INSERT) — данные уходят через PageSink. Разберём каждый.


Metadata: структура источника

ConnectorMetadata — сервис, который отвечает на все вопросы о структуре источника. Движок обращается к нему на фазах анализа и планирования запроса, ещё до того как прочитана хоть одна строка данных.

Что предоставляет Metadata:

  • Список схем в catalog (listSchemaNames).
  • Список таблиц в схеме и проверку существования таблицы (listTables, getTableHandle).
  • Столбцы таблицы, их имена и типы (getColumnHandles, getColumnMetadata).
  • Статистику таблицы для оптимизатора: число строк, NDV по столбцам, доля null, min/max (getTableStatistics).
  • При записи — операции DDL: создать таблицу, удалить, добавить столбец.

Именно здесь, в Metadata, реализуется pushdown. Движок не «угадывает», что коннектор умеет протолкнуть в источник — он спрашивает Metadata явно через специальные методы:

  • applyFilter — «можешь ли ты применить вот этот фильтр (WHERE) сам?» Если коннектор умеет, он возвращает новую версию table handle с уже встроенным фильтром.
  • applyProjection — «можешь ли ты читать только вот эти столбцы / достать вот это поле из ROW?»
  • applyAggregation — «можешь ли ты посчитать вот эту агрегацию (COUNT, SUM) на своей стороне?»

Если коннектор реализует эти методы и источник такое умеет — операция «проваливается» в источник. Если нет — метод возвращает «не могу», и движок выполнит операцию сам, своим оператором. Это и есть механика pushdown через SPI: переговоры между движком и коннектором о том, кто что считает.

NOTE

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


SplitManager: разбиение на splits

После планирования движок знает, какие таблицы читать. Но читать большую таблицу одним потоком — значит не использовать распределённость. Нужно разбить данные на части, которые можно читать параллельно на разных worker-ах. Этим занимается ConnectorSplitManager.

Split — это секция данных таблицы: кусок, над которым работает одна задача. Что физически становится split, решает коннектор, и это зависит от природы источника:

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

Ключевая деталь, к которой мы вернёмся в отдельном уроке, — lazy split generation. SplitManager не возвращает сразу полный список всех splits. Он отдаёт ConnectorSplitSource — источник splits, у которого движок забирает их батчами по мере необходимости. Для таблицы в сотни тысяч файлов это критично: материализовать все splits в памяти разом было бы дорого и не нужно, ведь часть из них может вообще не понадобиться (например, если сработал dynamic filtering и часть партиций отсеялась).

Splits как единицы параллельного чтения
ТаблицаЛогическая таблица в источнике — может быть тысячами файлов на object storage.
SplitManager делит
split 1Часть данных — например, один Parquet-файл. Читается одной задачей.
split 2Другая часть данных — другой файл. Читается параллельно другой задачей.
split 3Ещё часть. Параллелизм чтения = число splits, обрабатываемых одновременно.

PageSource: чтение split-а

Splits розданы задачам. Теперь задачу нужно физически прочитать. Этим занимается ConnectorPageSource — сервис чтения. Для каждого split коннектор создаёт PageSource, и движок вызывает его метод getNextPage() снова и снова, пока split не вычитан полностью.

Здесь важна форма возвращаемых данных. PageSource отдаёт не строки, а Page — единицу передачи данных в Trino. Page это набор Block-ов, и каждый Block хранит данные одного столбца. То есть формат колоночный: вместо «строка за строкой» движок получает «батч значений столбца за батчем».

Почему так. Trino — векторизованный движок: его операторы обрабатывают значения батчами, а не по одному. Колоночная раскладка батча означает, что значения одного столбца лежат в памяти подряд. Это даёт эффективное использование кэшей процессора (соседние значения попадают в одну cache line) и открывает дорогу SIMD-инструкциям. Подробно структуру Page и Block мы разберём в модуле про распределённое исполнение — пока зафиксируем: PageSource это граница, на которой данные источника превращаются в колоночный in-memory формат Trino.

Хорошо написанный PageSource не читает «всё подряд». Если на фазе Metadata сработал projection pushdown, PageSource читает только нужные столбцы — для колоночных форматов вроде Parquet или ORC это означает не трогать на диске байты ненужных столбцов вообще. Если есть predicate pushdown и формат хранит min/max по блокам, PageSource может пропускать целые row-group, не подходящие под фильтр.

-- Запрос, на котором видна работа сервисов коннектора
SELECT customer_id, total
FROM iceberg.warehouse.orders
WHERE order_date = DATE '2026-05-01';

Здесь Metadata через applyProjection сообщает, что нужны только два столбца, а через applyFilter принимает предикат по order_date. SplitManager отдаёт splits, причём предикат позволяет пропустить файлы других дат. PageSource читает только столбцы customer_id и total из оставшихся файлов. Один SQL-запрос — а задействованы три сервиса коннектора.


PageSink: запись

Последний сервис — ConnectorPageSink, зеркало PageSource. Он существует только у коннекторов, поддерживающих запись. Когда запрос пишет данные (CREATE TABLE AS SELECT, INSERT), движок передаёт PageSink готовые Page, а тот записывает их в источник: формирует Parquet/ORC-файлы и кладёт в object storage, выполняет INSERT через JDBC, отправляет сообщения в Kafka.

PageSink тоже работает распределённо: на запись данные тоже разбиты, и несколько задач могут писать параллельно через свои экземпляры PageSink. По завершении каждый PageSink возвращает фрагмент информации о том, что записал (например, список созданных файлов), а коннектор затем атомарно фиксирует результат — например, создаёт новый снапшот таблицы Iceberg.

TIP

Когда коннектор в документации помечен как read-only, это буквально означает: он не реализует ConnectorPageSink и DDL-методы Metadata. Такой коннектор отлично подходит для аналитики поверх источника, но через него нельзя сделать CREATE TABLE или INSERT — данные в источник придётся загружать другим путём.


Полная картина: сервисы по фазам запроса

Соберём четыре сервиса в один конвейер по фазам жизни запроса:

Сервисы коннектора по фазам запроса
Фаза 1: планированиеДвижок анализирует и планирует запрос, спрашивает структуру и согласовывает pushdown.
план готов
Фаза 2: разбиениеДвижок выясняет, как распараллелить чтение таблицы.
splits розданы
Фаза 3: чтениеWorker-ы читают свои splits, получая данные страницами.
если запрос пишет
Фаза 4: записьРезультат записывается в источник, если коннектор поддерживает запись.

Эта структура объясняет важное свойство Trino. Движок одинаково обращается ко всем источникам именно потому, что каждый коннектор раскладывается на одни и те же четыре сервиса. Hive-коннектор и PostgreSQL-коннектор устроены внутри совершенно по-разному, но снаружи у обоих есть Metadata, SplitManager, PageSource — и движок работает с ними по единому контракту. Разница (читать ли файлы или слать SQL по JDBC) спрятана внутри реализации сервисов.


Попробуй сам

Возьмите страницу любого коннектора в документации Trino (например, Iceberg или PostgreSQL) и проведите «обратный разбор» по четырём сервисам:

  1. Найдите раздел про типы данных и маппинг — это работа Metadata (getColumnMetadata).
  2. Найдите упоминание про параллелизм чтения или партиционирование — это SplitManager. Для JDBC-коннектора отметьте, что параллелизм по умолчанию ограничен.
  3. Найдите раздел про pushdown — predicate, projection, aggregation. Это методы applyFilter / applyProjection / applyAggregation в Metadata.
  4. Найдите, поддерживает ли коннектор запись (CREATE TABLE, INSERT). Наличие записи означает, что реализован PageSink. Отсутствие — коннектор read-only.

Сравните два коннектора по этим четырём пунктам. Вы увидите, что они сильно различаются по возможностям, хотя движок работает с обоими через один и тот же SPI.


Проверка знанийKnowledge check
За что отвечают четыре главных сервиса Connector SPI, и почему именно такая структура позволяет движку Trino работать со всеми источниками одинаково?
ОтветAnswer
Connector SPI раскладывается на четыре сервиса, выстроенных по фазам запроса. Metadata описывает структуру источника — схемы, таблицы, столбцы, типы, статистику; здесь же через методы applyFilter, applyProjection, applyAggregation согласуется pushdown. SplitManager разбивает данные таблицы на splits — куски для параллельного чтения, причём отдаёт их лениво, батчами, через SplitSource. PageSource читает данные одного split и отдаёт их движку в колоночном формате Page, состоящем из Block-ов по столбцам. PageSink принимает Page от движка и записывает их в источник — он есть только у коннекторов с поддержкой записи. Структура даёт единообразие: любой коннектор, как бы по-разному он ни был устроен внутри (чтение файлов или SQL по JDBC), снаружи раскладывается на эти же четыре сервиса. Движок обращается к каждому источнику через один контракт, а вся разница спрятана внутри реализации сервисов.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. На какой фазе выполнения запроса движок обращается к сервису Metadata коннектора?

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

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

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

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