《ClickHouse原理解析與應(yīng)用實(shí)踐》讀書總結(jié)

本文是對《ClickHouse原理解析與應(yīng)用實(shí)踐》一書的概括性總結(jié),整體章節(jié)和結(jié)構(gòu)尊重原文,由于書的出版在2019年,版本較舊,所以對應(yīng)部分有修正,修正來源于clickhouse官方設(shè)計(jì)文檔。因此本文是該書與clickhouse官方文檔的一個(gè)互補(bǔ)結(jié)合。

第二章

2.1 核心特性

  1. 列式存儲:純列式數(shù)據(jù)庫/數(shù)據(jù)壓縮
  2. 向量化執(zhí)行/SIMD
  3. 關(guān)系模型/標(biāo)準(zhǔn)SQL
  4. 存儲引擎抽象/20多種存儲引擎
  5. 多線程分布式/分區(qū)分片
  6. 多主架構(gòu)
  7. 數(shù)據(jù)分片(replica, ditribute table)

第三章

兩個(gè)有用的小工具:

  1. clickhouse-local 一個(gè)單機(jī)版的微內(nèi)核,和標(biāo)準(zhǔn)的clickhouse服務(wù)完全隔離開,數(shù)據(jù)也不共享,適用于小批量數(shù)據(jù)。
  2. clickhouse-benchmark: echo 'SELECT count(*) from tutorial.visits_v1' | clickhouse-benchmark -i 20000 可以對指定查詢做一次benchmark。我在1核2G的服務(wù)器上跑了下,得到結(jié)果:


    image.png

第四章

4.1 數(shù)據(jù)類型

clickhouse常量類型推斷:最小存儲代價(jià)

4.1.1 基礎(chǔ)類型

各種長度的整數(shù)、浮點(diǎn)數(shù),定點(diǎn)數(shù),字符串、定長字符串、32位UUID、三種時(shí)間類型(精度最高就到亞秒)

4.1.2 復(fù)合類型

其中array和nested兩種嵌套類型,可以之后再用SQL(array [left] join)打平成多行數(shù)據(jù)。

  • array:[1,2.0] array(float64) 要求同類型
  • tuple:(1,2,'a','2021-04-22 16:05:25') 可以不同類型,要求定長
  • enum:枚舉類型,類似C中的枚舉
  • nested:嵌套類型,但是最多只能嵌套一層,用以表明一些一對多的關(guān)系。本質(zhì)上是用多維數(shù)組來存儲,行內(nèi)的數(shù)組必須都等長;行之間的數(shù)組不必等長。

4.1.3 特殊類型

  1. nullable
    這是一種基本類型的修飾符,表示可以為空。但注意,被修飾的列不能被索引。這種修飾符要慎用,它會對nullable的列額外補(bǔ)充一個(gè)文件[column].null.bin,這意味著讀取時(shí),有雙倍的I/O。
  2. IPv4 IPv6
    這也是種特殊的類型,專門存IP的,底層是int32,提供合法性檢查,插入時(shí)用字符串插入即可。

4.2 DDL

4.2.1 數(shù)據(jù)庫

clickhouse共有5種庫引擎:

  • Memory:內(nèi)存的臨時(shí)庫,不落盤,重啟服務(wù)自動刪除;
  • Ordinary:默認(rèn)引擎;
  • Dictionary:第五章介紹;
  • Lzay:只能使用Log系列表引擎,Log表引擎第八章介紹;
  • Mysql:自動拉取遠(yuǎn)端Mysql數(shù)據(jù),并創(chuàng)建Mysql表引擎的數(shù)據(jù)表,第八章介紹。

數(shù)據(jù)庫本質(zhì)上就是文件目錄。

4.2.2 數(shù)據(jù)表

表也要自選引擎。

4.2.3 默認(rèn)值表達(dá)式

有三種默認(rèn)值表達(dá)式:

  • DEFAULT:可以顯示寫入,進(jìn)行物理存儲,隨SELECT *返回,插入時(shí)就計(jì)算該列的值
  • MATERIALZED:不能顯示寫入,但會進(jìn)行物理存儲,不隨SELECT *返回
  • ALIAS:不能顯示寫入,不進(jìn)行物理存儲,不隨SELECT *返回,只在需要該列時(shí),隨查詢計(jì)算

4.2.4 臨時(shí)表

只支持MEMORY表引擎,不屬于任何數(shù)據(jù)庫,生命周期和Session綁定,連接斷掉,表就廢掉。
一般不用,主要是內(nèi)核用。

4.2.5 分區(qū)表

概念同HIVE分區(qū)表,但是它只作用于本地,沒有什么分布式的概念。
只有MergeTree家族的表引擎才支持分區(qū)。
分區(qū)可以在不同的表間進(jìn)行復(fù)制遷移,但兩個(gè)表的結(jié)構(gòu)和分區(qū)鍵必須一致。
第六章詳細(xì)介紹。

