Анатомия Connector SPI
В прошлом уроке коннектор был «чёрным ящиком», реализующим SPI. Теперь вскроем ящик. Connector SPI это не один интерфейс, а набор сервисов, каждый со своей зоной ответственности. Понимание этих сервисов — не академическое упражнение: оно объясняет, в каком порядке Trino обращается к источнику при выполнении запроса, почему чтение распараллеливается и где именно происходит pushdown.
Этот урок разбирает четыре главных сервиса коннектора: Metadata, SplitManager, PageSource и PageSink.
Коннектор как набор сервисов
Когда движок получает экземпляр Connector от фабрики, он спрашивает у него несколько специализированных объектов. Каждый отвечает за одну фазу работы с источником:
Эти четыре сервиса выстраиваются в конвейер по фазам жизни запроса. Сначала движок планирует запрос — здесь работает 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: переговоры между движком и коннектором о том, кто что считает.
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 и часть партиций отсеялась).
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.
Когда коннектор в документации помечен как read-only, это буквально означает: он не реализует ConnectorPageSink и DDL-методы Metadata. Такой коннектор отлично подходит для аналитики поверх источника, но через него нельзя сделать CREATE TABLE или INSERT — данные в источник придётся загружать другим путём.
Полная картина: сервисы по фазам запроса
Соберём четыре сервиса в один конвейер по фазам жизни запроса:
Эта структура объясняет важное свойство Trino. Движок одинаково обращается ко всем источникам именно потому, что каждый коннектор раскладывается на одни и те же четыре сервиса. Hive-коннектор и PostgreSQL-коннектор устроены внутри совершенно по-разному, но снаружи у обоих есть Metadata, SplitManager, PageSource — и движок работает с ними по единому контракту. Разница (читать ли файлы или слать SQL по JDBC) спрятана внутри реализации сервисов.
Попробуй сам
Возьмите страницу любого коннектора в документации Trino (например, Iceberg или PostgreSQL) и проведите «обратный разбор» по четырём сервисам:
- Найдите раздел про типы данных и маппинг — это работа Metadata (
getColumnMetadata). - Найдите упоминание про параллелизм чтения или партиционирование — это SplitManager. Для JDBC-коннектора отметьте, что параллелизм по умолчанию ограничен.
- Найдите раздел про pushdown — predicate, projection, aggregation. Это методы
applyFilter/applyProjection/applyAggregationв Metadata. - Найдите, поддерживает ли коннектор запись (CREATE TABLE, INSERT). Наличие записи означает, что реализован PageSink. Отсутствие — коннектор read-only.
Сравните два коннектора по этим четырём пунктам. Вы увидите, что они сильно различаются по возможностям, хотя движок работает с обоими через один и тот же SPI.