KLOOK客路旅行基於Apache Hudi的資料湖實踐

leesf發表於2022-05-12

1. 業務背景介紹

客路旅行(KLOOK)是一家專注於境外目的地旅遊資源整合的線上旅行平臺,提供景點門票、一日遊、特色體驗、當地交通與美食預訂服務。覆蓋全球100個國家及地區,支援12種語言和41種貨幣的支付系統,與超過10000家商戶合作伙伴緊密合作,為全球旅行者提供10萬多種旅行體驗預訂服務。
KLOOK數倉RDS資料同步是一個很典型的網際網路電商公司數倉接入層的需求。對於公司數倉,約60%以上的資料直接來源與業務資料庫,資料庫有很大一部分為託管的AWS RDS-MYSQL 資料庫,有超100+資料庫/例項。RDS直接通過來的資料通過標準化清洗即作為數倉的ODS層,公司之前使用第三方商業工具進行同步,限制為每隔8小時的資料同步,無法滿足公司業務對資料時效性的要求,資料團隊在進行調研及一系列poc驗證後,最後我們選擇Debezium+Kafka+Flink+Hudi的ods層pipeline方案,資料秒級入湖,後續數倉可基於近實時的ODS層做更多的業務場景需求。

2. 架構改進

2.1 改造前架構

整體依賴於第三服務,通過Google alooma進行RDS全量增量資料同步,每隔8小時進行raw table的consolidation,後續使用data flow 每24小時進行刷入數倉ODS層

2.2 新架構

  1. 使用AWS DMS 資料遷移工具,將全量RDS Mysql 資料同步至S3儲存中;
  2. 通過Flink SQL Batch 作業將S3資料批量寫入Hudi 表;
  3. 建立Debeizum MySQL binlog 訂閱任務,將binlog 資料實時同步至Kafka;
  4. 通過Flink SQL 啟動兩個流作業,一個將資料實時寫入Hudi,另一個作業將資料追加寫入到S3,S3 binlog檔案儲存30天,以備資料回溯使用;
  5. 通過hive-hudi meta data sync tools,同步hudi catalog資料至Hive,通過Hive/Trino提供OLAP資料查詢。

2.3 新架構收益

  • 資料使用及開發靈活度提升,地方放同步服務限制明顯,改進後的架構易於擴充套件,並可以提供實時同步資料供其它業務使用;
  • 資料延遲問題得到解決,基於Flink on Hudi 的實時資料寫入,對於RDS資料攝入數倉可以縮短至分鐘甚至秒級,對於一些庫存、風控、訂單類的資料可以更快的進行資料取數分析,整體從原來近8小時的consolidation縮減至5分鐘
  • 成本更加可控,基於Flink on Hudi存算分離的架構,可以有效通過控制對資料同步計算處理資源配額、同步重新整理資料表落盤時間、資料儲存冷熱歸檔等進行成本控制,與第三方服務成本整體對比預計可以縮減40%

3. 實踐要點

3.1 Debezium 增量Binlog同步配置

Kafka connect 關鍵配置資訊

