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