4.2.6 視圖

  • 普通視圖:就是簡化SQL的一種手段,對存儲引擎沒有任何影響。
  • 物化視圖:實(shí)際上類似于一張表,帶有一定的邏輯和它的宿主表表示同步更新(僅插入,刪除和更新還不支持);初始化時(shí)可以從宿主表同步,也可以從0開始。
    clickhouse的物化視圖,還可以指定一個(gè)TO db.name,用于將數(shù)據(jù)從一張表同步到另一張表。

4.3 數(shù)據(jù)表基本操作

  • 列級別的增刪改:
    • 新增一列:默認(rèn)值補(bǔ)全數(shù)據(jù)
    • 修改數(shù)據(jù)類型:需要兼容
  • 表移動位置:換一個(gè)數(shù)據(jù)庫,可以通過RENAME實(shí)現(xiàn),但是只能在本地庫中,不能是遠(yuǎn)程的。

4.4 分區(qū)

可以將一個(gè)分區(qū)內(nèi)的某列數(shù)據(jù)清空,設(shè)置為初始值。
也可以將分區(qū)卸載/裝載,本質(zhì)上,是分區(qū)文件夾位置的遷移,不會真正的刪除。

4.5 分布式DDL

需要主動聲明ON CLUSTER xx_cluster,才會把DDL的SQL語句在某集群內(nèi)統(tǒng)一執(zhí)行。
數(shù)據(jù)塊為單位進(jìn)行操作,在塊級別有原子性

4.7 修改和刪除

由于列存儲,clickhouse的修改和刪除非常的重。會把一個(gè)表的所有分區(qū)的目錄copy一次,去掉那些刪除的行,直到寫一次merge時(shí),原先inactive的數(shù)據(jù)才會被刪除。
而且,異步、非原子性。

第五章 數(shù)據(jù)字典

存于內(nèi)存的一個(gè)scheme,可以build on top of many externel sources(clickhouse, mysql, linux file...)。
scheme以key-attributes形式存儲,key是一個(gè)/多個(gè)屬性,attributes就是一組屬性
默認(rèn)惰性讀取(用到時(shí)從外部source讀進(jìn)內(nèi)存),可以改配置為啟動時(shí)讀取。
外部source包括本地文件、遠(yuǎn)程文件、可執(zhí)行文件、clickhouse、ODBC、DBMS(mysql/postgres/mongoDB/redis)。
這意味著,傳統(tǒng)的ETL功能,很大程度上被代替了。但是實(shí)際上數(shù)據(jù)主要在內(nèi)存,沒有真正落入clickhouse表(雖然我們能以類似表的SQL去訪問它)。
在2020年及之后的clickhouse中,誕生了dictionary表引擎,使得訪問dictionary和訪問普通數(shù)據(jù)庫表的操作,完全一致。

5.2.4 擴(kuò)展字典的類型

  • 單數(shù)值key:
    • flat(recommended): 數(shù)組形式,size有上限,全存在內(nèi)存里,但性能最高;支持所有source;
    • hashed(recommended): 哈希表形式,全部存于內(nèi)存,size無上限;支持所有sources;
    • sparse hashed:稀疏哈希,更省內(nèi)存,更花費(fèi)CPU;
    • range_hashed: 可以由范圍哈希;
    • cache:固定數(shù)量的內(nèi)存slot,自己控制cache的一些策略,且不支持local file,很難使用;
    • direct:不存內(nèi)存,直接訪問外部源;
  • 復(fù)合key:
    • complex_key_hashed(recommended):也是哈希表,但是key可以由多個(gè)屬性構(gòu)成;
    • complex_key_cache:復(fù)合鍵cache
    • ip_trie:用于查ip前綴的,比較特殊。

5.2.6 更新策略

一定時(shí)間范圍內(nèi)隨機(jī)定期更新,能增量就增量,不能增量就全量。

第六章 MergeTree原理解析

6.1 MergeTree的創(chuàng)建方式與存儲結(jié)構(gòu)

MergeTree在寫入數(shù)據(jù)時(shí),是以block為單位寫入的,而且block是immutable的。clickhouse通過后臺線程,定期合并這些block,屬于同一個(gè)分區(qū)的block會被合成新的block,因此被稱為MergeTree。

6.1.1 創(chuàng)建MergeTree表的重要參數(shù)

幾個(gè)參數(shù):

  • PARTITION BY(optional):
  • ORDER BY(required):一個(gè)block內(nèi)部的數(shù)據(jù)按照什么排序,默認(rèn)按照primary key排序,也可以自定義一個(gè)/幾個(gè)key進(jìn)行排序;
  • PRIMARY KEY(optional):主鍵會生成主索引,在單個(gè)block內(nèi)部,數(shù)據(jù)按照主索引的規(guī)則升序排序,允許存在重復(fù)數(shù)據(jù)。默認(rèn)與ORDER BY一樣,所以是可選的。
  • SAMPLE BY(optional):抽樣表達(dá)式,表示數(shù)據(jù)以何種標(biāo)注抽樣,必須在主鍵中出現(xiàn);
  • SETTINGS: index_granuarity(optional):重要參數(shù),每間隔多少條數(shù)據(jù)生成一條索引,默認(rèn)8192;
  • SETTINGS: index_granualrity_bytes(optional):clickhouse會根據(jù)每一批次的數(shù)據(jù)的體量大小,動態(tài)劃分間隔大小。本參數(shù)表名每批數(shù)據(jù)體量大小,默認(rèn)10MB;
  • SETTINGS:merge_with_ttl_timeout(optional):數(shù)據(jù)TTL;
  • SETTINGS:storage_policy(optional):多路徑的存儲策略。

