1. 介紹
Apache Hudi是一個開源的資料湖框架,旨在簡化增量資料處理和資料管道開發。藉助Hudi可以在Amazon S3、Aliyun OSS資料湖中進行記錄級別管理插入/更新/刪除。AWS EMR叢集已支援Hudi元件,並且可以與AWS Glue Data Catalog無縫整合。此特性可使得直接在Athena或Redshift Spectrum查詢Hudi資料集。
對於企業使用AWS雲的一種常見資料流如圖1所示,即將資料實時複製到S3。
本篇文章將介紹如何使用Oracle GoldenGate
來捕獲變更事件並利用Hudi格式寫入S3資料湖。
Oracle GG可以使用多個處理程式和格式輸出,請檢視此處獲取更多資訊。
本篇文章中不關心處理程式,我們假設使用Avro Operation格式,這種格式較為冗長,但有著廣泛應用,因為其平衡了資料完整性和效能。如圖2所示,此格式包含每個記錄的before
和after
版本。
即使完整且易於生成,此格式也不適合用Athena或Spectrum進行分析,從使用角度也無法替代源資料。此外你可能需要對歷史資料進行分割槽處理以便快速檢索。
本文我們將介紹如何利用Apache Hudi框架做到這一點,以構建易於分析的目標資料集。
2. 系統架構
我們不詳細介紹如何將avro格式檔案放入Replica S3
桶中,整個資料體系結構如下所示
Hudi程式碼執行在EMR叢集中,從Replica S3
桶中讀取avro資料,並將目標資料集儲存到Target S3
桶中。
EMR軟體配置如下
硬體配置如下
由於插入/更新
始終保留最後一條記錄,因此Hudi作業非常具有彈性, 因此可以利用Spot Instance(搶佔式例項)
大大降低成本。
除此之外,還需要設定
- 源bucket(如 my-s3-sourceBucket)
- 目標bucket (如 my-s4-targetBucket)
- Glue資料庫(如 sales-db)
配置完後需要確保EMR叢集有讀寫許可權。
如果你需要一些樣例資料,可以點選此處獲取。當設定好桶後,啟動EMR叢集並將這些樣例資料匯入Replica
桶。
3. 關於分割槽的注意事項
為構建按時間劃分的資料集,必須確定不可變的日期型別欄位。參照示例資料集(銷售訂單),我們假設訂單日期永遠不會改變,因此我們將DAT_ORDER
欄位作為寫入Hudi資料集的分割槽欄位。
分割槽方式是YYYY/MM/DD,通過該方式,所有資料將被組織在巢狀的子資料夾中。Hudi框架將提供此分割槽資訊,並將一個特定欄位新增到關聯的Hive/Glue表中。當查詢時,該欄位上的過濾條件將轉換為超高效的分割槽修剪掃描條件。
實際上這是我們必須對資料集做的唯一強假設,所有其他資訊都在avro檔案中(欄位名稱,欄位型別,PK等)。
除此後設資料外,GoldenGate通常還會新增一些其他資訊,例如表名稱,操作時間戳,操作型別(插入/更新/刪除)和自定義標記。你可以利用這些欄位來構造通用邏輯並構建靈活的遷移平臺。
4. 步驟
啟動spark-shell
spark-shell --conf "spark.serializer=org.apache.spark.serializer.KryoSerializer" --conf "spark.sql.hive.convertMetastoreParquet=false" --jars /usr/lib/hudi/hudi-spark-bundle.jar,/usr/lib/spark/external/lib/spark-avro.jar
啟動後可以執行如下程式碼:
val ggDeltaFiles = "s3://" + sourceBucket + "/" + sourceSubFolder + "/" + sourceSystem + "/" + inputTableName + "/";
val rootDataframe:DataFrame = spark.read.format("avro").load(ggDeltaFiles);
// extract PK fields name from first line
val pkFields: Seq[String] = rootDataframe.select("primary_keys").limit(1).collect()(0).getSeq(0);
// take into account the "after." fields only
val columnsPre:Array[String] = rootDataframe.select("after.*").columns;
// exclude "_isMissing" fields added by Oracle GoldenGate
// The second part of the expression will safely preserve all native "**_isMissing" fields
val columnsPost:Array[String] = columnsPre.filter { x => (!x.endsWith("_isMissing")) || (!x.endsWith("_isMissing_isMissing") && (columnsPre.filter(y => (y.equals(x + "_isMissing")) ).nonEmpty))};
val columnsFinal:ArrayBuffer[String] = new ArrayBuffer[String]();
columnsFinal += "op_ts";
columnsFinal += "pos";
// add the "after." prefix
columnsPost.foreach(x => (columnsFinal += "after." + x));
// prepare the target dataframe with the partition additional column
val preparedDataframe = rootDataframe.select("opTypeFieldName", columnsFinal.toArray:_*).
withColumn("HUDI_PART_DATE", date_format(to_date(col("DAT_ORDER"), "yyyy-MM-dd"),"yyyy/MM/dd")).
filter(col(opTypeFieldName).isin(admittedValues.toList: _*));
// write data
preparedDataframe.write.format("org.apache.hudi").
options(hudiOptions).
option(DataSourceWriteOptions.RECORDKEY_FIELD_OPT_KEY, pkFields.mkString(",")).
mode(SaveMode.Append).
save(hudiTablePath);
上述簡化了部分程式碼,可以在此處找到完整的程式碼。
5. 結果
輸出的S3物件結果如下所示
同時Glue資料目錄將使該表可用於通過外部模式在Athena或Spectrum中進行查詢分析,外部表具有我們用於分割槽的hudi_part_date附加欄位。