<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          騰訊阿里頭條翻牌子 | ClickHouse中MergeTree的存儲結(jié)構(gòu)和查詢加速

          共 8724字,需瀏覽 18分鐘

           ·

          2021-03-04 00:08

          點(diǎn)擊上方藍(lán)色字體,選擇“設(shè)為星標(biāo)

          回復(fù)”資源“獲取更多資源

          在上上一篇文章中《ClickHouse表引擎到底怎么選》,我們提到了ClickHouse的引擎選擇問題,本文中我們會介紹在ClickHouse中的SQL執(zhí)行過程。
          在上篇文章中《騰訊阿里頭條翻牌子 | ClickHouse中SQL執(zhí)行過程》我們提到SQL在ClickHouse中的SQL執(zhí)行過程。

          引言

          ClickHouse是最近比較火的一款開源列式存儲分析型數(shù)據(jù)庫,它最核心的特點(diǎn)就是極致存儲壓縮率和查詢性能,本人最近正在學(xué)習(xí)ClickHouse這款產(chǎn)品中。從我個人的視角來看存儲是決定一款數(shù)據(jù)庫核心競爭力、適用場景的關(guān)鍵所在,所以接下來我會陸續(xù)推出一系列文章來分析ClickHouse中最重要的MergeTree存儲內(nèi)核。本文主旨在于介紹MergeTree的存儲格式,并且徹底剖析MergeTree存儲的極致檢索性能。

          MergeTree存儲

          MergeTree思想

          提到MergeTree這個詞,可能大家都會聯(lián)想到LSM-Tree這個數(shù)據(jù)結(jié)構(gòu),我們常用它來解決隨機(jī)寫磁盤的性能問題,MergeTree的核心思想和LSM-Tree相同。MergeTree存儲結(jié)構(gòu)需要對用戶寫入的數(shù)據(jù)做排序然后進(jìn)行有序存儲,數(shù)據(jù)有序存儲帶來兩大核心優(yōu)勢:
          ? 列存文件在按塊做壓縮時,排序鍵中的列值是連續(xù)或者重復(fù)的,使得列存塊的數(shù)據(jù)壓縮可以獲得極致的壓縮比。
          ? 存儲有序性本身就是一種可以加速查詢的索引結(jié)構(gòu),根據(jù)排序鍵中列的等值條件或者range條件我們可以快速找到目標(biāo)行所在的近似位置區(qū)間(下文會展開詳細(xì)介紹),而且這種索引結(jié)構(gòu)是不會產(chǎn)生額外存儲開銷的。
          大家可以從ClickHouse的官方文檔上找到一系列的MergeTree表引擎,包括基礎(chǔ)的MergeTree,擁有數(shù)據(jù)去重能力的ReplacingMergeTree、CollapsingMergeTree、VersionedCollapsingMergeTree,擁有數(shù)據(jù)聚合能力的SummingMergeTree、AggregatingMergeTree等。但這些擁有“特殊能力”的MergeTree表引擎在存儲上和基礎(chǔ)的MergeTree其實(shí)沒有任何差異,它們都是在數(shù)據(jù)Merge的過程中加入了“額外的合并邏輯”,這部分會在后續(xù)介紹MergeTree異步Merge機(jī)制的文章中詳細(xì)展開介紹。

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

          為了方便大家理解表的存儲結(jié)構(gòu),下面列舉了某個POC用戶的測試表DDL,我們將從這個表入手來分析MergeTree存儲的內(nèi)核設(shè)計(jì)。從DDL的PARTITION BY申明中我們可以看出用戶按每個區(qū)服每小時粒度創(chuàng)建了數(shù)據(jù)分區(qū),而每個數(shù)據(jù)分區(qū)內(nèi)部的數(shù)據(jù)又是按照(action_id, scene_id, time_ts, level, uid)作為排序鍵進(jìn)行有序存儲。
          CREATE TABLE user_action_log (
          `time` DateTime DEFAULT CAST('1970-01-01 08:00:00', 'DateTime') COMMENT '日志時間',
          `action_id` UInt16 DEFAULT CAST(0, 'UInt16') COMMENT '日志行為類型id',
          `action_name` String DEFAULT '' COMMENT '日志行為類型名',
          `region_name` String DEFAULT '' COMMENT '區(qū)服名稱',
          `uid` UInt64 DEFAULT CAST(0, 'UInt64') COMMENT '用戶id',
          `level` UInt32 DEFAULT CAST(0, 'UInt32') COMMENT '當(dāng)前等級',
          `trans_no` String DEFAULT '' COMMENT '事務(wù)流水號',
          `ext_head` String DEFAULT '' COMMENT '擴(kuò)展日志head',
          `avatar_id` UInt32 DEFAULT CAST(0, 'UInt32') COMMENT '角色id',
          `scene_id` UInt32 DEFAULT CAST(0, 'UInt32') COMMENT '場景id',
          `time_ts` UInt64 DEFAULT CAST(0, 'UInt64') COMMENT '秒單位時間戳',
          index avatar_id_minmax (avatar_id) type minmax granularity 3
          ) ENGINE = MergeTree()
          PARTITION BY (toYYYYMMDD(time), toHour(time), region_name)
          ORDER BY (action_id, scene_id, time_ts, level, uid)
          PRIMARY KEY (action_id, scene_id, time_ts, level);
          該表的MergeTree存儲結(jié)構(gòu)邏輯示意圖如下:

          MergeTree表的存儲結(jié)構(gòu)中,每個數(shù)據(jù)分區(qū)相互獨(dú)立,邏輯上沒有關(guān)聯(lián)。單個數(shù)據(jù)分區(qū)內(nèi)部存在著多個MergeTree Data Part。這些Data Part一旦生成就是Immutable的狀態(tài),Data Part的生成和銷毀主要與寫入和異步Merge有關(guān)。MergeTree表的寫入鏈路是一個極端的batch load過程,Data Part不支持單條的append insert。每次batch insert都會生成一個新的MergeTree Data Part。如果用戶單次insert一條記錄,那就會為那一條記錄生成一個獨(dú)立的Data Part,這必然是無法接受的。一般我們使用MergeTree表引擎的時候,需要在客戶端做聚合進(jìn)行batch寫入或者在MergeTree表的基礎(chǔ)上創(chuàng)建Distributed表來代理MergeTree表的寫入和查詢,Distributed表默認(rèn)會緩存用戶的寫入數(shù)據(jù),超過一定時間或者數(shù)據(jù)量再異步轉(zhuǎn)發(fā)給MergeTree表。MergeTree存儲引擎對數(shù)據(jù)實(shí)時可見要求非常高的場景是不太友好的。

          上圖展示了單個MergeTree Data Part里最核心的一部分磁盤文件(只畫了action_id和avatar_id列其關(guān)的存儲文件),從功能上分主要有三個類:
          1. 數(shù)據(jù)文件:action_id.bin、avatar_id.bin等都是單個列按塊壓縮后的列存文件。ClickHouse采用了非常極端的列存模式,這里展開一些細(xì)節(jié),單個列數(shù)據(jù)可能會對應(yīng)多個列存文件,例如申明一個Nullable字段時會多一個nullable標(biāo)識的列存文件,申明一個Array字段時會多一個array size的列存文件, 采用字典壓縮時字典Key也會單獨(dú)變成一個列存文件。有一點(diǎn)小Tips:當(dāng)用戶不需要Null值特殊標(biāo)識時,最好不要去申明Nullable,這是ClickHouse的極簡化設(shè)計(jì)思路。
          2. Mark標(biāo)識文件:action_id.mrk2、avatar_id.mrk2等都是列存文件中的Mark標(biāo)記,Mark標(biāo)記和MergeTree列存中的兩個重要概念相關(guān):Granule和Block。
          • Granule是數(shù)據(jù)按行劃分時用到的邏輯概念。關(guān)于多少行是一個Granule這個問題,在老版本中這是用參數(shù)index_granularity設(shè)定的一個常量,也就是每隔確定行就是一個Granule。在當(dāng)前版本中有另一個參數(shù)index_granularity_bytes會影響Granule的行數(shù),它的意義是讓每個Granule中所有列的sum size盡量不要超過設(shè)定值。老版本中的定長Granule設(shè)定主要的問題是MergeTree中的數(shù)據(jù)是按Granule粒度進(jìn)行索引的,這種粗糙的索引粒度在分析超級大寬表的場景中,從存儲讀取的data size會膨脹得非常厲害,需要用戶非常謹(jǐn)慎得設(shè)定參數(shù)。

          • Block是列存文件中的壓縮單元。每個列存文件的Block都會包含若干個Granule,具體多少個Granule是由參數(shù)min_compress_block_size控制,每次列的Block中寫完一個Granule的數(shù)據(jù)時,它會檢查當(dāng)前Block Size有沒有達(dá)到設(shè)定值,如果達(dá)到則會把當(dāng)前Block進(jìn)行壓縮然后寫磁盤。

          • 從以上兩點(diǎn)可以看出MergeTree的Block既不是定data size也不是定行數(shù)的,Granule也不是一個定長的邏輯概念。所以我們需要額外信息快速找到某一個Granule。這就是Mark標(biāo)識文件的作用,它記錄了每個Granule的行數(shù),以及它所在的Block在列存壓縮文件中的偏移,同時還有Granule在解壓后的Block中的偏移位置。

          3. 主鍵索引:primary.idx是表的主鍵索引。ClickHouse對主鍵索引的定義和傳統(tǒng)數(shù)據(jù)庫的定義稍有不同,它的主鍵索引沒用主鍵去重的含義,但仍然有快速查找主鍵行的能力。ClickHouse的主鍵索引存儲的是每一個Granule中起始行的主鍵值,而MergeTree存儲中的數(shù)據(jù)是按照主鍵嚴(yán)格排序的。所以當(dāng)查詢給定主鍵條件時,我們可以根據(jù)主鍵索引確定數(shù)據(jù)可能存在的Granule Range,再結(jié)合上面介紹的Mark標(biāo)識,我們可以進(jìn)一步確定數(shù)據(jù)在列存文件中的位置區(qū)間。ClickHoue的主鍵索引是一種在索引構(gòu)建成本和索引效率上相對平衡的粗糙索引。MergeTree的主鍵序列默認(rèn)是和Order By序列保存一致的,但是用戶可以把主鍵序列定義成Order By序列的部分前綴。
          4分區(qū)鍵索引:minmax_time.idx、minmax_region_name.idx是表的分區(qū)鍵索引。MergeTree存儲會把統(tǒng)計(jì)每個Data Part中分區(qū)鍵的最大值和最小值,當(dāng)用戶查詢中包含分區(qū)鍵條件時,就可以直接排除掉不相關(guān)的Data Part,這是一種OLAP場景下常用的分區(qū)裁剪技術(shù)。
          5Skipping索引:skp_idx_avatar_id_minmax.idx是用戶在avatar_id列上定義的MinMax索引。Merge Tree中 的Skipping Index是一類局部聚合的粗糙索引。用戶在定義skipping index的時候需要設(shè)定granularity參數(shù),這里的granularity參數(shù)指定的是在多少個Granule的數(shù)據(jù)上做聚合生成索引信息。用戶還需要設(shè)定索引對應(yīng)的聚合函數(shù),常用的有minmax、set、bloom_filter、ngrambf_v1等,聚合函數(shù)會統(tǒng)計(jì)連續(xù)若干個Granule中的列值生成索引信息。Skipping索引的思想和主鍵索引是類似的,因?yàn)閿?shù)據(jù)是按主鍵排序的,主鍵索引統(tǒng)計(jì)的其實(shí)就是每個Granule粒度的主鍵序列MinMax值,而Skipping索引提供的聚合函數(shù)種類更加豐富,是主鍵索引的一種補(bǔ)充能力。另外這兩種索引都是需要用戶在理解索引原理的基礎(chǔ)上貼合自己的業(yè)務(wù)場景來進(jìn)行設(shè)計(jì)的。

          MergeTree查詢

          這一章主要會結(jié)合ClickHouse的源碼為大家分析MergeTree表引擎上的數(shù)據(jù)查詢過程,我大致把這個過程分為兩塊:索引檢索和數(shù)據(jù)掃描。索引檢索部分對每個MergeTree Data Part是串行執(zhí)行,但Data Part之間的檢索沒有任何關(guān)聯(lián)。而在數(shù)據(jù)掃描部分中最底層的列存掃描是多所有Data Part并行執(zhí)行,各Data Part的列存掃描之間也沒有任何關(guān)聯(lián)。

          索引檢索

          MergeTree存儲在收到一個select查詢時會先抽取出查詢中的分區(qū)鍵和主鍵條件的KeyCondition,KeyCondition類上實(shí)現(xiàn)了以下三個方法,用于判斷過濾條件可能滿足的Mark Range。上一章講過MergeTree Data Part中的列存數(shù)據(jù)是以Granule為粒度被Mark標(biāo)識數(shù)組索引起來的,而Mark Range就表示Mark標(biāo)識數(shù)組里滿足查詢條件的下標(biāo)區(qū)間。
          /// Whether the condition is feasible in the key range.
          /// left_key and right_key must contain all fields in the sort_descr in the appropriate order.
          /// data_types - the types of the key columns.
          bool mayBeTrueInRange(size_t used_key_size, const Field * left_key, const Field * right_key, const DataTypes & data_types) const;
          /// Whether the condition is feasible in the direct product of single column ranges specified by `parallelogram`.
          bool mayBeTrueInParallelogram(const std::vector<Range> & parallelogram, const DataTypes & data_types) const;
          /// Is the condition valid in a semi-infinite (not limited to the right) key range.
          /// left_key must contain all the fields in the sort_descr in the appropriate order.
          bool mayBeTrueAfter(size_t used_key_size, const Field * left_key, const DataTypes & data_types) const;
          索引檢索的過程中首先會用分區(qū)鍵KeyCondition裁剪掉不相關(guān)的數(shù)據(jù)分區(qū),然后用主鍵索引挑選出粗糙的Mark Range,最后再用Skipping Index過濾主鍵索引產(chǎn)生的Mark Range。用主鍵索引挑選出粗糙的Mark Range的算法是一個不斷分裂Mark Range的過程,返回結(jié)果是一個Mark Range的集合。起始的Mark Range是覆蓋整個MergeTree Data Part區(qū)間的,每次分裂都會把上次分裂后的Mark Range取出來按一定粒度步長分裂成更細(xì)粒度的Mark Range,然后排除掉分裂結(jié)果中一定不滿足條件的Mark Range,最后Mark Range到一定粒度時停止分裂。這是一個簡單高效的粗糙過濾算法。
          使用Skipping Index過濾主鍵索引返回的Mark Range之前,需要構(gòu)造出每個Skipping Index的IndexCondition,不同的Skipping Index聚合函數(shù)有不同的IndexCondition實(shí)現(xiàn),但判斷Mark Range是否滿足條件的接口和KeyCondition是類似的。

          數(shù)據(jù)Sampling

          經(jīng)過上一小節(jié)的索引過濾之后,我們已經(jīng)得到了需要掃描的Mark Range集合,接下來就應(yīng)該是數(shù)據(jù)掃描部分了。這一小節(jié)插入簡單講一下MergeTree里的數(shù)據(jù)Sampling是如何實(shí)現(xiàn)的。它并不是在數(shù)據(jù)掃描過程中實(shí)現(xiàn)的,而是在索引檢索的過程中就已經(jīng)完成,這種做法是為了極致的sample效率。用戶在建表的時候可以指定主鍵中的某個列或者表達(dá)式作為Sampling鍵,ClickHouse在這里用了簡單粗暴的做法:Sampling鍵的值必須是數(shù)值類型的,并且系統(tǒng)假定它的值是隨機(jī)均勻分布的一個狀態(tài)。如果Sampling鍵的值類型是Uint32,當(dāng)我們設(shè)定sample比率是0.1的時候,索引檢索過程中會把sample轉(zhuǎn)換成一個filter條件:Sampling鍵的值 < Uint32::max * 0.1。用戶在使用Sampling功能時必須清楚這個細(xì)節(jié),不然容易出現(xiàn)采樣偏差。一般我們推薦Sampling鍵是列值加一個Hash函數(shù)進(jìn)行隨機(jī)打散。

          數(shù)據(jù)掃描

          MergeTree的數(shù)據(jù)掃描部分提供了三種不同的模式:
          • Final模式:該模式對CollapsingMergeTree、SummingMergeTree等表引擎提供一個最終Merge后的數(shù)據(jù)視圖。前文已經(jīng)提到過MergeTree基礎(chǔ)上的高級MergeTree表引擎都是對MergeTree Data Part采用了特定的Merge邏輯。它帶來的問題是由于MergeTree Data Part是異步Merge的過程,在沒有最終Merge成一個Data Part的情況下,用戶無法看到最終的數(shù)據(jù)結(jié)果。所以ClickHouse在查詢是提供了一個final模式,它會在各個Data Part的多條BlockInputStream基礎(chǔ)上套上一些高級的Merge Stream,例如DistinctSortedBlockInputStream、SummingSortedBlockInputStream等,這部分邏輯和異步Merge時的邏輯保持一致,這樣用戶就可以提前看到“最終”的數(shù)據(jù)結(jié)果了。

          • Sorted模式:sort模式可以認(rèn)為是一種order by下推存儲的查詢加速優(yōu)化手段。因?yàn)槊總€MergeTree Data Part內(nèi)部的數(shù)據(jù)是有序的,所以當(dāng)用戶查詢中包括排序鍵order by條件時只需要在各個Data Part的BlockInputStream上套一個做數(shù)據(jù)有序歸并的InputStream就可以實(shí)現(xiàn)全局有序的能力。

          • Normal模式:這是基礎(chǔ)MergeTree表最常用的數(shù)據(jù)掃描模式,多個Data Part之間進(jìn)行并行數(shù)據(jù)掃描,對于單查詢可以達(dá)到非常高吞吐的數(shù)據(jù)讀取。

          接下來展開介紹下Normal模式中幾個關(guān)鍵的性能優(yōu)化點(diǎn):
          • 并行掃描:傳統(tǒng)的計(jì)算引擎在數(shù)據(jù)掃描部分的并發(fā)度大多和存儲文件數(shù)綁定在一起,所以MergeTree Data Part并行掃描是一個基礎(chǔ)能力。但是MergeTree的存儲結(jié)構(gòu)要求數(shù)據(jù)不斷mege,最終合并成一個Data Part,這樣對索引和數(shù)據(jù)壓縮才是最高效的。所以ClickHouse在MergeTree Data Part并行的基礎(chǔ)上還增加了Mark Range并行。用戶可以任意設(shè)定數(shù)據(jù)掃描過程中的并行度,每個掃描線程分配到的是Mark Range In Data Part粒度的任務(wù),同時多個掃描線程之間還共享了Mark Range Task Pool,這樣可以避免在存儲掃描中的長尾問題。

          • 數(shù)據(jù)Cache:MergeTree的查詢鏈路中涉及到的數(shù)據(jù)有不同級別的緩存設(shè)計(jì)。主鍵索引和分區(qū)鍵索引在load Data Part的過程中被加載到內(nèi)存,Mark文件和列存文件有對應(yīng)的MarkCache和UncompressedCache,MarkCache直接緩存了Mark文件中的binary內(nèi)容,而UncompressedCache中緩存的是解壓后的Block數(shù)據(jù)。

          • SIMD反序列化:部分列類型的反序列化過程中采用了手寫的sse指令加速,在數(shù)據(jù)命中UncompressedCache的情況下會有一些效果。

          • PreWhere過濾:ClickHouse的語法支持了額外的PreWhere過濾條件,它會先于Where條件進(jìn)行判斷。當(dāng)用戶在sql的filter條件中加上PreWhere過濾條件時,存儲掃描會分兩階段進(jìn)行,先讀取PreWhere條件中依賴的列值,然后計(jì)算每一行是否符合條件。相當(dāng)于在Mark Range的基礎(chǔ)上進(jìn)一步縮小掃描范圍,PreWhere列掃描計(jì)算過后,ClickHouse會調(diào)整每個Mark對應(yīng)的Granule中具體要掃描的行數(shù),相當(dāng)于可以丟棄Granule頭尾的一部分行。

          結(jié)語

          隨著閱讀ClickHouse源碼深入了解它的內(nèi)核實(shí)現(xiàn),我認(rèn)為ClickHouse目前還不是一個特別完美的分析型數(shù)據(jù)庫。但它仍然有許多極致的性能優(yōu)化設(shè)計(jì),這些設(shè)計(jì)都是源于Yandex公司真實(shí)的分析場景,并且確實(shí)可以解決海量數(shù)據(jù)下的一些業(yè)務(wù)問題。我相信在一部分適合ClickHouse的業(yè)務(wù)場景中,它就是可以給用戶帶來最極致性能體驗(yàn)的數(shù)據(jù)庫。

          騰訊阿里頭條翻牌子|ClickHouse表引擎到底怎么選

          騰訊阿里頭條翻牌子 | ClickHouse中SQL執(zhí)行過程

          Uber大型實(shí)時數(shù)據(jù)智能平臺建設(shè)

          物聯(lián)網(wǎng)時代的答案 - Apache IoTDB

          一線互聯(lián)網(wǎng)公司面試進(jìn)階全攻略


          歡迎點(diǎn)贊+收藏+轉(zhuǎn)發(fā)朋友圈素質(zhì)三連

          文章不錯?點(diǎn)個【在看】吧! 
          瀏覽 67
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人操骚逼逼 | 透逼网| 乱伦激情网| www.av12 | 五月天在线高清无码 |