INSERT, телеметрия выполнения и сигналы об ошибках. Именно этот протокол используют клиент командной строки, драйвер на C++ и большинство сторонних нативных драйверов.
На этой странице описан сам протокол: формирование пакетов, конечный автомат соединения, согласование версий и тело каждого сообщения, кроме Block. Содержимое пакетов семейства Data (Block, его столбцы и кодирование для каждого типа) — отдельная тема, которой посвящена спецификация формата Native.
Сопутствующая спецификацияЭта страница входит в пару документов и публикуется вместе со спецификацией формата Native. Зоны ответственности чётко разделены: эта страница описывает пакетный и транспортный уровни, а спецификация формата Native — байты внутри пакетов семейства
Data.BlockInfo, поэтому один байт не на своём месте нарушает синхронизацию всего последующего потока. Протокол работает с сохранением состояния, и каждое TCP-соединение обрабатывает только один запрос за раз — мультиплексирование не поддерживается. Целые числа фиксированной ширины хранятся в порядке little-endian.
Обзор
Каждое передаваемое сообщение начинается с кода типа пакета в формате
VarUInt, за которым следует тело; его структура зависит от этого кода и от согласованной версии протокола.
Соединение проходит через три фазы: однократное рукопожатие, затем произвольное число обменов Ping или Query и, наконец, закрытие:
Собственный TCP-протокол всегда передаёт табличные данные в формате Native — вне зависимости от предложения FORMAT в SQL-запросе. Преобразование в RowBinary, CSV, JSON и другие форматы — задача клиента, которую он выполняет после декодирования блоков Native. (HTTP-интерфейс обрабатывается по другому пути в коде, где предложение FORMAT действительно учитывается; HTTP в данном документе не рассматривается.)
Безопасность
Защита на транспортном уровне (TLS)
TLS работает на транспортном уровне, под протоколом. Когда TLS включён, шифруется весь TCP-поток, а сообщения протокола побайтово совпадают независимо от того, используется TLS или нет.Аутентификация
Аутентификация выполняется во время рукопожатия, в сообщенииClientHello. Поля user и password передаются как строки в открытом виде, поэтому учётные данные при передаче защищает только шифрование на транспортном уровне (TLS).
Пустое поле user сервер интерпретирует как настроенного пользователя сеанса по умолчанию, то есть как значение настройки сервера default_session_user (по умолчанию — default), которое может быть переопределено для отдельного listener в секции protocols. Если на сервере в качестве пользователя сеанса по умолчанию задано пустое значение или версия сервера ниже 26.8, запрос с пустым полем user отклоняется с исключением. Обратите внимание, что clickhouse-client никогда не отправляет пустое имя пользователя: он подставляет default на стороне клиента.
Аутентификация SSH по схеме «запрос — ответ» доступна начиная с версии протокола 54466 — см. раздел Аутентификация SSH по схеме «запрос — ответ».
Межсерверный секрет
При выполнении распределённого запроса серверы аутентифицируют друг друга, доказывая знание общего секрета, — при этом сам секрет по сети не передаётся. Каждый пакет Query содержит в поле 4 пакетаQuery 32-байтовый хеш SHA-256 auth_hash, вычисляемый по соли, nonce, настроенному секрету и тексту запроса; принимающий сервер вычисляет хеш заново и сравнивает значения. Эта возможность доступна при наличии флага возможностей INTERSERVER_SECRET (v54441). Внешние клиенты всегда передают в этом поле пустую строку. См. раздел Межсерверная аутентификация.
Версионирование и флаги возможностей
Согласование версии
Во время рукопожатия клиент и сервер сообщают максимальную поддерживаемую ими версию протокола. Согласованной версией становится меньшая из них:Флаги возможностей
Каждая возможность определяется версией протокола, в которой она появилась, и считается активной, если согласованная версия больше или равна этому номеру.Таблица возможностей
Оболочка пакета
Все сообщения, передаваемые по сети, имеют одинаковую внешнюю структуру в обоих направлениях:VarUInt, а не как байт фиксированной длины. Для значений меньше 128 VarUInt даёт тот же самый одиночный байт, однако реализации должны использовать кодирование VarUInt, чтобы сохранить совместимость на случай, если номера типов пакетов в будущем достигнут 128 или превысят это значение.
В справочнике по сообщениям описано только тело каждого пакета — байты, следующие за кодом типа пакета. Поля нумеруются начиная с 1: номер 1 присваивается первому полю тела.
Кадрирование с фрагментацией (v54470+)
Если возможностьCHUNKED_PROTOCOL согласована (см. рукопожатие), каждый передаваемый пакет оборачивается в кадрирование с фрагментацией. Оборачивание выполняется для каждого направления отдельно: направления клиент→сервер и сервер→клиент согласуются независимо, поэтому в итоге могут работать в разных режимах (с фрагментацией или без кадрирования).
Формат передачи данных для каждого пакета:
VarUInt типа пакета находится внутри фрагментированного потока: это первый байт полезной нагрузки пакета (первый байт первого фрагмента), а не отдельный байт, отправляемый перед фреймингом. Полезная нагрузка фрагментов каждого пакета — это полная последовательность [VarUInt packet_type_code][message body] из оболочки пакета. Если клиент оставит тип пакета вне фрагментированного потока, peer прочитает байт типа как первый байт размера фрагмента u32, и соединение рассинхронизируется.
Один пакет может быть разбит на несколько фрагментов, если буфер пишущей стороны заполнится посреди пакета; граница разбиения может оказаться где угодно, в том числе внутри VarUInt типа пакета. Считыватель объединяет полезные нагрузки фрагментов и воспринимает завершающий 4-байтовый ноль как прозрачную границу пакета: он считывает его, но не передаёт дальше компоненту, читающему тела пакетов.
Пакеты без тела тоже оборачиваются: однобайтовый пакет, например Ping или Pong, после согласования разбиения на фрагменты превращается в [u32 size = 1][0x04][u32 0]. Любое упоминание «одного байта в передаваемых данных» в других разделах этой страницы относится к форме до разбиения на фрагменты.
Согласование. ServerHello и Addendum содержат по два поля String — по одному на каждое направление — со значениями из набора {"chunked", "notchunked", "chunked_optional", "notchunked_optional"}:
chunked/notchunked— строгие: сторона требует именно этот режим.- Варианты с
_optional— гибкие: они принимают любой режим, выбранный другой стороной.
На стороне клиента предпочтение клиента для SEND согласуется с предпочтением сервера для RECV, и наоборот.
Порядок во времени. Строки согласования передаются без фрейминга: ClientHello → ServerHello (предпочтения сервера) → Addendum (согласованные значения клиента). Фрейминг включается для всех байтов, отправленных после сброса Addendum. Сам Addendum, а также ClientHello и ServerHello всегда передаются без фрейминга.
Жизненный цикл соединения
В любой момент времени соединение находится ровно в одном из четырёх состояний:HANDSHAKE, READY, READING_RESPONSE или в завершённом. Поскольку протокол не поддерживает мультиплексирование, если клиент отправит новый запрос, не дочитав до конца предыдущий ответ, байты в передаваемых данных перемешаются и поток будет повреждён.
Состояния
Основной сценарий идёт сверху вниз —HANDSHAKE → READY → READING_RESPONSE → READY — с петлёй Ping/Pong, а все переходы при сбоях сходятся в единственное конечное состояние Terminated.
Фаза рукопожатия
На этой фазе выполняется аутентификация и согласуется версия протокола. Она выполняется ровно один раз для каждого соединения — прежде всего остального. TCP-соединение только что установлено, и обмена сообщениями ещё не было. Порядок действий:-
Клиент отправляет
ClientHelloс максимальной поддерживаемой им версией протокола. -
Клиент читает ответ и выбирает дальнейшие действия в зависимости от типа пакета:
-
Если
negotiated_version ≥ 54458(возможностьADDENDUM), клиент отправляетAddendum. Решение принимается на основе согласованной версии, а не версии, объявленной клиентом.
READY; при любой ошибке оно разрывается.
Фаза Ping
Проверка работоспособности на уровне приложения, не зависящая от TCP keepalive. Успешный обмен Ping/Pong подтверждает, что TCP-соединение активно в обоих направлениях, а сервер отвечает на запросы. Ping не хранит состояния и не связан ни с каким запросом, поэтому несколько последовательных Ping никак не зависят друг от друга. Начиная с состоянияREADY, процесс выглядит так:
-
Клиент отправляет
Ping. -
Клиент читает ответ:
Фаза запроса
Клиент отправляет SQL-оператор; сервер в потоковом режиме возвращает блоки результата и телеметрию выполнения. Ответ представляет собой последовательность пакетов, которая завершается ровно одним пакетомEndOfStream или Exception.
Начиная с состояния READY, процесс выглядит так:
Если на любом этапе возникает ошибка, сервер отправляет Exception вместо EndOfStream, и запрос завершается.
-
Клиент отправляет
Queryс уникальнымquery_id(обычно UUID). -
Клиент отправляет внешние таблицы (если они есть), а затем пустой маркер Data. У пустого пакета Data
table_name = "",num_columns = 0,num_rows = 0. Сервер не начинает выполнять запрос, пока не получит этот маркер. Внешняя таблица может быть разбита на несколько пакетовDataс одинаковымtable_name. Первый из них фиксирует схему таблицы, а каждый последующий пакет с этим именем должен содержать те же имена и типы столбцов в том же порядке, иначе сервер отклонит запрос с ошибкойINCORRECT_DATA(см. Data). -
Клиент переходит в состояние
READING_RESPONSEи сбрасывает буфер записи. -
Клиент в цикле читает пакеты ответа и обрабатывает их в зависимости от типа:
EndOfStream или обработанного Exception соединение возвращается в состояние READY. Нарушение протокола или ошибка ввода-вывода приводит к разрыву соединения.
На случае
num_rows == 0 часто спотыкаются новые реализации. Блок без строк — это граничный маркер или заголовок схемы, а не сигнал конца потока. Ответ завершается только пакетом EndOfStream или Exception.Фаза INSERT
Фаза INSERT — это фаза запроса с двумя дополнительными обменами. Клиент отправляет операторINSERT; сервер отвечает блоком схемы с описанием целевой таблицы; клиент потоком передаёт пакеты Data со строками, а затем пустой маркер Data; сервер завершает обмен пакетом EndOfStream или Exception.
Из состояния READY клиент отправляет SQL-запрос INSERT вида INSERT INTO <table> [(<cols>)] VALUES — без встроенного литерала VALUES (...), поскольку данные строк передаются в пакетах Data. Последовательность обмена:
- Клиент отправляет
Query, в полеbodyкоторого содержится SQL-запрос INSERT. - Клиент отправляет внешние таблицы, если они есть (для INSERT это редкость). В отличие от фазы запроса, здесь клиент не отправляет пустой маркер Data. Пакет
QueryсINSERTотправляется с признаком ожидающих данных, поэтому пустой блок конца данных откладывается до шага 5: если отправить его до блока схемы, сервер воспримет его как конец потока строк, завершит INSERT без строк, а затем разберёт первый настоящий пакет со строками как посторонний пакет верхнего уровня. - Клиент считывает пакеты метаданных (TableColumns, Progress, ProfileInfo, Log, ProfileEvents), пока не получит пакет Data со схемой — Block с 0 строк, но с полной структурой столбцов (имена и типы). Блок схемы служит контрактом: строки, которые клиент отправит далее, должны соответствовать этим столбцам.
- Клиент отправляет один или несколько блоков данных. Для каждого блока он записывает
VarUInt(ClientPacket::Data = 2), затемString("")в качестве пустого имени внешней таблицы и, наконец, сам Block. Типы столбцов должны позиционно совпадать со столбцами блока схемы. - Клиент отправляет признак конца ввода: пакет Data с пустым Block (0 столбцов, 0 строк).
- Клиент считывает поток ответа до
EndOfStream(успех) илиException(ошибка).
async_insert = 1, сервер ставит строки в очередь и сбрасывает их на диск в составе пакета. При согласованной версии ≥ 54484 (PROGRESS_IN_ASYNC_INSERT) после завершения сброса сервер отправляет дополнительный пакет Progress, сразу за ним — ProfileEvents вставки, а затем EndOfStream. При версии ниже 54484 сервер этот завершающий Progress не отправляет. Это обычный пакет Progress, однако сервер сбрасывает конвейер запроса до учёта счётчиков записи, поэтому на практике приращение содержит только затраченное время, а статистика по записанным строкам и байтам поступает клиенту через сопутствующие ProfileEvents. Клиенту, который уже обрабатывает чередующиеся пакеты Progress на шаге 6, достаточно принять ещё один пакет.
Соединение возвращается в состояние READY после EndOfStream или обработанного Exception. Нарушения протокола и ошибки ввода-вывода разрывают соединение.
Справочник по сообщениям
Поля перечислены в порядке их передачи по сети. В столбцеType используются следующие обозначения:
VarUInt— беззнаковое целое число переменной длины (см. VarUInt).String— последовательность байтов, перед которой указана длина в формате VarUInt (см. String).UInt8,Int32и т. д. — целые числа фиксированной ширины с порядком байтов little-endian.Bool— один байт:0x00или0x01.
Role указано, кто использует каждое поле:
- client — задаётся внешними клиентами.
- inter-server — имеет смысл только при взаимодействии между серверами; внешние клиенты записывают значение по умолчанию.
- universal — используется обеими сторонами.
ClientHello (тип пакета 0)
Client → Server. Первое сообщение после открытия TCP-соединения.ServerHello (тип пакета 0)
Сервер → клиент. Ответ на ClientHello при успешной аутентификации.
Rule — элемент
password_complexity_rules:
Список отражает политику паролей, настроенную оператором сервера, и носит исключительно рекомендательный характер — сервер не применяет эти правила во время рукопожатия. Клиент, поддерживающий смену или установку пароля, может использовать эти правила, чтобы выявлять ошибки ещё до отправки на сервер не соответствующего им пароля.
Чтобы ограничить потребление ресурсов при работе с враждебным или неправильно настроенным сервером, ограничьте декодированное значение
count 256 записями, а каждую строку pattern и message — 4096 байтами. Значение count, равное 0 (без последующих пар), — типичный случай для серверов без настроенной политики паролей.Addendum (без типа пакета)
Client → Server, доступно приADDENDUM (v54458). Отправляется сразу после завершения рукопожатия. Это не отдельный тип пакета: поля передаются по сети в сыром виде, без префиксного байта с типом пакета.
Переход на фрейминг с разбиением на фрагменты происходит после того, как этот Addendum будет сброшен на диск, — сам Addendum передаётся без фрейминга.
Ping (тип пакета 4)
Client → Server. Тело отсутствует: до применения кадрирования с фрагментацией пакет состоит из одного байта0x04; если согласовано разбиение на фрагменты, этот байт становится однобайтовой полезной нагрузкой фрагмента (см. кадрирование с фрагментацией).
Pong (тип пакета 4)
Server → Client. Тело отсутствует: до кадрирования с фрагментацией пакет состоит из одного байта0x04; если согласовано разбиение на фрагменты, этот байт становится однобайтовой полезной нагрузкой фрагмента (см. кадрирование с фрагментацией).
Исключение (тип пакета 2)
Server → Client. Отправляется, если на любом этапе на сервере возникает ошибка.Query (тип пакета 1)
Client → Server.ClientInfo (встроен в Query)
Client → Server, встраивается в тело сообщения Query (поле 2). Доступность определяетсяCLIENT_INFO (v54032). (Некоторые поля внутри ClientInfo доступны только в более поздних версиях — это отмечено ниже для каждого поля.)
Структура, зависящая от интерфейса (поля 7–12)Поля 7–12 выше относятся к ветви TCP. Если
query_interface (поле 6) не равно TCP, эти поля заменяются другим форматом передачи данных — речь идёт не просто о пропуске необязательных полей, поэтому декодер должен выполнять ветвление по полю 6.query_interface = 2(HTTP): вместо них записываются сведения об HTTP-запросе, переданные сервером, —http_method(UInt8),http_user_agent(String), затемforwarded_for(String, при наличииX_FORWARDED_FOR_IN_CLIENT_INFOv54443),http_referer(String, при наличииREFERER_IN_CLIENT_INFOv54447) и, наконец,http_handler_name(String) иhttp_request_url(String) (оба при наличииHTTP_HANDLER_IN_CLIENT_INFOv54490, именно в таком порядке). Поляos_user/client_hostname/client_name/version_*/protocol_versionотсутствуют. Последние два поля содержат имя сопоставленного HTTP-обработчика, определённого на SQL, и URL запроса, чтобы эти данные доходили до удалённых сегментов (для HTTP-запроса без обработчика они пусты).- Любой другой интерфейс: не записываются ни поля TCP (7–12), ни поля HTTP; поток сразу продолжается полем
quota_key.
quota_key (поле 13) и distributed_depth (поле 14), а затем только для TCP записывается version_patch (поле 15).Эта ветвь важна главным образом для межсерверного трафика, когда инициирующий сервер пересылает запрос, изначально поступивший по HTTP. Декодер, который всегда читает поля TCP, будет неверно интерпретировать такие пакеты, принимая http_method или http_user_agent за quota_key.Межсерверная аутентификация
Поле 4 пакета Query (auth_hash) при передаче по сети не содержит общий секрет кластера. Отправка секрета в исходном виде не только привела бы к ошибке аутентификации, но и раскрыла бы его. Вместо этого сервер, выступающий в роли межсерверного клиента, доказывает, что знает секрет, с помощью хеша SHA-256 с солью:
- Переход в межсерверный режим. Подключающийся сервер сообщает об этом в
ClientHello: полеuserсодержит межсерверный маркер, аpasswordостаётся пустым. Затем он дописывает ещё две строки — имя кластера и только что сгенерированную 32-байтовуюsalt(encodeSHA256от случайного значения) — сразу после полейuser/password, в том же пакетеClientHello. Сервер читает эти две строки до отправкиServerHello, поэтому клиент должен записать их сразу; если сначала дожидатьсяServerHello, возникнет взаимная блокировка, поскольку сервер заблокирован на их чтении. - Получение nonce. Если согласована ревизия
INTERSERVER_SECRET_V2(v54462),ServerHelloсодержит 8-байтовый nonce типаUInt64. - Вычисление хеша. Для каждого пакета Query, кроме
InitialQuery, клиент записывает в поле 4 значениеencodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles + current_roles)— 32-байтовый дайджест. (nonceиспользуется в виде десятичной строки и присутствует, только если согласована ревизия ≥ v54462;external_rolesдописывается, только если согласованаINTERSERVER_EXTERNALLY_GRANTED_ROLES(v54472);current_roles— это сериализованный список имён ролей[VarUInt count][String]*countбез байта присутствия, который дописывается, только если согласованаINTERSERVER_CURRENT_ROLES(v54488) и список присутствует.) ДляInitialQuery, а также если секрет кластера не настроен, клиент записывает пустую строку. - Проверка. Сервер читает поле 4 с ограничением в 32 байта и заново вычисляет ту же конкатенацию, используя собственную копию секрета кластера; если дайджесты не совпадают, соединение отклоняется.
auth_hash.
Setting
Кодируется непосредственно в списке настроек тела Query (пакет Query, поле 3). Список присутствует всегда, независимо от согласованной версии, и завершается элементом Setting с пустым ключом — единственнымVarUInt 0, за которым не следуют ни флаги, ни значение. От согласованной версии зависит только кодирование отдельных настроек; оно определяется SETTINGS_SERIALIZED_AS_STRINGS (v54429).
v54429+ (STRINGS_WITH_FLAGS) — каждая настройка представляет собой следующую тройку:
Если
key пуст, поля 2 и 3 отсутствуют.
До 54429 (BINARY) — каждая настройка имеет вид [String key][type-specific binary value]: поле flags не записывается, а значение кодируется в собственной бинарной форме, соответствующей типу настройки (например, как целое число фиксированной ширины или строка с префиксом длины), а не как десятичная/текстовая строка. Список по-прежнему завершается пустым key. Клиент, работающий с согласованной версией ниже 54429, должен читать и записывать именно эту бинарную форму, а не приведённую выше тройку. (Исключение составляют пользовательские настройки: в обоих вариантах кодирования они всегда содержат flags и строковое значение.)
Поле flags содержит:
0x01— Important: настройка влияет на результаты запроса, и более старые peer не должны молча её игнорировать.0x02— Custom: пользовательская настройка.0x0c— двухбитовое поле уровня, а не самостоятельный флаг:0x00= Production,0x04= Obsolete,0x08= Experimental,0x0c= Beta. Считывайте оба бита (flags & 0x0c): наивная проверкаflags & 0x04ошибочно отнесёт Beta (0x0c) к Obsolete.0x80— HotReload (перезагрузка конфигурации без перезапуска; определён в перечислении флагов, встречается в основном у настроек координации).
Parameter
Параметры запроса для параметризованных запросов, напримерSELECT {x:UInt64}. Кодируются так же, как Setting с установленным флагом Custom (0x02), и точно так же завершаются пустым ключом.
Значение параметра — это его SQL-представление, а не необработанный литерал. Параметры строкового типа нужно передавать уже заключёнными в одинарные кавычки (например, для
{name:String} значение должно быть 'Alice', а не Alice), иначе парсер значений на сервере их отклонит.Data (тип пакета 1 сервер→клиент, тип пакета 2 клиент→сервер)
Используется в обоих направлениях. Передаёт блоки результатов, данные INSERT, внешние таблицы и маркеры конца данных. Формат передачи данных симметричен: в обоих направлениях перед Block передаётся префиксtable_name. Отличается только байт типа пакета.
Маркер конца данных — это пакет с пустым блоком (
0 столбцов и 0 строк), независимо от значения table_name. Сервер считает клиентский пакет Data завершающим, только если декодированный блок пуст (block.empty()); пакет с table_name = "" и непустым блоком — это обычный пакет строк, а не завершающий. Таким образом, поток строк INSERT состоит из последовательности непустых блоков Data и одного пустого блока Data, который его завершает.
Варианты блоков и их назначение описаны в разделе Варианты блоков.
Прогресс (тип пакета 3)
Server → Client. Периодически отправляется во время выполнения запроса. Все поля имеют тип VarUInt, и каждый пакет содержит приращения с момента предыдущего пакетаProgress, а не накопленные итоговые значения. Перед отправкой сервер считывает свои счётчики, атомарно обнуляет их и вычисляет elapsed_ns как интервал времени с момента последней отправки. Поэтому клиент должен суммировать последовательные пакеты локально, чтобы получать текущие итоговые значения: если воспринимать пакет как абсолютное значение, то, как только придёт больше одного пакета, индикатор прогресса начнёт откатываться назад или показывать заниженные значения.
ProfileInfo (тип пакета 6)
Server → Client. Отправляется один раз на запрос, ближе к концу выполнения.Totals (тип пакета 7)
Server → Client. Отправляется для запросов сWITH TOTALS. Формат передачи данных такой же, как у Data: строка table_name (всегда пустая), за которой следует Block. Различается только байт типа пакета.
Extremes (тип пакета 8)
Server → Client. Отправляется, когда включена настройкаextremes. Формат передачи данных совпадает с форматом Data. Блок содержит ровно 2 строки: в строке 0 находятся минимальные значения каждого столбца, в строке 1 — максимальные.
Log (тип пакета 10)
Server → Client. Отправляется, если для запроса включена очередь журналов (настройкаsend_logs_level; см. потоковую передачу журналов).
Оболочка и формат тела такие же, как у Data. Блок имеет фиксированное значение num_columns = 8 и предопределённую схему. Каждая строка журнала занимает одну строку во всех 8 столбцах, а один пакет Log может содержать множество строк.
ProfileEvents (тип пакета 14)
Server → Client. Передаёт счётчики производительности по каждому запросу. Оболочка и формат тела такие же, как у Data. Блок имеет фиксированное значениеnum_columns = 6 и предопределённую схему. Каждому событию соответствует одна строка.
Тип элементов столбца
value может различаться от пакета к пакету: старые серверы отправляют UInt64, новые — Int64. Считывайте строку типа столбца из заголовка блока, а не рассчитывайте на какую-то одну разрядность.TableColumns (тип пакета 11)
Server → Client, доступен приCOLUMN_DEFAULTS_METADATA (v54410). Сервер отправляет этот пакет перед блоком схемы INSERT, чтобы передать метаданные о значениях столбцов по умолчанию, но только если согласованная версия ≥ 54410 и включена настройка input_format_defaults_for_omitted_fields. При версии ниже 54410 пакет не отправляется никогда, поэтому старый клиент не должен его ожидать — сразу приходит блок схемы Data. Клиент версии v54410+ должен быть готов к обоим вариантам: сначала может прийти необязательный TableColumns, а затем блок схемы.
Сжатое тело начиная с v54481При согласованной версии ≥ 54481 (
COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS) сервер записывает оба поля через тот же выходной поток с необязательным сжатием. Поэтому, если в запросе задано compression = true, всё тело TableColumns (external_table + columns_description) находится внутри кадра сжатия, и клиент читает его через соответствующий поток распаковки. Если сжатие в запросе не используется, тело передаётся по сети без сжатия — в точности так, как показано в таблице выше. Это важно для ответов со схемой INSERT: клиент, который переключает обработку сжатия для Log и ProfileEvents, но не для TableColumns, неправильно прочитает ответ, если для запроса включено сжатие.TimezoneUpdate (тип пакета 17)
Server → Client, доступен приTIMEZONE_UPDATES (v54464). Отправляется только в одном месте: в инициализаторе табличной функции input (запрос вида INSERT INTO <table> SELECT ... FROM input('<structure>'), который потоково передаёт строки от клиента). Сразу после отправки блока Data со схемой входных данных (см. фазу INSERT) сервер отправляет TimezoneUpdate с текущим значением session_timezone из контекста запроса, чтобы клиент разбирал строки, которые собирается отправить, с тем же часовым поясом. Сервер не отправляет этот пакет ни при произвольных изменениях через SET session_timezone во время выполнения запроса, ни для того, чтобы сообщить клиенту, как форматировать последующие блоки результата.
Пакет приходит один раз — сразу после блока со схемой входных данных и до того, как клиент начнёт отправлять блоки строк. Декодер, игнорирующий
TimezoneUpdate, ОБЯЗАН всё равно прочитать завершающее значение String, чтобы не нарушить выравнивание данных в потоке.
Аутентификация SSH по схеме challenge-response (типы пакетов 11, 12, 18)
Доступна приSSH_AUTHENTICATION (v54466) и включается только явно. Соединение переходит в режим SSH, когда ClientHello передаёт user = " SSH KEY AUTHENTICATION " + <real_user> (с начальным и конечным пробелами) и password = "". Сервер считывает префикс, отбрасывает его, чтобы получить настоящее имя пользователя, и переключается на схему challenge-response.
Этот процесс заменяет аутентификацию по паролю, причём обмен челленджем и ответом происходит до ServerHello — сервер откладывает свой ответ Hello до успешной аутентификации:
- Клиент отправляет ClientHello с маркерным префиксом SSH и пустым паролем.
-
Клиент отправляет
SSHChallengeRequest(пакет 11). Сервер ещё не отправил ServerHello — сначала он выполняет аутентификацию и на этом шаге блокируется в ожидании этого пакета. -
Сервер отвечает пакетом
SSHChallengeсо случайными байтами (пакет 18). -
Клиент формирует строку для подписи и подписывает именно её, а не исходный челлендж, после чего отправляет
SSHChallengeResponse(пакет 12) с подписью. Подписываемое сообщение — побайтовая конкатенация без разделителей четырёх частей строго в следующем порядке: -
Сервер проверяет подпись по зарегистрированному открытому ключу пользователя, восстанавливая ту же строку
decimal(protocol_version) + default_database + user + challenge. В случае успеха он отправляетServerHello— тот же ответ, что и при аутентификации по паролю, — и рукопожатие продолжается в обычном порядке (Addendum и т. д.); в случае неудачи он возвращаетExceptionи разрывает соединение. Клиент, подписывающий только исходные байты челленджа, не пройдёт аутентификацию.
Это обратный порядок по сравнению с рукопожатием при аутентификации по паролю, где ServerHello следует сразу за ClientHello. При SSH-аутентификации ServerHello отправляется только после проверки подписи, поэтому обмен SSH challenge-response встраивается в рукопожатие ещё до появления ServerHello.
MergeTreeAllRangesAnnouncementResponse (тип пакета 14)
Client → Server, только для межсерверного взаимодействия. Доступен при условииparallel_replicas_protocol_version ≥ 8 (см. VERSIONED_PARALLEL_REPLICAS_PROTOCOL). Внешние клиенты никогда не отправляют этот пакет.
Если согласованная версия протокола параллельных реплик ≥ 8, цикл запроса/ответа initiator при получении MergeTreeAllRangesAnnouncement от follower (тип пакета 15, направление server→client) меняется:
- Follower открывает свой конвейер чтения и отправляет
MergeTreeAllRangesAnnouncementна initiator. - Только если
modeв уведомлении отличается отDefault(WithOrder = 1илиReverseOrder = 2; оба используются для упорядоченного параллельного чтения), initiator отвечает пакетомMergeTreeAllRangesAnnouncementResponse. Приmode = Default = 0initiator ничего не отправляет, а follower не ждёт ответа: в режимеDefaultдиапазоны выдаются вместе с каждымMergeTreeReadTaskRequest, поэтому предварительный список частей не нужен. - Прежде чем отправить первый
MergeTreeReadTaskRequest(серверный пакет16, отправляется в направлении follower→initiator; initiator отвечает пакетомMergeTreeReadTaskResponse, клиентский пакет10), follower блокируется в ожидании ответа (если ответ ожидается). Полученный список частей он использует, чтобы создавать источники только для тех частей, которые принадлежат его потоку#split_i.
8 уведомление отправляется в режиме fire-and-forget независимо от mode, а follower создаёт источники по всем локально известным частям (устаревшее поведение).
Тело
Тело RangesInDataPartsDescription
Тело RangesInDataPartDescription
Тело MergeTreePartInfo
Тело MarkRanges
Справочник по типам пакетов
Client → Server
Server → Client
QueryPlan
QueryPlan — клиентский пакет, используемый только при взаимодействии между серверами. Он следует за пакетом Query, у которого stage равен QueryPlan (7), и содержит один сериализованный поток плана.
До версии
10 поля 2 и 3 отсутствуют. Если у initiator хотя бы одно из ограничений выполнения отличается от значения по умолчанию, он не должен отправлять план в старой структуре: вместо этого он передаёт исходный SQL-запрос, чтобы приёмник старой версии восстановил ограничения из настроек запроса.
Конфигурация
В этом разделе описаны настраиваемые параметры, определяющие поведение соединений по собственному протоколу:- Настройки транспортного уровня — параметры TCP-сокета и тайм-ауты, влияющие на поведение самого TCP-соединения.
- Настройки прикладного уровня — параметры уровня запроса, передаваемые в списке настроек пакета Query. Они определяют, что именно сервер передаёт по сети и как эти данные разбиваются на кадры.
- Настройки, не относящиеся к протоколу — настройки, которые часто путают с настройками протокола, хотя на самом деле они управляют выполнением SQL или хранилищем.
Настройки транспортного уровня
Параметры сокета
Тайм-ауты
Тайм-ауты вложены друг в друга следующим образом:
Ограничения соединений
Соединение, через которое регулярно выполняются запросы, может существовать сколь угодно долго. Через час закрываются только бездействующие соединения, а максимальное время жизни соединения по умолчанию не ограничено.
Настройки прикладного уровня
Эти настройки передаются с каждым запросом в списке настроек пакета Query. Они влияют на то, какие данные сервер передаёт по сети и как они разбиваются на кадры.Сжатие
Флаг
compression в пакете Query (поле 6) включает и отключает сжатие, а эти настройки определяют, какой кодек используется при включённом сжатии.
Потоковая передача журналов
Если для
send_logs_level задано любое значение, кроме "none", сервер во время выполнения запроса отправляет пакеты Log.
Отчёты о прогрессе
Это целевое минимальное значение, а не строгий максимум: сервер может отправлять пакеты Progress реже, если запрос выполняет работу недостаточно быстро.
Оболочка результата
Async INSERT
Распределённая трассировка
Настройки, не относящиеся к протоколу
Эти настройки иногда ошибочно принимают за настройки уровня протокола, однако они управляют выполнением SQL, хранилищем или использованием CPU, а не форматом передачи данных. Реализации протокола не нужно обрабатывать их особым образом.max_threads— степень параллелизма при выполнении запроса.max_memory_usage— ограничение памяти на один запрос.max_block_size,preferred_block_size_bytes— внутренний размер блоков при обработке запроса; от них не зависят блоки, передаваемые по сети.compile_expressions— JIT-компиляция; влияет только на CPU.async_insert_max_data_size— буфер очереди на стороне сервера.- Все настройки
input_format_*иoutput_format_*, за исключением семействаinput_format_native_*/output_format_native_*: остальные (неnative) настройки выбирают или настраивают другие форматы (например, при работе через HTTP) и не влияют на блокиDataсобственного протокола.
*_native_*: они меняют байты внутри блоков Data, передаваемых по нативному TCP, поэтому реализация протокола должна их учитывать. output_format_native_encode_types_in_binary_format заменяет текстовую строку в поле type столбца на бинарное кодирование типа (кроме вторичного запроса, см. формат передачи данных столбца), output_format_native_write_json_as_string передаёт JSON-столбцы как String, а output_format_native_use_flattened_dynamic_and_json_serialization включает структуру FLATTENED для Dynamic/JSON. Поскольку эти настройки влияют на тело блока, а не на оболочку пакета, они описаны в спецификации формата Native — см. разделы формат передачи данных столбца и версионируемые типы.
Глоссарий
Cancel — пакет (тип 3), отправляемый клиентом для прерывания выполняющегося запроса. Подробно на этой странице не описывается. Маркер конца клиентских данных — пустой пакет Data (0 столбцов, 0 строк), который клиент отправляет, чтобы закрыть входной поток. Момент его отправки зависит от вида запроса:- Обычный запрос (
SELECTи т. п.): отправляется после пакета Query и всех пакетов Data с внешними таблицами и сообщает, что внешних данных больше нет. После этого сервер приступает к выполнению запроса. INSERT: клиент не отправляет маркер до получения схемы. Сначала сервер отправляет блок схемы, затем клиент передаёт блоки Data со строками и только после этого отправляет пустой пакет Data, завершая поток строк. Пустой маркер, отправленный до блока схемы, будет воспринят как немедленный конец строк, и данные будут потеряны.
min(client_version, server_version); вычисляется во время рукопожатия и определяет, какие возможности активны в течение всего времени жизни соединения.
Пакет — сообщение в формате передачи: код типа пакета (VarUInt), за которым следует тело, формат которого зависит от типа. См. оболочку пакета.
Код типа пакета — начальное значение VarUInt в пакете, определяющее его формат. В настоящее время заняты значения 0–18. См. справочник по типам пакетов.
Поток ответов — последовательность пакетов, которые сервер отправляет во время выполнения запроса. Длина потока не ограничена; он завершается ровно одним пакетом EndOfStream (успех) или Exception (ошибка). См. фазу запроса.
Блок схемы — блок заголовка (Block со столбцами, но без строк), который сервер отправляет на этапе INSERT, чтобы сообщить ожидаемую структуру столбцов до того, как клиент начнёт отправлять данные.
Список настроек — последовательность кортежей (key, flags, value) в теле Query, завершающаяся пустым ключом. Содержит конфигурацию прикладного уровня для отдельного запроса. См. Setting.
Stage — поле VarUInt в пакете Query (поле 5), определяющее, до какого этапа сервер выполняет запрос. Внешние клиенты обычно отправляют 2 (Complete); более высокие значения используются в распределённых запросах и сериализованных планах запросов. Полный набор значений см. в описании поля 5 пакета Query.
Терминатор — пакет, завершающий поток. Ответ на Query завершается пакетом EndOfStream (успех) или Exception (ошибка). Входной поток клиента завершается пустым маркером Data.