ClickHouse特性及底層存儲原理

ClickHouse的特性

ClickHouse是一款MPP架構的列式存儲數據庫,但MPP和列式存儲並不是什麼"稀罕"的設計。擁有類似架構的其他數據庫產品也有很多,但是爲什麼偏偏只有ClickHouse的性能如此出衆呢?ClickHouse發展至今的演進過程一共經歷了四個階段,每一次階段演進,相比之前都進一步取其精華去其糟粕。可以說ClickHouse汲取了各家技術的精髓,將每一個細節都做到了極致。接下來將介紹ClickHouse的一些核心特性,正是這些特性形成的合力使得ClickHouse如此優秀。

1. 完備的DBMS功能

ClickHouse擁有完備的管理功能,所以它稱得上是一個DBMS ( Database Management System,數據庫管理系統 ),而不僅是一個數據庫。作爲一個DBMS,它具備了一些基本功能,如下所示。

  • DDL ( 數據定義語言 ):可以動態地創建、修改或刪除數據庫、表和視圖,而無須重啓服務。

  • DML ( 數據操作語言 ):可以動態查詢、插入、修改或刪除數據。

  • 權限控制:可以按照用戶粒度設置數據庫或者表的操作權限,保障數據的安全性。

  • 數據備份與恢復:提供了數據備份導出與導入恢復機制,滿足生產環境的要求。

  • 分佈式管理:提供集羣模式,能夠自動管理多個數據庫節點。

這裏只列舉了一些最具代表性的功能,但已然足以表明爲什麼Click House稱得上是DBMS了。

2. 列式存儲與數據壓縮

列式存儲和數據壓縮,對於一款高性能數據庫來說是必不可少的特性。一個非常流行的觀點認爲,如果你想讓查詢變得更快,最簡單且有效的方法是減少數據掃描範圍和數據傳輸時的大小,而列式存儲和數據壓縮就可以幫助我們實現上述兩點。列式存儲和數據壓縮通常是伴生的,因爲一般來說列式存儲是數據壓縮的前提。

按列存儲與按行存儲相比,前者可以有效減少查詢時所需掃描的數據量,這一點可以用一個示例簡單說明。假設一張數據表A擁有50個字段A1~A50,以及100行數據。

SELECT A1,A2,A3,A4,A5 FROM A

按列存儲相比按行存儲的另一個優勢是對數據壓縮的友好性。同樣可以用一個示例簡單說明壓縮的本質是什麼。假設有兩個字符串abcdefghi和bcdefghi,現在對它們進行壓縮,如下所示:

壓縮前:abcdefghi_bcdefghi
壓縮後:abcdefghi_(9,8)

可以看到,壓縮的本質是按照一定步長對數據進行匹配掃描,當發現重複部分的時候就進行編碼轉換。例如上述示例中的 (9,8),表示如果從下劃線開始向前移動9個字節,會匹配到8個字節長度的重複項,即這裏的bcdefghi。

真實的壓縮算法自然比這個示例更爲複雜,但壓縮的實質就是如此。數據中的重複項越多,則壓縮率越高;壓縮率越高,則數據體量越小;而數據體量越小,則數據在網絡中的傳輸越快,對網絡帶寬和磁盤IO的壓力也就越小。既然如此,那怎樣的數據最可能具備重複的特性呢?答案是屬於同一個列字段的數據,因爲它們擁有相同的數據類型和現實語義,重複項的可能性自然就更高。

ClickHouse就是一款使用列式存儲的數據庫,數據按列進行組織,屬於同一列的數據會被保存在一起,列與列之間也會由不同的文件分別保存 ( 這裏主要指MergeTree表引擎 )。數據默認使用LZ4算法壓縮,在Yandex.Metrica的生產環境中,數據總體的壓縮比可以達到8:1 ( 未壓縮前17PB,壓縮後2PB )。列式存儲除了降低IO和存儲的壓力之外,還爲向量化執行做好了鋪墊。

3. 向量化執行引擎

