B 站構建實時資料湖的探索和實踐

大資料技術前線發表於2023-05-16

來源:Apache Flink

摘要:本文整理自 bilibili 大資料實時團隊資深開發工程師周暉棟,在 Flink Forward Asia 2022 實時湖倉專場的分享。本篇內容主要分為四個部分:


    1. 背景和痛點
    2. 場景探索
    3. 基建最佳化
    4. 總結和展望



01

背景和痛點


B 站構建實時資料湖的探索和實踐

在大資料場景應用中,業務不僅要計算資料結果,而且要保障時效性。目前,我司演化出兩條鏈路。時效性高的資料走 Kafka、Flink 實時鏈路;時效性要求低的資料走 Spark 離線鏈路。上圖簡單描述了 B 站資料上報、處理和使用的鏈路。資料採集主要透過 APP 端上報的行為事件資料。服務端上報的日誌資料會透過閘道器以及分發層,流式分發到大資料數倉體系內。

MySQL 中儲存的業務資料,透過 Datax 週期性的批式同步到數倉內。時效性高的資料會透過 Flink+Kafka 進行流式計算。時效性低的資料透過 Spark+HDFS 進行批計算最後出倉到 MySQL Redis Kafka 的介質中,為 AI、BI 的模型訓練、報表分析場景使用。

在使用的過程中,也發現了諸多問題。

1. 離線資料的時效性不足,離線批計算以小時/天為單位。越來越多的業務方希望時效性達到分鐘級,離線的小時計算或天計算不能滿足業務方的需求。為了達到更高的時效性,業務會再開發一條實時鏈路。

2. 但實時鏈路的可觀測性較弱。因為在 Kafka 裡檢視資料並不方便,所以需要將 Kafka 裡的資料移動到其他儲存中,才能進行檢視。實時資料鏈路普遍不容易和業務時間對齊,難以準確定位到需要重跑的起點。如果資料出現異常,業務一般不會選擇在實時流上進行重跑,而是進行離線鏈路的 T-1 修復。

3. 實時離線雙鏈路會有雙份的資源開銷,開發以及運維成本。除此之外,口徑不一致還會帶來額外的解釋成本。

4. 全天計算資源高峰集中在凌晨,整個大資料叢集的使用峰值在凌晨 2 點~8 點。主要在跑天級任務,也存在任務排隊的現象。其他時段的資源比較空閒,使用時有明顯的峰谷現象。整體資源使用率有一定的最佳化空間。

5. 在資料出倉孤島方面,對於使用者來說還需要克隆一份資料到 HDFS 上,因此會存在資料一致性的問題。當資料出倉後,許可權以及資料聯邦查詢都會存在不足。

我們希望透過實時資料湖方案,解決以上痛點。

1. 我們透過 Flink+Hudi 將資料增量,以及增量計算產出的結果儲存在 Hudi 中,支援分鐘級的資料計算,進一步增強了資料的時效性。

2. 除此之外,Hudi 具備流表二象性,不但可以進行實時的流式增量消費,而且可以作為表,直接進行查詢。相比 Kafka 鏈路,進一步增強了可觀測性。

3. 實時資料湖同時滿足實時和離線的需求,達到降本增效的效果。除此之外,它還支援離線數倉資料重跑的訴求。

4. 透過增量計算,將原本在 0 點以後分配的資料資源進行細分,分攤到全天的每分鐘,錯峰的使用資源。

5. 透過排序、索引、物化等方式,我們可以直接查詢 Hudi 表中的資料,從而達到資料不出倉的效果。

02

場景探索


2.1 DB 入倉場景

B 站構建實時資料湖的探索和實踐