bootstrap.servers=localhost:9092
# unique name for the cluster, used in forming the Connect cluster group. Note that this must not conflict with consumer group IDs
group.id=connect-cluster
# The converters specify the format of data in Kafka and how to translate it into Connect data. Every Connect user will
# need to configure these based on the format they want their data in when loaded from or stored into Kafka
key.converter=org.apache.kafka.connect.json.JsonConverter
value.converter=org.apache.kafka.connect.json.JsonConverter
# Converter-specific settings can be passed in by prefixing the Converter's setting with the converter we want to apply
key.converter.schemas.enable=true
value.converter.schemas.enable=true
# Topic to use for storing offsets. This topic should have many partitions and be replicated and compacted.
# Kafka Connect will attempt to create the topic automatically when needed, but you can always manually create
# the topic before starting Kafka Connect if a specific topic configuration is needed.
# Most users will want to use the built-in default replication factor of 3 or in some cases even specify a larger value.
# Since this means there must be at least as many brokers as the maximum replication factor used, we'd like to be able
# to run this example on a single-broker cluster and so here we instead set the replication factor to 1.
offset.storage.topic=connect-offsets
# Topic to use for storing connector and task configurations; note that this should be a single partition, highly replicated,
# and compacted topic. Kafka Connect will attempt to create the topic automatically when needed, but you can always manually create
# the topic before starting Kafka Connect if a specific topic configuration is needed.
# Most users will want to use the built-in default replication factor of 3 or in some cases even specify a larger value.
# Since this means there must be at least as many brokers as the maximum replication factor used, we'd like to be able
# to run this example on a single-broker cluster and so here we instead set the replication factor to 1.
config.storage.topic=connect-configs
# Topic to use for storing statuses. This topic can have multiple partitions and should be replicated and compacted.
# Kafka Connect will attempt to create the topic automatically when needed, but you can always manually create
# the topic before starting Kafka Connect if a specific topic configuration is needed.
# Most users will want to use the built-in default replication factor of 3 or in some cases even specify a larger value.
# Since this means there must be at least as many brokers as the maximum replication factor used, we'd like to be able
# to run this example on a single-broker cluster and so here we instead set the replication factor to 1.
status.storage.topic=connect-status

查詢 MySQL 最近binlog file 資訊

SQL
MySQL [(none)]> show binary logs;
| mysql-bin-changelog.094531 |    176317 |
| mysql-bin-changelog.094532 |    191443 |
| mysql-bin-changelog.094533 |   1102466 |
| mysql-bin-changelog.094534 |    273347 |
| mysql-bin-changelog.094535 |    141555 |
| mysql-bin-changelog.094536 |      4808 |
| mysql-bin-changelog.094537 |    146217 |
| mysql-bin-changelog.094538 |     29607 |
| mysql-bin-changelog.094539 |    141260 |
+----------------------------+-----------+
MySQL [(none)]> show binlog events in 'mysql-bin-changelog.094539';
MySQL [(none)]> show binlog events in 'mysql-bin-changelog.094539' limit 10;
+----------------------------+-----+----------------+------------+-------------+---------------------------------------------------------------------------+
| Log_name                   | Pos | Event_type     | Server_id  | End_log_pos | Info                                                                      |
+----------------------------+-----+----------------+------------+-------------+---------------------------------------------------------------------------+
| mysql-bin-changelog.094539 |   4 | Format_desc    | 1399745413 |         123 | Server ver: 5.7.31-log, Binlog ver: 4                                     |
| mysql-bin-changelog.094539 | 123 | Previous_gtids | 1399745413 |         194 | 90710e1c-f699-11ea-85c0-0ec6a6bed381:1-108842347                          |

指定server name key 傳送offset 記錄到offset.storage.topic

$ ./bin/kafka-console-producer.sh -bootstrap-server localhost:9092 --topic  connect-offsets --property "parse.key=true" --property "key.separator=>"
$>["test_servername",{"server":"test_servername"}]>{"ts_sec":1647845014,"file":"mysql-bin-changelog.007051","pos":74121553,"row":1,"server_id":1404217221,"event":2}

編輯task api 請求,啟動debezium task


{
    "name":"test_servername",
    "config":{
        "connector.class":"io.debezium.connector.mysql.MySqlConnector",
        "snapshot.locking.mode":"none",
        "database.user":"db_user",
        "transforms.Reroute.type":"io.debezium.transforms.ByLogicalTableRouter",
        "database.server.id":"1820615119",
        "database.history.kafka.bootstrap.servers":"localhost:9092",
        "database.history.kafka.topic":"history-topic",
        "inconsistent.schema.handling.mode":"skip",
        "transforms":"Reroute", // 配置binlog資料轉發到一個topic,預設一個表一個topic
        "database.server.name":"test_servername",
        "transforms.Reroute.topic.regex":"test_servername(.*)",
        "database.port":"3306",
        "include.schema.changes":"true",
        "transforms.Reroute.topic.replacement":"binlog_data_topic",
        "table.exclude.list":"table_test",
        "database.hostname":"host",
        "database.password":"******",
        "name":"test_servername",
        "database.whitelist":"test_db",
        "database.include.list":"test_db",
        "snapshot.mode":"schema_only_recovery"  // 使用recovery模式從指定binlog檔案的offset同步
    }
}

