Apache Hudi在信息服務行業構建流批一體的實踐

個人介紹

李昂
高級數據研發工程師
Apache Doris & Hudi Contributor

業務背景

部門成立早期, 爲了應對業務的快速增長, 數倉架構採用了最直接的Lambda架構

  1. 對數據新鮮度要求不高的數據, 採用離線數倉做維度建模, 採用每小時調度binlog+每日主鍵歸併的方式實現T+1數據更新
  2. 對數據時效性要求比較高的業務, 採用實時架構, 保證增量數據即時更新能力, 另一方面, 爲了保證整體上線效率, 存量數據採用離線SQL處理, 以提高計算吞吐量

Lambda整體架構如下


此時的架構存在以下缺陷

  1. **邏輯冗餘 : **同一個業務方案, 往往有離線與實時兩套開發邏輯, 代碼複用性低, 需求迭代成本大, 任務交接、項目管理複雜
  2. **數據不一致 : **應用層數據來源有多條鏈路, 在處理邏輯異構的情況下, 存在數據不一致的問題, 且問題排查成本大, 週期長
  3. **數據孤島 : **隨着業務增長, 爲了應對離線批處理、OLAP分析、C端高併發點查等場景, 引入的存儲引擎越來越多, 存在數據孤島

基於上述Lambda架構存在的缺陷, 我們希望對其作出改進, 實現以下目的

  1. 流批一體 : 同一個業務方案, 可以由一套代碼邏輯或者核心邏輯一致的SQL實現
  2. 數據整合 : 統一離線批處理與OLAP分析的數據存儲口徑, 同時查詢支持SparkSQL與Doris-Multi-Catalog, 打破數據孤島

選型調研

對比項 \ 選型 Apache Hudi Apache Iceberg Apache Paimon
增量實時upsert支持&性能 較好
存量離線insert支持&性能 較好
增量消費 支持 依賴Flink State 支持
社區活躍度 Fork 2.4K , Star 4.6K Fork 1.8K , Star 4.9K Fork 0.6K , Star 1.4K
Doris-Multi-Catalog支持 1.2+ 支持 1.2+ 支持 2.0+ 支持

綜合考慮以下幾點

  1. 項目成熟度 : 社區活躍度、國內Committer數量、國內羣聊活躍度、各公司最佳實踐發文等
  2. 數據初始化能力 : 考慮到需要對歷史項目進行覆蓋, 需要考慮存量數據寫入能力
  3. 數據更新能力 : 一方面是數據根據PrimaryKey或者UniqueKey的實時Upsert、Delete性能, 另一方面是Compaction性能
  4. CDC : 如果需要分層處理, 則要求數據湖作爲Flink Source時有產生撤回流的能力

我們最終選定使用Apache Hudi作爲數據湖底座

方案選型

業務痛點

實時流 join 是事實數倉的痛點之一, 在我們的場景下, 一條事實數據, 需要與多個維度的數據做關聯, 例如一場司法拍賣, 需要關聯企業最新名稱、董監高、企業性質、上市信息、委託法院、詢價評估機構等多個維度;一方面, 公司與董監高是1:N的對應關係, 無法實現一條寫入, 多條更新; 另一方面, 企業最新名稱的變更, 可能涉及到歷史冷數據的更新

方案設計

FlinkSQL+離線修復

方案描述
通過FlinkSQL實現增量數據的計算, 每日因爲狀態TTL過期或者lookup表變更而沒有被命中的數據, 通過凌晨的離線調度進行修復

優點

  1. SQL開發 : 便於維護
  2. 架構簡潔 : 不涉及其他非必要組件

缺點

  1. **批流沒有完全一體 : **同一邏輯仍然並存FlinkSQL與SparkSQL兩種執行方式
  2. **維護Flink大狀態 : **爲保證數據儘可能的join到, 需要設置天級甚至周級的TTL
  3. **時效性下限較低 : **最差仍然可能存在T+1的延遲

MySQL中間表

方案描述
使用MySQL實現數倉分層, 爲每張上游表, 都開發lookup邏輯, Hudi只負責做MySQL表的鏡像


優點

  1. 真正流批一體 : 整個鏈路徹底擺脫離線邏輯
  2. **時效性最高 : **所有更新都能及時反映到下游

缺點

  1. 維護成本大 : 每張Hudi表都鏡像於一張MySQL表, 鏈路加長, 複雜度提高
  2. 存儲冗餘 : 每張表各在MySQL與Hudi存一份, 同時, lookup還需要索引支撐, 磁盤佔用高

