INSERT, la télémétrie d’exécution et les signaux d’erreur. C’est sur ce protocole que reposent le client en ligne de commande, le driver C++ et la plupart des drivers natifs tiers.
Cette page décrit le protocole lui-même : le tramage des paquets, la machine à états de la connexion, la négociation de version et le corps de chaque message autre que Block. Les octets contenus dans les paquets de la famille Data (le Block, ses colonnes et les encodages propres à chaque type) relèvent d’un autre sujet, documenté dans la spécification du format Native.
Spécification complémentaireCette page forme un ensemble avec la spécification du format Native, publiée conjointement. Les deux spécifications se répartissent clairement les rôles : cette page couvre la couche des paquets et la couche de transport, tandis que la spécification du format Native couvre les octets contenus dans les paquets de la famille
Data.BlockInfo, si bien qu’un seul octet mal placé désynchronise tout ce qui suit. Il est avec état, et chaque connexion TCP traite une seule requête à la fois — il n’y a pas de multiplexage. Les entiers de taille fixe sont encodés en little-endian.
Vue d’ensemble
Dans le format binaire, chaque message commence par un code de type de paquet
VarUInt, suivi d’un corps dont la structure dépend de ce code et de la version de protocole négociée.
Une connexion se déroule en trois phases : un handshake initial unique, puis un nombre quelconque d’échanges Ping ou Query, et enfin la fermeture :
Le protocole TCP natif transporte toujours les données tabulaires au format Native, quelle que soit la clause FORMAT de la requête SQL. La conversion vers RowBinary, CSV, JSON, etc. incombe au client, qui l’effectue après avoir décodé les blocs Native. (L’interface HTTP emprunte un chemin de code distinct qui, lui, respecte la clause FORMAT ; HTTP n’est pas traité ici.)
Sécurité
Sécurité du transport (TLS)
TLS opère au niveau de la couche transport, en dessous du protocole. Lorsqu’il est activé, l’intégralité du flux TCP est chiffrée, et les messages du protocole restent identiques octet pour octet, que TLS soit utilisé ou non.Authentification
L’authentification a lieu pendant le handshake, dans le messageClientHello. Les champs user et password transitent sous forme de chaînes de caractères en clair : c’est donc le chiffrement au niveau de la couche transport (TLS) qui protège les identifiants en transit.
Lorsque le champ user est vide, le serveur utilise l’utilisateur de session par défaut configuré, défini par le paramètre serveur default_session_user (dont la valeur par défaut est default), qui peut être surchargé pour chaque listener dans la section protocols. Si le serveur est configuré avec un utilisateur de session par défaut vide, ou si sa version est antérieure à 26.8, un champ user vide est rejeté et une exception est levée. Notez que clickhouse-client n’envoie jamais de nom d’utilisateur vide : il le remplace par default côté client.
L’authentification SSH par défi-réponse est disponible à partir de la version de protocole 54466. Voir Authentification SSH par défi-réponse.
Secret interserveur
Pour l’exécution de requêtes distribuées, les serveurs s’authentifient mutuellement en prouvant qu’ils connaissent un secret partagé, sans jamais faire transiter ce secret sur le réseau. Chaque paquet Query contient unauth_hash SHA-256 de 32 octets dans le champ 4 de Query, calculé à partir d’un sel, d’un nonce, du secret configuré et de la requête ; le serveur destinataire le recalcule, puis compare les deux valeurs. Ce mécanisme dépend de la fonctionnalité INTERSERVER_SECRET (v54441). Les clients externes envoient toujours une chaîne vide dans ce champ. Voir Authentification inter-serveur.
Gestion des versions et activation conditionnelle des fonctionnalités
Négociation de version
Lors du handshake, le client et le serveur déclarent chacun la version de protocole maximale qu’ils prennent en charge. La version négociée correspond à la plus basse des deux :Activation conditionnelle des fonctionnalités
Une fonctionnalité est identifiée par la version du protocole qui l’a introduite ; elle est active dès lors que la version négociée est supérieure ou égale à ce numéro.Tableau des fonctionnalités
Enveloppe de paquet
Chaque message transmis dans le format binaire présente la même structure externe, dans les deux sens :VarUInt, et non un octet de taille fixe. Pour les valeurs inférieures à 128, un VarUInt produit le même octet unique, mais les implémentations doivent utiliser l’encodage VarUInt afin de rester compatibles si de futurs types de paquets atteignent ou dépassent 128.
La référence des messages décrit uniquement le corps de chaque paquet, c’est-à-dire les octets qui suivent le code du type de paquet. La numérotation des champs commence à 1, avec le premier champ du corps.
Tramage par fragments (v54470+)
Lorsque la fonctionnalitéCHUNKED_PROTOCOL est négociée (voir le handshake), chaque paquet transmis est enveloppé dans un tramage par fragments. Cet enveloppement s’applique par sens de communication : les sens client→serveur et serveur→client sont négociés séparément et peuvent aboutir à des modes différents (par fragments ou sans tramage).
Disposition wire de chaque paquet :
VarUInt du type de paquet se trouve à l’intérieur du flux fragmenté : il constitue le premier octet de la charge utile du paquet (le premier octet du premier fragment), et non un octet distinct envoyé avant le tramage. La charge utile des fragments de chaque paquet correspond à l’intégralité de [VarUInt packet_type_code][message body] issu de l’enveloppe de paquet. Si un client laisse le type de paquet en dehors du flux fragmenté, le pair lit cet octet de type comme le premier octet de la taille de fragment u32, ce qui désynchronise la connexion.
Un même paquet peut être réparti sur plusieurs fragments si le tampon du rédacteur se remplit en cours de paquet ; la coupure peut intervenir n’importe où, y compris à l’intérieur du VarUInt du type de paquet. Le lecteur concatène les charges utiles des fragments et traite le zéro final de 4 octets comme une frontière de paquet transparente : il le consomme, mais ne le transmet pas au composant qui lit les corps de paquets.
Les paquets sans corps sont malgré tout encapsulés : un paquet d’un seul octet tel que Ping ou Pong devient [u32 size = 1][0x04][u32 0] une fois la fragmentation négociée. Toute mention d’« un seul octet dans le format binaire » ailleurs sur cette page désigne la forme antérieure à la fragmentation.
Négociation. ServerHello et Addendum transportent chacun deux champs String, un par direction, dont les valeurs sont choisies parmi {"chunked", "notchunked", "chunked_optional", "notchunked_optional"} :
chunked/notchunkedsont stricts : le côté concerné exige exactement ce mode.- Les variantes
_optionalsont souples : elles acceptent le mode choisi par l’autre côté.
Côté client, la préférence d’ENVOI du client est négociée avec la préférence de RÉCEPTION du serveur, et inversement.
Chronologie. Les chaînes de négociation transitent sans tramage : ClientHello → ServerHello (préférences du serveur) → Addendum (valeurs négociées par le client). Le passage au tramage s’applique à chaque octet envoyé après le vidage de l’Addendum. L’Addendum lui-même, le ClientHello et le ServerHello sont toujours transmis sans tramage.
Cycle de vie de la connexion
À tout instant, une connexion se trouve dans un seul des quatre états suivants :HANDSHAKE, READY, READING_RESPONSE ou terminée. Le protocole ne gérant pas le multiplexage, un client qui envoie une nouvelle requête avant d’avoir entièrement lu la réponse précédente entremêle les octets transmis et corrompt le flux.
États
Le chemin nominal descend en ligne droite —HANDSHAKE → READY → READING_RESPONSE → READY — avec la boucle Ping/Pong sur elle-même, tandis que toutes les transitions d’échec convergent vers l’unique état final Terminated.
Phase de handshake
Authentifie le client et négocie la version du protocole. Cette phase a lieu une seule fois par connexion, avant toute autre opération. La connexion TCP vient d’être ouverte et aucun message n’a encore été échangé. Le déroulement est le suivant :-
Le client envoie
ClientHelloavec la version maximale du protocole qu’il prend en charge. -
Le client lit la réponse et la traite selon le type de paquet :
-
Si
negotiated_version ≥ 54458(fonctionnalitéADDENDUM), le client envoie unAddendum. Cette décision repose sur la version négociée, et non sur la version déclarée par le client.
READY ; en cas d’erreur, elle est interrompue.
Phase Ping
Vérification de disponibilité au niveau applicatif, indépendante du keepalive TCP. Un aller-retour Ping/Pong réussi confirme que la connexion TCP est active dans les deux sens et que le serveur répond. Le Ping est sans état et n’est lié à aucune requête : plusieurs Pings successifs sont donc indépendants les uns des autres. À partir de l’étatREADY, le déroulement est le suivant :
-
Le client envoie
Ping. -
Le client lit la réponse :
Phase de requête
Le client soumet un statement SQL ; le serveur renvoie en continu des blocs de résultats ainsi que la télémétrie d’exécution. La réponse est une séquence de paquets se terminant par exactement unEndOfStream ou un Exception.
À partir de l’état READY, le déroulement est le suivant :
Si une erreur survient à n’importe quel moment, le serveur envoie un paquet Exception au lieu de EndOfStream, ce qui met fin à la requête.
-
Le client envoie
Queryavec unquery_idunique (généralement un UUID). -
Le client envoie les éventuelles tables externes, puis le marqueur Data vide. Le paquet Data vide a
table_name = "",num_columns = 0,num_rows = 0. Le serveur ne commence à exécuter la requête qu’après avoir reçu ce marqueur. Une table externe peut être répartie sur plusieurs paquetsDataportant le mêmetable_name; le premier d’entre eux fixe le schéma de la table, et chaque paquet ultérieur pour ce nom doit contenir les mêmes noms et types de colonnes, dans le même ordre, faute de quoi le serveur rejette la requête avecINCORRECT_DATA(voir Data). -
Le client passe à l’état
READING_RESPONSEet vide son tampon d’écriture. -
Le client lit les paquets de réponse en boucle et les traite selon leur type :
EndOfStream ou d’un Exception traité, la connexion revient à l’état READY. Une violation de protocole ou une erreur d’E/S y met fin.
Le cas
num_rows == 0 piège souvent les nouvelles implémentations. Un bloc sans ligne est un marqueur de limite ou un en-tête de schéma, et non un signal de fin de flux. Seuls EndOfStream ou Exception mettent fin à la réponse.Phase INSERT
La phase INSERT correspond à la phase de requête, avec deux échanges supplémentaires. Le client soumet un statementINSERT, puis le serveur répond par un bloc de schéma décrivant la table cible. Le client envoie ensuite en flux des paquets Data contenant les lignes, suivis du marqueur Data vide. Enfin, le serveur termine par EndOfStream ou Exception.
À partir de l’état READY, la requête SQL est un INSERT de la forme INSERT INTO <table> [(<cols>)] VALUES, sans littéral VALUES (...) intégré, puisque les données des lignes transitent par les paquets Data. Le déroulement est le suivant :
- Le client envoie
Queryavecbodydéfini sur la requête SQL INSERT. - Le client envoie les éventuelles tables externes (rare pour un INSERT). Contrairement à la phase de requête, il n’envoie pas de marqueur Data vide à ce stade. Le paquet
Queryde l’INSERTest envoyé avec des données en attente : le bloc vide de fin de données est donc reporté à l’étape 5. S’il était envoyé avant le bloc de schéma, le serveur l’interpréterait comme la fin du flux de lignes, terminerait l’INSERT sans aucune ligne, puis analyserait le premier véritable paquet de lignes comme un paquet de premier niveau orphelin. - Le client consomme les paquets de métadonnées (TableColumns, Progress, ProfileInfo, Log, ProfileEvents) jusqu’à recevoir le paquet Data de schéma — un bloc avec 0 ligne mais une structure de colonnes complète (noms et types). Le bloc de schéma fait office de contrat : les lignes que le client envoie ensuite doivent respecter la forme de ces colonnes.
- Le client envoie le ou les blocs de données. Pour chaque bloc, il écrit
VarUInt(ClientPacket::Data = 2), puisString("")pour le nom vide de table externe, puis le bloc. Les types de colonnes doivent correspondre, position par position, aux colonnes du bloc de schéma. - Le client envoie le marqueur de fin d’entrée : un paquet Data contenant un bloc vide (0 colonne, 0 ligne).
- Le client consomme le flux de réponse jusqu’à
EndOfStream(succès) ouException(échec).
async_insert = 1, le serveur place les lignes en file d’attente et les vide par lots. À partir de la version négociée ≥ 54484 (PROGRESS_IN_ASYNC_INSERT), une fois le vidage terminé, le serveur émet un paquet Progress supplémentaire, immédiatement suivi des ProfileEvents de l’insertion, puis de EndOfStream. En dessous de 54484, le serveur n’envoie pas ce Progress final. Il s’agit d’un paquet Progress ordinaire ; comme le serveur réinitialise le pipeline de requête avant d’y intégrer les compteurs d’écriture, l’incrément ne contient en pratique que le temps écoulé, et les statistiques de lignes et d’octets écrits parviennent au client via les ProfileEvents qui l’accompagnent. Un client qui consomme déjà les Progress entrelacés à l’étape 6 n’a qu’à accepter un paquet supplémentaire.
La connexion revient à l’état READY à la réception de EndOfStream ou d’une Exception gérée. Les violations de protocole et les erreurs d’E/S y mettent fin.
Référence des messages
Les champs sont présentés dans l’ordre de transmission. La colonneType utilise les valeurs suivantes :
VarUInt— entier non signé de longueur variable (voir VarUInt).String— octets préfixés par un VarUInt (voir String).UInt8,Int32, etc. — entiers little-endian de largeur fixe.Bool— un seul octet,0x00ou0x01.
Role indique qui utilise chaque champ :
- client — défini par les clients externes.
- interserveur — pertinent uniquement pour la communication entre serveurs ; les clients externes y écrivent une valeur par défaut.
- universal — utilisé par les deux.
ClientHello (type de paquet 0)
Client → Serveur. Premier message envoyé après l’ouverture de la connexion TCP.ServerHello (type de paquet 0)
Serveur → Client. Réponse à ClientHello lorsque l’authentification réussit.
Rule — élément de
password_complexity_rules :
Cette liste reflète la politique de mots de passe configurée par l’opérateur du serveur et n’a qu’une valeur indicative : le serveur n’applique pas ces règles pendant la poignée de main. Un client qui permet de modifier ou de définir un mot de passe peut s’appuyer sur ces règles pour signaler les erreurs avant d’envoyer au serveur un mot de passe non conforme.
Pour limiter la consommation de ressources face à un serveur malveillant ou mal configuré, plafonnez la valeur décodée de
count à 256 entrées, et chaque String pattern et message à 4096 octets. Un count égal à 0 (aucune paire ne suit) est le cas habituel pour les serveurs sur lesquels aucune politique de mots de passe n’est configurée.Addendum (sans type de paquet)
Client → Serveur, conditionné parADDENDUM (v54458). Envoyé immédiatement après la fin de la poignée de main. Il ne s’agit pas d’un type de paquet distinct : les champs sont transmis bruts dans le format binaire, sans octet de type de paquet en préfixe.
Le passage au tramage fragmenté n’intervient qu’après le vidage de cet Addendum ; l’Addendum lui-même n’est pas tramé.
Ping (type de paquet 4)
Client → Serveur. Aucun corps — avant le tramage fragmenté, le paquet se compose d’un seul octet0x04 ; lorsque la fragmentation est négociée, cet octet devient la charge utile, d’un seul octet, d’un fragment (voir tramage fragmenté).
Pong (type de paquet 4)
Serveur → Client. Aucun corps — le paquet se résume à un seul octet0x04 avant le tramage fragmenté ; lorsque la fragmentation est négociée, cet octet constitue la charge utile, longue d’un octet, d’un fragment (voir tramage fragmenté).
Exception (type de paquet 2)
Serveur → Client. Envoyé lorsque le serveur rencontre une erreur, quelle que soit la phase.Query (type de paquet 1)
Client → Serveur.ClientInfo (intégré dans Query)
Client → Serveur, intégré dans le corps de Query (champ 2). Conditionné parCLIENT_INFO (v54032). (Certains champs de ClientInfo dépendent de versions ultérieures, comme indiqué ci-dessous pour chaque champ.)
Disposition dépendante de l’interface (champs 7–12)Les champs 7–12 ci-dessus correspondent à la branche TCP. Lorsque
query_interface (champ 6) n’est pas TCP, ces champs sont remplacés par une disposition wire différente — il ne s’agit pas de simples omissions facultatives : un décodeur doit donc bifurquer en fonction du champ 6.query_interface = 2(HTTP) : ce sont les informations de la requête HTTP transmise par le serveur qui sont écrites à la place —http_method(UInt8),http_user_agent(String), puisforwarded_for(String, conditionné parX_FORWARDED_FOR_IN_CLIENT_INFOv54443),http_referer(String, conditionné parREFERER_IN_CLIENT_INFOv54447), et enfinhttp_handler_name(String) ethttp_request_url(String) (tous deux conditionnés parHTTP_HANDLER_IN_CLIENT_INFOv54490, dans cet ordre). Aucun des champsos_user/client_hostname/client_name/version_*/protocol_versionn’est présent. Les deux derniers contiennent le nom du gestionnaire HTTP défini en SQL qui a été sélectionné ainsi que l’URL de la requête, afin qu’ils soient propagés jusqu’aux partitionnés distants (ils sont vides pour une requête HTTP ne passant pas par un gestionnaire).- Toute autre interface : aucun des champs TCP (7–12) ni aucun des champs HTTP n’est écrit ; le flux se poursuit directement avec
quota_key.
quota_key (champ 13) et distributed_depth (champ 14) suivent pour toutes les interfaces, puis version_patch (champ 15) n’est écrit que pour TCP.Cette branche concerne principalement le trafic interserveur, lorsque le serveur initiateur transmet une requête arrivée à l’origine via HTTP. Un décodeur qui lit systématiquement les champs TCP interprétera mal ces paquets, en prenant http_method ou http_user_agent pour quota_key.Authentification inter-serveur
Le champ 4 du paquet Query (auth_hash) ne contient pas, dans le format binaire, le secret de cluster partagé. Envoyer le secret brut ferait échouer l’authentification tout en le divulguant. Un serveur agissant en tant que client interserveur prouve plutôt qu’il connaît le secret au moyen d’un hash SHA-256 salé :
- Passer en mode interserveur. Le serveur qui se connecte le signale dans
ClientHello: le champusercontient le marqueur interserveur etpasswordest vide. Il ajoute ensuite deux autres chaînes de caractères — le nom du cluster et unsaltde 32 octets nouvellement généré (encodeSHA256d’une valeur aléatoire) — immédiatement après les champsuser/password, au sein du même paquetClientHello. Le serveur lit ces deux chaînes avant d’envoyerServerHello; un client doit donc les écrire d’emblée. Attendre d’abordServerHelloprovoque un interblocage, car le serveur reste bloqué dans l’attente de ces chaînes. - Obtenir le nonce.
ServerHellotransporte un nonceUInt64de 8 octets lorsqueINTERSERVER_SECRET_V2(v54462) est négocié. - Calculer le hash. Pour chaque paquet Query qui n’est pas une
InitialQuery, le client écritencodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles + current_roles)dans le champ 4, soit un digest de 32 octets. (nonceest représenté sous forme de chaîne décimale, présente uniquement si la version négociée est ≥ v54462 ;external_rolesn’est ajouté que lorsqueINTERSERVER_EXTERNALLY_GRANTED_ROLES(v54472) est négocié ;current_rolesest la liste sérialisée des noms de rôles —[VarUInt count][String]*count, sans l’octet de présence — ajoutée uniquement lorsqueINTERSERVER_CURRENT_ROLES(v54488) est négocié et que la liste est présente.) Pour uneInitialQuery, ou lorsqu’aucun secret de cluster n’est configuré, le client écrit à la place une chaîne vide. - Vérifier. Le serveur lit le champ 4 avec une limite de 32 octets et recalcule la même concaténation à partir de sa propre copie du secret de cluster ; la connexion est rejetée si les digests diffèrent.
auth_hash vide.
Setting
Encodé directement dans la liste de paramètres du corps de Query (le paquet Query, champ 3). La liste est toujours présente, quelle que soit la version négociée, et se termine par un Setting dont la clé est vide — un uniqueVarUInt 0, sans indicateurs ni valeur à la suite. Seul l’encodage de chaque paramètre dépend de la version négociée, selon SETTINGS_SERIALIZED_AS_STRINGS (v54429).
v54429+ (STRINGS_WITH_FLAGS) — chaque paramètre est représenté par le triplet suivant :
Les champs 2 et 3 sont absents lorsque
key est vide.
Avant 54429 (BINARY) — chaque paramètre est de la forme [String key][type-specific binary value] : le champ flags n’est pas écrit, et la valeur est encodée sous la forme binaire native du paramètre (par exemple un integer de largeur fixe ou une string préfixée par sa longueur) plutôt que sous forme de string décimale ou textuelle. La liste se termine toujours par une key vide. Un client ciblant une version négociée inférieure à 54429 doit lire et écrire cette forme binaire, et non le triplet ci-dessus. (Les paramètres personnalisés définis par l’utilisateur font exception : ils comportent toujours flags et une valeur de type string, quel que soit l’encodage.)
Le champ flags regroupe :
0x01— Important : le paramètre affecte le résultat de la requête et ne doit pas être ignoré silencieusement par les pairs plus anciens.0x02— Custom : un paramètre personnalisé défini par l’utilisateur.0x0c— un champ de niveau sur 2 bits, et non un indicateur indépendant :0x00= Production,0x04= Obsolete,0x08= Experimental,0x0c= Beta. Lisez les 2 bits (flags & 0x0c) — un test naïfflags & 0x04classerait à tort Beta (0x0c) comme Obsolete.0x80— HotReload (rechargement de la configuration sans redémarrage ; défini dans l’enum des indicateurs, présent principalement pour les paramètres de coordination).
Paramètre
Paramètres de requête, pour les requêtes paramétrées telles queSELECT {x:UInt64}. Ils sont encodés de la même manière qu’un Setting dont l’indicateur Custom (0x02) est activé, et se terminent de la même façon par une clé vide.
La valeur du paramètre correspond à la représentation SQL de la valeur, et non à un littéral brut. Les paramètres de type String doivent être transmis déjà entourés de guillemets simples (par exemple, la valeur de
{name:String} est 'Alice', et non Alice) ; sinon, le parseur de valeurs du serveur les rejette.Data (type de paquet 1 serveur→client, type de paquet 2 client→serveur)
Dans les deux sens. Transporte les blocs de résultats, les données d’INSERT, les tables externes et les marqueurs de fin de données. Le format wire est symétrique : dans les deux sens, un préfixetable_name précède le bloc. Seul l’octet du type de paquet diffère.
Le marqueur de fin de données est un paquet dont le bloc est vide (
0 colonne et 0 ligne), quelle que soit la valeur de table_name. Le serveur ne considère un paquet Data client comme marqueur de fin que si le bloc décodé est vide (block.empty()) ; un paquet avec table_name = "" et un bloc non vide est un paquet de lignes ordinaire, et non un marqueur de fin. Un flux de lignes d’INSERT se compose donc d’une suite de blocs Data non vides, clôturée par un unique bloc Data vide.
Les variantes de blocs et leur signification sont décrites dans Variantes de blocs.
Progress (type de paquet 3)
Serveur → Client. Envoyé périodiquement pendant l’exécution de la requête. Tous les champs sont de type VarUInt, et chaque paquet contient des incréments depuis le paquetProgress précédent, et non des totaux cumulés. Avant l’envoi, le serveur lit ses compteurs, les remet atomiquement à zéro, puis calcule elapsed_ns comme le temps écoulé depuis le dernier envoi. Un client doit donc cumuler localement les paquets successifs pour obtenir des totaux courants : traiter un paquet comme une valeur absolue fait reculer l’affichage de la progression ou fausse le décompte à la baisse dès que plus d’un paquet est reçu.
ProfileInfo (type de paquet 6)
Serveur → Client. Envoyé une seule fois par requête, vers la fin de l’exécution.Totals (type de paquet 7)
Serveur → Client. Envoyé pour les requêtes contenantWITH TOTALS. Le format wire est identique à celui de Data : une string table_name (toujours vide) suivie d’un bloc. Seul l’octet de type de paquet diffère.
Extremes (type de paquet 8)
Serveur → Client. Envoyé lorsque le paramètreextremes est activé. Le format wire est identique à celui de Data. Le bloc contient exactement 2 lignes : la ligne 0 contient le minimum de chaque colonne et la ligne 1, le maximum.
Log (type de paquet 10)
Serveur → Client. Envoyé lorsque la requête dispose d’une file d’attente de logs active (paramètresend_logs_level ; voir streaming des logs).
Même enveloppe et même format de corps que Data. Le bloc a une valeur fixe num_columns = 8 et un schéma prédéfini. Chaque ligne de log correspond à une ligne sur l’ensemble des 8 colonnes, et un même paquet Log peut contenir de nombreuses lignes.
ProfileEvents (type de paquet 14)
Serveur → Client. Transmet les compteurs de performance de chaque requête. Enveloppe et format de corps identiques à ceux de Data. Le bloc comporte un nombre fixe de colonnes (num_columns = 6) et un schéma prédéfini. Chaque événement correspond à une ligne.
Le type des éléments de la colonne
value varie d’un paquet à l’autre : les serveurs plus anciens émettent du UInt64, les plus récents du Int64. Lisez la string de type de la colonne dans l’en-tête du bloc plutôt que de présupposer une largeur donnée.TableColumns (type de paquet 11)
Serveur → Client, conditionné parCOLUMN_DEFAULTS_METADATA (v54410). Le serveur l’envoie avant le bloc de schéma de l’INSERT afin de transmettre les métadonnées des valeurs par défaut des colonnes, mais uniquement lorsque la version négociée est ≥ 54410 et que le paramètre input_format_defaults_for_omitted_fields est activé. En dessous de 54410, ce paquet n’est jamais envoyé ; un client plus ancien ne doit donc pas l’attendre, car le bloc Data de schéma arrive directement. Un client v54410+ doit être capable de gérer les deux cas : un TableColumns facultatif, suivi du bloc de schéma.
Corps compressé à partir de v54481À partir de la version négociée 54481 (
COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS), le serveur écrit les deux champs via le même chemin de sortie, éventuellement compressé. Ainsi, lorsque la requête a compression = true, l’intégralité du corps de TableColumns (external_table + columns_description) se trouve dans la trame de compression, et le client le lit via le flux décompressé correspondant. Lorsque la requête n’utilise pas de compression, le corps est transmis non compressé dans le format binaire, exactement comme l’indique le tableau ci-dessus. Ce point est important pour les réponses de schéma d’INSERT : un client qui adapte la gestion de la compression pour Log et ProfileEvents, mais pas pour TableColumns, interprétera mal la réponse lorsque la compression de la requête est activée.TimezoneUpdate (type de paquet 17)
Serveur → Client, conditionné parTIMEZONE_UPDATES (v54464). Envoyé à un seul endroit : l’initialiseur de la fonction de table input (une requête de la forme INSERT INTO <table> SELECT ... FROM input('<structure>'), qui reçoit en flux des lignes envoyées par le client). Juste après avoir envoyé le bloc Data contenant le schéma d’entrée (voir la phase INSERT), le serveur émet TimezoneUpdate avec la valeur actuelle de session_timezone dans le contexte de la requête, afin que le client analyse les lignes qu’il s’apprête à envoyer selon le même fuseau horaire. Le serveur n’émet pas ce paquet lors de modifications arbitraires via SET session_timezone en cours de requête, ni pour indiquer au client comment formater les blocs de résultats ultérieurs.
Le paquet arrive une seule fois, immédiatement après le bloc de schéma d’entrée et avant que le client ne commence à envoyer les blocs de lignes. Un décodeur qui ignore
TimezoneUpdate DOIT tout de même consommer la String qui suit afin de préserver l’alignement du wire.
Authentification SSH par challenge-réponse (types de paquet 11, 12, 18)
Conditionnée parSSH_AUTHENTICATION (v54466), et uniquement sur activation explicite. Une connexion entre dans le flux SSH lorsque ClientHello envoie user = " SSH KEY AUTHENTICATION " + <real_user> (avec les espaces de début et de fin) et password = "". Le serveur lit le préfixe, le supprime pour retrouver l’utilisateur réel et bascule en mode challenge-réponse.
Ce flux remplace l’authentification par mot de passe, et l’échange challenge-réponse a lieu avant ServerHello : le serveur diffère sa réponse Hello jusqu’à ce que l’authentification réussisse.
- Le client envoie ClientHello avec le préfixe marqueur SSH et un mot de passe vide.
-
Le client envoie
SSHChallengeRequest(paquet 11). Le serveur n’a pas encore envoyé ServerHello : il traite d’abord l’authentification et reste bloqué à cette étape en attendant ce paquet. -
Le serveur répond par un
SSHChallengecontenant des octets aléatoires (paquet 18). -
Le client construit la string à signer et signe celle-ci, et non le challenge brut, puis envoie
SSHChallengeResponse(paquet 12) avec la signature. Le message signé est la concaténation octet par octet, sans séparateur, de quatre parties dans cet ordre exact : -
Le serveur vérifie la signature à l’aide de la clé publique enregistrée pour l’utilisateur, en reconstruisant la même string
decimal(protocol_version) + default_database + user + challenge. En cas de succès, il envoieServerHello(la même réponse que dans le flux par mot de passe) et le handshake se poursuit normalement (Addendum, etc.) ; en cas d’échec, il renvoie uneExceptionet ferme la connexion. L’authentification échouera pour tout client qui ne signe que les octets bruts du challenge.
Il s’agit de l’inverse du handshake par mot de passe, dans lequel ServerHello suit immédiatement ClientHello. Avec l’authentification SSH auth, ServerHello n’est envoyé qu’une fois la signature vérifiée ; le challenge-réponse SSH s’intercale donc dans le handshake avant la réception de tout ServerHello.
MergeTreeAllRangesAnnouncementResponse (type de paquet 14)
Client → Serveur, interserveur uniquement. Conditionné parparallel_replicas_protocol_version ≥ 8 (voir VERSIONED_PARALLEL_REPLICAS_PROTOCOL). Les clients externes n’envoient jamais ce paquet.
Lorsque la version de parallel-replicas négociée est ≥ 8, le cycle requête/réponse de l’initiateur pour un MergeTreeAllRangesAnnouncement envoyé par un follower (type de paquet 15, sens serveur→client) est modifié comme suit :
- Un follower ouvre son pipeline de lecture et envoie
MergeTreeAllRangesAnnouncementà l’initiateur. - Uniquement lorsque le
modede l’annonce est différent deDefault(WithOrder = 1ouReverseOrder = 2, tous deux utilisés pour les lectures parallèles ordonnées), l’initiateur répond avecMergeTreeAllRangesAnnouncementResponse. Pourmode = Default = 0, l’initiateur ne répond pas et le follower n’attend pas : en modeDefault, les plages sont distribuées avec chaqueMergeTreeReadTaskRequest, si bien que la liste des parties de données n’est jamais nécessaire en amont. - Le follower attend la réponse de manière bloquante (lorsqu’elle est attendue) avant d’émettre son premier
MergeTreeReadTaskRequest(paquet serveur16, envoyé follower→initiateur ; l’initiateur répond avecMergeTreeReadTaskResponse, paquet client10), et s’appuie sur la liste des parties de données renvoyée pour restreindre la construction des sources aux seules parties de données dont son flux#split_iest propriétaire.
8, l’annonce fonctionne en mode fire-and-forget quel que soit le mode, et le follower construit des sources sur toutes les parties de données connues localement (comportement legacy).
Corps
Corps de RangesInDataPartsDescription
Corps RangesInDataPartDescription
Corps de MergeTreePartInfo
Corps MarkRanges
Référence des types de paquets
Client → Serveur
Serveur → Client
QueryPlan
QueryPlan est un paquet client réservé aux communications interserveurs. Il suit un paquet Query dont le stage vaut QueryPlan (7) et contient un unique flux de plan sérialisé.
Avant la version
10, les champs 2 et 3 sont absents. Un initiateur dont l’une des limites d’exécution diffère de la valeur par défaut ne doit pas envoyer cette ancienne disposition : il transmet à la place la requête SQL d’origine, ce qui permet à l’ancien destinataire de reconstituer les limites à partir des paramètres de la requête.
Configuration
Cette section présente les paramètres ajustables qui régissent les connexions du protocole natif :- Paramètres de la couche transport — options de socket TCP et délais d’expiration, qui agissent sur le comportement de la connexion TCP elle-même.
- Paramètres de la couche applicative — paramètres ajustables propres à chaque requête, transmis dans la liste des paramètres du paquet Query, qui déterminent ce que le serveur envoie dans le format binaire ou la manière dont les données sont découpées en trames.
- Paramètres hors périmètre — paramètres souvent confondus avec les paramètres du protocole, mais qui contrôlent en réalité l’exécution SQL ou le stockage.
Paramètres de la couche transport
Options de socket
Délais d’expiration
Les délais d’expiration s’imbriquent comme suit :
Limites de connexion
Une connexion qui envoie régulièrement des requêtes peut rester ouverte indéfiniment : seules les connexions inactives sont fermées au bout d’une heure, et aucune durée de vie maximale n’est définie par défaut.
Paramètres de la couche applicative
Ces paramètres sont transmis avec chaque requête dans la liste des paramètres du paquet Query. Ils modifient ce que le serveur envoie dans le format binaire, ou la façon dont ces données sont découpées en trames.Compression
L’indicateur
compression du paquet Query (champ 6) active ou désactive la compression ; ces paramètres déterminent quel codec est utilisé lorsqu’elle est activée.
Streaming des logs
Lorsque
send_logs_level est défini sur une valeur autre que "none", le serveur émet des paquets Log pendant l’exécution de la requête.
Rapport de progression
Il s’agit d’un minimum visé, et non d’un maximum strict : le serveur peut envoyer des paquets Progress moins souvent lorsque la requête ne progresse pas assez vite.
Enveloppe de résultat
INSERT asynchrone
Traçage distribué
Paramètres hors périmètre
Ces paramètres sont parfois pris à tort pour des paramètres de niveau protocole, mais ils contrôlent l’exécution SQL, le stockage ou l’utilisation du CPU, et non le comportement wire. Une implémentation du protocole n’a pas besoin de les traiter de manière particulière.max_threads— parallélisme lors de l’exécution des requêtes.max_memory_usage— plafond de mémoire par requête.max_block_size,preferred_block_size_bytes— dimensionnement interne des blocs lors du traitement des requêtes ; les blocs au format wire en sont indépendants.compile_expressions— compilation JIT ; concerne uniquement le CPU.async_insert_max_data_size— tampon de file d’attente côté serveur.- Tous les paramètres
input_format_*etoutput_format_*, à l’exception de la familleinput_format_native_*/output_format_native_*— les paramètres autres quenativesélectionnent ou ajustent d’autres formats (par exemple via HTTP) et ne modifient pas les blocsDatadu protocole natif.
*_native_* font exception : ils modifient les octets contenus dans les blocs Data transmis en TCP natif, et une implémentation du protocole doit donc les prendre en compte. output_format_native_encode_types_in_binary_format remplace, dans le champ type de la colonne, la string textuelle par un encodage binaire du type (sauf pour une requête secondaire, voir disposition wire des colonnes) ; output_format_native_write_json_as_string émet les colonnes JSON sous forme de String ; et output_format_native_use_flattened_dynamic_and_json_serialization sélectionne la disposition FLATTENED pour Dynamic/JSON. Comme ils portent sur le corps du bloc et non sur l’enveloppe du paquet, ils sont décrits dans la spécification du Native Format — voir disposition wire des colonnes et types versionnés.
Glossaire
Cancel — un paquet initié par le client (type 3) qui interrompt une requête en cours d’exécution. Non détaillé sur cette page. Marqueur de fin de données client — un paquet Data vide (0 colonne, 0 ligne) que le client envoie pour clore un flux d’entrée. Sa position varie selon le type de requête :- Requête normale (
SELECT, etc.) : envoyé après le paquet Query et les éventuels paquets Data de tables externes pour signaler qu’il n’y a « plus de données externes ». Le serveur commence alors l’exécution. INSERT: le client n’envoie pas de marqueur avant le schéma. Le serveur envoie d’abord le bloc de schéma, le client transmet ses blocs Data de lignes, et c’est seulement ensuite qu’il envoie le paquet Data vide pour terminer le flux de lignes. Un marqueur vide envoyé avant le bloc de schéma serait interprété comme une fin immédiate des lignes, et les données seraient perdues.
min(client_version, server_version), calculée lors du handshake. Détermine quelles features sont actives pendant toute la durée de vie de la connexion.
Paquet — un message wire : un code de type de paquet VarUInt suivi d’un corps dont le format dépend du type. Voir enveloppe de paquet.
Code de type de paquet — le VarUInt placé en tête d’un paquet, qui identifie son format. Les valeurs 0 à 18 sont actuellement attribuées. Voir la référence des types de paquets.
Flux de réponse — la séquence de paquets émis par le serveur pendant une requête. De longueur indéterminée, il se termine par exactement un EndOfStream (succès) ou un Exception (échec). Voir la phase de requête.
Bloc de schéma — le bloc d’en-tête (un bloc avec des colonnes mais 0 ligne) que le serveur envoie pendant la phase INSERT pour annoncer la structure des colonnes attendue avant que le client n’envoie les données.
Liste de paramètres — une séquence de tuples (key, flags, value) dans le corps de Query, terminée par une clé vide. Transporte la configuration de la couche applicative propre à chaque requête. Voir Setting.
Stage — un champ VarUInt du paquet Query (champ 5) qui détermine jusqu’où le serveur exécute la requête. Les clients externes envoient généralement 2 (Complete) ; les requêtes distribuées et les plans de requête sérialisés utilisent les valeurs supérieures. Voir le champ 5 de Query pour l’ensemble complet des valeurs wire.
Terminateur — un paquet qui met fin à un flux. La réponse à Query se termine par EndOfStream (succès) ou Exception (échec). Le flux d’entrée du client se termine par le marqueur Data vide.