6.1.2 MergeTree的存儲結(jié)構(gòu)

物理存儲結(jié)構(gòu)如下:


image.png
  • columns.txt:該分區(qū)各列字段信息,明文存儲;
  • count.txt:該分區(qū)下數(shù)據(jù)行數(shù),明文存儲;
  • primary.idx:二進(jìn)制文件,主索引,也是一個(gè)稀疏索引;
  • [Column].bin:二進(jìn)制數(shù)據(jù)文件,壓縮格式存儲;
  • [Column].mrk:二進(jìn)制文件,列的標(biāo)記文件,保存了數(shù)據(jù)文件中數(shù)據(jù)的offset信息。標(biāo)記文件與稀疏索引對齊,又與數(shù)據(jù)文件一一對應(yīng)。查詢時(shí),首先通過主索引找到對應(yīng)數(shù)據(jù)的offset信息,利用offset從數(shù)據(jù)文件讀取,后面細(xì)講。
  • [Column].mrk2:與mrk類似,但是是自適應(yīng)索引的標(biāo)記文件;
  • skp_idx_[Column].idx & skp_idx_[Colun].mrk:二級索引數(shù)據(jù)文件和標(biāo)記文件;
  • partition.dat:當(dāng)前分區(qū),分區(qū)表達(dá)式的值,其實(shí)就是哪個(gè)分區(qū);
  • minmax_[PartitionColumn].idx:當(dāng)前分區(qū),分區(qū)列的最大最小值;
  • checksum.txt:保存分區(qū)各文件的size及哈希,用于校驗(yàn)完整性。

6.2 數(shù)據(jù)分區(qū)

如果分區(qū)key是整數(shù)、日期,則轉(zhuǎn)化為字符串作為分區(qū)id,浮點(diǎn)、字符串要哈希之后作為分區(qū)id。多個(gè)key作為分區(qū)key用'-'來連接。

6.2.3 分區(qū)目錄合并

MergeTree的一個(gè)重要特征就是,每次寫入的時(shí)候,都會根據(jù)分區(qū)key產(chǎn)生一批新的分區(qū)目錄,這和原先有的分區(qū)目錄可能會有重復(fù)。比如,同樣是'202104'的分區(qū),可能最后產(chǎn)生了多個(gè)分區(qū)目錄,而不是在一個(gè)分區(qū)目錄下追加文件。
然后,這些相同分區(qū)的目錄,通過后臺任務(wù)進(jìn)行合并,稱為新的分區(qū)目錄。舊分區(qū)目錄延遲一段時(shí)間再刪除。

每一個(gè)分區(qū)目錄都有三個(gè)屬性:

  • MinBlockNum/MaxBlockNum:取同一個(gè)分區(qū)最大/最小block的num。注意這里的block含義比較特殊:可以這樣理解,block num是一個(gè)分區(qū)內(nèi),產(chǎn)生分區(qū)目錄的全局計(jì)數(shù),每新插入一個(gè)分區(qū)目錄(可能是重復(fù)的分區(qū),之后再合并),這個(gè)計(jì)數(shù)就會+1。對于新插入的分區(qū)目錄,minblocknum=maxblocknum,合并過的分區(qū)目錄則不一樣了。
  • level:合并次數(shù)。
image.png
image.png

6.3 主索引

一般來說ORDER KEY和PRIMARY KEY都是一致的,他們指定了主索引和數(shù)據(jù)的排序順序。
主索引是稀疏索引,默認(rèn)粒度8192. 由于占用空間極小,所以常駐內(nèi)存,訪問速度極快。
數(shù)據(jù)文件(.bin)也是按照索引粒度進(jìn)行數(shù)據(jù)塊的壓縮;標(biāo)記文件也會被索引粒度所影響。
primary.idx按照索引粒度,將相應(yīng)位置的PRIMARY KEY讀出來,按照順序緊密拼接起來,沒有一個(gè)多余的字節(jié)。多個(gè)key之間也不分隔。


image.png

image.png

6.3.4 索引的查詢過程

image.png

primary.idx將數(shù)據(jù)文件劃分為若干個(gè)等粒度的markRange,這些步長為1的markRange可以進(jìn)行連續(xù)的合并形成更大的markRange,在邏輯上構(gòu)成一個(gè)樹形。查詢的目標(biāo),就是定位到,哪些markRange(步長為1的)可能會含有QUERY值。
image.png

  1. 首先根據(jù)查詢解析到QUERY的PRIMARY KEY范圍;
  2. 從最大的數(shù)據(jù)范圍內(nèi)開始進(jìn)行遞歸查找,如果QUERY范圍和數(shù)據(jù)范圍有交集,則劃分成8個(gè)子區(qū)間(可以配置),如果已經(jīng)不能拆8份了,即數(shù)據(jù)范圍的markrange步長<8,那么就返回;如果沒交集,那直接剪枝掉。
  3. 最終把返回的markRange區(qū)間合并起來。

