基於 Flink 的小米資料整合實踐

ApacheFlink發表於2023-02-16

摘要:本文整理自小米計算平臺高階工程師胡煥,在 FFA 資料整合專場的分享。本篇內容主要分為四個部分:


  1. 發展現狀

  2. 思考實踐

  3. 引擎設計

  4. 未來規劃


01

發展現狀


基於 Flink 的小米資料整合實踐

首先介紹一下小米計算平臺,小米計算平臺主要負責小米集團的資料開發平臺的建設,體現在產品上是小米資料工場,底層引擎上常見的 Flink、Spark、Iceberg、Hive 等等都是由計算平臺在負責。


上圖是小米資料工場的技術架構圖。


正中間的藍色高亮框是小米自研的訊息中介軟體 Talos,可以把它替換成大家比較熟悉的 Kafka,這對今天的分享內容來說幾乎沒有任何差別。


Talos 右下方的藍色高亮框是表格儲存的技術選型,小米的資料湖技術選型選擇了 Iceberg,Iceberg 是小米資料整合的主要場景之一。


右下角的紅色高亮框就是資料整合。小米資料整合目前的主要場景還是資料的入湖入倉,但資料出湖出倉的場景也在快速增長。我們的最終建設目標是建立各種異構資料系統之間對接的能力,所以目前我們把資料整合作為一種基礎服務在建設。


基於 Flink 的小米資料整合實踐

在產品層面,我們將資料整合劃分為四個主要場景。


  1. 第一個主要場景是資料採集,主要是一些時間敏感資料的採集,比如客戶端和服務端的埋點資料的採集、日誌檔案採集、物聯網資料採集等等。這些場景需要一些專用的採集技術來支援,基本上沒有辦法整合到一個引擎中,我們將這些資料整合場景拆分為獨立的採集和整合兩個部分,兩者透過訊息佇列進行對接。


    採集的部分設計了單獨的資料採集中心,用來配置各種採集服務。這些採集服務統一都輸出到訊息佇列,這樣我們可以把整合的部分統一起來,透過組合不同的實時整合作業來支援各種資料整合的場景。


  2. 第二個主要場景是實時整合作業,目前只支援訊息佇列和資料庫作為上游資料來源。訊息佇列場景裡很大一部分的資料來源就是來源於資料採集中心,不同的採集場景對應的主流實時整合場景略有不同,例如埋點場景通常寫入到 Iceberg 進行進一步的加工和分析,日誌場景通常不做額外的加工直接寫入到 ES 裡,還有一些比較特殊的是 Schema On Read 場景,它們直接寫入 HDFS 檔案。


    資料庫場景與訊息佇列場景略有不同,訊息佇列場景裡的採集和整合這兩個部分是完全相互獨立的,在資料庫場景裡,這兩個部分則是緊密結合的。


  3. 第三個主要場景是離線整合作業,離線場景相對來說簡單很多,略微麻煩一點的就是 MySQL 的分庫分表和 Doris 這兩個場景,後面為大家做詳細介紹。


  4. 第四個主要場景是跨叢集同步作業,目前我們只支援 Hive 和 Iceberg 做跨叢集的同步,這是因為會涉及到跨國的資料傳輸,在合規和網路方面都會存在一些額外要求,所以我們儘量避免在採集或整合的環節做跨叢集同步。


這四個場景除了資料採集中心無法整合到一個引擎裡,其餘三個場景都透過小米資料整合引擎支援了。


基於 Flink 的小米資料整合實踐

上圖是整合作業的主要場景的作業數量,紅色是實時作業,藍色是離線作業,灰色字型的是存量作業。


我們將實時整合作業分為訊息佇列場景和資料庫場景,再加上離線整合作業、跨叢集同步作業,構成了四個主要場景。


注意到每個場景裡還有相當多的存量作業,在孵化小米資料整合引擎以前,這些場景都是用不同的引擎來支援的。


基於 Flink 的小米資料整合實踐

上圖展示的是小米資料整合的演進。


訊息佇列場景過去是由 Spark Streaming 支援的,現在換成了 Flink SQL。


