Flink CDC + Hudi 海量資料入湖在順豐的實踐

阿里雲開發者發表於2022-06-14

本文整理自順豐大資料研發工程師覃立輝在 5月 21 日 Flink CDC Meetup 的演講。主要內容包括:

  1. 順豐資料整合背景
  2. Flink CDC 實踐問題與優化
  3. 未來規劃

點選檢視直播回放 & 演講PDF

一、順豐資料整合背景

img

順豐是快遞物流服務提供商,主營業務包含了時效快遞、經濟快遞、同城配送以及冷鏈運輸等。

運輸流程背後需要一系列系統的支援,比如訂單管理系統、智慧物業系統、以及很多中轉場、汽車或飛機上的很多感測器,都會產生大量資料。如果需要對這些資料進行資料分析,那麼資料整合是其中很重要的一步。

img

順豐的資料整合經歷了幾年的發展,主要分為兩塊,一塊是離線資料整合,一塊是實時資料整合。離線資料整合以 DataX 為主,本文主要介紹實時資料整合方案。

2017 年,基於 Jstorm + Canal 的方式實現了第一個版本的實時資料整合方案。但是此方案存在諸多問題,比如無法保證資料的一致性、吞吐率較低、難以維護。 2019 年,隨著 Flink 社群的不斷髮展,它補齊了很多重要特性,因此基於 Flink + Canal 的方式實現了第二個版本的實時資料整合方案。但是此方案依然不夠完美,經歷了內部調研與實踐,2022 年初,我們全面轉向 Flink CDC 。

img

上圖為 Flink + Canal 的實時資料入湖架構。

Flink 啟動之後,首先讀取當前的 Binlog 資訊,標記為 StartOffset ,通過 select 方式將全量資料採集上來,發往下游 Kafka。全量採集完畢之後,再從 startOffset 採集增量的日誌資訊,發往 Kafka。最終 Kafka 的資料由 Spark 消費後寫往 Hudi。

但是此架構存在以下三個問題:

  • 全量與增量資料存在重複:因為採集過程中不會進行鎖表,如果在全量採集過程中有資料變更,並且採集到了這些資料,那麼這些資料會與 Binlog 中的資料存在重複;
  • 需要下游進行 Upsert 或 Merge 寫入才能剔除重複的資料,確保資料的最終一致性;
  • 需要兩套計算引擎,再加上訊息佇列 Kafka 才能將資料寫入到資料湖 Hudi 中,過程涉及元件多、鏈路長,且消耗資源大。

基於以上問題,我們整理出了資料整合的核心需求:

img

  1. 全量增量自動切換,並保證資料的準確性。Flink + Canal 的架構能實現全量和增量自動切換,但無法保證資料的準確性;
  2. 最大限度地減少對源資料庫的影響,比如同步過程中儘量不使用鎖、能流控等;
  3. 能在已存在的任務中新增新表的資料採集,這是非常核心的需求,因為在複雜的生產環境中,等所有表都準備好之後再進行資料整合會導致效率低下。此外,如果不能做到任務的合併,需要起很多次任務,採集很多次 Binlog 的資料,可能會導致 DB 機器頻寬被打滿;
  4. 能同時進行全量和增量日誌採集,新增表不能暫停日誌採集來確保資料的準確性,這種方式會給其他表日誌採集帶來延遲;
  5. 能確保資料在同一主鍵 ID 下按歷史順序發生,不會出現後發生的事件先傳送到下游。

img

Flink CDC 很好地解決了業務痛點,並且在可擴充套件性、穩定性、社群活躍度方面都非常優秀。

  • 首先,它能無縫對接 Flink 生態,複用 Flink 眾多 sink 能力,使用 Flink 資料清理轉換的能力;
  • 其次,它能進行全量與增量自動切換,並且保證資料的準確性;
  • 第三,它能支援無鎖讀取、斷點續傳、水平擴充套件,特別是在水平擴充套件方面,理論上來說,給的資源足夠多時,效能瓶頸一般不會出現在 CDC 側,而是在於資料來源/目標源是否能支援讀/寫這麼多資料。

二、Flink CDC 實踐問題與優化

img

上圖為 Flink CDC 2.0 的架構原理。 它基於 FLIP-27 實現,核心步驟如下:

  1. Enumerator 先將全量資料拆分成多個 SnapshotSplit,然後按照上圖中第一步將 SnapshotSplit 傳送給 SourceReader 執行。執行過程中會對資料進行修正來保證資料的一致性;
  2. SnapshotSplit 讀取完成後向 Enumerator 彙報已讀取完成的塊資訊;
  3. 重複執行 (1) (2) 兩個步驟,直到將全量資料讀取完畢;
  4. 全量資料讀取完畢之後,Enumerator 會根據之前全量完成的 split 資訊, 構造一個 BinlogSplit。 傳送給 SourceRead 執行,讀取增量日誌資料。