坊間有句玩笑,即"能用錢解決的問題,千萬別花時間"。而業界也有種調侃如出一轍,即"能升級硬件解決的問題,千萬別優化程序"。有時候,你千辛萬苦優化程序邏輯帶來的性能提升,還不如直接升級硬件來得簡單直接。這雖然只是一句玩笑不能當真,但硬件層面的優化確實是最直接、最高效的提升途徑之一。向量化執行就是這種方式的典型代表,這項寄存器硬件層面的特性,爲上層應用程序的性能帶來了指數級的提升。

向量化執行,可以簡單地看作一項消除程序中循環的優化。這裏用一個形象的例子比喻。小胡經營了一家果汁店,雖然店裏的鮮榨蘋果汁深受大家喜愛,但客戶總是抱怨製作果汁的速度太慢。小胡的店裏只有一臺榨汁機,每次他都會從籃子裏拿出一個蘋果,放到榨汁機內等待出汁。如果有8個客戶,每個客戶都點了一杯蘋果汁,那麼小胡需要重複循環8次上述的榨汁流程,才能榨出8杯蘋果汁。如果製作一杯果汁需要5分鐘,那麼全部製作完畢則需要40分鐘。爲了提升果汁的製作速度,小胡想出了一個辦法。他將榨汁機的數量從1臺增加到了8臺,這麼一來,他就可以從籃子裏一次性拿出8個蘋果,分別放入8臺榨汁機同時榨汁。此時,小胡只需要5分鐘就能夠製作出8杯蘋果汁。爲了製作n杯果汁,非向量化執行的方式是用1臺榨汁機重複循環制作n次,而向量化執行的方式是用n臺榨汁機只執行1次。

爲了實現向量化執行,需要利用CPU的SIMD指令。SIMD的全稱是Single Instruction Multiple Data,即用單條指令操作多條數據。現代計算機系統概念中,它是通過數據並行以提高性能的一種實現方式 ( 其他的還有指令級並行和線程級並行 ),它的原理是在CPU寄存器層面實現數據的並行操作。

在計算機系統的體系結構中,存儲系統是一種層次結構。典型服務器計算機的存儲層次結構如圖1所示。一個實用的經驗告訴我們,存儲媒介距離CPU越近,則訪問數據的速度越快。

從上圖中可以看到,從左向右,距離CPU越遠,則數據的訪問速度越慢。從寄存器中訪問數據的速度,是從內存訪問數據速度的300倍,是從磁盤中訪問數據速度的3000萬倍。所以利用CPU向量化執行的特性,對於程序的性能提升意義非凡。

ClickHouse目前利用SSE4.2指令集實現向量化執行。

4. 關係模型與SQL查詢

相比HBase和Redis這類NoSQL數據庫,ClickHouse使用關係模型描述數據並提供了傳統數據庫的概念 ( 數據庫、表、視圖和函數等 )。與此同時,ClickHouse完全使用SQL作爲查詢語言 ( 支持GROUP BY、ORDER BY、JOIN、IN等大部分標準SQL ),這使得它平易近人,容易理解和學習。因爲關係型數據庫和SQL語言,可以說是軟件領域發展至今應用最爲廣泛的技術之一,擁有極高的"羣衆基礎"。也正因爲ClickHouse提供了標準協議的SQL查詢接口,使得現有的第三方分析可視化系統可以輕鬆與它集成對接。在SQL解析方面,ClickHouse是大小寫敏感的,這意味着SELECT a 和 SELECT A所代表的語義是不同的。

關係模型相比文檔和鍵值對等其他模型,擁有更好的描述能力,也能夠更加清晰地表述實體間的關係。更重要的是,在OLAP領域,已有的大量數據建模工作都是基於關係模型展開的 ( 星型模型、雪花模型乃至寬表模型 )。ClickHouse使用了關係模型,所以將構建在傳統關係型數據庫或數據倉庫之上的系統遷移到ClickHouse的成本會變得更低,可以直接沿用之前的經驗成果。

5. 多樣化的表引擎

也許因爲Yandex.Metrica的最初架構是基於MySQL實現的,所以在ClickHouse的設計中,能夠察覺到一些MySQL的影子,表引擎的設計就是其中之一。與MySQL類似,ClickHouse也將存儲部分進行了抽象,把存儲引擎作爲一層獨立的接口。截至本書完稿時,ClickHouse共擁有合併樹、內存、文件、接口和其他6大類20多種表引擎。其中每一種表引擎都有着各自的特點,用戶可以根據實際業務場景的要求,選擇合適的表引擎使用。

