大數據

漲姿勢 | 一文讀懂備受大廠青睞的ClickHouse高性能列存核心原理

作者:和君


引言

ClickHouse是近年來備受關注的開源列式數據庫,主要用於數據分析(OLAP)領域。目前國內各個大廠紛紛跟進大規模使用:

  • 今日頭條內部用ClickHouse來做用戶行為分析,內部一共幾千個ClickHouse節點,單集群最大1200節點,總數據量幾十PB,日增原始數據300TB左右。
  • 騰訊內部用ClickHouse做遊戲數據分析,並且為之建立了一整套監控運維體系。
  • 攜程內部從18年7月份開始接入試用,目前80%的業務都跑在ClickHouse上。每天數據增量十多億,近百萬次查詢請求。
  • 快手內部也在使用ClickHouse,存儲總量大約10PB, 每天新增200TB, 90%查詢小於3S。
  • 阿里內部專門孵化了相應的雲數據庫ClickHouse,並且在包括手機淘寶流量分析在內的眾多業務被廣泛使用。

在國外,Yandex內部有數百節點用於做用戶點擊行為分析,CloudFlare、Spotify等頭部公司也在使用。

在開源的短短几年時間內,ClickHouse就俘獲了諸多大廠的“芳心”,並且在Github上的活躍度超越了眾多老牌的經典開源項目,如Presto、Druid、Impala、Geenplum等;其受歡迎程度和社區火熱程度可見一斑。

而這些現象背後的重要原因之一就是它的極致性能,極大地加速了業務開發速度,本文嘗試解讀ClickHouse存儲層的設計與實現,剖析它的性能奧妙。

ClickHouse的組件架構

下圖是一個典型的ClickHouse集群部署結構圖,符合經典的share-nothing架構。

1.png

整個集群分為多個shard(分片),不同shard之間數據彼此隔離;在一個shard內部,可配置一個或多個replica(副本),互為副本的2個replica之間通過專有複製協議保持最終一致性。

ClickHouse根據表引擎將表分為本地表和分佈式表,兩種表在建表時都需要在所有節點上分別建立。其中本地表只負責當前所在server上的寫入、查詢請求;而分佈式表則會按照特定規則,將寫入請求和查詢請求進行拆解,分發給所有server,並且最終彙總請求結果。

ClickHouse寫入鏈路

ClickHouse提供2種寫入方法,1)寫本地表;2)寫分佈式表。

寫本地表方式,需要業務層感知底層所有server的IP,並且自行處理數據的分片操作。由於每個節點都可以分別直接寫入,這種方式使得集群的整體寫入能力與節點數完全成正比,提供了非常高的吞吐能力和定製靈活性。但是相對而言,也增加了業務層的依賴,引入了更多複雜性,尤其是節點failover容錯處理、擴縮容數據re-balance、寫入和查詢需要分別使用不同表引擎等都要在業務上自行處理。

而寫分佈式表則相對簡單,業務層只需要將數據寫入單一endpoint及單一一張分佈式表即可,不需要感知底層server拓撲結構等實現細節。寫分佈式表也有很好的性能表現,在不需要極高寫入吞吐能力的業務場景中,建議直接寫入分佈式表降低業務複雜度。

以下闡述分佈式表的寫入實現原理。

ClickHouse使用Block作為數據處理的核心抽象,表示在內存中的多個列的數據,其中列的數據在內存中也採用列存格式進行存儲。示意圖如下:其中header部分包含block相關元信息,而id UInt8、name String、_date Date則是三個不同類型列的數據表示。

2.png

在Block之上,封裝了能夠進行流式IO的stream接口,分別是IBlockInputStream、IBlockOutputStream,接口的不同對應實現不同功能。

當收到INSERT INTO請求時,ClickHouse會構造一個完整的stream pipeline,每一個stream實現相應的邏輯:

InputStreamFromASTInsertQuery        #將insert into請求封裝為InputStream作為數據源
-> CountingBlockOutputStream         #統計寫入block count
-> SquashingBlockOutputStream        #積攢寫入block,直到達到特定內存閾值,提升寫入吞吐
-> AddingDefaultBlockOutputStream    #用default值補全缺失列
-> CheckConstraintsBlockOutputStream #檢查各種限制約束是否滿足
-> PushingToViewsBlockOutputStream   #如有物化視圖,則將數據寫入到物化視圖中
-> DistributedBlockOutputStream      #將block寫入到分佈式表中

注:*左右滑動閱覽

