Skip to main content
原生协议是 ClickHouse 客户端与服务器之间基于 TCP 通信的二进制、面向连接的协议。它负责传输 SQL 查询、结果数据、INSERT 载荷、执行遥测数据以及错误信号。命令行客户端、C++ 驱动程序以及大多数第三方原生驱动程序都基于该协议实现。 本页介绍协议本身:数据包帧信息、连接状态机、版本协商,以及所有非 Block 消息的请求正文。Data 家族数据包内部的字节 (即 Block、其中的列以及各类型的编码) 属于另一部分内容,详见 Native 格式 规范。
配套规范本页与 Native 格式 规范互为配套,二者一同发布。两份规范分工明确:本页负责数据包与传输层,Native 格式规范则负责 Data 家族数据包内部的字节。
以下几项特性贯穿整个协议。该协议是二进制且按位置解析的:除 BlockInfo 内部外不存在字段标签,因此只要有一个字节错位,其后的所有内容都会失去同步。该协议是有状态的,每个 TCP 连接一次只处理一个查询,不支持多路复用。定长整数采用小端序。

概述

线上传输的每条消息都以一个 VarUInt 数据包类型代码开头,后接请求正文,其形态取决于该代码以及协商后的协议版本。 一个连接会经历三个阶段:首先进行一次性握手,然后进行任意数量的 Ping 或 Query 交互,最后关闭连接: 无论 SQL 中是否带有 FORMAT 子句,原生 TCP 协议始终以 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。 从协议版本 54466 起支持 SSH 质询-响应身份验证,详见 SSH 质询-响应身份验证。

服务器间通信密钥

在分布式查询执行中,服务器之间通过证明自己知晓共享密钥来相互进行身份验证,而无需在线上传输该密钥。每个 Query 都会在 Query 的第 4 个字段中携带一个 32 字节的 SHA-256 auth_hash,该值由盐值、随机数 (nonce) 、已配置的密钥以及查询共同计算得出,接收方服务器会重新计算该值并进行比对。此机制受 INTERSERVER_SECRET 特性 (v54441) 控制。外部客户端在此处始终发送空字符串。参见服务器间身份验证。

版本控制与功能开关

版本协商

在握手阶段,客户端和服务器会各自声明所支持的最高协议版本。协商版本取两者中的较小值:
此后的每条消息都会依据协商版本来确定线上传输时包含哪些字段。

功能开关

每个功能都以引入它的协议版本来标识;当协商版本大于或等于该版本号时,该功能即处于活跃状态。
当某个功能处于活跃状态时,其字段在线上传输时必须存在。该协议严格按位置解析,因此一旦省略受功能开关控制的字段,其后所有字段的字节流都会被破坏。

功能对照表

数据包封装

无论传输方向如何,线上传输的每条消息都采用相同的外层结构:
完整的数据包类型表请参见数据包类型参考。 数据包类型采用 VarUInt 编码,而非固定宽度的字节。对于小于 128 的值,VarUInt 编码结果同样是单个字节,但实现必须使用 VarUInt 编码,以确保将来数据包类型值达到或超过 128 时仍能保持兼容。 消息参考仅描述每个数据包的请求正文,即数据包类型代码之后的字节。字段编号从 1 开始,对应请求正文的第一个字段。

分块成帧 (v54470+)

当双方经过协商启用 CHUNKED_PROTOCOL 特性后 (参见握手) ,线上传输的每个数据包都会采用分块成帧进行封装。这种封装按方向生效:客户端→服务器与服务器→客户端两个方向各自独立协商,最终可能采用不同的模式 (分块或无帧) 。 每个数据包的传输布局如下:
每个块的传输布局:
数据包类型 VarUInt 位于分块流内部:它是数据包载荷的第一个字节 (即第一个块的第一个字节) ,而不是在帧信息之前单独发送的字节。每个数据包的块载荷都是数据包封装中完整的 [VarUInt packet_type_code][message body]。如果客户端将数据包类型放在分块流之外,对端会把该类型字节当作 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_id (通常为 UUID) 的 Query。
  2. 客户端发送所有外部表,然后发送空 Data 标记。空 Data 数据包的 table_name = ""、num_columns = 0、num_rows = 0。服务器在收到该标记之前不会开始执行查询。一个外部表可以拆分到多个 table_name 相同的 Data 数据包中;其中第一个数据包确定该表的 schema,此后该名称下的每个数据包都必须按相同顺序携带相同的列名和类型,否则服务器会以 INCORRECT_DATA 拒绝该查询 (参见 Data) 。
  3. 客户端进入 READING_RESPONSE 状态,并刷写其写缓冲区。
  4. 客户端循环读取响应数据包,并按类型分别处理:
收到 EndOfStream 或已处理的 Exception 后,连接返回 READY 状态。若发生协议违规或 I/O 错误,则终止连接。
新的实现很容易在 num_rows == 0 这一情况上踩坑。零行块是边界标记或 schema 头块,而不是流结束信号。只有 EndOfStream 或 Exception 才会结束响应。

INSERT 阶段

INSERT 阶段即在 Query 阶段的基础上额外增加两轮交互。客户端提交一条 INSERT 语句;服务器返回一个描述目标表结构的 schema 块;客户端以流式方式发送携带行数据的 Data 数据包,随后发送空的 Data 标记;最后服务器以 EndOfStream 或 Exception 结束。 从 READY 状态开始,所发送的 SQL 是形如 INSERT INTO <table> [(<cols>)] VALUES 的 INSERT 语句——其中不包含内联的 VALUES (...) 字面量,因为行数据通过 Data 数据包传输。流程如下:
  1. 客户端发送 Query,并将 body 设置为 INSERT SQL。
  2. 客户端发送外部表 (如有;INSERT 中很少使用) 。与 Query 阶段不同,此处不发送空的 Data 标记。INSERT 的 Query 数据包在发送时带有待处理数据,因此表示数据结束的空块会推迟到第 5 步发送;如果在 schema 块之前发送它,服务器会将其视为行流的结束,在没有任何行的情况下完成 INSERT,随后把第一个真正的行数据包当作游离的顶层数据包来解析。
  3. 客户端排空元数据数据包 (TableColumns、Progress、ProfileInfo、Log、ProfileEvents) ,直到读取到 schema Data 数据包——一个包含 0 行但具有完整列结构 (名称和类型) 的 Block。schema 块即为契约:客户端随后发送的行必须与这些列的形态相匹配。
  4. 客户端发送一个或多个数据块。对于每个块,客户端先写入 VarUInt(ClientPacket::Data = 2),再写入表示空外部表名称的 String(""),最后写入该 Block。列类型必须按位置与 schema 块中的列一一对应。
  5. 客户端发送输入结束终止符:一个携带空 Block (0 列、0 行) 的 Data 数据包。
  6. 客户端排空响应流,直到收到 EndOfStream (成功) 或 Exception (失败) 。
异步 INSERT (v54484+) 。 当查询携带 async_insert = 1 时,服务器会将这些行放入队列,并随批次一起刷写。当协商版本 ≥ 54484 (PROGRESS_IN_ASYNC_INSERT) 时,刷写完成后服务器会额外发送一个 Progress 数据包,紧接着发送该插入操作的 ProfileEvents,最后发送 EndOfStream。若协商版本低于 54484,服务器则不会发送这个尾随的 Progress。该数据包就是普通的 Progress;由于服务器在合并写入计数之前会重置查询管道,该增量实际上仅携带耗时,已写入的行数和字节数统计则通过随附的 ProfileEvents 传递给客户端。如果客户端在第 6 步中已能排空交错出现的 Progress,则只需多接收一个数据包即可。 收到 EndOfStream 或已处理的 Exception 后,连接会回到 READY 状态。协议违规和 I/O 错误则会终止该连接。

消息参考文档

字段按传输顺序列出。Type 列使用以下类型:
  • VarUInt — 变长无符号整数 (参见 VarUInt) 。
  • String — 以 VarUInt 为前缀的字节序列 (参见 String) 。
  • UInt8、Int32 等 — 定长小端序整数。
  • Bool — 单个字节,取值为 0x00 或 0x01。
Role 列说明各字段由哪一方使用:
  • client — 由外部客户端设置。
  • inter-server — 仅在服务器间通信中有意义;外部客户端写入默认值即可。
  • universal — 双方均会使用。
这些表仅描述每个数据包中位于数据包类型代码之后的请求正文部分。

ClientHello (数据包类型 0)

