kubectl get pods --namespace presto
1 В книге использован термин «encrypted», но, разумеется, кодирование в BASE64
нельзя считать шифрованием. – Прим. перев.
48 Первые шаги в освоении Presto
Ответ этой команды должен быть примерно таким:
NAME READY STATUS RESTARTS AGE
mysql-5c58b74b44-6lckl 1/1 Running 0 9s
presto-coordinator-vpf7w 1/1 Running 0 9s
presto-worker-26mjb 1/1 Running 0 9s
Запросы к кластеру Presto
Теперь, когда мы запустили кластер, давайте попробуем выполнить на нем
какие-либо запросы. Для работы с Presto будем использовать клиент командной строки, развернутый вместе с узлом-координатором. Чтобы получить
к нему доступ, откройте терминал и введите следующую команду:
kubectl exec -it presto-coordinator-vpf7w -n presto -- bash
Затем запустите клиент Presto:
root@presto-coordinator-vpf7w:/# presto
presto>
Список каталогов
Все данные, которые доступны Presto, хранятся в каталогах. Чтобы увидеть
список доступных каталогов, введите следующую команду:
show catalogs;
Вывод этого запроса покажет, что вам доступен системный каталог, а также два каталога, которые мы сконфигурировали на предыдущих шагах, –
mysql и tpch:
Catalog
mysql
system
tpch
(2 rows)
Query 20220921_144917_00003_i6r2z, FINISHED, 1 node
Тайм-аут сокета
Иногда, когда вы запускаете запрос в Presto, система отвечает сообщением об ошибке: Error running command: java.net.SocketTimeoutException: timeout
Скорее всего, причина в том, что узел Presto находится в процессе инициализации
и вам надо просто немного подождать. Когда узел будет готов, вы сможете выполнять
запросы и получать ответы.
Запросы к кластеру Presto 49
Каждый каталог может содержать таблицы (хотя каталог может быть и пустым). Таблицы сгруппированы в схемы. Полное имя таблицы состоит из трех
компонентов: имени каталога, имени схемы и собственно имени таблицы:
<catalog_name>.<schema_name>.<table_name>
Давайте посмотрим, как получать списки схем и таблиц, а также извлекать
из таблиц данные.
Список схем
Все данные, которыми оперирует Presto, организованы в базы данных1, схемы и таблицы. Чтобы посмотреть схемы, содержащиеся в базе данных (каталоге), используется команда show schemas:
presto> show schemas from tpch;
Schema
information_schema
sf1
sf100
sf1000
sf10000
sf100000
sf300
sf3000
sf30000
tiny
(10 rows)
Query 20210908_045023_00001_j5vw7, FINISHED, 1 node
Splits: 19 total, 19 done (100.00%)
361ms [10 rows, 119B] [27 rows/s, 329B/s]
В каталоге tpch все схемы состоят из одного и того же набора таблиц. В имени схемы закодирован объем данных: так, схема tiny содержит наименьшее
количество объектов (около 10 тысяч), а схема sf100000 – наибольшее (около
100 млрд объектов).
Список таблиц
В каждой схеме содержится как минимум одна таблица. Чтобы вывести на
экран список всех таблиц схемы, используется команда show tables, параметром которой является полное имя схемы, то есть имя, содержащее имя
каталога и имя схемы, разделенные точкой:
1 Авторы используют термин database наряду с термином catalog. – Прим. перев.
50 Первые шаги в освоении Presto
presto> show tables from tpch.sf100;
Table
customer
lineitem
nation
orders
part
partsupp
region
supplier
(8 rows)
Query 20210908_045225_00002_j5vw7, FINISHED, 1 node
Splits: 19 total, 19 done (100.00%)
236ms [8 rows, 174B] [33 rows/s, 735B/s]
Запрос к таблице
Наконец, давайте попробуем вывести на экран несколько строк из таблицы
customer, которая находится в схеме sf100. Здесь мы также будем использовать
полное имя таблицы, состоящее из трех компонентов, разделенных точками.
Прежде чем запрашивать данные из таблицы, посмотрим на ее структуру.
Для этого используется команда show create table, которая в большинстве
СУБД называется describe table:
presto> show create table tpch.sf100.customer;
Create Table
CREATE TABLE tpch.sf100.customer (
"custkey" bigint NOT NULL,
"name" varchar(25) NOT NULL,
"address" varchar(40) NOT NULL,
"nationkey" bigint NOT NULL,
"phone" varchar(15) NOT NULL,
"acctbal" double NOT NULL,
"mktsegment" varchar(10) NOT NULL,
"comment" varchar(117) NOT NULL
)
(1 row)
Query 20210908_050304_00011_j5vw7, FINISHED, 1 node
Splits: 1 total, 1 done (100.00%)
43ms [0 rows, 0B] [0 rows/s, 0B/s]
Теперь мы знаем имена полей в таблице и можем явно перечислить эти
имена в команде select, вместо того чтобы писать select *. Запрос со звез-
Запросы к кластеру Presto 51
дочкой вернет все поля, независимо от того, нужны они нам или нет. Давайте
для примера запросим контактную информацию клиента:
select custkey,name,address,phone from tpch.sf100.customer limit 5; Ниже приведен ответ сервера:
presto> select custkey,name,address,phone from tpch.sf100.customer limit 5; custkey | name | address | phone
11250001 | Customer#011250001 | ... | 24-397-921-1461
11250002 | Customer#011250002 | ... | 34-747-594-8280
11250003 | Customer#011250003 | ... | 22-456-401-1500
11250004 | Customer#011250004 | ... | 24-596-187-3005
11250005 | Customer#011250005 | ... | 18-965-501-9572
(5 rows)
Query 20210908_050526_00012_j5vw7, FINISHED, 1 node
Splits: 29 total, 21 done (72.41%)
209ms [51.2K rows, 0B] [245K rows/s, 0B/s]
Чтобы избавиться от необходимости каждый раз писать полное имя таблицы, можно воспользоваться командой use <schema>:
presto> use tpch.sf100;
USE
presto:sf100>
До тех пор, пока вы еще раз не измените текущую схему командой use или не выйдете из сеанса, ко всем таблицам из схемы tpch.sf100 можно обращаться по короткому имени – без точек. Следующий пример показывает
тот же самый запрос, но уже с обращением к таблице просто по имени после
использования команды use:
presto:sf100> select custkey,name,address,phone from customer limit 5; custkey | name | address | phone
11250001 | Customer#011250001 | ... | 24-397-921-1461
11250002 | Customer#011250002 | ... | 34-747-594-8280
11250003 | Customer#011250003 | ... | 22-456-401-1500
11250004 | Customer#011250004 | ... | 24-596-187-3005
11250005 | Customer#011250005 | ... | 18-965-501-9572
(5 rows)
Query 20210908_050903_00016_j5vw7, FINISHED, 1 node
Splits: 29 total, 18 done (62.07%)
255ms [12.8K rows, 0B] [50.1K rows/s, 0B/s]
Мы закончили настройку локального кластера Presto и убедились, что можем обращаться к нему при помощи клиента командной строки.
52 Первые шаги в освоении Presto
Заключение
В этой главе вы научились устанавливать и запускать Presto двумя разными
способами – при помощи Docker и при помощи Kubernetes. Docker позволяет
запустить локальный экземпляр Presto, не заботясь об установке всевоз-можного программного обеспечения, от которого Presto зависит и которое
необходимо ему для запуска.
Kubernetes позволяет развернуть кластер с узлом-координатором и нуж-ным количеством рабочих узлов. Более того, в том же кластере Kubernetes можно запускать и базы данных, используемые как каталоги, к которым обращается Presto. Мы запустили сервер MySQL и подключили его к Presto.
Наконец, вы запустили на локальной машине полноценный кластер в ба-зовой конфигурации. В главе 3 вы научитесь создавать дополнительные коннекторы и подключать к кластеру Presto дополнительные каталоги.
Глава 3
Коннекторы
Коннектор в Presto – это расширение, дополнительный модуль, позволяющий подключаться к внешнему источнику данных – каталогу. Для Presto уже
написано множество коннекторов к самым разным источникам – реляционным и нереляционным СУБД, файловым системам и т. д.
В этой главе вы узнаете, как разработать собственный коннектор. Для начала мы рассмотрим концепцию SPI ( Service Provider Interface) – интерфейса, предоставляемого Presto. Этот интерфейс позволяет создавать расширения
для Presto – как коннекторы, так и собственные типы, функции и методы
управления доступом.
Затем мы разберем концепцию коннектора и детально изучим реализацию этой концепции. Вы научитесь создавать собственные коннекторы.
Кроме того, мы расскажем об альтернативной реализации коннектора на
базе Apache Thrift, позволяющей Presto обмениваться информацией с любым
сервером, написанным на любом из множества языков, поддерживающих
Thrift, например Python или PHP.
Наконец, мы подключим к Presto хранилище, работающее в режиме реального времени, на базе Apache Pinot.
Концепция SPI (Service Provider
Interface)
SPI – это интерфейс, с помощью которого любой разработчик может расширить возможности платформы. В Presto реализован собственный SPI. Архитектура взаимодействия с расширениями показана на рис. 3.1. Presto SPI – это
модуль, являющийся частью сервера Presto. При помощи расширений он
обеспечивает взаимодействие между Presto и внешними системами.
Система (service provider), желающая предоставить доступ Presto, должна
обзавестись модулем (расширением), с которым будет взаимодействовать
Presto SPI. В свою очередь Presto SPI предоставляет интерфейс (API), через
который сам Presto вызывает функции внешних систем-провайдеров.

