位元組跳動資料湖在實時數倉中的實踐

陶然陶然發表於2023-02-01

   一、實時數倉場景介紹

  為了資料湖更好的落地,我們在落地之前與業務做了一些深入的溝通,並根據不同業務的特點主要分為了三個場景:

  1)場景一典型的業務主要是短影片和直播,它的資料量級一般都比較大,例如大流量的日誌資料,其計算週期一般是自然的天、小時或者分鐘級別的,實時性的要求一般是五分鐘內,主要訴求是批流的複用,可以容忍少量資料的不一致。

  2)場景二一般是直播或者電商的部分場景,資料量一般是中等體量,為長週期計算,對於實時性的要求一般是一分鐘以內,主要訴求是低成本的資料回溯以及冷啟動。

  3)場景三主要是電商和教育的一些場景,一般都是小規模的業務資料,會對資料做全量計算,其實時性要求是秒級的,主要訴求是強一致性以及高QPS。

  我們結合這些特點基於資料湖做了一些成套的解決方案,接下來我們會基於實際的一些場景和案例一一去了解。

   二、實時數倉場景初探

  本節我們討論的是位元組實時數倉場景的初探以及遇到的問題和解決方案。

  坦白地講,在最初落地時大家對資料湖能支援線上生產的態度都是存疑的,我們開始的方案也就比較保守。我們首先挑選一些對比現有解決方案,資料湖具有凸顯的優勢的場景,針對其中的一些痛點問題嘗試小規模的落地。  

  離線數倉有兩個比較大的問題:

  一是時效性問題,現狀一般是天或小時級;

  二是更新問題,例如需要更新某個小時內的部分資料,現狀需要將分割槽內資料全部重刷,這樣的更新效率是很低的。

  對於這樣的場景,資料湖兼具時效性和高效更新能力。同時相對於實時數倉來說,資料湖可以一份儲存,批流兩用,從而直接進行高效的資料分析。

  基於以上對業務的分析,我們會按照以下步驟來做一線的落地。

  1、基於影片後設資料的落地方案  

  看上圖我們原有的方案有三個Hive表,Hive Table 1,2,3。對於整個鏈路來說我們會把左邊MySQL資料來源的資料導到Table 1中,右邊Redis的資料導到Table 2中,然後將兩個表做Join。這裡存在兩個比較大的問題:

  一是高峰期的資源佔用率較高,因為天級 Dump 資料量較大,且都集中在凌晨;

  二是就緒時間比較長,因為存在去重邏輯,會將 T-1 天分割槽的資料和當天分割槽的資料合併去重計算後落到當天(T天)的分割槽。  

  我們透過引入Hudi把天級的Dump分攤到每個小時進行Upsert。由於Hudi自身可以支援去重的邏輯,我們可以將Table 1看成一個實時的全量資料,當小時級別(例如23點)的資料一旦Upsert完成之後,我們就可以直接進行下游的Join邏輯,這樣的話我們可以將資料的就緒時間提前3.5個小時左右,高峰期的資源消耗可以減少40%。

  2、近實時資料校驗方案  

  對於實時場景來說,當實時任務進行一個比較頻繁的變更,比如最佳化或者新增指標的改動,一般需要校驗實時任務的產出是否符合預期。我們當前的方案是會跑一個小時級別的Job,將一個小時的資料從Kafka Dump到Hive之後再校驗全量資料是否符合預期。在一些比較緊急的場景下,我們只能抽查部分資料,這時候就對時效性的要求就比較高。在使用基於的Hudi 方案後,我們可以透過Flink將資料直接Upsert到Hudi表中,之後直接透過Presto查詢資料從而做到全量資料近實時的可見可測。從線上效果來看可以大大提高實時任務的開發效率,同時保證資料質量。  

  在以上探索過程中遇到了比較多的問題,第一個問題就是易用性比較差,運維成本和解釋成本比較高。對於易用性這一部分,我們起初是透過指令碼來提交SQL,可以看到SQL中的引數是比較多的,並且包含DDL的Schema,這在當列數比較多的情況下是比較麻煩的,會導致易用性較差,並且對業務側來說也是不可接受的。  

  對於以上問題我們做了一個針對性的解決方案,首先我們對之前的任務提交方式替換為了純SQL化提交,並且透過接入統一的Catalog自動化讀取 Schema和必要引數,入湖的SQL就可以簡化為如圖的形式。

   三、典型場景實踐  

  接下來讓我們看位元組目前基於Hudi的實時數倉整體鏈路。

  可以看到,我們支援資料的實時入湖,例如MySQL,Kafka透過Flink可以直接落到Hudi;也支援進行一定的湖內計算,比如圖中左下將MySQL資料透過Flink匯入Hudi進一步透過Flink做一些計算後再落到Hudi。在資料分析方面,我們可以使用Spark和Presto連線看板BI進行一些互動式查詢。當我們需要接到其他線上系統,尤其是QPS較高的場景,我們會先接入到KV儲存,再接入業務系統。

  讓我們來看具體場景。

  1、實時多維彙總  

  對於一個實時多維彙總的場景,我們可以把Kafka 資料增量寫入到 Hudi 的輕度彙總層中。對於分析場景,可以基於 Presto 按需進行多維度的重度彙總計算,並可以直接構建對應的視覺化看板。這個場景對QPS和延遲要求都不是很高,所以可以直接構建,但是對於高 QPS 和低延遲訴求的資料產品場景,目前的一個解決方案是透過 Presto 進行多維度預計算,然後匯入到 KV 系統,進一步對接資料產品。從中長期來看我們會採取基於物化檢視的方式,這樣就可以進一步去簡化業務側的一些操作。  

  在以上鍊路中,我們也遇到了比較多的問題:

  寫入穩定性差。第一點就是Flink在入湖的過程中任務佔用資源比較大,第二點是任務頻繁重啟很容易導致失敗,第三點是Compaction沒有辦法及時執行從而影響到查詢。

  更新效能差。會導致任務的反壓比較嚴重。

  併發度難提升。會對Hudi Metastore Service(目前位元組內部自主研發的Hudi後設資料服務,相容Hive介面,準備貢獻到社群)穩定性產生比較大的影響。

  查詢效能比較差。有十分鐘的延遲甚至經常查詢失敗。

  面對這些問題,我接下來簡單介紹一下針對性的一些解決方案:

  1)寫入穩定性治理  

  這一塊我們透過非同步的Compaction + Compaction Service的方案去解決這個問題。我們之前Flink入湖預設是在Flink內部去做Compaction,發現這一步是暴露以上一系列問題的關鍵。經過最佳化,Flink入湖任務只負責增量資料的寫入,以及 Schedule Compaction邏輯,而Compaction執行則由Compaction Service負責。具體而言,Compaction Service 會從Hudi Metastore非同步拉取Pending Compaction Plan,並提交Spark批任務完成實際的Compact。Compaction執行任務與Flink寫入任務完全非同步隔離,從而對穩定性有較大提升。

  2)高效更新索引  

  支援資料量級的大幅提升。簡單來說,我們可以基於雜湊計算快速定位目標檔案,提升寫入效能;同時可以進行雜湊過濾,從而也可以進行查詢分析側的最佳化。

  3)請求模型的最佳化  

  當前的Hudi社群版的WriteTask 會輪詢Timeline,導致持續訪問Hudi Metastore,從而造成擴充能力受限的問題。我們將WriteTask的輪詢請求從Hudi Metastore轉移到了對JobManager快取的拉取,這樣就能大幅降低對Hudi Metastore的影響。經過這個最佳化可以讓我們從幾十萬量級的RPS(Request Per Sec)提升到近千萬的量級。

  接下來我們來講一下查詢相關的最佳化。

  4)MergeOnRead列裁剪  

  對於原生的MergeOnRead來說,我們會在全量讀取LogFile和BaseFile之後做合併,這在只查詢部分列的時候會造成效能損耗,尤其是列比較多的情況。我們所做的最佳化是把列的讀取下推到Scan層,同時在進行log檔案合併時,會使用map結構儲存K,V(K是主鍵,V是行記錄),之後對行記錄做列裁剪,最後再進行Log Merge的操作。這樣會對序列化和反序列化開銷以及記憶體使用率都有極大降低。

  5)並行讀最佳化  

  一般引擎層在讀Hudi時,一個Filegroup只對應一個Task,這樣當單個 FileGroup 資料量較大時就極易造成效能瓶頸。我們對此的最佳化方案是對BaseFile進行切分,每個切分的檔案對應一個Task從而提高讀並行度。

  6)Combine Engine  

  Hudi社群版目前在記憶體中對資料的合併和傳輸的實現完全是基於Avro格式,這會造成與具體引擎對接時有大量的序列化與反序列化計算,從而導致比較大的效能問題。對於這個問題我們與社群合作做了Combine Engine的最佳化,具體做法就是將介面深入到了引擎層的資料結構。例如在讀取FileGroup時我們直接讀取的就是Spark的InternalRow或是Flink的RowData,從而儘量減少對Avro格式的依賴。這樣的最佳化可以極大地提高MergeOnRead和Compaction的效能。

  2、實時資料分析  

  這個場景我們可以把明細資料直接透過Flink匯入到Hudi中,還會根據DIM表做一個寬表的處理從而落到Hudi表。這個場景的訴求主要有兩點,一個是日誌型資料的高效入湖,另一個是實時資料的關聯。對於這兩個場景的訴求,我們針對性的進行了一些最佳化。

  1)日誌型資料高效入湖  

  對於日誌型資料,我們支援了NonIndex的索引。Hudi社群版主要支援是基於有主鍵的索引,比如Bloom Filter或者是我們給社群提供的Bucket Index。生成基於主鍵的索引方式主要會有兩個步驟,第一個步驟是資料在寫進來的時候會先對資料做定位,查詢是否有歷史資料存在,如果有的話就Update,沒有的話就Insert,之後會定位到對應的檔案把資料Append到Log中。然後在Merge或者在Compaction的過程中要在記憶體中做合併與去重處理,這兩個操作也是比較耗時的。對於NonIndex來說,是不存在主鍵的概念的,所以支援的也是沒有主鍵的日誌型資料入湖。這樣對於日誌型資料在寫入時可以直接Append到Log File中,在合併的過程中,我們可以不做去重處理,直接將增量資料資料Append到Base File中。這樣就對入湖的效率有了很大的提升。

  2)實時資料關聯  

  針對目前實時Join出現的一系列問題,我們基於Hudi支援了儲存層的關聯。對Hudi來說不同的流可以完成其所對應列的寫入,並在Merge的時候做拼接,這樣對於外界查詢來說就是一個完整的寬表。具體來說,在實時資料寫入的過程中有一個比較大的問題是怎麼處理多個流的寫入衝突問題。我們主要是基於Hudi Metastore來做衝突檢測。  

  對於讀的流程,我們會先將多個LogFile讀入記憶體進行Merge,然後再與BaseFile進行最終Merge,最後輸出查詢結果,Merge和Compaction都會使用到這個最佳化。

   四、未來規劃

  1、彈性可擴充套件的索引系統  

  我們剛剛介紹了Bucket Index支援大資料量場景下的更新,Bucket Index也可以對資料進行分桶儲存,但是對於桶數的計算是需要根據當前資料量的大小進行評估的,如果後續需要re-hash的話成本也會比較高。在這裡我們預計透過建立Extensible Hash Index來提高雜湊索引的可擴充套件能力。

  2、自適應的表最佳化服務  

  為了降低使用者的理解和使用成本,我們會與社群深度合作推出Table Management Service來託管Compaction,Clean,Clustering以及Index Building的作業。這樣對使用者來說相關的最佳化都是透明的,從而降低使用者的使用成本。

  3、後設資料服務增強  

  目前我們內部已經使用Hudi Metastore穩定支援了一些線上業務,但是也有更多需求隨之而來,預計增強的後設資料服務如下:

  Schema Evolution:支援業務對Hudi Schema變更的訴求。

  Concurrency Control:在Hudi Metastore中支援批流併發寫入。

  4、批流一體  

  對於流批一體處理,我們的規劃如下:

  Unified SQL:做到批流統一的SQL層,Runtime由Flink/Spark/Presto多引擎協同計算。

  Unified Storage:基於Hudi的實時資料湖儲存,由Hudi來做統一的儲存。

  Unified Catalog:統一後設資料的構建以及接入。

來自 “ DataFunTalk ”, 原文作者:張友軍;原文連結:http://server.it168.com/a2023/0201/6787/000006787792.shtml,如有侵權,請聯絡管理員刪除。

相關文章