最終結論

  1. 因爲C端業務的特殊性, 需要MySQL提供點查能力, 所以第二種方案的磁盤冗餘處於可接受範圍
  2. 第一種方案T+1的下限無法被接受, 若提高離線修復的頻率, 考慮到Flink已經維護大狀態, 或將需要較大的內存開銷

所以最終方案選定爲第二種 : **MySQL中間表方案**, 優化後的整體架構如下

  1. ODS層的Hudi充當一個Queryable Kafka, 提供CDC給下游數據
  2. 實時ETL通過MySQL完成, 對於每一張新的結果表, 都會原樣鏡像一份到Hudi
  3. Doris與Hive通過讀RO表完成與Hudi的統一集成

方案實施

增量實時寫入

table.type

根據上述方案, 我們的數據寫入是完全鏡像於每個flink job的產出MySQL表, 絕大部分表日更新量在50w~300w, 爲了保證寫入的穩定性, 我們決定採用MOR表

index.type

在選擇index的時候, 因爲BLOOM隨着數據量的上升, 瓶頸出現比較快, 我們的候選方式有FLINK_STATE與BUCKET, 綜合考慮以下幾點要素

  1. **數據量 : **當數據量超過5e, 社區的推薦方案是使用BUCKET, 目前我們常見的表數據量浮動在2e - 4e
  2. **維護成本 : **使用Flink_STATE作爲index時, 程序重啓如果沒有從檢查點恢復, 需要開始bootstrap重新加載索引
  3. **資源佔用 : **爲保證穩定, FLINK_STATE需要TaskManager劃分0.5~1G左右的內存用於運行Rocksdb, 而BUCKET則幾乎不需要狀態開銷
  4. **橫向擴展 : **bucket_num一經確認, 則無法更改(高版本的CONSISTENT_HASHING BUCKET依賴Clustering可以實現動態bucket_num), FLINK_STATE無相關概念

我們最終選擇使用BUCKET

  1. 它不與RocksDB綁定, 資源佔用較低
  2. 不需要bootstrap, 便於維護
  3. 考慮到數據量與橫向擴展, 我們預估數據量爲5e~10e, 在該場景下, BUCKET會有更好的表現

同時, 綜合參考社區推薦與相關最佳實踐的文獻,

  1. 我們限制每個Parquet文件在2G以內
  2. 假設Parquet+Gzip的壓縮比率在5:1
  3. 預估數據量在10e量級的表

最終, 我們設置bucket_num爲128

離線寫入

爲了快速整合到歷史已經上線的表, 存量數據的快速導入同樣也是必不可少的, 通過官網學習, 我們設計了兩種方案

  1. **bulk_insert : **優點是速度快, 沒有log小文件, 缺點是不夠便捷, 需要學習和引入成本
  2. **大併發的upsert : **優點是隻需要加大並行度, 使用最簡單, 缺點是產生大量小文件, 寫入完畢後第一次compaction非常耗費資源

在分別對上述2種方案進行測試後, 我們決定採用bulk_insert的方式, 最大的因素還是大併發的Upsert在第一次寫入後, 需要的compaction資源非常大, 需要在第一次compaction後再次調整運行資源, 不便於自動化

Compaction

  • 同步
    • **優點 : **便於維護
    • **缺點 : **流量比較大的時候, 干擾寫流程; 在存量數據大, 增量數據小的情況下, 資源難以分配
  • 異步
    • **優點 : **與同步任務隔離, 不干擾寫流程, 可自由配置資源
    • **缺點 : **對於每個表, 都需要單獨維護一個定時任務

綜合考慮運維難度與資源分配後, 我們決定採用異步調度的方式, 因爲我們讀的都是RO表, 所以對Compaction頻率和單次Compaction時間都有限制, 目前的方案是Compacion Plan由同步任務生成, Checkpoint Interval爲1分鐘, 觸發策略爲15次Commits

成果落地

流批一體

整合實時鏈路與離線鏈路, 所有產出表均由實時邏輯產出

  • 開發工時由之前普遍的離線2PD+實時3PD提升至實時3PD, 效率提升40%
  • 每個單元維護成本由1名實時組同學+1名離線組同學變更爲只需要1位實時組同學, 維護成本節約50%

數據整合

配合Doris多源Catalog, 完成數據整合, 打破數據孤島

  • 使司內Doris集羣完成存算分離與讀寫分離, 節約磁盤資源30T+, 集羣故障率由最高3次/月降至1次/月, 穩定性提升70%
  • 下線高性能(SSD存儲)Hbase與GaussDB, 節約成本50w/年

平衡計算壓力

之前Hive的每日數據由單獨離線集羣通過凌晨的多路歸併完成多版本合併, 目前只需要一個實時集羣

  • 退訂離線集羣70%彈性節點, 節約成本30w/年