當業務系統資料儲存在 MySQL 中,需要將這些資料匯入到大資料的數倉中,進行報表、分析、計算等場景。目前,業務資料入倉不僅用於離線 ETL,還期望具備時效性。比如在稿件內容稽核場景中,工作人員希望知道近十分鐘的稿件增長量是否匹配稿件稽核人力,存在實時監控訴求,以及告警訴求。稿件的資料來源於業務資料庫,它並不滿足於當前天級、小時級資料同步的時效性,希望能達到分鐘級的時效性。

在降本增效的大背景下,我們不僅要滿足業務訴求,還要考慮效能。實時鏈路用於實時場景,離線鏈路用於批次的 ETL 場景比較浪費。所以我們希望透過實時資料湖,構建一套流批統一的方案,同時滿足實時和離線場景的訴求。透過調研發現,現有的方案中有以下幾類:

第一,DataX 定期批次匯出資料到 Hive。

Hive 本身並不具備更新能力,一般按天來匯出全量,並不滿足時效性訴求。除此之外,該方案還存在資料冗餘的問題。Hive 表每天的分割槽,都是 MySQL 表當天的快照。比如一張使用者資訊表的資訊變化很少,每天都需要儲存一遍全量快照。即每條使用者資訊,每天都會被儲存一次。如果 Hive 表的生命週期是 365 天,那這條資料會被重複儲存 365 次。

第二,Canal/CDC to Hudi 方案。

DB 的資料透過 Canal 或 Flink CDC 寫入 Hudi 中,從而滿足時效性的訴求。由於 Hudi 的資料實時更新,不具備可重複讀的能力。因此該方案並不滿足 ETL 場景。即使使用 Hudi"快照讀"的能力。雖然可以讀取 Hudi 的歷史的 Commit,獲取一份某一時刻的快照資料。但如果長時間的保留 Commit 資料,會導致檔案過多,會影響訪問 timeline 的效能,進而影響到 Hudi 的讀寫效能。

第三,Hudi export Hive 方案。

該方案是前兩個方案的結合,將 DB 的資料透過 Canal/CDC,寫入 Hudi 之後,進行週期性的匯出到 Hive 表。Hudi 表用於實時場景,Hive 表用於離線的 ETL 場景。從而同時滿足兩方面的場景訴求。其缺點在於,使用者在使用過程中用到了兩張表,存在一定的理解成本,以及資料冗餘問題。

第四,Hudi Savepoint 方案。

主要解決資料冗餘的問題。透過週期性的 Savepoint,可以儲存 Hudi 當時的 timeline 後設資料。訪問 Savepoint 時,會對映地訪問到 Hudi 的實際檔案,避免冗餘的儲存資料檔案。每天一個 Savepoint,相當於每天儲存一份 MySQL 快照,從而滿足了 ETL 場景復讀的訴求。與此同時,還可以直接訪問 Hudi 的最新資料,滿足使用者的實時訴求。

但該方案仍有一些缺陷,它無法精確的切分跨天資料。透過 Flink 增量寫 Hudi 時,會週期性的產生 Commit,無法控制業務時間和 Commit 對齊。如果昨天和今天的資料落在同一個 Commit 裡,Savepoint 會以 Commit 為最小力度。當訪問昨天的 Savepoint 時,它會包含今天的資料,與使用者的預期不符。

B 站構建實時資料湖的探索和實踐

為了解決上述問題,我們提出的解決方案是 Hudi Snapshot View 快照檢視。在 Hudi Savepoint 方案上做了改進,簡單來說是一個帶過濾的 Hudi Savepoint。

在匯出 Hive 方案中是可以加過濾條件,將第二天的資料過濾出去。我們把過濾邏輯納入到 Hudi 快照檢視中。在快照檢視裡將過濾邏輯,做在 Hudi 底層,儲存在 Hudi Meta 中。在訪問快照檢視時,我們會吐出過濾後的資料,從而解決快照裡存在跨天資料的問題。