資料庫實時場景過去是由自研採集服務支援的,且只支援 MySQL 資料庫,現在整體升級到了 Flink CDC + Flink SQL,並藉助 Flink CDC 支援了更多的資料庫。


離線場景裡過去是由 DataX 支援的,現在我們也都換成了 Flink SQL。


跨叢集同步場景過去由 Hadoop 的 DISTCP 命令來實現的,這個命令只能複製底層的檔案,在複製 Hive 表的時候需要配合另外的命令來新增分割槽,體驗非常糟糕,換到了 Flink SQL 就沒有這個問題了,還增加了對 Iceberg 表的跨叢集實時複製的支援。


直到現在小米還有大量的存量作業在等待遷移,這些存量作業的各種引擎給我們帶來了非常大的維護負擔。一方面佔用了大量人力去熟悉和維護這些引擎,另一方面是我們很難在這些差異巨大的引擎上構建出統一的產品,這需要增加很多分支判斷,也很容易出錯。所以我們非常迫切地希望把這些場景由同一個引擎裡支援起來,目前來看,Flink 在資料整合領域的優勢極其明顯,小米的資料整合引擎就是基於 Flink 構建的。


02

思考實踐


首先我們嘗試抽象出幾個核心概念,並透過核心問題來界定每個核心概念的範圍和邊界。


基於 Flink 的小米資料整合實踐

我們將資料整合領域的生產實踐都歸類到這三個層級上,分別是資料整合領域、資料整合產品、資料整合引擎,這三個層級處理的核心問題是一層一層遞進的。


基於 Flink 的小米資料整合實踐

資料整合領域的核心問題是連線。


資料整合的概念是相對於資料開發的概念來定義的,上圖的右側展示的就是資料開發領域最常見的技術棧,左側展示的則是資料整合領域的範圍。


在資料開發領域,離線數倉通常使用 Spark+Hive 的技術棧,實時數倉的最新的技術棧是 Flink+資料湖,這裡的 Hive 和資料湖都可以用 Flink 或 Spark 直接訪問。我們通常所說的資料開發的工作主要就是在離線數倉或資料湖內進行的,我們可以認為資料開發領域主要是基於現成的可以直接訪問的資料進行開發。


在資料整合領域,關注的重點則是:資料在哪裡,怎麼訪問這些資料,怎麼讓資料正確的參與到計算中。圖中最左側列出來的是最常見的幾個資料整合場景,在這些場景裡我們不能像常規的資料開發一樣直接訪問這些資料。比如我們肯定不會在要進行資料計算的時候才實時的連線到伺服器甚至客戶端讀取資料,即使是最常規的資料庫場景,我們通常也需要用一張 ODS 表來代替資料庫參與計算,在作業中直連資料庫很容易會讓資料庫壓力過大,產生各種異常。


我們希望在資料開發過程中能夠以一種統一的方式訪問上游的各種資料來源,這個處理過程就是資料整合領域裡要解決的核心問題:連線。


注意這裡強調的是連線,資料整合經常與資料匯入的概念混淆,把資料匯入到 ODS 表之後再去做資料開發是資料整合領域的一個優秀實踐,但不是所有場景都必須要有匯入這個步驟,在 Flink 中只要有 Connector 的支援,在簡單場景中我們完全可以直接連線資料來源做資料開發。


基於 Flink 的小米資料整合實踐

資料整合產品的核心問題是效率。


上圖展示了最常見的數倉建模規範,我們主要關注其中的 ODS 表。


資料開發過程中經常需要多次讀取原始資料,將原始資料匯入到 ODS 表,再用 ODS 表替代原始資料參與資料開發,就可以避免重複連線上游的資料來源。


現在經常提到的用 ELT 替代 ETL 的做法,代表的是 ODS 表設計的一個優秀實踐,我們稱之為映象同步。映象同步要求 ODS 表結構與上游資料的結構儘可能保持完全一致,並儘可能的保留上游資料的所有細節,資料清洗的步驟則往後放,改為基於 ODS 表實施,這樣如果清洗邏輯存在問題,我們基於 ODS 表進行修復的代價也非常小。