客户端 → 服务器。TCP 连接建立后发送的第一条消息。

ServerHello (数据包类型 0)

服务器 → 客户端。身份验证成功后,服务器以此数据包回复 ClientHello。 Rule —— password_complexity_rules 中的元素: 该列表反映服务器运维人员配置的密码策略,仅供参考——服务器在握手期间不会强制执行这些规则。提供密码修改/设置功能的客户端可利用这些规则提前提示错误,而不必等到将不合规的密码发送至服务器后才发现问题。
为防止恶意或配置错误的服务器耗尽资源,应将解码出的 count 限制在 256 条以内,并将每个 pattern 和 message String 限制在 4096 字节以内。对于未配置密码策略的服务器,count 通常为 0 (后面不跟任何条目) 。

附加数据 (无数据包类型)

客户端 → 服务器,受 ADDENDUM (v54458) 控制。在握手交换完成后立即发送。它并不是一种独立的数据包类型——这些字段以原始形式直接线上传输,前面不带数据包类型字节前缀。 分块成帧的切换在此附加数据刷写之后才生效——附加数据本身不带帧信息。

Ping (数据包类型 4)

客户端 → 服务器。无请求正文——在分块成帧之前,该数据包仅包含单个字节 0x04;若协商启用了分块,该字节将作为一个块的单字节载荷发送 (参见分块成帧) 。

Pong (数据包类型 4)

服务器 → 客户端。无请求正文——在进行分块成帧之前,该数据包仅包含单个字节 0x04;若协商启用了分块,该字节将作为一个块的单字节载荷发送 (参见分块成帧) 。

Exception (数据包类型 2)

服务器 → 客户端。服务器在任一阶段遇到错误时发送。

Query (数据包类型 1)

客户端 → 服务器。

ClientInfo (嵌入在 Query 中)

客户端 → 服务器,嵌入在 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 字段。最后两个字段携带匹配到的、以 SQL 定义的 HTTP handler 名称及请求 URL,使其能够传递到远程分片 (若 HTTP 查询未经 handler 处理,则为空) 。
  • 其他任何接口:既不写入 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) :

服务器间身份验证

Query 字段 4 (auth_hash) 在传输格式中并不是共享的集群 secret。直接发送原始 secret 不仅会导致身份验证失败,还会造成 secret 泄露。实际做法是:作为服务器间客户端的服务器使用加盐的 SHA-256 哈希来证明自己知道该 secret:
  1. 进入服务器间模式。 发起连接的服务器通过 ClientHello 表明这一点:user 字段设为服务器间标记,password 为空。随后,它在 user/password 字段之后紧接着追加两个字符串:集群名称,以及一个新生成的 32 字节 salt (对随机值执行 encodeSHA256 的结果) 。这两个字符串同属一个 ClientHello 数据包。服务器会在发送 ServerHello 之前读取这两个字符串,因此客户端必须预先写入;如果先等待 ServerHello,就会发生死锁,因为此时服务器正阻塞在读取这两个字符串上。
  2. 获取 nonce。 若协商了 INTERSERVER_SECRET_V2 (v54462) ,ServerHello 会携带一个 8 字节的 UInt64 nonce。
  3. 计算哈希。 对于每个非 InitialQuery 的 Query 数据包,客户端会将 encodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles + current_roles) 写入字段 4,即一个 32 字节的摘要。 (nonce 取其十进制字符串形式,仅在协商版本 ≥ v54462 时存在;external_roles 仅在协商了 INTERSERVER_EXTERNALLY_GRANTED_ROLES (v54472) 时追加;current_roles 是序列化后的角色名列表,格式为 [VarUInt count][String]*count,不含存在标志字节,仅在协商了 INTERSERVER_CURRENT_ROLES (v54488) 且该列表存在时追加。) 对于 InitialQuery,或未配置集群 secret 的情况,客户端则写入空字符串。
  4. 验证。 服务器读取字段 4 (上限为 32 字节) ,并使用自己持有的集群 secret 重新计算同样的拼接结果;若两个摘要不一致,则拒绝该连接。
外部 (非服务器间) 客户端从不进入此模式,始终发送空的 auth_hash。

Setting