如上圖所示,Delta Commit T3 包含了 11 月 1 號和 11 月 2 號的資料。快照檢視的源資料在儲存時,將歷史的 T1、T2、T3 的源資料全部進行儲存。除此之外,還儲存了 Delta<=11 月 1 號的過濾條件。在讀取時將資料進行過濾,將僅包含 11 月 1 號及以前的資料給查詢端,並不包含 11 月 2 號的資料。

快照檢視同樣是儲存後設資料,透過對映的方式,訪問實際的資料檔案,不存在資料冗餘儲存的問題。也同時滿足實時和離線場景,實現了流批統一。除此之外,快照檢視是獨立切出了一個 timeline,支援再做 Compaction、Clustering 等操作來加速查詢。

B 站構建實時資料湖的探索和實踐

接下來,講一講 Snapshot View 的生成時機。使用者應該在哪次 Commit 後,做下這個快照檢視?這裡需要理解兩個概念,一個是事件時間,一個是處理時間。

當資料出現延遲,雖然現實中的時間到達了 0 點,但它可能還在處理 22 點的資料。此時,如果進行快照檢視,使用者讀取的快照檢視資料就會偏少。因為 Commit 是處理時間,不是業務的事件時間。這個 Snapshot View 要在事件時間推進到 0 點後進行,才能夠保證資料的完整。為此,我們在 Hudi 中增加了處理進度。這個概念類似於 Flink 中使用 Watermark 來標識處理進度。我們擴充套件了 Hudi Meta 在 Commit 中儲存了處理進度。當事件時間推進到 0 點後,開始進行 Snapshot View 操作,通知下游任務可以被調起。在 Meta 中有了事件的處理進度,在查詢 Hudi 時也能獲取處理進度,從而進行一些判斷。

B 站構建實時資料湖的探索和實踐

除此之外,我們還做了引擎層的適配。在使用方面,使用者寫的 SQL 和原來的基本一致,透過 hint 或者 set 引數,指定是查詢快照分割槽或者是查詢實時分割槽。在 DB 入倉的場景上,既滿足了實時場景的時效性,又滿足了 ETL 的離線訴求,成功實現了實時和離線的統一,達到降本增效的目的。

2.2 埋點入倉場景

B 站構建實時資料湖的探索和實踐

接下來,講一講埋點入倉場景。我司作為一家網際網路公司,也會進行使用者的行為事件定義,收集資料、上報入倉,然後進行分析和使用。用資料驅動指導業務發展。

我司的使用者⾏為事件上報已經頗具規模,行為事件非常多。現在已經定義了上萬個行為事件的 ID,每天日增千億條資料,流量非常大。埋點入倉是公司級的專案,全站各個業務方都在上報埋點。在使用埋點時,我司存在大量部門業務線的交叉使用。比如廣告 AI 需要使用其他業務線上報的資料,進行樣本收集和訓練。

在原有架構上,APP 端上報的資料經過傳輸和清洗,落入數倉預先劃分好的表分割槽中,供給業務方進行開發使用。這種業務劃分是根據 BU、事件型別等業務資訊,進行的粗粒度劃分。下游的任務可以使用自己部門的表以及其他部門的表,只需要申請許可權即可。

但該架構也存在一些痛點。一條流資料的隔離性不夠,上萬個埋點由同一個渠道傳輸,清洗,隔離性不足。容易出現活動期間某個行為事件猛增,影響整體的任務處理進度。除此之外,業務線使用需要過濾大量無用資料。在下游業務的任務中,可能僅用到自己的一個行為事件進行分析。但此時行為事件與同部門其他的行為事件混在一起。在條件過濾時,引擎層只能做到分割槽級的過濾。將整個分割槽的檔案載入進來,然後進行過濾,有較大檔案讀取的 IO 浪費。與此同時,部門在交叉使用資料時,許可權管理較難,使用到了其他 BU 的一個行為事件,需要申請整個 BU 表的許可權。粒度粗,存在風險。下游有分鐘級的訴求。目前,資料進行流式傳輸是小時級的清洗。下游的時效性是小時級別,不滿足使用者的時效性的訴求。