有映象同步這個優秀實踐作為基礎,我們就能夠將資料整合的過程標準化了。將映象同步的整個流程中的各種重複工作固化下來,就形成了資料整合產品,從而可以大幅提高資料整合工作的開發效率。我們可以認為資料整合產品就是從資料整合領域的各種優秀實踐上發展而來的。


這裡需要關注一個細節,將資料開發的結果匯出到資料應用的過程中,同樣存在很多優秀實踐,而且這些優秀實踐的展現形式、底層技術與資料整合產品的相似度都非常高。很多情況下我們會把這兩種場景整合到一個產品裡,在技術上這是非常合理的決策,但本質上資料匯出場景更合適的名稱是資料系統整合,這種做法在某種意義上是擴充了資料整合領域的邊界。


基於 Flink 的小米資料整合實踐

資料整合引擎的核心問題是異構。


在底層技術上,我們需要有相應的資料整合引擎來支援我們的資料整合產品。在引擎的設計中,最核心的問題是解決異構資料系統對接帶來的各種問題,引擎涉及到的資料系統越多,碰到的問題和解決方案也就越複雜。


上圖展示的是其中的一個例子,不同資料系統支援的欄位型別在語義上有細微差別,這些語義差別是資料整合引擎的主要問題來源之一。


舉個例子,MySQL 沒有布林型別,通常我們用 tinyint(1)來實現布林型別,但轉換為布林型別還需要配置 JDBC 引數對 Connector 行為做動態的調整。當映象同步到 Hive 或者 Iceberg 的時候,欄位型別如果沒有匹配上,就可能會出現型別轉換的異常。


Unsigned 也是一個非常經典的問題,其中最容易出錯的是 Bigint Unsigned。Flink、Hive、Iceberg 都沒有無符號型別,如果考慮到精度問題,我們就只能使用 decimal(20,0)來儲存 Bigint Unsigned。但很多欄位設計成 Unsigned 只是希望保證這個欄位沒有負數值,並不會產生有符號數值溢位的情況,所以很多使用者仍然希望在 ODS 表中繼續使用 Bigint,這裡就很容易導致作業出現各種問題。


直到現在我們還有很多問題在解決中,對於資料整合引擎來說,解決異構資料系統對接帶來的各種問題,遮蔽這些資料系統之間的差異,是最核心也是最有挑戰的問題。


基於 Flink 的小米資料整合實踐

總結一下關鍵詞,資料整合領域的核心是連線,資料整合產品的核心問題是效率,資料整合引擎的核心問題是異構。各種生產實踐基本都可以對應到這三個層級。


基於 Flink 的小米資料整合實踐

Auto Catalog 特性是小米在資料整合領域的一個核心實踐,透過 Auto Catalog 特性可以大幅度提升涉及異構資料系統的作業的開發效率。


正中間的 SQL 就是透過 Auto Catalog 的方式,實現的 MySQL 寫入 Iceberg 的作業。右上角的語句是透過 Create Table 語法來引用 MySQL 表的方式,我們使用 Catalog 語法,也就是下面 SQL 中的“mysql_order.order.orders”三層結構來引用 MySQL 表的時候,就可以省略掉 Create Table 語句,在列比較多的情況下,這個 Create Table 語句構造很繁瑣也很容易出錯。


常規情況下使用 Catalog 語法,我們需要提前使用 Create Catalog 語句對 Catalog 進行註冊,左上角和下方的兩條語句就是 Create Catalog 語句。Auto Catalog 特性是在引擎中自動解析 SQL 中的 Catalog 進行自動註冊,這樣我們就可以省略這兩個 Create Catalog 語句。這不僅可以提高我們的開發效率,最主要還可以避免一些敏感資訊的洩露。


省略掉這三個 DDL 語句之後,整個 SQL 就變得非常簡潔了,對資料開發和資料查詢的效率提升都非常顯著。


基於 Flink 的小米資料整合實踐

Auto Catalog 特性的兩個環節都需要配合底層技術的支援。


第一個是在使用 Catalog 語法引用庫表的環節。Catalog 語法雖然很簡潔,但目前只有少數 Connector 原生提供了相應的 Catalog 實現。