6.4 二級索引(data_skipping_index)

二級索引要在建表時(shí)候主動聲明。merge tree會為每個(gè)二級索引,建一個(gè)skp_idx_[Column].idx索引文件和skp_idx_[Column].mrk的標(biāo)記文件。

CREATE TABLE table_name
(
    u64 UInt64,
    i32 Int32,
    s String,
    ...
    INDEX a (u64 * i32, s) TYPE minmax GRANULARITY 3,
    INDEX b (u64 * length(s)) TYPE set(1000) GRANULARITY 4
) ENGINE = MergeTree()
...

注意到,這里的GRANULARITY和主索引中的index_granularity的含義是不一樣的。GRANULARITY控制的是每幾個(gè)index_granularity,在索引文件中,輸出一條匯總聚合信息。


image.png

skip_index的種類:

  • minmax:計(jì)算表達(dá)式的極值;
  • set(max_rows):計(jì)算表達(dá)式的唯一值,max_rows表示每個(gè)index_granularity里,索引最多記錄的行數(shù),0表示無限制;
  • bloom_fliter(false positive rate):支持大多數(shù)類型的bf;
  • ngrambf_v1(token_length, size_of_bf, num_of_hash_functions,random_seed):僅用于字符串的bf,對于in,notin,like,equals,notequals查詢有幫助,但是是按照固定長度進(jìn)行分割;
  • tokenbf_v1(size_of_bf, num_of_hash_functions,random_seed):這個(gè)也是字符串bf,但是分割方式不一樣,是按照非字符數(shù)字的字符串作為分割token。

6.5 數(shù)據(jù)存儲

列存儲,每個(gè)列都有一個(gè).bin數(shù)據(jù)文件。每列數(shù)據(jù)都是經(jīng)過壓縮的,目前支持LZ4,ZSTD,Multiple,Delta,數(shù)據(jù)按照ORDER KEY進(jìn)行排序,以壓縮數(shù)據(jù)塊的格式寫入數(shù)據(jù)文件。

6.5.2 壓縮數(shù)據(jù)塊

image.png

數(shù)據(jù)文件的寫入,首先是以批次為單位的,即index_granularity所規(guī)定的條數(shù)。然而這些條數(shù)的size是不確定的,對于這個(gè)size,有兩個(gè)極限值來限制(min_compress_block_size(64KB), max_compress_block_size(1MB))。
考慮三種情況:

  • 單批次size正好在64KB-1MB之間:那么直接壓縮落盤成一個(gè)壓縮塊;
  • 單批次size超過了1MB:那么每1MB都要截?cái)嗌梢粋€(gè)壓縮塊落盤,剩下最后一個(gè)如果不到64KB,見第三種情況;
  • 單批次size不足64KB:暫時(shí)不壓縮,繼續(xù)獲取下一批次,累積到64KB再壓縮落盤。


    image.png

    clickhouse對于數(shù)據(jù)最細(xì)粒度的使用就是這個(gè)壓縮塊,也就意味著,即使是只查詢一個(gè)分區(qū)的一列數(shù)據(jù),也可以通過只讀部分壓縮塊來減少I/O。

6.6 數(shù)據(jù)標(biāo)記

.mrk文件為索引文件.idx和數(shù)據(jù)文件.bin建立聯(lián)系。
標(biāo)記文件的邏輯結(jié)構(gòu)如下圖所示,每一行都代表了一個(gè)index_granularity,文件中記錄了granularity在壓縮文件中的位置,以及將對應(yīng)的壓縮塊解壓縮后,在解壓縮塊中的偏移量。


image.png

標(biāo)記文件并不常駐內(nèi)存,而是使用LRU策略緩存。

6.6.2 數(shù)據(jù)標(biāo)記的工作方式

  1. 給定index_granularity的標(biāo)號,去.mrk文件里面讀出壓縮塊偏移量,然后再找到下一個(gè)有變化的偏移量,二者之間就是本個(gè)granularity的壓縮塊的位置;
  2. 把壓縮塊拉到內(nèi)存里進(jìn)行解壓;
  3. 根據(jù).mrk文件中的解壓縮塊的偏移量(類似于第一步),掃描相應(yīng)位置,讀到數(shù)據(jù)。

6.7 MegeTree Summarization

6.7.1 寫入

image.png

6.7.2 查詢

查詢的性能主要取決于WHERE條件的Selectivity是否命中了索引,如果沒有索引,也只能順序掃描全部,可以多線程并行掃描。


image.png

第七章 MergeTree系列表引擎

MergeTree家族是clickhouse最核心的存儲引擎。

7.1 MergeTree

本章只講兩個(gè)額外的mergeTree特性。

7.1.1 TTL