54 Коннекторы
Внешняя
Расширение
система 1
1
SPI
API
Presto SPI
Ядро
Presto
Внешняя
Расширение
Узел Presto
система 1
1
Рис. 3.1 Архитектура Presto SPI
Самые популярные типы расширений Presto – это коннекторы, типы, функции и методы управления доступом. В этом разделе мы коротко расскажем об этих расширениях, а более детальное описание всегда можно найти
в документации1.
Коннекторы
Коннектор позволяет Presto читать данные из какого-либо источника или
записывать в него данные. Как мы увидим далее, коннектор отвечает за
то, чтобы представить структуру источника в виде, принятом в Presto, а именно в виде схем, таблиц и полей. Кроме того, коннектор должен преобразовать типы данных, с которыми работает источник, к типам Presto.
Типы
Тип – это расширение, реализующее пользовательский тип данных, который можно использовать в SQL-запросах Presto. Presto уже содержит множество встроенных типов, например BOOLEAN, INTEGER, VARCHAR и др. У коннектора нет необходимости реализовывать все типы данных.
Функции
В Presto есть множество функций для доступа к данным и их обработки. Примерами таких функций могут служить математические функции
и функции обработки строк, агрегирующие функции и т. д. Используя специальный интерфейс для функций, вы можете писать свои собственные
функции.
Методы управления доступом
Presto позволяет создавать политики управления доступом, в рамках которых вы можете определять различные роли, обладающие разными при-
вилегиями. Вы можете писать расширения, которые позволят реализовать
собственные привилегии. Подробнее вопросы разграничения доступа мы
рассмотрим в главе 7.
1

Архитектура коннектора 55
Архитектура коннектора
На рис. 3.2 показан поток данных между Presto и коннектором. Когда клиент
запускает запрос, координатор разбирает его ( Parser), а затем анализирует
( Analyzer), извлекая из каждого каталога, к которому обращается запрос, информацию о его структуре – метаданные. Эта информация включает список
доступных таблиц, определения полей и т. д. Для извлечения этой информации расширение реализует специальный интерфейс – Metadata API.
Узел-координатор
Разбор и анализ
Клиент
Оптимизатор (planner)
Metadata API
Организатор
Data location API
(scheduler)
Коннектор
Рабочий узел
Data stream API
Каталог
Коннектор
Рабочий узел
Data stream API
Каталог
Коннектор
Рис. 3.2 Взаимодействие Presto с коннектором
Затем координатор планирует запрос ( Planner), оптимизирует его и рассы-лает задачи рабочим узлам ( Scheduler). Координатор вызывает Data Location API, чтобы определить физическое местонахождение части данных, а потом
распределяет эти части между рабочими узлами. Получив задачу на обработку своей части данных, рабочий узел читает данные при помощи Data Stream API, обрабатывает их и отправляет координатору, который, в свою очередь, возвращает результаты запроса клиенту.
56 Коннекторы
Популярные коннекторы
С Presto поставляется множество коннекторов, обеспечивающих доступ к самым разным источникам, включая реляционные и нереляционные СУБД, системы реального времени, а также объектные хранилища (например, S3) и файловые системы (например, HDFS, Hadoop Distributed File System). Список доступных коннекторов постоянно пополняется – ищите его в соответствующем разделе документации1.
В этой книге мы остановимся всего на трех коннекторах: Thrift, Apache Pinot и Apache Hudi. Первые два мы рассмотрим в данной главе, а Apache Hudi – в главе 5. Краткая информация о рассматриваемых коннекторах приведена в табл. 3.1.
Таблица 3.1. Коннекторы, описываемые в этой книге
Коннектор
Краткое описание технологии
Thrift
Протокол, обеспечивающий эффективное взаимодействие между
сервисами, реализованными на разных языках
Apache Pinot Аналитическая СУБД, работающая в режиме реального времени, с открытым исходным кодом. Предназначена для быстрой агрегации
больших объемов данных
Apache Hudi Платформа для озера данных, хранящая данные в собственном
табличном формате
В главе 2 вы научились подключать к Presto коннекторы. Для этого достаточно добавить новый конфигурационный файл, называющийся mycatalog.
properties, в папку catalog вашей инсталляции Presto. Минимальная информация, которая должна содержаться в этом файле, – имя каталога, например
connector.name=tpch.
Thrift
Apache Thrift – это фреймворк для разработки масштабируемых сервисов, компоненты которых написаны на разных языках. Подробное описание
данного фреймворка и его архитектуры выходит за рамки книги. Если вам
интересны подробности, обратитесь к официальной документации Thrift2.
Для наших целей достаточно отметить, что Apache Thrift содержит компилятор интерфейсов. На вход он получает описание интерфейса на специа-лизированном языке (IDL, Interface Definition Language), а на выходе выдает
заготовку кода на одном из языков программирования, которая работает
с этим интерфейсом. Компилятор поддерживает Python, PHP, Java и другие
1 .
2 .
Разработка коннектора 57
популярные языки. Дописывая в полученную заготовку код, реализующий
логику вашего сервиса, вы получаете готовые модули.
Если вам важна независимость вашего сервиса от платформы, то фреймворк Apache Thrift будет чрезвычайно полезен: он позволяет создавать модули, взаимодействующие друг с другом независимо от того, на каком языке
они написаны и на какой платформе запущены. Создатели Thrift позабо-тились и о производительности, и о гибкости, и о простоте использования, создав поистине универсальный инструмент.
Научив Presto взаимодействию по протоколу Thrift, мы получим практически идеальный инструмент для разработки масштабируемых платформен-но-независимых приложений, поскольку Presto сможет получать данные от
любого приложения, написанного на любом языке, – лишь бы это приложение также поддерживало Thrift.
Коннектор для Thrift уже реализован в Presto, и вы можете использовать
его так же, как и любой другой коннектор. Зайдите в папку etc/catalogs/ узла-координатора и создайте там файл с минимальной конфигурацией:
connector.name=my-thrift-connector
presto.thrift.client.addresses=host:port,host:port
Кроме названия коннектора, вам необходимо указать адреса серверов приложения, которое вы хотите использовать в качестве источника. В качестве
приложения для начала можно разместить модули-заготовки, созданные
компилятором Thrift. Полный список настроек, поддерживаемых коннектором Thrift, можно найти в документации Presto1.
Чтобы создать приложение-источник, загрузите описание интерфейса на
языке IDL из нашего репозитория – файл называется 03/thrift/PrestoThriftService.thrift. Поместите этот файл в рабочую папку вашего компьютера и запустите компилятор Thrift для создания заготовки приложения:
thrift --gen java PrestoThriftService.thrift
Эта команда создаст заготовку на Java, но если вы предпочитаете другой
язык, то замените параметр java в командной строке на название вашего
любимого языка. В репозитории вы можете взять и уже созданную заготовку
для Java – она находится в папке 03/thrift/thrift-stub-java.
Создав заготовку, напишите код для выполнения логики сервиса, который
будет представлять собой каталог. Чтобы протестировать созданный сервис, воспользуйтесь модулем presto-thrift-testing-server, доступным в репозитории PrestoDB на GitHub.
Разработка коннектора
Давайте вообразим, что у нас есть собственная СУБД и мы хотим, чтобы
Presto мог к ней обращаться. Но вот незадача – ни один из существующих
1
58 Коннекторы
коннекторов не умеет работать с нашей уникальной платформой1. В этом
случае лучшим решением будет разработка собственного коннектора.
В этом разделе мы создадим коннектор с нуля. Вот четыре важнейших
компонента, из которых состоит коннектор:
расширение (классы Plugin и Module);
конфигурация;
метаданные;
ввод-вывод.
Чтобы понять функции этих компонентов, обратимся к модельному кон-
нектору2 для HTTP. Это базовый коннектор, который читает CSV-файлы через
HTTP-соединение. Его код находится в модуле presto-example-http, который
можно загрузить из репозитория PrestoDB на GitHub3.
Предварительные требования
Поскольку код Presto написан на Java, коннекторы также пишутся на Java.
Вдобавок, поскольку для сборки Presto используется Apache Maven, для сборки коннектора вам он тоже понадобится. Обязательно загрузите jar-файл
Presto4 и добавьте его к своему проекту как библиотеку. Использовать или
не использовать IDE – ваш выбор, но мы применяем IntelliJ IDEA5. Использование IDE существенно упрощает управление проектами в Maven, поэтому
и вам мы настоятельно рекомендуем применять какую-либо интегрированную среду. Но если вы по каким-то причинам не хотите ее использовать, убедитесь, что у вас установлены все необходимые компоненты, включая
JDK и библиотеки.
Создайте новый проект. В IntelliJ IDEA для этого необходимо выбрать
в меню пункт File ⇒ New ⇒ Project ⇒ New Project. Выберите Maven в качестве способа сборки и нажмите кнопку Create. Добавьте в проект библиотеки Presto. Если у вас не установлен JDK, выберите пункт JDK ⇒ Download JDK и загрузите версию 1.8.
К созданному проекту надо добавить библиотеки Presto. Нажмите на имя
проекта в левой части окна, затем Open Module Settings ⇒ Libraries ⇒ + ⇒
Java. Найдите загруженный на предыдущем шаге jar-файл Presto и нажмите OK.
Если проекту нужны дополнительные модули, добавьте их через меню Open Module Settibngs ⇒ Libraries ⇒ From Maven, выбрав необходимые файлы.
Чтобы загрузить в IntelliJ IDEA код HTTP-коннектора, загрузите исходный
код6 и откройте пример через меню File ⇒ New ⇒ Project ⇒ New Project from existing sources ⇒ Import project from external model ⇒ Maven.
1 На практике разработчики новых СУБД обычно реализуют один из стандартных
клиентских протоколов, например PostgreSQL или MySQL для реляционных платформ и Redis или Memcached для платформ класса «ключ–значение». – Прим. перев.
2 .
3 .
4
5
6 .