通常而言,一個通用系統意味着更廣泛的適用性,能夠適應更多的場景。但通用的另一種解釋是平庸,因爲它無法在所有場景內都做到極致。

在軟件的世界中,並不會存在一個能夠適用任何場景的通用系統,爲了突出某項特性,勢必會在別處有所取捨。其實世間萬物都遵循着這樣的道理,就像信天翁和蜂鳥,雖然都屬於鳥類,但它們各自的特點卻鑄就了完全不同的體貌特徵。信天翁擅長遠距離飛行,環繞地球一週只需要1至2個月的時間。因爲它能夠長時間處於滑行狀態,5天才需要扇動一次翅膀,心率能夠保持在每分鐘100至200次之間。而蜂鳥能夠垂直懸停飛行,每秒可以揮動翅膀70~100次,飛行時的心率能夠達到每分鐘1000次。如果用數據庫的場景類比信天翁和蜂鳥的特點,那麼信天翁代表的可能是使用普通硬件就能實現高性能的設計思路,數據按粗粒度處理,通過批處理的方式執行;而蜂鳥代表的可能是按細粒度處理數據的設計思路,需要高性能硬件的支持。

將表引擎獨立設計的好處是顯而易見的,通過特定的表引擎支撐特定的場景,十分靈活。對於簡單的場景,可直接使用簡單的引擎降低成本,而複雜的場景也有合適的選擇。

6. 多線程與分佈式

ClickHouse幾乎具備現代化高性能數據庫的所有典型特徵,對於可以提升性能的手段可謂是一一用盡,對於多線程和分佈式這類被廣泛使用的技術,自然更是不在話下。

如果說向量化執行是通過數據級並行的方式提升了性能,那麼多線程處理就是通過線程級並行的方式實現了性能的提升。相比基於底層硬件實現的向量化執行SIMD,線程級並行通常由更高層次的軟件層面控制。現代計算機系統早已普及了多處理器架構,所以現今市面上的服務器都具備良好的多核心多線程處理能力。由於SIMD不適合用於帶有較多分支判斷的場景,ClickHouse也大量使用了多線程技術以實現提速,以此和向量化執行形成互補。

如果一個籃子裝不下所有的雞蛋,那麼就多用幾個籃子來裝,這就是分佈式設計中分而治之的基本思想。同理,如果一臺服務器性能喫緊,那麼就利用多臺服務的資源協同處理。爲了實現這一目標,首先需要在數據層面實現數據的分佈式。因爲在分佈式領域,存在一條金科玉律—計算移動比數據移動更加划算。在各服務器之間,通過網絡傳輸數據的成本是高昂的,所以相比移動數據,更爲聰明的做法是預先將數據分佈到各臺服務器,將數據的計算查詢直接下推到數據所在的服務器。ClickHouse在數據存取方面,既支持分區 ( 縱向擴展,利用多線程原理 ),也支持分片 ( 橫向擴展,利用分佈式原理 ),可以說是將多線程和分佈式的技術應用到了極致。

7. 多主架構

HDFS、Spark、HBase和Elasticsearch這類分佈式系統,都採用了Master-Slave主從架構,由一個管控節點作爲Leader統籌全局。而ClickHouse則採用Multi-Master多主架構,集羣中的每個節點角色對等,客戶端訪問任意一個節點都能得到相同的效果。這種多主的架構有許多優勢,例如對等的角色使系統架構變得更加簡單,不用再區分主控節點、數據節點和計算節點,集羣中的所有節點功能相同。所以它天然規避了單點故障的問題,非常適合用於多數據中心、異地多活的場景。

8. 在線查詢

ClickHouse經常會被拿來與其他的分析型數據庫作對比,比如Vertica、SparkSQL、Hive和Elasticsearch等,它與這些數據庫確實存在許多相似之處。例如,它們都可以支撐海量數據的查詢場景,都擁有分佈式架構,都支持列存、數據分片、計算下推等特性。這其實也側面說明了ClickHouse在設計上確實吸取了各路奇技淫巧。與其他數據庫相比,ClickHouse也擁有明顯的優勢。例如,Vertica這類商用軟件價格高昂;SparkSQL與Hive這類系統無法保障90%的查詢在1秒內返回,在大數據量下的複雜查詢可能會需要分鐘級的響應時間;而Elasticsearch這類搜索引擎在處理億級數據聚合查詢時則顯得捉襟見肘。