3.2 Hudi 全量接增量資料寫入

在已經有全量資料在Hudi表的場景中,後續從kafka消費的binlog資料需要增量upsert到Hudi表。debezium的binlog格式攜帶每條資料更新的資訊,需要將其解析為可直接插入的資料。

示例解析生成Flink SQL的Python程式碼

# 寫入資料到ODS Raw表
insert_hudi_raw_query = '''
INSERT INTO 
{0}_ods_raw.{1}
SELECT 
{2}
FROM 
{0}_debezium_kafka.kafka_rds_{1}_log
WHERE 
REGEXP(GET_JSON_OBJECT(payload, '$.source.table'), '^{3}$') 
AND GET_JSON_OBJECT(payload, '$.source.db') = '{4}' 
AND IF(GET_JSON_OBJECT(payload, \'$.op\') = \'d\', GET_JSON_OBJECT(payload, \'$.before.{5}\'), GET_JSON_OBJECT(payload, \'$.after.{5}\')) IS NOT NULL
AND GET_JSON_OBJECT(payload, '$.op') IN ('d', 'c', 'u')
'''.format(
    database_name, 
    table_name, 
    hudi_schema, 
    mysql_table_name, 
    mysql_database_name,
    primary_key
)

如上對Debezium的三種binlog資料進行解析,我們將insert及update的資料只取after後的資料,對於delete,我們追加一個硬刪除欄位標記進行插入,Hudi則會自動去重。
在這裡為了保證增量更新的hudi資料不重複,需要開啟index bootstrap功能。

Hudi配置引數

名稱 Required 預設值 說明
index.bootstrap.enabled true false 開啟索引載入,會將已存表的最新資料一次性載入到 state 中
index.partition.regex false * 設定正規表示式進行分割槽篩選,預設為載入全部分割槽
  1. CREATE TABLE 建立和 Hoodie 表對應的語句,注意 table type 要正確
  2. 設定 index.bootstrap.enabled = true開啟索引載入功能
  3. 索引載入為併發載入,根據資料量大小載入時間不同,可以在log中搜尋finish loading the index under partition 和 Load records from file 日誌來觀察索引載入進度
  4. 重啟任務將 index.bootstrap.enabled 關閉,引數配置到合適的大小,如果RowDataToHoodieFunction 和 BootstrapFunction 併發不同,可以重啟避免 shuffle

3.3 Hudi同步Metastore自定義分割槽格式改寫

Hudi 提供了HIVE Sync Tool https://hudi.apache.org/docs/syncing_metastore 用來將Hudi的meta data 同步至Hive 進行查詢,同時 PrestoDB / Trino 可以直接通過配置Hive的catalog資訊實現Hudi表的秒級查詢。但目前HiveSyncTool 僅自帶支援幾種格式的Hudi partion ,原始碼位置如下位置:

如果要同步的hudi表沒有分割槽,或者符合hive 的’yyyy-MM-dd’ / ‘yyyy-MM-dd-HH’ 分割槽格式,可以直接使用引數--partition-value-extractor 指定到Non/SlashEncodedDayPartitionValueExtractor/SlashEncodedHourPartitionValueExtractor 進行同步,如下命令:

sh  run_sync_tool.sh  --jdbc-url jdbc:hive2:\/\xxxx:10000 --user hive --pass hive --partitioned-by partition --partition-value-extractor  org.apache.hudi.hive.SlashEncodedHourPartitionValueExtractor --base-path s3://xxx/raw/order_business_db/ord_basics  --auto-create-database  --database order_business_db_ods_raw_hive_sync  --table ord_basics

