個人介紹
李昂
高級數據研發工程師
Apache Doris & Hudi Contributor
業務背景
部門成立早期, 爲了應對業務的快速增長, 數倉架構採用了最直接的Lambda架構
- 對數據新鮮度要求不高的數據, 採用離線數倉做維度建模, 採用每小時調度binlog+每日主鍵歸併的方式實現T+1數據更新
- 對數據時效性要求比較高的業務, 採用實時架構, 保證增量數據即時更新能力, 另一方面, 爲了保證整體上線效率, 存量數據採用離線SQL處理, 以提高計算吞吐量
Lambda整體架構如下
此時的架構存在以下缺陷
- **邏輯冗餘 : **同一個業務方案, 往往有離線與實時兩套開發邏輯, 代碼複用性低, 需求迭代成本大, 任務交接、項目管理複雜
- **數據不一致 : **應用層數據來源有多條鏈路, 在處理邏輯異構的情況下, 存在數據不一致的問題, 且問題排查成本大, 週期長
- **數據孤島 : **隨着業務增長, 爲了應對離線批處理、OLAP分析、C端高併發點查等場景, 引入的存儲引擎越來越多, 存在數據孤島
基於上述Lambda架構存在的缺陷, 我們希望對其作出改進, 實現以下目的
- 流批一體 : 同一個業務方案, 可以由一套代碼邏輯或者核心邏輯一致的SQL實現
- 數據整合 : 統一離線批處理與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+ 支持 |
綜合考慮以下幾點
- 項目成熟度 : 社區活躍度、國內Committer數量、國內羣聊活躍度、各公司最佳實踐發文等
- 數據初始化能力 : 考慮到需要對歷史項目進行覆蓋, 需要考慮存量數據寫入能力
- 數據更新能力 : 一方面是數據根據PrimaryKey或者UniqueKey的實時Upsert、Delete性能, 另一方面是Compaction性能
- CDC : 如果需要分層處理, 則要求數據湖作爲Flink Source時有產生撤回流的能力
我們最終選定使用Apache Hudi作爲數據湖底座
方案選型
業務痛點
實時流 join 是事實數倉的痛點之一, 在我們的場景下, 一條事實數據, 需要與多個維度的數據做關聯, 例如一場司法拍賣, 需要關聯企業最新名稱、董監高、企業性質、上市信息、委託法院、詢價評估機構等多個維度;一方面, 公司與董監高是1:N的對應關係, 無法實現一條寫入, 多條更新; 另一方面, 企業最新名稱的變更, 可能涉及到歷史冷數據的更新
方案設計
FlinkSQL+離線修復
方案描述
通過FlinkSQL實現增量數據的計算, 每日因爲狀態TTL過期或者lookup表變更而沒有被命中的數據, 通過凌晨的離線調度進行修復
優點
- SQL開發 : 便於維護
- 架構簡潔 : 不涉及其他非必要組件
缺點
- **批流沒有完全一體 : **同一邏輯仍然並存FlinkSQL與SparkSQL兩種執行方式
- **維護Flink大狀態 : **爲保證數據儘可能的join到, 需要設置天級甚至周級的TTL
- **時效性下限較低 : **最差仍然可能存在T+1的延遲
MySQL中間表
方案描述
使用MySQL實現數倉分層, 爲每張上游表, 都開發lookup邏輯, Hudi只負責做MySQL表的鏡像
優點
- 真正流批一體 : 整個鏈路徹底擺脫離線邏輯
- **時效性最高 : **所有更新都能及時反映到下游
缺點
- 維護成本大 : 每張Hudi表都鏡像於一張MySQL表, 鏈路加長, 複雜度提高
- 存儲冗餘 : 每張表各在MySQL與Hudi存一份, 同時, lookup還需要索引支撐, 磁盤佔用高
最終結論
- 因爲C端業務的特殊性, 需要MySQL提供點查能力, 所以第二種方案的磁盤冗餘處於可接受範圍
- 第一種方案T+1的下限無法被接受, 若提高離線修復的頻率, 考慮到Flink已經維護大狀態, 或將需要較大的內存開銷
所以最終方案選定爲第二種 : **MySQL中間表方案**
, 優化後的整體架構如下
- ODS層的Hudi充當一個Queryable Kafka, 提供CDC給下游數據
- 實時ETL通過MySQL完成, 對於每一張新的結果表, 都會原樣鏡像一份到Hudi
- Doris與Hive通過讀RO表完成與Hudi的統一集成
方案實施
增量實時寫入
table.type
根據上述方案, 我們的數據寫入是完全鏡像於每個flink job的產出MySQL表, 絕大部分表日更新量在50w~300w, 爲了保證寫入的穩定性, 我們決定採用MOR表
index.type
在選擇index的時候, 因爲BLOOM隨着數據量的上升, 瓶頸出現比較快, 我們的候選方式有FLINK_STATE與BUCKET, 綜合考慮以下幾點要素
- **數據量 : **當數據量超過5e, 社區的推薦方案是使用BUCKET, 目前我們常見的表數據量浮動在2e - 4e
- **維護成本 : **使用Flink_STATE作爲index時, 程序重啓如果沒有從檢查點恢復, 需要開始bootstrap重新加載索引
- **資源佔用 : **爲保證穩定, FLINK_STATE需要TaskManager劃分0.5~1G左右的內存用於運行Rocksdb, 而BUCKET則幾乎不需要狀態開銷
- **橫向擴展 : **bucket_num一經確認, 則無法更改(高版本的CONSISTENT_HASHING BUCKET依賴Clustering可以實現動態bucket_num), FLINK_STATE無相關概念
我們最終選擇使用BUCKET
- 它不與RocksDB綁定, 資源佔用較低
- 不需要bootstrap, 便於維護
- 考慮到數據量與橫向擴展, 我們預估數據量爲5e~10e, 在該場景下, BUCKET會有更好的表現
同時, 綜合參考社區推薦與相關最佳實踐的文獻,
- 我們限制每個Parquet文件在2G以內
- 假設Parquet+Gzip的壓縮比率在5:1
- 預估數據量在10e量級的表
最終, 我們設置bucket_num爲128
離線寫入
爲了快速整合到歷史已經上線的表, 存量數據的快速導入同樣也是必不可少的, 通過官網學習, 我們設計了兩種方案
- **bulk_insert : **優點是速度快, 沒有log小文件, 缺點是不夠便捷, 需要學習和引入成本
- **大併發的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佔用情況, 做到
- 優化資源, 提升程序穩定性
- 排查潛在不確定因素, 風險預判
- 接入告警, 加速問題響應
統一元數據管理
目前是採用封裝工具類的方式, 讓每個開發同學在產出一張結果表的同時, 在同一個job中啓動一條Hudi同步鏈路, 缺少對Hudi同步任務的統一管理與把控, 後續準備對所有Hudi鏈路遷出, 進行統一的任務整合與元數據管理
引入CONSISTENT_HASHING BUCKET
後續計劃中我們希望在1.0發行版中可以正式將CONSISTENT_HASHING BUCKET投入到線上環境, 現在線上許多3e~5e量級的表都是提前按照10e數據量來預估資源與bucket_num, 有資源浪費的情況, 希望可以通過引入一致性hash的bucket索引, 來解決上述問題