這裡我們用了兩個措施:


  1. 基於 Netflix Metacat 建設了統一的後設資料服務,我們把連線資訊和賬號密碼等敏感資訊都儲存到 Metacat 裡,這就避免了對外暴露。


  2. 利用 HiveCatalog 的相容表特性,在 Flink 裡變相實現其他資料系統的 Catalog。主要做法就是用 Metacat 實現 HiveMetaStore 介面,這個做法有個缺點,就是增加了型別轉換的複雜度。比如原生提供的 Iceberg Catalog,它只需要關注 Iceberg 型別與 Flink 型別之間的雙向型別轉換,但如果用 Metacat,型別轉換過程就變成了原生型別、Metacat 型別、Hive 型別到最後的 Flink 型別的四重型別轉換,複雜度顯著提升。所以建議有原生 Catalog 的情況下,儘量使用原生 Catalog。


第二個是自動註冊 Catalog 的環節。手工構造 DDL 語句比較繁瑣,因為它需要連線資訊和賬號密碼,但正因如此構造 DDL 語句的過程本身就包含了授權步驟。而自動註冊 Catalog 就規避掉了這個鑑權過程,所以我們引入了 Apache Ranger,它是一個安全管理的框架。我們基於 Apache Ranger 建設了統一許可權機制,在 Flink SQL 中做了一個外掛,透過在 SQL 最佳化器中增加規則的方式,來實施表級別的鑑權,這樣我們就可以避免使用者去訪問無許可權的 Catalog 或者庫表了。


基於 Flink 的小米資料整合實踐

整合作業是我們最主要的資料整合產品,用於提供異構的資料來源匯入到 ODS 表的最佳實踐。整合作業底層引擎是基於 Flink SQL 的,但與常規的 Flink SQL 作業相比,它額外提供了三個特性:


  1. 映象同步,即在整合作業中整合了自動建立目標表的邏輯。在表的列非常多,或者包含一些很複雜的巢狀結構型別的情況下,這個特性可以節省很多工作量。


  2. 自動同步,即在源表的表結構發生變動的時候,自動將表結構的改動同步到目標表中。既可以保證資料的完整性,同時也減少了人工介入。


  3. 流批一體,即保障一次作業提交就可以完成整體同步,自動無縫銜接全量同步的批作業步驟和增量同步的流作業步驟。


這三個特性與常規的 Flink SQL 作業存在較大差異,所以我們在 Flink SQL 作業的基礎上整合成了一個整合作業的產品,並進而發展出了資料整合引擎。


基於 Flink 的小米資料整合實踐

MySQL 實時整合作業裡我們也多加了三個額外的特性。


第一個,專用採集賬號。MySQL 的 Binlog 採集許可權是整個 MySQL 例項級別的。也就是說只要有了採集許可權,就相當於有了該例項所有 DB 的讀許可權。在小米 MySQL 例項部署多個 DB 的情況非常普遍,對使用者賬號直接開放採集許可權的話,DBA 那邊是完全沒有辦法接受的。所以我們仍然沿用採集作業和整合作業分開的一個架構,中間透過訊息佇列對接。


這樣採集賬號就只在採集作業中使用,採集作業裡我們需要去做一個控制,即我們只向 Topic 輸出單個 DB 的 Binlog。透過這個方式,將採集許可權給限制在了 DB 的級別。


第二個,自動斷點續傳。我們與 DBA 平臺打通了獲取主從庫拓撲結構的介面。這樣我們就可以優先連線從庫進行採集,在從庫失效的情況下,我們還可以嘗試獲取其他可用的從庫做自動重連。但這裡有個前提是需要 MySQL 開啟了 GTID,GTID 是 MySQL 的全域性事務標誌,在主從庫中都能保持唯一,GTID 是自動斷點續傳的基礎。


第三個,千表同步連線問題。MySQL 例項上,如果建立的採集作業太多,就會給我們的服務造成壓力,所以我們需要儘可能複用採集作業。因為前面我們就提供了 DB 級別的 Binlog Topic,我們就直接共享了 DB 級別的 Binlog Topic。同一個 DB 上的所有表都會複用同一個 Binlog Topic。