問題一:新增表會停止 Binlog 日誌流

img

在已存在的任務中新增新表是非常重要的需求, Flink CDC 2.0 也支援了這一功能。但是為了確保資料的一致性,Flink CDC 2.0 在新增表的流程中,需要停止 Binlog 日誌流的讀取,再進行新增表的全量資料讀取。等新增表的全量資料讀取完畢之後,再將之前停止的 Binlog 任務重新啟動。這也意味著新增表會影響其他表的日誌採集進度。然而我們希望全量和增量兩個任務能夠同時進行,為了解決這一問題,我們對 Flink CDC 進行了擴充,支援了全量和增量日誌流並行讀取,步驟如下:

img

  1. 程式啟動後,在 Enumerator 中建立 BinlogSplit ,放在分配列表的第一位,分配給 SourceReader 執行增量資料採集;
  2. 與原有的全量資料採集一樣,Enumerator 將全量採集切分成多個 split 塊,然後將切分好的塊分配給 SourceReader 去執行全量資料的採集;
  3. 全量資料採集完成之後,SourceReader 向 Enumerator 彙報已經完成的全量資料採集塊的資訊;
  4. 重複 (2) (3) 步,將全量的表採集完畢。

以上就是第一次啟動任務,全量與增量日誌並行讀取的流程。新增表後,並行讀取實現步驟如下:

img

  1. 恢復任務時,Flink CDC 會從 state 中獲取使用者新表的配置資訊;
  2. 通過對比使用者配置資訊與狀態資訊,捕獲到要新增的表。對於 BinlogSplit 任務,會增加新表 binlog 資料的採集;對於 Enumerator 任務,會對新表進行全量切分;
  3. Enumerator 將切分好的 SnapshotSplit 分配給 SourceReader 執行全量資料採集;
  4. 重複步驟 (3),直到所有全量資料讀取完畢。

然而,實現全量和增量日誌並行讀取後,又出現了資料衝突問題。

img

如上圖所示, Flink CDC 在讀取全量資料之前,會先讀取當前 Binlog 的位置資訊,將其標記為 LW,接著通過 select 的方式讀取全量資料,讀取到上圖中 s1、s2、 s3、s4 四條資料。再讀取當前的 Binlog 位置,標記為 HW, 然後將 LW 和 HW 中變更的資料 merge 到之前全量採集上來的資料中。經過一系列操作後,最終全量採集到的資料是 s1、s2、s3、s4 和 s5。

而增量採集的程式也會讀取 Binlog 中的日誌資訊,會將 LW 和 HW 中的 s2、s2、s4、s5 四條資料發往下游。

上述整個流程中存在兩個問題:首先,資料多取,存在資料重複,上圖中紅色標識即存在重複的資料;其次,全量和增量在兩個不同的執行緒中,也有可能是在兩個不同的 JVM 中,因此先發往下游的資料可能是全量資料,也有可能是增量資料,意味著同一主鍵 ID 到達下游的先後順序不是按歷史順序,與核心需求不符。

針對資料衝突問題,我們提供了基於 GTID 實現的處理方案。

img

首先,為全量資料打上 Snapshot 標籤,增量資料打上 Binlog 標籤;其次,為全量資料補充一個高水位 GTID 資訊,而增量資料本身攜帶有 GTID 資訊,因此不需要補充。將資料下發,下游會接上一個 KeyBy 運算元,再接上資料衝突處理運算元,資料衝突的核心是保證發往下游的資料不重複,並且按歷史順序產生。

如果下發的是全量採集到的資料,且此前沒有 Binlog 資料下發,則將這條資料的 GTID 儲存到 state 並把這條資料下發;如果 state 不為空且此條記錄的 GTID 大於等於狀態中的 GTID ,也將這條資料的 GTID 儲存到 state 並把這條資料下發;

通過這種方式,很好地解決了資料衝突的問題,最終輸出到下游的資料是不重複且按歷史順序發生的。

img

然而,新的問題又產生了。在處理演算法中可以看出,為了確保資料的不重複並且按歷史順序下發,會將所有記錄對應的 GTID 資訊儲存在狀態中,導致狀態一直遞增。

清理狀態一般首選 TTL,但 TTL 難以控制時間,且無法將資料完全清理掉。第二種方式是手動清理,全量表完成之後,可以下發一條記錄告訴下游清理 state 中的資料。

解決了以上所有問題,並行讀取的最終方案如下圖所示。

img

首先,給資料打上四種標籤,分別代表不同的狀態:

  • SNAPSHOT:全量採集到的資料資訊。
  • STATE_BINLOG:還未完成全量採集, Binlog 已採集到這張表的變更資料。
  • BINLOG:全量資料採集完畢之後,Binlog 再採集到這張表的變更資料。
  • TABLE_FINISHED:全量資料採集完成之後通知下游,可以清理 state。