B 站構建實時資料湖的探索和實踐

為了解決上述問題,我們做了一些架構上的最佳化。如上圖所示,資料上報傳輸後,將資料落到戶的 Hudi 表裡。使用者透過 View 來訪問或使用這些資料,可以用於離線 ETL、實時計算、BI 報表分析等場景。

對於秒級時效性訴求的資料,會走高優 Kafka 鏈路,提供給線上服務使用,這種佔比就比較小了。北極星事件管理平臺和後設資料管理,負責管理整個行為事件埋點的生命週期、分發規則等等。

平臺控制從邊緣上報開始,進行規則分流,業務隔離,提升隔離性。當資料落入業務 Hudi 表後,進行 Clustering,對業務資料進行排序和索引。透過引擎層,進行檔案級別/資料塊級別的 Dataskip,減少實際讀取資料量的 IO 開銷。使用者透過 Hive View 讀取資料,平臺透過給使用者的 View 增加有許可權的行為事件,達到行為事件級別許可權管理。比如 a 部門的同學,在使用 b 部門的行為事件時,在 a 部門的 View 上增加一個 b 部門行為事件的 ID 即可。在提交 SQL 進行檢查時,會進行行為事件級別的許可權校驗。增量傳輸清洗 Hudi 表時,Hudi 表支援增量消費,可以達到分鐘級的時效性。下游實時任務可以接在這個 View 後面進行使用,從而達到流批統一。

在 Hudi 側的最佳化方面,因為流量資料入湖不更新,所以我們採用了 no index 模式,去掉 bucket assign 等過程,從而提升寫入速度。與此同時,Flink 增量 Clustering 下游的 ETL 延遲,無明顯增加。經過 Clustering 之後,資料開始變得有序。索引記錄了行為事件的分佈情況,可以透過條件查詢,進行檔案級別和資料塊級別的過濾。除此之外,Flink、Spark 等引擎也支援 Hudi 表的謂詞下推,進一步提升了效率。在 Flink 對於 View 的支援方面,View 下游可以再去定義 Watermark,也可以在 View 上定義 with 屬性等等。

透過架構調整和 Hudi 能力的結合,我們增強了埋點管理的隔離性、時效性,也節約了資源。

2.3 BI 實時報表場景

B 站構建實時資料湖的探索和實踐

接下來,講一講 BI 實時報表場景。在原先架構下,流量資料和 DB 資料匯入數倉後,會進行 Join 打寬,聚合後將原來的計算結果輸出到 MySQL 之類的儲存。BI 報表會直接對接 MySQL,進行資料展示。另外一條離線鏈路,會進行 T-1 的修數兜底。

原先架構的痛點在於,實時和離線兩條鏈路重複建設,計算儲存成本高,開發運維成本高,口徑解釋成本高。Kafka 資料需要複製其他儲存,才能進行查詢,可觀測性比較弱。除此之外,Kafka 鏈路難做資料修復。Kafka 鏈路很難確定修復起點,通常使用 T-1 的方式進行修復。存在資料出倉孤島等問題。

B 站構建實時資料湖的探索和實踐

BI 實時報表場景一般沒有秒級的時效性訴求,分鐘級的時效性就可以滿足訴求。我們透過 Hudi 替換 Kafka,同時滿足了實時和離線的訴求,實現流批統一,達到降本,資料口徑得到統一。

Hudi 相比 Kafka,可以直接查詢 Hudi 中的資料,比 Kafka 更容易、更方便的進行告警。

比如在 Kafka 上對比七天前的資料,做一個閾值告警。需要消費七天前的資料以及當前資料,進行計算以後,再進行告警。整個過程比較複雜。Hudi 的查詢 SQL 和離線的查詢 SQL 是一致的。對於 DQC 系統來說,實時 DQC 和離線 DQC 的方案是統一的,開發成本較低。對於有秒級時效性要求的任務,還需要走 Kafka 鏈路。

