ClickHouse内核分析-MergeTree的Merge和Mutation机制
注:以下分析基于開源 v19.15.2.2-stable 版本進行
引言
ClickHouse內(nèi)核分析系列文章,繼上一篇文章?MergeTree查詢鏈路?之后,這次我將為大家介紹MergeTree存儲引擎的異步Merge和Mutation機制。建議讀者先補充上一篇文章的基礎(chǔ)知識,這樣會比較容易理解。
MergeTree Mutation功能介紹
在上一篇系列文章中,我已經(jīng)介紹過ClickHouse內(nèi)核中的MergeTree存儲一旦生成一個Data Part,這個Data Part就不可再更改了。所以從MergeTree存儲內(nèi)核層面,ClickHouse就不擅長做數(shù)據(jù)更新刪除操作。但是絕大部分用戶場景中,難免會出現(xiàn)需要手動訂正、修復(fù)數(shù)據(jù)的場景。所以ClickHouse為用戶設(shè)計了一套離線異步機制來支持低頻的Mutation(改、刪)操作。
Mutation命令執(zhí)行
ALTER TABLE [db.]table DELETE WHERE filter_expr; ALTER TABLE [db.]table UPDATE column1 = expr1 [, ...] WHERE filter_expr;ClickHouse的方言把Delete和Update操作也加入到了Alter Table的范疇中,它并不支持裸的Delete或者Update操作。當用戶執(zhí)行一個如上的Mutation操作獲得返回時,ClickHouse內(nèi)核其實只做了兩件事情:
兩者的主體邏輯分別在MutationsInterpreter::validate函數(shù)和StorageMergeTree::mutate函數(shù)中。
MutationsInterpreter::validate函數(shù)dry run一個異步Mutation執(zhí)行的全過程,其中涉及到檢查Mutation是否合法的判斷原則是列值更新后記錄的分區(qū)鍵和排序鍵不能有變化。因為分區(qū)鍵和排序鍵一旦發(fā)生變化,就會導(dǎo)致多個Data Part之間之間Merge邏輯的復(fù)雜化。剩余的Mutation執(zhí)行過程可以看做是打開一個Data Part的BlockInputStream,在這個BlockStream的基礎(chǔ)上封裝刪除操作的FilterBlockInputStream,再加上更新操作的ExpressionBlockInputStream,最后把數(shù)據(jù)通過BlockOutputStream寫回到新的Data Part中。這里簡單介紹一下ClickHouse的計算層實現(xiàn),整體上它是一個火山模型的計算引擎,數(shù)據(jù)的各種filer、投影、join、agg都是通過BlockStrem抽象實現(xiàn),在BlockStream中數(shù)據(jù)是按照Block進行傳輸處理的,而Block中的數(shù)據(jù)又是按照列模式組織,這使得ClickHouse在單列的計算上可以批量化并使用一些SIMD指令加速。BlockOutputStream承擔了MergeTree Data Part列存寫入和索引構(gòu)建的全部工作,我會在后續(xù)的文章中會詳細展開介紹ClickHouse計算層中各類功能的BlockStream,以及BlockOutputStream中構(gòu)建索引的實現(xiàn)細節(jié)。
在Mutation命令的執(zhí)行過程中,我們可以看到MergeTree會把整條Alter命令保存到存儲文件夾下,然后創(chuàng)建一個MergeTreeMutationEntry對象保存到表的待修改狀態(tài)中,最后喚醒一個異步處理merge和 mutation的工作線程。這里有一個關(guān)鍵的問題,因為Mutation的實際操作是異步發(fā)生的,在用戶的Alter命令返回之后仍然會有數(shù)據(jù)寫入,系統(tǒng)如何在異步訂正的過程中排除掉Alter命令之后寫入的數(shù)據(jù)呢?下一節(jié)中我會介紹MergeTree中Data Part的Version機制,它可以在Data Part級別解決上面的問題。但是因為ClickHouse寫入鏈路的異步性,ClickHouse仍然無法保證Alter命令前Insert的每條紀錄都被更新,只能確保Alter命令前已經(jīng)存在的Data Part都會被訂正,推薦用戶只用來訂正T+1場景的離線數(shù)據(jù)。
異步Merge&Mutation
Batch Insert和Mutation的數(shù)據(jù)一致性
struct MergeTreePartInfo {String partition_id;Int64 min_block = 0;Int64 max_block = 0;UInt32 level = 0;Int64 mutation = 0; /// If the part has been mutated or contains mutated parts, is equal to mutation version number..../// Get block number that can be used to determine which mutations we still need to apply to this part/// (all mutations with version greater than this block number).Int64 getDataVersion() const { return mutation ? mutation : min_block; }... bool operator<(const MergeTreePartInfo & rhs) const{return std::forward_as_tuple(partition_id, min_block, max_block, level, mutation)< std::forward_as_tuple(rhs.partition_id, rhs.min_block, rhs.max_block, rhs.level, rhs.mutation);} }在具體展開MergeTree的異步merge和mutation機制之前,先需要詳細介紹一下MergeTree中對Data Part的管理方式。每個Data Part都有一個MergeTreePartInfo對象來保存它的meta信息,MergeTreePartInfo類的結(jié)構(gòu)如上方代碼所示。
解釋了MergeTreePartInfo類中的信息含義,我們就可以理解上一節(jié)中遺留的異步Mutation如何選擇哪些Data Parts需要訂正的問題。系統(tǒng)可以通過MergeTreePartInfo::getDataVersion() { return mutation ? mutation : min_block }函數(shù)來判斷當前Data Part是否需要進行某個mutation訂正,比較兩者version即可。
Merge&Mutation工作任務(wù)
ClickHouse內(nèi)核中異步merge、mutation工作由統(tǒng)一的工作線程池來完成,這個線程池的大小用戶可以通過參數(shù)background_pool_size進行設(shè)置。線程池中的線程Task總體邏輯如下,可以看出這個異步Task主要做三塊工作:清理殘留文件,merge Data Parts 和 mutate Data Part。
BackgroundProcessingPoolTaskResult StorageMergeTree::mergeMutateTask() {....try{/// Clear old parts. It is unnecessary to do it more than once a second.if (auto lock = time_after_previous_cleanup.compareAndRestartDeferred(1)){{/// TODO: Implement tryLockStructureForShare.auto lock_structure = lockStructureForShare(false, "");clearOldPartsFromFilesystem();clearOldTemporaryDirectories();}clearOldMutations();}///TODO: read deduplicate option from table configif (merge(false /*aggressive*/, {} /*partition_id*/, false /*final*/, false /*deduplicate*/))return BackgroundProcessingPoolTaskResult::SUCCESS;if (tryMutatePart())return BackgroundProcessingPoolTaskResult::SUCCESS;return BackgroundProcessingPoolTaskResult::ERROR;}... }需要清理的殘留文件分為三部分:過期的Data Part,臨時文件夾,過期的Mutation命令文件。如下方代碼所示,MergeTree Data Part的生命周期包含多個階段,創(chuàng)建一個Data Part的時候分兩階段執(zhí)行Temporary->Precommitted->Commited,淘汰一個Data Part的時候也可能會先經(jīng)過一個Outdated狀態(tài),再到Deleting狀態(tài)。在Outdated狀態(tài)下的Data Part仍然是可查的。異步Task在收集Outdated Data Part的時候會根據(jù)它的shared_ptr計數(shù)來判斷當前是否有查詢Context引用它,沒有的話才進行刪除。清理臨時文件的邏輯較為簡單,在數(shù)據(jù)文件夾中遍歷搜索"tmp_"開頭的文件夾,并判斷創(chuàng)建時長是否超過temporary_directories_lifetime。臨時文件夾主要在ClickHouse的兩階段提交過程可能造成殘留。最后是清理數(shù)據(jù)已經(jīng)全部訂正完成的過期Mutation命令文件。
enum class State{Temporary, /// the part is generating now, it is not in data_parts listPreCommitted, /// the part is in data_parts, but not used for SELECTsCommitted, /// active data part, used by current and upcoming SELECTsOutdated, /// not active data part, but could be used by only current SELECTs, could be deleted after SELECTs finishesDeleting, /// not active data part with identity refcounter, it is deleting right now by a cleanerDeleteOnDestroy, /// part was moved to another disk and should be deleted in own destructor};Merge邏輯
StorageMergeTree::merge函數(shù)是MergeTree異步Merge的核心邏輯,Data Part Merge的工作除了通過后臺工作線程自動完成,用戶還可以通過Optimize命令來手動觸發(fā)。自動觸發(fā)的場景中,系統(tǒng)會根據(jù)后臺空閑線程的數(shù)據(jù)來啟發(fā)式地決定本次Merge最大可以處理的數(shù)據(jù)量大小,max_bytes_to_merge_at_min_space_in_pool和max_bytes_to_merge_at_max_space_in_pool參數(shù)分別決定當空閑線程數(shù)最大時可處理的數(shù)據(jù)量上限以及只剩下一個空閑線程時可處理的數(shù)據(jù)量上限。當用戶的寫入量非常大的時候,應(yīng)該適當調(diào)整工作線程池的大小和這兩個參數(shù)。當用戶手動觸發(fā)merge時,系統(tǒng)則是根據(jù)disk剩余容量來決定可處理的最大數(shù)據(jù)量。
接下來介紹merge過程中最核心的邏輯:如何選擇Data Parts進行merge?為了方便理解,這里先介紹一下Data Parts在MergeTree表引擎中的管理組織方式。上一節(jié)中提到的MergeTreePartInfo類中定義了比較操作符,MergeTree中的Data Parts就是按照這個比較操作符進行排序管理,排序鍵是(partition_id, min_block, max_block, level, mutation),索引管理結(jié)構(gòu)如下圖所示:
自動Merge的處理邏輯,首先是通過MergeTreeDataMergerMutator::selectPartsToMerge函數(shù)篩選出本次merge要合并的Data Parts,這個篩選過程需要準守三個原則:
所以我們上面的Data Parts組織關(guān)系邏輯示意圖中,相同顏色的Data Parts是可以合并的。雖然圖中三個不同顏色的Data Parts序列都是可以合并的,但是合并工作線程每次只會挑選其中某個序列的一小段進行合并(如前文所述,系統(tǒng)會限定每次合并的Data Parts的數(shù)據(jù)量)。對于如何從這些序列中挑選出最佳的一段區(qū)間,ClickHouse抽象出了IMergeSelector類來實現(xiàn)不同的邏輯。當前主要有兩種不同的merge策略:TTL數(shù)據(jù)淘汰策略和常規(guī)策略。
- TTL數(shù)據(jù)淘汰策略:TTL數(shù)據(jù)淘汰策略啟用的條件比較苛刻,只有當某個Data Part中存在數(shù)據(jù)生命周期超時需要淘汰,并且距離上次使用TTL策略達到一定時間間隔(默認1小時)。TTL策略也非常簡單,首先挑選出TTL超時最嚴重Data Part,把這個Data Part所在的數(shù)據(jù)分區(qū)作為要進行數(shù)據(jù)合并的分區(qū),最后會把這個TTL超時最嚴重的Data Part前后連續(xù)的所有存在TTL過期的Data Part都納入到merge的范圍中。這個策略簡單直接,每次保證優(yōu)先合并掉最老的存在過期數(shù)據(jù)的Data Part。
- 常規(guī)策略:這里的選舉策略就比較復(fù)雜,基本邏輯是枚舉每個可能合并的Data Parts區(qū)間,通過啟發(fā)式規(guī)則判斷是否滿足合并條件,再有啟發(fā)式規(guī)則進行算分,選取分數(shù)最好的區(qū)間。啟發(fā)式判斷是否滿足合并條件的算法在SimpleMergeSelector.cpp::allow函數(shù)中,其中的主要思想分為以下幾點:系統(tǒng)默認對合并的區(qū)間有一個Data Parts數(shù)量的限制要求(每5個Data Parts才能合并);如果當前數(shù)據(jù)分區(qū)中的Data Parts出現(xiàn)了膨脹,則適量放寬合并數(shù)量限制要求(最低可以兩兩merge);如果參與合并的Data Parts中有很久之前寫入的Data Part,也適量放寬合并數(shù)量限制要求,放寬的程度還取決于要合并的數(shù)據(jù)量。第一條規(guī)則是為了提升寫入性能,避免在高速寫入時兩兩merge這種低效的合并方式。最后一條規(guī)則則是為了保證隨著數(shù)據(jù)分區(qū)中的Data Part老化,老齡化的數(shù)據(jù)分區(qū)內(nèi)數(shù)據(jù)全部合并到一個Data Part。中間的規(guī)則更多是一種保護手段,防止因為寫入和頻繁mutation的極端情況下,Data Parts出現(xiàn)膨脹。啟發(fā)式算法的策略則是優(yōu)先選擇IO開銷最小的Data Parts區(qū)間完成合并,盡快合并掉小數(shù)據(jù)量的Data Parts是對在線查詢最有利的方式,數(shù)據(jù)量很大的Data Parts已經(jīng)有了很較好的數(shù)據(jù)壓縮和索引效率,合并操作對查詢帶來的性價比較低。
Mutation邏輯
StorageMergeTree::tryMutatePart函數(shù)是MergeTree異步mutation的核心邏輯,主體邏輯如下。系統(tǒng)每次都只會訂正一個Data Part,但是會聚合多個mutation任務(wù)批量完成,這點實現(xiàn)非常的棒。因為在用戶真實業(yè)務(wù)場景中一次數(shù)據(jù)訂正邏輯中可能會包含多個Mutation命令,把這多個mutation操作聚合到一起訂正效率上就非常高。系統(tǒng)每次選擇一個排序鍵最小的并且需要訂正Data Part進行操作,本意上就是把數(shù)據(jù)從前往后進行依次訂正。
Mutation功能是MergeTree表引擎最新推出一大功能,從我個人的角度看在實現(xiàn)完備度上還有一下兩點需要去優(yōu)化:
最后在經(jīng)過后臺工作線程一輪merge和mutation操作之后,上一節(jié)中展示的MergeTree表引擎中的Data Parts可能發(fā)生的變化如下圖所示,2020-05-10數(shù)據(jù)分區(qū)下的頭兩個Data Parts被merge到了一起,并且完成了Mutation 37和Mutation 39的數(shù)據(jù)訂正,新產(chǎn)生的Data Part如紅色所示:
Clickhouse產(chǎn)品鏈接:https://www.aliyun.com/product/clickhouse
ClickHouse內(nèi)核分析系列文章:
MergeTree查詢鏈路
希望通過內(nèi)核分析系列文章,讓大家更好地了解這款世界領(lǐng)先的列式存儲分析型數(shù)據(jù)庫。
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的ClickHouse内核分析-MergeTree的Merge和Mutation机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: “数据驱动、智能引领”,打造未来智能小镇
- 下一篇: MySQL实战—更新过程