clickhouse可以對列/表設(shè)置TTL,表示清除它們的時(shí)間;對列清除是把它們變成默認(rèn)值,對表清除是把過期的行刪掉。
TTL的設(shè)置必須依賴于表中已有的一個(gè)日期/時(shí)間字段,在它的基礎(chǔ)上定義過期的絕對時(shí)間。
可以在表定義的時(shí)候指定TTL,也可以后面再修改。
具體到實(shí)現(xiàn)層面,TTL的實(shí)現(xiàn)會在每個(gè)分區(qū)目錄下寫一個(gè)ttl.txt文件,用json格式配置TTL信息:

  • columns:列級別TTL
  • tables:表級別TTL
  • min/max:保存當(dāng)前分區(qū)內(nèi),過期時(shí)間戳的最小值和最大值。
    只有在MergeTree合并分區(qū)時(shí),才會觸發(fā)TTL刪除機(jī)制。

7.1.2 多路徑存儲

可以以分區(qū)為最小單元,將數(shù)據(jù)寫入多個(gè)磁盤目錄。

  • 默認(rèn)策略:所有分區(qū)寫入path指定的位置;
  • JBOD(Just a Bunch Of Disks)策略:round rubin方式寫入多個(gè)磁盤位置;
  • HOT/COLD策略:分成HOT/COLD區(qū)域,開始寫都向SSD區(qū)域?qū)憻釘?shù)據(jù),熱數(shù)據(jù)分區(qū)數(shù)據(jù)囤聚累計(jì)到閾值了,數(shù)據(jù)就自行移動到COLD區(qū)域。每個(gè)區(qū)域內(nèi)部可以用JBOD策略。

7.2 ReplacingMergeTree

MergeTree允許主鍵重復(fù),ReplacingMergeTree可以在分區(qū)內(nèi)部保證在充分merge之后(OPTIMIZE命令),數(shù)據(jù)按照ORDER KEY不重復(fù)。

  • 僅在merge分區(qū)時(shí)才會觸發(fā)刪除重復(fù)數(shù)據(jù);
  • 不同分區(qū)不去重(因?yàn)椴粫黄疬M(jìn)行排序);
  • 去重默認(rèn)根據(jù)數(shù)據(jù)插入時(shí)間,保留最新的;但也可以在建表時(shí)設(shè)置一個(gè)ver參數(shù),ver代表表的一個(gè)列,去重會保留最大的該列數(shù)據(jù)。

7.3 SummingMergeTree

如果用戶只關(guān)心該表的匯總數(shù)據(jù)(SUM),不關(guān)心明細(xì)數(shù)據(jù),而且GROUP BY條件預(yù)先都設(shè)置好的,則可以用SummingMergeTree。
這其實(shí)就是數(shù)倉中的DWS層,只不過clickhouse在存儲引擎層做了這個(gè)事情。

  1. 根據(jù)ORDER BY作為聚合數(shù)據(jù)的最細(xì)粒度;
  2. 以分區(qū)為單位進(jìn)行聚合(SUM),不同分區(qū)不聚合;
  3. 可以建表時(shí)指定需要聚合的列,默認(rèn)是所有數(shù)值列;非聚合列則使用第一行數(shù)據(jù);
  4. 嵌套數(shù)據(jù)也可以內(nèi)部按照key聚合。

注意在SummingMergeTree和后面的AggregatingMergeTree中,出現(xiàn)一種use case,就是PRIMARY KEY(索引包含字段)和ORDER KEY(數(shù)據(jù)排列順序)不一致。
這種不一致只能是PRIMARY KEY是ORDER KEY的前綴。
用戶可能想讓匯總表按照A、B、C、D四個(gè)字段為粒度的匯總,但是從查詢過濾的角度來說,只需要過濾A即可,后面的區(qū)分度不大,那么可以讓數(shù)據(jù)按ABCD進(jìn)行ORDER BY排序,主索引只有按A排列。
后面如果業(yè)務(wù)上不需要按照ABCD為粒度進(jìn)行匯總了,那么也可以修改為ABC或者AB。

7.4 AggregatingMergeTree

是一個(gè)增強(qiáng)版的SummingMergeTree,可以對各個(gè)需要聚合的字段在建表時(shí)確定聚合函數(shù)。
雖然提供了非常強(qiáng)大的DWS功能,但是使用起來,插入/查詢十分不便,因?yàn)椴荒苤付忻@式寫特殊函數(shù),因此AggregatingMergeTree通常會作為明細(xì)表的物化視圖的引擎而出現(xiàn)。插入,正常插入底表,同步至物化視圖;查詢,則需要特殊函數(shù)指定聚合列。

CREATE MATERIALIZED VIEW test.basic
ENGINE = AggregatingMergeTree() PARTITION BY toYYYYMM(StartDate) ORDER BY (CounterID, StartDate)
AS SELECT
    CounterID,
    StartDate,
    sumState(Sign)    AS Visits,
    uniqState(UserID) AS Users
FROM test.visits
GROUP BY CounterID, StartDate;
SELECT
    StartDate,
    sumMerge(Visits) AS Visits,
    uniqMerge(Users) AS Users
FROM test.basic
GROUP BY StartDate
ORDER BY StartDate;

7.5 CollapsingMergeTree

支持行級別的數(shù)據(jù)修改和刪除的引擎。
以增代刪,需要指定一個(gè)列,作為刪除的標(biāo)記字段。

