<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ## **ClickHouse 架構概述** &emsp;&emsp;ClickHouse 是一個真正的列式數據庫管理系統(DBMS)。在 ClickHouse 中,數據始終是按列存儲的,包括矢量(向量或列塊)執行的過程。只要有可能,操作都是基于矢量進行分派的,而不是單個的值,這被稱為?矢量化查詢執行?,它有利于降低實際的數據處理開銷。 > 這個想法并不新鮮,其可以追溯到`APL`編程語言及其后代:`A +`、`J`、`K`和`Q`。矢量編程被大量用于科學數據處理中。即使在關系型數據庫中,這個想法也不是什么新的東西:比如,矢量編程也被大量用于`Vectorwise`系統中。 &emsp;&emsp;通常有兩種不同的加速查詢處理的方法:矢量化查詢執行和運行時代碼生成。在后者中,動態地為每一類查詢生成代碼,消除了間接分派和動態分派。這兩種方法中,并沒有哪一種嚴格地比另一種好。運行時代碼生成可以更好地將多個操作融合在一起,從而充分利用 CPU 執行單元和流水線。矢量化查詢執行不是特別實用,因為它涉及必須寫到緩存并讀回的臨時向量。如果 L2 緩存容納不下臨時數據,那么這將成為一個問題。但矢量化查詢執行更容易利用 CPU 的 SIMD 功能。朋友寫的一篇研究論文表明,將兩種方法結合起來是更好的選擇。ClickHouse 使用了矢量化查詢執行,同時初步提供了有限的運行時動態代碼生成。 ## 列(Columns) &emsp;&emsp;要表示內存中的列(實際上是列塊),需使用`IColumn`接口。該接口提供了用于實現各種關系操作符的輔助方法。幾乎所有的操作都是不可變的:這些操作不會更改原始列,但是會創建一個新的修改后的列。比如,`IColumn::filter`方法接受過濾字節掩碼,用于`WHERE`和`HAVING`關系操作符中。另外的例子:`IColumn::permute`方法支持`ORDER BY`實現,`IColumn::cut`方法支持`LIMIT`實現等等。 &emsp;&emsp;不同的`IColumn`實現(`ColumnUInt8`、`ColumnString`等)負責不同的列內存布局。內存布局通常是一個連續的數組。對于數據類型為整型的列,只是一個連續的數組,比如`std::vector`。對于`String`列和`Array`列,則由兩個向量組成:其中一個向量連續存儲所有的`String`或數組元素,另一個存儲每一個`String`或`Array`的起始元素在第一個向量中的偏移。而`ColumnConst`則僅在內存中存儲一個值,但是看起來像一個列。 ## 字段 &emsp;&emsp;盡管如此,有時候也可能需要處理單個值。表示單個值,可以使用`Field`。`Field`是`UInt64`、`Int64`、`Float64`、`String`和`Array`組成的聯合。`IColumn`擁有`operator[]`方法來獲取第`n`個值成為一個`Field`,同時也擁有`insert`方法將一個`Field`追加到一個列的末尾。這些方法并不高效,因為它們需要處理表示單一值的臨時`Field`對象,但是有更高效的方法比如`insertFrom`和`insertRangeFrom`等。 &ensp;&ensp;`Field`中并沒有足夠的關于一個表(table)的特定數據類型的信息。比如,`UInt8`、`UInt16`、`UInt32`和`UInt64`在`Field`中均表示為`UInt64`。 ## 抽象漏洞 &emsp;&emsp;`IColumn`具有用于數據的常見關系轉換的方法,但這些方法并不能夠滿足所有需求。比如,`ColumnUInt64`沒有用于計算兩列和的方法,`ColumnString`沒有用于進行子串搜索的方法。這些無法計算的例程在`Icolumn`之外實現。 &emsp;&emsp;列(Columns)上的各種函數可以通過使用`Icolumn`的方法來提取`Field`值,或根據特定的`Icolumn`實現的數據內存布局的知識,以一種通用但不高效的方式實現。為此,函數將會轉換為特定的`IColumn`類型并直接處理內部表示。比如,`ColumnUInt64`具有`getData`方法,該方法返回一個指向列的內部數組的引用,然后一個單獨的例程可以直接讀寫或填充該數組。實際上,?抽象漏洞(leaky abstractions)?允許我們以更高效的方式來實現各種特定的例程。 ## 數據類型 `IDataType`負責序列化和反序列化:讀寫二進制或文本形式的列或單個值構成的塊。`IDataType`直接與表的數據類型相對應。比如,有`DataTypeUInt32`、`DataTypeDateTime`、`DataTypeString`等數據類型。 `IDataType`與`IColumn`之間的關聯并不大。不同的數據類型在內存中能夠用相同的`IColumn`實現來表示。比如,`DataTypeUInt32`和`DataTypeDateTime`都是用`ColumnUInt32`或`ColumnConstUInt32`來表示的。另外,相同的數據類型也可以用不同的`IColumn`實現來表示。比如,`DataTypeUInt8`既可以使用`ColumnUInt8`來表示,也可以使用過`ColumnConstUInt8`來表示。 `IDataType`僅存儲元數據。比如,`DataTypeUInt8`不存儲任何東西(除了 vptr);`DataTypeFixedString`僅存儲`N`(固定長度字符串的串長度)。 `IDataType`具有針對各種數據格式的輔助函數。比如如下一些輔助函數:序列化一個值并加上可能的引號;序列化一個值用于 JSON 格式;序列化一個值作為 XML 格式的一部分。輔助函數與數據格式并沒有直接的對應。比如,兩種不同的數據格式`Pretty`和`TabSeparated`均可以使用`IDataType`接口提供的`serializeTextEscaped`這一輔助函數。 ## 塊(Block) `Block`是表示內存中表的子集(chunk)的容器,是由三元組:`(IColumn, IDataType, 列名)`構成的集合。在查詢執行期間,數據是按`Block`進行處理的。如果我們有一個`Block`,那么就有了數據(在`IColumn`對象中),有了數據的類型信息告訴我們如何處理該列,同時也有了列名(來自表的原始列名,或人為指定的用于臨時計算結果的名字)。 &emsp;&emsp;當我們遍歷一個塊中的列進行某些函數計算時,會把結果列加入到塊中,但不會更改函數參數中的列,因為操作是不可變的。之后,不需要的列可以從塊中刪除,但不是修改。這對于消除公共子表達式非常方便。 `Block`用于處理數據塊。注意,對于相同類型的計算,列名和類型對不同的塊保持相同,僅列數據不同。最好把塊數據(block data)和塊頭(block header)分離開來,因為小塊大小會因復制共享指針和列名而帶來很高的臨時字符串開銷。 ## 塊流(Block Streams) &emsp;&emsp;塊流用于處理數據。我們可以使用塊流從某個地方讀取數據,執行數據轉換,或將數據寫到某個地方。`IBlockInputStream`具有`read`方法,其能夠在數據可用時獲取下一個塊。`IBlockOutputStream`具有`write`方法,其能夠將塊寫到某處。 *塊流負責:* + 1. 讀或寫一個表。表僅返回一個流用于讀寫塊。 + 2. 完成數據格式化。比如,如果你打算將數據以`Pretty`格式輸出到終端,你可以創建一個塊輸出流,將塊寫入該流中,然后進行格式化。 + 3. 執行數據轉換。假設你現在有`IBlockInputStream`并且打算創建一個過濾流,那么你可以創建一個`FilterBlockInputStream`并用`IBlockInputStream`進行初始化。之后,當你從`FilterBlockInputStream`中拉取塊時,會從你的流中提取一個塊,對其進行過濾,然后將過濾后的塊返回給你。查詢執行流水線就是以這種方式表示的。 &emsp;&emsp;還有一些更復雜的轉換。比如,當你從`AggregatingBlockInputStream`拉取數據時,會從數據源讀取全部數據進行聚集,然后將聚集后的數據流返回給你。另一個例子:`UnionBlockInputStream`的構造函數接受多個輸入源和多個線程,其能夠啟動多線程從多個輸入源并行讀取數據。 > 塊流使用?pull?方法來控制流:當你從第一個流中拉取塊時,它會接著從嵌套的流中拉取所需的塊,然后整個執行流水線開始工作。?pull?和?push?都不是最好的方案,因為控制流不是明確的,這限制了各種功能的實現,比如多個查詢同步執行(多個流水線合并到一起)。這個限制可以通過協程或直接運行互相等待的線程來解決。如果控制流明確,那么我們會有更多的可能性:如果我們定位了數據從一個計算單元傳遞到那些外部的計算單元中其中一個計算單元的邏輯。閱讀這篇文章來獲取更多的想法。 &emsp;&emsp;我們需要注意,查詢執行流水線在每一步都會創建臨時數據。我們要盡量使塊的大小足夠小,從而 CPU 緩存能夠容納下臨時數據。在這個假設下,與其他計算相比,讀寫臨時數據幾乎是沒有任何開銷的。我們也可以考慮一種替代方案:將流水線中的多個操作融合在一起,使流水線盡可能短,并刪除大量臨時數據。這可能是一個優點,但同時也有缺點。比如,拆分流水線使得中間數據緩存、獲取同時運行的類似查詢的中間數據以及相似查詢的流水線合并等功能很容易實現。 ## 格式(Formats &emsp;&emsp;數據格式同塊流一起實現。既有僅用于向客戶端輸出數據的?展示?格式,如`IBlockOutputStream`提供的`Pretty`格式,也有其它輸入輸出格式,比如`TabSeparated`或`JSONEachRow`。 &emsp;&emsp;此外還有行流:`IRowInputStream`和`IRowOutputStream`。它們允許你按行 pull/push 數據,而不是按塊。行流只需要簡單地面向行格式實現。包裝器`BlockInputStreamFromRowInputStream`和`BlockOutputStreamFromRowOutputStream`允許你將面向行的流轉換為正常的面向塊的流。 ## I/O[ &emsp;&emsp;對于面向字節的輸入輸出,有`ReadBuffer`和`WriteBuffer`這兩個抽象類。它們用來替代 C++ 的`iostream`。不用擔心:每個成熟的 C++ 項目都會有充分的理由使用某些東西來代替`iostream`。 &emsp;&emsp;`ReadBuffer`和`WriteBuffer`由一個連續的緩沖區和指向緩沖區中某個位置的一個指針組成。實現中,緩沖區可能擁有內存,也可能不擁有內存。有一個虛方法會使用隨后的數據來填充緩沖區(針對`ReadBuffer`)或刷新緩沖區(針對`WriteBuffer`),該虛方法很少被調用。 &emsp;&emsp;`ReadBuffer`和`WriteBuffer`的實現用于處理文件、文件描述符和網絡套接字(socket),也用于實現壓縮(`CompressedWriteBuffer`在寫入數據前需要先用一個`WriteBuffer`進行初始化并進行壓縮)和其它用途。`ConcatReadBuffer`、`LimitReadBuffer`和`HashingWriteBuffer`的用途正如其名字所描述的一樣。 &emsp;&emsp;`ReadBuffer`和`WriteBuffer`僅處理字節。為了實現格式化輸入和輸出(比如以十進制格式寫一個數字),`ReadHelpers`和`WriteHelpers`頭文件中有一些輔助函數可用。 &emsp;&emsp;讓我們來看一下,當你把一個結果集以`JSON`格式寫到標準輸出(stdout)時會發生什么。你已經準備好從`IBlockInputStream`獲取結果集,然后創建`WriteBufferFromFileDescriptor(STDOUT_FILENO)`用于寫字節到標準輸出,創建`JSONRowOutputStream`并用`WriteBuffer`初始化,用于將行以`JSON`格式寫到標準輸出,你還可以在其上創建`BlockOutputStreamFromRowOutputStream`,將其表示為`IBlockOutputStream`。然后調用`copyData`將數據從`IBlockInputStream`傳輸到`IBlockOutputStream`,一切工作正常。在內部,`JSONRowOutputStream`會寫入 JSON 分隔符,并以指向`IColumn`的引用和行數作為參數調用`IDataType::serializeTextJSON`函數。隨后,`IDataType::serializeTextJSON`將會調用`WriteHelpers.h`中的一個方法:比如,`writeText`用于數值類型,`writeJSONString`用于`DataTypeString`。 ## 表(Tables) &emsp;&emsp;表由`IStorage`接口表示。該接口的不同實現對應不同的表引擎。比如`StorageMergeTree`、`StorageMemory`等。這些類的實例就是表。 &emsp;&emsp;`IStorage`中最重要的方法是`read`和`write`,除此之外還有`alter`、`rename`和`drop`等方法。`read`方法接受如下參數:需要從表中讀取的列集,需要執行的`AST`查詢,以及所需返回的流的數量。`read`方法的返回值是一個或多個`IBlockInputStream`對象,以及在查詢執行期間在一個表引擎內完成的關于數據處理階段的信息。 &emsp;&emsp;在大多數情況下,`read`方法僅負責從表中讀取指定的列,而不會進行進一步的數據處理。進一步的數據處理均由查詢解釋器完成,不由`IStorage`負責。 但是也有值得注意的例外: * AST 查詢被傳遞給`read`方法,表引擎可以使用它來判斷是否能夠使用索引,從而從表中讀取更少的數據。 * 有時候,表引擎能夠將數據處理到一個特定階段。比如,`StorageDistributed`可以向遠程服務器發送查詢,要求它們將來自不同的遠程服務器能夠合并的數據處理到某個階段,并返回預處理后的數據,然后查詢解釋器完成后續的數據處理。 &emsp;&emsp;表的`read`方法能夠返回多個`IBlockInputStream`對象以允許并行處理數據。多個塊輸入流能夠從一個表中并行讀取。然后你可以通過不同的轉換對這些流進行裝飾(比如表達式求值或過濾),轉換過程能夠獨立計算,并在其上創建一個`UnionBlockInputStream`,以并行讀取多個流。 &emsp;&emsp;另外也有`TableFunction`。`TableFunction`能夠在查詢的`FROM`字句中返回一個臨時的`IStorage`以供使用。 要快速了解如何實現自己的表引擎,可以查看一些簡單的表引擎,比如`StorageMemory`或`StorageTinyLog`。 > 作為`read`方法的結果,`IStorage`返回`QueryProcessingStage`\- 關于 storage 里哪部分查詢已經被計算的信息。當前我們僅有非常粗粒度的信息。Storage 無法告訴我們?對于這個范圍的數據,我已經處理完了 WHERE 字句里的這部分表達式?。我們需要在這個地方繼續努力。 ## 解析器(Parsers) &emsp;&emsp;查詢由一個手寫遞歸下降解析器解析。比如,`ParserSelectQuery`只是針對查詢的不同部分遞歸地調用下層解析器。解析器創建`AST`。`AST`由節點表示,節點是`IAST`的實例。 > 由于歷史原因,未使用解析器生成器。 ## 解釋器(Interpreters) &emsp;&emsp;解釋器負責從`AST`創建查詢執行流水線。既有一些簡單的解釋器,如`InterpreterExistsQuery`和`InterpreterDropQuery`,也有更復雜的解釋器,如`InterpreterSelectQuery`。查詢執行流水線由塊輸入或輸出流組成。比如,`SELECT`查詢的解釋結果是從`FROM`字句的結果集中讀取數據的`IBlockInputStream`;`INSERT`查詢的結果是寫入需要插入的數據的`IBlockOutputStream`;`SELECT INSERT`查詢的解釋結果是`IBlockInputStream`,它在第一次讀取時返回一個空結果集,同時將數據從`SELECT`復制到`INSERT`。 `InterpreterSelectQuery`使用`ExpressionAnalyzer`和`ExpressionActions`機制來進行查詢分析和轉換。這是大多數基于規則的查詢優化完成的地方。`ExpressionAnalyzer`非常混亂,應該進行重寫:不同的查詢轉換和優化應該被提取出來并劃分成不同的類,從而允許模塊化轉換或查詢。 ## 函數(Functions) &emsp;&emsp;函數既有普通函數,也有聚合函數。對于聚合函數,請看下一節。 &emsp;&emsp;普通函數不會改變行數 - 它們的執行看起來就像是獨立地處理每一行數據。實際上,函數不會作用于一個單獨的行上,而是作用在以`Block`為單位的數據上,以實現向量查詢執行。 &emsp;&emsp;還有一些雜項函數,比如塊大小,它們對塊進行處理,并且不遵從行的獨立性。 &emsp;&emsp;ClickHouse 具有強類型,因此隱式類型轉換不會發生。如果函數不支持某個特定的類型組合,則會拋出異常。但函數可以通過重載以支持許多不同的類型組合。比如,`plus`函數(用于實現`+`運算符)支持任意數字類型的組合:`UInt8`+`Float32`,`UInt16`+`Int8`等。同時,一些可變參數的函數能夠級接收任意數目的參數,比如`concat`函數。 &emsp;&emsp;實現函數可能有些不方便,因為函數的實現需要包含所有支持該操作的數據類型和`IColumn`類型。比如,`plus`函數能夠利用 C++ 模板針對不同的數字類型組合、常量以及非常量的左值和右值進行代碼生成。 > 這是一個實現動態代碼生成的好地方,從而能夠避免模板代碼膨脹。同樣,運行時代碼生成也使得實現融合函數成為可能,比如融合?乘-加?,或者在單層循環迭代中進行多重比較。 &emsp;&emsp;由于向量查詢執行,函數不會?短路?。比如,如果你寫`WHERE f(x) AND g(y)`,兩邊都會進行計算,即使是對于`f(x)`為 0 的行(除非`f(x)`是零常量表達式)。但是如果`f(x)`的選擇條件很高,并且計算`f(x)`比計算`g(y)`要劃算得多,那么最好進行多遍計算:首先計算`f(x)`,根據計算結果對列數據進行過濾,然后計算`g(y)`,之后只需對較小數量的數據進行過濾。 ## 聚合函數 &emsp;&emsp;聚合函數是狀態函數。它們將傳入的值激活到某個狀態,并允許你從該狀態獲取結果。聚合函數使用`IAggregateFunction`接口進行管理。狀態可以非常簡單(`AggregateFunctionCount`的狀態只是一個單一的`UInt64`值),也可以非常復雜(`AggregateFunctionUniqCombined`的狀態是由一個線性數組、一個散列表和一個`HyperLogLog`概率數據結構組合而成的)。 &emsp;&emsp;為了能夠在執行一個基數很大的`GROUP BY`查詢時處理多個聚合狀態,需要在`Arena`(一個內存池)或任何合適的內存塊中分配狀態。狀態可以有一個非平凡的構造器和析構器:比如,復雜的聚合狀態能夠自己分配額外的內存。這需要注意狀態的創建和銷毀并恰當地傳遞狀態的所有權,以跟蹤誰將何時銷毀狀態。 &emsp;&emsp;聚合狀態可以被序列化和反序列化,以在分布式查詢執行期間通過網絡傳遞或者在內存不夠的時候將其寫到硬盤。聚合狀態甚至可以通過`DataTypeAggregateFunction`存儲到一個表中,以允許數據的增量聚合。 > 聚合函數狀態的序列化數據格式目前尚未版本化。如果只是臨時存儲聚合狀態,這樣是可以的。但是我們有`AggregatingMergeTree`表引擎用于增量聚合,并且人們已經在生產中使用它。這就是為什么在未來當我們更改任何聚合函數的序列化格式時需要增加向后兼容的支持。 ## 服務器(Server) 服務器實現了多個不同的接口: * 一個用于任何外部客戶端的 HTTP 接口。 * 一個用于本機 ClickHouse 客戶端以及在分布式查詢執行中跨服務器通信的 TCP 接口。 * 一個用于傳輸數據以進行拷貝的接口。 &emsp;&emsp;在內部,它只是一個沒有協程、纖程等的基礎多線程服務器。服務器不是為處理高速率的簡單查詢設計的,而是為處理相對低速率的復雜查詢設計的,每一個復雜查詢能夠對大量的數據進行處理分析。 &emsp;&emsp;服務器使用必要的查詢執行需要的環境初始化`Context`類:可用數據庫列表、用戶和訪問權限、設置、集群、進程列表和查詢日志等。這些環境被解釋器使用。 &emsp;&emsp;我們維護了服務器 TCP 協議的完全向后向前兼容性:舊客戶端可以和新服務器通信,新客戶端也可以和舊服務器通信。但是我們并不想永久維護它,我們將在大約一年后刪除對舊版本的支持。 > 對于所有的外部應用,我們推薦使用 HTTP 接口,因為該接口很簡單,容易使用。TCP 接口與內部數據結構的聯系更加緊密:它使用內部格式傳遞數據塊,并使用自定義幀來壓縮數據。我們沒有發布該協議的 C 庫,因為它需要鏈接大部分的 ClickHouse 代碼庫,這是不切實際的。 ## 分布式查詢執行 &emsp;&emsp;集群設置中的服務器大多是獨立的。你可以在一個集群中的一個或多個服務器上創建一個`Distributed`表。`Distributed`表本身并不存儲數據,它只為集群的多個節點上的所有本地表提供一個?視圖(view)?。當從`Distributed`表中進行 SELECT 時,它會重寫該查詢,根據負載平衡設置來選擇遠程節點,并將查詢發送給節點。`Distributed`表請求遠程服務器處理查詢,直到可以合并來自不同服務器的中間結果的階段。然后它接收中間結果并進行合并。分布式表會嘗試將盡可能多的工作分配給遠程服務器,并且不會通過網絡發送太多的中間數據。 > 當`IN`或`JOIN`子句中包含子查詢并且每個子查詢都使用分布式表時,事情會變得更加復雜。我們有不同的策略來執行這些查詢。 &emsp;&emsp;分布式查詢執行沒有全局查詢計劃。每個節點都有針對自己的工作部分的本地查詢計劃。我們僅有簡單的一次性分布式查詢執行:將查詢發送給遠程節點,然后合并結果。但是對于具有高基數的`GROUP BY`或具有大量臨時數據的`JOIN`這樣困難的查詢的來說,這是不可行的:在這種情況下,我們需要在服務器之間?改組?數據,這需要額外的協調。ClickHouse 不支持這類查詢執行,我們需要在這方面進行努力。 ## 合并樹[?] &emsp;&emsp;`MergeTree`是一系列支持按主鍵索引的存儲引擎。主鍵可以是一個任意的列或表達式的元組。`MergeTree`表中的數據存儲于?分塊?中。每一個分塊以主鍵序存儲數據(數據按主鍵元組的字典序排序)。表的所有列都存儲在這些?分塊?中分離的`column.bin`文件中。`column.bin`文件由壓縮塊組成,每一個塊通常是 64 KB 到 1 MB 大小的未壓縮數據,具體取決于平均值大小。這些塊由一個接一個連續放置的列值組成。每一列的列值順序相同(順序由主鍵定義),因此當你按多列進行迭代時,你能夠得到相應列的值。 &emsp;&emsp;主鍵本身是?稀疏?的。它并不是索引單一的行,而是索引某個范圍內的數據。一個單獨的`primary.idx`文件具有每個第 N 行的主鍵值,其中 N 稱為`index_granularity`(通常,N = 8192)。同時,對于每一列,都有帶有標記的`column.mrk`文件,該文件記錄的是每個第 N 行在數據文件中的偏移量。每個標記是一個 pair:文件中的偏移量到壓縮塊的起始,以及解壓縮塊中的偏移量到數據的起始。通常,壓縮塊根據標記對齊,并且解壓縮塊中的偏移量為 0。`primary.idx`的數據始終駐留在內存,同時`column.mrk`的數據被緩存。 &emsp;&emsp;當我們要從`MergeTree`的一個分塊中讀取部分內容時,我們會查看`primary.idx`數據并查找可能包含所請求數據的范圍,然后查看`column.mrk`并計算偏移量從而得知從哪里開始讀取些范圍的數據。由于稀疏性,可能會讀取額外的數據。ClickHouse 不適用于高負載的簡單點查詢,因為對于每一個鍵,整個`index_granularity`范圍的行的數據都需要讀取,并且對于每一列需要解壓縮整個壓縮塊。我們使索引稀疏,是因為每一個單一的服務器需要在索引沒有明顯內存消耗的情況下,維護數萬億行的數據。另外,由于主鍵是稀疏的,導致其不是唯一的:無法在 INSERT 時檢查一個鍵在表中是否存在。你可以在一個表中使用同一個鍵創建多個行。 &emsp;&emsp;當你向`MergeTree`中插入一堆數據時,數據按主鍵排序并形成一個新的分塊。為了保證分塊的數量相對較少,有后臺線程定期選擇一些分塊并將它們合并成一個有序的分塊,這就是`MergeTree`的名稱來源。當然,合并會導致?寫入放大?。所有的分塊都是不可變的:它們僅會被創建和刪除,不會被修改。當運行`SELECT`查詢時,`MergeTree`會保存一個表的快照(分塊集合)。合并之后,還會保留舊的分塊一段時間,以便發生故障后更容易恢復,因此如果我們發現某些合并后的分塊可能已損壞,我們可以將其替換為原分塊。 &emsp;&emsp;`MergeTree`不是 LSM 樹,因為它不包含?memtable?和?log?:插入的數據直接寫入文件系統。這使得它僅適用于批量插入數據,而不適用于非常頻繁地一行一行插入 - 大約每秒一次是沒問題的,但是每秒一千次就會有問題。我們這樣做是為了簡單起見,因為我們已經在我們的應用中批量插入數據。 > `MergeTree`表只能有一個(主)索引:沒有任何輔助索引。在一個邏輯表下,允許有多個物理表示,比如,可以以多個物理順序存儲數據,或者同時表示預聚合數據和原始數據。 &emsp;&emsp;有些`MergeTree`引擎會在后臺合并期間做一些額外工作,比如`CollapsingMergeTree`和`AggregatingMergeTree`。這可以視為對更新的特殊支持。請記住這些不是真正的更新,因為用戶通常無法控制后臺合并將會執行的時間,并且`MergeTree`中的數據幾乎總是存儲在多個分塊中,而不是完全合并的形式。 ## 復制(Replication) &emsp;&emsp;ClickHouse 中的復制是基于表實現的。你可以在同一個服務器上有一些可復制的表和不可復制的表。你也可以以不同的方式進行表的復制,比如一個表進行雙因子復制,另一個進行三因子復制。 &emsp;&emsp;復制是在`ReplicatedMergeTree`存儲引擎中實現的。`ZooKeeper`中的路徑被指定為存儲引擎的參數。`ZooKeeper`中所有具有相同路徑的表互為副本:它們同步數據并保持一致性。只需創建或刪除表,就可以實現動態添加或刪除副本。 &emsp;&emsp;復制使用異步多主機方案。你可以將數據插入到與`ZooKeeper`進行會話的任意副本中,并將數據復制到所有其它副本中。由于 ClickHouse 不支持 UPDATEs,因此復制是無沖突的。由于沒有對插入的仲裁確認,如果一個節點發生故障,剛剛插入的數據可能會丟失。 &emsp;&emsp;用于復制的元數據存儲在 ZooKeeper 中。其中一個復制日志列出了要執行的操作。操作包括:獲取分塊、合并分塊和刪除分區等。每一個副本將復制日志復制到其隊列中,然后執行隊列中的操作。比如,在插入時,在復制日志中創建?獲取分塊?這一操作,然后每一個副本都會去下載該分塊。所有副本之間會協調進行合并以獲得相同字節的結果。所有的分塊在所有的副本上以相同的方式合并。為實現該目的,其中一個副本被選為領導者,該副本首先進行合并,并把?合并分塊?操作寫到日志中。 &emsp;&emsp;復制是物理的:只有壓縮的分塊會在節點之間傳輸,查詢則不會。為了降低網絡成本(避免網絡放大),大多數情況下,會在每一個副本上獨立地處理合并。只有在存在顯著的合并延遲的情況下,才會通過網絡發送大塊的合并分塊。 &emsp;&emsp;另外,每一個副本將其狀態作為分塊和校驗和組成的集合存儲在 ZooKeeper 中。當本地文件系統中的狀態與 ZooKeeper 中引用的狀態不同時,該副本會通過從其它副本下載缺失和損壞的分塊來恢復其一致性。當本地文件系統中出現一些意外或損壞的數據時,ClickHouse 不會將其刪除,而是將其移動到一個單獨的目錄下并忘記它。 > ClickHouse 集群由獨立的分片組成,每一個分片由多個副本組成。集群不是彈性的,因此在添加新的分片后,數據不會自動在分片之間重新平衡。相反,集群負載將變得不均衡。該實現為你提供了更多控制,對于相對較小的集群,例如只有數十個節點的集群來說是很好的。但是對于我們在生產中使用的具有數百個節點的集群來說,這種方法成為一個重大缺陷。我們應該實現一個表引擎,使得該引擎能夠跨集群擴展數據,同時具有動態復制的區域,這些區域能夠在集群之間自動拆分和平衡。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看