在表特別多的情況下,Binlog Topic 的消費速度仍然有可能會成為瓶頸,因此我們在訊息佇列上還增加了按表過濾的環節,把訊息過濾的邏輯下推到訊息佇列的服務端執行,這樣能夠有效減少網路流量、提高消費速度。


整個架構裡,我們把採集作業的部分換成了 Flink CDC,但整體仍然是以訊息佇列為核心的架構。


基於 Flink 的小米資料整合實踐

分庫分表中介軟體主要有兩種實現。一種是將分片規則直接下發到 Client 端,這種情況中介軟體對外會直接暴露分表或者分庫的名稱。另外一種是基於代理的,對外展示為單庫單表,實際上是由代理服務去轉發請求到各個分庫或者分表裡。


第一種分庫分表的支援相對簡單一點,我們在 Catalog 語法中擴充了正則匹配的支援,可以顯著的提升這種場景的開發效率。


第二種基於代理的中介軟體就會麻煩很多,代理中介軟體不太友好的地方,一個是在實現細節上和 MySQL 服務端有很多差異,另外一個是透過代理服務也無法採集它的 Binlog。所以關鍵點還是依賴於前面提到的後設資料管理服務,就是我們需要透過 Metacat 去獲取它真實的拓撲結構。


我們現在的實現是增加一個特殊的字尾,把它真正的分庫分表的名稱暴露出來,在實際執行的時候,SQL 語句會被轉換成 UNION ALL 的形式後再執行。


基於 Flink 的小米資料整合實踐

上圖是 Doris 寫入支援分割槽覆蓋語義的案例。


Doris 本身不支援 OVERWRITE 這個語義,但在實際場景中,我們有很多使用者希望使用這個特性,而 Doris 本身又有類似的機制可以實現相似效果,只是目前的 Connector 還沒有支援。


我們在資料整合引擎里加了一個處理,將 OVERWRITE 這個語句轉換成等價於右邊的三個 SQL 語句的操作,用 Doris 的臨時分割槽特性來實現了 OVERWRITE 的語義。


基於 Flink 的小米資料整合實踐

前面兩個例子都是把輸入的 SQL 做了一些處理,實際執行的 SQL 是在資料整合引擎內生成的。這個機制,我們也同樣用在了自動同步的特性上。


這裡我們抽象出了一個叫 Schema Job 的概念。Schema Job 總是基於源表的表結構,按照最佳實踐生成一個目標表的表結構,再把目標表的表結構替換掉,跑完了 Schema Job 我們就可以認為源表和目標表的表結構已經保持一致了。


離線整合作業支援自動同步非常簡單,只需要在跑 Batch Job 之前執行一次 Schema Job 就可以了。


實時整合作業支援自動同步會稍微麻煩一點,仍然是先跑一個 Schema Job 把源表和目標表的表結構變成一致的,然後再跑起 Stream Job。當 Stream Job 退出的時候,我們需要做一個額外的判斷,如果 Stream Job 是因為 Schema 變更而退出的,我們就再排程一個 Schema Job 去保持表結構一致,然後再嘗試按照新的表結構跑起 Stream Job,就這樣一直迴圈下去。


這裡有一個細節,對於資料庫場景,在發生 DDL 變更時,通常在 CDC Connector 裡可以採集到一條 DDL 訊息,我們可以用這個 DDL 訊息觸發 Stream Job 的退出。但在訊息佇列場景裡,訊息體的結構變更是不會產生類似 DDL 的訊息的,這個時候如果我們不做任何處理,這個作業會一直正常的執行下去,但這些新的欄位可能就被遺漏掉或者丟棄掉了。


這個時候我們就依賴一個叫做 fail-on-unknown-field 的特性,設定了這個特性之後,我們會實時檢查訊息結構體中是否有 SQL 中沒有定義的欄位。當檢測到未知欄位後,我們就會令當前的 Stream Job 失敗,嘗試觸發 Schema Job 的迴圈。


我們在半結構化的資料接入場景上非常依賴這個特性,舉例一個非常經典的場景:


很多業務的後端團隊和資料團隊在組織架構上是分開的,中間透過訊息佇列做資料對接。這種場景裡,訊息的生產端是後端團隊在負責,消費端是資料團隊自己建作業去消費,訊息體很多情況下就是某個核心領域模型的 JSON。


在很多情況下,後端團隊更新領域模型後,資料團隊是不知道的。不做額外處理的情況下,Flink SQL 作業會一直正常執行並忽略訊息體中的新欄位,甚至在開啟了 ignore-parse-errors 特性時可能導致整個訊息都被丟棄。在這個場景裡我們就可以用 fail-on-unknown-field 特性將作業主動失敗掉,然後提示使用者更新訊息體的 Schema。


基於 fail-on-unknown-field 特性實施 Schema Evolution 有兩個前提,第一個是訊息體結構變更不會特別頻繁,第二個是訊息體結構變更本身是能夠向前相容的。如果不滿足這兩個前提,這套方案的可靠性就有很大的隱患。


這種情況下我們需要回歸到 Schema On Read 的高可靠性方案,也就是基於 Hive/HDFS 的方案。Hive 本身有一套非常成熟的 Schema On Read 的工具包,Schema On Read 在寫鏈路上不需要解析訊息的結構,直接把整個訊息體按行存的格式寫入到 HDFS 檔案上,只在讀這些檔案的時候才需要用到 Schema 去嘗試解析。這樣即使我們的 Schema 與訊息體不匹配,也只是影響解析出來的資料,原始資料本身是不會丟失的。


比較可惜的是,目前的幾個主流資料湖技術都是基於列式儲存的,沒有現成可用的 Schema On Read 方案,這也是我們後期可能要去擴充的一個方向。


基於 Flink 的小米資料整合實踐

這裡再分享一下 TiDB 百億級單表實時整合的案例。


TIDB 是一款非常優秀的國產分散式開源資料庫,從資料整合的角度來看,它有兩個非常顯著的特點:第一是單表的資料量能夠支援非常大的規模,可以上到百億行/數十 TB 的規模;第二是支援快照機制,這對流批一體是非常友好的特性。


我們在 TIDB 實時整合的開發過程中,碰到的主要困難都是在全量同步步驟中寫入 Iceberg 的過程發生的。這裡最主要的問題是,Iceberg 的 Flink Connector 實現只提供了 Stream Writer,Stream Writer 在資料量巨大的批處理場景下的效能比較差,我們主要做了兩個最佳化。


上圖展示的是 write-distribution-mode 的最佳化,從上圖可以看到整合作業的邏輯非常簡單,作業透過 TableSourceScan 從 TIDB 讀資料,再透過 IcebergStreamWriter 往 Iceberg 裡寫資料。TableSourceScan 在讀到資料之後,怎麼把資料傳送給 IcebergStreamWriter 呢?這裡就是 Iceberg 的 write-distribution-mode 的配置。


目前有兩種模式,左上方是 None 模式,這個模式裡 Writer 不佔用單獨的 stage,而是直接在 TableSourceScan 的 TaskManager 上寫入 Iceberg 中。這個模式少了一個 shuffle 階段,如果 TableSourceScan 的資料分佈比較均勻,它的入湖速度就會非常快。但因為 Iceberg 每個 Writer 寫入每個分割槽的時候都會產生一批寫入檔案,這樣寫入檔案數量就等於 Partition 數量乘以 Writer 的數量。當表規模很大的時候就會產生大量的小檔案,對 Compaction 和 HDFS NameNode 造成很大的壓力。


左下方是 Hash 模式,這個模式專門為小檔案數量做最佳化,保證每個 Partition 只能由一個 Writer 寫入。但分配 Partition 到 Writer 時是用的雜湊演算法進行分配,因為 Partition 的數量本身就非常少,用雜湊演算法去分配的時候,幾乎無法避免的會產生資料傾斜的問題。


這兩種模式在流場景下表現都很不錯,但是在 TIDB 的全量同步的過程中,它的問題就會被放大到令我們無法接受。所以我們就引入了 RoundRobin 模式,主要還是在雜湊模式的基礎上去解決資料傾斜的問題。