7.6 VersionedCollapsingMergeTree

通過指定一個(gè)version列,保證最大的version可以被保留,使得分區(qū)內(nèi)的修改和刪除順序,是可以保證的,即使是在多線程寫入的情況下。

7.7 MergeTree家族關(guān)系

MergeTree家族的各個(gè)引擎,僅是在Merge的過程中,按照ORDERY KEY排序的過程中各展所長而已(折疊、聚合、去重)。


image.png

除此之外,每個(gè)引擎,還可以通過zookeeper廣播log entry來實(shí)現(xiàn)replicated版本。


image.png

第八章 其他表引擎

8.1 外部存儲引擎

clichhouse存儲元數(shù)據(jù),數(shù)據(jù)文件由外部提供。

  • HDFS:clickhouse可以通過HDFS引擎,操控HDFS文件,查詢、寫入等;也可以直接去讀HDFS上已有的文件。
  • MySQL:遠(yuǎn)程操作表。
  • JDBC
  • Kafka:可以用表的形式做消費(fèi)端。每隔一段時(shí)間拉一次數(shù)據(jù),先放緩存,然后入表。僅支持at least once語義。
  • File:直接讀取本地文件,可以讀寫。

8.2 內(nèi)存類型

內(nèi)存引擎都是直接在內(nèi)存里讀數(shù)據(jù),在內(nèi)存里進(jìn)行查詢。實(shí)際上,某些引擎也會將數(shù)據(jù)落盤以防止丟失。數(shù)據(jù)表加載時(shí)再讀到內(nèi)存中。沒有主鍵,order key等。

  • Memory:將數(shù)據(jù)原樣保存于內(nèi)存,沒有壓縮、序列化、落盤,重啟就會丟失。clickhouse內(nèi)部將其用于臨時(shí)廣播表。
  • Set:自動去重。數(shù)據(jù)首先寫入內(nèi)存,然后同步至磁盤。可以INSERT,不能SELECT查詢,只能位于IN查詢的右側(cè)條件。
    • [num].bin:num表示數(shù)據(jù)寫入的批次,自增的,不合并,所有列的數(shù)據(jù)都在這個(gè)文件里;
    • tmp:寫入臨時(shí)目錄,進(jìn)入數(shù)據(jù)文件后清理掉。
  • Join:和Set引擎的邏輯非常像,但是可以直接SELECT查詢,主要用于和其他表進(jìn)行join。
  • Buffer:某個(gè)主表的緩沖區(qū),寫入首先寫入內(nèi)存Buffer表,然后定期/定量的并行地刷入主表,以減少主表merge。不用于查詢,不持久化。

8.3 日志類型

數(shù)據(jù)量很小(不到100W行),一次寫入多次查詢,查詢不復(fù)雜,才會用日志引擎。不支持索引、分區(qū),不支持并發(fā)讀寫,完全同步,有物理存儲。

  • TinyLog:只有每列一個(gè).bin數(shù)據(jù)文件,單線程讀寫;
  • StripeLog:所有列都在一個(gè).bin數(shù)據(jù)文件,但是有標(biāo)記文件.mrk,可以并行讀。
  • Log:性能最高的日志引擎。各列都有.bin數(shù)據(jù)文件,但是每個(gè)表只有一個(gè).mrk文件,存著各列數(shù)據(jù)文件的位置信息。

8.4 接口類型

本身不存儲任何數(shù)據(jù),作為“膠水”整合其他數(shù)據(jù)表。

  • Merge:不能寫入,可以作為代理整合同庫同表結(jié)構(gòu)的其他表(分區(qū)定義可以不同),對它的查詢會dispatch下去,異步并行執(zhí)行,最終合成結(jié)果集返回。
  • Dictionary:之前說過,是磁盤上數(shù)據(jù)scheme的一個(gè)內(nèi)存映射,可以以表的形式訪問,創(chuàng)建一個(gè)Dictionary庫,可以把所有Dictionary弄成表。
  • Distributed:分布式分shard的中間件,后面介紹。

第九章 數(shù)據(jù)查詢

這里只標(biāo)注一些clickhouse特殊的數(shù)據(jù)查詢形式。

  • Join strictness:ch的join不僅有傳統(tǒng)的內(nèi)外連接,還有一個(gè)連接嚴(yán)格度
    • ALL:這和我們普通SQL所支持的是一樣的A ALL INNER JOIN B ON A.key=B.key,如果A中一條記錄匹配B中多條記錄,那么全部emit;
    • ANY:只返回匹配的第一條;
    • ASOF:模糊查詢,可以定義一個(gè)模糊key,滿足>=關(guān)系即認(rèn)為是匹配的,然后只emit第一條匹配的。這個(gè)主要是要由于clickhouse只支持equal join,不支持其他的join 匹配方式。

