ByteHouse雲數倉版是位元組跳動資料平臺團隊在複用開源 ClickHouse runtime 的基礎上,基於雲原生架構重構設計,並新增和最佳化了大量功能。在位元組內部,ByteHouse被廣泛用於各類實時分析領域,最大的一個叢集規模大於2400節點,管理的總資料量超過700PB。本分享將介紹ByteHouse雲原生版的整體架構,並重點介紹ByteHouse在查詢上的最佳化(如最佳化器、MPP執行模式、排程最佳化等)和對MySQL生態的完善(基於社群MaterializedMySQL功能),最後結合實際應用案例總結最佳化的效果。
在2023雲資料庫技術沙龍 “MySQL x ClickHouse” 專場上,火山引擎ByteHouse的研發工程師遊致遠,為大家分享一下《ByteHouse雲數倉版查詢最佳化和MySQL生態完善》的一些工作。
本文內容根據演講錄音以及PPT整理而成。
遊致遠,火山引擎ByteHouse資深研發工程師,負責ByteHouse雲數倉版引擎計算模組。之前先後就職於網易、菜鳥集團、螞蟻集團,有多年大資料計算引擎、分散式系統相關研發經歷。
今天我主要分享的內容大綱,分為下面這四個部分。首先主要是跟大家講一下ByteHouse雲數倉版的背景和整體架構、然後重點講下查詢引擎上做的最佳化和完善 MySQL 生態的一些工作,最後是總結。
Clickhouse 是基於 shared nothing 架構,這種架構也帶來了比較極致的效能。位元組跳動的話,從2018年就開始線上上 使用 Clickhouse,然後到現在已經是非常大的機器量和資料量。但是 Clickhouse 的shared nothing 架構,也給我們帶來了很大的困難,主要是資料的擴縮容比較難,包括儲存和計算資源的繫結,導致我們想做一些彈性的伸縮也比較難。然後讀寫不分離帶來的影響,以及在公共叢集上中小業務的查詢的影響。
為了徹底解決這個問題,然後我們在2020年的時候,開始做一個基於雲原生架構的Clickhouse,當時內部的代號叫CNCH,現在在火山上叫ByteHouse雲數倉版。然後現在CNSH在內部也是有非常大的使用規模,到2022年的時候,我們決定把這個回饋給社群,當時跟 Clickhouse 社群也進行了一些討論,後來覺得架構差異太大,然後就單獨以ByConity專案開源了,在今年1月份已經在GitHub上開源了。歡迎大家去關注和參與一下。
下圖就是 ByteHouse雲數倉版的整體架構,這是比較經典的架構。服務層負責就是資料,事務查詢計劃的協調,資源的管理。中間這層是可伸縮的計算組,我們叫做virtual warehouse(VW),也叫虛擬數倉,業務是可以按virtual warehouse進行隔離,相互不會影響,可以隨意的擴縮容,是一個無狀態的計算資源。最下面是資料儲存,我們是抽象了虛擬的檔案層,可以支援HDFS,以及還有物件儲存S3等。當然在實際查詢的時候,就是我們也會做一些熱資料的local cache.
下面重點來講我們在查詢引擎的最佳化。我們知道ClickHouse的單機執行非常強,然後這個是2021年的ClickHouse的單機執行邏輯,非常簡單的count(*)的聚合運算。ClickHouse 首先會生成一個邏輯計劃,叫QueryPlan。這裡可以透過 EXPLAIN 看到每一步,就query plan step,就是讀表,然後做聚合。
然後再透過 QueryPlan 會生成一個 QueryPipeline。這個過程中可以看到,query plan step被翻譯成了QueryPipeline裡面的一步,叫做processor,或者叫做物理運算元。
ClickHouse的單機模型其實是非常的強的,然後整體Pipeline驅動模式可以參考下面這個圖,這裡就不再具體展開。
接下來我們就看下另外一個場景,分散式執行。這是一個分散式表,然後有三個分片。做一個簡單的count,在ClickHouse這塊的話,就是把它改寫成三個本地執行的子查詢,然後分別計算,生成中間的Partial merge result,最後在coordinator節點上進行聚合,最後生成一個完整的結果返回給使用者。
這個模型特點就是非常的簡單,然後實現起來也是非常高效,但是在實際業務中也發現一些缺點。首先對於兩階段的話,第二個階段的計算如果比較複雜,Coordinator 的計算壓力會非常的大,很容易出現瓶頸。在聚合運算的時候,比如count distinct的經常會出現OOM或者算不出來,它整個架構是沒有Shuffle的。如果有Hash Join,右表的大小不能放到一個單機的記憶體裡面,基本上就是跑不出來。整個計劃層的話,下發ast或者sql的方式,表達能力是非常有限的,我們之前是想基於這個做一些複雜最佳化,也是不太好做,靈活度也比較低。最後的它只有一個基於規則的最佳化,像一些比較重要的join reorder的排序也是沒法做。
基於上面提到的問題,我們是基本上重寫了分散式執行的查詢引擎。主要做了兩點,一個是就是支援多階段執行,這也是大部分主流的MPP資料庫,以及一些數倉產品的做法。第二個我們自研的整個最佳化器。下面是一個比較完整的執行圖。可以看到,相比於剛才二階段執行,一個查詢過來之後,他的第二階段就是Final agg可以在兩個節點上了。TableScan做完之後,透過一定的規則進行shuffle。這個是透過exchange。然後最後的結果再彙集到Coordinator。當然這裡還有ByteHouse雲數倉的一些其他元件,這裡不再細講。
為了支援多階段的執行模型,我們就引入了PlanSegment。簡單說就是每一個worker上的一段邏輯的執行計劃。從實現上來講,它其實就是單機計劃的QueryPlan,再加上輸入輸出的一些描述。然後這邊就是PlanSegment的介紹,輸入的PlanSegment和輸出要到輸出到哪個PlanSegment。
瞭解PlanSegment之後,可以就會問這個PlanSegment是從哪裡來的。其實剛才介紹了,就是透過最佳化器進行計劃生成和最佳化得來的。整體的一個流程就是從Parser把一個SQL變成了一個AST(抽象語法樹),然後在最佳化器這個模組裡面,在interpreter裡面變成了一個PlanSegmentTree,切分成一組PlanSegment再下發給各個worker。
最佳化器,主要就是查詢計劃的變換。分為rule based optimizer和cost based optimizer,就是基於規則和基於代價。基於規則的話,我們是實現了一個種基於visitor的一個改寫框架,主要做一些全域性的改寫,支援從上到下,從下到上的方式,包括一些condition的下推,還有SQL指紋,這種像需要正則化SQL的。我們還支援基於區域性的pattern-match改寫,例如。發現兩個Filter是相連的,那就會到合併到一起,Projection也是類似的做法。
CBO,下面是一個通用的CBO的框架。當一個查詢計劃過來的時候,我們會透過optimizer Task的規則,和Property來不斷的擴充這個grouping。中間這個是memo,記錄等價的QueryPlan。然後把所有的QueryPlan生成之後,根據計算的代價,最後選擇代價比較低的作為輸出。當然在具體實現的時候,其實是有很多考慮,會包括生成的時候怎麼降低等價plan的數量,以及怎麼在生成的同時選擇分散式計劃最優方案。
當最佳化器生成了PlanSegment的時候,就涉及到該如何下發。下面就是我們的排程器模組。當查詢生成完一組PlanSegment之後,我們可以根據排程的型別,現在我們主要是MPP的多階段執行。就會把它生成一個子圖一次下發,後面也會考慮其他的一些排程方式,根據任務型別,包括類似於Spark的BSP,或者是分階段排程。生成完這個一個子圖的排程之後,馬上就要選擇PlanSegment到哪些worker執行?
這裡的話。就是剛才講service層,congresource manager拿各個worker層的負載資訊,排程source的話,我們是主要考慮快取的親和度;然後排程計算plansegment的話是worker可以純無狀態,我們是主要考慮負載,就是儘量保證負載均衡來進行排程。這裡也是儘量避開一些慢節點,以及一些已經死掉的節點。當然我們也在做其他的排程的方式,就是一些資源的預估和預算。這個具體解決問題可以後面再講。我們生成完PlanSegment,然後發給worker之後,它的執行就是剛才講的clickhouse的單機執行了。
剛剛提到一點,就是資料的就是的傳入和傳出,這個是依賴於Exchange模組。Exchange就是資料在PlanSegment的例項之間進行資料交換的邏輯概念,從具體實現上的話,我們是把它分的資料傳輸層以及運算元層。
資料傳輸層的話,其主要是基於定義Receiver/ Sender的介面,然後同程式傳輸基於佇列,跨程式是基於基於BRPC Stream,支援保序、狀態碼傳輸、壓縮、連線池複用。連線池複用、簡單來說,就是把大叢集上的兩個節點之間的只建立一個連線,所有的查詢都在這個連線上通訊,當然我們是連線池,所以實際上是兩個節點之間是固定數量的一個連線,這樣會比單連線的穩定好更高。
運算元層的話,我們是支援了四種場景。一個是一對多的Broadcast。然後多對多的Repartition,以及是多對一的Gather,一般在本程式之間的Round-Robin。這裡面也做了一些最佳化,包括Broadcast怎麼樣避免重複的序列化,然後Repartition怎麼提升效能,以及sink怎麼攢批。在大叢集下,怎樣透過一個ExchangeSouce讀取多個receiver的資料,來降低執行緒數。
這裡是比較高階的一些最佳化點,第一,RuntimeFilter 就是在執行期間生成的動態filter,比如這是兩張表的一個等值join。我們可以在右表構建雜湊table的時候,會生成一個bloom filter(或者其他型別的filter)。然後把各個worker上的bloom filter的收集後merge成一個,然後再發給左表所在的worker,這樣在左表進行table scan的時候,可以過濾掉非常多不必要的資料,然後也可以節省一些計算的資源。這個的話需要最佳化器整個參與決策,因為生成和傳輸過程也是有代價的,看哪個代價更低。或者他還會判斷一下過濾能力。
另外就是在執行層的話,我們有一些壓縮演算法的最佳化,就比如說表級別的全域性字典。我們知道社群有一個低階數型別,它的字典是part級別的,已經可以在一些計算上做到不解壓計算了,當我們擴充套件成表級別的時候,大部分的計算都可以直接在編碼值上或者在字典上進行,就完全不需要去解壓資料了,甚至傳輸也可以傳輸編碼後資料的。函式計算,聚合運算也是,這塊在TPCDS上應該有20%的提升。
其他的最佳化,這裡可以簡單的說一下,包括Windows運算元的並行化,然後Windows裡面 Partition 的 top下推;公共表示式的複用;以及現在多階段模型下,對社群為兩階段模型實現的aggregation、join的運算元做了一些重構,為了更好的適應這個模型。我們還支援Bucket Join、簡單查詢上併發效能的最佳化。最後就是ClickHouse單機模型的缺點,就是它每個Pipeline是獨立的執行緒池,當併發比較高的時候執行緒會比較多,上下文的切換的開銷比較大。我們會把它做成協程化,避免過多的執行緒。
這是整體的一個效果。然後在社群的兩階段,我們透過改寫,能跑完26個SQL。我們在多階段執行和最佳化器完成之後,基本上是整個TPC-DS的99個SQL都是可以跑完的,效能也是得到了極大的提升。
然後下面講一下過程碰到的挑戰,以及沒有解決的問題。第一個就是所有平行計算框架的老大難問題:資料傾斜。如果比較有熱點key,或者聚合件裡面的key過少的話,即使有再多的worker,最後也只會在一個worker上進行計算。計劃層,其實是可以做兩階段聚合的調整,然後把key過少的問題可以解決,但是熱點key的問題還是很難解決,其實可以在執行層做一些自適應的執行,這個還是在探索階段,可能類似於Spark的AQE,但是因為MPP的話有很多限制,做不到這麼完善。
第二個挑戰,超大的MPP叢集的問題。業內的話一般超過200個的MPP叢集,就是會碰到一些比較多的慢節點的問題,或短板效應導致線性度急劇下降,穩定性也會下降。我們在內部已經有大概將近800個節點的計算組,然後可能馬上就會有超過上千個節點的一個計算組。是要怎麼樣保證這種大超大MPP叢集的穩定性和效能,我們做了一些自適應的控制,提高整體的穩定性。就我剛才講的自適應排程、資源的預估和預佔是一個方面,另外就是限制每一個查詢的複雜度和使用資源,避免大查詢導致把某個work的資源就是佔的過滿,然後導致的慢節點。最後一個就是對使用者無感的一個VW的一個自動劃分,劃分一些小的子集,這個子集的話是固定的,是為了保證cache的親和度,我們會根據查詢的大小來自動的選擇,這個也算規避了超大的問題。
最難的還是怎麼構建容錯的能力,在這種大叢集情形下,如果假設每一個節點的錯誤率為e的話,那節點數量為N的話,那執行正常機率就是(1-e)^N。節點數量擴大,錯誤率就會指數級上升。我們在探索就是query的狀態的snapshot,類似於flink非同步的snapshot的方案,可以構建一定的恢復能力,另外一個我們是有bucket table,就是會有一些計算是在閉合在bucket內部的,某一個bucket失敗可完全不影響其他bucket,是可以單獨去重試的。這是我們碰到的兩個主要的挑戰。
這個專場是關於MySQL和ClickHouse,我們也講一下ByteHouse在MySQL生態上做的一些事情。我們知道把從MySQL資料匯入到ClickHouse的話,主要現在有三種方案。一種是ClickHouse的MySQL表引擎,你可以直接透過資料庫引擎建一個MySQL的外表,然後用insert select的方式一次性的把資料匯入,但是有資料量的限制不能太大,也不能持續的同步。其實在GitHub上有開源的工具,它是基於binlog同步的。但這個操作是比較複雜的,然後並且在已經停止更新了。社群最近是開發了一個materialized MySQL的一個功能。這個我認為是未來的一個最佳實踐。
Materialized MySQL的話,它的原理也比較簡單。使用者的話就是建立一個 Materialized MySQL的資料庫引擎,這樣ClickHouse會有後臺的一個執行緒,然後非同步的去拉取MySQL的Binlog。然後會寫到一個Replacing MergeTree裡面。這個為什麼要用replacing MergeTree,因為它是可以進行逐漸的去重。它雖然是那種非同步的,但也是可以近似的完成去重工作。然後ClickHouse是做了一些trick,就是在這個replacing MergeTree裡面可以給同步的Binlog加兩個欄位,一個是sign,一個是version,然後後續replacing MergeTree,就依靠這兩個欄位會進行一些去重,sign表示的是。資料的是否刪除,version代表的是這次資料的版本,如果你加了final的話,它會就是在查詢的時候,會用最高的版本覆蓋低的版本。
這個介紹大概的使用,使用者從Materialized MySQL的資料庫引擎。在ClickHouse裡面建立,然後在MySQL裡面透過insert語句去寫入各種資料,你在ClickHouse裡面可以查到,當然還有一些沒有展示,就是你在Materialized MySQL裡面去建立一些表。然後也會動態的在ClickHouse這邊生成,就是DDL的也可以同步過來的。剛才我為啥說這個是未來的最佳實踐,因為這個還是實驗性的功能,它會有很多不完善的地方。
首先,它是不支援不相容的DDL,只要有一個報錯,然後整個同步就停止了,然後停止又是悄無聲息,你沒有辦法去手動的去觸發它的再同步。第二點,就是社群的Materialized MySQL的replacing MergeTree其實是一個單機引擎,只能在單點上同步,如果出現一個單節點的故障的話,就是高可用會成為問題,另外單節點也會有吞吐量的限制。第三個就是剛才講的運維的困難,看不到同步的狀態、現在同步的資訊、以及沒有同步重啟的任務。
然後ClickHouse的做了一個CNCH的Materialized MySQL的資料庫引擎,也是把引擎給雲化,修復了社群的一些缺陷,真正做到的生產可用。它的原理主要就是透過我們的service層,按照表的力度去在各個worker上去排程執行緒,寫到我們的唯一鍵引擎裡面。
現在講一下解決的這些問題,第一個有非常詳細的系統表,可以看到現在執行的狀態。然後也有停止啟動重啟的各種指令,就是這個整個運維是可用了。我們支援按表多worker的併發消費。因為是基於原生的架構,存算分離,如果單個work失敗,可以馬上自動的重新排程Rebalance。最後我們是基於唯一鍵引擎,它是為讀最佳化的,就查詢效能會更好。最後是支援配置跳過不相容的DDL。做了這些工作之後,我們這個引擎基本上是可以說是生產可用了。
總結一下,今天的一些主要的內容吧,就是主要給大家講了一下,ByteHouse雲數倉版的背景以及整體架構。第二部分是重點講了在查詢引擎上的整體設計和最佳化點。最後講了一下我們生產可用的雲數倉版的Materialized MySQL的表引擎,為了完善MySQL生態做的一些工作。