
注:以下分析基于開源 v19.15.2.2-stable 版本進行
引言
ClickHouse核心分析系列文章,本文将為大家深度解讀Zookeeper在ClickHouse叢集中的作用,目前和Zookeeper密切相關的功能包括分布式DDL執行和ReplicatedMergeTree表引擎。最近碰到了很多同學詢問和Zookeeper相關的問題,希望通過本文大家可以深刻了解ClickHouse用Zookeeper到底解決哪些問題。正文将會為大家依次介紹分布式DDL執行和ReplicatedMergeTree表引擎依賴的實作細節,建議讀者先補充系列文章中關于MergeTree表引擎的前兩篇文章,這樣會比較容易了解ReplicatedMergeTree表引擎。ReplicatedMergeTree表引擎中的主備同步完全依賴Zookeeper,并且邏輯十分複雜,本文隻能為大家呈現一個大體的邏輯鍊路。
Zookeeper在ClickHouse中的應用簡介
Zookeeper作為一個分布式一緻性存儲服務,提供了豐富的讀寫接口和watch機制,分布式應用基于Zookeeper可以解決很多常見問題,例如心跳管理、主備切換、分布式鎖等。建議對Zookeeper完全沒有了解的同學先補充一些Zookeeper的基本概念再來讀本文。
ClickHouse中依賴Zookeeper解決的問題可以分為兩大類:分布式DDL執行、ReplicatedMergeTree表主備節點之間的狀态同步。
分布式DDL執行:ClickHouse中DDL執行預設不是分布式化的,使用者需要在DDL語句中加上on Cluster XXX的申明才能觸發這個功能。和其他完全分布式化的資料庫不同,ClickHouse對庫、表的管理都是在存儲節點級别獨立的,叢集中各節點之間的庫、表中繼資料資訊沒有一緻性限制。這是由ClickHouse的架構特色決定的:1)徹底Share Nothing,各節點之間完全沒有互相依賴;2)節點完全對等,叢集中的節點角色統一,ClickHouse沒有傳統MPP資料庫中的前端節點、Worker節點、中繼資料節點等概念。ClickHouse的這種架構特色決定它可以靈活化、小規模部署,叢集可以任意進行分裂、合并,當然前提要求是感覺資料在叢集節點上的分布。在ClickHouse的架構形态下,使用者可以直接連接配接任意一個節點進行請求,當使用者發送DDL指令時,預設隻會在目前連接配接的節點執行指令。現實中如果使用者有一個100台機器的叢集,為了建立一個分布式存儲的表難道使用者需要依次連接配接每台機器發送DDL指令嗎?這會讓使用者抓狂的,并且存在多個DDL之間的沖突問題無法解決:使用者A和使用者B同時建立同名表但是表字段又不一緻,這肯定會讓系統陷入一個詭異的不一緻狀态。這個就是分布式DDL執行要解決的問題了,ClickHouse叢集的每個節點都會把收到的分布式執行DDL請求放入到一個公共的Zookeeper任務隊列中,然後每個節點的背景線程會依次任務隊列裡的DDL,保證了所有分布式DDL的串行執行順序。
主備節點狀态同步:ClickHouse叢集化部署中有三個邏輯概念需要先展開介紹一下Cluster、Shard和Replicate,這三者都是ClickHouse在叢集節點資源規劃上的概念。一個叢集可以包括若幹個Cluster,一個Cluster可以包括若幹個Shard,一個Shard又可以包含若幹個Replicate,一個Replicate就是一個特定的節點執行個體,使用者可以通過ClickHouse啟動的config.xml來配置這套節點規劃邏輯。基于這套邏輯,使用者可以把一個叢集規劃成若幹個Cluster,每個Cluster可自定義Shard數量,每個Shard又可以自定義副本數量。這三個概念隻作用于資源規劃上,單個存儲節點内部不同Cluster之間的表都是互相可見的。在資料分析線上化的大趨勢下,使用者的分析場景對RT和QPS有越來越高的要求。降低RT的一個核心能力是自定義表的Shard數量(Scale Out),傳統的MPP資料也都有這個能力。而提升QPS的一個核心能力是自定義表的Replicate數量,傳統的MPP資料庫都沒有表級别的自定義副本數能力,隻能做全庫的副本數配置。ClickHouse能做到表的Replicate數量自定義技術核心是它把主備同步邏輯放到了具體的表引擎中實作,而不是在節點級别做資料複制。目前隻有ReplicatedMergeTree表引擎可以自動做主備狀态同步,其他表引擎沒有狀态同步機制。如果使用者需要在多副本Cluster下建立其他表引擎,則需要在寫傳入連結路上配置多寫邏輯。ReplicatedMergeTree表引擎的同步包括寫入同步、異步Merge同步、異步Mutation同步等,它所有的同步邏輯都是強依賴Zookeeper。
分布式DDL執行鍊路
在介紹具體的分布式DDL執行鍊路之前,先為大家梳理一下到底哪些操作是可以走分布式DDL執行鍊路的,大家也可以自己在源碼中檢視一下ASTQueryWithOnCluster的繼承類有哪些:
- ASTCreateQuery:包括常見的建庫、建表、建視圖,還有ClickHouse獨有的Attach Table(可以從存儲檔案中直接加載一個之前解除安裝的資料表)。
- ASTAlterQuery:包括ATTACH_PARTITION、FETCH_PARTITION、FREEZE_PARTITION、FREEZE_ALL等操作(對表的資料分區粒度進行操作)。
- ASTDropQuery:其中包含了三種不同的删除操作(Drop / Truncate / Detach),Detach Table和Attach Table對應,它是表的解除安裝動作,把表的存儲目錄整個移到專門的detach檔案夾下,然後關閉表在節點RAM中的"引用",這張表在節點中不再可見。
- ASTOptimizeQuery:這是MergeTree表引擎特有的操作指令,它可以手動觸發MergeTree表的合并動作,并可以強制資料分區下的所有Data Part合并成一個。
- ASTRenameQuery:修改表名,可更改到不同庫下。
- ASTKillQueryQuery:可以Kill正在運作的Query,也可以Kill之前發送的Mutation指令。
DDL Query Task分發
ClickHouse核心對每種SQL操作都有對應的IInterpreter實作類,其中的execute方法負責具體的操作邏輯。而以上列舉的ASTQuery對應的IInterpreter實作類中的execute方法都加入了分布式DDL執行判斷邏輯,把所有分布式DDL執行鍊路統一都DDLWorker::executeDDLQueryOnCluster方法中。executeDDLQueryOnCluster的過程大緻可以分為三個步驟:檢查DDLQuery的合法性,把DDLQuery寫入到Zookeeper任務隊列中,等待Zookeeper任務隊列的回報把結果傳回給使用者。
檢查Query合法性這塊有一點值得注意:使用者在目前session的database空間下執行一個分布式DDL指令,真實執行DDL操作的節點會在什麼database下執行這個DDL呢?這裡的邏輯是:1)優先使用DDL Query中指明的database,2)當DDL Query中沒有指明database時,優先使用config.xml中的Cluster配置,每個Shard配置可以申明自己的default database,3)若前兩者都沒有,則使用目前session的database。
DDL Query的分發過程依賴Zookeeper,每一條需要分發的DDL Query轉換成一個如下的DDL LogEntry,然後把LogEntry序列化成字元串儲存到Zookeeper的任務隊列中。LogEntry中包含了SQL資訊,分布式執行目标Cluster對應的所有節點位址資訊,LogEntry的生成者資訊。Zookeeper的任務隊列位置是在config.xml配置中統一配置的(使用者可以讓多個ClickHouse叢集共用一套Zookeeper,預設路徑為/clickhouse/task_queue/ddl)。ClickHouse都是利用Zookeeper序列自增節點(Sequence Znodes)的特性實作來任務隊列,把每個DDL LogEntry儲存為任務隊列目錄下的一個Persistent Sequential Znode,相當于對每個DDL Query賦予了一個叢集自增的數字ID,在每個DDL LogEntry對應的Znode下面,還需要建立兩個status節點:active Znode用來管理目前有多少節點正在執行這個DDL,finished Znode用來管理目前有多少節點以及完成這個DDL并收集傳回的狀态資訊(包括Exception)。
struct DDLLogEntry
{
String query;
std::vector<HostID> hosts;
String initiator; // optional
static constexpr int CURRENT_VERSION = 1;
...
}
分布式DDL的執行鍊路如下圖所示:
1)節點收到使用者的分布式DDL請求;
2)節點校驗分布式DDL請求合法性,在Zookeeper的任務隊列中建立Znode并上傳DDL LogEntry(示例中為query-0000000115),同時在LogEntry的Znode下建立active和finish兩個狀态同步的Znode;
3)Cluster中的節點背景線程消費Zookeeper中的LogEntry隊列執行處理邏輯,處理過程中把自己注冊到acitve Znode下,并把處理結果寫回到finish Znode下;
4)使用者的原始請求節點,不斷輪詢LogEntry Znode下的active和finish狀态Znode,當目标節點全部執行完成任務或者觸發逾時邏輯時,使用者就會獲得結果回報;
這個分發邏輯中有個值得注意的點:分布式DDL執行鍊路中有逾時邏輯,如果觸發逾時使用者将無法從用戶端傳回中确定最終執行結果,需要自己去Zookeeper上check節點傳回結果(也可以通過system.zookeeper系統表檢視)。每個節點隻有一個背景線程在消費執行DDL任務,碰到某個DDL任務(典型的是optimize任務)執行時間很長時,會導緻DDL任務隊列積壓進而産生大面積的逾時回報。
DDL Query Task執行和清理
節點的背景線程在處理一個DDL LogEntry Task時,首先會檢查自己是否在DDL LogEntry的目标hosts中,這樣可以區分出不同Cluster上的DDL任務,在具體執行DDL之前把自己注冊到active Znode下,執行完成DDL之後會把傳回結果包括異常資訊寫回到finish Znode下。
具體的DDL任務執行邏輯還是複用單節點上的執行邏輯,節點之間在處理DDL任務時互不感覺。但是在ReplicatedMergeTree表引擎上有一些差異,ReplicatedMergeTree表引擎上的Alter、Optimize、Truncate指令都隻在主副本節點上執行,備副本節點拿到這類DDL任務時會直接丢棄掉,主副本節點在執行的過程中也會使用Zookeeper分布式鎖鎖住這個任務再執行。因為ReplicatedMergeTree表引擎上的資料修改鍊路有自己内部的一套機制保證主備互相同步,這樣避免了破壞主備之間的同步邏輯。下一章會詳細講ReplicatedMergeTree表主備之間的同步問題。
每個節點背景除了一個DDL任務消費線程外,還有一個過期DDL任務清理線程。清理線程會根據DDL任務隊列的容量以及過期時間來清理以及全部完成的任務,清理過程中依舊會使用基于Zookeeper實作的分布式鎖進行保護。
DDL Query Task狀态收集
使用者請求節點會不斷輪訓DDL LogEntry Znode下的active Znode和finish Znode,拉取執行狀态,随着輪訓次數的增加線程不斷增加sleep時間,最後等到逾時或者全部節點完成任務才把統計資訊傳回給用戶端。到這裡整個分布式DDL執行鍊路就已經全部完成啦,可以看出Zookeeper在分布式DDL執行過程中主要充當DDL Task的分發、串行化執行、結果收集的一緻性媒體。分布式DDL功能對Zookeeper不會造成很大的性能壓力,多個ClickHouse叢集可以共享同一套Zookeeper來完成分布式DDL任務。最後ClickHouse雖然用Zookeeper解決了分布式DDL串行化執行的問題,但是目前還沒有實作兩階段送出的邏輯,使用者需要注意分布式DDL如果失敗可能會導緻節點間的狀态不一緻。
ReplicatedMergeTree主備同步
上一章介紹的分布式DDL功能對Zookeeper的依賴情況還是比較輕量級的,接下來介紹的ReplicatedMergeTree表引擎對Zookeeper的依賴可以說是所有表操作全方面的依賴,真實叢集中大量的ReplicatedMergeTree表會對Zookeeper造成非常大的請求壓力,需要使用者關注Zookeeper的運維。
ReplicatedMergeTree表引擎實作的主備同步和傳統主備同步有很大的差異:1)它不是一個(搶主,主節點執寫入更新,備節點同步follow)的模型,ClickHouse的主節點和備節點都可以寫,同步是雙向的;2)它不是實體同步,ClickHouse沒有基于實體檔案的WAL;3)它的邏輯同步日志粒度是MergeTree的Data Part級别的(沒有單條記錄的同步日志),包含Data Part的增、删、改。ReplicatedMergeTree表的Data Part Log主要包含以下幾類:
enum Type
{
EMPTY, /// Not used.
GET_PART, /// Get the part from another replica.
MERGE_PARTS, /// Merge the parts.
DROP_RANGE, /// Delete the parts in the specified partition in the specified number range.
CLEAR_COLUMN, /// Drop specific column from specified partition.
CLEAR_INDEX, /// Drop specific index from specified partition.
REPLACE_RANGE, /// Drop certain range of partitions and replace them by new ones
MUTATE_PART, /// Apply one or several mutations to the part.
};
這些類型的Log中部分隻有主節點可以生成("MERGE_PARTS"),部分是主備節點都可以生成的("GET_PART")。"GET_PART"日志是節點資料寫入時産生的,且主備節點都可以寫,每個節點寫入資料後上傳一個"GET_PART"日志到Zookeeper通知其他副本節點從自己這裡下載下傳資料。這裡大家可能會疑惑:既然主備節點都可以寫入,那為什麼主備節點不能獨立進行merge或者mutation?我認為核心原因有兩個:
1)降低代碼邏輯複雜度,MergeTree表引擎有兩類背景異步任務(Merge/Mutation),同時又有所有節點可寫的設定,這兩個邏輯融合到一起的話複雜度會爆炸,ClickHouse的核心實作中是把寫入和異步動作的鍊路完全解耦開的。主節點負責分發各種異步任務到Zookeeper上的任務隊列,Shard下的所有節點觀察任務隊列進行follow執行。當萬一某個其他節點上的資料和主節點不一緻無法完成某個異步任務時,還有保底方法是讓它直接從主節點去下載下傳完成merge / mutation的Data Part。
2)MergeTree結構的表引擎有衆多的變種merge邏輯(ReplacingMergeTree、CollapsingMergeTree等),再加上異步mutation的機制,多副本之間獨立merge / mutation的話,副本間的資料視圖同步進度就會完全失控(使用者可能需要停寫很長時間再加上手動Optimize才能達到副本間一緻)。
上一篇系列文章中,我介紹過MergeTree表對Data Part的管理方式,要實作基于Data Part Log的同步,首先要確定節點之間的Data Part有統一的命名體系,而決定Data Part命名的核心因素是每一批資料寫入時被賦予的blockNumber(資料的寫入版本号),ClickHouse的寫傳入連結路利用了Zookeeper來生成全局一緻的blockNumber序列。其次為了資料一緻性保證,ClickHouse把ReplacingMergeTree表引擎中的所有Data Parts都注冊到了Zookeeper上(包括它們的列資訊和checksum),最終本地資料都要以Zookeeper上的狀态為準。
ReplicatedMergeTree同步寫入
ReplicatedMergeTree的寫傳入連結路分為三步:
1)把資料寫入到本地的臨時Data Part中,
2)從Zookeeper上申請自增的blockNumber序列号,
3)commit臨時Data Part,生成一條"GET_PART"的同步日志上傳到Zookeeper任務隊列中。和分布式DDL執行任務隊列不同,每一個ReplicatedMergeTree表引擎在Zookeeper上都有一個獨立的Znode,這個Znode的路徑可以在建表示配置。下圖展示了示例dm_t_pecust_lab_part表的Znode目錄結構,每個Shard和Replicate邏輯都有自己獨立的目錄空間。
一次正常的ReplicatedMergeTree批量寫入首先會把寫入的資料按照資料分區進行拆分,然後依次處理拆分後的每個資料Block。把資料Block寫入到存儲的臨時Data Part後,ClickHouse需要從Zookeeper中擷取下一個全局的blockNumber,這部分邏輯主要在StorageReplicatedMergeTree::allocateBlockNumber函數中,核心是調用Zookeeper生成一個Ephemeral Sequential Znode來擷取全局唯一的序列(這裡的"全局"是單個資料分區級别唯一,跨資料分區是可以重合的)。最後是commit這個Data Part,commit的過程需要完成一系列的"檢查動作"最後上傳一個"GET_PART"類型的Log到Shard對應的Zookeeper目錄下的的log Znode下,其他副本通過觀察Zookeeper會異步來拷貝寫入的Data Part。前序的"檢查動作"目前包括,檢查本地表的meta(列資訊)版本是否已經落後于Zookeeper上的狀态,注冊寫入Data Part的columns資訊和checksums到Data Part的Znode下。從這裡可以看出一次Batch寫入的過程和Zookeeper互動的次數不下10次,要是Batch資料跨10個資料分區的話那就是100次。老話重提一下:使用ClickHouse時一定要做Batch寫入并且按照資料分區提前聚合。
兩階段送出的設計中,有個普遍問題是往Zookeeper上送出"GET_PART" Log時zk session斷開或者逾時了怎麼辦?本地的Data Part是Commit還是Rollback?ClickHouse在這裡的解法是Commit Data Part,并抛錯給使用者重試寫入資料,同時把Data Part丢到一個異步檢查線程的任務隊列中,異步檢查線程會等待重連Zookeeper,檢查本地的Data Part是否注冊在Zookeeper上,如果沒有則會移除本地的Data Part。相當于一個異步的資料修複保護手段,在其他兩階段送出鍊路中碰到相同的問題也都是依賴這個異步檢查線程來進行修複。具體的異步檢查線程代碼在ReplicatedMergeTreePartCheckThread::run函數中,有興趣的同學可以仔細看一下這塊代碼。
ReplicatedMergeTree的寫傳入連結路有關的還有幾個開關值得注意:
use_minimalistic_part_header_in_zookeeper,這是一個降低Zookeeper壓力的配置(預設關閉)。開啟之後每個新寫入的Data Part不再注冊自己的columns資訊和checksums到Zookeeper上。而是壓縮成Hash值寫到Data Part的Znode data中。
insert_quorum,這個開關會強迫寫傳入連結路檢查資料同步的副本數達到要求才能成功傳回(預設是0)。開啟之後寫入節點在Commit Data Part時還會建立一個Shard級别的quorum/status Znode,其他節點同步完資料之後需要更新到quorum/status,寫入節點這邊通過Watch機制收到通知再傳回客戶的寫入請求。這個開關不建議開啟的,因為寫傳入連結路的RT肯定會明顯上升,同時因為quorum/status Znode是Shard級别建立,不能再多個副本并行寫入。
insert_deduplicate,簡單實用的資料去重功能(預設開啟)。ClickHouse會對每次收到的批量寫入資料計算一個Hash Value,然後注冊到Zookeeper上。後續如果出現完全重複的一批資料,寫傳入連結路上會出現Zookeeper建立重複節點異常,使用者就會收到重複寫入回報。當然批量寫入的Hash Value儲存是有視窗大小限制的,有統一的異步背景線程會清理這些Zookeeper上的過期記錄,清理的邏輯代碼在ReplicatedMergeTreeCleanupThread::run函數中,有興趣的同學可以仔細看一下這塊代碼。
ReplicatedMergeTree異步Task
介紹完ReplicatedMergeTree的整個同步寫入過程,接下來就是多副本之間的異步同步過程了,ClickHouse為每個ReplicatedMergeTree表引擎執行個體建立了非常多的異步Task,所有Data Part的生命周期管理由這些異步Task共同完成。因為文章篇幅原因下面隻會依次簡單介紹每個Task所做的事情以及其中邏輯特别複雜的點,希望讀者對ReplicatedMergeTree主備同步的邏輯複雜度有一個簡單的了解:
StorageReplicatedMergeTree::queueUpdatingTask
同步Zookeeper中Shard級别下的Data Part Log任務隊列資料到自己的Znode任務隊列中,同時在自己的Znode下維護更新目前正在處理的log_pointer(目前已經拷貝過的最大log Id)和min_unprocessed_insert_time(近似評估寫入的延遲時間)資訊,最後會把任務放到節點的RAM隊列中。從Shard級别的公共任務隊列遷移資料到節點自己的任務隊列,核心問題是高頻寫入時公共任務隊列裡的任務釋出會非常頻繁,需要盡快清理公共隊列,防止公共任務隊列膨脹,因為所有節點都在輪訓讀取公共隊列(Zookeeper的任務隊列無法增量讀取)。
StorageReplicatedMergeTree::mutationsUpdatingTask
從Zookeeper的Shard級别下的Mutation任務隊列同步資料到節點的RAM狀态中,這裡沒有再為每個節點維護自己的内部Znode隊列,Mutation是相對低頻的操作,公共的任務隊列不會有資料積壓。另一個問題是Mutation操作如此低頻,ClickHouse如何排程Task運作呢?這裡核心的機制也是依賴Zookeeper的Watch機制來通知ClickHouse的BackgroundSchedulePool排程起工作Task,包括上一個queueUpdatingTask也是相同機制被排程。
StorageReplicatedMergeTree::queueTask
上面兩個Task都是從Zookeeper中同步任務到RAM的任務隊列中,而且Task都是單線程排程執行。queueTask則是負責從RAM任務隊列中消費執行具體的操作,并且會有多個背景線程被排程起并行執行多個任務。由于queueTask會被并行執行,運作的過程中有一個問題是如何從RAM中的任務隊列裡找到下一個要執行的任務?如果Task A是merge Data Part 1 和 Data Part 2, 而Data Part 2的下載下傳任務正在另一個線程中執行,這時Task A就不能排程執行。ClickHouse在RAM狀态中追蹤了所有正在執行的任務即将産生和依賴的Data Part,可以保證有資料依賴關系的任務串行化執行。對于"GET_PART"類型的任務,Task執行邏輯會嘗試從遠端節點下載下傳資料到本地,同時如果有quorum數量要求的話更新quorum統計資訊。這裡ClickHouse對節點下載下傳遠端資料的并行數做了控制,詳見參數replicated_max_parallel_fetches、replicated_max_parallel_fetches_for_[table|host]。
對于"MERGE_PARTS","MUTATE_PART"的任務,節點首先會嘗試在本地進行實際的merge或者mutation動作,但是當本地的Input Data Part存在缺失或者損壞時,ClickHouse可以采用保守政策:嘗試從遠端下載下傳merge完成的Data Part。當Input Data Part的資料量很大同時這個任務建立時間又很長時(遠端大機率已經存在Output Data Part),ClickHouse會直接選擇從遠端下載下傳的政策來跳過本地merge / mutation加速任務執行。在大規模資料場景下,每次merge、mutation的開銷都是非常大的,配置隻選擇主副本完成merge、mutation任務,而讓其他副本直接從遠端下載下傳可以大幅減輕叢集的負載。
當一些極端場景出現,遠端的結果Data Part N也無法下載下傳時(一般是這個任務對應的遠端Data Part N再次發生了資料變更變成了Data Part M),節點會把目前這個任務放回到任務隊列的尾端,讓它延遲執行,然後等待生成Data Part M的任務到來,隻要從遠端直接下載下傳到Data Part M,通過Merge Tree的版本機制節點就可以直接跳過生成Data Part N的任務,因為這個任務生成的資料已經“過時”了,可以直接忽略。
StorageReplicatedMergeTree::mergeSelectingTask
這個Task的隻有主副本節點會排程,它負責不斷選擇下一次要進行merge / mutation的Data Parts,把具體的merge / mutation的任務日志釋出到Zookeeper的任務隊列上。主節點在選擇需要merge的Data Parts時,主體邏輯和上一篇系列文章中提到的MergeTree表的啟發式挑選規則一緻,唯一有一點不同的是這裡還需要去從Zookeeper上擷取實時寫入資料的同步狀态的,新寫入的Data Part隻有同步到全部副本節點後才可以參與merge。mergeSelectingTask是一個背景的任務釋出者(上一節中的資料寫傳入連結路也會釋出任務),而queueUpdatingTask和mutationsUpdating Task作為任務消費者會把任務拉到節點的RAM任務隊列中,最後由queueTask去執行RAM隊列裡的任務。以上4個Task配合完成了ReplicatedMergeTree最核心的主備異步同步邏輯。實際上這4個Task在運作的過程中,存在着資料依賴關系,彼此之間會有一些同步調用或者異步喚醒排程的邏輯,這裡不再展開講這個排程流程。
StorageReplicatedMergeTree::movePartsTask
這個異步Task主要是配合ClickHouse的存儲分層設計,當高性能(SDD)的存儲空間快用滿時,它會不斷自動地把資料往更低級(HDD)的存儲上去遷移。目前Task是每個節點獨立工作,不感覺主備狀态,不需要和Zookeeper互動。但是後續如果要做基于DFS(分布式檔案系統)的存儲分層的話,Data Part遷移的邏輯也會需要考慮主備狀态的問題,同個Data Part隻需要遷移一次。基于DFS的存儲分層功能實作代價還比較高,因為ClickHouse中沒有File System層面的抽象設計,估計社群也不會很快支援。
**
StorageReplicatedMergeTree::mutationsFinalizingTask**
這個Task的作用是異步去更新目前副本的mutation任務隊列執行進度,它需要檢查目前節點中所有的Data Part以及正在同步的Data Part的資料版本都以及超過mutation。
ReplicatedMergeTree表引擎中除了上述的一些異步Task在排程運作之外,還有一些背景線程在一直工作:
- ReplicatedMergeTreeCleanupThread是負責清理Zookeeper上的過期資料,上述所有異步Task在Zookeeper上的過期資料都由該線程統一清理。
- ReplicatedMergeTreeAlterThread是負責監聽Zookeeper上Shard級别的表列資訊變更,并執行實際的Alter操作。在ReplicatedMergeTree表上的Alter操作流程和第一節中将的分布式DDL執行很像,當某個副本節點收到Alter指令時,它就現在本地完成Alter操作,然後把新的表結構版本釋出到Zookeeper上,等待其他副本follow執行。
- ReplicatedMergeTreePartCheckThread是專門處理在兩階段送出過程中如何和Zookeeper失聯,就把對應的Data Part丢到一個異步Check隊列裡,由這個線程去延遲檢查和Zookeeper的狀态是否一緻并修複資料。
- ReplicatedMergeTreeRestartingThread負責在Zookeeper上的心跳注冊管理。
這一章中講到的所有ReplicatedMergeTree表引擎的異步Task以及在Zookeeper上的任務隊列、心跳注冊、狀态存儲都是ReplicatedMergeTree表級别獨立的,叢集裡的ReplicatedMergeTree表數量、副本數量、寫入流量都會影響Zookeeper的服務壓力(壓力山大),這也是ClickHouse在表引擎級别實作副本邏輯的代價。大家在大規模叢集環境中需要謹慎運維Zookeeper。最後ClickHouse在引入異步Mutation機制之後,對副本同步鍊路的複雜度有比較大的影響,mutation和merge的處理邏輯最大的不同是mutation一次涉及的Input Data Parts幾乎是全表,它不能像merge任務一樣一次把所有的Input轉化到Output,mutation任務需要對Input Data Parts拆分進行挨個操作,任務執行的生命周期特别長,并且Input Data Parts可能動态變化。
結語
ClickHouse核心分析系列文章:
[MergeTree的存儲結構和查詢加速
](
https://developer.aliyun.com/article/761931?spm=a2c6h.13148508.0.0.5cfd4f0eL05mWn) MergeTree的Merge和Mutation機制希望通過核心分析系列文章,讓大家更好地了解這款世界領先的列式存儲分析型資料庫。