clickhouse執(zhí)行join(本地)時(shí)會把右表當(dāng)做小表完全拉進(jìn)內(nèi)存與左表比較;而且JOIN沒有任何緩存,頻繁使用的右表,最好都做成JOIN引擎表來進(jìn)行緩存;clickhouse是大表模式,join非常吃力,如果需要連續(xù)補(bǔ)充多個(gè)維度,可以將維度表作為數(shù)據(jù)字典來join。

  • prewhere:這是where的一個(gè)優(yōu)化版本。除了有索引的條件外,where一般會在select之后,返回之前執(zhí)行,作為篩選;而prewhere則會在最初在相應(yīng)的列選出滿足條件的數(shù)據(jù),之后在select過程中再補(bǔ)充其他列。

第十章 副本與分片

10.2 副本

image.png

插入數(shù)據(jù)時(shí),數(shù)據(jù)首先寫入內(nèi)存緩沖,然后刷到磁盤tmp目錄,全部刷完后,整合進(jìn)正式分區(qū),然后將日志entry同步至zookeeper。數(shù)據(jù)寫入以block為基本單元和最小粒度(max_insert_block_size),對block的寫入保持原子性。
clickhouse副本寫入依賴zk,但是查詢并不依賴zk。同一個(gè)shard寫進(jìn)同一個(gè)zk_path,各個(gè)副本保持不一樣的replica_name。
ch的副本是表級別的,而且是多主架構(gòu),這種架構(gòu)使得副本不僅僅為了容錯(cuò),每個(gè)副本都可以作為讀寫的入口,用以負(fù)載均衡。

10.3 ReplicatedMergeTree原理解析

replicatedMergeTree會在zk_path上為這張表創(chuàng)建一組監(jiān)聽節(jié)點(diǎn),分成以下幾類:

  • 元數(shù)據(jù):表metadata,列字段,副本們
  • 判斷標(biāo)識:主副本的選舉工作;block數(shù)據(jù)塊的哈希值和所屬的partition_id;quorun數(shù)量,最少寫成功副本;block_nums,數(shù)據(jù)塊的寫入順序;
  • 操作日志:常規(guī)log和mutations(被稱為logEntry和MutationEntry);log執(zhí)行任務(wù)隊(duì)列;log/mutation執(zhí)行offset;

LogEntry包含以下信息:

  • source_replica
  • type(get,merge,mutate)
  • block_id
  • partition_name

MutationEntry包含以下信息:

  • source_replica
  • commands(DELETE/UPDATE)
  • mutation_id
  • partition_id

需要主副同步的操作主要有INSERT,MERGE,MUTATION,ALTER四種,即數(shù)據(jù)寫入,分區(qū)合并,數(shù)據(jù)修改,元數(shù)據(jù)修改四種。
對于其他SQL指令,如SELECT,CREATE,DROP,RENAME,ATTACH等,不支持分布式,要不然登錄每臺機(jī)器結(jié)點(diǎn)分別執(zhí)行,要不然用一些trick,后面講。

  • INSERT:哪臺服務(wù)器作為接口提供寫入,哪臺服務(wù)器就是主服務(wù)器,首先在本地寫成分區(qū)的形式,然后把寫入log放到zk,寫入log中只包含分區(qū)信息,不含具體數(shù)據(jù),其他副本監(jiān)聽log,讀取后放入隊(duì)列,移動log pointer。副本的后臺線程從zk隊(duì)列中讀取log,并選擇log pointer最大,隊(duì)列最短的節(jié)點(diǎn),建立http連接,讀取分區(qū)。
  • MERGE:無論在哪個(gè)副本上觸發(fā)了MERGE條件,MERGE最終都是主節(jié)點(diǎn)來進(jìn)行的。follower向主副本進(jìn)行通信,主副本制定MERGE計(jì)劃,做成LOG推送到zk中。與此同時(shí),主副本鎖住執(zhí)行線程,監(jiān)聽MERGE執(zhí)行情況,各副本將LOG拉到自己的zk QUEUE里,然后進(jìn)行消費(fèi),在本地執(zhí)行MERGE,直到達(dá)到用戶設(shè)置的個(gè)數(shù)之后,主副本線程解鎖。
  • MUTATION:和MERGE類似,通知主副本指定MUTATION計(jì)劃,各副本執(zhí)行即可;
  • ALTER:直接修改zk元數(shù)據(jù),更改zk元數(shù)據(jù)版本;各副本會對zk元數(shù)據(jù)進(jìn)行監(jiān)聽,有變化時(shí)會進(jìn)行對照修改。全部修改完成后誰執(zhí)行誰負(fù)責(zé)結(jié)束。

10.4 數(shù)據(jù)分片

clickhouse可以靈活配置多個(gè)cluster,在每個(gè)cluster,一個(gè)表可以由多個(gè)shard(水平數(shù)據(jù)分區(qū)),每個(gè)shard內(nèi)部還可以有數(shù)據(jù)副本(垂直數(shù)據(jù)冗余)。
有了cluster name的配置后,一些DDL語句可以用ON CLUSTER cluster_name進(jìn)行分布式執(zhí)行,原理就是根據(jù)配置,在每個(gè)replica都執(zhí)行相同的指令。
其中{replica},{shard}可以以宏的形式寫,因?yàn)樵诩好颗_機(jī)器內(nèi),都有表存儲了當(dāng)前機(jī)器的replica shard分別是什么值。


image.png

