ClickHouse/docs/fr/development/architecture.md
Ivan Blinkov d91c97d15d
[docs] replace underscores with hyphens (#10606)
* Replace underscores with hyphens

* remove temporary code

* fix style check

* fix collapse
2020-04-30 21:19:18 +03:00

37 KiB
Raw Blame History

machine_translated machine_translated_rev toc_priority toc_title
true f865c9653f 62 Vue d'ensemble de L'Architecture ClickHouse

Vue densemble De LArchitecture ClickHouse

ClickHouse est un véritable SGBD orienté colonne. Les données sont stockées par colonnes et lors de lexécution de tableaux (vecteurs ou morceaux de colonnes). Dans la mesure du possible, les opérations sont distribuées sur des tableaux, plutôt que sur des valeurs individuelles. Il est appelé “vectorized query execution,” et cela aide à réduire le coût du traitement des données réel.

Cette idée nest pas nouvelle. Il remonte à la APL langage de programmation et ses descendants: A +, J, K, et Q. La programmation de tableau est utilisée dans le traitement des données scientifiques. Cette idée nest pas non plus nouvelle dans les bases de données relationnelles: par exemple, elle est utilisée dans le Vectorwise système.

Il existe deux approches différentes pour accélérer le traitement des requêtes: lexécution vectorisée des requêtes et la génération de code dexécution. Ce dernier supprime toute indirection et expédition dynamique. Aucune de ces approches est strictement meilleure que lautre. La génération de code dexécution peut être meilleure lorsquelle fusionne de nombreuses opérations, utilisant ainsi pleinement les unités Dexécution du processeur et le pipeline. Lexécution de requête vectorisée peut être moins pratique car elle implique des vecteurs temporaires qui doivent être écrits dans le cache et lus. Si les données temporaires ne rentre pas dans le cache L2, cela devient un problème. Mais lexécution de requête vectorisée utilise plus facilement les capacités SIMD de la CPU. Un document de recherche écrit par nos amis montre quil est préférable de combiner les deux approches. ClickHouse utilise lexécution de requête vectorisée et a un support initial limité pour la génération de code dexécution.

Colonne

IColumn linterface est utilisée pour représenter des colonnes en mémoire (en fait, des morceaux de colonnes). Cette interface fournit des méthodes daide pour la mise en œuvre de divers opérateurs relationnels. Presque toutes les opérations sont immuables: elles ne modifient pas la colonne dorigine, mais en créent une nouvelle modifiée. Par exemple, l IColumn :: filter méthode accepte un masque doctet de filtre. Il est utilisé pour le WHERE et HAVING opérateurs relationnels. Exemples supplémentaires: IColumn :: permute méthode de soutien ORDER BY, le IColumn :: cut méthode de soutien LIMIT.

Divers IColumn application (ColumnUInt8, ColumnString et ainsi de suite) sont responsables de la mémoire disposition de colonnes. La disposition de la mémoire est généralement un tableau contigu. Pour le type entier de colonnes, cest juste un contiguë tableau, comme std :: vector. Pour String et Array colonnes, il sagit de deux vecteurs: Un pour tous les éléments du tableau, placé de manière contiguë, et un second pour les décalages au début de chaque tableau. Il y a aussi ColumnConst cela stocke une seule valeur en mémoire, mais ressemble à une colonne.

Champ

Néanmoins, il est possible de travailler avec des valeurs individuelles ainsi. Pour représenter une valeur individuelle, la Field est utilisée. Field est juste une union discriminée de UInt64, Int64, Float64, String et Array. IColumn a l operator[] méthode pour obtenir la n-ème valeur en tant que Field et la insert méthode pour ajouter un Field à la fin dune colonne. Ces méthodes ne sont pas très efficaces, car ils nécessitent de traiter avec temporaire Field des objets représentant une valeur individuelle. Il existe des méthodes plus efficaces, telles que insertFrom, insertRangeFrom et ainsi de suite.

Field ne pas avoir assez dinformations sur un type de données spécifique pour une table. Exemple, UInt8, UInt16, UInt32, et UInt64 tous sont représentés comme UInt64 dans un Field.

Abstractions Qui Fuient

IColumn a des méthodes pour les transformations relationnelles communes des données, mais elles ne répondent pas à tous les besoins. Exemple, ColumnUInt64 ne pas avoir une méthode pour calculer la somme des deux colonnes, et ColumnString na pas de méthode pour exécuter une recherche de sous-chaîne. Ces innombrables routines sont mises en œuvre en dehors de IColumn.

Diverses fonctions sur les colonnes peuvent être implémentées de manière générique et non efficace en utilisant IColumn méthodes pour extraire Field valeurs, ou dune manière spécialisée en utilisant la connaissance de la disposition de la mémoire interne des données dans un IColumn application. Il est implémenté en lançant des fonctions à un IColumn tapez et traitez directement la représentation interne. Exemple, ColumnUInt64 a l getData méthode qui renvoie une référence à un tableau interne, puis une autre routine lit ou remplit ce tableau directement. Nous avons “leaky abstractions” permettent de spécialisations diverses routines.

Types De Données

IDataType est responsable de la sérialisation et de la désérialisation: pour la lecture et lécriture de morceaux de colonnes ou de valeurs individuelles sous forme binaire ou de texte. IDataType correspond directement aux types de données dans les tables. Par exemple, il y a DataTypeUInt32, DataTypeDateTime, DataTypeString et ainsi de suite.

IDataType et IColumn ne sont que faiblement liés les uns aux autres. Différents types de données peuvent être représentés en mémoire par le même IColumn application. Exemple, DataTypeUInt32 et DataTypeDateTime sont tous deux représentés par ColumnUInt32 ou ColumnConstUInt32. En outre, le même type de données peut être représentée par différents IColumn application. Exemple, DataTypeUInt8 peut être représenté par ColumnUInt8 ou ColumnConstUInt8.

IDataType stocke uniquement les métadonnées. Par exemple, DataTypeUInt8 ne stocke rien du tout (sauf vptr) et DataTypeFixedString magasins juste N (la taille des chaînes de taille fixe).

IDataType a des méthodes daide pour différents formats de données. Des exemples sont des méthodes pour sérialiser une valeur avec des guillemets possibles, pour sérialiser une valeur pour JSON et pour sérialiser une valeur dans le format XML. Il ny a pas de correspondance directe avec les formats de données. Par exemple, les différents formats de données Pretty et TabSeparated pouvez utiliser le même serializeTextEscaped méthode daide à partir de la IDataType interface.

Bloc

A Block est un conteneur qui représente un sous-ensemble (morceau) dune table en mémoire. Cest juste un ensemble de triplets: (IColumn, IDataType, column name). Pendant lexécution de la requête, les données sont traitées par Blocks. Si nous avons un Block, nous disposons de données (dans le IColumn objet), nous avons des informations sur son type (dans IDataType) qui nous indique comment traiter cette colonne, et nous avons le nom de la colonne. Il peut sagir du nom de colonne dorigine de la table ou dun nom artificiel attribué pour obtenir des résultats temporaires de calculs.

Lorsque nous calculons une fonction sur des colonnes dans un bloc, nous ajoutons une autre colonne avec son résultat au bloc, et nous ne touchons pas les colonnes pour les arguments de la fonction car les opérations sont immuables. Plus tard, les colonnes inutiles peuvent être supprimées du bloc, mais pas modifiées. Il est pratique pour lélimination des sous-expressions communes.

Des blocs sont créés pour chaque bloc de données traité. Notez que pour le même type de calcul, les noms et les types de colonnes restent les mêmes pour différents blocs, et seules les données de colonne changent. Il est préférable de diviser les données de bloc de len-tête de bloc car les petites tailles de Bloc ont une surcharge élevée de chaînes temporaires pour copier shared_ptrs et les noms de colonnes.

Bloquer Les Flux

Les flux de blocs sont destinés au traitement des données. Nous utilisons des flux de blocs pour lire des données quelque part, effectuer des transformations de données ou écrire des données quelque part. IBlockInputStream a l read méthode pour récupérer le bloc suivant, tandis que des. IBlockOutputStream a l write méthode pour pousser le bloc quelque part.

Les flux sont responsables de:

  1. De la lecture ou de lécriture dans une table. La table renvoie simplement un flux pour lire ou écrire des blocs.
  2. Mise en œuvre des formats de données. Par exemple, si vous souhaitez envoyer des données vers un terminal Pretty format, vous créez un flux de sortie de bloc où vous poussez des blocs, et il les formate.
  3. Effectuer des transformations de données. Disons que vous avez IBlockInputStream et veulent créer un flux filtré. Vous créez FilterBlockInputStream et linitialiser avec votre flux de données. Puis quand vous tirez un bloc de FilterBlockInputStream, il extrait un bloc de votre flux, le filtre et vous renvoie le bloc filtré. Les pipelines dexécution des requêtes sont représentés de cette façon.

Il y a des transformations plus sophistiquées. Par exemple, lorsque vous tirez de AggregatingBlockInputStream il lit toutes les données à partir de sa source, agrégats, puis renvoie un flux de données agrégées pour vous. Un autre exemple: UnionBlockInputStream accepte de nombreuses sources dentrée dans le constructeur et également un certain nombre de threads. Il lance plusieurs threads et lit à partir de plusieurs sources en parallèle.

Les flux de blocs utilisent le “pull” approche pour contrôler le flux: lorsque vous extrayez un bloc du premier flux, il extrait par conséquent les blocs requis des flux imbriqués, et lensemble du pipeline dexécution fonctionnera. Ni “pull” ni “push” est la meilleure solution, car le flux de contrôle est implicite, ce qui limite limplémentation de diverses fonctionnalités telles que lexécution simultanée de plusieurs requêtes (fusion de plusieurs pipelines ensemble). Cette limitation pourrait être surmontée avec des coroutines ou simplement en exécutant des threads supplémentaires qui sattendent les uns aux autres. Nous pouvons avoir plus de possibilités si nous rendons le flux de contrôle explicite: si nous localisons la logique pour passer des données dune unité de calcul à une autre en dehors de ces unités de calcul. Lire ce article pour plus de pensées.

Il convient de noter que le pipeline dexécution de la requête crée des données temporaires à chaque étape. Nous essayons de garder la taille du bloc suffisamment petite pour que les données temporaires tiennent dans le cache du processeur. Avec cette hypothèse, lécriture et la lecture de données temporaires sont presque libres en comparaison avec dautres calculs. Nous pourrions envisager une alternative, qui est de fusionner de nombreuses opérations dans le pipeline ensemble. Cela pourrait rendre le pipeline aussi court que possible et supprimer une grande partie des données temporaires, ce qui pourrait être un avantage, mais cela présente également des inconvénients. Par exemple, un pipeline divisé facilite limplémentation de la mise en cache de données intermédiaires, le vol de données intermédiaires à partir de requêtes similaires exécutées en même temps et la fusion de pipelines pour des requêtes similaires.

Format

Les formats de données sont implémentés avec des flux de blocs. Il y a “presentational” formats appropriés uniquement pour la sortie de données vers le client, tels que Pretty format, qui fournit seulement IBlockOutputStream. Et il existe des formats dentrée / sortie, tels que TabSeparated ou JSONEachRow.

Il y a aussi des flux de lignes: IRowInputStream et IRowOutputStream. Ils vous permettent de tirer/pousser des données par des lignes individuelles, pas par des blocs. Et ils ne sont nécessaires que pour simplifier la mise en œuvre des formats orientés ligne. Wrapper BlockInputStreamFromRowInputStream et BlockOutputStreamFromRowOutputStream vous permet de convertir des flux orientés ligne en flux orientés blocs réguliers.

I/O

Pour lentrée/sortie orientée octet, il y a ReadBuffer et WriteBuffer les classes abstraites. Ils sont utilisés à la place de C++ iostreams. Ne vous inquiétez pas: chaque projet c++ mature utilise autre chose que iostreams pour de bonnes raisons.

ReadBuffer et WriteBuffer sont juste un tampon contigu et un curseur pointant vers la position dans ce tampon. Les implémentations peuvent posséder ou non la mémoire du tampon. Il existe une méthode virtuelle pour remplir le tampon avec les données suivantes (pour ReadBuffer) ou pour vider le tampon quelque part (pour WriteBuffer). Les méthodes virtuelles sont rarement cités.

Les implémentations de ReadBuffer/WriteBuffer sont utilisés pour travailler avec des fichiers et des descripteurs de fichiers et des sockets réseau, pour implémenter la compression (CompressedWriteBuffer is initialized with another WriteBuffer and performs compression before writing data to it), and for other purposes the names ConcatReadBuffer, LimitReadBuffer, et HashingWriteBuffer parler pour eux-mêmes.

Read / WriteBuffers ne traite que les octets. Il y a des fonctions de ReadHelpers et WriteHelpers fichiers den-tête pour aider à formater lentrée / sortie. Par exemple, il existe des assistants pour écrire un nombre au format décimal.

Regardons ce qui se passe lorsque vous voulez écrire un ensemble de résultats dans JSON format de sortie standard (stdout). Vous avez un jeu de résultats prêt à être récupéré IBlockInputStream. Vous créez WriteBufferFromFileDescriptor(STDOUT_FILENO) pour écrire des octets dans stdout. Vous créez JSONRowOutputStream, initialisé avec qui WriteBuffer, pour écrire des lignes dans JSON à stdout. Vous créez BlockOutputStreamFromRowOutputStream de plus, pour la représenter comme IBlockOutputStream. Ensuite, vous appelez copyData pour transférer des données de IBlockInputStream de IBlockOutputStream et tout fonctionne. Interne, JSONRowOutputStream écrira divers délimiteurs JSON et appellera IDataType::serializeTextJSON méthode avec une référence à IColumn et le numéro de ligne comme arguments. Conséquent, IDataType::serializeTextJSON appellera une méthode de WriteHelpers.h: exemple, writeText pour les types numériques et writeJSONString pour DataTypeString.

Table

Le IStorage linterface représente les tables. Différentes implémentations de cette interface sont des moteurs de table différents. Les exemples sont StorageMergeTree, StorageMemory et ainsi de suite. Les Instances de ces classes ne sont que des tables.

Clé IStorage les méthodes sont read et write. Il y a aussi des alter, rename, drop et ainsi de suite. Le read méthode accepte les arguments suivants: lensemble de colonnes à lire à partir dun tableau, l AST requête à considérer, et le nombre souhaité de flux de retour. Il renvoie un ou plusieurs IBlockInputStream objets et informations sur létape de traitement des données qui a été effectuée dans un moteur de table lors de lexécution de la requête.

Dans la plupart des cas, la méthode read nest responsable que de la lecture des colonnes spécifiées à partir dune table, et non dun traitement ultérieur des données. Tout traitement ultérieur des données est effectué par linterpréteur de requêtes et nest pas de la responsabilité de IStorage.

Mais il y a des exceptions notables:

  • La requête AST est transmise au read et le moteur de table peut lutiliser pour dériver lutilisation de lindex et pour lire moins de données à partir dune table.
  • Parfois, le moteur de table peut traiter les données lui-même à une étape spécifique. Exemple, StorageDistributed peut envoyer une requête aux serveurs distants, leur demander de traiter les données à une étape où les données de différents serveurs distants peuvent être fusionnées, et renvoyer ces données prétraitées. Linterpréteur de requête termine ensuite le traitement des données.

Table read la méthode peut retourner plusieurs IBlockInputStream objets permettant le traitement parallèle des données. Ces flux dentrée de bloc multiples peuvent lire à partir dune table en parallèle. Ensuite, vous pouvez envelopper ces flux avec diverses transformations (telles que lévaluation dexpression ou le filtrage) qui peuvent être calculées indépendamment et créer un UnionBlockInputStream en plus deux, pour lire à partir de plusieurs flux en parallèle.

Il y a aussi des TableFunctions. Ce sont des fonctions qui renvoient un IStorage objet à utiliser dans le FROM la clause dune requête.

Pour avoir une idée rapide de la façon dimplémenter votre moteur de table, regardez quelque chose de simple, comme StorageMemory ou StorageTinyLog.

Comme le résultat de l read méthode, IStorage retourner QueryProcessingStage information about what parts of the query were already calculated inside storage.

Analyseur

Un analyseur de descente récursif écrit à la main analyse une requête. Exemple, ParserSelectQuery appelle simplement récursivement les analyseurs sous-jacents pour diverses parties de la requête. Les analyseurs créent un AST. Le AST est représenté par des nœuds, qui sont des instances de IAST.

Les générateurs danalyseurs ne sont pas utilisés pour des raisons historiques.

Interprète

Les interprètes sont responsables de la création du pipeline dexécution des requêtes à partir AST. Il existe des interprètes simples, tels que InterpreterExistsQuery et InterpreterDropQuery ou le plus sophistiqué de InterpreterSelectQuery. Le pipeline dexécution de requête est une combinaison de flux dentrée ou de sortie de bloc. Par exemple, le résultat de linterprétation de la SELECT la requête est la IBlockInputStream pour lire le jeu de résultats; le résultat de la requête dINSERTION est l IBlockOutputStream pour écrire des données à insérer, et le résultat de linterprétation INSERT SELECT la requête est la IBlockInputStream cela renvoie un jeu de résultats vide lors de la première lecture, mais qui copie SELECT de INSERT dans le même temps.

InterpreterSelectQuery utiliser ExpressionAnalyzer et ExpressionActions machines pour lanalyse des requêtes et des transformations. Cest là que la plupart des optimisations de requêtes basées sur des règles sont effectuées. ExpressionAnalyzer est assez désordonné et devrait être réécrit: diverses transformations et optimisations de requête doivent être extraites dans des classes séparées pour permettre des transformations modulaires ou une requête.

Fonction

Il y a des fonctions ordinaires et des fonctions agrégées. Pour les fonctions dagrégation, voir la section suivante.

Ordinary functions dont change the number of rows they work as if they are processing each row independently. In fact, functions are not called for individual rows, but for Blocks de données pour implémenter lexécution de requête vectorisée.

Il y a quelques fonctions diverses, comme la taille de bloc, rowNumberInBlock, et runningAccumulate, qui exploitent le traitement de bloc et violent lindépendance des lignes.

ClickHouse a un typage fort, donc il ny a pas de conversion de type implicite. Si une fonction ne prend pas en charge une combinaison spécifique de types, elle lève une exception. Mais les fonctions peuvent fonctionner (être surchargées) pour de nombreuses combinaisons de types différentes. Par exemple, l plus fonction (pour mettre en œuvre la + opérateur) fonctionne pour toute combinaison de types numériques: UInt8 + Float32, UInt16 + Int8 et ainsi de suite. En outre, certaines fonctions variadiques peuvent accepter nimporte quel nombre darguments, tels que concat fonction.

Limplémentation dune fonction peut être légèrement gênante car une fonction distribue explicitement les types de données pris en charge et pris en charge IColumns. Par exemple, l plus la fonction a du code généré par linstanciation Dun modèle C++ pour chaque combinaison de types numériques, et des arguments gauche et droit constants ou non constants.

Cest un excellent endroit pour implémenter la génération de code dexécution pour éviter le gonflement du code de modèle. En outre, il permet dajouter des fonctions fusionnées comme Fusionné Multiplier-Ajouter ou de faire plusieurs comparaisons dans une itération de boucle.

En raison de lexécution de requête vectorisée, les fonctions ne sont pas court-circuitées. Par exemple, si vous écrivez WHERE f(x) AND g(y) les deux faces sont calculés, même pour les lignes, quand f(x) est égal à zéro (sauf quand f(x) est une expression constante nulle). Mais si la sélectivité de l f(x) la condition est élevée, et le calcul de f(x) est beaucoup moins cher que g(y), il est préférable dimplémenter le calcul multi-pass. Il serait dabord calculer f(x) puis filtrer les colonnes par la suite, puis de calculer g(y) uniquement pour les petits morceaux de données filtrés.

Les Fonctions DAgrégation

Les fonctions dagrégation sont des fonctions avec État. Ils accumulent les valeurs passées dans certains etats et vous permettent dobtenir des résultats de cet état. Ils sont gérés avec le IAggregateFunction interface. Les États peuvent être assez simples (lÉtat pour AggregateFunctionCount est juste un seul UInt64 valeur) ou très complexes (létat de AggregateFunctionUniqCombined est une combinaison linéaire du tableau, dune table de hachage, et un HyperLogLog structure probabiliste des données).

Les États sont répartis en Arena (un pool de mémoire) pour traiter plusieurs états lors de lexécution dune cardinalité élevée GROUP BY requête. Les États peuvent avoir un constructeur et un destructeur non triviaux: par exemple, les États dagrégation compliqués peuvent allouer eux-mêmes de la mémoire supplémentaire. Il faut accorder une certaine attention à la création et à la destruction des États et à la transmission appropriée de leur propriété et de leur ordre de destruction.

Les États dagrégation peuvent être sérialisés et désérialisés pour passer sur le réseau pendant lexécution de la requête distribuée ou pour les écrire sur le disque où il ny a pas assez de RAM. Ils peuvent même être stockés dans une table avec le DataTypeAggregateFunction pour permettre lagrégation incrémentielle des données.

Le format de données sérialisé pour les états de fonction dagrégat nest pas versionné pour le moment. Cest ok si les États dagrégat ne sont stockés que temporairement. Mais nous avons l AggregatingMergeTree moteur de table pour lagrégation incrémentielle, et les gens lutilisent déjà en production. Cest la raison pour laquelle la rétrocompatibilité est requise lors de la modification du format sérialisé pour toute fonction dagrégat à lavenir.

Serveur

Le serveur implémente plusieurs interfaces différentes:

  • Une interface HTTP pour tous les clients étrangers.
  • Une interface TCP pour le client clickhouse natif et pour la communication inter-serveur lors de lexécution de la requête distribuée.
  • Une interface pour transférer des données pour la réplication.

En interne, il sagit simplement dun serveur multithread primitif sans coroutines ni fibres. Étant donné que le serveur nest pas conçu pour traiter un taux élevé de requêtes simples, mais pour traiter un taux relativement faible de requêtes complexes, chacun deux peut traiter une grande quantité de données à des fins danalyse.

Le serveur initialise le Context classe avec lenvironnement nécessaire à lexécution des requêtes: la liste des bases de données disponibles, des utilisateurs et des droits daccès, des paramètres, des clusters, la liste des processus, le journal des requêtes, etc. Les interprètes utilisent cet environnement.

Nous maintenons une compatibilité ascendante et descendante complète pour le protocole TCP du serveur: les anciens clients peuvent parler à de nouveaux serveurs, et les nouveaux clients peuvent parler à danciens serveurs. Mais nous ne voulons pas le maintenir éternellement, et nous supprimons le support pour les anciennes versions après environ un an.

!!! note "Note" Pour la plupart des applications externes, nous vous recommandons dutiliser Linterface HTTP car elle est simple et facile à utiliser. Le protocole TCP est plus étroitement lié aux structures de données internes: il utilise un format interne pour passer des blocs de données, et il utilise un cadrage personnalisé pour les données compressées. Nous navons pas publié de bibliothèque C pour ce protocole car elle nécessite de lier la plupart de la base de code ClickHouse, ce qui nest pas pratique.

Exécution De Requête Distribuée

Les serveurs dune configuration de cluster sont pour la plupart indépendants. Vous pouvez créer un Distributed table sur un ou tous les serveurs dans un cluster. Le Distributed table does not store data itself it only provides a “view” à toutes les tables sur plusieurs nœuds dun cluster. Lorsque vous sélectionnez à partir dun Distributed table, il réécrit cette requête, choisit les nœuds distants en fonction des paramètres déquilibrage de charge et leur envoie la requête. Le Distributed table demande aux serveurs distants de traiter une requête jusquà une étape où les résultats intermédiaires de différents serveurs peuvent être fusionnés. Puis il reçoit les résultats intermédiaires et les fusionne. La table distribuée essaie de distribuer autant de travail que possible aux serveurs distants et nenvoie pas beaucoup de données intermédiaires sur le réseau.

Les choses deviennent plus compliquées lorsque vous avez des sous-requêtes dans des clauses IN ou JOIN, et que chacune delles utilise un Distributed table. Nous avons différentes stratégies pour lexécution de ces requêtes.

Il nexiste pas de plan de requête global pour lexécution des requêtes distribuées. Chaque nœud a son plan de requête local pour sa partie du travail. Nous navons quune simple exécution de requête distribuée en une seule passe: nous envoyons des requêtes pour les nœuds distants, puis fusionnons les résultats. Mais cela nest pas possible pour les requêtes compliquées avec des groupes de cardinalité élevés ou avec une grande quantité de données temporaires pour la jointure. Dans de tels cas, nous avons besoin de “reshuffle” données entre les serveurs, ce qui nécessite une coordination supplémentaire. ClickHouse ne supporte pas ce type dexécution de requête, et nous devons y travailler.

Fusion De LArbre

MergeTree est une famille de moteurs de stockage qui prend en charge lindexation par clé primaire. La clé primaire peut être un tuple arbitraire de colonnes ou dexpressions. De données dans un MergeTree la table est stockée dans “parts”. Chaque partie stocke les données dans lordre de la clé primaire, de sorte que les données sont ordonnées lexicographiquement par le tuple de clé primaire. Toutes les colonnes du tableau sont stockés dans différents column.bin les fichiers dans ces régions. Les fichiers sont constitués de blocs compressés. Chaque bloc est généralement de 64 KO à 1 Mo de données non compressées, en fonction de la taille de la valeur moyenne. Les blocs sont constitués de valeurs de colonne placées de manière contiguë lune après lautre. Les valeurs de colonne sont dans le même ordre pour chaque colonne (la clé primaire définit lordre), donc lorsque vous itérez par plusieurs colonnes, vous obtenez des valeurs pour les lignes correspondantes.

La clé primaire elle-même est “sparse”. Il ne traite pas chaque ligne, mais seulement certaines plages de données. Séparé primary.idx fichier a la valeur de la clé primaire pour chaque N-ième ligne, où N est appelé index_granularity (habituellement, N = 8192). Aussi, pour chaque colonne, nous avons column.mrk les fichiers avec l “marks,” qui sont des décalages à chaque N-ème ligne dans le fichier de données. Chaque marque est une paire: le décalage dans le fichier au début du bloc compressé, et le décalage dans le bloc décompressé au début des données. Habituellement, les blocs compressés sont alignés par des marques, et le décalage dans le bloc décompressé est nul. Les données pour primary.idx réside toujours dans la mémoire, et les données pour column.mrk les fichiers sont mis en cache.

Quand nous allons lire quelque chose dune partie dans MergeTree nous regardons primary.idx données et locate plages qui pourraient contenir des données demandées, puis regardez column.mrk données et calculer des décalages pour savoir où commencer à lire ces plages. En raison de la rareté, les données excédentaires peuvent être lues. ClickHouse ne convient pas à une charge élevée de requêtes ponctuelles simples, car toute la gamme avec index_granularity les lignes doivent être lues pour chaque clé, et le bloc compressé entier doit être décompressé pour chaque colonne. Nous avons rendu lindex clairsemé parce que nous devons être en mesure de maintenir des milliards de lignes par serveur unique sans consommation de mémoire notable pour lindex. De plus, comme la clé primaire est clairsemée, elle nest pas unique: elle ne peut pas vérifier lexistence de la clé dans la table au moment de linsertion. Vous pourriez avoir plusieurs lignes avec la même clé dans une table.

Lorsque vous INSERT un tas de données dans MergeTree, ce groupe est trié par ordre de clé primaire et forme une nouvelle partie. Il existe des threads darrière-plan qui sélectionnent périodiquement certaines parties et les fusionnent en une seule partie triée pour maintenir le nombre de parties relativement faible. Cest pourquoi il est appelé MergeTree. Bien sûr, la fusion conduit à “write amplification”. Toutes les parties sont immuables: elles sont seulement créées et supprimées, mais pas modifiées. Lorsque SELECT est exécuté, il contient un instantané de la table (un ensemble de parties). Après la Fusion, nous conservons également les anciennes pièces pendant un certain temps pour faciliter une récupération après une défaillance, donc si nous voyons quune partie fusionnée est probablement cassée, nous pouvons la remplacer par ses parties sources.

MergeTree nest pas un arbre LSM car il ne contient pas “memtable” et “log”: inserted data is written directly to the filesystem. This makes it suitable only to INSERT data in batches, not by individual row and not very frequently about once per second is ok, but a thousand times a second is not. We did it this way for simplicitys sake, and because we are already inserting data in batches in our applications.

Les tables MergeTree ne peuvent avoir quun seul index (primaire): il ny a pas dindex secondaires. Il serait bon dautoriser plusieurs représentations physiques sous une table logique, par exemple, pour stocker des données dans plus dun ordre physique ou même pour autoriser des représentations avec des données pré-agrégées avec des données originales.

Il existe des moteurs MergeTree qui effectuent un travail supplémentaire lors des fusions en arrière-plan. Les exemples sont CollapsingMergeTree et AggregatingMergeTree. Cela pourrait être traité comme un support spécial pour les mises à jour. Gardez à lesprit que ce ne sont pas de vraies mises à jour car les utilisateurs nont généralement aucun contrôle sur le moment où les fusions en arrière-plan sont exécutées et les données dans un MergeTree la table est presque toujours stockée dans plus dune partie, pas sous une forme complètement fusionnée.

Réplication

La réplication dans ClickHouse peut être configurée sur une base par table. Vous pouvez avoir des tables répliquées et des tables non répliquées sur le même serveur. Vous pouvez également avoir des tables répliquées de différentes manières, comme une table avec une réplication à deux facteurs et une autre avec trois facteurs.

La réplication est implémentée dans le ReplicatedMergeTree moteur de stockage. Le chemin daccès dans ZooKeeper est spécifié comme paramètre pour le moteur de stockage. Toutes les tables avec le même chemin dans ZooKeeper devenez des répliques les unes des autres: elles synchronisent leurs données et maintiennent la cohérence. Les répliques peuvent être ajoutées et supprimées dynamiquement simplement en créant ou en supprimant une table.

La réplication utilise un schéma multi-maître asynchrone. Vous pouvez insérer des données dans nimporte quel réplica qui a une session avec ZooKeeper, et les données sont répliquées à toutes les autres répliques de manière asynchrone. Parce que ClickHouse ne prend pas en charge les mises à jour, la réplication est sans conflit. Comme il ny a pas daccusé de réception de quorum des insertions, les données juste insérées peuvent être perdues si un nœud échoue.

Les métadonnées pour la réplication sont stockées dans ZooKeeper. Il existe un journal de réplication qui répertorie les actions à effectuer. Les Actions sont: obtenir une partie; fusionner des parties; déposer une partition, et ainsi de suite. Chaque réplica copie le journal de réplication dans sa file dattente, puis exécute les actions de la file dattente. Par exemple, sur linsertion, l “get the part” laction est créée dans le journal, et chaque réplique téléchargements de la partie. Les fusions sont coordonnées entre les répliques pour obtenir des résultats identiques aux octets. Toutes les parties sont fusionnées de la même manière sur toutes les répliques. Il est réalisé en élisant une réplique en tant que leader, et cette réplique initie fusionne et écrit “merge parts” actions dans le journal.

La réplication est physique: seules les parties compressées sont transférées entre les nœuds, pas les requêtes. Les fusions sont traitées sur chaque réplique indépendamment dans la plupart des cas pour réduire les coûts du réseau en évitant lamplification du réseau. Grand fusionné les pièces sont envoyées sur le réseau uniquement en cas de retard de réplication.

En outre, chaque réplique stocke son état dans ZooKeeper comme lensemble des pièces et ses sommes de contrôle. Lorsque létat sur le système de fichiers local diverge de létat de référence dans ZooKeeper, le réplica restaure sa cohérence en téléchargeant les parties manquantes et brisées à partir dautres réplicas. Lorsquil y a des données inattendues ou brisées dans le système de fichiers local, ClickHouse ne les supprime pas, mais les déplace dans un répertoire séparé et les oublie.

!!! note "Note" Le cluster ClickHouse est constitué de fragments indépendants, et chaque fragment est constitué de répliques. Le cluster est pas élastique, donc, après avoir ajouté un nouveau fragment, les données ne sont pas rééquilibrées automatiquement entre les fragments. Au lieu de cela, la charge du cluster est censée être ajustée pour être inégale. Cette implémentation vous donne plus de contrôle, et cest ok pour des clusters relativement petits, tels que des dizaines de nœuds. Mais pour les clusters avec des centaines de nœuds que nous utilisons en production, cette approche devient un inconvénient important. Nous devrions implémenter un moteur de table qui sétend sur le cluster avec des régions répliquées dynamiquement qui pourraient être divisées et équilibrées automatiquement entre les clusters.

{## Article Original ##}