經驗總結

Checkpoint反壓優化

在我們測試寫入的時候, Checkpoint時間比較長, 而且會有反壓產生, 追蹤StreamWriteFunction.processElement()方法, 發現數據緩情況如下

爲了將flush的壓力分攤開, 我們的方案就是減小buffer

ps : 默認write.task.max.size必須大於228M
最終的參數 :

-- index
'index.type' = 'BUCKET',
'hoodie.bucket.index.num.buckets' = '128',
-- write
'write.tasks' = '4',
'write.task.max.size' = '512',
'write.batch.size' = '8',
'write.log_block.size' = '64',

FlinkSQL TIMESTAMP類型兼容性

當表結構中有TIMESTAMP(0)數據類型時, 在使用bulk_insert寫入存量數據後, 對接upsert流並進行compaction時, 會報錯

Caused by: java.lang.IllegalArgumentException: INT96 is deprecated. As interim enable READ_INT96_AS_FIXED flag to read as byte array.

提交issue https://github.com/apache/hudi/issues/9804 與社區溝通
最終發現是TIMESTAMP類型, 目前只對TIMESTAMP(3)與TIMESTAMP(6)進行了parquet文件與avro文件的類型標準化
解決方法是暫時使用TIMESTAMP(3)替代TIMESTAMP(0)

Hudi Hive Sync Fail

將Hudi表信息同步到Hive原數據時, 遇到報錯, 且無法通過修改pom文件依賴解決

java.lang.NoSuchMethodError: org.apache.parquet.schema.Types$PrimitiveBuilder.as(Lorg/apache/parquet/schema/LogicalTypeAnnotation;)Lorg/apache/parquet/schema/Types$Builder

與社區溝通, 發現了相同的問題 https://github.com/apache/hudi/issues/3042
解決方法是修改源碼的 packaging/hudi-flink-bundle/pom.xml , 加入

<relocation>
  <pattern>org.apache.parquet</pattern>
  <shadedPattern>${flink.bundle.shade.prefix}org.apache.parquet</shadedPattern>
</relocation>

並使用

mvn clean install package -Dflink1.17 -Dscala2.12 -DskipTests -Drat.skip=true -Pflink-bundle-shade-hive3 -T 10

手動install源碼, 在程序的pom文件中, 使用自己編譯的jar包

Hudi Hive Sync 使用 UTC 時區

當使用FlinkSQL TIMESTAMP(3)數據類型寫入Hudi, 並開啓Hive Sync的時, 查詢Hive中的數據, timestamp類型總是比原值多8小時
原因是Hudi寫入數據時, 支持UTC時區, 詳情見issue https://github.com/apache/hudi/issues/9424
目前的解決方法是寫入數據時, 使用FlinkSQL的 CONVERT_TZ 函數

insert into dwd
select
id,CAST(CONVERT_TZ(CAST(op_ts AS STRING), 'Asia/Shanghai', 'UTC') AS TIMESTAMP(3)) op_ts
from ods;

HoodieConfig.setDefaults() NPE

在TaskManager初始化階段, 偶爾遇到NPE, 且調用棧如下

java.lang.NullPointerException: null
at org.apache.hudi.common.config.HoodieConfig.setDefaults(HoodieConfig.java:123)

通過與社區交流, 發現是ReflectionUtils的CLAZZ_CACHE使用HashMap存在線程安全問題
解決方法是引入社區提供的PR : https://github.com/apache/hudi/pull/9786 通過ConcurrentHashMap解除線程安全問題

未來規劃

Metric監控

對接Pushgateway、Prometheus與Grafana, 通過圖形化更直截了當的監控Hudi內部相關服務、進程的內存與CPU佔用情況, 做到

  1. 優化資源, 提升程序穩定性
  2. 排查潛在不確定因素, 風險預判
  3. 接入告警, 加速問題響應

統一元數據管理

目前是採用封裝工具類的方式, 讓每個開發同學在產出一張結果表的同時, 在同一個job中啓動一條Hudi同步鏈路, 缺少對Hudi同步任務的統一管理與把控, 後續準備對所有Hudi鏈路遷出, 進行統一的任務整合與元數據管理

引入CONSISTENT_HASHING BUCKET

後續計劃中我們希望在1.0發行版中可以正式將CONSISTENT_HASHING BUCKET投入到線上環境, 現在線上許多3e~5e量級的表都是提前按照10e數據量來預估資源與bucket_num, 有資源浪費的情況, 希望可以通過引入一致性hash的bucket索引, 來解決上述問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章