但存在分割槽不滿足上述格式,如果使用non分割槽同步,則會出現查詢不到資料的問題,這個時候需要自己實現一個Extractor,實現程式碼位於package org.apache.hudi.hive,繼承 PartitionValueExtractor 定義 SlashEncodedHourPartitionValueExtractor 實現extractPartitionValuesInPath 方法,程式碼片段如下,實現格式 dd-MM-yy,程式碼片段擷取如下:

然後重新打包,執行如下命令,隨後在PrestoDB/Hive/Trino 均可直接進行查詢。

sh  run_sync_tool.sh  --jdbc-url jdbc:hive2:\/\/xxxx10000 --user hive --pass hive --partitioned-by partition --partition-value-extractor  org.apache.hudi.hive.KlookEncodedDayPartitionValueExtractor --base-path s3://xxxx/raw/order_business_db/ord_basics  --auto-create-database  --database order_business_db_ods_raw_hive_sync  --table ord_basics

AWS  EMR  上需要注意的:

  • 找不到log4j 修改run_sync_tool.sh HADOOP_HIVE_JARS=${HIVE_JARS}:${HADOOP_HOME}/*:${HADOOP_HOME}/lib/*:/usr/lib/hadoop-hdfs/*:/usr/lib/hadoop-mapreduce/*:/usr/share/aws/emr/emrfs/lib/*:/usr/share/aws/emr/emrfs/auxlib/*:${GLUE_JARS}
  • 找不到libfb修改 java -cp $HUDI_HIVE_UBER_JAR:${HADOOP_HIVE_JARS}:${HIVE_CONF_DIR}:${HADOOP_CONF_DIR}:${EMRFS_CONF_DIR}:/usr/lib/hudi/cli/lib/libfb303-0.9.3.jar org.apache.hudi.hive.HiveSyncTool "$@"

4. 經驗總結

  • 當前整體RDS資料同步解決了對資料時效性及靈活擴充套件性的業務需求,但如上述,資料鏈路較長帶來大量手動操作。因此,我們做了一些流程自動化的工作,使用Airflow 將DMS全量同步S3,S3同步Hudi的Flink 批作業進行自動排程觸發,使得我們填寫簡單資料庫同步引數就可完成一個鏈路的資料入湖。對於增量Debezium 資料同步,我們也通過編寫一些指令碼,在啟動Flink Stream SQL作業時,同步拉取最新MySQL schema,生成解析binlog資料的SQL ,進行自動任務提交。
  • 在穩定性方面,當前主要考慮增量流作業的穩定性,我們從kafka備份了binlog原始資料,這些資料會在S3儲存30天,如果出現流作業寫入Hudi異常,我們可以很快跑一個批任務將資料回溯。
  • 該方案執行近一年時間,期間Hudi版本快速迭代fix很多問題,例如前期Hudi在增量接全量時開啟index後,必須一次將index快取在state,index階段為了提升速度,我們設定了較大的並行度資源,需要人工值守等待一個checkpoint週期然後調低。初期,諮詢社群後,提出了全量也使用流讀等方式,避免增加改表引數的問題,後續社群也做了一些優化,非同步執行index併發載入索引等,無需等待checkpoint完成,index不會阻塞資料寫入checkpoint等。
  • 在OLAP選擇上,我們在採用Trino進行資料查詢Hudi時,由於需要同步工具對Hudi所有分割槽進行索引同步,我們也遇到了需要相容分割槽策略等問題。我們參考了Hudi同步metastore工具編寫了轉換類相容了自定義分割槽。

5. 未來展望

在使用Hudi開源元件過程中,我們體會到必須緊密與社群保持溝通,及時反饋問題,也可以與來自其它公司不同業務場景的工程師進行交流,分享我們遇到的問題及解決思路。
後續的改進,我們會從脫離第三方服務DMS 試圖直接使用Flink 進行全量資料同步,減少鏈路中元件的維護數量,同樣的,我們將積極跟隨Hudi及Flink的發展,優化整體鏈路的效率。

相關文章