Разработка коннектора 59
Классы Plugin и Module
Инициализация каталога при его загрузке в Presto выполняется несколькими
классами, составляющими костяк расширения. В нашем примере эти клас-сы называются ExamplePlugin, ExampleConnectorFactory, ExampleModule, ExampleConnector и ExampleHandleResolver. Взаимодействие этих классов показано на
рис. 3.3.
Как называть классы
При разработке расширений для Presto нет каких-то обязательных соглашений об
именовании. Однако мы рекомендуем составлять имя класса из двух частей: первая
часть – название коннектора (в нашем случае – Example), а вторая – реализуемый
интерфейс (например, Plugin).
Классы, составляющие верхний уровень абстракции, отмечены на рисунке
темными блоками, а те классы, экземпляры которых создаются и используются базовыми классами, – светлыми.
New
Example plugin
Example connector factory
New
New
Управление
Example module
жизненным
Example handle resolver
циклом
Example connector
Example table layout handle
Example connector ID
Example table handle
Example metadata
Example column handle
Конфигурирует
Example client
Указывает на
Возвращает экземпляр
Example split
Example split manager
Example transaction handle
Example record set provider
Example config
Рис. 3.3 Базовые классы, составляющие интерфейсный уровень расширения
ExamplePlugin
Этот класс реализует интерфейс Plugin, через который Presto запрашивает
список функций, реализованных расширением. Поскольку мы разрабатыва-ем коннектор, класс ExamplePlugin реализует единственный метод getConnec-torFactory() и возвращает экземпляр класса ExampleConnectorFactory. Если вы
60 Коннекторы
хотите создать расширение с другой функциональностью, например с допол-нительными функциями и типами, вам надо реализовать и другие методы.
ExampleConnectorFactory
ConnectorFactory – базовый интерфейс, при помощи которого Presto создает
соединения к источнику, представленному конкретным коннектором. Класс
ExampleConnectorFactory реализует этот интерфейс и выполняет следующие
функции:
getName() – возвращает имя коннектора, example-http. Это имя должно
быть уникальным в рамках инсталляции Presto, именно по нему Presto идентифицирует коннектор. Это имя используется в конфигурационном файле (см. главу 2) для создания каталога;
getHandleResolver() – создает объект ExampleHandleResolver, при помощи
которого Presto узнает, какие классы использовать для этого коннектора;
create() – создает модули, необходимые для запуска коннектора.
Для внедрения зависимостей Presto использует Google Guice1. Рассказ об
этом фреймворке и объяснение концепции внедрения зависимостей выходят
за рамки книги. Если совсем коротко, то вы определяете модуль, который необходим для работы коннектора, а затем создаете экземпляр этого модуля, используя внедрение зависимостей. Guice анализирует модуль, понимает, от
каких классов тот зависит, и при необходимости создает экземпляры требу-емых классов. Если какие-то классы не находятся, то Guice завершает про-грамму с ошибкой на этапе инициализации. Подробнее об этом фреймворке
вы можете прочитать в его документации2.
ExampleModule
Этот класс реализует интерфейс Module, определенный в Google Guice. Настройка класса, использующего Guice, выполняется в методе configure(): public void configure(Binder binder)
{
binder.bind(ExampleConnector.class).in(Scopes.SINGLETON);
...
}
ExampleConnector
Этот класс реализует собственно функциональность коннектора, предоставляя ее Presto через интерфейс Connector. Presto создает экземпляр этого класса, передавая в качестве аргументов экземпляры четырех других классов.
Один из них – LifeCycleManager, а три оставшихся – реализации интерфейсов
MetaData, SplitManager и RecordSetProvider.
1
2
Разработка коннектора 61
В классе ExampleConnector реализованы методы, которые создают и возвращают объекты, реализующие эти интерфейсы, а также методы для открытия
транзакций и для завершения работы расширения:
public class ExampleConnector implements Connector
{
@Inject
public ExampleConnector(
LifeCycleManager lifeCycleManager,
ExampleMetadata metadata,
ExampleSplitManager splitManager,
ExampleRecordSetProvider recordSetProvider){...}
public ConnectorTransactionHandle
beginTransaction(IsolationLevel isolationLevel, boolean readOnly){...}
public ConnectorMetadata
getMetadata(ConnectorTransactionHandle transactionHandle){...}
public ConnectorSplitManager getSplitManager(){...}
public ConnectorRecordSetProvider getRecordSetProvider(){...}
public final void shutdown(){...}
}
Аннотация @Inject перед конструктором показывает фреймворку Google Guice, что класс надо добавить в зависимость от внедрения.
ExampleHandleResolver
Этот класс реализует интерфейс ConnectorHandleResolver, который используется Presto для определения реализованных обработчиков. Любая реализация коннектора должна содержать как минимум следующие обработчики: ConnectorTableHandle, ConnectorTableLayoutHandle, ColumnHandle, ConnectorTransactionHandle и ConnectorSplit. Подробнее о них поговорим в этом разделе чуть
позже.
Для каждого реализованного обработчика класс ExampleHandleResolver определяет метод, возвращающий класс этого обработчика:
@Override
public Class<? extends ConnectorTableHandle> getTableHandleClass()
{
return ExampleTableHandle.class;
}
Конфигурация
Конфигурация включает классы, содержащие настройки каталога, например URL базы данных, имя пользователя и пароль, параметры сеанса и т. д.
В Presto есть три интерфейса (а в нашем примере – три класса, реализующих
эти интерфейсы), которые так или иначе относятся к конфигурации коннектора: свойства коннектора (ExampleConfig), настройки сеанса (SessionProperties) и описание таблицы (TableProperties).
62 Коннекторы
ExampleConfig
Свойства коннектора содержат статическую информацию, используемую
коннектором, например информацию, необходимую для установления со-
единения, такую как URL, имя пользователя и пароль. Класс ExampleConfig содержит методы для чтения и установки настроек коннектора Presto, которые хранятся в конфигурационных файлах каталога, например example-http.
properties.
Наш класс ExampleConfig работает с настройками именно нашего коннектора. Например, чтобы поменять значение параметра metadata-uri, в нем
определен следующий метод:
@Config("metadata-uri")
public ExampleConfig setMetadata(URI metadata)
{
this.metadata = metadata;
return this;
}
Аннотация @Config определяет название параметра, который будет задан
в файле example-http.properties. Есть также аннотации для методов-геттеров, позволяющие наложить некоторые ограничения на значения параметров.
Например, аннотация @NotNull сообщает Presto, что значение этого параметра не может быть пустым, а @Size(min=1) не позволяет пользователю задать
для этого параметра значение меньше, чем единица.
SessionProperties
Настройки сеанса – это некие значения, которые передаются в каждое клиентское соединение. Обычно они влияют на работу сервера базы данных
и используются для тонкой настройки соединения, связанной с обработкой
определенного типа нагрузки. Через командную строку настройки сеанса
обычно задаются примерно так:
SET SESSION connectorname.propertyname = 'value'
Наш коннектор не определяет никаких настроек сеанса. Однако чтобы понять, как работают эти настройки, мы определим простую настройку LOG_VERBOSITY, которая позволяет включить журналирование сеанса.
Конструктор класса ExampleSessionProperties создает список доступных настроек. У каждой настройки есть имя, описание, типы данных, используемые
в SQL и в Java, значение по умолчанию, признак скрытой настройки и функции кодирования и декодирования. Настройка LOG_VERBOSITY описывается
следующим образом:
PropertyMetadata<Boolean> s1 = booleanProperty(
LOG_VERBOSITY,
"Set to true to enable log verbosity.",
true,
false
);
Разработка коннектора 63
Мы использовали конструктор PropertyMetadata для создания этой настройки. Чтобы проверить, включена ли настройка, класс ExampleSessionProperties определяет следующий метод:
public static boolean isLogVerbosityEnabled(ConnectorSession session)
{
return session.getProperty(LOG_VERBOSITY, Boolean.class);
}
TableProperties
Описание таблицы содержит информацию о конкретной таблице в вашем
хранилище, например параметры секционирования. Структура класса очень
похожа на структуру класса, описывающего настройки сеанса. В качестве
примера можете посмотреть исходный код класса HiveTableProperties1, реализованного в коннекторе для Hive.
При создании таблицы ее свойства включаются в команду внутри фразы
WITH:
CREATE TABLE foo (a BIGINT)
WITH
(
myTableProperty = 'value',
myOtherTableProperty = 'othervalue'
);
Метаданные
К метаданным относятся классы, описывающие модель данных и позволяющие Presto получить доступ к информации о модели. Кроме того, для управления метаданными разработчик коннектора должен реализовать еще два
класса – Metadata и Client. На рис. 3.4 показаны основные классы, образующие
метаданные нашего коннектора.
ExampleClient содержит ассоциативный массив Supplier, который, собственно, и представляет модель данных. Позже в этой главе мы разберем структуру
модели более подробно. ExampleMetadata содержит указатель на ExampleClient и при необходимости создает экземпляры классов ExampleTableLayoutHandle, ExampleTableHandle и ExampleColumnHandle.
Модель данных
Presto считает, что все источники представляют собой реляционные базы
данных. Коннектор должен представлять платформу, для которой он разработан, именно в виде реляционной БД, даже если платформа на самом
деле нереляционная. Данные в реляционной модели представлены в виде
иерархии схем, таблиц и полей. Схема может состоять из одной или более
1 .