正如ClickHouse的"廣告詞"所言,其他的開源系統太慢,商用的系統太貴,只有Clickouse在成本與性能之間做到了良好平衡,即又快又開源。ClickHouse當之無愧地闡釋了"在線"二字的含義,即便是在複雜查詢的場景下,它也能夠做到極快響應,且無須對數據進行任何預處理加工。

9. 數據分片與分佈式查詢

數據分片是將數據進行橫向切分,這是一種在面對海量數據的場景下,解決存儲和查詢瓶頸的有效手段,是一種分治思想的體現。ClickHouse支持分片,而分片則依賴集羣。每個集羣由1到多個分片組成,而每個分片則對應了ClickHouse的1個服務節點。分片的數量上限取決於節點數量 ( 1個分片只能對應1個服務節點 )。

ClickHouse並不像其他分佈式系統那樣,擁有高度自動化的分片功能。ClickHouse提供了本地表 ( Local Table ) 與分佈式表 ( Distributed Table ) 的概念。一張本地表等同於一份數據的分片。而分佈式表本身不存儲任何數據,它是本地表的訪問代理,其作用類似分庫中間件。藉助分佈式表,能夠代理訪問多個數據分片,從而實現分佈式查詢。

這種設計類似數據庫的分庫和分表,十分靈活。例如在業務系統上線的初期,數據體量並不高,此時數據表並不需要多個分片。所以使用單個節點的本地表 ( 單個數據分片 ) 即可滿足業務需求,待到業務增長、數據量增大的時候,再通過新增數據分片的方式分流數據,並通過分佈式表實現分佈式查詢。這就好比一輛手動擋賽車,它將所有的選擇權都交到了使用者的手中。

ClickHouse存儲層

ClickHouse從OLAP場景需求出發,定製開發了一套全新的高效列式存儲引擎,並且實現了數據有序存儲、主鍵索引、稀疏索引、數據Sharding、數據Partitioning、TTL、主備複製等豐富功能。以上功能共同爲ClickHouse極速的分析性能奠定了基礎。

列式存儲

與行存將每一行的數據連續存儲不同,列存將每一列的數據連續存儲。示例圖如下:

相比於行式存儲,列式存儲在分析場景下有着許多優良的特性。

1)如前所述,分析場景中往往需要讀大量行但是少數幾個列。在行存模式下,數據按行連續存儲,所有列的數據都存儲在一個block中,不參與計算的列在IO時也要全部讀出,讀取操作被嚴重放大。而列存模式下,只需要讀取參與計算的列即可,極大的減低了IO cost,加速了查詢。

2)同一列中的數據屬於同一類型,壓縮效果顯著。列存往往有着高達十倍甚至更高的壓縮比,節省了大量的存儲空間,降低了存儲成本。

3)更高的壓縮比意味着更小的data size,從磁盤中讀取相應數據耗時更短。

4)自由的壓縮算法選擇。不同列的數據具有不同的數據類型,適用的壓縮算法也就不盡相同。可以針對不同列類型,選擇最合適的壓縮算法。

5)高壓縮比,意味着同等大小的內存能夠存放更多數據,系統cache效果更好。

數據有序存儲

ClickHouse支持在建表時,指定將數據按照某些列進行sort by。

排序後,保證了相同sort key的數據在磁盤上連續存儲,且有序擺放。在進行等值、範圍查詢時,where條件命中的數據都緊密存儲在一個或若干個連續的Block中,而不是分散的存儲在任意多個Block, 大幅減少需要IO的block數量。另外,連續IO也能夠充分利用操作系統page cache的預取能力,減少page fault。

主鍵索引

ClickHouse支持主鍵索引,它將每列數據按照index granularity(默認8192行)進行劃分,每個index granularity的開頭第一行被稱爲一個mark行。主鍵索引存儲該mark行對應的primary key的值。