在以上過程中,ClickHouse非常注重細節優化,處處為性能考慮。在SQL解析時,ClickHouse並不會一次性將完整的INSERT INTO table(cols) values(rows)解析完畢,而是先讀取insert into table(cols)這些短小的頭部信息來構建block結構,values部分的大量數據則採用流式解析,降低內存開銷。在多個stream之間傳遞block時,實現了copy-on-write機制,盡最大可能減少內存拷貝。在內存中採用列存存儲結構,為後續在磁盤上直接落盤為列存格式做好準備。

SquashingBlockOutputStream將客戶端的若干小寫,轉化為大batch,提升寫盤吞吐、降低寫入放大、加速數據Compaction。

默認情況下,分佈式表寫入是異步轉發的。DistributedBlockOutputStream將Block按照建表DDL中指定的規則(如hash或random)切分為多個分片,每個分片對應本地的一個子目錄,將對應數據落盤為子目錄下的.bin文件,寫入完成後就返回client成功。隨後分佈式表的後臺線程,掃描這些文件夾並將.bin文件推送給相應的分片server。.bin文件的存儲格式示意如下:

3.png

ClickHouse存儲格式

ClickHouse採用列存格式作為單機存儲,並且採用了類LSM tree的結構來進行組織與合併。一張MergeTree本地表,從磁盤文件構成如下圖所示。

4.png

本地表的數據被劃分為多個Data PART,每個Data PART對應一個磁盤目錄。Data PART在落盤後,就是immutable的,不再變化。ClickHouse後臺會調度MergerThread將多個小的Data PART不斷合併起來,形成更大的Data PART,從而獲得更高的壓縮率、更快的查詢速度。當每次向本地表中進行一次insert請求時,就會產生一個新的Data PART,也即新增一個目錄。如果insert的batch size太小,且insert頻率很高,可能會導致目錄數過多進而耗盡inode,也會降低後臺數據合併的性能,這也是為什麼ClickHouse推薦使用大batch進行寫入且每秒不超過1次的原因。

在Data PART內部存儲著各個列的數據,由於採用了列存格式,所以不同列使用完全獨立的物理文件。每個列至少有2個文件構成,分別是.bin 和 .mrk文件。其中.bin是數據文件,保存著實際的data;而.mrk是元數據文件,保存著數據的metadata。此外,ClickHouse還支持primary index、skip index等索引機制,所以也可能存在著對應的pk.idx,skip_idx.idx文件。

在數據寫入過程中,數據被按照index_granularity切分為多個顆粒(granularity),默認值為8192行對應一個顆粒。多個顆粒在內存buffer中積攢到了一定大小(由參數min_compress_block_size控制,默認64KB),會觸發數據的壓縮、落盤等操作,形成一個block。每個顆粒會對應一個mark,該mark主要存儲著2項信息:1)當前block在壓縮後的物理文件中的offset,2)當前granularity在解壓後block中的offset。所以Block是ClickHouse與磁盤進行IO交互、壓縮/解壓縮的最小單位,而granularity是ClickHouse在內存中進行數據掃描的最小單位。

如果有ORDER BY key或Primary key,則ClickHouse在Block數據落盤前,會將數據按照ORDER BY key進行排序。主鍵索引pk.idx中存儲著每個mark對應的第一行數據,也即在每個顆粒中各個列的最小值。

當存在其他類型的稀疏索引時,會額外增加一個<col>_<type>.idx文件,用來記錄對應顆粒的統計信息。比如:

  • minmax會記錄各個顆粒的最小、最大值;
  • set會記錄各個顆粒中的distinct值;
  • bloomfilter會使用近似算法記錄對應顆粒中,某個值是否存在;

5.png

在查找時,如果query包含主鍵索引條件,則首先在pk.idx中進行二分查找,找到符合條件的顆粒mark,並從mark文件中獲取block offset、granularity offset等元數據信息,進而將數據從磁盤讀入內存進行查找操作。類似的,如果條件命中skip index,則藉助於index中的minmax、set等信心,定位出符合條件的顆粒mark,進而執行IO操作。藉助於mark文件,ClickHouse在定位出符合條件的顆粒之後,可以將顆粒平均分派給多個線程進行並行處理,最大化利用磁盤的IO吞吐和CPU的多核處理能力。

總結

本文主要從整體架構、寫入鏈路、存儲格式等幾個方面介紹了ClickHouse存儲層的設計,ClickHouse巧妙地結合了列式存儲、稀疏索引、多核並行掃描等技術,最大化壓榨硬件能力,在OLAP場景中性能優勢非常明顯。

Leave a Reply

Your email address will not be published. Required fields are marked *