64 Коннекторы
таблиц, в таблице должно быть как минимум одно поле. Данные каждой
таблицы на самом деле могут быть физически распределены по нескольким
объектам хранения.
ExampleClient
New
Указывает на
Источник данных
ExampleMetadata
ExampleTable
Schema 1
Table 1
ExampleTableLayoutHandle
ExampleColumn
Schema 2
Table 2
ExampleColumn
ExampleTableHandle
ExampleColumn
Schema N
Table N
ExampleColumnHandle
ExampleColumn
Рис. 3.4 Основные классы, из которых состоят метаданные
В коде коннектора вы обязаны создать как минимум классы Table и Column.
Они должны быть определены как простые объекты (POJO, Plain Old Java Object) и снабжены аннотацией @JsonCreator. Аннотация используется Presto, чтобы сериализовать эти объекты в JSON для передачи между рабочими узлами и координатором.
Обработчики
Чтобы Preso мог получить доступ к классам, реализующим модель данных, вы должны определить для них также классы-обработчики, которые Presto использует при разборе и планировании запроса: TableHandle, ColumnHandle и TableLayoutHandle.
На рис. 3.5 изображена последовательность вызовов, позволяющая Presto получить из нашего коннектора указатель на таблицу.
Пользо
Example
Example
ватель
metadata
client
Источник данных
getTableHandle
(schema, tableName)
getTable
(schema, tableName)
Schema
tableName
ExampleTable
new
ExampleTableHandle
ExampleTable
(schema, tableName)
Рис. 3.5 Последовательность запросов для получения указателя на таблицу
Разработка коннектора 65
Пользователь вызывает метод getTableHandle() экземпляра класса ExampleMetadata, передавая в качестве параметров имя схемы ( schema) и имя таблицы ( tableName), на которую он хочет получить указатель. Указатель представляет собой экземпляр класса ExampleTableHandle. Объект ExampleMetadata, содержащий указатель на объект ExampleClient, транслирует этот вызов в вызов метода ExampleClient.getTable(). В свою очередь, ExampleClient извлекает
объект ExampleTable и возвращает его. Объект ExampleMetadata проверяет, не
получен ли null, и если все в порядке, то создает новый экземпляр класса
ExampleTableHandle и возвращает его пользователю.
ExampleMetadata
Этот класс реализует интерфейс ConnectorMetadata, в котором содержатся методы для отображения структуры объектов на реляционную модель Presto, –
схемы, таблицы, поля. Реализация каждого из этих методов зависит от того, какую модель отображения вы выбрали.
ExampleClient
В этом классе реализована функциональность извлечения информации
о схемах, таблицах и полях. В нашем коннекторе реализация этого класса
предельно проста, поскольку это всего лишь учебный пример. Настоящий
промышленный коннектор обращается к API системы, которую он подключает к Presto, и отображает полученные объекты на объекты реляционной
модели Presto. На рис. 3.4 показано, что экземпляр класса ExampleClient поддерживает ассоциативный массив, представляющий логическую структуру
каталога.
Ввод-вывод
Под вводом-выводом понимается набор классов, которые читают данные из
внешней системы или записывают их.
Чтобы прочитать данные из таблицы, вы можете разбить таблицу на несколько частей и создать соответствующие этим частям объекты. Эти объекты будут запущены параллельно рабочими узлами Presto. Для каждого
читающего объекта создается объект-набор записей (recordset), который содержит курсор, позволяющий Presto работать с набором.
На рис. 3.6 показаны классы, реализующие уровень ввода-вывода в нашем
коннекторе. Поскольку у нас всего лишь учебный пример, в нем реализованы
только операции чтения.
ExampleSplitManager создает N экземпляров ExampleSplit, где N – количество
независимых частей, из которых состоит таблица. Для каждого такого экземпляра ExampleRecordSetProvider создает экземпляр ExampleRecordSet, в котором
среди прочего есть и указатель на ExampleSplit, строки которого он обрабатывает. Кроме того, каждый экземпляр ExampleRecordSet создает экземпляр
ExampleRecordCursor, используемый для навигации по записям.