具體實現步驟如下:

  1. 分配 Binlog ,此時 Binlog 採集到的資料都為 STATE_BINLOG 標籤;
  2. 分配 SnapshotSplit 任務,此時全量採集到的資料都為 SNAPSHOT 標籤;
  3. Enumerator 實時監控表的狀態,某一張表執行完成並完成 checkpoint 後,通知 Binlog 任務。Binlog 任務收到通知後,將此表後續採集到的 Binlog 資訊都打上 BINLOG 標籤;此外,它還會構造一條 TABLE_FINISHED 記錄發往下游做處理;
  4. 資料採集完成後,除了接上資料衝突處理運算元,此處還新增了一個步驟:從主流中篩選出來的 TABLE_FINISHED 事件記錄,通過廣播的方式將其發往下游,下游根據具體資訊清理對應表的狀態資訊。

問題二:寫 Hudi 時存在資料傾斜

img

如上圖,Flink CDC 採集三張表資料的時候,會先讀取完 tableA 的全量資料,再讀取tableB 的全量資料。讀取 tableA 的過程中,下游只有 tableA 的 sink 有資料流入。

img

我們通過多表混合讀取的方式來解決資料傾斜的問題。

引入多表混合之前,Flink CDC 讀取完 tableA 的所有 chunk,再讀取 tableB 的所有 chunk。實現了多表混合讀取後,讀取的順序變為讀取 tableA 的 chunk1、tableB 的 chunk1、tableC 的 chunk1,再讀取 tableA 的 chunk2,以此類推,最終很好地解決了下游 sink 資料傾斜的問題,保證每個 sink 都有資料流入。

img

我們對多表混合讀取的效能進行了測試,由 TPCC 工具構造的測試資料,讀取了 4。張表,總並行度為 8,每個 sink 的並行度為 2,寫入時間由原來的 46 分鐘降至 20 分鐘,效能提升 2.3 倍。

需要注意的是,如果 sink 的並行度和總並行度相等,則效能不會有明顯提升,多表混合讀取主要的作用是更快地獲取到每張表下發的資料。

問題三:需要使用者手動指定 schema 資訊

img

使用者手動執行 DB schema 與 sink 之間 schema 對映關係,開發效率低,耗時長且容易出錯。

img

為了降低使用者的使用門檻,提升開發效率,我們實現了 Oracle catalog ,讓使用者能以低程式碼的方式、無需指定 DB schema 資訊與 sink schema 資訊的對映關係,即可通過 Flink CDC 將資料寫入到 Hudi。

三、未來規劃

img

第一, 支援 schema 資訊變更同步。比如資料來源發生了 schema 資訊變更,能夠將其同步到 Kafka 和 Hudi 中;支援平臺接入更多資料來源型別,增強穩定性,實現更多應用場景的落地。

第二, 支援 SQL 化的方式,使用 Flink CDC 將資料同步到 Hudi 中,降低使用者的使用門檻。

第三, 希望技術更開放,與社群共同成長,為社群貢獻出自己的一份力量。

問答

Q:斷點續傳採集如何處理?

A:斷點續傳有兩種,分為全量和 Binlog。但它們都是基於 Flink state 的能力,同步的過程中會將進度儲存到 state 中。如果失敗了,下一次再從 state 中恢復即可。

Q:MySQL 在監控多表使用 SQL 寫入 Hudi 表中的時候,存在多個 job,維護很麻煩,如何通過單 job 同步整庫?

A:我們基於 GTID 的方式對 Flink CDC 進行了擴充,支援任務中新增表,且不影響其他表的採集進度。不考慮新增表影響到其他表進度的情況下,也可以基於 Flink CDC 2.2 做新增表的能力。

Q:順豐這些特性會在 CDC 開源版本中實現嗎?

A:目前我們的方案還存在一些侷限性,比如必須用 MySQL 的 GTID,需要下游有資料衝突處理的運算元,因此較難實現在社群中開源。

Q:Flink CDC 2.0 新增表支援全量 + 增量嗎?

A:是的。

Q:GTID 去重運算元會不會成為效能瓶頸?

A:經過實踐,不存在效能瓶頸,它只是做了一些資料的判斷和過濾。

點選檢視直播回放 & 演講PDF


更多 Flink 相關技術問題,可掃碼加入社群釘釘交流群
第一時間獲取最新技術文章和社群動態,請關注公眾號~

img

活動推薦

阿里雲基於 Apache Flink 構建的企業級產品-實時計算Flink版現開啟活動:
99 元試用 實時計算Flink版(包年包月、10CU)即有機會獲得 Flink 獨家定製衛衣;另包 3 個月及以上還有 85 折優惠!
瞭解活動詳情:https://www.aliyun.com/produc...

image.png

相關文章