對於where條件中含有primary key的查詢,通過對主鍵索引進行二分查找,能夠直接定位到對應的index granularity,避免了全表掃描從而加速查詢。

但是值得注意的是:ClickHouse的主鍵索引與MySQL等數據庫不同,它並不用於去重,即便primary key相同的行,也可以同時存在於數據庫中。要想實現去重效果,需要結合具體的表引擎ReplacingMergeTree、CollapsingMergeTree、VersionedCollapsingMergeTree實現,我們會在未來的文章系列中再進行詳細解讀。

數據插入、更新、刪除

Clickhouse是個分析型數據庫。這種場景下,數據一般是不變的,因此Clickhouse對update、delete的支持是比較弱的,實際上並不支持標準的update、delete操作。
Clickhouse通過alter方式實現更新、刪除,它把update、delete操作叫做mutation(突變)。
標準SQL的更新、刪除操作是同步的,即客戶端要等服務端反回執行結果(通常是int值);而Clickhouse的update、delete是通過異步方式實現的,當執行update語句時,服務端立即反回,但是實際上此時數據還沒變,而是排隊等着。

Mutation具體過程
首先,使用where條件找到需要修改的分區;然後,重建每個分區,用新的分區替換舊的,分區一旦被替換,就不可回退;對於每個分區,可以認爲是原子性的;但對於整個mutation,如果涉及多個分區,則不是原子性的。
• 更新功能不支持更新有關主鍵或分區鍵的列。
• 更新操作沒有原子性,即在更新過程中select結果很可能是一部分變了,一部分沒變,從上邊的具體過程就可以知道。
• 更新是按提交的順序執行的。
• 更新一旦提交,不能撤銷,即使重啓Clickhouse服務,也會繼續按照system.mutations的順序繼續執行。
• 已完成更新的條目不會立即刪除,保留條目的數量由finished_mutations_to_keep存儲引擎參數確定。 超過數據量時舊的條目會被刪除。
• 更新可能會卡住,比如update intvalue='abc’這種類型錯誤的更新語句執行不過去,那麼會一直卡在這裏,此時,可以使用KILL MUTATION來取消。

使用建議

 按照官方的說明,update/delete 的使用場景是一次更新大量數據,也就是where條件篩選的結果應該是一大片數據。

舉例:alter table test update status=1 where status=0 and day='2020-04-01',一次更新一天的數據。

那麼,能否一次只更新一條數據呢?例如:alter table test update pv=110 where id=100當然也可以,但頻繁的這種操作,可能會對服務造成壓力。這很容易理解,如上文提到,更新的單位是分區,如果只更新一條數據,那麼需要重建一個分區;如果更新100條數據,而這100條可能落在3個分區上,則需重建3個分區;相對來說一次更新一批數據的整體效率遠高於一次更新一行。對於頻繁單條更新的這種場景,建議使用ReplacingMergeTree引擎來變相解決。具體如何使用,以後有時間再整理。

Hbase隨機讀寫,但是Hbase的update操作不是真的update,它的實際操作是insert一條新的數據,打上不同的timestamp,而老的數據會在有效期之後自動刪除。而Clickhouse乾脆就不支持update和delete。

clickhouse核心涉及模塊

1. Column與Field

Column和Field是ClickHouse數據最基礎的映射單元。作爲一款百分之百的列式存儲數據庫,ClickHouse按列存儲數據,內存中的一列數據由一個Column對象表示。Column對象分爲接口和實現兩個部分,在IColumn接口對象中,定義了對數據進行各種關係運算的方法,例如插入數據的insertRangeFrom和insertFrom方法、用於分頁的cut,以及用於過濾的filter方法等。而這些方法的具體實現對象則根據數據類型的不同,由相應的對象實現,例如ColumnString、ColumnArray和ColumnTuple等。在大多數場合,ClickHouse都會以整列的方式操作數據,但凡事也有例外。如果需要操作單個具體的數值 ( 也就是單列中的一行數據 ),則需要使用Field對象,Field對象代表一個單值。與Column對象的泛化設計思路不同,Field對象使用了聚合的設計模式。在Field對象內部聚合了Null、UInt64、String和Array等13種數據類型及相應的處理邏輯。

2. DataType