我們分析了幾個最常見的分割槽函式,透過設定一個特殊排序,按照順序逐個把 Partition 分配到 Writer,來確保 Partition 與 Writer 的均衡。這裡用到了 PartitionCustom 的分割槽方法,透過自定義的 Partitioner 對分割槽進行匹配,目前完成適配的分割槽函式如下:


  1. Bucket 分割槽函式,只需要將 bucket_id 按 Writer 數量取模就可以達到理論上最好的均衡效果。


  2. Truncate 分割槽函式,只能支援數值型,用分割槽名稱除以分割槽函式的寬度,就可以得到一個連續的整數值,再按 Writer 數量取模即可,這個方式在常見場景和合理配置下可以達到最優的均衡效果,但如果寬度設定過大,反而可能導致資料被集中在少數 Writer 中。


  3. Identity 分割槽函式,只能支援數值型,將分割槽名稱代表的數值取整,再按 Writer 數量取模,當分割槽名稱連續變化時效果比較好,分割槽名稱是離散值時效果較差。


RoundRobin 模式在常見場景的效果非常顯著,實際測出來的效能相比前兩者能有三倍的提升。


基於 Flink 的小米資料整合實踐

上圖左邊展示的是 Iceberg 實現 Row Level Delete 的核心邏輯。Iceberg 有個 Delete Storage 來快取 Checkpoint 期間的所有新增操作,更新和刪除操作會根據 Delete Storage 中是否有相應的記錄,決定是寫入到 eq-delete-file 還是 pos-delete-file,正常情況下 Iceberg 一次 Checkpoint 會提交三個資料檔案。


在做大表的全量同步時,Delete Storage 經常快取了太多資料觸發 OOM,我們最終決定在全量同步的階段跳過 Delete Storage 的步驟,因為全量同步階段只有新增,沒有更新和刪除,實際上用不到 Delete Storage。


這個效果非常顯著,我們終於成功的支援了百億級別單表的全量同步。


基於 Flink 的小米資料整合實踐

但跳過 Delete Storage 會帶來一個問題,增量同步過程是必須依賴 Delete Storage 的,這就導致全量同步和增量同步無法一起執行,基於 HybridSource 的方案就不適合使用這個最佳化措施了。


我們只能將 TIDB 實時整合作業拆分成兩個單獨的作業:Batch Job 和 Stream Job。


Batch Job 基於快照讀取 TiDB 的全量資料,它會配置前面說的各個最佳化項,並行度設定也會大一點。


Batch Job 執行完成後,再執行 Stream Job,Stream Job 從訊息佇列中按照快照時間點接上全量同步的進度,繼續消費 CDC 事件執行實時同步的步驟,這個階段的配置是單獨最佳化的,並行度也會設定的相對小一些。


我們將這兩個作業的調動邏輯放在 Flink Application 中實施,這樣在使用者層面看起來就只排程了一個作業,但在實際執行的過程中,Flink Application 會按需排程不同的 Flink Job。


基於 Flink 的小米資料整合實踐

這裡重溫一下 Flink Application 這個概念,在 Flink on Yarn 模式中,Flink 作業的 jar 包提交到 Yarn 叢集后,在 main 方法中跑的邏輯就是 Flink Application。Flink Application 跑在 JobManager 的節點上,但邏輯上仍然是兩個獨立的模組。而且我們可以在 Flink Application 中提交多個 Flink Job。


目前我們在 Flink Application 中是序列排程各個 Flink Job,這樣在狀態恢復的時候就會比較簡單,因為每次只有一個 Flink Job 需要恢復。從本質上來說,外部排程 Flink Job 也能達到完全一致的效果,只是 Flink Application 中剛好有一個比較合適的地方可以放這些邏輯。


小米資料整合引擎的核心邏輯就是跑在 Flink Application 中的。


03

引擎設計


基於 Flink 的小米資料整合實踐

上圖是小米資料整合引擎的總體架構圖,目前稱這個引擎為 MIDI(Mi Data Integration)。MIDI 的核心邏輯都跑在 Flink Application 中,Flink Application 會在適當的時間排程三種作業:Batch Job、Stream Job、Schema Job。