66 Коннекторы
New
New
ExampleSplitManager
ExampleRecordSetProvider
New
Example split 1
Example record set 1
Example record cursor 1
New
Чтение
Example split 2
Example record set 2
Example record cursor 2
Таблица
New
Example split N
Example record set N
Example record cursor N
Рис. 3.6 Основные классы, реализующие уровень ввода-вывода
PageSourceProvider и PageSinkProvider
Для ускорения обмена данными в дополнение к RecordSetProvider вы можете реализовать интерфейс PageSourceProvider, который передает данные в Presto не построчно, а целыми страницами. Также вы можете реализовать PageSinkProvider для
быстрой записи данных.
ExampleSplitManager
Этот класс реализует интерфейс ConnectorSplitManager, содержащий единственный метод getSplits(), возвращающий список фрагментов таблицы, каждый из которых рабочие узлы Presto могут читать независимо и параллельно. У этого метода три параметра: ConnectorSession, который используется для доступа к настройкам сеанса, TableLayoutHandle для доступа к таблице и контекст. Presto получает список, возвращаемый методом getSplits(), и передает фрагменты экземпляру RecordSetProvider.
ExampleSplit
Этот класс реализует интерфейс ConnectorSplit. Экземпляр этого класса ас-социируется с фрагментом таблицы (Split). Как и другие классы, представ-ляющие собой описание метаданных, ExampleSplit – простой объект (POJO), который сериализуется в JSON при помощи Jackson. Он содержит имя схемы, имя таблицы и URI для чтения данных.
ExampleRecordSetProvider и ExampleRecordSet
Класс ExampleRecordSetProvider реализует интерфейс ConnectorRecordSetProvider. Из фрагмента таблицы при помощи метода getRecordSet() он создает
экземпляр набора записей – RecordSet. Кроме того, он содержит список полей, которые потребитель ожидает прочитать из таблицы и, соответственно, из
всех наборов записей, ассоциированных с этой таблицей. Если пользователь
обратится к таблице с запросом вида SELECT *, то получит в ответ значения
всех полей из этого списка.
Разработка коннектора 67
Класс ExampleRecordSet реализует интерфейс RecordSet и используется для
порождения экземпляров ExampleRecordCursor, которые осуществляют нави-гацию по содержимому фрагмента.
ExampleRecordCursor
Этот класс реализует интерфейс RecordCursor и выполняет операции чтения.
Наша реализация читает CSV-файлы, поэтому в ExampleRecordCursor содержит
методы, построчно читающие файл и разбивающие полученные строки на
поля, значения которых и возвращаются из коннектора.
Когда создается экземпляр ExampleRecordCursor, он загружает в память все
строки из файла в итератор:
public ExampleRecordCursor(
List<ExampleColumnHandle> columnHandles,
ByteSource byteSource)
{
...
lines = byteSource.asCharSource(UTF_8).readLines().iterator();
...
}
Далее ExampleRecordCursor для выполнения различных методов доступа
к данным вызывает функции итератора. Например, переход к следующей
строке реализован так:
public boolean advanceNextPosition()
{
if (!lines.hasNext()) {
return false;
}
String line = lines.next();
fields = LINE_SPLITTER.splitToList(line);
return true;
}
LINE_SPLITTER – статический объект класса ExampleRecordCursor. Все, что он
делает, – разделяет строку на поля:
private static final Splitter LINE_SPLITTER = Splitter.on(",").trimResults(); Установка коннектора
Чтобы установить коннектор, соберите проект при помощи Maven, используя
пакет presto-plugin. Для создания конфигурационного файла pom.xml возь-мите файл из проекта presto-example-http и измените его под свой проект.
Обязательно поменяйте название проекта, а при необходимости – и под-ключаемые зависимости.
Собрать проект можно двумя способами: из командной строки или с использованием IntelliJ. Чтобы собрать Maven-проект из командной строки,
68 Коннекторы
просто запустите в рабочей папке вашего проекта команду mvn package. Если
же вы хотите воспользоваться IntelliJ, сделайте следующее:
1) откройте панель Maven-проекта в правой части экрана;
2) найдите ваш проект и выберите в нем раздел Lifecycle;
3) запустите сборку пакета двойным кликом по его названию.
Когда пакет собран, скопируйте содержимое целевой папки ( target) в папку с расширениями в вашей инсталляции Presto. Затем добавьте папку для
вашего коннектора в папке etc/catalogs и перезапустите Presto.
Apache Pinot
Apache Pinot – это распределенная СУБД для аналитической обработки данных
(OLAP) в режиме реального времени, предназначенная для обеспечения сверх-быстрой аналитики даже при обработке огромных потоков данных. Вы можете
использовать Pinot для получения данных из любых источников. Эта платформа одинаково хороша как для источников, работающих в режиме реального
времени, например Apache Kafka, так и для пакетных источников ( batch), например HDFS (Hadoop Distributed File System). Когда говорят о Pinot примени-тельно к аналитике в реальном времени, обычно рассматривают эту платформу
как компонент хранилища-озера, подключенный к объектному хранилищу
с интерфейсом S3. Архитектуру такого хранилища-озера мы опишем в главе 5.
В этой главе мы уделим внимание именно Apache Pinot, поскольку данная
платформа хорошо подходит для обработки многомерных данных в режиме
реального времени. Кроме того, возможность быстрого выполнения запросов в Pinot делает его идеальным источником данных для Presto. В результате
Pinot и Presto образуют мощную платформу для быстрой и эффективной
аналитики на больших наборах данных.
В Presto уже есть коннектор для Pinot, и в этом разделе мы научимся его подключать. Подробное описание Pinot и его архитектуры выходит за рамки книги. Если вам это интересно, обратитесь к официальной документации Pinot1.
Настройка и конфигурация Presto
Интерфейс Apache Pinot не совместим со стандартными API ODBC/JDBC, а язык запросов не соответствует стандарту ANSI SQL. Подключая Pinot к Presto, мы решаем эту проблему, поскольку теперь пользователи могут обращаться к данным при помощи Presto, который поддерживает и ANSI SQL, и JDBC.
Есть и еще два преимущества совместного использования Presto и Pinot:
одни и те же SQL-запросы можно использовать как для доступа к данным
Pinot, так и для доступа к другим источникам данных в озере. Например, вы можете запускать один и тот же запрос как на свежих, так и на
исторических данных (парадигма «один запрос для любых данных»);
1
Apache Pinot 69
федеративные запросы, включающие наряду с Pinot другие источники, позволяют проанализировать значительно больше данных и получить
гораздо более интересные результаты.
Установка Pinot
Мы запустим Apache Pinot в одном из узлов кластера Kubernetes, который
мы создали в главе 2. Поскольку ресурсов у нас не так много, всего одна машина, мы развернем единственный узел Pinot. Разумеется, в промышленных
системах Pinot разворачивается как многоузловой кластер.
Конфигурационные файлы для Pinot и Presto, которые используются в этом
разделе, можно взять в папке 03/presto-and-pinot из нашего репозитория.
Настройка Pinot
Pinot состоит из четырех основных модулей: контроллера ( controller), брокера
( broker), серверов ( servers) и распределенного хранилища конфигурации ( DCS, distributed configuration store)1. Подробное описание этих модулей остается за
рамками книги. Для наших целей достаточно знать, что все модули образуют
кластер, и управляет этим кластером модуль-контроллер – подобно тому, как
кластером Presto управляет узел-координатор.
Чтобы развернуть на своей машине Pinot, следуйте инструкции из официальной документации. Однако если у вас не особенно мощная машина
(менее 16 Гбайт памяти), которой не хватит для одновременной работы Pinot и Presto, воспользуйтесь инструкцией из этого раздела. Мы развернем все
модули Pinot в одном поде ( pinot-deployment.yaml):
apiVersion: v1
kind: ReplicationController
metadata:
name: pinot
spec:
replicas: 1
template:
metadata:
labels:
app: pinot
spec:
containers:
- name: pinot
image: apachepinot/pinot:latest
command: ["/opt/pinot/bin/pinot-admin.sh" ]
args: ["QuickStart" , "-type" , "batch" ]
ports:
1 Хранилище конфигурации в Pinot реализовано на базе Zookeeper, и в оригинале
модуль так и называется – «zookeeper». Термин DCS заимствован из кластерного
ПО Patroni, в котором, помимо Zookeeper, могут использоваться и конкурирующие
платформы, например etcd и consul. – Прим. перев.
70 Коннекторы
- containerPort: 9000
resources:
requests:
memory: "2Gi"
limits:
memory: "2Gi"
Для развертывания Pinot, как и для рабочих узлов Presto, используется
шаб лон развертывания ReplicationController. После загрузки образа apachepinot/pinot:latest скрипт запускает команду /opt/pinot/bin/pinot-admin.sh QuickStart -type batch, устанавливающую все необходимые компоненты, и добавляет в базу данных несколько таблиц, которые мы сможем использовать в качестве примера данных. Подробности и дополнительные примеры
можно найти в официальной документации. Вы можете настроить объем
ресурсов для Pinot в зависимости от мощности вашей машины, изменяя
параметры в секции resources.
Настройка интеграции Presto и Pinot
Чтобы Presto смог обращаться к Pinot, добавьте к инсталляции Presto новый
каталог. Для этого в файл presto-config-map.yaml нужно добавить еще одну
секцию:
pinot.properties: |
connector.name=pinot
pinot.controller-urls=pinot:9000
pinot.controller-rest-service=pinot:9000
Другие параметры и их возможные значения можно посмотреть в доку-
ментации Presto1.
Чтобы подключить новый каталог к узлу-координатору и к рабочим узлам, добавьте новый элемент в секцию volumeMount файлов presto-coordinator.yaml и presto-workers.yaml:
containers:
- name:
volumeMounts:
- name: pinot
mountPath: "/opt/presto/etc/catalog/pinot.properties"
subPath: pinot.properties
...
volumes:
- name: pinot
configMap:
name: presto-config
items:
- key: "pinot.properties"
path: "pinot.properties"
1
Apache Pinot 71
Запрос из Presto в Pinot
Теперь, когда настройка закончена, выполним пару запросов. Как и в про-шлый раз, будем использовать клиент командной строки Presto, развернутый
вместе с узлом-координатором.
Для начала выведем список таблиц, которые содержатся в базе данных
Pinot:
presto> show tables from pinot.default;
Table
-------------------------
airlinestats
baseballstats
billing
dimbaseballteams
githubcomplextypeevents
githubevents
starbucksstores
(7 rows)
Query 20221019_100155_00006_47zex, FINISHED, 1 node
Splits: 19 total, 19 done (100.00%)
0:17 [7 rows, 217B] [0 rows/s, 12B/s]
В следующем запросе обратимся к таблице baseballstats. Чтобы понять, как Presto выполняет запрос, перед его выполнением посмотрим его план
с помощью оператора EXPLAIN. Наш запрос показывает, сколько раз имя каждого игрока встречается в таблице:
EXPLAIN
SELECT playerName, count(1) \
FROM pinot. default.baseballStats \
GROUP BY playerName;
В результате выполнения этой команды мы увидим длинную простыню
текста, состоящую из вызовов различных компонентов, вложенных друг
в друга. Запрос проходит с самого верхнего уровня (TableScan) на более низ-кие (PinotColumnHandle и GeneratePinotQuery). Ниже приведен этот текст, показывающий последовательность вызовов в виде псевдокода:
TableScan ->
TableHandle ->
expectedColumnHandles ->
PinotColumnHandle{columnName="playerName", ...}
PinotColumnHandle{columnName="count", ...}
layout ->
PinotTableHandle ->
expectedColumnHandles ->
PinotColumnHandle{columnName="playerName", ...}
PinotColumnHandle{columnName="count", ...}
72 Коннекторы
pinotQuery ->
GeneratedPinotQuery
{
query=SELECT "playerName", count(*)
FROM baseballStats
GROUP BY "playerName"
LIMIT 10000
}
Заключение
В этой главе вы научились создавать коннекторы для Presto с использованием Presto SPI и Apache Thrift.
Чтобы написать свой коннектор, вы должны написать код на языке Java, реализующий несколько классов. Эти классы представляют собой компоненты коннектора: классы Plugin и Module, а также классы, обеспечивающие
конфигурацию и ввод-вывод. Кроме того, для каждого из компонентов требуется реализовать несколько классов, обеспечивающих их взаимодействие
с Presto. Независимо от того, является ли ваш источник реляционной базой
данных, коннектор должен представлять его для Presto именно как реляционную БД.
Кроме того, вы узнали, как использовать при взаимодействии Presto и коннектора Apache Thrift – фреймворк, позволяющий разрабатывать масшта-бируемые сервисы, независимые от языков программирования, на которых
разработаны их компоненты. Благодаря использованию этого фреймворка
Presto может взаимодействовать с сервисами, написанными на любых языках, а не только на Java.
Вы также узнали, как подключить Presto к Apache Pinot, распределенной
аналитической БД, которая может работать в режиме реального времени. Вы
запустили на своей машине экземпляр Pinot в поде Kubernetes и подключили
его к кластеру Presto, развернутому в главе 2.
В главе 4 вы узнаете о различных методах подключения клиентов к Presto.
Глава 4
Подключение клиентов
Клиент Presto – это процесс, который отправляет запросы в Presto и каким-то
образом получает и отображает результаты запроса. Есть множество способов обращения к Presto – через REST API, ODBC, JDBC... Кроме того, существует множество библиотек для разных языков программирования – R, Python, Node.js и др.
Эта глава состоит из двух частей. В первой части вы узнаете, как развернуть клиент Presto. Клиент Presto можно запустить как отдельный процесс
на одном из существующих узлов кластера, но мы развернем клиентский
процесс на выделенном узле, чтобы смоделировать внешнее приложение, обращающееся к кластеру Presto. Мы научимся работать с Presto через REST
API, ODBC, JDBC, а также с помощью библиотек Python, R, Node.js. Процедура
развертывания клиента примерно одинакова для всех способов обращения
к кластеру, поэтому вы легко можете воспроизвести эту процедуру для вашего любимого языка программирования.
Во второй части мы создадим простой веб-сервис на языке Python, который запрашивает данные в Presto и показывает результат в виде простой
информационной панели. Цель этого упражнения – продемонстрировать
возможности клиента Presto.
Настройка окружения
Чтобы добавить в кластер Presto клиентский узел, прежде всего требуется
настроить окружение. Разверните следующие три компонента, которые вы
уже научились разворачивать в главе 3:
клиент Presto;
Docker-образ, содержащий клиент Presto;
узел кластера Kubernetes, в котором запущен образ с клиентским про-граммным обеспечением.
Все конфигурационные файлы, использованные в этом разделе, можно
взять в папке 04 нашего репозитория.
74 Подключение клиентов
Клиент Presto
Клиент подключается к Presto, запускает запросы и отображает результат.
Вы можете написать клиентское приложение на любом языке, который вам
нравится. Сохраните код вашего приложения в папке, которая будет смонти-рована на узле Kubernetes как отдельный том. Если хотите, можете написать
веб-приложение, к которому можно обращаться извне по протоколу HTTP.
Позже в этой главе мы научимся разворачивать и запускать такие приложения. Можете также написать консольное клиентское приложение и работать
с ним через терминал.
Docker-образ
Docker-образ содержит окружение, необходимое для запуска клиента Presto.
При сборке образа используйте такой шаблон для Dockerfile 1: FROM <basic_image>
RUN apt-get update && \
<install required libraries>
Например, если вы хотите применять клиент, написанный на R, используйте такой Dockerfile:
FROM r-base
RUN apt-get update && \
apt install -y build-essential libcurl4-gnutls-dev libxml2-dev libssl-dev RUN R -e "install.packages('RPresto')"
Чтобы собрать готовый образ, введите в терминале команду
docker build -t client-presto .
Узел Kubernetes
Чтобы развернуть узел Kubernetes, напишите конфигурационный .yaml-файл, который просто запускает собранный на предыдущем шаге Docker-образ: apiVersion: apps/v1
kind: Deployment
metadata:
1 В главе 1 авторы используют CentOS, который базируется на rpm и применяет
менеджер пакетов yum, а здесь вдруг рекомендуют использование дистрибутива
семейства Debian с менеджером apt. Данная книга ни в коем случае не может использоваться как пособие для обучения работе с Docker и Linux – эти премудрости
придется изучать по другим источникам. – Прим. перев.
Настройка окружения 75
name: my-client
labels:
app: my-client
spec:
replicas: 1
selector:
matchLabels:
app: my-client
template:
metadata:
labels:
app: my-client
spec:
containers:
- name: my-client
image: client-presto:latest
imagePullPolicy: Never
command: [ "sleep" ]
args: [ "infinity" ]
Команда sleep infinity гарантирует, что клиент никогда не завершится.
Альтернативный вариант – запустить клиентское приложение как сервис, соответственно изменив и файл конфигурации Kubernetes.
Чтобы запустить в вашем контейнере произвольный скрипт, смонтируйте
к нему том:
spec:
containers:
- name: client
# add details based on the client type
volumeMounts:
- name: scripts
mountPath: "/path/in/the/container"
volumes:
- name: scripts
hostPath:
path: "/path/to/scripts/in/the/host"
И наконец, установите ваше клиентское приложение в кластер Kubernetes следующей командой:
kubectl apply -f client.yaml --namespace presto
После того как клиент установлен, подключитесь к запущенному контейнеру и выполните скрипт из смонтированной папки.
Предупреждение относительно монтирования
Для монтирования тома со своим кодом используйте директиву volumeMount. Если же
вы собираетесь использовать этот код в промышленной среде, то скопируйте этот код
непосредственно в файловую систему Docker-образа, вместо того чтобы монтировать
папку с этим кодом через .yaml-файл.
76 Подключение клиентов
Подключение к Presto
В этом разделе мы расскажем, как использовать клиентские библиотеки, чтобы подключиться к кластеру Presto. Мы продемонстрируем подключение из
нескольких языков. Пример с вашим любимым языком можно найти в папке
04/language нашего репозитория. Если вы не используете какой-то язык из
рассмотренных нами, просто пропустите соответствующий раздел.
REST API
Один из компонентов узла-координатора Presto – HTTP-сервер, выступа-ющий как точка доступа для REST API. Все взаимодействия между клиентом и сервером Presto (точнее, между клиентом и узлом-координатором) осуществляются именно по протоколу HTTP. Чтобы использовать REST API, вы должны указать URL координатора, запрос и минимальный контекст, состоящий из указания каталога и схемы. Контекст передается в виде HTTP-заголовков:
curl -d "SELECT * FROM customer LIMIT 5" \
--request POST \
-s "presto-coordinator:8080/<endpoint>" \
-H "X-Presto-User: client" \
-H "X-Presto-Catalog: tpch" \
-H "X-Presto-Schema: sf1"
Заголовок -H "X-Presto-User: client" указывает имя пользователя. По умолчанию Presto допускает любое имя, поэтому совершенно не важно, что вы
укажете. В главе 7 мы покажем, как сконфигурировать в Presto различных
пользователей.
Если вызов API завершился успешно, Presto возвращает JSON-объект, который содержит часть данных в поле data и поле nextUri. Чтобы получить все
данные, следующими вызовами вы должны обращаться к URI, указанному
в этом поле, пока не получите ответ, в котором такого поля не будет: curl -s "<URL contained in nextUri>"
Чуть ниже будет пример, показывающий, как работает клиентский сеанс.
REST API экспортирует множество функций:
/v1/node – возвращает информацию об узле;
/v1/statement – запускает запрос;
/v1/query – возвращает информацию (метрики мониторинга) о последних выполненных запросах. Можно указать идентификатор кон-
кретного запроса, и тогда вы получите информацию только об этом
конкретном запросе;
/v1/thread – возвращает информацию о потоке;
/v1/task – возвращает информацию о задаче.
Подключение к Presto 77
Код ниже1 демонстрирует полноценный пример обращения к Presto через
REST API на языке Python:
1) делаем первый вызов:
import requests
import time
url = "http://presto-coordinator:8080/v1/statement"
headers = {
"X-Presto-User" : "client", \
"X-Presto-Catalog": "tpch", \
"X-Presto-Schema": "sf1"
}
sql = "SELECT * FROM customer LIMIT 5"
resp = requests.post(url, headers=headers, data=sql)
json_resp = resp.json()
2) в цикле извлекаем данные, а затем, если найдено поле nextUri, обраща-емся по указанному в нем адресу, пока не получим ответа, в котором
этого поля нет. Чтобы дождаться данных, используем time.sleep(0.5).
Если за это время ваш клиент не успевает получить данные, увеличьте
промежуток.
while True:
json_resp = resp.json()
data = json_resp['data']
for i in range(0, len(data)): print(data[i])
if not 'nextUri' in json_resp: break
time.sleep(0.5)
resp = requests.get(json_resp['nextUri'])
На самом деле API Presto принимает множество дополнительных параметров, описание которых вы можете найти в документации2.
Python
Установите библиотеку presto-python-client. Для этого в Dockerfile добавьте
следующую команду:
pip install presto-python-client
Затем в коде Python установите соединение с Presto:
import prestodb
conn=prestodb.dbapi.connect(
1 Код в книге и в репозитории содержит грубые ошибки. Для русского перевода код
переписан. – Прим. перев.
2 .
78 Подключение клиентов
host='presto-coordinator',
port=8080,
user='client',
catalog='tpch',
schema='sf1',
)
Запустите запрос – и получите возвращаемые им данные:
cur = conn.cursor()
cur.execute('SELECT * FROM customer LIMIT 5')
rows = cur.fetchall()
R
Установите библиотеку RPresto. Для этого добавьте в Dockerfile следующую
команду:
RUN R -e "install.packages('RPresto')"
Чтобы обратиться из R к Presto, сначала импортируйте библиотеки:
library(RPresto)
library(DBI)
Затем подключитесь к серверу Presto:
con <- DBI::dbConnect(
drv = RPresto::Presto(),
host = "presto-coordinator",
port = 8080,
user = "r-client",
catalog = "tpch",
schema = "sf1"
)
Для выполнения запросов используйте функцию DBI::dbGetQuery():
DBI::dbGetQuery(con, "SELECT * FROM customer LIMIT 5") JDBC
Используйте драйвер com.facebook.presto.jdbc.PrestoDriver. Если вы приме-няете Maven для сборки проекта, добавьте в pom.xml следующие зависимости:
<dependencies>
<dependency>
<groupId> com.facebook.presto</groupId>
<artifactId> presto-jdbc</artifactId>
<version> 0.276</version>
</dependency>
</dependencies>
Подключение к Presto 79
Если вы не используете Maven, то загрузите файл presto-jdbc-0.276.jar с веб-сайта Presto и скопируйте его в одну из папок, перечисленных в $CLASSPATH.
Используйте самую свежую версию драйвера – на момент написания книги
это версия 0.276.
В коде приложения запишите URL сервера Presto и установите соединение, используя экземпляр Connection:
import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.SQLException;
String url = "jdbc:presto://presto-coordinator:8080/tpch/sf1"; try {
Class.forName("com.facebook.presto.jdbc.PrestoDriver");
Connection connection =
DriverManager.getConnection(url, "test", null);
}
catch(SQLException e){
e.printStackTrace();
}
catch(ClassNotFoundException e){
e.printStackTrace();
}
В строке URL укажите каталог (tpch) и схему (sf1). Теперь вы можете выполнить запрос:
String sql = "SELECT * FROM customer LIMIT 5";
try{
Statement statement = connection.createStatement();
ResultSet rs = statement.executeQuery(sql);
while (rs.next()) {
System.out.println(rs.getString(1));
}
}
catch(SQLException e){
e.printStackTrace();
}
Этот пример печатает на экране значение первого поля каждой полученной строки.
Node.js
Установите библиотеку presto-client:
npm install -g presto-client
В коде приложения импортируйте библиотеку и создайте новый экземп-
ляр Client():
80 Подключение клиентов
var presto = require('presto-client');
var client = new presto.Client({
user: 'myuser',
'host': 'http://presto-coordinator',
port: '8080'
});
И затем выполните запрос:
client.execute({
query: 'SELECT * FROM customer LIMIT 5',
catalog: 'tpch',
schema: 'sf1',
source: 'nodejs-client',
data: function(error, data, columns, stats){ console.log(data); }, success: function(error, stats){console.log(stats);},
error: function(error){console.log(error);}
});
ODBC
ODBC, Open Database Connectivity, – открытый стандарт API для доступа к любым базам данных. Ведущие производители СУБД, такие как Oracle, Microsoft, IBM, поставляют ODBC-драйверы для своих платформ, что позволяет
разработчикам работать с этими платформами одинаково1 и не зависеть от
их внутреннего устройства.
Чтобы создать ODBC-клиент для Presto, для начала следует установить
ODBC-драйвер Presto. Таких драйверов несколько; в наших примерах мы
использовали драйвер CData2.
Чтобы этот драйвер заработал, необходимо установить пакеты unixodbc, unixodbc-dev и tdsodbc. В Ubuntu и других дистрибутивах, основанных на Debian, это можно сделать следующей командой:
apt-get install -y unixodbc unixodbc-dev tdsodbc
Затем загрузите драйвер CData для Liniux, установите пакет и активируйте
лицензию:
dpkg -i PrestoODBCDriverforUnix.deb
cd /opt/cdata/cdata-odbc-driver-for-presto/bin/
./install-license.sh
Потом установите клиентскую библиотеку для вашего любимого языка, которая работает с Presto через ODBC. Например, для Python установите
библиотеку pyodbc:
pip install pyodbc
1 Последовательность вызовов функций ODBC для любой СУБД действительно одна
и та же, но не надо забывать, что синтаксис запросов может существенно отличаться от платформы к платформе. – Прим. перев.
2
Разработка клиентской информационной панели на Python 81
Установите соединение с Presto:
import pyodbc
conn=pyodbc.connect("DRIVER=CData ODBC Driver for Presto;\
server=presto-coordinator;Port=8080;user=client;\
catalog=tpch;schema=tiny")
И наконец выполните запрос:
cur = conn.cursor()
cur.execute('SELECT * FROM customer LIMIT 5')
rows = cur.fetchall()
print(rows)
Прочие клиентские библиотеки для Presto
Presto поддерживает множество клиентских библиотек на самых разных
языках, включая C, Go, PHP и Ruby. Процедура подключения к Presto при
помощи любого из перечисленных языков очень похожа на те, что мы рассмотрели в этом разделе. Загрузить шаблон приложения на интересующем
вас языке можно из репозитория Presto на GitHub1. Например, образец кода
на Go можно взять в разделе репозитория, посвященном Go2.
Разработка клиентской
информационной панели на Python
В этом разделе мы разработаем с нуля простую информационную панель.
Источником данных, разумеется, будет Presto, а клиентский код будет написан на Python. Панель представляет собой веб-приложение, которое подключается к Presto, запускает запрос, а затем на двух графиках показывает
полученные данные.
Код, который мы разработаем в этом разделе, можно скачать из нашего
репозитория; он находится в папке 04/client-app.
Настройка клиента
Мы развернем наш код как узел Kubernetes. Он будет представлять собой
веб-приложение с доступом по HTTP. Для реализации нам понадобится несколько библиотек:
1 .
2
82 Подключение клиентов
Streamlit – превращает скрипт на Python в веб-приложение;
Altair – библиотека визуализации данных;
Pandas – популярная библиотека для обработки данных.
Чтобы сконфигурировать наше приложение, выполним несколько шагов.
1. Создайте Dockerfile, который копирует в образ код приложения, устанавливает необходимые библиотеки, экспортирует порт, на котором
по умолчанию ожидают подключения приложения на Strreamlit, и за-
пускает приложение:
FROM python:3.8.6
WORKDIR /app
COPY app .
RUN pip install --upgrade pip
RUN pip install presto-python-client
RUN pip install streamlit altair pandas
EXPOSE 8501
CMD ["./run.sh"]
Скрипт run.sh просто запускает Streamlit-сервер:
#!/bin/bash
streamlit run app.py --browser.serverAddress 0.0.0.0
2. Создайте Docker-образ, запустив в папке, где находится Dockerfile, следующую команду:
docker build -t client-app .
3. Создайте из Docker-образа узел Kubernetes. Для этого надо проделать ту
же процедуру, которую вы уже однажды проделали для Python-клиента.
Монтировать внешний том не надо, поскольку код приложения уже на-
ходится внутри образа. И не забудьте открыть порт для доступа извне.
spec:
containers:
- name: client-app
image: client-app:latest
...
ports:
- containerPort: 8501
4. Создайте сервис для веб-приложения:
---
apiVersion: v1
kind: Service
metadata:
name: client-service
labels:
Разработка клиентской информационной панели на Python 83
app: client-service
spec:
ports:
- port: 8501
protocol: TCP
targetPort: 8501
type: LoadBalancer
selector:
app: client-app
Сервис выставляет наружу тот же порт, что и оригинальный веб-сервер.
Чтобы сервис был доступен приложениям вне кластера Kubernetes, мы
используем балансировщик нагрузки (LoadBalancer).
5. Разверните созданный узел в кластер presto:
kubectl apply -f client-app.yaml --namespace presto
6. Запустите браузер и откройте страницу http://localhost:8501.
Разработка панели
Мы разработали веб-приложение, которое подключается к Presto, запускает запрос и показывает полученные данные в виде графиков. Цель нашего
упражнения – показать потенциал технологии, а вовсе не построить готовое
приложение, выполняющее анализ данных.
Код можно взять в папке 04/client-app/app нашего репозитория. Все приложение состоит из единственного файла app.py, который запускается внутри
веб-сервера Streamlit. Приложение выполняет следующие шаги:
подключается к Presto и выполняет запрос;
обрабатывает результаты запроса;
создает первую диаграмму;
создает вторую диаграмму.
Подключение к Presto и выполнение запроса
Прежде всего импортируем все необходимые библиотеки:
import streamlit as st
import prestodb
import pandas as pd
import altair as alt
Затем устанавливаем подключение к Presto, используя библиотеку prestodb – код подключения можно посмотреть в предыдущем разделе.
Не забывайте про ресурсы!
Если мощности вашей машины не хватает, чтобы работать с большим набором данных, укажите в параметрах подключения к Presto схему tiny из каталога TPC-H. В этой
схеме содержится всего около 10 тысяч строк.
84 Подключение клиентов
Установив соединение, приложение запускает запрос, выводящий товары, отправленные в каждый из дней, отсортированные по дате отправки:
query = """SELECT
count(*) as nitems,
shipdate
FROM
lineitem
GROUP BY shipdate
ORDER BY shipdate ASC
"""
Обработка результатов запроса
Результаты запроса сохраняются во фрейме библиотеки Pandas:
cur.execute(query)
df = pd.DataFrame(cur.fetchall(), columns=['nitems', 'shipdate'])
Чтобы построить диаграммы, приложение разбирает поле shipdate:
df['shipdate'] = pd.to_datetime(df['shipdate'])
df['dayofweek'] = df['shipdate'].dt.day_name()
df['year'] = df['shipdate'].dt.year
df['month'] = df['shipdate'].dt.month_name()
df['weekofyear'] = df['shipdate'].dt.isocalendar().week
В процессе разбора приложение получает дополнительные поля: день недели, год, месяц и номер недели в году.
Построение первой диаграммы
Первая диаграмма показывает динамику продаж за 1992 год. Каждая неделя года представлена на диаграмме линией, отражающей объем продаж по
дням:
df_1992 = df[df['year'] == 1992 ]
Диаграмма состоит из двух частей. На первой части мы видим линию для
каждой недели:
days_of_weeks = ['Sunday', ... ,'Saturday']
bar = alt.Chart(df_1992).mark_line().encode(
x = alt.X('dayofweek:O', sort=days_of_weeks, title=''),
y = alt.Y('nitems:Q', title='nitems'),
color = alt.Color('weekofyear:N',scale=alt.Scale(range=['#C8C8C8']),legend=None)
)
Вторая часть диаграммы показывает средний объем продаж по каждому
дню недели:

Разработка клиентской информационной панели на Python 85
mean = alt.Chart(df_1992).mark_line(color='black').encode(
x = alt.X('dayofweek:O', sort=days_of_weeks, title=''),
y = alt.Y('weekly_count:Q', title=''),
).transform_aggregate(
weekly_count = 'mean(nitems)',
groupby=['dayofweek']
)
Наконец, приложение накладывает полученные изображения друг на дру-га, и получается диаграмма, которую мы хотели увидеть:
chart1 = (bar + mean).properties(
width=300,
height=300,
title='Number of shipped items in 1992'
)
st.altair_chart(chart1)
Для построения диаграммы используется функция st.altair_chart(). Результат показан на рис. 4.1.
Количество проданных товаров в 1992 году
40
35
30
25
о товаров 20
Количеств 15
10
5
0
ца
Вторник
Среда
Четверг
Пятни
Суббота
Воскресенье
Понедельник
День недели
Рис. 4.1 Объемы продаж в 1992 году по дням недели

86 Подключение клиентов
Построение второй диаграммы
Вторая диаграмма показывает «тепловую карту» продаж. По горизонтальной
оси отображаются месяцы, по вертикальной – годы:
chart2 = alt.Chart(df).mark_rect().encode(
x = alt.X('year:O'),
y = alt.Y('month:O', sort= days_of_weeks),
color = alt.Color(
'nitems:Q',
scale=alt.Scale(range=['#F5F5F5','#000000'])
)
).properties(
width=300,
height=300
)
st.altair_chart(chart2)
Результат выполнения этого кода показан на рис. 4.2.
Количество проданных товаров по месяцам
Январь
Февраль
40
Март
Апрель
30
Май
о товаров
Июнь
Месяц
Июль
20
Количеств
Август
Сентябрь
10
Октябрь
Ноябрь
Декабрь
1992
1993
1994
1995
1996
1997
1998
Год
Рис. 4.2 «Тепловая карта» продаж
Заключение
В этой главе мы научились использовать Presto, включая работу через REST
API и JDBC, а также через клиентские библиотеки Python, R и Node.js. Процедура практически одинакова для всех языков: собираем Docker-образ, де-
Заключение 87
лаем из него узел Kubernetes и запускаем этот узел, чтобы получить данные
из Presto.
Мы также создали полноценное клиентское приложение, которое обращается за данными к Presto и показывает простые отчеты. Это приложение
обращается к каталогу TPC-H и формирует диаграммы, показывающие количество продаж.
В главе 5 мы узнаем, как построить хранилище-озеро и как выполнять
в нем аналитические запросы.
Глава 5
Open Data Lakehouse
Итак, вы знаете, как подключить Presto к озеру данных с использованием
стандартных коннекторов, например MySQL или Apache Pinot. Вы знаете, как самостоятельно создать коннектор к любому источнику данных, используя JDK и библиотеки Presto. Наконец, вы знаете, как написать клиентское
приложение, которое подключится к Presto и выполнит запросы. И теперь
пришло время применить все эти знания на практике и рассмотреть типичный сценарий использования Presto для управления большими данными.
Этот сценарий включает в себя и обработку данных, и конкурентный доступ
к данным, и разграничение доступа.
В этой главе мы узнаем, что такое хранилище-озеро, и построим такое
хранилище. В первой части главы мы опишем архитектуру современного
хранилища, подробно рассматривая все его основные компоненты, а во второй займемся собственно разработкой хранилища с использованием Presto и других открытых платформ.
Появление хранилища-озера
Первые озера данных базировались преимущественно на распределенной
файловой системе HDFS (Hadoop Distributed Filesystem). Они показали всем, на что способны технологии обработки больших данных, и в результате многие компании стали разрабатывать хранилища данных с использованием
новых архитектурных решений. Эти хранилища включали и озеро данных, и традиционные хранилища структурированных данных, а также конвейеры
обработки данных и формализованные процессы. Правда, первые такие хранилища были весьма сложными, ненадежными и дорогостоящими, да к тому
же скорость обновления данных оставляла желать лучшего1.
1 Michael Armbrust, Ali Ghodsi, Reynold Xin, and Matei Zharia, “Lakehouse: a new ge-ne ration of open platforms that unify data warehousing and advanced analytics,” Proceedings of CIDR (2021), .
Появление хранилища-озера 89
Владельцы хранилищ пытались расширить функциональность как озера
данных, так и структурированного хранилища, но существенных успехов на
этом пути добиться не удалось.
Такие платформы, как Presto, позволили существенно поднять производительность выполнения запросов над данными из озера. Однако сложные процессы обработки и трансформации по-прежнему служили постоянными источниками ошибок, например перезаписи данных или падения конвейеров
из-за сбоев. Требовались весьма сложные средства контроля этих процессов, и все равно время от времени в хранилище появлялись или поврежденные
файлы, или устаревшие данные.
Предпринимались множественные попытки добавить машинное обучение
на хранилищах структурированных данных, например Snowflake for AI and ML1 или ML in Amazon RedShift2, но повсеместной практикой это так и не
стало: многие инструменты машинного обучения до сих пор рассчитаны на
прямой доступ к файлам.
Озера данных появились в конце 2000-х. С тех пор мы стали свидетелями
огромного прогресса в распределенных и облачных вычислениях, а также
в системах искусственного интеллекта3,4. Все это расширяет возможности
использования озер данных и гарантирует, что они никуда не денутся.
Инженеры продолжают искать способы упростить управление озерами
данных. В середине 2010-х годов появилось несколько технологий и протоколов, позволяющих выполнять над данными в озере полноценные ACID-транзакции, эффективно обновлять и версионировать данные.
Термин «хранилище-озеро» (data lakehouse) появился в конце 2020 – начале 2021 года. Его ввели в обиход Майкл Армбруст (Michael Armbrust) и его
коллеги из компании Databricks. Этот термин означает унифицированную
систему управления поверх озера данных. Хранилище-озеро объединяет
преимущества озера данных и традиционного структурированного хранилища, обеспечивая обработку всех традиционных типов нагрузки. Оно позволяет как выполнять ad hoc запросы при помощи SQL, так и запускать
всевозможную продвинутую аналитику, включающую методы машинного
обучения и искусственного интеллекта. Сегодня «хранилище-озеро» – вполне зрелый подход, обеспечивающий разработку полноценных систем хранения, управления и обработки данных5.
1
2
3 Alex Krizhevsky, Ilya Sutskever, and Geoffrey E. Hinton, “mageNet classification with deep convolutional neural networks,” NIPS’12: Proceedings of the 25th International Conference on Neural Information Processing Systems 1 (December 2012): 1097–1105.
4 Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N.
Gomez, Łukasz Kaiser, and Illia Polosukhin, “Attention is all you need,” NIPS’17: Proceedings of the 31st International Conference on Neural Information Processing Systems (December 2017): 6000–6010.
5 Matt Bornstein, Jennifer Li, and Martin Casado, “Emerging Architectures of Modern Data Infrastructure,”Andreessen-Horowitz, 2020, .

