Skip to main content
Собственный протокол — это бинарный протокол с установлением соединения, по которому клиенты и серверы ClickHouse взаимодействуют через TCP. По нему передаются SQL-запросы, результаты запросов, полезная нагрузка 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-соединение только что установлено, и обмена сообщениями ещё не было. Порядок действий:
  1. Клиент отправляет ClientHello с максимальной поддерживаемой им версией протокола.
  2. Клиент читает ответ и выбирает дальнейшие действия в зависимости от типа пакета:
  3. Если negotiated_version ≥ 54458 (возможность ADDENDUM), клиент отправляет Addendum. Решение принимается на основе согласованной версии, а не версии, объявленной клиентом.
В случае успеха соединение переходит в состояние READY; при любой ошибке оно разрывается.

Фаза Ping

Проверка работоспособности на уровне приложения, не зависящая от TCP keepalive. Успешный обмен Ping/Pong подтверждает, что TCP-соединение активно в обоих направлениях, а сервер отвечает на запросы. Ping не хранит состояния и не связан ни с каким запросом, поэтому несколько последовательных Ping никак не зависят друг от друга. Начиная с состояния READY, процесс выглядит так:
  1. Клиент отправляет Ping.
  2. Клиент читает ответ:

Фаза запроса

Клиент отправляет SQL-оператор; сервер в потоковом режиме возвращает блоки результата и телеметрию выполнения. Ответ представляет собой последовательность пакетов, которая завершается ровно одним пакетом EndOfStream или Exception. Начиная с состояния READY, процесс выглядит так: Если на любом этапе возникает ошибка, сервер отправляет Exception вместо EndOfStream, и запрос завершается.
  1. Клиент отправляет Query с уникальным query_id (обычно UUID).
  2. Клиент отправляет внешние таблицы (если они есть), а затем пустой маркер Data. У пустого пакета Data table_name = "", num_columns = 0, num_rows = 0. Сервер не начинает выполнять запрос, пока не получит этот маркер. Внешняя таблица может быть разбита на несколько пакетов Data с одинаковым table_name. Первый из них фиксирует схему таблицы, а каждый последующий пакет с этим именем должен содержать те же имена и типы столбцов в том же порядке, иначе сервер отклонит запрос с ошибкой INCORRECT_DATA (см. Data).
  3. Клиент переходит в состояние READING_RESPONSE и сбрасывает буфер записи.
  4. Клиент в цикле читает пакеты ответа и обрабатывает их в зависимости от типа:
После 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. Последовательность обмена:
  1. Клиент отправляет Query, в поле body которого содержится SQL-запрос INSERT.
  2. Клиент отправляет внешние таблицы, если они есть (для INSERT это редкость). В отличие от фазы запроса, здесь клиент не отправляет пустой маркер Data. Пакет Query с INSERT отправляется с признаком ожидающих данных, поэтому пустой блок конца данных откладывается до шага 5: если отправить его до блока схемы, сервер воспримет его как конец потока строк, завершит INSERT без строк, а затем разберёт первый настоящий пакет со строками как посторонний пакет верхнего уровня.
  3. Клиент считывает пакеты метаданных (TableColumns, Progress, ProfileInfo, Log, ProfileEvents), пока не получит пакет Data со схемой — Block с 0 строк, но с полной структурой столбцов (имена и типы). Блок схемы служит контрактом: строки, которые клиент отправит далее, должны соответствовать этим столбцам.
  4. Клиент отправляет один или несколько блоков данных. Для каждого блока он записывает VarUInt(ClientPacket::Data = 2), затем String("") в качестве пустого имени внешней таблицы и, наконец, сам Block. Типы столбцов должны позиционно совпадать со столбцами блока схемы.
  5. Клиент отправляет признак конца ввода: пакет Data с пустым Block (0 столбцов, 0 строк).
  6. Клиент считывает поток ответа до EndOfStream (успех) или Exception (ошибка).
Асинхронная вставка (v54484+). Если запрос содержит 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_INFO v54443), http_referer (String, при наличии REFERER_IN_CLIENT_INFO v54447) и, наконец, http_handler_name (String) и http_request_url (String) (оба при наличии HTTP_HANDLER_IN_CLIENT_INFO v54490, именно в таком порядке). Поля 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.
Кодирование OpenTelemetry (поле 16):

Межсерверная аутентификация

Поле 4 пакета Query (auth_hash) при передаче по сети не содержит общий секрет кластера. Отправка секрета в исходном виде не только привела бы к ошибке аутентификации, но и раскрыла бы его. Вместо этого сервер, выступающий в роли межсерверного клиента, доказывает, что знает секрет, с помощью хеша SHA-256 с солью:
  1. Переход в межсерверный режим. Подключающийся сервер сообщает об этом в ClientHello: поле user содержит межсерверный маркер, а password остаётся пустым. Затем он дописывает ещё две строки — имя кластера и только что сгенерированную 32-байтовую salt (encodeSHA256 от случайного значения) — сразу после полей user/password, в том же пакете ClientHello. Сервер читает эти две строки до отправки ServerHello, поэтому клиент должен записать их сразу; если сначала дожидаться ServerHello, возникнет взаимная блокировка, поскольку сервер заблокирован на их чтении.
  2. Получение nonce. Если согласована ревизия INTERSERVER_SECRET_V2 (v54462), ServerHello содержит 8-байтовый nonce типа UInt64.
  3. Вычисление хеша. Для каждого пакета 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. Проверка. Сервер читает поле 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 может содержать множество строк.
