Client protocol: REST-протокол, polling, nextUri
В прошлых уроках модуля мы разобрали внутреннее устройство кластера: координатор, воркеры, discovery. Теперь посмотрим на кластер снаружи — глазами клиента. Как Trino CLI, JDBC-драйвер или Python-клиент на самом деле общаются с координатором? Что физически летает между клиентом и кластером, когда вы выполняете запрос?
Ответ — client protocol, клиентский протокол Trino. Это REST-протокол поверх HTTP, и устроен он не так, как может показаться. Многие представляют себе общение с базой так: послал запрос, подождал, получил весь результат. Trino работает иначе — через опрос (polling). Понять этот протокол важно по двум причинам: во-первых, это объясняет, почему результаты приходят постепенно и почему запрос можно отменить; во-вторых, без этого нельзя понять, как устроены все клиенты Trino и как при необходимости обратиться к Trino напрямую из своего кода.
Почему не «послал и жди»
Сначала разберёмся, почему простая модель «послал запрос, заблокировался, получил весь ответ» для Trino не годится.
Trino выполняет аналитические запросы, которые могут идти долго — секунды, минуты, иногда дольше — и возвращать большие результаты. Если бы клиент просто отправлял запрос и держал одно HTTP-соединение открытым до самого конца, возникло бы несколько проблем. Долгое висящее соединение хрупко: его легко обрывают сетевые таймауты, прокси, балансировщики. Клиент при этом «слеп» — он не знает, что происходит: запрос ещё планируется, уже исполняется, наполовину готов? И отменить запрос в такой модели тоже непросто.
Trino решает это иначе: вместо одного длинного соединения — серия коротких HTTP-запросов. Клиент не висит в ожидании, а периодически опрашивает координатор: «как там мой запрос? есть данные?». Это и называется polling — опрос. Каждый отдельный HTTP-запрос короткий и быстрый, а из их последовательности складывается получение результата.
REST-протокол: общая механика
Client protocol Trino — это REST поверх HTTP. Клиент и координатор обмениваются обычными HTTP-запросами и ответами; тело ответов — JSON. Никакого специального бинарного протокола базы данных нет, и это сознательно: HTTP universально, проходит через прокси и балансировщики, его легко реализовать на любом языке.
Разберём жизненный цикл одного запроса по шагам.
Шаг 1. Клиент отправляет SQL. Клиент делает HTTP-запрос на координатор и передаёт в теле текст SQL-запроса. Вместе с ним идут заголовки с контекстом сессии: имя пользователя, каталог и схема по умолчанию, параметры сессии.
Шаг 2. Координатор отвечает первым ответом. Координатор регистрирует запрос (присваивает query id) и сразу отвечает. Но в этом первом ответе обычно ещё нет данных — запрос только начал жить. Ключевое, что есть в ответе, — поле nextUri.
Шаг 3. Клиент опрашивает по nextUri. nextUri — это URL, по которому клиент должен сделать следующий HTTP-запрос, чтобы узнать продолжение. Клиент идёт по nextUri и получает очередной ответ. В нём — снова статус, возможно, порция данных, и снова nextUri для следующего шага.
Шаг 4. Цикл опроса. Шаг 3 повторяется. Клиент раз за разом ходит по очередному nextUri, получая ответы. Пока запрос исполняется, ответы могут приходить без данных (просто «ещё работаю»). Когда у координатора готовы данные, очередной ответ несёт порцию результата — страницу (page) строк.
Шаг 5. Завершение. В какой-то момент запрос завершён, все данные отданы. В финальном ответе поля nextUri уже нет — это сигнал клиенту: опрашивать больше нечего, запрос закончен. Если запрос упал с ошибкой, в ответе будет описание ошибки.
Ключевая идея протокола — поле nextUri. Пока в ответе есть nextUri, история не закончена: клиент обязан пойти по нему дальше. Как только nextUri пропал — запрос завершён. Всё общение клиента с Trino после отправки SQL — это движение по цепочке nextUri.
Зачем результат приходит страницами
Обратите внимание на важную деталь шага 4: данные приходят не одним куском, а страницами — порциями строк, по странице на ответ. Это не случайность, а намеренное свойство протокола, и оно даёт несколько практических преимуществ.
Потоковая выдача результата. Клиенту не нужно ждать, пока готов весь результат. Как только координатор собрал первую порцию строк, он отдаёт её в ближайшем ответе. CLI начинает печатать строки на экран, ещё пока запрос не завершён целиком. Для больших результатов это сильно лучше, чем ждать всё разом.
Контроль памяти на клиенте. Клиент обрабатывает результат порция за порцией и не обязан держать в памяти весь набор сразу. Запрос может вернуть миллионы строк, а клиент потребляет их страницами по мере поступления.
Управляемая нагрузка. Polling позволяет клиенту задавать темп. Клиент сам решает, когда сделать следующий запрос за очередной страницей; он может притормозить, если не успевает обрабатывать.
Что polling даёт: статус и отмена
Поскольку клиент постоянно опрашивает координатор, протокол естественно даёт ещё две вещи.
Видимость статуса. Каждый ответ при опросе несёт не только данные, но и информацию о состоянии запроса: на какой он стадии, сколько примерно сделано, статистику. Именно поэтому Trino CLI умеет показывать прогресс-бар выполнения, а JDBC-драйвер — отдавать статус. Клиент не слеп: он на каждом шаге опроса узнаёт, как идёт запрос.
Возможность отмены. Раз у запроса есть идентификатор и REST-эндпоинт, клиент может в любой момент послать координатору HTTP-запрос на отмену этого запроса. Координатор получит сигнал, остановит исполнение и освободит ресурсы кластера. Когда вы в CLI прерываете запрос — это именно такой запрос на отмену по протоколу.
Все клиенты говорят на этом протоколе
Важно понять: client protocol — это единственный способ общения с Trino, и все клиенты построены поверх него.
Trino CLI под капотом шлёт SQL координатору и ходит по nextUri, печатая приходящие страницы. JDBC-драйвер прячет тот же протокол за стандартным JDBC API: вызвал executeQuery() — драйвер внутри делает polling по nextUri и отдаёт строки через ResultSet. trino-python-client делает то же самое для Python. BI-инструменты подключаются через JDBC или собственный коннектор — и снова в основе тот же REST-протокол.
Это значит, что обратиться к Trino можно из любого языка, где есть HTTP-клиент, вообще без специальной библиотеки: отправить SQL, разобрать JSON-ответ, идти по nextUri. Официальные клиенты просто делают это удобно, добавляя обработку ошибок, аутентификацию, маппинг типов.
Почему протокол устроен именно так
Стоит отдельно оценить, почему выбор в пользу REST поверх HTTP — это удачное инженерное решение, а не случайность.
Главное достоинство — универсальность HTTP. HTTP понимают все: любой язык программирования, любая платформа, любой сетевой посредник. Прокси, балансировщики нагрузки, корпоративные шлюзы, инструменты мониторинга — всё это умеет работать с HTTP-трафиком из коробки. Будь у Trino собственный бинарный протокол на голом TCP, каждый такой посредник пришлось бы учить его понимать. С HTTP этой проблемы нет: трафик Trino проходит через типовую сетевую инфраструктуру без специальной настройки.
Второе — простота реализации клиентов. Чтобы написать клиент Trino, не нужно реализовывать сложный протокол базы данных с рукопожатиями, бинарными форматами и состоянием соединения. Нужен лишь HTTP-клиент и разбор JSON — а это есть в стандартной библиотеке практически любого языка. Именно поэтому вокруг Trino так много клиентов и интеграций: порог входа для их авторов низкий.
Третье — протокол естественно ложится на stateless-природу Trino, которую мы разбирали в этом модуле. Каждый шаг polling — это отдельный самодостаточный HTTP-запрос, несущий в себе всё нужное, чтобы продолжить с правильного места. Состояние запроса держит координатор, а не висящее соединение. Это делает общение клиента с кластером устойчивым: обрыв одного короткого запроса не фатален, клиент просто повторяет шаг по тому же nextUri.
Платой за универсальность можно считать то, что HTTP с JSON менее компактен, чем специализированный бинарный протокол. Для аналитической нагрузки Trino это приемлемый размен: запросов в единицу времени не миллионы, как в OLTP, а накладные расходы HTTP с лихвой окупаются простотой и совместимостью.
| Аспект протокола | Как устроено |
|---|---|
| Транспорт | HTTP, REST, тело ответов в JSON |
| Модель получения результата | Polling: серия коротких HTTP-запросов, не одно длинное соединение |
| Связующее звено | Поле nextUri в каждом ответе — URL для следующего шага |
| Формат выдачи данных | Страницами (порциями строк), потоково |
| Сигнал завершения | Отсутствие nextUri в ответе |
| Дополнительно | Статус запроса в каждом ответе, отмена по идентификатору |
Если хотите своими глазами увидеть протокол, выполните запрос через CLI с флагом --debug — он покажет служебные детали обмена. Ещё нагляднее — обратиться к координатору напрямую инструментом вроде curl: отправить SQL на REST-эндпоинт и вручную пройти по цепочке nextUri, наблюдая, как из последовательности ответов складывается результат. Задание ниже предлагает это сделать.
Попробуй сам
Сначала запустите запрос через CLI с флагом --debug (docker exec -it trino trino --debug), выполните SELECT count(*) FROM tpch.sf1.orders; и посмотрите на дополнительную служебную информацию, которую CLI печатает про обмен с координатором.
Затем разберите протокол вручную через curl. Отправьте простой запрос на REST-эндпоинт координатора (на статьи /v1/statement в документации trino.io описан точный формат) с заголовком пользователя и текстом SQL в теле. В JSON-ответе найдите поле nextUri. Сделайте curl по этому nextUri, в новом ответе снова найдите nextUri и повторяйте, пока поле не исчезнет. Пронаблюдайте, в каких ответах появляются данные, а в каких — только статус. Запишите своими словами, сколько шагов опроса понадобилось и как вы поняли, что запрос завершён.