以内联方式编码在 Query 请求正文的 settings 列表中 (即 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 — 一个 2 位层级 字段,而非独立标志:0x00 = Production,0x04 = Obsolete,0x08 = Experimental,0x0c = Beta。需同时读取这 2 位 (flags & 0x0c) ——若仅用 flags & 0x04 做简单判断,会将 Beta (0x0c) 误判为 Obsolete。
  • 0x80 — HotReload (无需重启即可重新加载 config;定义于 flags 枚举中,主要见于协调相关设置) 。

参数

查询参数,用于诸如 SELECT {x:UInt64} 之类的参数化查询。其编码方式与设置了 Custom 标志 (0x02) 的 Setting 完全相同,同样以空键作为结束标记。
参数值是该值的 SQL 表示形式,而非原始字面量。String 类型的参数在传递前必须已用单引号括起 (例如,{name:String} 的值应为 'Alice',而非 Alice) ,否则会被服务器的值解析器拒绝。

Data (数据包类型 1:服务器→客户端;数据包类型 2:客户端→服务器)

双向通用。用于承载结果块、INSERT 数据、外部表以及数据结束标记。 两个方向的传输格式完全对称——都会在 Block 之前带有一个 table_name 前缀,唯一的区别在于数据包类型字节。
数据结束标记是指 Block 为空 (0 列且 0 行) 的数据包,与 table_name 无关。只有当解码后的块为空 (block.empty()) 时,服务器才会将客户端 Data 数据包视为终止符;table_name = "" 但块非空的数据包是普通的行数据包,而非终止符。因此,INSERT 行流由一系列非空 Data 块组成,最后以一个空 Data 块结束。 各种块变体及其含义详见 块的各种变体。

Progress (数据包类型 3)

服务器 → 客户端。在查询执行期间定期发送。所有字段均为 VarUInt,每个数据包携带的是自上一个 Progress 数据包以来的增量,而非累计总数。发送前,服务器会读取其计数器并以原子方式将其清零,同时将自上次发送以来的时间间隔作为 elapsed_ns。因此,客户端必须在本地累加陆续收到的数据包,才能得到实时总数。如果将单个数据包视为绝对值,一旦收到多个数据包,进度显示就会出现倒退或计数偏少。

ProfileInfo (数据包类型 6)

服务器 → 客户端。每个查询发送一次,在执行即将结束时发送。

Totals (数据包类型 7)

服务器 → 客户端。在查询带有 WITH TOTALS 时发送。其传输格式与 Data 完全相同:先是一个 table_name 字符串 (始终为空) ,随后是一个 Block。两者仅在数据包类型字节上有所不同。

Extremes (数据包类型 8)

服务器 → 客户端。在启用 extremes 设置时发送。传输格式与 Data 相同。该块恰好包含 2 行:第 0 行为各列的最小值,第 1 行为各列的最大值。

日志 (数据包类型 10)

服务器 → 客户端。当查询启用了活跃的日志队列时发送 (通过 send_logs_level 设置开启;参见日志流式传输) 。 外层封装和请求正文格式均与 Data 相同。该块固定为 num_columns = 8,并采用预定义的 schema。每条日志对应一行,横跨全部 8 列;单个日志数据包可包含多行。
共 8 列,严格按以下顺序排列:

ProfileEvents (数据包类型 14)

服务器 → 客户端。用于传输每个查询的性能计数器。 封装结构和请求正文格式与 Data 相同。该块的列数固定为 num_columns = 6,并使用预定义的 schema。每个事件对应一行。
共 6 列:
value 列的元素类型在不同数据包中并不固定:较旧的服务器发送 UInt64,较新的服务器则发送 Int64。请从块头中读取该列的类型字符串,不要假定其为某一固定宽度。

TableColumns (数据包类型 11)

服务器 → 客户端,受 COLUMN_DEFAULTS_METADATA (v54410) 控制。服务器会在 INSERT schema 块之前发送该数据包,用于携带列默认值元数据,但仅当协商版本 ≥ 54410 且 已启用 input_format_defaults_for_omitted_fields 设置时才会发送。版本低于 54410 时,服务器从不发送该数据包,因此旧版客户端不得等待它——schema Data 块会直接到达。v54410 及以上版本的客户端应能处理两种情况:先收到可选的 TableColumns,再收到 schema 块;或直接收到 schema 块。
v54481 及以上版本中的压缩请求正文当协商版本 ≥ 54481 (COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS) 时,服务器会通过同一条可选压缩的输出路径写入两个字段。因此,当查询设置了 compression = true 时,整个 TableColumns 请求正文 (external_table + columns_description) 都位于压缩帧内,客户端需通过对应的解压流读取。当查询未启用压缩时,请求正文以未压缩形式线上传输,格式与上表完全一致。这一点对 INSERT 的 schema 响应至关重要:如果客户端只为 Log 和 ProfileEvents 切换了压缩处理,而没有为 TableColumns 切换,那么在启用查询压缩时就会错误解析响应。

TimezoneUpdate (数据包类型 17)

服务器 → 客户端,受 TIMEZONE_UPDATES (v54464) 控制。该数据包只在一处发送:input 表函数的初始化阶段 (即形如 INSERT INTO <table> SELECT ... FROM input('<structure>') 的查询,该查询会从客户端流式接收行) 。服务器发送完输入 schema 的 Data 块后 (参见 INSERT 阶段) ,会立即发出 TimezoneUpdate,携带查询上下文当前的 session_timezone,使客户端在解析即将发送的行时采用相同的时区。如果在查询执行过程中任意执行 SET session_timezone 更改时区,服务器不会为此发出该数据包,也不会用它来告知客户端如何格式化后续的结果块。 该数据包只会出现一次,紧跟在输入 schema 块之后、客户端开始发送行块之前。即使解码器忽略 TimezoneUpdate,也必须读取其后尾随的 String,以保持线路数据对齐。

SSH 质询-响应身份验证 (数据包类型 11、12、18)

受 SSH_AUTHENTICATION (v54466) 控制,且仅在显式启用时生效。当 ClientHello 发送 user = " SSH KEY AUTHENTICATION " + <real_user> (包含前导和尾随空格) 以及 password = "" 时,连接即进入 SSH 流程。服务器读取该前缀并将其去除,从而还原出真实用户名,随后切换到质询-响应模式。 该流程取代密码身份验证,且质询-响应交互发生在 ServerHello 之前——服务器会等到身份验证成功后才发送 Hello 回复:
  1. 客户端发送带有 SSH 标记前缀和空密码的 ClientHello。
  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 质询-响应流程会穿插在握手过程中,并在任何 ServerHello 出现之前完成。
不使用 SSH 身份验证的外部客户端永远不会收到数据包 11、12 或 18——除非用户通过用户名前缀显式启用,否则这些数据包根本不会在线路上传输。

MergeTreeAllRangesAnnouncementResponse (数据包类型 14)

客户端 → 服务器,仅用于服务器间通信。受 parallel_replicas_protocol_version ≥ 8 控制 (参见 VERSIONED_PARALLEL_REPLICAS_PROTOCOL) 。外部客户端从不发送此数据包。 当协商得到的并行副本协议版本 ≥ 8 时,发起服务器处理跟随者发来的 MergeTreeAllRangesAnnouncement (数据包类型 15,服务器→客户端方向) 时,其请求/响应流程会有所变化:
  1. 跟随者打开其读取管道,并向发起服务器发送 MergeTreeAllRangesAnnouncement。
  2. 仅当通知的 mode 不为 Default 时 (即 WithOrder = 1 或 ReverseOrder = 2,二者均用于按序并行读取) ,发起服务器才会回复 MergeTreeAllRangesAnnouncementResponse。当 mode = Default = 0 时,发起服务器不作回复,跟随者也不会等待——Default 模式会随每个 MergeTreeReadTaskRequest 分配 ranges,因此无需预先获取分片列表。
  3. 在发出第一个 MergeTreeReadTaskRequest (服务器数据包 16,由跟随者发送给发起服务器;发起服务器以客户端数据包 10 MergeTreeReadTaskResponse 回复) 之前,跟随者会阻塞等待该响应 (若预期会收到) ,并根据返回的分片列表过滤数据源的构建,使其恰好只覆盖其 #split_i stream 所拥有的分片。
在低于 8 的版本中,无论处于何种模式,该通知都是发出即忘的,跟随者会基于本地已知的全部分片构建数据源 (即旧版行为) 。

请求正文

RangesInDataPartsDescription 请求正文

RangesInDataPartDescription 请求正文

MergeTreePartInfo 请求正文

MarkRanges 请求正文

数据包类型参考

客户端 → 服务器

服务器 → 客户端

QueryPlan

QueryPlan 是一种仅用于服务器间通信的客户端数据包。它紧跟在 stage 为 QueryPlan (7) 的 Query 数据包之后,包含一个序列化的计划 stream。 在版本 10 之前,不存在字段 2 和 3。如果发起服务器设置了任一非默认的执行限制,则不得发送这种旧布局,而应改为发送原始 SQL 查询,由旧版接收方根据查询设置重建这些限制。

配置

本节介绍影响原生协议连接的各项可调参数: 以下默认值基于较新的服务器版本,在不同版本和部署环境中可能有所不同。

传输层设置

套接字选项

超时

各超时之间的嵌套关系如下:
操作系统的 keepalive 最先触发,可能在内核层面静默地检测出已失效的对端。应用程序的接收超时是第二道防线。空闲超时则是最后的兜底手段,负责回收长时间未使用的连接。

连接限制

持续定期发送查询的连接可以无限期存活。只有空闲连接会在一小时后被回收,且默认不设最长生命周期。

应用层设置

这些设置按查询逐一传递,位于 Query 数据包的 settings 列表中。它们会改变服务器线上传输的内容或其分帧方式。

压缩

Query 数据包中的 compression 标志 (字段 6) 用于开启或关闭压缩;启用压缩时,由上述设置决定所使用的编解码器。

日志流式传输

只要将 send_logs_level 设为 "none" 以外的值,服务器就会在查询执行期间发送 日志 数据包。

进度报告

这是目标最小值,而非严格的上限:当查询产出工作量的速度不够快时,服务器发送 Progress 数据包的频率可能会更低。

结果封装

异步 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_* 设置则是例外:它们会改变原生 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 (客户端数据结束标记) — 客户端为关闭输入 stream 而发送的空 Data 数据包 (0 列,0 行) 。其发送时机因查询类型而异:
  • 普通查询 (SELECT 等) : 在 Query 数据包以及所有外部表 Data 数据包之后发送,表示“没有更多外部数据”。服务器随后开始执行查询。
  • INSERT: 客户端不会在 schema 之前发送标记。服务器先发送 schema 块,客户端随后以流式方式发送行 Data 块,最后才发送空 Data 数据包来终止行 stream。如果在 schema 块之前发送空标记,服务器会将其视为行数据立即结束,从而导致数据丢失。
Feature (功能) — 在特定协议版本中引入的线上传输格式变更。当协商版本大于或等于该功能对应的版本时生效。参见版本控制与功能开关。 Inter-server (服务器间) — 一种字段角色标签,表示该字段仅在服务器之间的分布式查询中有意义。外部客户端写入默认值即可 (通常为空字符串、0 或 false) 。 Negotiated version (协商版本) — min(client_version, server_version),在握手期间计算得出,决定了在整个连接生命周期内哪些功能处于活跃状态。 Packet (数据包) — 一条线上传输消息:由 VarUInt 数据包类型代码及随后的请求正文组成,请求正文格式取决于类型。参见数据包封装。 Packet type code (数据包类型代码) — 位于数据包开头、用于标识其格式的 VarUInt。目前已分配的值为 0–18。参见数据包类型参考文档。 Response stream (响应 stream) — 服务器在查询期间发出的数据包序列。其长度不固定,且必定以唯一一个 EndOfStream (成功) 或 Exception (失败) 结束。参见查询阶段。 Schema block (schema 块) — 服务器在 INSERT 阶段发送的头部块 (一个包含列但行数为 0 的 Block) ,用于在客户端发送数据之前声明预期的列结构。 Settings list (settings 列表) — Query 请求正文中由 (key, flags, value) 元组组成的序列,以空键结尾,用于承载每个查询的应用层配置。参见 Setting。 Stage (阶段) — Query 数据包中的一个 VarUInt 字段 (字段 5) ,用于控制服务器将查询执行到哪个阶段。外部客户端通常发送 2 (Complete) ;分布式查询和序列化查询计划则使用更高的值。完整的线上传输取值请参见 Query 字段 5。 Terminator (终止符) — 用于结束 stream 的数据包。Query 响应以 EndOfStream (成功) 或 Exception (失败) 结束;客户端的输入 stream 则以空 Data 标记结束。