MIDI 的輸入我們稱之為 MIDI SQL,是在 Flink SQL 的基礎上增加了一些自定義語法,MIDI SQL 目前只支援簡單的資料整合場景。


從 MIDI SQL 中我們會解析出三張表,Source Table 是上圖最左邊源資料系統中的表,Sink Table 是右邊的目標資料系統中的表,Middle Table 是下方的長條,代表的是包含源表 CDC 事件的 Topic。此外還有一個叫做 Application State Backend 的概念,主要用來記錄 Flink Job 的執行情況。


基於 Flink 的小米資料整合實踐

上圖是一個典型的資料庫實時整合作業的時序圖,包含了自動同步和流批一體的特性。


MIDI 首先會排程一個 Schema Job 去保證源表和目標表的表結構完全一致,然後生成 Batch SQL 並排程一個 Batch Job 來執行全量同步的步驟。全量同步步驟採用批模式執行,並行度設定相對高一些。


之後 MIDI 會獲取全量同步的進度點,然後按進度點生成對應的 Stream SQL,並排程 Stream Job 接上之前的進度繼續執行實時的增量同步的步驟。實時同步階段採用流模式執行,並行度設定相對會低一點。


MIDI 執行完每一個 Flink Job 都會記錄一個執行日誌,也就是 Flink Application State Backend 的作用。它實際上就是一個文字檔案,與 Checkpoint 目錄放在一起。當作業中斷後再恢復的時候,MIDI 會先從執行日誌裡找到當時正在跑的那個 Flink Job,再去執行相應的恢復動作。


再回到時序圖裡。如果源表 Schema 沒有任何變更,Stream Job 跑起來之後會一直執行。當源表發生了 Schema 變更,就會觸發作業退出,然後進入一個迴圈。我們會嘗試排程 Schema Job 來完成表結構同步,再基於新的表結構排程 Stream Job。這樣我們就能始終保持當前正在跑的 Stream Job,一定是以最新的表結構在進行同步。


在這個設計思路下,Stream Job 成為了一種特殊的有界流,Stream Job 的生命週期與它執行的 Stream SQL 的 Schema 是強繫結的,Schema 失效後,Stream Job 也就相應的結束退出了。


基於 Flink 的小米資料整合實踐

這是我們定義的 MIDI-SQL,主要引入三個自定義語法:Auto、Stream、Stream With。


Auto 語法是用來開啟自動同步的,它必須與“Select *”共同使用。在實際執行的時候,“Auto *”會被替換為當前最新的表結構欄位。


Stream 語法其實早就被 Flink 拋棄了,現在 Flink SQL 語法上並不區分批作業和流作業,主要以 Source 是否為有界流來確定執行模式。但 MIDI 因為使用了 Catalog 語法來引用庫表,同樣的 SQL 語句,使用 CDC Connector 時就是流模式,使用 JDBC Connector 時就是批模式,我們無法透過 SQL 語句區分兩種情況,所以就把 Stream 語法又加回來了,用來判斷執行模式,在 Catalog 中選擇不同的 Connector。


Stream With 語法是用來實施流批一體的,MIDI 的流批一體方案還是基於訊息佇列的,我們用這個語法將訊息佇列與源表關聯起來。


04

未來規劃


基於 Flink 的小米資料整合實踐

未來我們將對上圖提到的幾點進行探索,這裡重點提三個點:


  1. Schema On Read 場景支援:基於 Flink 和資料湖的方案更適合的是結構化資料整合的場景,在半結構化和非結構化場景裡,Schema On Read 仍然是一個最佳實踐,未來我們希望繼續探索如何在資料湖技術上提供 Schema On Read 的支援。


  2. 智慧資料補償:這是我們嘗試增加的第四種 Flink Job,我們希望定時執行這個步驟,自動的增量的對源表和目標表的資料做比對和補償。


  3. 引擎特性打磨:MIDI 目前仍然在比較初期的階段,很多特性還需要打磨,目前正在整理部分特性反饋到社群共同建設。


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

相關文章