INSERT のペイロード、実行時のテレメトリー、エラー通知を伝送します。コマンドラインクライアント、C++ ドライバー、および大半のサードパーティ製ネイティブドライバーは、このプロトコルを使用しています。
このページでは、プロトコルそのもの、すなわちパケットのフレーミング、接続のステートマシン、バージョンネゴシエーション、および Block 以外のすべてのメッセージのボディについて説明します。Data 系パケット内部のバイト列 (Block、そのカラム、および型ごとのエンコーディング) は別個のトピックとして、Native Format 仕様で説明しています。
対となる仕様このページは 2 部構成の仕様の一方であり、対となる Native Format 仕様とあわせて公開されています。2 つの仕様の役割は明確に分かれており、このページはパケット層とトランスポート層を、Native Format 仕様は
Data 系パケット内部のバイト列を扱います。BlockInfo の内部を除いてフィールドタグは存在しないため、1 バイトでも位置がずれると、それ以降のすべてのデータが正しく解釈できなくなります。また、プロトコルはステートフルであり、各 TCP 接続は一度に 1 つのクエリしか処理しません。多重化はサポートされていません。固定長整数はリトルエンディアンで表現されます。
概要
wire 上のすべてのメッセージは、
VarUInt 型のパケットタイプコードで始まり、その後にボディが続きます。ボディの構造は、そのコードとネゴシエートされたプロトコルバージョンによって決まります。
接続は 3 つのフェーズで進行します。最初に一回限りのハンドシェイクを行い、続いて Ping または Query のやり取りを任意の回数行い、最後に接続を閉じます。
ネイティブ TCP プロトコルは、SQL に FORMAT 句が指定されているかどうかにかかわらず、表形式のデータを常に Native フォーマットで送信します。RowBinary、CSV、JSON などへの再フォーマットはクライアント側の処理であり、Native ブロックをデコードした後に行われます。(HTTP インターフェイスは別のコードパスで処理され、こちらでは FORMAT 句が実際に反映されます。HTTP については本ドキュメントの対象外です。)
セキュリティ
トランスポートセキュリティ (TLS)
TLS はプロトコルより下位のトランスポート層で動作します。TLS を有効にすると TCP ストリーム全体が暗号化されますが、プロトコルメッセージ自体は TLS を使用するかどうかにかかわらず、バイト単位で完全に同一です。認証
認証はハンドシェイク中のClientHello メッセージで行われます。user フィールドと password フィールドは平文の文字列として送信されるため、転送時の認証情報はトランスポート層の暗号化 (TLS) によって保護する必要があります。
user フィールドが空の場合、サーバーはこれを設定済みのデフォルトセッションユーザーとして解釈します。デフォルトセッションユーザーは default_session_user サーバー設定 (デフォルト値は default) で指定され、protocols セクションでリスナーごとに上書きすることもできます。サーバーでデフォルトセッションユーザーが空に設定されている場合、またはサーバーのバージョンが 26.8 より前の場合、空の user フィールドは例外を返して拒否されます。なお、clickhouse-client が空のユーザー名を送信することはありません。クライアント側で default に置き換えて送信します。
SSH チャレンジレスポンス認証は、プロトコルバージョン 54466 以降で利用できます。詳しくは SSH チャレンジレスポンス認証 を参照してください。
サーバー間通信用のシークレット
分散クエリ実行では、各サーバーは共有シークレットを知っていることを証明することで相互に認証します。その際、シークレット自体が wire 上に送出されることはありません。各 Query は、Query のフィールド 4 に 32 バイトの SHA-256 auth_hash を保持します。この値はソルト、ノンス、設定済みのシークレット、およびクエリから計算され、受信側のサーバーが同じ計算を行って照合します。この仕組みが有効かどうかは、INTERSERVER_SECRET 機能 (v54441) によって決まります。外部クライアントは、このフィールドに常に空文字列を送信します。詳細は Inter-server authentication を参照してください。
バージョニングと機能ゲート
バージョンネゴシエーション
クライアントとサーバーは、ハンドシェイク時にそれぞれがサポートする最大のプロトコルバージョンを宣言します。ネゴシエート済みバージョンは、両者のうち小さい方になります。機能ゲート
各機能は、その機能を導入したプロトコルバージョンによって識別され、ネゴシエートされたバージョンがその番号以上である場合に有効になります。機能一覧
パケットエンベロープ
wire 上のすべてのメッセージは、送信・受信のどちらの方向でも、同じ外側の構造を持ちます。VarUInt です。値が 128 未満であれば VarUInt でも同じ 1 バイトになりますが、将来パケットタイプが 128 以上になった場合にも互換性を保てるよう、実装では必ず VarUInt エンコーディングを使用してください。
メッセージリファレンスでは、各パケットのボディ(パケットタイプコードに続くバイト列)のみを記載しています。フィールド番号は、ボディの最初のフィールドを 1 として振られます。
チャンク化フレーミング (v54470+)
CHUNKED_PROTOCOL 機能がネゴシエートされると (ハンドシェイクを参照) 、wire 上のすべてのパケットはチャンク化フレーミングでラップされます。このラップは方向ごとに適用されます。クライアント→サーバーとサーバー→クライアントはそれぞれ個別にネゴシエートされるため、方向によって異なるモード (チャンク化または非フレーム化) になることがあります。
パケットごとのワイヤレイアウト:
VarUInt はチャンク化されたストリームの内側にあります。つまり、フレーミングの前に別途送信されるバイトではなく、パケットペイロードの先頭バイト (最初のchunkの先頭バイト) です。各パケットのchunkペイロードは、パケットエンベロープで示した [VarUInt packet_type_code][message body] 全体です。クライアントがパケットタイプをチャンク化されたストリームの外側に置くと、ピアはそのタイプバイトを u32 chunkサイズの先頭バイトとして読み取ってしまい、接続の同期が崩れます。
ライターのバッファがパケットの途中で満杯になると、1つのパケットが複数のchunkに分割されることがあります。分割位置は任意で、パケットタイプの VarUInt の途中で分割されることもあります。リーダーはchunkペイロードを連結し、末尾の4バイトのゼロを透過的なパケット境界として扱います。つまり、このゼロは消費されますが、パケットボディを読み取る側には渡されません。
ボディを持たないパケットもラップされます。Ping や Pong のような1バイトのパケットは、チャンク化がネゴシエートされると [u32 size = 1][0x04][u32 0] になります。このページの他の箇所にある「wire上で1バイト」という記述は、チャンク化前の形式を指しています。
ネゴシエーション。 ServerHello と Addendum は、それぞれ方向ごとに1つずつ、計2つの String フィールドを持ちます。その値は {"chunked", "notchunked", "chunked_optional", "notchunked_optional"} のいずれかです。
chunked/notchunkedは厳格な指定です。その側は必ずそのモードを要求します。_optionalバリアントは柔軟な指定です。相手側が選択したモードをどちらでも受け入れます。
クライアント側では、クライアントの SEND 設定がサーバーの RECV 設定と照合されてネゴシエートされます。逆方向も同様です。
タイミング。 ネゴシエーション文字列は、フレーミングされていないwire上でやり取りされます (ClientHello → ServerHello (サーバーの設定) → Addendum (クライアントがネゴシエートした値) ) 。フレーミングの切り替えは、Addendum がflushされた後に送信されるすべてのバイトに適用されます。Addendum 自体、ClientHello、ServerHello は常にフレーミングなしで送信されます。
接続のライフサイクル
接続は常に、HANDSHAKE、READY、READING_RESPONSE、終了済みの 4 つの状態のうち、いずれか 1 つにあります。このプロトコルは多重化に対応していないため、クライアントが前のレスポンスを読み切る前に新しいリクエストを送信すると、wire 上でバイトが入り混じり、ストリームが破損します。
状態
正常系の経路はHANDSHAKE → READY → READING_RESPONSE → READY と上から下へまっすぐ進みます。Ping/Pong は自己ループとなり、失敗時の遷移はすべて単一の終端状態である Terminated に集約されます。
ハンドシェイクフェーズ
認証を行い、プロトコルバージョンをネゴシエートします。このフェーズは、各接続において他のどの処理よりも先に、1 回だけ実行されます。 TCP 接続が確立された直後で、まだメッセージは一切やり取りされていない状態です。処理の流れは次のとおりです。-
クライアントは、自身がサポートする最大のプロトコルバージョンを指定して
ClientHelloを送信します。 -
クライアントはレスポンスを読み取り、パケットタイプに応じて処理を振り分けます。
-
negotiated_version ≥ 54458(ADDENDUM機能) の場合、クライアントはAddendumを送信します。この判定は、クライアントが宣言したバージョンではなく、ネゴシエート後のバージョンに基づいて行われます。
READY に遷移します。エラーが発生した場合は、接続が終了します。
Ping フェーズ
TCP キープアライブとは独立した、アプリケーションレベルの死活確認です。Ping/Pong の往復が成功すれば、TCP 接続が双方向で有効であり、サーバーが応答可能であることを確認できます。Ping はステートレスで、どのクエリとも関連付けられないため、連続して送信される複数の Ping は互いに独立しています。READY 状態から開始した場合のフローは次のとおりです。
-
クライアントは
Pingを送信します。 -
クライアントはレスポンスを読み取ります。
クエリフェーズ
クライアントが SQL ステートメントを送信すると、サーバーは結果ブロックと実行テレメトリーをストリーミングで返します。レスポンスは一連のパケットで構成され、最後は必ず 1 つのEndOfStream または Exception で終わります。
READY 状態から始まるフローは次のとおりです。
いずれかの時点でエラーが発生した場合、サーバーは EndOfStream の代わりに Exception を送信し、クエリはそこで終了します。
-
クライアントは、一意の
query_id(通常は UUID) を付与したQueryを送信します。 -
クライアントは外部テーブルがあればそれらを送信し、続いて空の Data マーカーを送信します。空の Data パケットは
table_name = ""、num_columns = 0、num_rows = 0です。サーバーはこのマーカーを受信するまでクエリの実行を開始しません。外部テーブルは、同じtable_nameを持つ複数のDataパケットに分割して送信できます。この場合、最初のパケットでテーブルのスキーマが確定し、同じ名前を持つ以降のパケットはすべて、同じカラム名と型を同じ順序で含んでいる必要があります。そうでない場合、サーバーはINCORRECT_DATAでクエリを拒否します (Data を参照) 。 -
クライアントは
READING_RESPONSEに遷移し、書き込みバッファを flush します。 -
クライアントはループでレスポンスパケットを読み取り、種別に応じて処理を振り分けます。
EndOfStream を受信した場合、または Exception を処理した場合、接続は READY に戻ります。プロトコル違反や I/O エラーが発生した場合は接続が終了します。
num_rows == 0 のケースは、新たに実装する際につまずきやすいポイントです。行数ゼロのブロックは境界マーカーまたはスキーマヘッダーであり、ストリーム終了のシグナルではありません。レスポンスを終了させるのは EndOfStream または Exception だけです。INSERT フェーズ
INSERT フェーズは、クエリフェーズに 2 つのやり取りを加えたものです。クライアントがINSERT ステートメントを送信すると、サーバーはターゲットテーブルの構造を記述したスキーマブロックを返します。クライアントは行を格納した Data パケットをストリーミング送信し、最後に空の Data マーカーを送信します。サーバーは EndOfStream または Exception を返して処理を完了します。
READY 状態から開始し、SQL には INSERT INTO <table> [(<cols>)] VALUES 形式の INSERT を使用します。行データは Data パケットで送られるため、インラインの VALUES (...) リテラルは記述しません。処理の流れは次のとおりです。
- クライアントは、
bodyに INSERT SQL を設定したQueryを送信します。 - クライアントは外部テーブルがあればそれを送信します (INSERT ではまれです) 。クエリフェーズとは異なり、ここでは空の Data マーカーを送信しません。
INSERTのQueryパケットは保留中のデータを伴って送信されるため、データ終端を示す空のブロックの送信はステップ 5 まで遅延されます。スキーマブロックより前にこれを送信すると、サーバーはそれを行ストリームの終端と解釈して行なしで INSERT を完了し、その後に届く最初の実際の行パケットを、想定外のトップレベルパケットとして解析してしまいます。 - クライアントは、スキーマ Data パケット (行数は 0 だが、完全なカラム構造 (名前と型) を持つ Block) を読み取るまで、メタデータパケット (TableColumns、Progress、ProfileInfo、Log、ProfileEvents) を読み捨てます。スキーマブロックは契約として機能し、クライアントが次に送信する行はこれらのカラムの形状に一致しなければなりません。
- クライアントはデータブロックを送信します。各ブロックについて、まず
VarUInt(ClientPacket::Data = 2)、次に空の外部テーブル名としてString("")、最後に Block を書き込みます。カラム型は、スキーマブロックのカラムと位置ごとに一致している必要があります。 - クライアントは入力の終端を示すターミネーターとして、空の Block (0 カラム、0 行) を持つ Data パケットを送信します。
- クライアントは、
EndOfStream(成功) またはException(失敗) を受信するまでレスポンスストリームを読み取ります。
async_insert = 1 が指定されている場合、サーバーは行をキューに入れ、バッチの一部として flush します。ネゴシエートされたバージョンが 54484 (PROGRESS_IN_ASYNC_INSERT) 以上の場合、flush の完了後にサーバーは追加の Progress パケットを送出し、その直後に挿入の ProfileEvents、続いて EndOfStream を送出します。54484 未満では、サーバーはこの末尾の Progress を送出しません。このパケットは通常の Progress ですが、サーバーは書き込みカウントを反映する前にクエリパイプラインをリセットするため、実際の増分には経過時間しか含まれず、書き込まれた行数とバイト数の統計は付随する ProfileEvents を通じてクライアントに届きます。ステップ 6 で途中に挟まる Progress をすでに読み取っているクライアントであれば、パケットをもう 1 つ受け入れるだけで対応できます。
接続は、EndOfStream または処理済みの Exception を受信すると READY に戻ります。プロトコル違反や I/O エラーが発生した場合、接続は終了します。
メッセージリファレンス
フィールドはwire上の順序で記載しています。Type カラムでは次の表記を使用します。
VarUInt— 可変長の符号なし整数 (VarUInt を参照) 。String— VarUInt のプレフィックスが付いたバイト列 (String を参照) 。UInt8、Int32など — 固定幅のリトルエンディアン整数。Bool— 1 バイトで、値は0x00または0x01。
Role カラムは、各フィールドを使用する側を示します。
- client — 外部クライアントが設定します。
- inter-server — サーバー間通信でのみ意味を持ちます。外部クライアントはデフォルト値を書き込みます。
- universal — 双方で使用されます。
ClientHello (パケットタイプ 0)
Client → Server。TCP 接続の確立直後に最初に送信されるメッセージです。ServerHello (パケットタイプ 0)
Server → Client。認証に成功した場合に ClientHello への応答として送信されます。
Rule —
password_complexity_rules の要素:
このリストはサーバー運用者が設定したパスワードポリシーを反映したもので、あくまで参考情報です。サーバーはハンドシェイク中にこれらのルールを適用しません。パスワードの変更・設定機能を提供するクライアントは、要件を満たさないパスワードをサーバーに送信する前に、これらのルールを使ってエラーを検出できます。
悪意のあるサーバーや設定に誤りのあるサーバーによるリソースの過剰消費を防ぐため、デコードした
count は最大 256 エントリ、各 pattern および message の String は最大 4096 バイトに制限してください。パスワードポリシーが設定されていないサーバーでは、通常 count は 0 (後続のペアなし) です。Addendum (パケットタイプなし)
Client → Server。ADDENDUM (v54458) が有効な場合に送信されます。ハンドシェイクのやり取りが完了した直後に送信されます。これは独立したパケットタイプではなく、各フィールドはパケットタイプを示すバイトのプレフィックスを付けずに、生のまま wire 上に送出されます。
チャンク化フレーミングへの切り替えは、この Addendum が flush された後に適用されます。Addendum 自体はフレーミングされません。
Ping (パケットタイプ 4)
Client → Server。ボディはありません。チャンク化フレーミング適用前のパケットは、1 バイトの0x04 のみで構成されます。チャンク化がネゴシエートされている場合、このバイトは 1 バイトのペイロードを持つ chunk として送信されます (チャンク化フレーミングを参照) 。
Pong (パケットタイプ 4)
Server → Client。ボディはありません。チャンク化フレーミングの適用前、このパケットは 1 バイトの0x04 のみで構成されます。チャンク化がネゴシエートされている場合、このバイトが chunk の 1 バイトのペイロードになります (チャンク化フレーミングを参照) 。
Exception (パケットタイプ 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 以外 の場合、これらのフィールドは別の wire レイアウトに置き換えられます。単に任意項目が省略されるのではないため、デコーダはフィールド 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フィールドは含まれません。最後の 2 つは、マッチした SQL 定義の HTTP ハンドラー名とリクエスト URL を保持し、リモートの分片にも引き継がれるようにするためのものです (ハンドラーを経由しない HTTP クエリの場合は空になります)。- その他のインターフェイス: TCP フィールド (7–12) も HTTP フィールドも一切書き込まれず、ストリームはそのまま
quota_keyに進みます。
quota_key (フィールド 13) と distributed_depth (フィールド 14) が続き、その後の version_patch (フィールド 15) は TCP の場合にのみ書き込まれます。この分岐が特に問題となるのはサーバー間通信で、開始元のサーバーが元々 HTTP 経由で受け取ったクエリを転送するケースです。常に TCP フィールドを読み取るデコーダは、このようなパケットを誤って解釈し、http_method や http_user_agent を quota_key として扱ってしまいます。Inter-server authentication
Query フィールド 4 (auth_hash) として wire 上で送信されるのは、共有クラスターシークレットそのものではありません。生のシークレットを送信すると、認証に失敗するうえにシークレットも漏洩してしまいます。そのため、サーバー間クライアントとして動作するサーバーは、ソルト付き SHA-256 ハッシュを用いて、シークレットを知っていることを証明します。
- サーバー間モードに入る。 接続元のサーバーは、
ClientHello内でこのモードを通知します。具体的には、userフィールドにサーバー間マーカーを設定し、passwordを空にします。さらに、同じClientHelloパケットの一部として、user/passwordフィールドの直後に 2 つの文字列を追加します。1 つはクラスター名、もう 1 つは新たに生成した 32 バイトのsalt(ランダムな値のencodeSHA256) です。サーバーはServerHelloを送信する前にこれら 2 つの文字列を読み取るため、クライアントはあらかじめ書き込んでおく必要があります。先にServerHelloを待つと、サーバーはこれらの文字列の読み取りでブロックされたままとなり、デッドロックが発生します。 - nonce を取得する。
INTERSERVER_SECRET_V2(v54462) がネゴシエートされている場合、ServerHelloには 8 バイトのUInt64nonce が含まれます。 - ハッシュを計算する。
InitialQuery以外のすべての Query パケットにおいて、クライアントはencodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles + current_roles)(32 バイトのダイジェスト) をフィールド 4 に書き込みます。 (nonceは 10 進文字列で表現され、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) — 各設定は次の 3 つ組で構成されます:
key が空の場合、フィールド 2 と 3 は存在しません。
54429 より前 (BINARY) — 各設定は [String key][type-specific binary value] の形式です。flags フィールドは書き込まれず、値は 10 進数やテキストの文字列ではなく、その設定のネイティブなバイナリ形式 (固定幅の整数や長さプレフィックス付きの文字列など) でエンコードされます。この場合も、リストは空の key で終端されます。ネゴシエートされたバージョンが 54429 未満の場合、クライアントは上記の 3 つ組ではなく、このバイナリ形式で読み書きする必要があります (ただし、ユーザー定義のカスタム設定は例外で、どちらのエンコーディングでも常に flags と文字列値を持ちます) 。
flags フィールドには次の値がまとめて格納されます:
0x01— Important: この設定はクエリ結果に影響するため、古いピアが暗黙的に無視してはなりません。0x02— Custom: ユーザー定義のカスタム設定です。0x0c— 独立したフラグではなく、2 ビットの tier フィールドです:0x00= Production、0x04= Obsolete、0x08= Experimental、0x0c= Beta。必ず 2 ビットをまとめて (flags & 0x0c) 読み取ってください。flags & 0x04のような単純な判定では、Beta (0x0c) を Obsolete と誤判定してしまいます。0x80— HotReload (再起動せずに設定を再読み込み可能。flags の enum で定義されており、主に協調関連の設定で使われます) 。
パラメータ
SELECT {x:UInt64} のようなパラメータ化クエリで使用するクエリパラメータです。Custom フラグ (0x02) を設定した Setting と同じ方法でエンコードされ、同様に空のキーで終端されます。
パラメータ値は、生のリテラルではなく、値の SQL 表現です。String 型のパラメータは、あらかじめシングルクォートで囲んだ状態で渡す必要があります (例えば、
{name:String} の値は Alice ではなく 'Alice' です)。そうしないと、サーバーの値パーサーによって拒否されます。Data (パケットタイプ 1 server→client, パケットタイプ 2 client→server)
双方向で使用されます。結果ブロック、INSERT データ、外部テーブル、およびデータ終端マーカーを伝送します。 wire 形式は双方向で共通であり、どちらの方向でも Block の前にtable_name プレフィックスが付きます。異なるのはパケットタイプのバイトのみです。
データ終了マーカーとは、
table_name の値にかかわらず、Block が空 (カラム 0 個、行 0 行) のパケットを指します。サーバーがクライアントの Data パケットを終端として扱うのは、デコードされたブロックが空 (block.empty()) の場合に限られます。table_name = "" であってもブロックが空でなければ、そのパケットは通常の行パケットであり、終端ではありません。つまり、INSERT の行ストリームは、空でない Data ブロックが続いた後、最後に 1 つの空の Data ブロックで終了する構成になります。
ブロックのバリアントとその意味については、ブロックのバリアントを参照してください。
Progress (パケットタイプ 3)
Server → Client。クエリ実行中に定期的に送信されます。すべてのフィールドは VarUInt で、各パケットには累積の合計値ではなく、直前のProgress パケット以降の増分が格納されます。サーバーは送信前に自身のカウンターを読み取ってアトミックにゼロへリセットし、前回の送信からの経過時間を elapsed_ns として算出します。そのため、クライアントは累計値を得るために、受信したパケットをローカルで累積する必要があります。パケットを絶対値として扱うと、2 つ目以降のパケットを受信した時点で進捗表示が逆戻りしたり、実際より少なく表示されたりします。
ProfileInfo (パケットタイプ 6)
サーバー → Client。クエリごとに 1 回、実行の終了間際に送信されます。Totals (パケットタイプ 7)
サーバー → Client。WITH TOTALS を指定したクエリに対して送信されます。ワイヤ形式は Data と同一で、table_name 文字列 (常に空) の後に Block が続きます。異なるのはパケットタイプのバイトだけです。
Extremes (パケットタイプ 8)
サーバー → クライアント。extremes 設定が有効な場合に送信されます。ワイヤ形式は Data と同一です。この Block は常に 2 行で構成され、行 0 には各カラムの最小値、行 1 には各カラムの最大値が格納されます。
Log (パケットタイプ 10)
サーバー → Client。クエリでログキューが有効になっている場合に送信されます(send_logs_level 設定で制御されます。ログストリーミングを参照してください)。
エンベロープとボディのフォーマットは Data と同じです。Block は num_columns = 8 で固定されており、スキーマも事前に定義されています。各ログ行は 8 つのカラムすべてにわたる 1 行に対応し、1 つの Log パケットに複数の行を含めることができます。
ProfileEvents (パケットタイプ 14)
サーバー → Client。クエリごとのパフォーマンスカウンターを送信します。 エンベロープとボディのフォーマットは Data と同じです。Block はnum_columns = 6 に固定されており、スキーマもあらかじめ定義されています。1 つのイベントが 1 行に対応します。
value カラムの要素型はパケットによって異なります。古いサーバーは UInt64 を、新しいサーバーは Int64 を送信します。特定のビット幅を前提とせず、Block ヘッダーからカラムの型文字列を読み取ってください。TableColumns (パケットタイプ 11)
サーバー → Client。COLUMN_DEFAULTS_METADATA (v54410) で有効になります。サーバーは INSERT のスキーマBlockの前にこのパケットを送信し、カラムのデフォルト値に関するメタデータを伝えます。ただし、送信されるのは negotiated version が ≥ 54410 かつ input_format_defaults_for_omitted_fields 設定が有効な場合に限られます。54410 未満ではこのパケットが送信されることはないため、古いクライアントはこれを待ってはいけません。この場合、スキーマ用の Data Blockが直接届きます。v54410 以降のクライアントは、どちらの順序にも対応できるようにしておく必要があります。つまり、任意で TableColumns が届き、その後にスキーマBlockが続きます。
v54481 以降の圧縮ボディnegotiated version が ≥ 54481 (
COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS) の場合、サーバーは両方のフィールドを、圧縮が任意で適用される同一の出力経路で書き込みます。そのため、クエリで compression = true が指定されていると、TableColumns のボディ全体 (external_table + columns_description) が 圧縮フレーム 内に格納され、クライアントは対応する展開ストリームを介してこれを読み取ります。クエリで圧縮を使用しない場合、ボディは上の表に示すとおり、非圧縮のままワイヤー上を流れます。この点は INSERT のスキーマレスポンスで重要になります。Log と ProfileEvents では圧縮の扱いを切り替えても TableColumns では切り替えないクライアントは、クエリの圧縮が有効なときにレスポンスを誤って読み取ってしまいます。TimezoneUpdate (パケットタイプ 17)
サーバー → Client 方向のパケットで、TIMEZONE_UPDATES (v54464) によって有効化されます。送信されるのは 1 か所のみで、input テーブル関数 (クライアントから行をストリーミングする INSERT INTO <table> SELECT ... FROM input('<structure>') 形式のクエリ) の初期化処理です。サーバーは入力スキーマの Data Block (INSERT フェーズ を参照) を送信した直後に、クエリコンテキストの現在の session_timezone を格納した TimezoneUpdate を送出します。これにより、クライアントはこれから送信する行をサーバーと同じタイムゾーンでパースできます。クエリ途中で任意に実行された SET session_timezone による変更に対してサーバーがこのパケットを送出することはなく、後続の結果Blockのフォーマット方法をクライアントに伝える目的で送出することもありません。
このパケットは、入力スキーマBlockの直後、かつクライアントが行Blockの送信を開始する前に 1 回だけ届きます。
TimezoneUpdate を無視するデコーダであっても、wire 上の位置を揃えるために、末尾の String は必ず消費しなければなりません (MUST)。
SSH チャレンジレスポンス認証 (パケットタイプ 11、12、18)
SSH_AUTHENTICATION (v54466) で有効化される機能で、オプトイン方式でのみ利用できます。ClientHello が user = " SSH KEY AUTHENTICATION " + <real_user> (先頭と末尾のスペースを含む) と password = "" を送信すると、接続は SSH フローに移行します。サーバーはプレフィックスを読み取り、それを除去して実際のユーザー名を復元したうえで、チャレンジレスポンス方式に切り替えます。
このフローはパスワード認証の代わりに実行され、チャレンジレスポンスのやり取りは ServerHello より前に行われます。サーバーは認証が成功するまで Hello の応答を返しません。
- クライアントは、SSH マーカーのプレフィックスと空のパスワードを含む ClientHello を送信します。
-
クライアントは
SSHChallengeRequest(パケット 11) を送信します。この時点でサーバーはまだ ServerHello を送信していません。サーバーは先に認証を処理するため、ここでこのパケットの到着を待ってブロックします。 -
サーバーは、ランダムなバイト列を含む
SSHChallenge(パケット 18) で応答します。 -
クライアントは署名対象の文字列を組み立て、生のチャレンジではなくその文字列に署名したうえで、署名を含む
SSHChallengeResponse(パケット 12) を送信します。署名対象のメッセージは、次の 4 つのパーツをこの順序どおりに、区切り文字なしでバイト単位で連結したものです。 -
サーバーは同じ
decimal(protocol_version) + default_database + user + challenge文字列を再構築し、ユーザーの登録済み公開鍵で署名を検証します。検証に成功するとServerHello(パスワードフローと同じ応答) を送信し、ハンドシェイクは通常どおり続行されます (Addendum など) 。失敗した場合はExceptionを返して接続を終了します。生のチャレンジバイト列だけに署名したクライアントは、認証に失敗します。
これはパスワードによるハンドシェイクとは逆の順序です。パスワード認証では ClientHello の直後に ServerHello が送信されます。一方、SSH 認証では署名の検証が完了するまで ServerHello が保留されるため、ServerHello が現れる前に SSH のチャレンジレスポンスがハンドシェイクの途中に挟み込まれます。
MergeTreeAllRangesAnnouncementResponse (パケットタイプ 14)
Client → サーバー、サーバー間専用。parallel_replicas_protocol_version ≥ 8 の場合にのみ有効です (VERSIONED_PARALLEL_REPLICAS_PROTOCOL を参照) 。外部クライアントがこのパケットを送信することはありません。
ネゴシエートされた parallel-replicas のバージョンが ≥ 8 の場合、フォロワーが送信する MergeTreeAllRangesAnnouncement (パケットタイプ 15、サーバー→client 方向) に対して、イニシエーターのリクエスト/レスポンスのサイクルが次のように変わります。
- フォロワーが読み取りパイプラインを開き、
MergeTreeAllRangesAnnouncementをイニシエーターに送信します。 - announcement の
modeがDefault以外の場合に限り (WithOrder = 1またはReverseOrder = 2。いずれも順序どおりの並列読み取りで使用されます) 、イニシエーターはMergeTreeAllRangesAnnouncementResponseで応答します。mode = Default = 0の場合、イニシエーターは応答せず、フォロワーも待機しません。DefaultモードではMergeTreeReadTaskRequestのたびに範囲が割り当てられるため、事前にパーツ一覧を受け取る必要はありません。 - フォロワーは (レスポンスが想定される場合) 最初の
MergeTreeReadTaskRequest(サーバーパケット16。フォロワーからイニシエーターへ送信され、イニシエーターはクライアントパケット10のMergeTreeReadTaskResponseで応答します) を発行する前に、レスポンスを受信するまでブロックします。その後、返されたパーツ一覧を使ってソースの構築対象をフィルタし、自身の#split_iストリームが担当するパーツだけに絞り込みます。
8 未満では、モードにかかわらず announcement は fire-and-forget で送信され、フォロワーはローカルで認識しているすべてのパーツを対象にソースを構築します (レガシーの動作) 。
ボディ
RangesInDataPartsDescription ボディ
RangesInDataPartDescription ボディ
MergeTreePartInfo ボディ
MarkRanges ボディ
パケットタイプリファレンス
Client → サーバー
Server → Client
QueryPlan
QueryPlan は、サーバー間通信専用のクライアントパケットです。stage が QueryPlan (7) である Query の後に送信され、シリアル化されたプランストリームを 1 つ含みます。
バージョン
10 より前では、フィールド 2 と 3 は存在しません。いずれかの実行上限にデフォルト以外の値が設定されているイニシエーターは、この旧レイアウトで送信してはなりません。代わりに元の SQL クエリを送信し、旧バージョンの受信側がクエリ設定から上限値を再構築できるようにします。
設定
このセクションでは、ネイティブプロトコルの接続の挙動を左右する調整項目について説明します。- トランスポート層の設定 — TCP ソケットオプションとタイムアウト。TCP 接続そのものの挙動に影響します。
- アプリケーション層の設定 — Query パケットの設定リストで送信されるクエリ単位の調整項目。サーバーが wire 上で送信する内容や、そのフレーミング方法に影響します。
- 対象外の設定 — プロトコル設定と混同されがちですが、実際には SQL の実行やストレージを制御する設定です。
トランスポート層の設定
ソケットオプション
タイムアウト
各タイムアウトは次のような入れ子構造になっています。
接続数の制限
定期的にクエリを発行している接続は、無期限に維持されます。1 時間経過後に回収されるのはアイドル状態の接続のみで、デフォルトでは存続期間の上限は設定されていません。
アプリケーション層の設定
これらの設定は、Query パケットの設定リストに含まれてクエリごとに送信されます。これらは、サーバーが wire 上で送信する内容や、そのフレーミング方法を変更します。圧縮
Query パケットの
compression フラグ (フィールド 6) で圧縮のオン/オフを切り替えます。圧縮がオンの場合に使用するコーデックは、これらの設定で指定します。
ログストリーミング
send_logs_level を "none" 以外に設定すると、サーバーはクエリ実行中に Log パケットを送信します。
進捗レポート
これは目標とする最小値であり、厳密な上限ではありません。クエリの処理が十分な速さで進まない場合、サーバーが Progress パケットを送信する頻度はこれより低くなることがあります。
結果エンベロープ
非同期 INSERT
分散トレーシング
対象外の設定
以下の設定はプロトコルレベルの設定と誤解されることがありますが、実際に制御するのは wire 上の挙動ではなく、SQL の実行、ストレージ、または CPU の使用です。プロトコル実装でこれらを特別に扱う必要はありません。max_threads— クエリ実行時の並列度。max_memory_usage— クエリごとのメモリ上限。max_block_size、preferred_block_size_bytes— クエリ処理中の内部的なブロックサイズ。wire ブロックはこれらの影響を受けません。compile_expressions— JIT コンパイル。影響するのは CPU のみです。async_insert_max_data_size— サーバー側のキューバッファ。input_format_native_*/output_format_native_*系を除く、すべてのinput_format_*およびoutput_format_*設定 —native以外の設定は、他のフォーマット (例:HTTP 経由で使用するもの) の選択や調整を行うものであり、ネイティブプロトコルのDataブロックには影響しません。
*_native_* 設定は例外です。これらはネイティブ TCP の Data ブロック内のバイト列を変更するため、プロトコル実装で考慮する必要があります。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 Format の仕様で規定されています。詳しくは カラムのワイヤレイアウト および バージョン付き型 を参照してください。
用語集
Cancel — 実行中のクエリを中止するために、クライアントから送信されるパケット(タイプ 3)。このページでは詳細を規定していません。 End-of-client-data marker — 入力ストリームを閉じるためにクライアントが送信する空の Data パケット(0 カラム、0 行)。送信するタイミングはクエリの種類によって異なります。- 通常のクエリ(
SELECTなど): Query パケットと外部テーブルの Data パケット(存在する場合)の後に送信し、「外部データはこれ以上ない」ことを示します。これを受けて、サーバーがクエリの実行を開始します。 INSERT: クライアントはスキーマ受信前のマーカーを送信しません。まずサーバーがスキーマブロックを送信し、続いてクライアントが行の Data ブロックをストリーミングし、最後に空の Data パケットを送信して行ストリームを終了します。スキーマブロックより前に空のマーカーを送信すると、直ちに行の終端と解釈され、データが失われます。
min(client_version, server_version)。接続が維持されている間、どの機能が有効になるかを決定します。
パケット — wire 上のメッセージ。VarUInt のパケットタイプコードと、それに続くボディで構成されます。ボディのフォーマットはタイプによって異なります。パケットエンベロープを参照してください。
パケット type code — パケットの先頭にある VarUInt で、パケットのフォーマットを識別します。現在、値 0〜18 が割り当てられています。パケットタイプリファレンスを参照してください。
Response stream — クエリの実行中にサーバーが送出する一連のパケット。長さは不定で、必ず 1 つの EndOfStream(成功)または Exception(失敗)で終了します。クエリフェーズを参照してください。
Schema block — INSERT フェーズにおいて、クライアントがデータを送信する前に、想定されるカラムの構造を通知するためにサーバーが送信するヘッダーブロック(カラムを持つが行数が 0 の Block)。
設定リスト — Query ボディ内に含まれる (key, flags, value) タプルの列で、空のキーで終端されます。クエリ単位のアプリケーション層の設定を伝達します。Settingを参照してください。
Stage — Query パケット内の VarUInt フィールド(フィールド 5)で、サーバーがクエリをどの段階まで実行するかを制御します。外部クライアントは通常 2(Complete)を送信し、分散クエリやシリアル化されたクエリプランではより大きい値を使用します。wire 上で取りうる値の一覧については、Query のフィールド 5 を参照してください。
Terminator — ストリームを終了させるパケット。Query のレスポンスは EndOfStream(成功)または Exception(失敗)で終了し、クライアントの入力ストリームは空の Data マーカーで終了します。