01. Hudi 數據模型分析
主題説明
Hudi 的數據模型是整個系統的核心抽象,説白了就是定義了數據記錄在系統中是怎麼表示的、怎麼操作的。理解數據模型是理解 Hudi 工作原理的基礎,就像蓋房子要先打地基一樣。
在 Hudi 裏,一條數據記錄不是簡單的字符串或者字節數組,而是一個結構化的對象,包含了記錄本身的數據、唯一標識、存儲位置等信息。這種設計讓 Hudi 能夠高效地管理數據,支持更新、刪除、合併等複雜操作。
細化內容
HoodieRecord - 記錄的基礎抽象
HoodieRecord 是所有記錄的抽象基類,它定義了記錄的基本結構。簡單來説,一條 HoodieRecord 包含:
- key:記錄的鍵,類型是
HoodieKey,用來唯一標識這條記錄 - data:記錄的實際數據,類型是泛型
T,通常是HoodieRecordPayload的實現 - currentLocation:記錄當前在存儲中的位置,包含文件ID和時間戳
- newLocation:記錄寫入後的新位置
- operation:操作類型,比如 INSERT、UPDATE、DELETE
這個設計很巧妙,把記錄的標識、數據、位置信息都封裝在一起了。這樣在更新數據的時候,可以快速定位到記錄在哪裏,然後進行合併操作。
HoodieKey - 記錄的唯一標識
HoodieKey 是記錄的唯一標識,包含兩個字段:
- recordKey:記錄的主鍵,比如用户ID、訂單號等
- partitionPath:分區路徑,比如
2023/01/01這樣的日期分區
這兩個字段組合起來就能唯一確定一條記錄。比如用户ID是 user123,分區是 2023/01/01,那麼這條記錄在表中的位置就確定了。
HoodieRecordPayload - 數據負載的抽象
HoodieRecordPayload 是一個接口,定義了數據合併的邏輯。這是 Hudi 最核心的部分之一,因為它決定了當同一條記錄有多個版本時,應該怎麼合併。
主要方法包括:
- preCombine:在寫入前合併同一批次中的重複記錄
- combineAndGetUpdateValue:合併存儲中的舊記錄和新的更新記錄
- getInsertValue:獲取要插入的新記錄
不同的實現類有不同的合併策略:
OverwriteWithLatestAvroPayload:直接用新值覆蓋舊值DefaultHoodieRecordPayload:根據排序字段選擇最新的值EventTimeAvroPayload:基於事件時間合併
HoodieAvroRecord - 基於 Avro 的記錄實現
HoodieAvroRecord 是 HoodieRecord 的具體實現,使用 Avro 格式來存儲數據。
什麼是 Avro?
Avro 是 Apache 的一個數據序列化系統,簡單説就是把數據轉換成二進制格式,方便存儲和傳輸。Avro 有幾個特點:
- Schema 驅動:數據結構和 Schema 是分開的,Schema 可以獨立存儲
- 緊湊的二進制格式:比 JSON 等文本格式更省空間
- 支持 Schema 演化:可以修改 Schema 而不影響已有數據
- 跨語言支持:Java、Python、C++ 等都能用
在 Hudi 裏,Avro 主要用於存儲增量日誌(LogFile),因為它是行式存儲,寫入性能好。而基礎文件(BaseFile)用的是 Parquet,列式存儲,查詢性能好。
元數據字段
每條 Hudi 記錄都包含一些元數據字段,這些字段是系統自動添加的:
_hoodie_commit_time:提交時間,記錄這條數據是什麼時候寫入的_hoodie_commit_seqno:提交序列號,同一個提交內的記錄排序_hoodie_record_key:記錄鍵_hoodie_partition_path:分區路徑_hoodie_file_name:文件名,記錄這條數據在哪個文件裏_hoodie_operation:操作類型(INSERT、UPDATE、DELETE)
這些元數據字段讓 Hudi 能夠追蹤每條記錄的歷史,支持時間旅行查詢等功能。
關鍵技術
記錄的序列化和反序列化
Hudi 使用 Kryo 來序列化記錄,Kryo 是一個高效的 Java 序列化框架。序列化就是把對象轉換成字節數組,方便在網絡傳輸或者持久化存儲。
在 HoodieRecord 中,實現了 KryoSerializable 接口,可以自定義序列化邏輯。這對於大數據場景很重要,因為序列化的性能直接影響整體性能。
記錄的合併策略
Hudi 支持多種合併策略,主要通過 HoodieRecordPayload 的不同實現來支持:
- OverwriteWithLatest:直接用新值覆蓋,最簡單粗暴
- EventTimeBased:基於事件時間,選擇時間最新的記錄
- PartialUpdate:部分更新,只更新指定字段
- Custom:自定義合併邏輯
合併策略的選擇取決於業務需求。比如訂單表,通常用 OverwriteWithLatest;而用户行為表,可能用 EventTimeBased。
記錄的排序和分區
記錄在寫入前會進行排序,排序的依據可以是:
- 提交時間(默認)
- 自定義排序字段
- 分區路徑
排序的目的是優化寫入性能,把相同分區的記錄放在一起寫入,減少文件數量。
記錄的元數據管理
元數據字段是自動管理的,不需要用户手動設置。系統會在寫入時自動填充這些字段,在讀取時自動解析。
關鍵對象説明
類關係圖
關鍵類説明
- HoodieRecord:抽象記錄類,定義了記錄的基本結構和方法。它是所有記錄類型的基類。
- HoodieKey:記錄鍵,包含 recordKey 和 partitionPath。實現了 equals 和 hashCode,用於記錄的去重和查找。
- HoodieRecordPayload:記錄負載接口,定義了數據合併的核心邏輯。不同的實現類有不同的合併策略。
- HoodieAvroRecord:基於 Avro 的記錄實現,是 Hudi 中最常用的記錄類型。它把數據序列化成 Avro 格式存儲。
- HoodieRecordLocation:記錄位置,包含 fileId(文件ID)和 instantTime(時間戳)。用於定位記錄在存儲中的位置。
關鍵操作時序圖
下面是一個記錄合併操作的時序圖,展示了當更新一條已存在的記錄時,各個類是如何協作的:
這個時序圖展示了:
- 客户端調用 upsert 方法
- 表通過索引查找記錄位置
- 合併器合併新舊記錄
- Payload 執行具體的合併邏輯
- 寫入合併後的記錄
代碼示例
創建一條記錄
// 創建記錄鍵
HoodieKey key = new HoodieKey("user123", "2023/01/01");
// 創建 Avro 記錄數據
GenericRecord avroRecord = new GenericData.Record(schema);
avroRecord.put("id", "user123");
avroRecord.put("name", "張三");
avroRecord.put("age", 25);
// 創建 Payload
HoodieAvroPayload payload = new HoodieAvroPayload(
Option.of(avroRecord),
System.currentTimeMillis() // 排序值
);
// 創建 Hudi 記錄
HoodieRecord<HoodieAvroPayload> record =
new HoodieAvroRecord<>(key, payload);
記錄合併示例
// 假設存儲中有一條舊記錄
IndexedRecord oldRecord = ...; // 從存儲讀取
// 新來的更新記錄
HoodieAvroPayload newPayload = new HoodieAvroPayload(newRecord, timestamp);
// 執行合併
Option<IndexedRecord> merged = newPayload.combineAndGetUpdateValue(
oldRecord,
schema,
properties
);
// merged 就是合併後的結果
if (merged.isPresent()) {
// 寫入合併後的記錄
writeRecord(merged.get());
} else {
// 返回空表示刪除這條記錄
deleteRecord(key);
}
總結
Hudi 的數據模型設計得很巧妙,把記錄的標識、數據、位置信息都封裝在一起。核心要點:
- HoodieRecord 是基礎抽象,包含 key、data、location 等核心屬性
- HoodieKey 是唯一標識,由 recordKey 和 partitionPath 組成
- HoodieRecordPayload 定義了合併邏輯,支持多種合併策略
- Avro 是行式存儲格式,適合增量日誌,寫入性能好
- 元數據字段 自動管理,支持時間旅行等高級功能
理解數據模型是理解 Hudi 的基礎,後續的存儲、索引、查詢等功能都建立在這個模型之上。