數據的序列化和反序列化工作由DataType負責。IDataType接口定義了許多正反序列化的方法,它們成對出現,例如serializeBinary和deserializeBinary、serializeTextJSON和deserializeTextJSON等,涵蓋了常用的二進制、文本、JSON、XML、CSV和Protobuf等多種格式類型。IDataType也使用了泛化的設計模式,具體方法的實現邏輯由對應數據類型的實例承載,例如DataTypeString、DataTypeArray及DataTypeTuple等。

DataType雖然負責序列化相關工作,但它並不直接負責數據的讀取,而是轉由從Column或Field對象獲取。在DataType的實現類中,聚合了相應數據類型的Column對象和Field對象。例如,DataTypeString會引用字符串類型的ColumnString,而DataTypeArray則會引用數組類型的ColumnArray,以此類推。

3. Block與Block流

ClickHouse內部的數據操作是面向Block對象進行的,並且採用了流的形式。雖然Column和Filed組成了數據的基本映射單元,但對應到實際操作,它們還缺少了一些必要的信息,比如數據的類型及列的名稱。於是ClickHouse設計了Block對象,Block對象可以看作數據表的子集。Block對象的本質是由數據對象、數據類型和列名稱組成的三元組,即Column、DataType及列名稱字符串。Column提供了數據的讀取能力,而DataType知道如何正反序列化,所以Block在這些對象的基礎之上實現了進一步的抽象和封裝,從而簡化了整個使用的過程,僅通過Block對象就能完成一系列的數據操作。在具體的實現過程中,Block並沒有直接聚合Column和DataType對象,而是通過ColumnWithTypeAndName對象進行間接引用。

有了Block對象這一層封裝之後,對Block流的設計就是水到渠成的事情了。流操作有兩組頂層接口:IBlockInputStream負責數據的讀取和關係運算,IBlockOutputStream負責將數據輸出到下一環節。Block流也使用了泛化的設計模式,對數據的各種操作最終都會轉換成其中一種流的實現。IBlockInputStream接口定義了讀取數據的若干個read虛方法,而具體的實現邏輯則交由它的實現類來填充。

IBlockInputStream接口總共有60多個實現類,它們涵蓋了ClickHouse數據攝取的方方面面。這些實現類大致可以分爲三類:第一類用於處理數據定義的DDL操作,例如DDLQueryStatusInputStream等;第二類用於處理關係運算的相關操作,例如LimitBlockInput-Stream、JoinBlockInputStream及AggregatingBlockInputStream等;第三類則是與表引擎呼應,每一種表引擎都擁有與之對應的BlockInputStream實現,例如MergeTreeBaseSelect-BlockInputStream ( MergeTree表引擎 )、TinyLogBlockInputStream ( TinyLog表引擎 ) 及KafkaBlockInputStream ( Kafka表引擎 ) 等。

IBlockOutputStream的設計與IBlockInputStream如出一轍。IBlockOutputStream接口同樣也定義了若干寫入數據的write虛方法。它的實現類比IBlockInputStream要少許多,一共只有20多種。這些實現類基本用於表引擎的相關處理,負責將數據寫入下一環節或者最終目的地,例如MergeTreeBlockOutputStream 、TinyLogBlockOutputStream及StorageFileBlock-OutputStream等。

4. Table

在數據表的底層設計中並沒有所謂的Table對象,它直接使用IStorage接口指代數據表。表引擎是ClickHouse的一個顯著特性,不同的表引擎由不同的子類實現,例如IStorageSystemOneBlock ( 系統表 )、StorageMergeTree ( 合併樹表引擎 ) 和StorageTinyLog ( 日誌表引擎 ) 等。IStorage接口定義了DDL ( 如ALTER、RENAME、OPTIMIZE和DROP等 ) 、read和write方法,它們分別負責數據的定義、查詢與寫入。在數據查詢時,IStorage負責根據AST查詢語句的指示要求,返回指定列的原始數據。後續對數據的進一步加工、計算和過濾,則會統一交由Interpreter解釋器對象處理。對Table發起的一次操作通常都會經歷這樣的過程,接收AST查詢語句,根據AST返回指定列的數據,之後再將數據交由Interpreter做進一步處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章