除此之外,資料可以做到不出倉。BI 報表可以直接查詢,對接查詢的 Hudi 表,進行資料展示。

B 站構建實時資料湖的探索和實踐

在實際使用過程中,也存在一些問題。直接對 Hudi 的明細表進行聚合查詢時,查詢時間過長,存在讀放大的問題。

假設實時 DQC 每五分鐘統計近一個小時的資料,進行資料條數監控。五分鐘會計算近一個小時的資料,下一個五分鐘再計算近一個小時的資料。在滑動視窗的過程中,中間的資料會被計算好多次,存在比較嚴重的 IO 放大。

除此之外,以 BI 報表場景為例。假設展示一個 DAU 曲線,每個點都是歷史資料的累計值。1 點的資料就是 0 點~1 點資料的累計值。2 點的資料就是 0 點~2 點資料的累計值。在介面展示時,就需要計算 n 個點,每個點都會進行重複的計算,導致查詢的時間較長,存在讀放大的問題。

除此之外,開發運維的成本較高。使用者會在一個 BI 皮膚的介面,展示多個指標。可能是從同一張 Hudi 的明細表裡,出的不同維度的資料。如果出十個指標,就需要開發和運維十個實時任務,開發和運維成本較高,可靠性較低。當一個實時任務出現異常,這個皮膚就會缺失一部分的指標。

我們提出的最佳化方案是,透過 Flink+Hudi 構建 Projection 物化檢視。透過 Flink State 狀態,僅需攝取增量資料計算即可,避免讀放大問題。將查詢結果提前計算出來,直接查詢結果資料,來達到加速查詢的效果。

具體的流程是,使用者提交一個查詢 SQL 給 Excalibur Server,進行 SQL 解析。在解析過程中,會提交 Projection 建立,提交一個 Stream 任務,然後增量讀取原始表中的資料,進行物化計算以後,再儲存到 Projection 物化表中。當查詢 SQL 命中物化規則時,就會改寫查詢,直接查詢結果表,達到加速的效果。

B 站構建實時資料湖的探索和實踐

透過擴充套件 Flink Batch SQL 的解析過程,查詢時候會載入物化規則以及 Projection 的後設資料。並且判斷物化表當前 Watermark 物化進度。如果滿足要求,則改寫查詢 Projection 物化表。

B 站構建實時資料湖的探索和實踐

我們參考 Calcite 的物化規則,增加了 TVF 的語法⽀持。

支援 Projection 的建立,使用者提交批查詢,可以透過在 select 語句上增加 hint,提示查詢引擎,該查詢會進行復用。引擎會對該查詢建立 Projection。

支援 Flink SQL 的 Projection DDL 語法以及 SQL 查詢改寫的規則。當使用者提交批查詢時,如果有對應的 Projection,就能進行改寫。改寫後可以直接使用 Projection 的結果,大大加速查詢,能夠做到秒級甚至毫秒級的響應。

Projection 的改寫降級,是根據 Watermark 等指標,遮蔽掉 Projection 實時任務的延遲和失敗等問題,保障了查詢結果的可靠性。我們在 Hudi Meta 增加了 Watermark 資料處理進度資訊。在資料寫入的過程中,我們會在 Commit Meta 中記錄物化進度。在執行物化規則匹配時,如果落後當前時間太多,就會拒絕當前 Projection 改寫,直接降級到原表,進行資料查詢。

在 select 語句上增加 hint 建立,透過物化的能力達到加速查詢的效果。不但解決了讀放大的問題,透過自動降級,也減少了使用者的開發和運維成本。

在未來,我們會圍繞 Projection 效率進行最佳化。回收長期無法命中的 Projection。合併多個維度相同的 Projection,降低 Projection 的計算成本。除此之外,我們會和指標系統對接,透過指標系統快取加速查詢,滿足一些高 QPS 場景的流計算。