90 Open Data Lakehouse
Архитектура хранилища-озера
На рис. 5.1 изображена архитектура хранилища-озера. Основными его ком-понентами являются:
озеро данных ( data lake), которое в свою очередь состоит из табличного
формата ( table format), формата файлов ( file format) и системы хранения
( file store) (объяснения этих терминов будут даны чуть ниже);
инструментарий машинного обучения;
движок SQL-запросов;
механизм управления метаданными;
механизм стратегического управления данными ( data governance).
В озеро данных загружаются данные из самых разных источников – структурированных, слабоструктурированных и неструктурированных. Эти данные используются всеми видами аналитических приложений – системами
отчетности, информационными панелями и др.
Хранилище-озеро
Стратегическое управление данными
Отчеты
Озеро данных
Data science
Поставка
Информацион
Загрузка
и машинное обучение
Табличный
данных
ные панели
формат
Управление
метаданными
Выгрузки данных
Формат файла
Приложения
Движок
СХД
SQL-запросов
Рис. 5.1 Архитектура хранилища-озера
Поскольку эта книга посвящена платформе Presto, предназначенной для
выполнения SQL-запросов, мы сосредоточимся именно на SQL, а машинное обучение и искусственный интеллект пока оставим в стороне. Таким
образом, мы будем рассматривать четыре компонента хранилища-озера: собственно озеро; движок SQL-запросов; метаданные, позволяющие движку
получать доступ к данным; механизм стратегического управления данными, обеспечивающий качество данных. Под качеством данных мы понимаем их
доступность, согласованность и удобство использования. Оставшаяся часть
этой главы будет посвящена описанию функций каждого из этих компонентов и их взаимодействию.
Озеро данных 91
Озеро данных
Как мы говорили в главе 1, озеро данных – это место, где компания сохраняет все свои данные. Это важнейший компонент хранилища-озера. Озеро
данных состоит из системы хранения, форматов файлов и форматов таблиц.
Озеро должно обеспечивать возможность дешевого хранения данных и обращения к этим данным как к набору байтов.
Система хранения
Система хранения – это программно-аппаратное решение, которое, собственно, и обеспечивает хранение данных. Ей все равно, какие данные хранить; любые данные для нее – просто последовательность байтов. Системы
хранения представляют собой или файловые системы (например, HDFS), или объектные хранилища. Наиболее популярные объектные хранилища –
Amazon S3 (Simple Storage Service), Google Cloud Storage, Azure Blob Storage.
Мы будем использовать в качестве объектного хранилища MinIO1, поскольку
его можно развернуть локально, не платя за услуги облачных провайдеров.
Объектные хранилища и файловые системы
Объектное хранилище – то же самое хранилище файлов, но, в отличие от традиционных файловых систем, файлы хранятся в плоской структуре типа «ключ–значение»
безо всякой иерархии. Плоская структура облегчает реализацию хранения, а также
процедур извлечения и изменения данных. Объектные хранилища, в отличие от файловых систем, не имеют какой-либо фиксированной структуры. Так же, как и файловые системы, объектные хранилища позволяют хранить абсолютно любые данные.
Для обращения к объектным хранилищам используются всего четыре операции: PUT, GET, DELETE и LIST.
Системы хранения масштаба предприятия имеют огромные объемы – от
терабайтов (212) до экзабайтов (218). Разумеется, при таких объемах очень
важна стоимость хранения.
Форматы файлов
Хоть с точки зрения системы хранения файл – это просто последовательность байтов, с точки зрения озера в целом файл – это данные в каком-то из
общепринятых форматов. Типичные форматы для современных озер дан-
ных – Apache Parquet и ORC. Оба этих формата колоночные, оба позволяют
эффективно хранить структурированные данные, и оба обладают двумя важ-ными чертами.
1
92 Open Data Lakehouse
Во-первых, в каждом файле вместе с данными хранятся и метаданные
(например, имена колонок, диапазоны значений и т. д.). Это позволяет SQL-движку быстро определить, нужен ли данный конкретный файл для выполнения запроса, не читая данные в этом файле.
Во-вторых, оба формата – колоночные, то есть в каждом файле хранятся
значения конкретной колонки (или набора колонок). Поскольку значения, лежащие в одной колонке, относятся к одному и тому же типу данных, такой
формат позволяет крайне эффективно сжимать данные, уменьшая объем
файлов.
Табличные форматы
Хранение данных в таких форматах, как Parquet или ORC, весьма эффективно. Однако наличие единственного набора файлов для хранения данных
одной таблицы ограничивает возможности озера. Такой подход не позволяет
хранить историю эволюции схемы данных и историю изменения самих данных. Дополнительные метаданные, обеспечивающие расширенные возмож-
ности управления данными, называются табличным форматом.
Табличные форматы обеспечиваются дополнительным набором метадан-
ных поверх форматов файлов. Они предоставляют новые возможности работы с таблицами, такие как ACID-транзакции, версионирование данных, управление эволюцией схемы данных. Кроме того, табличный формат повышает общую производительность озера, позволяя использовать эффективные алгоритмы обновления данных. Табличный формат позволяет также
оптимизировать хранение, например выбирать правильный размер файла
(зачастую файлы большего размера обрабатываются эффективнее) и класте-ризовать данные (то есть совместно размещать данные нескольких колонок
или нескольких таблиц).
Обычно табличные форматы состоят из дополнительных метаданных, рас-полагающихся в хранилище данных, и протоколов их использования. Можно
реализовать табличные форматы и по-другому, но обсуждение подходов к их
разработке выходит за рамки книги. Распространенные табличные форматы
дополняют файлы данных в озере (например, в формате Parquet) файлами
метаданных.
Все существующие табличные форматы добавляют журнал, в котором со-
храняется информация обо всех операциях обновления данных, представ-ленных файлами в системе хранения. На рис. 5.2 показаны примеры реализации табличных форматов. В деталях реализация табличных форматов
отличается, например форматом журнала.
Самые распространенные табличные форматы сегодня – это Apache Hudi, Apache Iceberg и Delta Lake. У них у всех открытый исходный код, и на основе
каждого из них есть коммерческие продукты. Базовая функциональность, такая как ACID-транзакции и версионирование данных, во всех форматах
реализована практически одинаково. Устройство этих форматов отличается
незначительно, и отличия обусловлены особенностями сценариев, в которых
предполагается их применение.
