Коннектор - специальный микросервис, выступающий в роли прокси между YDB и внешними источниками данных. Коннекторы формируют специальный слой абстракции, изолирующий YDB от специфики сторонних хранилищ. Благодаря этому YDB может через один и тот же интерфейс работать с разнообразными источниками данных.
Заметим, что внутри YDB активно применяется и более низкоуровневая абстракция аналогичного назначения - провайдеры. Это библиотеки, написанные на языке С++ и отвечающие за оптимизацию запросов и выполнение ввода-вывода во внешние источники данных. Большинство провайдеров поддерживают только какой-то один источник данных (например, провайдер S3
отвечает только за работу с объектным хранилищем); их разработка чрезвычайно трудозатратна.
В связи с этим было принято решение реализовать Generic
провайдер - универсальную библиотеку, через которую YDB сможет работать с любыми источниками данных посредством обращений к внешниму микросервису - коннектору.
Благодаря этому архитектурному решению добавление новых источников значительно облегчается, а кодовая база YDB не распухает от новых зависимостей и остаётся относительно стабильной. Коннектор может быть реализован на любом языке программирования по имеющейся GRPC-спецификации.
Коннектор fq-connector-go
- типичный микросервис, который может одновременно отвечать на запросы сразу по нескольким слушающим сокетам:
- Основной GRPC-сервер - порт
2130
(Protobuf API хранится в репозитории YDB). - HTTP-сервер, отдающий статистику - порт
8766
. - HTTP-сервер профилировщика Go Runtime - порт
6060
.
В production среде в качестве клиента к коннектору выступает сам YDB (исполняемый файл ydbd
).
В процессе разработки и отладки для обращения к коннектору можно пользоваться:
- Встроенной командой
fq-connector-go client
; - Встроенной командой
fq-connector-go bench
; - Инструментом dqrun, основанным на кодовой базе YDB;
- Инструментом kqprun, основанным на кодовой базе YDB.
- Непосредственно через Web UI YDB.
Любой пользовательский запрос в YDB (да и во всех современных базах данных) выполняется в два этапа:
- Фаза оптимизации запроса. В оперативной памяти YDB запрос представляется виде графа, узлами которого являются "лямбды" - функции, описывающие процесс извлечения, обработки и преобразования данных. Специальные оптимизаторы многократно обходят этот граф и трансформируют его с целью ускорения фазы выполнения. В конечном итоге из графа конструируется внутренняя "программа", которая исполняется движком на следующем этапе.
- Фаза выполнения запроса или "runtime". На данном этапе движок потоково извлекает данные из внешних источников и выполняет над этими данными операции в соответствии с запросом пользователя.
В фазе оптимизации запроса YDB обращается за метаданными таблицы через метод DescribeTable
. В фазе выполнения запроса YDB сначала просит коннектор разбить таблицу на сплиты (split - в большинстве случаев это синоним горизонтальной партиции таблицы) с помощью метода ListSplits
, а затем извлекает данные сплитов через метод ReadSplits
.
Внешние источники данных в сервисе fq-connector-go
скрываются за интерфейсом DataSource. У него всего-навсего два метода: метод для описания метаданных таблицы и для извлечения данных. Несмотря на лаконичность интерфейса, имплементировать его придётся постепенно, по частям, добавляя новую функциональность.
Логика работы с реляционными СУБД может быть в значительной степени обобщена и переиспользована в коде, относящемся к разным базам данных, поэтому имплементация DataSource
для РСУБД у нас на данный момент одна - с помощью структуры Preset в ней меняются только источнико-специфичные части:
- ConnectionManager отвечает за создание сетевых соединений, которые описываются абстракцией Connection. Этот интерфейс напоминает усечённую версию
*sql.DB
из стандартной библиотеки. - SQLFormatter формирует запросы к источнику на принятом у него диалекте SQL.
- TypeMapper отвечает за преобразование метаданных о таблице из системы типов источника данных в систему типов языка
YQL
, использующегося вYDB
, то есть отвечает за одно из преобразований типов, подробно описанных ниже. - SchemaProvider извлекает метаинформацию о таблице (количество, имена и типы столбцов), чтобы в дальнейшем отправить её в
YDB
в понятном ей формате.
Эти интерфейсы - наиболее верхнеуровневые, но есть ещё и несколько вспомогательных. В написании имлпементаций этих интерфейсов и заключается наша основная задача.
Коннектор должен превращать (трансформировать) данные из внешних систем в формат, поддерживаемый YDB, а также описывать эти данные в понятной YDB системе типов.
С точки зрения работы с метаданными такой системой типов является система типов языка YQL. Описания типов хранятся в Public Protobuf API YDB. По запросу от YDB (метод DescribeTable
) коннектор должен извлечь описание таблицы из источника (это описание, разумеется, хранится в системе типов, специфичной для источника) и предоставить схему таблицы в системе типов YQL.
В качестве формата передачи данных используется колоночный формат Apache Arrow (тип IPC Streaming). Колоночное представление данных часто встречается в аналитических СУБД, поскольку позволяет сэкономить дорогостоящие операции ввода-вывода. В Arrow используется собственная система типов. При этом коннекторы вычитывают данные из соединения с внешним источником данных в объекты-приёмники (Acceptor), которые описываются в системе типов Go: rows.Scan(acceptors...)
. Уже позднее эти объекты накапливаются в колоночных буферах (ColumnarBuffer), те, в свою очередь, сериализуются и отправляются по сети в сторону YDB в формате Arrow.
Таким образом в коннекторе встречаются сразу 4 системы типов:
- Система типов YDB (YQL).
- Система типов источника данных.
- Система типов Apache Arrow.
- Система типов языка Go.
Код, выполняющий трансформацию между этими системами типов, традиционно сконцентрирован в файлах type_mapper.go
(PG, CH).
Ещё один смысл, вкладываемый в термин трансформации данных - это преобразование данных из строкового в колоночное представление. Логика перекладывания данных из элементов строки (row) в колоночные буфера реализована однократно для всех источников данных в функции RowTransformerDefault.AppendToArrowBuilders.
Если использование нового внешнего источника данных поддержано в аналогичной федеративной базе данных Trino, вы можете изучить его работу, запустив Trino и источник локально.
Работу по добавлению нового источника можно начать с создания в папке rdbms подпапки для нового источника данных. Нейминг должен соответствовать enum из YDB API. В этой папке можно реализовать перечисленные выше интерфейсы в самом примитивном виде (на заглушках), и заполнить ими структуру Preset
.
Сразу после этого новый источник данных надо подключить в фабрике источников. После этого вы сможете делать обращения к коннектору через тестовый клиент fq-connector-go client
.
Скомпилируйте и запустите коннектор командой:
make build
make run
Затем подготовьте файл с конфигурацией клиента по примеру и попробуйте сходить в коннектор:
./fq-connector-go client connector --config ./your/config.txt --table some_table_name
Если в коде сервиса не будет ошибок, вы получите какие-то ответы (в соответствии с данными, "зашитыми" в заглушках). После этого можно приступать к наполнению DataSource
источнико-специфичным кодом.
Начать стоит с реализации интерфейса СonnectionManager
. Здесь вам нужно просто научиться по параметрам, пришедшим в структуре типа TGenericDataSourceInstance
, конструировать сетевое соединение к базе. Наиболее хрестоматийные примеры можно посмотреть в папках clickhouse и postgresql.
Important
Для работы с внешними источниками данных вам потребуется драйвер - библиотека на языке Go, которая реализует протокол взаимодействия с базой. Существуют важные нюансы при выборе библиотек:
- Лицензионная чистота (используем только MIT, Apache, BSD и подобные permissive лицензии; из лицензий с ограничениями разрешена MPL-2.0).
- При прочих равных стараемся выбирать библиотеку, которая не встраивается в
database/sql
, а предоставляет свою реализацию всех необходимых нам абстракций (стандартная библиотека Go в этом месте тормозит, так как используетreflect
). - Существует закрытый для внешних лиц перечень разрешённых версий сторонних библиотек. Когда выберете библиотеку, уточните у ментора, какую версию данной библиотеки можно использовать.
Некоторые источники данных предоставляют несколько сетевых интерфейсов для доступа данных: например, к ClickHouse можно подключиться как по TCP-протоколу, так и по HTTP-протоколу. Изучите ваш источник данных в этом отношении. В большинстве случаев достаточно только реализации NATIVE
(то есть TCP) протокола.
Иногда при соединении с источником требуется указать какие-то особенные параметры, например, у PostgreSQL есть понятие схемы (пространства имён для таблиц). Если вам недостаточн общее параметров, уже присутствующих в структуре TGenericDataSourceInstance, вы можете добавить в опциональное поле options
новую структуру, описывающую специфику именно вашего источника.
ConnectionManager
должен возвращать абстракцию соединения - Connection. Соединение умеет выполнять запросы (метод Query
). Результатом обработки запроса является интерфейс Rows. Фактически это итератор, сильно напоминающий по интерфейсу sql.Rows
. С помощью него имплементация DataSource
может вычитывать данные из соединения с РСУБД потоково, строчка за строчкой.
У Rows
есть важный метод - MakeTransformer
, который возвращает шаблонный интерфейс RowsTransformer[Acceptor]
. Он выполняет большую часть работы по конвертации данных между разными системами типов. В остальном работа с Rows
практически не отличается от работы с sql.Rows
из стандартной библиотеки.
Итак, вы успешно смогли прочитать данные с помощью отладочного клиента к fq-connector-go
. Финальный этап работ - сделать так, чтобы к вашему источнику данных мог обратиться самый важный клиент к коннектору - само YDB. Для этого необходимо внести изменения в его кодовую базу.
Important
Компиляция YDB из исходников требует больших вычислительных мощностей и может занимать очень много времени (на сервере с 56 ядрами - около 3 часов). Здесь на помощь приходит кэш артефактов компиляции, который поддерживается мейнтейнерами YDB. Этот кэш прогревается ежедневно во время ночных сборок ветки main
. Поэтому если вы хотите, чтобы при локальных сборках с помощью ya
hit rate кэша оставался достаточно высоким, вам необходимо поддерживать свои исходники в относительно актуальном состоянии и периодически ребейзиться на main
апстрима. Достичь этого можно, например, так:
gh repo sync юзернейм/ydb -s ydb-platform/ydb
git checkout main
git pull origin main
git checkout feature-branch
git rebase origin/main
Для поддержки нового источника в YDB предлагается следующий алгоритм:
-
Форкните репозиторий YDB. Склонируйте репозиторий на ту машину, где у вас будет идти разработка YDB. Эта машина должна быть достаточно мощной (минимум 16 ядер CPU , 32 Gb RAM), и создайте рабочую ветку.
git clone [email protected]:юзернейм/ydb.git cd ydb git checkout -b feature-branch
-
Выполните команду.
./ya ide vscode-clangd -P ~/projects/ydb.vscode-clangd ydb contrib/libs
-
В целевой папке появится workspase для VSCode.
-
(если работаете на виртуальной машине) В VSCode надо поставить плагин для удалённой работы по ssh и зайти на хост.
-
В VSCode на целевой машине надо поставить плагин с поддержкой clangd.
-
После открытия воркспейса clangd начнёт индексацию проекта (ориентируйтесь на несколько часов).
-
Проверьте ваш git global user.name и user.email командой:
git config --list --show-origin
Если там не указано ничего, или указаны не ваши данные, поменяйте их командыми:
git config --global user.name "ваш user.name"
git config --global user.email "ваш user.email"
Это важно для того, чтобы в вашем профиле Гитхаб отображались PRы в Ydb.
-
Скомпилируйте инструмент
kqprun
с помощью встроенного инструментаya
:./ya make --build relwithdebinfo ydb/tests/tools/kqprun
-
Разверните свой источник данных в виде Docker-контейнера.
-
Создайте какую-нибудь таблицу в вашем источнике данных (хороший GUI-инструмент для реляционных баз данных - DBeaver).
-
Разверните сервис коннектора (например,
make run
). -
Подготовьте файл
app_conf.txt
, в котором укажите хост и порт для подключения к сервису коннектора:FeatureFlags { EnableExternalDataSources: true EnableScriptExecutionOperations: true } QueryServiceConfig { Generic { Connector { Endpoint { host: "localhost" port: 2130 } UseSsl: false } DefaultSettings { Name: "DateTimeFormat" Value: "YQL" } } }
-
Подготовьте YQL-скрипт
schema.yql
, который регистрирует ваш источник данных как внешний для YDB, а также укажите пароль для доступа к источнику. Подставьте актуальные значения во все поля.CREATE OBJECT secret_password (TYPE SECRET) WITH (value = "<password>"); CREATE EXTERNAL DATA SOURCE external_data_source WITH ( SOURCE_TYPE="<data_source_type>", LOCATION="<host>:<port>", DATABASE_NAME="<table>", AUTH_METHOD="BASIC", LOGIN="<username>", PASSWORD_SECRET_NAME="secret_password", PROTOCOL="NATIVE", USE_TLS="FALSE" );
-
Подготовьте YQL-скрипт для извлечения данных
data.yql
, где вместо<table_name>
подставьте имя таблицы, которую создали на одном из предыдущих шагов.SELECT * FROM external_data_source.<table_name>
-
Вызовите
./kqprun
следующей командой./kqprun -s schema.yql -p data.yql --app-config=app_conf.txt
Если в результате вызова вы увидели JSON, похожий на те данные, что вы положили в таблицу, поздравляю - ваша работа окончена. Но с первого раза, конечно, ничего не получится. Проанализируйте ошибку, исправьте код и продолжайте компилировать и запускать
kqprun
до тех пор, пока не почините все ошибки.
Можно выделить несколько областей кода в YDB, которые нуждаются в добавлении нового источника данных:
- YQL Providers:
- https://github.com/ydb-platform/ydb/blob/main/ydb/library/yql/providers/common/db_id_async_resolver/db_async_resolver.h#L11-L44
- https://github.com/ydb-platform/ydb/blob/24.1.14/ydb/library/yql/providers/generic/provider/yql_generic_load_meta.cpp#L267-L293
- https://github.com/ydb-platform/ydb/blob/24.1.14/ydb/library/yql/providers/generic/provider/yql_generic_load_meta.cpp#L319-L331
- https://github.com/ydb-platform/ydb/blob/24.1.14/ydb/library/yql/providers/generic/provider/yql_generic_dq_integration.cpp#L191-L207
- https://github.com/ydb-platform/ydb/blob/6f2b38f212e36e0bcd0729525aef2e04494141a0/ydb/library/yql/providers/generic/actors/yql_generic_provider_factories.cpp#L34-L37
- https://github.com/ydb-platform/ydb/blob/24.1.14/ydb/library/yql/providers/generic/provider/yql_generic_dq_integration.cpp#L158-L171
- https://github.com/ydb-platform/ydb/blob/38a7ef26dd27509de68226e2d1117ed6ef933646/ydb/library/yql/providers/generic/provider/yql_generic_dq_integration.cpp#L24-L41
- https://github.com/ydb-platform/ydb/blob/e5ae52da8cfbfcdfa05ff85a236b85f19419d168/ydb/library/yql/providers/generic/provider/yql_generic_cluster_config.cpp#L195
- External Sources:
- Proto
- DDL (Если потребуется)
Список этих файлов может быть неисчерпывающим; если заметите что-то ещё - PRs are welcome :)
Примеры PR в YDB
Примеры PR в fq-connector-go
Периодически возникает необходимость как-либо расширить API Коннектора (например, добавить туда что-то специфичное для вашего источника данных) или поменять его конфигурацию. И API, и конфигурация описываются в виде Protobuf-файлов, по которым генерируется исходный код на языке Go. Сгенерированные файлы сохраняются в репозитории в fq-connector-go
.
Чтобы регенерировать исходники, выполните следующую команду:
# клонируйте репозиторий YDB
git clone [email protected]:ydb-platform/ydb.git
# при необходимости внесите изменения в исходники YDB
# перейдите в папку с исходинками коннектора и запустите скрипт
cd path/to/fq-connector-go/repo
./generate.py --ydb-repo=path/to/ydb/repo --connector-repo=path/to/fq-connector-go/repo
# Если вы вносили изменения в исходники YDB, не забудьте закоммитить их в апстрим через процедуру code review.