涨姿势 | 一文读懂备受大厂青睐的ClickHouse高性能列存核心原理
簡介: 本文嘗試解讀ClickHouse存儲層的設(shè)計與實現(xiàn),剖析它的性能奧妙
作者:和君
?
?
?
引言
?
ClickHouse是近年來備受關(guān)注的開源列式數(shù)據(jù)庫,主要用于數(shù)據(jù)分析(OLAP)領(lǐng)域。目前國內(nèi)各個大廠紛紛跟進(jìn)大規(guī)模使用:
?
- 今日頭條內(nèi)部用ClickHouse來做用戶行為分析,內(nèi)部一共幾千個ClickHouse節(jié)點,單集群最大1200節(jié)點,總數(shù)據(jù)量幾十PB,日增原始數(shù)據(jù)300TB左右。
- 騰訊內(nèi)部用ClickHouse做游戲數(shù)據(jù)分析,并且為之建立了一整套監(jiān)控運維體系。
- 攜程內(nèi)部從18年7月份開始接入試用,目前80%的業(yè)務(wù)都跑在ClickHouse上。每天數(shù)據(jù)增量十多億,近百萬次查詢請求。
- 快手內(nèi)部也在使用ClickHouse,存儲總量大約10PB, 每天新增200TB, 90%查詢小于3S。
- 阿里內(nèi)部專門孵化了相應(yīng)的云數(shù)據(jù)庫ClickHouse,并且在包括手機淘寶流量分析在內(nèi)的眾多業(yè)務(wù)被廣泛使用。
?
在國外,Yandex內(nèi)部有數(shù)百節(jié)點用于做用戶點擊行為分析,CloudFlare、Spotify等頭部公司也在使用。
?
在開源的短短幾年時間內(nèi),ClickHouse就俘獲了諸多大廠的“芳心”,并且在Github上的活躍度超越了眾多老牌的經(jīng)典開源項目,如Presto、Druid、Impala、Geenplum等;其受歡迎程度和社區(qū)火熱程度可見一斑。
?
而這些現(xiàn)象背后的重要原因之一就是它的極致性能,極大地加速了業(yè)務(wù)開發(fā)速度,本文嘗試解讀ClickHouse存儲層的設(shè)計與實現(xiàn),剖析它的性能奧妙。
?
?
?
ClickHouse的組件架構(gòu)
?
下圖是一個典型的ClickHouse集群部署結(jié)構(gòu)圖,符合經(jīng)典的share-nothing架構(gòu)。
?
?
整個集群分為多個shard(分片),不同shard之間數(shù)據(jù)彼此隔離;在一個shard內(nèi)部,可配置一個或多個replica(副本),互為副本的2個replica之間通過專有復(fù)制協(xié)議保持最終一致性。
?
ClickHouse根據(jù)表引擎將表分為本地表和分布式表,兩種表在建表時都需要在所有節(jié)點上分別建立。其中本地表只負(fù)責(zé)當(dāng)前所在server上的寫入、查詢請求;而分布式表則會按照特定規(guī)則,將寫入請求和查詢請求進(jìn)行拆解,分發(fā)給所有server,并且最終匯總請求結(jié)果。
?
?
?
?
ClickHouse寫入鏈路
?
ClickHouse提供2種寫入方法,1)寫本地表;2)寫分布式表。
?
寫本地表方式,需要業(yè)務(wù)層感知底層所有server的IP,并且自行處理數(shù)據(jù)的分片操作。由于每個節(jié)點都可以分別直接寫入,這種方式使得集群的整體寫入能力與節(jié)點數(shù)完全成正比,提供了非常高的吞吐能力和定制靈活性。但是相對而言,也增加了業(yè)務(wù)層的依賴,引入了更多復(fù)雜性,尤其是節(jié)點failover容錯處理、擴縮容數(shù)據(jù)re-balance、寫入和查詢需要分別使用不同表引擎等都要在業(yè)務(wù)上自行處理。
?
而寫分布式表則相對簡單,業(yè)務(wù)層只需要將數(shù)據(jù)寫入單一endpoint及單一一張分布式表即可,不需要感知底層server拓?fù)浣Y(jié)構(gòu)等實現(xiàn)細(xì)節(jié)。寫分布式表也有很好的性能表現(xiàn),在不需要極高寫入吞吐能力的業(yè)務(wù)場景中,建議直接寫入分布式表降低業(yè)務(wù)復(fù)雜度。
?
以下闡述分布式表的寫入實現(xiàn)原理。
?
ClickHouse使用Block作為數(shù)據(jù)處理的核心抽象,表示在內(nèi)存中的多個列的數(shù)據(jù),其中列的數(shù)據(jù)在內(nèi)存中也采用列存格式進(jìn)行存儲。示意圖如下:其中header部分包含block相關(guān)元信息,而id UInt8、name String、_date Date則是三個不同類型列的數(shù)據(jù)表示。
?
?
在Block之上,封裝了能夠進(jìn)行流式IO的stream接口,分別是IBlockInputStream、IBlockOutputStream,接口的不同對應(yīng)實現(xiàn)不同功能。
?
當(dāng)收到INSERT INTO請求時,ClickHouse會構(gòu)造一個完整的stream pipeline,每一個stream實現(xiàn)相應(yīng)的邏輯:
?
InputStreamFromASTInsertQuery #將insert into請求封裝為InputStream作為數(shù)據(jù)源 -> CountingBlockOutputStream #統(tǒng)計寫入block count -> SquashingBlockOutputStream #積攢寫入block,直到達(dá)到特定內(nèi)存閾值,提升寫入吞吐 -> AddingDefaultBlockOutputStream #用default值補全缺失列 -> CheckConstraintsBlockOutputStream #檢查各種限制約束是否滿足 -> PushingToViewsBlockOutputStream #如有物化視圖,則將數(shù)據(jù)寫入到物化視圖中 -> DistributedBlockOutputStream #將block寫入到分布式表中注:*左右滑動閱覽
?
在以上過程中,ClickHouse非常注重細(xì)節(jié)優(yōu)化,處處為性能考慮。在SQL解析時,ClickHouse并不會一次性將完整的INSERT INTO table(cols) values(rows)解析完畢,而是先讀取insert into table(cols)這些短小的頭部信息來構(gòu)建block結(jié)構(gòu),values部分的大量數(shù)據(jù)則采用流式解析,降低內(nèi)存開銷。在多個stream之間傳遞block時,實現(xiàn)了copy-on-write機制,盡最大可能減少內(nèi)存拷貝。在內(nèi)存中采用列存存儲結(jié)構(gòu),為后續(xù)在磁盤上直接落盤為列存格式做好準(zhǔn)備。
?
SquashingBlockOutputStream將客戶端的若干小寫,轉(zhuǎn)化為大batch,提升寫盤吞吐、降低寫入放大、加速數(shù)據(jù)Compaction。
?
默認(rèn)情況下,分布式表寫入是異步轉(zhuǎn)發(fā)的。DistributedBlockOutputStream將Block按照建表DDL中指定的規(guī)則(如hash或random)切分為多個分片,每個分片對應(yīng)本地的一個子目錄,將對應(yīng)數(shù)據(jù)落盤為子目錄下的.bin文件,寫入完成后就返回client成功。隨后分布式表的后臺線程,掃描這些文件夾并將.bin文件推送給相應(yīng)的分片server。.bin文件的存儲格式示意如下:
?
?
?
ClickHouse存儲格式
?
ClickHouse采用列存格式作為單機存儲,并且采用了類LSM tree的結(jié)構(gòu)來進(jìn)行組織與合并。一張MergeTree本地表,從磁盤文件構(gòu)成如下圖所示。
?
?
本地表的數(shù)據(jù)被劃分為多個Data PART,每個Data PART對應(yīng)一個磁盤目錄。Data PART在落盤后,就是immutable的,不再變化。ClickHouse后臺會調(diào)度MergerThread將多個小的Data PART不斷合并起來,形成更大的Data PART,從而獲得更高的壓縮率、更快的查詢速度。當(dāng)每次向本地表中進(jìn)行一次insert請求時,就會產(chǎn)生一個新的Data PART,也即新增一個目錄。如果insert的batch size太小,且insert頻率很高,可能會導(dǎo)致目錄數(shù)過多進(jìn)而耗盡inode,也會降低后臺數(shù)據(jù)合并的性能,這也是為什么ClickHouse推薦使用大batch進(jìn)行寫入且每秒不超過1次的原因。
?
在Data PART內(nèi)部存儲著各個列的數(shù)據(jù),由于采用了列存格式,所以不同列使用完全獨立的物理文件。每個列至少有2個文件構(gòu)成,分別是.bin 和 .mrk文件。其中.bin是數(shù)據(jù)文件,保存著實際的data;而.mrk是元數(shù)據(jù)文件,保存著數(shù)據(jù)的metadata。此外,ClickHouse還支持primary index、skip index等索引機制,所以也可能存在著對應(yīng)的pk.idx,skip_idx.idx文件。
?
在數(shù)據(jù)寫入過程中,數(shù)據(jù)被按照index_granularity切分為多個顆粒(granularity),默認(rèn)值為8192行對應(yīng)一個顆粒。多個顆粒在內(nèi)存buffer中積攢到了一定大小(由參數(shù)min_compress_block_size控制,默認(rèn)64KB),會觸發(fā)數(shù)據(jù)的壓縮、落盤等操作,形成一個block。每個顆粒會對應(yīng)一個mark,該mark主要存儲著2項信息:1)當(dāng)前block在壓縮后的物理文件中的offset,2)當(dāng)前granularity在解壓后block中的offset。所以Block是ClickHouse與磁盤進(jìn)行IO交互、壓縮/解壓縮的最小單位,而granularity是ClickHouse在內(nèi)存中進(jìn)行數(shù)據(jù)掃描的最小單位。
?
如果有ORDER BY key或Primary key,則ClickHouse在Block數(shù)據(jù)落盤前,會將數(shù)據(jù)按照ORDER BY key進(jìn)行排序。主鍵索引pk.idx中存儲著每個mark對應(yīng)的第一行數(shù)據(jù),也即在每個顆粒中各個列的最小值。
?
當(dāng)存在其他類型的稀疏索引時,會額外增加一個<col>_<type>.idx文件,用來記錄對應(yīng)顆粒的統(tǒng)計信息。比如:
?
- minmax會記錄各個顆粒的最小、最大值;
- set會記錄各個顆粒中的distinct值;
- bloomfilter會使用近似算法記錄對應(yīng)顆粒中,某個值是否存在;
?
?
在查找時,如果query包含主鍵索引條件,則首先在pk.idx中進(jìn)行二分查找,找到符合條件的顆粒mark,并從mark文件中獲取block offset、granularity offset等元數(shù)據(jù)信息,進(jìn)而將數(shù)據(jù)從磁盤讀入內(nèi)存進(jìn)行查找操作。類似的,如果條件命中skip index,則借助于index中的minmax、set等信心,定位出符合條件的顆粒mark,進(jìn)而執(zhí)行IO操作。借助于mark文件,ClickHouse在定位出符合條件的顆粒之后,可以將顆粒平均分派給多個線程進(jìn)行并行處理,最大化利用磁盤的IO吞吐和CPU的多核處理能力。
?
總結(jié)
?
本文主要從整體架構(gòu)、寫入鏈路、存儲格式等幾個方面介紹了ClickHouse存儲層的設(shè)計,ClickHouse巧妙地結(jié)合了列式存儲、稀疏索引、多核并行掃描等技術(shù),最大化壓榨硬件能力,在OLAP場景中性能優(yōu)勢非常明顯。
原文鏈接
本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的涨姿势 | 一文读懂备受大厂青睐的ClickHouse高性能列存核心原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 后疫情时代,这家在线教育机构如何乘“云”
- 下一篇: Java单元测试技巧之PowerMock