Apache Hudi:CDC的黃金搭檔

leesf發表於2021-04-06

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所示,此格式包含每個記錄的beforeafter版本。

即使完整且易於生成,此格式也不適合用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附加欄位。

相關文章