B 站構建實時資料湖的探索和實踐

之前在流式寫入時,使用 Flink SQL,批次修數使用 Spark SQL,仍然需要開發和運維兩套 SQL。在 Hudi 實時資料庫的方案下,我們參考了離線修數方案。

歷史分割槽重跑,使用 Flink Batch Overwrite,與離線修數方式是一致的。

當前分割槽重跑,我們使用 Flink Stream Overwrite 的方式。比如需要將當前的分割槽資料進行清空刪除,然後再進行寫入。因為它是 no index 的方式去寫入,所以它沒有沒有辦法透過 update 的形式覆蓋之前寫入的資料。我們透過擴充套件 Hudi Catalog,支援 Flink SQL 的方式,alter table drop partition 操作刪除分割槽及資料。然後透過重新流式寫入的方式,實現了 Flink Stream Overwrite。

當工具支援級聯重跑任務後,我們就可以從最源端的 ODS 層級,修復到最末端,不再需要開發運維 Spark T-1 修復任務。真正達到了流批一體的效果。

03

基建最佳化


B 站構建實時資料湖的探索和實踐

在基建最佳化方面,我們對 Table Service 進行最佳化。由於 Compaction、Clustering 等任務耗資源較多,和寫入任務相互影響,導致寫入的效能下降。我們透過拆分 Table Service,透過獨立資源執行,來解決這個問題。

我們將 Compaction plan、Clustering plan 執行計劃的生成過程,放在寫入任務中,將真正執行 Compaction、Clustering 的 task 獨立程式,進行執行。避免寫入和 Table Service 相互影響,提高了寫入效能。與此同時,支援了動態調整 Compaction plan 的策略,透過調整頻次,減少不必要的 IO。

B 站構建實時資料湖的探索和實踐

Hudi Manager 用於規模化的管理託管,包括表服務託管,比如 Compaction、Clustering、Projection 任務託管獨立執行,資源隔離,提升寫入穩定性。支援自動拉起,可批可流。

在表管理方面,是在建表時構建 Hudi 的源資料,取代第一次寫入時構建的源資料,避免重要引數遺漏。比如將資料批次匯入到 Hudi 時,不關心 preCombine 比較欄位,初始化好了表的後設資料。流式寫入時,不會修改表的後設資料。該匹配欄位的缺失,會導致無法得到正確的合併結果。

在策略配置方面,使用者選擇 OLAP、ETL 場景時,可以自動配置不同的表服務的執行間隔。比如下游的 ETL 場景是天級別排程。相比 OLAP 場景,我們可以使用更低的 Compaction 頻次。

B 站構建實時資料湖的探索和實踐

如上圖所示,我們在實際使用的過程中,發現和解決了不少資料質量、穩定性、效能方面的問題,並做了功能性的增強,貢獻給社群。涵蓋了 Sink、Compaction、Clustering、Common 包、Source、Catalog 等若干方面。前面場景中提到的一些能力,我們陸續也會以 PR 或者 RFC 的形式推給社群。

04

總結和展望


B 站構建實時資料湖的探索和實踐

我們在流量資料入湖、DB 資料入湖場景、報表場景以及流批一體,都做了一系列的實踐。

接下來,我們還會深入數倉領域,探索透過物化任務減少數倉分層,透過分層任務的分析、診斷、最佳化,進行智慧分層。使業務同學更加專注於資料的使用,減輕數倉分層的工作量,向湖倉一體的方向演進。增強增量化計算,支援圍繞 Hudi 的 Join ETL,在儲存層最佳化 Join 的邏輯。探索 Hudi 在 AI 領域的運用。

在核心方面,我們後續會增強 Hudi Meta Store,統一後設資料管理;增強 Table Service;增強 Hudi Join 的列拼接能力。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027827/viewspace-2952302/,如需轉載,請註明出處,否則將追究法律責任。

相關文章