ClickHouse 是一個真正的列式數(shù)據(jù)庫管理系統(tǒng)(DBMS)。在 ClickHouse 中,數(shù)據(jù)始終是按列存儲的,包括矢量(向量或列塊)執(zhí)行的過程土陪。只要有可能,操作都是基于矢量進行分派的肴熏,而不是單個的值鬼雀,這被稱為?矢量化查詢執(zhí)行?,它有利于降低實際的數(shù)據(jù)處理開銷蛙吏。
這個想法并不新鮮源哩,其可以追溯到?APL?編程語言及其后代:A +鞋吉、J、K?和?Q励烦。矢量編程被大量用于科學(xué)數(shù)據(jù)處理中谓着。即使在關(guān)系型數(shù)據(jù)庫中,這個想法也不是什么新的東西:比如崩侠,矢量編程也被大量用于?Vectorwise?系統(tǒng)中漆魔。
通常有兩種不同的加速查詢處理的方法:矢量化查詢執(zhí)行和運行時代碼生成。在后者中却音,動態(tài)地為每一類查詢生成代碼改抡,消除了間接分派和動態(tài)分派。這兩種方法中系瓢,并沒有哪一種嚴格地比另一種好阿纤。運行時代碼生成可以更好地將多個操作融合在一起,從而充分利用 CPU 執(zhí)行單元和流水線夷陋。矢量化查詢執(zhí)行不是特別實用欠拾,因為它涉及必須寫到緩存并讀回的臨時向量。如果 L2 緩存容納不下臨時數(shù)據(jù)骗绕,那么這將成為一個問題藐窄。但矢量化查詢執(zhí)行更容易利用 CPU 的 SIMD 功能。朋友寫的一篇研究論文表明酬土,將兩種方法結(jié)合起來是更好的選擇荆忍。ClickHouse 使用了矢量化查詢執(zhí)行,同時初步提供了有限的運行時動態(tài)代碼生成撤缴。更多好文章刹枉,關(guān)注“數(shù)據(jù)中臺研習(xí)社”公眾號。
列(Columns)
要表示內(nèi)存中的列(實際上是列塊)屈呕,需使用?IColumn?接口微宝。該接口提供了用于實現(xiàn)各種關(guān)系操作符的輔助方法。幾乎所有的操作都是不可變的:這些操作不會更改原始列虎眨,但是會創(chuàng)建一個新的修改后的列蟋软。比如,IColumn::filter?方法接受過濾字節(jié)掩碼嗽桩,用于?WHERE?和?HAVING?關(guān)系操作符中钟鸵。另外的例子:IColumn::permute?方法支持?ORDER BY?實現(xiàn),IColumn::cut?方法支持?LIMIT?實現(xiàn)等等涤躲。
不同的?IColumn?實現(xiàn)(ColumnUInt8、ColumnString?等)負責(zé)不同的列內(nèi)存布局贡未。內(nèi)存布局通常是一個連續(xù)的數(shù)組种樱。對于數(shù)據(jù)類型為整型的列蒙袍,只是一個連續(xù)的數(shù)組,比如?std::vector嫩挤。對于?String?列和?Array?列害幅,則由兩個向量組成:其中一個向量連續(xù)存儲所有的?String?或數(shù)組元素,另一個存儲每一個?String?或?Array?的起始元素在第一個向量中的偏移岂昭。而?ColumnConst?則僅在內(nèi)存中存儲一個值以现,但是看起來像一個列。
字段
盡管如此约啊,有時候也可能需要處理單個值邑遏。表示單個值,可以使用?Field恰矩。Field?是?UInt64记盒、Int64、Float64外傅、String?和?Array?組成的聯(lián)合纪吮。IColumn?擁有?operator[]?方法來獲取第?n?個值成為一個?Field,同時也擁有?insert?方法將一個?Field?追加到一個列的末尾萎胰。這些方法并不高效碾盟,因為它們需要處理表示單一值的臨時?Field?對象,但是有更高效的方法比如?insertFrom?和?insertRangeFrom?等技竟。
Field?中并沒有足夠的關(guān)于一個表(table)的特定數(shù)據(jù)類型的信息冰肴。比如,UInt8灵奖、UInt16嚼沿、UInt32?和?UInt64?在?Field?中均表示為?UInt64。
抽象漏洞
IColumn?具有用于數(shù)據(jù)的常見關(guān)系轉(zhuǎn)換的方法瓷患,但這些方法并不能夠滿足所有需求骡尽。比如,ColumnUInt64?沒有用于計算兩列和的方法擅编,ColumnString?沒有用于進行子串搜索的方法攀细。這些無法計算的例程在?Icolumn?之外實現(xiàn)。
列(Columns)上的各種函數(shù)可以通過使用?Icolumn?的方法來提取?Field?值爱态,或根據(jù)特定的?Icolumn?實現(xiàn)的數(shù)據(jù)內(nèi)存布局的知識谭贪,以一種通用但不高效的方式實現(xiàn)。為此锦担,函數(shù)將會轉(zhuǎn)換為特定的?IColumn?類型并直接處理內(nèi)部表示俭识。比如,ColumnUInt64?具有?getData?方法洞渔,該方法返回一個指向列的內(nèi)部數(shù)組的引用套媚,然后一個單獨的例程可以直接讀寫或填充該數(shù)組缚态。實際上包个,?抽象漏洞(leaky abstractions)?允許我們以更高效的方式來實現(xiàn)各種特定的例程粹庞。更多好文章,關(guān)注“數(shù)據(jù)中臺研習(xí)社”公眾號蚓土。
數(shù)據(jù)類型
IDataType?負責(zé)序列化和反序列化:讀寫二進制或文本形式的列或單個值構(gòu)成的塊本辐。IDataType?直接與表的數(shù)據(jù)類型相對應(yīng)桥帆。比如,有?DataTypeUInt32慎皱、DataTypeDateTime老虫、DataTypeString?等數(shù)據(jù)類型。
IDataType?與?IColumn?之間的關(guān)聯(lián)并不大宝冕。不同的數(shù)據(jù)類型在內(nèi)存中能夠用相同的?IColumn?實現(xiàn)來表示张遭。比如,DataTypeUInt32?和?DataTypeDateTime?都是用?ColumnUInt32?或?ColumnConstUInt32?來表示的地梨。另外菊卷,相同的數(shù)據(jù)類型也可以用不同的?IColumn?實現(xiàn)來表示。比如宝剖,DataTypeUInt8?既可以使用?ColumnUInt8?來表示洁闰,也可以使用過?ColumnConstUInt8?來表示。
IDataType?僅存儲元數(shù)據(jù)万细。比如扑眉,DataTypeUInt8?不存儲任何東西(除了 vptr);DataTypeFixedString?僅存儲?N(固定長度字符串的串長度)赖钞。
IDataType?具有針對各種數(shù)據(jù)格式的輔助函數(shù)腰素。比如如下一些輔助函數(shù):序列化一個值并加上可能的引號;序列化一個值用于 JSON 格式雪营;序列化一個值作為 XML 格式的一部分弓千。輔助函數(shù)與數(shù)據(jù)格式并沒有直接的對應(yīng)。比如献起,兩種不同的數(shù)據(jù)格式?Pretty?和?TabSeparated?均可以使用?IDataType?接口提供的?serializeTextEscaped?這一輔助函數(shù)洋访。
塊(Block)
Block?是表示內(nèi)存中表的子集(chunk)的容器,是由三元組:(IColumn, IDataType, 列名)?構(gòu)成的集合谴餐。在查詢執(zhí)行期間姻政,數(shù)據(jù)是按?Block?進行處理的。如果我們有一個?Block岂嗓,那么就有了數(shù)據(jù)(在?IColumn?對象中)汁展,有了數(shù)據(jù)的類型信息告訴我們?nèi)绾翁幚碓摿校瑫r也有了列名(來自表的原始列名,或人為指定的用于臨時計算結(jié)果的名字)善镰。
當(dāng)我們遍歷一個塊中的列進行某些函數(shù)計算時妹萨,會把結(jié)果列加入到塊中,但不會更改函數(shù)參數(shù)中的列炫欺,因為操作是不可變的。之后熏兄,不需要的列可以從塊中刪除品洛,但不是修改。這對于消除公共子表達式非常方便摩桶。
Block?用于處理數(shù)據(jù)塊桥状。注意,對于相同類型的計算硝清,列名和類型對不同的塊保持相同辅斟,僅列數(shù)據(jù)不同。最好把塊數(shù)據(jù)(block data)和塊頭(block header)分離開來芦拿,因為小塊大小會因復(fù)制共享指針和列名而帶來很高的臨時字符串開銷士飒。更多好文章,關(guān)注“數(shù)據(jù)中臺研習(xí)社”公眾號蔗崎。
塊流(Block Streams)
塊流用于處理數(shù)據(jù)酵幕。我們可以使用塊流從某個地方讀取數(shù)據(jù),執(zhí)行數(shù)據(jù)轉(zhuǎn)換缓苛,或?qū)?shù)據(jù)寫到某個地方芳撒。IBlockInputStream?具有?read?方法,其能夠在數(shù)據(jù)可用時獲取下一個塊未桥。IBlockOutputStream?具有?write?方法笔刹,其能夠?qū)K寫到某處。
塊流負責(zé):
讀或?qū)懸粋€表冬耿。表僅返回一個流用于讀寫塊舌菜。
完成數(shù)據(jù)格式化。比如淆党,如果你打算將數(shù)據(jù)以?Pretty?格式輸出到終端酷师,你可以創(chuàng)建一個塊輸出流,將塊寫入該流中染乌,然后進行格式化山孔。
執(zhí)行數(shù)據(jù)轉(zhuǎn)換。假設(shè)你現(xiàn)在有?IBlockInputStream?并且打算創(chuàng)建一個過濾流荷憋,那么你可以創(chuàng)建一個?FilterBlockInputStream?并用?IBlockInputStream?進行初始化台颠。之后,當(dāng)你從?FilterBlockInputStream?中拉取塊時,會從你的流中提取一個塊串前,對其進行過濾瘫里,然后將過濾后的塊返回給你。查詢執(zhí)行流水線就是以這種方式表示的荡碾。
還有一些更復(fù)雜的轉(zhuǎn)換谨读。比如,當(dāng)你從?AggregatingBlockInputStream?拉取數(shù)據(jù)時坛吁,會從數(shù)據(jù)源讀取全部數(shù)據(jù)進行聚集劳殖,然后將聚集后的數(shù)據(jù)流返回給你。另一個例子:UnionBlockInputStream?的構(gòu)造函數(shù)接受多個輸入源和多個線程拨脉,其能夠啟動多線程從多個輸入源并行讀取數(shù)據(jù)哆姻。
塊流使用?pull?方法來控制流:當(dāng)你從第一個流中拉取塊時,它會接著從嵌套的流中拉取所需的塊玫膀,然后整個執(zhí)行流水線開始工作矛缨。?pull?和?push?都不是最好的方案,因為控制流不是明確的帖旨,這限制了各種功能的實現(xiàn)箕昭,比如多個查詢同步執(zhí)行(多個流水線合并到一起)。這個限制可以通過協(xié)程或直接運行互相等待的線程來解決碉就。如果控制流明確盟广,那么我們會有更多的可能性:如果我們定位了數(shù)據(jù)從一個計算單元傳遞到那些外部的計算單元中其中一個計算單元的邏輯。閱讀這篇文章來獲取更多的想法瓮钥。
我們需要注意筋量,查詢執(zhí)行流水線在每一步都會創(chuàng)建臨時數(shù)據(jù)。我們要盡量使塊的大小足夠小碉熄,從而 CPU 緩存能夠容納下臨時數(shù)據(jù)桨武。在這個假設(shè)下,與其他計算相比锈津,讀寫臨時數(shù)據(jù)幾乎是沒有任何開銷的呀酸。我們也可以考慮一種替代方案:將流水線中的多個操作融合在一起,使流水線盡可能短琼梆,并刪除大量臨時數(shù)據(jù)性誉。這可能是一個優(yōu)點,但同時也有缺點茎杂。比如错览,拆分流水線使得中間數(shù)據(jù)緩存、獲取同時運行的類似查詢的中間數(shù)據(jù)以及相似查詢的流水線合并等功能很容易實現(xiàn)煌往。
格式(Formats)
數(shù)據(jù)格式同塊流一起實現(xiàn)倾哺。既有僅用于向客戶端輸出數(shù)據(jù)的?展示?格式,如?IBlockOutputStream?提供的?Pretty?格式,也有其它輸入輸出格式羞海,比如?TabSeparated?或?JSONEachRow忌愚。
此外還有行流:IRowInputStream?和?IRowOutputStream。它們允許你按行 pull/push 數(shù)據(jù)却邓,而不是按塊硕糊。行流只需要簡單地面向行格式實現(xiàn)。包裝器?BlockInputStreamFromRowInputStream?和?BlockOutputStreamFromRowOutputStream?允許你將面向行的流轉(zhuǎn)換為正常的面向塊的流申尤。更多好文章癌幕,關(guān)注“數(shù)據(jù)中臺研習(xí)社”公眾號。
I/O
對于面向字節(jié)的輸入輸出昧穿,有?ReadBuffer?和?WriteBuffer?這兩個抽象類。它們用來替代 C++ 的?iostream橙喘。不用擔(dān)心:每個成熟的 C++ 項目都會有充分的理由使用某些東西來代替?iostream时鸵。
ReadBuffer?和?WriteBuffer?由一個連續(xù)的緩沖區(qū)和指向緩沖區(qū)中某個位置的一個指針組成。實現(xiàn)中厅瞎,緩沖區(qū)可能擁有內(nèi)存饰潜,也可能不擁有內(nèi)存。有一個虛方法會使用隨后的數(shù)據(jù)來填充緩沖區(qū)(針對?ReadBuffer)或刷新緩沖區(qū)(針對?WriteBuffer)和簸,該虛方法很少被調(diào)用彭雾。
ReadBuffer?和?WriteBuffer?的實現(xiàn)用于處理文件、文件描述符和網(wǎng)絡(luò)套接字(socket)锁保,也用于實現(xiàn)壓縮(CompressedWriteBuffer?在寫入數(shù)據(jù)前需要先用一個?WriteBuffer?進行初始化并進行壓縮)和其它用途薯酝。ConcatReadBuffer、LimitReadBuffer?和?HashingWriteBuffer?的用途正如其名字所描述的一樣爽柒。
ReadBuffer?和?WriteBuffer?僅處理字節(jié)吴菠。為了實現(xiàn)格式化輸入和輸出(比如以十進制格式寫一個數(shù)字),ReadHelpers?和?WriteHelpers?頭文件中有一些輔助函數(shù)可用浩村。
讓我們來看一下做葵,當(dāng)你把一個結(jié)果集以?JSON?格式寫到標(biāo)準輸出(stdout)時會發(fā)生什么。你已經(jīng)準備好從?IBlockInputStream?獲取結(jié)果集心墅,然后創(chuàng)建?WriteBufferFromFileDescriptor(STDOUT_FILENO)?用于寫字節(jié)到標(biāo)準輸出酿矢,創(chuàng)建?JSONRowOutputStream?并用?WriteBuffer?初始化,用于將行以?JSON?格式寫到標(biāo)準輸出怎燥,你還可以在其上創(chuàng)建?BlockOutputStreamFromRowOutputStream瘫筐,將其表示為?IBlockOutputStream。然后調(diào)用?copyData?將數(shù)據(jù)從?IBlockInputStream?傳輸?shù)?IBlockOutputStream刺覆,一切工作正常严肪。在內(nèi)部,JSONRowOutputStream?會寫入 JSON 分隔符,并以指向?IColumn?的引用和行數(shù)作為參數(shù)調(diào)用?IDataType::serializeTextJSON?函數(shù)驳糯。隨后篇梭,IDataType::serializeTextJSON?將會調(diào)用?WriteHelpers.h?中的一個方法:比如,writeText?用于數(shù)值類型酝枢,writeJSONString?用于?DataTypeString?恬偷。
表(Tables)
表由?IStorage?接口表示。該接口的不同實現(xiàn)對應(yīng)不同的表引擎帘睦。比如?StorageMergeTree袍患、StorageMemory?等。這些類的實例就是表竣付。
IStorage?中最重要的方法是?read?和?write诡延,除此之外還有?alter、rename?和?drop?等方法古胆。read?方法接受如下參數(shù):需要從表中讀取的列集肆良,需要執(zhí)行的?AST?查詢,以及所需返回的流的數(shù)量逸绎。read?方法的返回值是一個或多個?IBlockInputStream?對象惹恃,以及在查詢執(zhí)行期間在一個表引擎內(nèi)完成的關(guān)于數(shù)據(jù)處理階段的信息。
在大多數(shù)情況下棺牧,read?方法僅負責(zé)從表中讀取指定的列巫糙,而不會進行進一步的數(shù)據(jù)處理。進一步的數(shù)據(jù)處理均由查詢解釋器完成颊乘,不由?IStorage?負責(zé)参淹。
但是也有值得注意的例外:
AST 查詢被傳遞給?read?方法,表引擎可以使用它來判斷是否能夠使用索引疲牵,從而從表中讀取更少的數(shù)據(jù)承二。
有時候,表引擎能夠?qū)?shù)據(jù)處理到一個特定階段纲爸。比如亥鸠,StorageDistributed?可以向遠程服務(wù)器發(fā)送查詢,要求它們將來自不同的遠程服務(wù)器能夠合并的數(shù)據(jù)處理到某個階段识啦,并返回預(yù)處理后的數(shù)據(jù)负蚊,然后查詢解釋器完成后續(xù)的數(shù)據(jù)處理。更多好文章颓哮,關(guān)注“數(shù)據(jù)中臺研習(xí)社”公眾號家妆。
表的?read?方法能夠返回多個?IBlockInputStream?對象以允許并行處理數(shù)據(jù)。多個塊輸入流能夠從一個表中并行讀取冕茅。然后你可以通過不同的轉(zhuǎn)換對這些流進行裝飾(比如表達式求值或過濾)伤极,轉(zhuǎn)換過程能夠獨立計算蛹找,并在其上創(chuàng)建一個?UnionBlockInputStream,以并行讀取多個流哨坪。
另外也有?TableFunction庸疾。TableFunction?能夠在查詢的?FROM?字句中返回一個臨時的?IStorage?以供使用。
要快速了解如何實現(xiàn)自己的表引擎当编,可以查看一些簡單的表引擎届慈,比如?StorageMemory?或?StorageTinyLog。
作為?read?方法的結(jié)果忿偷,IStorage?返回?QueryProcessingStage?- 關(guān)于 storage 里哪部分查詢已經(jīng)被計算的信息金顿。當(dāng)前我們僅有非常粗粒度的信息。Storage 無法告訴我們?對于這個范圍的數(shù)據(jù)鲤桥,我已經(jīng)處理完了 WHERE 字句里的這部分表達式?揍拆。我們需要在這個地方繼續(xù)努力。
解析器(Parsers)
查詢由一個手寫遞歸下降解析器解析茶凳。比如礁凡,?ParserSelectQuery?只是針對查詢的不同部分遞歸地調(diào)用下層解析器。解析器創(chuàng)建?AST慧妄。AST?由節(jié)點表示,節(jié)點是?IAST?的實例剪芍。
由于歷史原因塞淹,未使用解析器生成器。
解釋器(Interpreters)
解釋器負責(zé)從?AST?創(chuàng)建查詢執(zhí)行流水線罪裹。既有一些簡單的解釋器饱普,如?InterpreterExistsQuery?和?InterpreterDropQuery,也有更復(fù)雜的解釋器状共,如?InterpreterSelectQuery套耕。查詢執(zhí)行流水線由塊輸入或輸出流組成。比如峡继,SELECT?查詢的解釋結(jié)果是從?FROM?字句的結(jié)果集中讀取數(shù)據(jù)的?IBlockInputStream冯袍;INSERT?查詢的結(jié)果是寫入需要插入的數(shù)據(jù)的?IBlockOutputStream;SELECT INSERT?查詢的解釋結(jié)果是?IBlockInputStream碾牌,它在第一次讀取時返回一個空結(jié)果集康愤,同時將數(shù)據(jù)從?SELECT?復(fù)制到?INSERT。
InterpreterSelectQuery?使用?ExpressionAnalyzer?和?ExpressionActions?機制來進行查詢分析和轉(zhuǎn)換舶吗。這是大多數(shù)基于規(guī)則的查詢優(yōu)化完成的地方征冷。ExpressionAnalyzer?非常混亂誓琼,應(yīng)該進行重寫:不同的查詢轉(zhuǎn)換和優(yōu)化應(yīng)該被提取出來并劃分成不同的類检激,從而允許模塊化轉(zhuǎn)換或查詢肴捉。
函數(shù)(Functions)
函數(shù)既有普通函數(shù),也有聚合函數(shù)叔收。對于聚合函數(shù)齿穗,請看下一節(jié)。
普通函數(shù)不會改變行數(shù) - 它們的執(zhí)行看起來就像是獨立地處理每一行數(shù)據(jù)今穿。實際上缤灵,函數(shù)不會作用于一個單獨的行上,而是作用在以?Block?為單位的數(shù)據(jù)上蓝晒,以實現(xiàn)向量查詢執(zhí)行腮出。
還有一些雜項函數(shù),比如?塊大小芝薇、rowNumberInBlock胚嘲,以及?跑累積,它們對塊進行處理洛二,并且不遵從行的獨立性馋劈。
ClickHouse 具有強類型,因此隱式類型轉(zhuǎn)換不會發(fā)生晾嘶。如果函數(shù)不支持某個特定的類型組合妓雾,則會拋出異常。但函數(shù)可以通過重載以支持許多不同的類型組合垒迂。比如械姻,plus?函數(shù)(用于實現(xiàn)?+?運算符)支持任意數(shù)字類型的組合:UInt8?+?Float32,UInt16?+?Int8?等机断。同時楷拳,一些可變參數(shù)的函數(shù)能夠級接收任意數(shù)目的參數(shù),比如?concat?函數(shù)吏奸。
實現(xiàn)函數(shù)可能有些不方便欢揖,因為函數(shù)的實現(xiàn)需要包含所有支持該操作的數(shù)據(jù)類型和?IColumn?類型。比如奋蔚,plus?函數(shù)能夠利用 C++ 模板針對不同的數(shù)字類型組合她混、常量以及非常量的左值和右值進行代碼生成。
這是一個實現(xiàn)動態(tài)代碼生成的好地方旺拉,從而能夠避免模板代碼膨脹产上。同樣,運行時代碼生成也使得實現(xiàn)融合函數(shù)成為可能蛾狗,比如融合?乘-加?晋涣,或者在單層循環(huán)迭代中進行多重比較。
由于向量查詢執(zhí)行沉桌,函數(shù)不會?短路?谢鹊。比如算吩,如果你寫?WHERE f(x) AND g(y),兩邊都會進行計算佃扼,即使是對于?f(x)?為 0 的行(除非?f(x)?是零常量表達式)偎巢。但是如果?f(x)?的選擇條件很高,并且計算?f(x)?比計算?g(y)?要劃算得多兼耀,那么最好進行多遍計算:首先計算?f(x)压昼,根據(jù)計算結(jié)果對列數(shù)據(jù)進行過濾,然后計算?g(y)瘤运,之后只需對較小數(shù)量的數(shù)據(jù)進行過濾窍霞。
聚合函數(shù)
聚合函數(shù)是狀態(tài)函數(shù)。它們將傳入的值激活到某個狀態(tài)拯坟,并允許你從該狀態(tài)獲取結(jié)果但金。聚合函數(shù)使用?IAggregateFunction?接口進行管理。狀態(tài)可以非常簡單(AggregateFunctionCount?的狀態(tài)只是一個單一的UInt64?值)郁季,也可以非常復(fù)雜(AggregateFunctionUniqCombined?的狀態(tài)是由一個線性數(shù)組冷溃、一個散列表和一個?HyperLogLog?概率數(shù)據(jù)結(jié)構(gòu)組合而成的)。
為了能夠在執(zhí)行一個基數(shù)很大的?GROUP BY?查詢時處理多個聚合狀態(tài)梦裂,需要在?Arena(一個內(nèi)存池)或任何合適的內(nèi)存塊中分配狀態(tài)似枕。狀態(tài)可以有一個非平凡的構(gòu)造器和析構(gòu)器:比如,復(fù)雜的聚合狀態(tài)能夠自己分配額外的內(nèi)存年柠。這需要注意狀態(tài)的創(chuàng)建和銷毀并恰當(dāng)?shù)貍鬟f狀態(tài)的所有權(quán)菠净,以跟蹤誰將何時銷毀狀態(tài)。
聚合狀態(tài)可以被序列化和反序列化彪杉,以在分布式查詢執(zhí)行期間通過網(wǎng)絡(luò)傳遞或者在內(nèi)存不夠的時候?qū)⑵鋵懙接脖P。聚合狀態(tài)甚至可以通過?DataTypeAggregateFunction?存儲到一個表中牵咙,以允許數(shù)據(jù)的增量聚合派近。
聚合函數(shù)狀態(tài)的序列化數(shù)據(jù)格式目前尚未版本化。如果只是臨時存儲聚合狀態(tài)洁桌,這樣是可以的渴丸。但是我們有?AggregatingMergeTree?表引擎用于增量聚合,并且人們已經(jīng)在生產(chǎn)中使用它另凌。這就是為什么在未來當(dāng)我們更改任何聚合函數(shù)的序列化格式時需要增加向后兼容的支持谱轨。
服務(wù)器(Server)
服務(wù)器實現(xiàn)了多個不同的接口:
一個用于任何外部客戶端的 HTTP 接口。
一個用于本機 ClickHouse 客戶端以及在分布式查詢執(zhí)行中跨服務(wù)器通信的 TCP 接口吠谢。
一個用于傳輸數(shù)據(jù)以進行拷貝的接口土童。
在內(nèi)部,它只是一個沒有協(xié)程工坊、纖程等的基礎(chǔ)多線程服務(wù)器献汗。服務(wù)器不是為處理高速率的簡單查詢設(shè)計的敢订,而是為處理相對低速率的復(fù)雜查詢設(shè)計的,每一個復(fù)雜查詢能夠?qū)Υ罅康臄?shù)據(jù)進行處理分析罢吃。
服務(wù)器使用必要的查詢執(zhí)行需要的環(huán)境初始化?Context?類:可用數(shù)據(jù)庫列表楚午、用戶和訪問權(quán)限、設(shè)置尿招、集群矾柜、進程列表和查詢?nèi)罩镜取_@些環(huán)境被解釋器使用就谜。
我們維護了服務(wù)器 TCP 協(xié)議的完全向后向前兼容性:舊客戶端可以和新服務(wù)器通信怪蔑,新客戶端也可以和舊服務(wù)器通信。但是我們并不想永久維護它吁伺,我們將在大約一年后刪除對舊版本的支持饮睬。
對于所有的外部應(yīng)用,我們推薦使用 HTTP 接口篮奄,因為該接口很簡單捆愁,容易使用。TCP 接口與內(nèi)部數(shù)據(jù)結(jié)構(gòu)的聯(lián)系更加緊密:它使用內(nèi)部格式傳遞數(shù)據(jù)塊窟却,并使用自定義幀來壓縮數(shù)據(jù)昼丑。我們沒有發(fā)布該協(xié)議的 C 庫,因為它需要鏈接大部分的 ClickHouse 代碼庫夸赫,這是不切實際的菩帝。
分布式查詢執(zhí)行
集群設(shè)置中的服務(wù)器大多是獨立的。你可以在一個集群中的一個或多個服務(wù)器上創(chuàng)建一個?Distributed?表茬腿。Distributed?表本身并不存儲數(shù)據(jù)呼奢,它只為集群的多個節(jié)點上的所有本地表提供一個?視圖(view)?。當(dāng)從?Distributed?表中進行 SELECT 時切平,它會重寫該查詢握础,根據(jù)負載平衡設(shè)置來選擇遠程節(jié)點,并將查詢發(fā)送給節(jié)點悴品。Distributed?表請求遠程服務(wù)器處理查詢禀综,直到可以合并來自不同服務(wù)器的中間結(jié)果的階段。然后它接收中間結(jié)果并進行合并苔严。分布式表會嘗試將盡可能多的工作分配給遠程服務(wù)器定枷,并且不會通過網(wǎng)絡(luò)發(fā)送太多的中間數(shù)據(jù)。
當(dāng)?IN?或?JOIN?子句中包含子查詢并且每個子查詢都使用分布式表時届氢,事情會變得更加復(fù)雜欠窒。我們有不同的策略來執(zhí)行這些查詢。
分布式查詢執(zhí)行沒有全局查詢計劃退子。每個節(jié)點都有針對自己的工作部分的本地查詢計劃贱迟。我們僅有簡單的一次性分布式查詢執(zhí)行:將查詢發(fā)送給遠程節(jié)點姐扮,然后合并結(jié)果。但是對于具有高基數(shù)的?GROUP BY?或具有大量臨時數(shù)據(jù)的?JOIN?這樣困難的查詢的來說衣吠,這是不可行的:在這種情況下茶敏,我們需要在服務(wù)器之間?改組?數(shù)據(jù),這需要額外的協(xié)調(diào)缚俏。ClickHouse 不支持這類查詢執(zhí)行惊搏,我們需要在這方面進行努力。
合并樹
MergeTree?是一系列支持按主鍵索引的存儲引擎忧换。主鍵可以是一個任意的列或表達式的元組恬惯。MergeTree?表中的數(shù)據(jù)存儲于?分塊?中。每一個分塊以主鍵序存儲數(shù)據(jù)(數(shù)據(jù)按主鍵元組的字典序排序)亚茬。表的所有列都存儲在這些?分塊?中分離的?column.bin?文件中酪耳。column.bin?文件由壓縮塊組成,每一個塊通常是 64 KB 到 1 MB 大小的未壓縮數(shù)據(jù)刹缝,具體取決于平均值大小碗暗。這些塊由一個接一個連續(xù)放置的列值組成。每一列的列值順序相同(順序由主鍵定義)梢夯,因此當(dāng)你按多列進行迭代時言疗,你能夠得到相應(yīng)列的值。
主鍵本身是?稀疏?的颂砸。它并不是索引單一的行噪奄,而是索引某個范圍內(nèi)的數(shù)據(jù)。一個單獨的?primary.idx?文件具有每個第 N 行的主鍵值人乓,其中 N 稱為?index_granularity(通常勤篮,N = 8192)。同時色罚,對于每一列叙谨,都有帶有標(biāo)記的?column.mrk?文件,該文件記錄的是每個第 N 行在數(shù)據(jù)文件中的偏移量保屯。每個標(biāo)記是一個 pair:文件中的偏移量到壓縮塊的起始,以及解壓縮塊中的偏移量到數(shù)據(jù)的起始涤垫。通常姑尺,壓縮塊根據(jù)標(biāo)記對齊,并且解壓縮塊中的偏移量為 0蝠猬。primary.idx?的數(shù)據(jù)始終駐留在內(nèi)存切蟋,同時?column.mrk?的數(shù)據(jù)被緩存。
當(dāng)我們要從?MergeTree?的一個分塊中讀取部分內(nèi)容時榆芦,我們會查看?primary.idx?數(shù)據(jù)并查找可能包含所請求數(shù)據(jù)的范圍柄粹,然后查看?column.mrk?并計算偏移量從而得知從哪里開始讀取些范圍的數(shù)據(jù)喘鸟。由于稀疏性,可能會讀取額外的數(shù)據(jù)驻右。ClickHouse 不適用于高負載的簡單點查詢什黑,因為對于每一個鍵,整個?index_granularity?范圍的行的數(shù)據(jù)都需要讀取堪夭,并且對于每一列需要解壓縮整個壓縮塊愕把。我們使索引稀疏,是因為每一個單一的服務(wù)器需要在索引沒有明顯內(nèi)存消耗的情況下森爽,維護數(shù)萬億行的數(shù)據(jù)恨豁。另外,由于主鍵是稀疏的爬迟,導(dǎo)致其不是唯一的:無法在 INSERT 時檢查一個鍵在表中是否存在橘蜜。你可以在一個表中使用同一個鍵創(chuàng)建多個行。
當(dāng)你向?MergeTree?中插入一堆數(shù)據(jù)時付呕,數(shù)據(jù)按主鍵排序并形成一個新的分塊计福。為了保證分塊的數(shù)量相對較少,有后臺線程定期選擇一些分塊并將它們合并成一個有序的分塊凡涩,這就是?MergeTree?的名稱來源棒搜。當(dāng)然,合并會導(dǎo)致?寫入放大?活箕。所有的分塊都是不可變的:它們僅會被創(chuàng)建和刪除力麸,不會被修改。當(dāng)運行?SELECT?查詢時育韩,MergeTree?會保存一個表的快照(分塊集合)克蚂。合并之后,還會保留舊的分塊一段時間筋讨,以便發(fā)生故障后更容易恢復(fù)埃叭,因此如果我們發(fā)現(xiàn)某些合并后的分塊可能已損壞,我們可以將其替換為原分塊悉罕。
MergeTree?不是 LSM 樹赤屋,因為它不包含?memtable?和?log?:插入的數(shù)據(jù)直接寫入文件系統(tǒng)。這使得它僅適用于批量插入數(shù)據(jù)壁袄,而不適用于非常頻繁地一行一行插入 - 大約每秒一次是沒問題的类早,但是每秒一千次就會有問題。我們這樣做是為了簡單起見嗜逻,因為我們已經(jīng)在我們的應(yīng)用中批量插入數(shù)據(jù)涩僻。
MergeTree?表只能有一個(主)索引:沒有任何輔助索引。在一個邏輯表下,允許有多個物理表示逆日,比如嵌巷,可以以多個物理順序存儲數(shù)據(jù),或者同時表示預(yù)聚合數(shù)據(jù)和原始數(shù)據(jù)室抽。
有些?MergeTree?引擎會在后臺合并期間做一些額外工作搪哪,比如?CollapsingMergeTree?和?AggregatingMergeTree。這可以視為對更新的特殊支持狠半。請記住這些不是真正的更新噩死,因為用戶通常無法控制后臺合并將會執(zhí)行的時間,并且?MergeTree?中的數(shù)據(jù)幾乎總是存儲在多個分塊中神年,而不是完全合并的形式已维。
復(fù)制(Replication)
ClickHouse 中的復(fù)制是基于表實現(xiàn)的。你可以在同一個服務(wù)器上有一些可復(fù)制的表和不可復(fù)制的表已日。你也可以以不同的方式進行表的復(fù)制垛耳,比如一個表進行雙因子復(fù)制,另一個進行三因子復(fù)制飘千。
復(fù)制是在?ReplicatedMergeTree?存儲引擎中實現(xiàn)的堂鲜。ZooKeeper?中的路徑被指定為存儲引擎的參數(shù)。ZooKeeper?中所有具有相同路徑的表互為副本:它們同步數(shù)據(jù)并保持一致性护奈。只需創(chuàng)建或刪除表缔莲,就可以實現(xiàn)動態(tài)添加或刪除副本。
復(fù)制使用異步多主機方案霉旗。你可以將數(shù)據(jù)插入到與?ZooKeeper?進行會話的任意副本中痴奏,并將數(shù)據(jù)復(fù)制到所有其它副本中。由于 ClickHouse 不支持 UPDATEs厌秒,因此復(fù)制是無沖突的读拆。由于沒有對插入的仲裁確認,如果一個節(jié)點發(fā)生故障鸵闪,剛剛插入的數(shù)據(jù)可能會丟失檐晕。
用于復(fù)制的元數(shù)據(jù)存儲在 ZooKeeper 中。其中一個復(fù)制日志列出了要執(zhí)行的操作蚌讼。操作包括:獲取分塊辟灰、合并分塊和刪除分區(qū)等。每一個副本將復(fù)制日志復(fù)制到其隊列中篡石,然后執(zhí)行隊列中的操作芥喇。比如,在插入時夏志,在復(fù)制日志中創(chuàng)建?獲取分塊?這一操作,然后每一個副本都會去下載該分塊。所有副本之間會協(xié)調(diào)進行合并以獲得相同字節(jié)的結(jié)果沟蔑。所有的分塊在所有的副本上以相同的方式合并湿诊。為實現(xiàn)該目的,其中一個副本被選為領(lǐng)導(dǎo)者瘦材,該副本首先進行合并厅须,并把?合并分塊?操作寫到日志中。
復(fù)制是物理的:只有壓縮的分塊會在節(jié)點之間傳輸食棕,查詢則不會朗和。為了降低網(wǎng)絡(luò)成本(避免網(wǎng)絡(luò)放大),大多數(shù)情況下簿晓,會在每一個副本上獨立地處理合并眶拉。只有在存在顯著的合并延遲的情況下,才會通過網(wǎng)絡(luò)發(fā)送大塊的合并分塊憔儿。
另外忆植,每一個副本將其狀態(tài)作為分塊和校驗和組成的集合存儲在 ZooKeeper 中。當(dāng)本地文件系統(tǒng)中的狀態(tài)與 ZooKeeper 中引用的狀態(tài)不同時谒臼,該副本會通過從其它副本下載缺失和損壞的分塊來恢復(fù)其一致性朝刊。當(dāng)本地文件系統(tǒng)中出現(xiàn)一些意外或損壞的數(shù)據(jù)時,ClickHouse 不會將其刪除蜈缤,而是將其移動到一個單獨的目錄下并忘記它拾氓。
ClickHouse 集群由獨立的分片組成,每一個分片由多個副本組成底哥。集群不是彈性的咙鞍,因此在添加新的分片后,數(shù)據(jù)不會自動在分片之間重新平衡叠艳。相反奶陈,集群負載將變得不均衡。該實現(xiàn)為你提供了更多控制附较,對于相對較小的集群吃粒,例如只有數(shù)十個節(jié)點的集群來說是很好的。但是對于我們在生產(chǎn)中使用的具有數(shù)百個節(jié)點的集群來說拒课,這種方法成為一個重大缺陷徐勃。我們應(yīng)該實現(xiàn)一個表引擎,使得該引擎能夠跨集群擴展數(shù)據(jù)早像,同時具有動態(tài)復(fù)制的區(qū)域僻肖,這些區(qū)域能夠在集群之間自動拆分和平衡。