INSERT, telemetría de ejecución y señales de error. Es el protocolo en el que se basan el cliente de línea de comandos, el controlador de C++ y la mayoría de los controladores nativos de terceros.
Esta página describe el protocolo en sí: la estructura de paquetes, la máquina de estados de la conexión, la negociación de versión y el cuerpo de todos los mensajes distintos de Block. Los bytes contenidos en los paquetes de la familia Data (el Block, sus columnas y las codificaciones de cada tipo) son una cuestión aparte, que se documenta en la especificación del formato Native.
Especificación complementariaEsta página forma parte de un par de documentos y se publica junto con la especificación complementaria del formato Native. Ambas especificaciones se reparten el trabajo de forma nítida: esta página se ocupa de la capa de paquetes y de transporte, mientras que la especificación del formato Native se ocupa de los bytes contenidos en los paquetes de la familia
Data.BlockInfo, de modo que un solo byte fuera de lugar desincroniza todo lo que viene después. Es un protocolo con estado, y cada conexión TCP procesa una sola consulta a la vez: no hay multiplexación. Los enteros de ancho fijo se codifican en little-endian.
Descripción general
Cada mensaje en el wire comienza con un código de tipo de paquete
VarUInt, seguido de un cuerpo cuya estructura depende de ese código y de la versión del protocolo negociada.
Una conexión pasa por tres fases: un único handshake inicial, después cualquier número de intercambios Ping o Query y, por último, el cierre:
El protocolo TCP nativo siempre transporta datos tabulares en formato Native, independientemente de la cláusula FORMAT que incluya la consulta SQL. La conversión a RowBinary, CSV, JSON, etc., es tarea del client, que la realiza después de decodificar los bloques Native. (La interfaz HTTP sigue una ruta de código distinta que sí respeta la cláusula FORMAT; HTTP queda fuera del alcance de este documento.)
Seguridad
Seguridad de transporte (TLS)
TLS opera en la capa de transporte, por debajo del protocolo. Cuando está habilitado, se cifra todo el flujo TCP y los mensajes del protocolo son idénticos byte a byte, se use TLS o no.Autenticación
La autenticación tiene lugar durante el handshake, en el mensajeClientHello. Los campos user y password se transmiten como cadenas en texto plano, por lo que es el cifrado a nivel de transporte (TLS) el que protege las credenciales en tránsito.
El servidor interpreta un campo user vacío como su usuario de sesión predeterminado configurado, es decir, el valor del ajuste de servidor default_session_user (cuyo valor predeterminado es default), que puede sobrescribirse para cada listener en la sección protocols. Si el servidor tiene configurado un usuario de sesión predeterminado vacío, o si su versión es anterior a la 26.8, se rechaza el campo user vacío con una excepción. Tenga en cuenta que clickhouse-client nunca envía un nombre de usuario vacío, sino que lo sustituye por default en el lado del cliente.
La autenticación SSH de tipo desafío-respuesta está disponible a partir de la versión de protocolo 54466; consulte Autenticación SSH de tipo desafío-respuesta.
Secreto entre servidores
Para la ejecución distribuida de consultas, los servidores se autentican entre sí demostrando que conocen un secreto compartido, sin llegar a transmitirlo por el wire. Cada Query incluye unauth_hash SHA-256 de 32 bytes en el campo 4 de Query, calculado a partir de una sal, un nonce, el secreto configurado y la consulta; el servidor receptor lo vuelve a calcular y lo compara. Esta funcionalidad depende de la feature INTERSERVER_SECRET (v54441). Los client externos siempre envían una cadena vacía en este campo. Consulte Autenticación entre servidores.
Control de versiones y control de funcionalidad
Negociación de versión
Durante el handshake, tanto el client como el servidor declaran la versión máxima del protocolo que admiten. La versión negociada es la menor de las dos:Control de funcionalidad
Cada funcionalidad se identifica por la versión del protocolo en la que se introdujo y está activa cuando la versión negociada es mayor o igual que ese número.Tabla de funcionalidades
Envoltura del paquete
Todos los mensajes que se transmiten por el wire comparten la misma estructura externa, en ambas direcciones:VarUInt, no un byte de ancho fijo. Para valores inferiores a 128, un VarUInt genera ese mismo byte único, pero las implementaciones deben usar la codificación VarUInt para mantener la compatibilidad en caso de que futuros tipos de paquete alcancen el valor 128 o superior.
La referencia de mensajes documenta únicamente el cuerpo de cada paquete, es decir, los bytes que siguen al código de tipo de paquete. La numeración de los campos comienza en 1 con el primer campo del cuerpo.
Enmarcado por fragmentos (v54470+)
Cuando se negocia la funcionalidadCHUNKED_PROTOCOL (consulte el handshake), cada paquete que se transmite en formato wire se encapsula mediante enmarcado por fragmentos. El encapsulado se aplica por dirección: los sentidos client→servidor y servidor→client se negocian por separado y pueden acabar en modos distintos (por fragmentos o sin enmarcar).
Disposición en formato wire de cada paquete:
VarUInt del tipo de paquete está dentro del stream fragmentado: es el primer byte de la carga útil del paquete (el primer byte del primer fragmento), no un byte independiente enviado antes del encapsulado. La carga útil de fragmento de cada paquete es el [VarUInt packet_type_code][message body] completo del sobre del paquete. Si un cliente deja el tipo de paquete fuera del stream fragmentado, el otro extremo leerá ese byte de tipo como el primer byte del tamaño de fragmento u32, lo que desincroniza la conexión.
Un mismo paquete puede dividirse en varios fragmentos si el búfer del escritor se llena a mitad del paquete; la división puede producirse en cualquier punto, incluso dentro del VarUInt del tipo de paquete. El lector concatena las cargas útiles de los fragmentos y trata el cero final de 4 bytes como un límite de paquete transparente: lo consume, pero no lo expone al componente que lea los cuerpos de los paquetes.
Los paquetes sin cuerpo también se encapsulan: un paquete de un solo byte como Ping o Pong se convierte en [u32 size = 1][0x04][u32 0] una vez negociada la fragmentación. Cualquier descripción de “un solo byte en el wire” que aparezca en otras partes de esta página corresponde a la forma previa a la fragmentación.
Negociación. ServerHello y Addendum transportan cada uno dos campos String, uno por dirección, con valores obtenidos de {"chunked", "notchunked", "chunked_optional", "notchunked_optional"}:
chunked/notchunkedson estrictos: ese lado exige exactamente ese modo.- Las variantes
_optionalson flexibles: aceptan el modo que elija el otro lado.
En el lado del cliente, la preferencia de ENVÍO del cliente se negocia con la preferencia de RECEPCIÓN del servidor, y viceversa.
Momento. Las cadenas de negociación viajan por el wire sin encapsular: ClientHello → ServerHello (preferencias del servidor) → Addendum (valores negociados por el cliente). El cambio de encapsulado se aplica a todos los bytes enviados después del vaciado del Addendum. El propio Addendum, el ClientHello y el ServerHello nunca se encapsulan.
Ciclo de vida de la conexión
En todo momento, una conexión se encuentra exactamente en uno de estos cuatro estados:HANDSHAKE, READY, READING_RESPONSE o terminada. Como el protocolo no admite multiplexación, un client que envía una nueva solicitud antes de vaciar la respuesta anterior intercala bytes en la transmisión y corrompe el flujo de datos.
Estados
El flujo nominal avanza en línea recta hacia abajo —HANDSHAKE → READY → READING_RESPONSE → READY—, con el bucle Ping/Pong sobre sí mismo y todas las aristas de fallo confluyendo en el único sumidero Terminated.
Fase de handshake
Autentica al cliente y negocia la versión del protocolo. Se produce exactamente una vez por conexión, antes que cualquier otra operación. La conexión TCP acaba de abrirse y todavía no se ha intercambiado ningún mensaje. El flujo es el siguiente:-
El cliente envía
ClientHellocon la versión máxima del protocolo que admite. -
El cliente lee la respuesta y actúa según el tipo de paquete:
-
Si
negotiated_version ≥ 54458(la funcionalidadADDENDUM), el cliente envía unAddendum. Esta decisión se basa en la versión negociada, no en la versión declarada por el cliente.
READY; ante cualquier error, se cierra.
Fase de Ping
Una comprobación de actividad a nivel de aplicación, independiente del keepalive de TCP. Un intercambio Ping/Pong completado con éxito confirma que la conexión TCP sigue activa en ambas direcciones y que el servidor responde. Ping no tiene estado ni está asociado a ninguna consulta, por lo que varios Pings consecutivos son independientes entre sí. A partir deREADY, el flujo es el siguiente:
-
El client envía
Ping. -
El client lee la respuesta:
Fase de consulta
El cliente envía una sentencia SQL y el servidor transmite progresivamente bloques de resultados y telemetría de ejecución. La respuesta es una secuencia de paquetes que finaliza con un únicoEndOfStream o Exception.
A partir de READY, el flujo es el siguiente:
Si se produce un error en cualquier momento, el servidor envía un Exception en lugar de EndOfStream, lo que pone fin a la consulta.
-
El client envía
Querycon unquery_idúnico (normalmente un UUID). -
El client envía las tablas externas que haya y, a continuación, el marcador Data vacío. El paquete Data vacío tiene
table_name = "",num_columns = 0,num_rows = 0. El servidor no empieza a ejecutar la consulta hasta recibir este marcador. Una tabla externa puede dividirse en varios paquetesDatacon el mismotable_name; el primero de ellos fija el esquema de la tabla, y todos los paquetes posteriores con ese nombre deben contener los mismos nombres y tipos de columna, en el mismo orden; de lo contrario, el servidor rechaza la consulta conINCORRECT_DATA(consulte Data). -
El client pasa a
READING_RESPONSEy vacía su búfer de escritura. -
El client lee los paquetes de respuesta en un bucle y los procesa según su tipo:
EndOfStream o un Exception gestionado, la conexión vuelve a READY. Una violación del protocolo o un error de E/S la cierra.
El caso
num_rows == 0 suele confundir a las nuevas implementaciones. Un bloque sin filas es un marcador de límite o un encabezado de esquema, no una señal de fin de flujo. Solo EndOfStream o Exception ponen fin a la respuesta.Fase INSERT
La fase INSERT es la fase de consulta con dos intercambios adicionales. El client envía una sentenciaINSERT; el servidor responde con un bloque de esquema que describe la tabla de destino; el client transmite en flujo paquetes Data con las filas y, a continuación, el marcador Data vacío; el servidor termina con EndOfStream o Exception.
A partir de READY, el SQL es un INSERT con la forma INSERT INTO <table> [(<cols>)] VALUES, sin ningún literal VALUES (...) en línea, ya que los datos de las filas viajan en paquetes Data. El flujo es el siguiente:
- El cliente envía
Queryconbodyestablecido en la sentencia SQL INSERT. - El cliente envía las tablas externas que haya (algo poco habitual en un INSERT). A diferencia de la fase Query, aquí no envía un marcador Data vacío. El paquete
QuerydelINSERTse envía con datos pendientes, por lo que el bloque vacío de fin de datos se aplaza hasta el paso 5; si se enviara antes del bloque de esquema, el servidor lo interpretaría como el final del flujo de filas, finalizaría el INSERT sin filas y luego analizaría el primer paquete de filas real como un paquete de nivel superior inesperado. - El cliente consume los paquetes de metadata (TableColumns, Progress, ProfileInfo, Log, ProfileEvents) hasta que lee el paquete Data de esquema: un Block con 0 filas pero con la estructura completa de columnas (nombres y tipos). El bloque de esquema actúa como contrato: las filas que el cliente envíe a continuación deben ajustarse a la forma de estas columnas.
- El cliente envía uno o varios bloques de datos. Para cada bloque escribe
VarUInt(ClientPacket::Data = 2), luegoString("")como nombre vacío de tabla externa y, por último, el Block. Los tipos de columna deben corresponderse por posición con las columnas del bloque de esquema. - El cliente envía el terminador de fin de entrada: un paquete Data con un Block vacío (0 columnas, 0 filas).
- El cliente consume el flujo de respuesta hasta recibir
EndOfStream(éxito) oException(error).
async_insert = 1, el servidor encola las filas y las vacía como parte de un lote. Con una versión negociada ≥ 54484 (PROGRESS_IN_ASYNC_INSERT), una vez completado el vaciado, el servidor emite un paquete Progress adicional, seguido inmediatamente de los ProfileEvents del insert y, a continuación, de EndOfStream. Con versiones inferiores a 54484, el servidor omite ese Progress final. El paquete es un Progress normal; como el servidor reinicia la canalización de la consulta antes de incorporar los recuentos de escritura, en la práctica el incremento solo contiene el tiempo transcurrido, y las estadísticas de filas y bytes escritos llegan al cliente a través de los ProfileEvents que lo acompañan. A un cliente que ya consume paquetes Progress intercalados en el paso 6 le basta con aceptar un paquete más.
La conexión vuelve a READY al recibir EndOfStream o una Exception gestionada. Las violaciones del protocolo y los errores de E/S cierran la conexión.
Referencia de mensajes
Los campos se enumeran en orden de wire. La columnaType utiliza:
VarUInt: integer sin signo de longitud variable (consulte VarUInt).String: bytes precedidos por un VarUInt (consulte String).UInt8,Int32, etc.: integers de ancho fijo en formato little-endian.Bool: un único byte,0x00o0x01.
Role indica quién utiliza cada campo:
- client: lo establecen los client externos.
- inter-server: solo es relevante en la comunicación entre servidores; los client externos escriben un valor predeterminado.
- universal: lo utilizan ambos.
ClientHello (tipo de paquete 0)
Client → Server. Es el primer mensaje que se envía tras abrirse la conexión TCP.ServerHello (tipo de paquete 0)
Server → Client. La respuesta a ClientHello cuando la autenticación se completa correctamente.
Rule: un elemento de
password_complexity_rules:
La lista refleja la política de contraseñas configurada por el operador del servidor y es meramente orientativa: el servidor no aplica estas reglas durante el handshake. Un client que permita cambiar o establecer contraseñas puede usar las reglas para señalar errores antes de enviar al servidor una contraseña que no las cumpla.
Para acotar el consumo de recursos frente a un servidor hostil o mal configurado, limite el
count decodificado a 256 entradas y cada String pattern y message a 4096 bytes. Un count de 0 (sin pares a continuación) es el caso habitual en servidores que no tienen configurada ninguna política de contraseñas.Addendum (sin tipo de paquete)
Client → Server, condicionado porADDENDUM (v54458). Se envía inmediatamente después de que finaliza el intercambio de handshake. No es un tipo de paquete propio: los campos se transmiten on the wire sin procesar, sin el byte de tipo de paquete como prefijo.
El cambio al enmarcado fragmentado se aplica después del vaciado de este Addendum; el propio Addendum se envía sin enmarcar.
Ping (tipo de paquete 4)
Client → Server. Sin cuerpo: el paquete consiste en un único byte0x04 antes del enmarcado por fragmentos; cuando se negocia la fragmentación, ese byte pasa a ser la carga útil de un byte de un fragmento (consulte enmarcado por fragmentos).
Pong (tipo de paquete 4)
Server → Client. Sin cuerpo: el paquete consiste en un único byte0x04 antes del enmarcado por fragmentos; cuando se negocia la fragmentación, ese byte pasa a ser la carga útil de un byte de un fragmento (consulte enmarcado por fragmentos).
Exception (tipo de paquete 2)
Server → Client. Se envía cuando el servidor encuentra un error en cualquier fase.Query (tipo de paquete 1)
Client → Server.ClientInfo (incrustado en Query)
Client → Server, incrustado en el cuerpo de Query (field 2). Condicionado porCLIENT_INFO (v54032). (Algunos campos de ClientInfo dependen de versiones posteriores, como se indica en cada campo más abajo).
Disposición dependiente de la interfaz (campos 7–12)Los campos 7–12 anteriores corresponden a la rama TCP. Cuando
query_interface (campo 6) no es TCP, estos campos se sustituyen por una disposición en formato wire diferente; no se trata simplemente de omisiones opcionales, por lo que un decodificador debe bifurcarse en función del campo 6.query_interface = 2(HTTP): en su lugar se escribe la información de la solicitud HTTP redirigida por el servidor:http_method(UInt8),http_user_agent(String), luegoforwarded_for(String, condicionado porX_FORWARDED_FOR_IN_CLIENT_INFOv54443),http_referer(String, condicionado porREFERER_IN_CLIENT_INFOv54447) y, por último,http_handler_name(String) yhttp_request_url(String) (ambos condicionados porHTTP_HANDLER_IN_CLIENT_INFOv54490, en ese orden). No se incluyen los camposos_user/client_hostname/client_name/version_*/protocol_version. Los dos últimos contienen el nombre del handler HTTP definido en SQL que se ha asociado a la solicitud y la URL de esta, para que lleguen hasta los segmentos remotos (quedan vacíos en una consulta HTTP que no pase por un handler).- Cualquier otra interfaz: no se escribe ninguno de los campos TCP (7–12) ni de los campos HTTP; el stream continúa directamente con
quota_key.
quota_key (campo 13) y distributed_depth (campo 14) aparecen a continuación en todas las interfaces, y después version_patch (campo 15) se escribe solo para TCP.Esta rama es relevante sobre todo para el tráfico entre servidores, cuando el servidor iniciador redirige una consulta que llegó originalmente por HTTP. Un decodificador que lea siempre los campos TCP interpretará mal estos paquetes y tratará http_method o http_user_agent como quota_key.Autenticación entre servidores
El campo 4 de Query (auth_hash) no es el secreto compartido del cluster tal como viaja on the wire. Enviar el secreto sin procesar haría fallar la autenticación y, además, lo expondría. En su lugar, un servidor que actúa como client entre servidores demuestra que conoce el secreto mediante un hash SHA-256 con sal:
- Entrar en modo entre servidores. El servidor que inicia la conexión lo indica dentro de
ClientHello: el campousercontiene el marcador de modo entre servidores ypasswordestá vacío. A continuación, añade dos cadenas más —el nombre del cluster y unsaltde 32 bytes recién generado (encodeSHA256de un valor aleatorio)— justo después de los camposuser/password, dentro del mismo paqueteClientHello. El servidor lee estas dos cadenas antes de enviarServerHello, por lo que el client debe escribirlas de antemano; si espera primero aServerHello, se produce un interbloqueo, porque el servidor se queda bloqueado esperando leerlas. - Obtener el nonce.
ServerHelloincluye un nonceUInt64de 8 bytes cuando se negociaINTERSERVER_SECRET_V2(v54462). - Calcular el hash. En cada paquete Query que no sea
InitialQuery, el client escribeencodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles + current_roles)en el campo 4: un digest de 32 bytes. (noncese representa como cadena decimal y solo está presente cuando se negocia ≥ v54462;external_rolesse añade solo cuando se negociaINTERSERVER_EXTERNALLY_GRANTED_ROLES(v54472);current_roleses la lista serializada de nombres de roles —[VarUInt count][String]*count, sin el byte de presencia— y se añade solo cuando se negociaINTERSERVER_CURRENT_ROLES(v54488) y la lista está presente). En el caso de unaInitialQuery, o si no hay ningún secreto de cluster configurado, el client escribe una cadena vacía. - Verificar. El servidor lee el campo 4 con un límite de 32 bytes y recalcula la misma concatenación con su propia copia del secreto del cluster; si los digests no coinciden, rechaza la conexión.
auth_hash vacío.
Setting
Se codifica en línea dentro de la Lista de Settings del cuerpo de Query (el paquete Query, campo 3). La lista está siempre presente, independientemente de la versión negociada, y termina con un Setting con clave vacía: un únicoVarUInt 0, sin indicadores ni valor a continuación. Lo único que depende de la versión negociada es la codificación de cada setting, condicionada por SETTINGS_SERIALIZED_AS_STRINGS (v54429).
v54429+ (STRINGS_WITH_FLAGS): cada setting es la siguiente tripleta:
Los campos 2 y 3 se omiten cuando
key está vacío.
Anterior a 54429 (BINARY): cada setting es [String key][type-specific binary value]. El campo flags no se escribe, y el valor se codifica en la forma binaria nativa del setting (por ejemplo, un integer de ancho fijo o una cadena precedida de su longitud) en lugar de como una cadena decimal o de texto. La lista sigue terminando con una key vacía. Un client que trabaje con una versión negociada inferior a 54429 debe leer y escribir esta forma binaria, no la tripleta anterior. (La excepción son los settings personalizados definidos por el usuario: siempre incluyen flags y un valor de cadena, en ambas codificaciones).
El campo flags agrupa:
0x01— Important: el setting afecta a los resultados de la consulta y los peers más antiguos no deben ignorarlo silenciosamente.0x02— Custom: un setting personalizado definido por el usuario.0x0c— un campo de nivel de 2 bits, no un indicador independiente:0x00= Production,0x04= Obsolete,0x08= Experimental,0x0c= Beta. Lea ambos bits (flags & 0x0c): una comprobación simplista comoflags & 0x04clasificaría erróneamente Beta (0x0c) como Obsolete.0x80— HotReload (recarga de la configuración sin reiniciar; definido en el enum de indicadores, aparece principalmente en los settings de coordinación).
Parámetro
Parámetros de consulta, para consultas parametrizadas comoSELECT {x:UInt64}. Se codifican exactamente igual que un Setting con el indicador Custom (0x02) activado y, del mismo modo, terminan con una clave vacía.
El valor del parámetro es la representación SQL del valor, no un literal sin procesar. Los parámetros de tipo String deben pasarse ya entre comillas simples (por ejemplo, el valor de
{name:String} es 'Alice', no Alice); de lo contrario, el analizador de valores del servidor los rechazará.Data (tipo de paquete 1 servidor→cliente, tipo de paquete 2 cliente→servidor)
Se usa en ambas direcciones. Transporta bloques de resultados, datos de INSERT, tablas externas y marcadores de fin de datos. El formato wire es simétrico: en ambas direcciones se incluye un prefijotable_name antes del Block. Lo único que cambia es el byte del tipo de paquete.
El marcador de fin de datos es un paquete cuyo Block está vacío (
0 columnas y 0 filas), sea cual sea el valor de table_name. El servidor solo trata un paquete Data del cliente como terminador cuando el bloque decodificado está vacío (block.empty()); un paquete con table_name = "" y un bloque no vacío es un paquete de filas ordinario, no un terminador. Así pues, un stream de filas de INSERT consiste en una secuencia de bloques Data no vacíos seguida de un único bloque Data vacío que lo cierra.
Las variantes de bloque y su significado se describen en Variantes de Block.
Progress (packet type 3)
Server → Client. Se envía periódicamente durante la ejecución de la consulta. Todos los campos son VarUInt, y cada paquete contiene incrementos desde el paqueteProgress anterior, no totales acumulados. Antes de enviarlo, el servidor lee sus contadores, los restablece a cero de forma atómica y calcula elapsed_ns como la diferencia de tiempo transcurrida desde el último envío. Por lo tanto, un cliente debe acumular localmente los paquetes sucesivos para obtener los totales parciales: si trata un paquete como un valor absoluto, el indicador de progreso retrocederá o mostrará valores inferiores a los reales en cuanto llegue más de un paquete.
ProfileInfo (packet type 6)
Server → Client. Se envía una vez por consulta, cerca del final de la ejecución.Totals (tipo de paquete 7)
Servidor → Cliente. Se envía en las consultas conWITH TOTALS. El formato wire es idéntico al de Data: una cadena table_name (siempre vacía) seguida de un Block. Lo único que cambia es el byte del tipo de paquete.
Extremes (tipo de paquete 8)
Server → Client. Se envía cuando la configuraciónextremes está habilitada. El formato wire es idéntico al de Data. El bloque tiene exactamente 2 filas: la fila 0 contiene el mínimo de cada columna y la fila 1, el máximo.
Log (packet type 10)
Server → Client. Se envía cuando la consulta tiene una cola de log activa (configuraciónsend_logs_level; consulte streaming de logs).
Usa el mismo envelope y el mismo formato de cuerpo que Data. El bloque tiene un valor fijo de num_columns = 8 y un esquema predefinido. Cada línea de log corresponde a una fila que abarca las 8 columnas, y un único paquete Log puede contener muchas filas.
ProfileEvents (tipo de paquete 14)
Server → Client. Transporta contadores de rendimiento por consulta. Usa el mismo envoltorio y formato de cuerpo que Data. El bloque tiene un valor fijonum_columns = 6 y un esquema predefinido. Cada evento corresponde a una fila.
El tipo de elemento de la columna
value no es el mismo en todos los paquetes: los servidores más antiguos emiten UInt64 y los más recientes, Int64. Lea la cadena de tipo de la columna en el encabezado del bloque en lugar de dar por supuesto un ancho determinado.TableColumns (packet type 11)
Server → Client, condicionado porCOLUMN_DEFAULTS_METADATA (v54410). El servidor lo envía antes del bloque de esquema de INSERT para transmitir los metadata de valores predeterminados de las columnas, pero solo cuando la versión negociada es ≥ 54410 y la configuración input_format_defaults_for_omitted_fields está habilitada. Por debajo de 54410, el paquete nunca se envía, por lo que un client más antiguo no debe esperarlo: el bloque de esquema Data llega directamente. Un client v54410+ debe estar preparado para cualquiera de los dos órdenes: un TableColumns opcional seguido del bloque de esquema, o directamente el bloque de esquema.
Cuerpo comprimido a partir de v54481Con una versión negociada ≥ 54481 (
COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS), el servidor escribe ambos campos a través de la misma ruta de salida con compresión opcional, de modo que, cuando la consulta tiene compression = true, todo el cuerpo de TableColumns (external_table + columns_description) va dentro del marco de compresión; el client lo lee a través del flujo descomprimido correspondiente. Cuando la consulta no usa compresión, el cuerpo se transmite sin comprimir, exactamente como se muestra en la tabla anterior. Esto es importante para las respuestas de esquema de INSERT: un client que adapta el tratamiento de la compresión para Log y ProfileEvents, pero no para TableColumns, interpretará mal la respuesta cuando la compresión de la consulta esté habilitada.TimezoneUpdate (packet type 17)
Server → Client, condicionado porTIMEZONE_UPDATES (v54464). Se envía en un único punto: el inicializador de la función de tabla input (una consulta de la forma INSERT INTO <table> SELECT ... FROM input('<structure>'), que transmite filas desde el client). Justo después de que el servidor envíe el bloque Data del esquema de entrada (consulte la fase INSERT), emite TimezoneUpdate con el valor actual de session_timezone del contexto de la consulta, para que el client analice las filas que está a punto de enviar con la misma zona horaria. El servidor no emite este paquete ante cambios arbitrarios mediante SET session_timezone a mitad de la consulta, ni para indicar al client cómo formatear los bloques de resultados posteriores.
El paquete llega una sola vez, inmediatamente después del bloque de esquema de entrada y antes de que el client empiece a enviar bloques de filas. Un decodificador que ignore
TimezoneUpdate DEBE consumir igualmente el String final para mantener alineado el wire.
Autenticación SSH de desafío-respuesta (tipos de paquete 11, 12, 18)
Está condicionada porSSH_AUTHENTICATION (v54466) y solo se activa de forma explícita. Una conexión entra en el flujo SSH cuando ClientHello envía user = " SSH KEY AUTHENTICATION " + <real_user> (con los espacios inicial y final) y password = "". El servidor lee el prefijo, lo elimina para recuperar el usuario real y pasa al modo de desafío-respuesta.
Este flujo sustituye a la autenticación por contraseña, y el intercambio de desafío-respuesta tiene lugar antes de ServerHello: el servidor pospone su respuesta Hello hasta que la autenticación se completa correctamente.
- El cliente envía ClientHello con el prefijo marcador SSH y una contraseña vacía.
-
El cliente envía
SSHChallengeRequest(paquete 11). El servidor aún no ha enviado ServerHello: primero procesa la autenticación y queda bloqueado en este punto a la espera de este paquete. -
El servidor responde con
SSHChallenge, que contiene bytes aleatorios (paquete 18). -
El cliente construye la cadena que se debe firmar y firma esa cadena, no el desafío sin procesar; a continuación, envía
SSHChallengeResponse(paquete 12) con la firma. El mensaje firmado es la concatenación byte a byte, sin separadores, de cuatro partes en este orden exacto: -
El servidor verifica la firma con la clave pública registrada del usuario y reconstruye la misma cadena
decimal(protocol_version) + default_database + user + challenge. Si la verificación es correcta, envíaServerHello(la misma respuesta que en el flujo con contraseña) y el handshake continúa con normalidad (Addendum, etc.); si falla, devuelve unaExceptiony cierra la conexión. Un cliente que firme únicamente los bytes sin procesar del desafío no superará la autenticación.
Esto es lo contrario de lo que ocurre en el handshake con contraseña, donde ServerHello sigue inmediatamente a ClientHello. Con la autenticación SSH, ServerHello no se envía hasta que se verifica la firma, por lo que el desafío-respuesta SSH se intercala en el handshake antes de que aparezca ningún ServerHello.
MergeTreeAllRangesAnnouncementResponse (tipo de paquete 14)
Client → Server, solo entre servidores. Condicionado porparallel_replicas_protocol_version ≥ 8 (consulte VERSIONED_PARALLEL_REPLICAS_PROTOCOL). Los clientes externos nunca envían este paquete.
Cuando la versión negociada de réplicas paralelas es ≥ 8, cambia el ciclo de solicitud/respuesta del iniciador ante un MergeTreeAllRangesAnnouncement de un seguidor (tipo de paquete 15, dirección servidor→cliente):
- Un seguidor abre su canalización de lectura y envía
MergeTreeAllRangesAnnouncemental iniciador. - Solo cuando el
modedel anuncio es distinto deDefault(WithOrder = 1oReverseOrder = 2, ambos utilizados en lecturas paralelas ordenadas), el iniciador responde conMergeTreeAllRangesAnnouncementResponse. Conmode = Default = 0, el iniciador no responde y el seguidor no espera: el modoDefaultreparte los rangos con cadaMergeTreeReadTaskRequesty nunca necesita la lista de partes por adelantado. - El seguidor queda bloqueado esperando la respuesta (cuando corresponde) antes de emitir su primer
MergeTreeReadTaskRequest(paquete de servidor16, enviado de seguidor a iniciador; el iniciador responde conMergeTreeReadTaskResponse, paquete de cliente10), y utiliza la lista de partes devuelta para limitar la construcción de fuentes exactamente a las partes que pertenecen a su stream#split_i.
8, el anuncio funciona en modo fire-and-forget sea cual sea el modo, y el seguidor construye fuentes sobre todas las partes conocidas localmente (el comportamiento heredado).
Cuerpo
Cuerpo de RangesInDataPartsDescription
Cuerpo de RangesInDataPartDescription
Cuerpo de MergeTreePartInfo
Cuerpo de MarkRanges
Referencia de tipos de paquete
Client → Server
Server → Client
QueryPlan
QueryPlan es un paquete de cliente exclusivo para la comunicación entre servidores. Se envía a continuación de un Query cuyo stage es QueryPlan (7) y contiene un único stream de plan serializado.
Antes de la versión
10, los campos 2 y 3 no existen. Un iniciador que tenga alguno de estos límites de ejecución con un valor no predeterminado no debe enviar esa disposición antigua: en su lugar, envía la consulta SQL original para que el receptor antiguo reconstruya los límites a partir de los ajustes de consulta.
Configuración
Esta sección describe los parámetros ajustables que determinan el comportamiento de las conexiones del protocolo nativo:- Ajustes de la capa de transporte: opciones de socket TCP y tiempos de espera, que afectan al comportamiento de la propia conexión TCP.
- Ajustes de la capa de aplicación: parámetros ajustables por consulta que se incluyen en la Lista de Settings del paquete Query y que afectan a lo que el servidor envía por la red (on the wire) o a cómo lo divide en tramas.
- Ajustes fuera del alcance: ajustes que suelen confundirse con los del protocolo, pero que en realidad controlan la ejecución de SQL o el almacenamiento.
Ajustes de la capa de transporte
Opciones de socket
Tiempos de espera
Los tiempos de espera se anidan de la siguiente manera:
Límites de conexión
Una conexión que envía consultas con regularidad puede mantenerse abierta indefinidamente. Solo se cierran las conexiones que permanecen inactivas durante una hora, y no existe una duración máxima predeterminada.
Ajustes de la capa de aplicación
Estos ajustes se envían con cada consulta en la lista de Settings del paquete Query. Modifican lo que el servidor transmite on the wire o la forma en que se estructura en tramas.Compresión
El indicador
compression del paquete Query (campo 6) activa o desactiva la compresión; esta configuración determina qué códec se utiliza cuando está activada.
Streaming de logs
Si se establece
send_logs_level en cualquier valor distinto de "none", el servidor emite paquetes Log durante la ejecución de la consulta.
Informe de progreso
Se trata de un mínimo objetivo, no de un máximo estricto: el servidor puede enviar paquetes Progress con menor frecuencia si la consulta no genera trabajo con la suficiente rapidez.
Envoltorio de resultados
INSERT asíncrono
Trazas distribuidas
Ajustes fuera del alcance
Estas configuraciones a veces se confunden con configuraciones a nivel de protocolo, pero controlan la ejecución de SQL, el almacenamiento o el uso de CPU, no el comportamiento en formato wire. Una implementación del protocolo no necesita darles ningún tratamiento especial.max_threads— paralelismo en la ejecución de consultas.max_memory_usage— límite de memoria por consulta.max_block_size,preferred_block_size_bytes— dimensionamiento interno de bloques durante el procesamiento de consultas; los bloques wire no dependen de ellas.compile_expressions— compilación JIT; solo afecta a la CPU.async_insert_max_data_size— búfer de cola del lado del servidor.- Todas las configuraciones
input_format_*youtput_format_*excepto la familiainput_format_native_*/output_format_native_*: las que no sonnativeseleccionan o ajustan otros formatos (por ejemplo, sobre HTTP) y no modifican los bloquesDatadel protocolo nativo.
*_native_* son la excepción: modifican los bytes dentro de los bloques Data de TCP nativo, por lo que una implementación del protocolo debe tenerlas en cuenta. output_format_native_encode_types_in_binary_format cambia el campo type de la columna de una cadena de texto a una codificación binaria de tipos (excepto en una consulta secundaria; consulte disposición en formato wire de columnas), output_format_native_write_json_as_string emite las columnas JSON como String, y output_format_native_use_flattened_dynamic_and_json_serialization selecciona la disposición FLATTENED de Dynamic/JSON. Como afectan al cuerpo del bloque y no al envoltorio del paquete, se describen en la especificación del formato Native; consulte disposición en formato wire de columnas y tipos versionados.
Glosario
Cancel — un paquete iniciado por el cliente (tipo 3) que cancela una consulta en ejecución. No se especifica en detalle en esta página. Marcador de fin de datos del cliente — un paquete Data vacío (0 columnas, 0 filas) que el cliente envía para cerrar un flujo de entrada. Su posición varía según el tipo de consulta:- Consulta normal (
SELECT, etc.): se envía después del paquete Query y de los posibles paquetes Data de tablas externas para indicar «no hay más datos externos». A continuación, el servidor comienza la ejecución. INSERT: el cliente no envía un marcador previo al esquema. El servidor envía primero el bloque de esquema, el cliente transmite sus bloques Data de filas y solo entonces envía el paquete Data vacío para finalizar el flujo de filas. Si se enviara un marcador vacío antes del bloque de esquema, se interpretaría como un fin inmediato de las filas y se perderían los datos.
min(client_version, server_version), calculada durante el handshake. Determina qué features están activas durante toda la vida de la conexión.
Paquete — un mensaje wire: un código de tipo de paquete VarUInt seguido de un cuerpo cuyo formato depende del tipo. Consulte envoltura del paquete.
Código de tipo de paquete — el VarUInt inicial de un paquete que identifica su formato. Actualmente están asignados los valores 0–18. Consulte la referencia de tipos de paquete.
Flujo de respuesta — la secuencia de paquetes que el servidor emite durante una consulta. Su longitud no está acotada y termina exactamente con un EndOfStream (éxito) o un Exception (fallo). Consulte la fase de consulta.
Bloque de esquema — el bloque de encabezado (un Block con columnas pero 0 filas) que el servidor envía durante la fase INSERT para anunciar la estructura esperada de las columnas antes de que el cliente envíe los datos.
Lista de Settings — una secuencia de tuplas (key, flags, value) en el cuerpo de Query, terminada por una clave vacía. Transporta la configuración de la capa de aplicación para cada consulta. Consulte Setting.
Stage — un campo VarUInt del paquete Query (campo 5) que controla hasta qué punto el servidor ejecuta la consulta. Los clientes externos suelen enviar 2 (Complete); las consultas distribuidas y los planes de consulta serializados usan los valores superiores. Consulte el campo 5 de Query para ver el conjunto completo de valores wire.
Terminador — un paquete que finaliza un flujo. La respuesta de Query termina con EndOfStream (éxito) o Exception (fallo). El flujo de entrada del cliente termina con el marcador Data vacío.