8 столбцов строго в следующем порядке:

ProfileEvents (тип пакета 14)

Server → Client. Передаёт счётчики производительности по каждому запросу. Оболочка и формат тела такие же, как у Data. Блок имеет фиксированное значение num_columns = 6 и предопределённую схему. Каждому событию соответствует одна строка.
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 до успешной аутентификации:
  1. Клиент отправляет ClientHello с маркерным префиксом SSH и пустым паролем.
  2. Клиент отправляет SSHChallengeRequest (пакет 11). Сервер ещё не отправил ServerHello — сначала он выполняет аутентификацию и на этом шаге блокируется в ожидании этого пакета.
  3. Сервер отвечает пакетом SSHChallenge со случайными байтами (пакет 18).
  4. Клиент формирует строку для подписи и подписывает именно её, а не исходный челлендж, после чего отправляет SSHChallengeResponse (пакет 12) с подписью. Подписываемое сообщение — побайтовая конкатенация без разделителей четырёх частей строго в следующем порядке:
  5. Сервер проверяет подпись по зарегистрированному открытому ключу пользователя, восстанавливая ту же строку decimal(protocol_version) + default_database + user + challenge. В случае успеха он отправляет ServerHello — тот же ответ, что и при аутентификации по паролю, — и рукопожатие продолжается в обычном порядке (Addendum и т. д.); в случае неудачи он возвращает Exception и разрывает соединение. Клиент, подписывающий только исходные байты челленджа, не пройдёт аутентификацию.
Это обратный порядок по сравнению с рукопожатием при аутентификации по паролю, где ServerHello следует сразу за ClientHello. При SSH-аутентификации ServerHello отправляется только после проверки подписи, поэтому обмен SSH challenge-response встраивается в рукопожатие ещё до появления ServerHello.
Внешние клиенты, не использующие SSH-аутентификацию, никогда не получают пакеты 11, 12 и 18: эти пакеты не передаются по сети, если пользователь явно не включит этот режим с помощью префикса в имени пользователя.

MergeTreeAllRangesAnnouncementResponse (тип пакета 14)

Client → Server, только для межсерверного взаимодействия. Доступен при условии parallel_replicas_protocol_version ≥ 8 (см. VERSIONED_PARALLEL_REPLICAS_PROTOCOL). Внешние клиенты никогда не отправляют этот пакет. Если согласованная версия протокола параллельных реплик ≥ 8, цикл запроса/ответа initiator при получении MergeTreeAllRangesAnnouncement от follower (тип пакета 15, направление server→client) меняется:
  1. Follower открывает свой конвейер чтения и отправляет MergeTreeAllRangesAnnouncement на initiator.
  2. Только если mode в уведомлении отличается от Default (WithOrder = 1 или ReverseOrder = 2; оба используются для упорядоченного параллельного чтения), initiator отвечает пакетом MergeTreeAllRangesAnnouncementResponse. При mode = Default = 0 initiator ничего не отправляет, а follower не ждёт ответа: в режиме Default диапазоны выдаются вместе с каждым MergeTreeReadTaskRequest, поэтому предварительный список частей не нужен.
  3. Прежде чем отправить первый 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-запрос, чтобы приёмник старой версии восстановил ограничения из настроек запроса.

Конфигурация

В этом разделе описаны настраиваемые параметры, определяющие поведение соединений по собственному протоколу: Приведённые ниже значения по умолчанию соответствуют одному из последних релизов сервера и могут отличаться в зависимости от версии и конкретного развертывания.

Настройки транспортного уровня

Параметры сокета

Тайм-ауты

Тайм-ауты вложены друг в друга следующим образом:
Механизм keepalive ОС срабатывает первым и может незаметно выявлять недоступные узлы на уровне ядра. Следующая линия защиты — тайм-аут получения данных на уровне приложения. Тайм-аут бездействия — крайняя мера: он закрывает соединения, которые долго не использовались.

Ограничения соединений

Соединение, через которое регулярно выполняются запросы, может существовать сколь угодно долго. Через час закрываются только бездействующие соединения, а максимальное время жизни соединения по умолчанию не ограничено.

Настройки прикладного уровня

Эти настройки передаются с каждым запросом в списке настроек пакета 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, завершая поток строк. Пустой маркер, отправленный до блока схемы, будет воспринят как немедленный конец строк, и данные будут потеряны.
Возможность — изменение формата передачи данных, появившееся в определённой версии протокола. Активна, если согласованная версия не ниже версии, в которой появилась возможность. См. версионирование и флаги возможностей. Inter-server — метка роли для поля, которое имеет смысл только в распределённых запросах между серверами. Внешние клиенты записывают в него значение по умолчанию (обычно пустую строку, 0 или false). Согласованная версия — 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.