DDL的分布式執(zhí)行也是借助于zk,task的發(fā)布、狀態(tài)、完成情況都記錄在zk,秉著誰發(fā)起誰負(fù)責(zé)的策略,發(fā)起者負(fù)責(zé)監(jiān)視是否cluster內(nèi)所有節(jié)點(diǎn)任務(wù)完成,完成則結(jié)束返回,否則要轉(zhuǎn)入后臺執(zhí)行。

10.5 Distributed原理解析

分布式表對應(yīng)多個(gè)本地表,在多個(gè)本地shard表提供一個(gè)分布式透明性的服務(wù)。分布式表的INSERT SELECT直接作用于其管理的本地表,但是CREATE DROP RENAME之類的元數(shù)據(jù)操作只作用于自身,不作用與本地表。不支持MUTATION。
分布式表要求所有本地表結(jié)構(gòu)一致,命名相同,僅shard和replica參數(shù)不同,在讀時(shí)檢查;
分布式表創(chuàng)建時(shí)也要指定ON CLUSTER,在集群所有節(jié)點(diǎn)創(chuàng)建分布式表,使得整個(gè)集群都可以成為讀寫入口。
分布式表創(chuàng)建時(shí)要指定一個(gè)sharding key和sharding function進(jìn)行分區(qū),function最終返回一個(gè)整數(shù)即可; 每個(gè)shard在配置文件中都有一個(gè)權(quán)重,代表數(shù)據(jù)流入的比率,clickhouse按照weights將sharding function的值域劃分為幾個(gè)連續(xù)區(qū)間,承接數(shù)據(jù)寫入。


image.png

10.5.4 DISTRIBUTED寫入

DISTRIBUTED表的數(shù)據(jù)寫入一個(gè)shard節(jié)點(diǎn),首先是,把本shard內(nèi)的數(shù)據(jù)寫入分區(qū),然后把其他shard分區(qū)的挑出來,分別寫到一個(gè)固定位置去,形成分區(qū);本shard內(nèi)有一個(gè)對固定位置的監(jiān)視器,監(jiān)測到分區(qū)目錄變化后,會根據(jù)目錄名,立即與遠(yuǎn)程shard建立聯(lián)系,壓縮傳遞數(shù)據(jù)。秉著誰執(zhí)行誰負(fù)責(zé)的原則,寫入shard負(fù)責(zé)確保數(shù)據(jù)都已經(jīng)正確寫入,結(jié)束返回。
寫入的過程可以設(shè)置同步/異步,異步不用等待遠(yuǎn)程寫完,同步需要設(shè)置超時(shí)。

上面的過程只考慮了sharding,沒有考慮replica,replica的同步寫入可以有兩種模式,通過配置文件寫死配置。第一種可以通過上述的方式,由distribute引擎寫入replica,然而這種方式,寫入節(jié)點(diǎn)要傳輸和寫入的replica太多了,容易造成單點(diǎn)瓶頸;另一種方式是通過Repliacted-MergeTree,利用zk傳輸日志來進(jìn)行同步,這樣寫入節(jié)點(diǎn)只需在每個(gè)shard選一個(gè)replica寫入即可,具體選哪個(gè)可以根據(jù)一個(gè)全局計(jì)數(shù)器errors_count來選擇。


image.png

10.5.5 DISTRIBUTED查詢

分布式查詢,要在每一個(gè)shard選擇一個(gè)replica,這就涉及一個(gè)負(fù)載均衡算法,由參數(shù)控制,有以下四種

  • random:默認(rèn)算法。選擇errors_count最小的replica,相等的話就隨機(jī)選擇;
  • nearest_hostname:還是首先選擇errors_count最小的,然后選hostname最接近的;
  • in_order:先選errors_count,相等的話看配置文件的順序;
  • first_or_random:先選errors_count,然后按配置文件的第一個(gè)看,如果第一個(gè)不可用就隨機(jī)選了。

可想而知,查詢也是誰執(zhí)行誰負(fù)責(zé),誰是入口查詢節(jié)點(diǎn),誰就要串聯(lián)整個(gè)查詢過程。包括分割分布式查詢?yōu)楸镜刈硬樵儯x擇連接其他shard的節(jié)點(diǎn),傳遞SQL,收到結(jié)果數(shù)據(jù),UNION返回結(jié)果。

  • 使用GLOBAL優(yōu)化分布式子查詢
    對于SQL中的子查詢和JOIN,很可能在一條SQL中出現(xiàn)兩次分布式表,那么就會出現(xiàn)SQL被反復(fù)傳遞以獲取信息,導(dǎo)致查詢請求冪次擴(kuò)大的問題,為了解決這個(gè)問題,在第二/N次出現(xiàn)分布式表的時(shí)候,加入GLOBAL字段,查詢中首先執(zhí)行GLOBAL子查詢,得到結(jié)果返回,構(gòu)成內(nèi)存表,再廣播給各個(gè)節(jié)點(diǎn),最后執(zhí)行主查詢。
    由于這種分布式IN/JOIN方式,子句返回的結(jié)果不能太大,要在內(nèi)存中放得下,因此最好要提前DISTINCT或者篩選掉一部分。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,998評論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內(nèi)容