未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用范围与待维护事项
本文用于Hudi架构理解及CoW/MoR选型讨论,执行示例需具备Spark、分布式存储与表维护能力。原稿未指定Hudi、Spark、Flink版本,索引选项、默认值、锁配置及目录布局未经本轮运行验证,不应直接作为生产模板。示例路径与时间需替换;写入前需明确记录合并及排序字段,overwrite仅用于可丢弃测试表。禁用内联compaction后仍需安排表服务;保留3次提交不能保证时间旅行窗口。生产使用前应验证并发模式、清理策略及资源参数。
🏗️ Apache Hudi架构与核心特性
📚 学习目标
通过本章节的学习,你将能够:
- 理解Hudi的整体架构设计和各个组件的作用
- 掌握CoW和MoR两种表类型的原理和适用场景
- 了解索引机制的设计和不同索引类型的特点
- 理解时间线管理和元数据管理机制
- 掌握数据文件的组织结构和存储优化策略
tip 💡 学习建议
架构设计是Hudi的核心,可能会涉及一些复杂的概念。不要担心一开始理解不了所有细节,就像理解一座建筑物的结构一样,我们先了解整体框架,然后逐步深入每个部分的细节。
🎯 Hudi整体架构
架构层次结构
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
subgraph APP["🎯 应用层 (Application Layer)"]
A1[Spark SQL]
A2[Flink SQL]
A3[Presto]
A4[Trino]
end
subgraph API["🔌 API层 (API Layer)"]
B1[Spark DataSource]
B2[Flink Sink]
B3[REST API]
end
subgraph CORE["⚡ 核心层 (Core Layer)"]
C1[Timeline Service<br/>时间线服务]
C2[Index Service<br/>索引服务]
C3[Table Services<br/>表服务]
end
subgraph STORAGE["💾 存储层 (Storage Layer)"]
D1[HDFS]
D2[S3]
D3[Azure Storage]
D4[本地文件系统]
end
APP --> API --> CORE --> STORAGE
class APP,A1,A2,A3,A4,API,B1,B2,B3,CORE,C1,C2,C3,STORAGE,D1,D2,D3,D4 default
核心组件概述
1. 时间线机制与时间线服务 (Timeline / Timeline Service)
- 时间线记录表操作及其状态,为版本视图提供依据
- 时间旅行查询还依赖目标版本的数据文件仍被保留
- Timeline Service提供文件系统视图等服务;并发控制还依赖写入客户端、并发模式与锁等机制
2. 索引机制 (Index)
- 写入索引用于定位记录所在的文件组
- 支持多种索引类型,适用性取决于引擎与版本
- 查询加速索引与写入索引需区分,不能将所有写入索引视为查询索引
3. 表服务 (Table Services)
- 管理表的元数据
- 处理压缩和清理
- 维护数据一致性
abstract 📖 架构设计理念
类比理解:就像一座现代化的图书馆,有专门的管理系统(时间线服务)记录每本书的借还历史,有高效的索引系统(索引服务)帮助快速定位图书,还有专门的维护团队(表服务)负责图书的分类整理和空间清理。
专业表述:这里采用分层解耦的教学视图,而非完整的部署拓扑。时间线记录表操作及状态,Timeline Service提供文件系统视图等服务;写入索引帮助定位记录,Table Services处理compaction、clustering、cleaning等维护操作。不同计算引擎与存储系统的集成能力需按具体版本确认,图中的REST API也不应理解为通用数据读写接口。
🔄 表类型详解
Copy on Write (CoW) 表
工作原理
CoW表在写入时立即合并数据,所有的更新操作都会重写相关的数据文件。
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TB
subgraph BEFORE["📝 写入前的文件状态"]
A1[data_001.parquet<br/>📊 包含记录 A, B, C]
end
subgraph AFTER["🔄 更新记录 B 后"]
A2[data_002.parquet<br/>📊 包含记录 A, B' 更新后, C]
end
BEFORE --> |"更新操作<br/>重写整个文件"| AFTER
style A1 fill:#e8f4f7,stroke:#6c757d
style A2 fill:#d4edda,stroke:#6c757d
代码示例
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("HudiCoWExample") \
.getOrCreate()
# 创建CoW表的配置
cow_options = {
"hoodie.table.name": "user_cow_table",
"hoodie.datasource.write.recordkey.field": "user_id",
"hoodie.datasource.write.partitionpath.field": "city",
"hoodie.datasource.write.table.name": "user_cow_table",
"hoodie.datasource.write.table.type": "COPY_ON_WRITE", # 指定CoW类型
"hoodie.datasource.write.operation": "upsert"
}
# 初始数据写入
initial_data = [
{"user_id": 1, "name": "张三", "age": 25, "city": "北京"},
{"user_id": 2, "name": "李四", "age": 30, "city": "上海"},
{"user_id": 3, "name": "王五", "age": 35, "city": "广州"}
]
df_initial = spark.createDataFrame(initial_data)
df_initial.write \
.format("hudi") \
.options(**cow_options) \
.mode("overwrite") \
.save("/path/to/hudi/cow_table")
# 更新数据
update_data = [
{"user_id": 1, "name": "张三", "age": 26, "city": "北京"}, # 年龄更新
{"user_id": 4, "name": "赵六", "age": 28, "city": "深圳"} # 新增用户
]
df_update = spark.createDataFrame(update_data)
df_update.write \
.format("hudi") \
.options(**cow_options) \
.mode("append") \
.save("/path/to/hudi/cow_table")
优势与局限
info 📚 CoW表类型理解
类比理解:CoW表就像一本纸质百科全书,每当需要更新某个词条时,就要重新印刷整本书。虽然读者总是能拿到最新最完整的版本,但更新成本很高,需要大量时间和资源。
专业表述:Copy on Write表类型在数据更新时会重写包含变更记录的整个数据文件,确保数据文件始终保持最新状态。这种方式避免了读取时合并增量日志的开销,但实际查询性能取决于数据布局与执行引擎,在频繁更新场景下通常会产生较高的I/O开销。
优势:
- ✅ 读取路径较简单:无需合并增量日志,读取当前可见版本的基础文件;实际性能需测试
- ✅ 存储效率高:不需要存储增量日志文件
- ✅ 实现简单:逻辑相对简单,易于理解
局限:
- ❌ 写入性能较低:每次更新都需要重写整个文件
- ❌ 并发写入受限:同一文件的并发更新会冲突
- ❌ 资源消耗大:重写文件需要更多的CPU和内存
Merge on Read (MoR) 表
工作原理
MoR表使用基础文件和增量日志文件组织数据,更新通常可追加到日志;插入等写入路径也可能生成基础文件。快照查询按需合并基础文件与日志,只读优化查询仅读取基础文件。
info 📝 MoR表类型理解
类比理解:MoR表就像一本活页笔记本,原始内容写在主页上(基础文件),后续的修改和补充都记在便利贴上(增量日志)。当需要查看完整内容时,要同时参考主页和所有便利贴。这样修改很快,但阅读时需要整合。
专业表述:Merge on Read表类型采用基础文件+增量日志的存储模式。更新通常可追加增量日志,降低立即重写基础文件的开销,但并非所有写入都只生成日志。查询时通过合并基础文件和增量日志来获得最新的数据视图,在写入性能和存储效率之间取得平衡。
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TB
subgraph MOR["🗂️ MoR 表的文件结构"]
subgraph BASE["📄 基础文件"]
B1[base_001.parquet<br/>📊 包含记录 A, B, C]
end
subgraph DELTA["📝 增量日志"]
D1[delta_001.log<br/>📝 B' 更新, D 新增]
end
subgraph RESULT["🔍 查询时合并结果"]
R1[📋 最终结果<br/>A, B' 更新后, C, D]
end
BASE --> R1
DELTA --> R1
style B1 fill:#e8f4f7,stroke:#6c757d
style D1 fill:#fff3cd,stroke:#6c757d
style R1 fill:#d4edda,stroke:#6c757d
end
代码示例
# 创建MoR表的配置
mor_options = {
"hoodie.table.name": "user_mor_table",
"hoodie.datasource.write.recordkey.field": "user_id",
"hoodie.datasource.write.partitionpath.field": "city",
"hoodie.datasource.write.table.name": "user_mor_table",
"hoodie.datasource.write.table.type": "MERGE_ON_READ", # 指定MoR类型
"hoodie.datasource.write.operation": "upsert",
# MoR 特有配置
"hoodie.compact.inline": "false", # 禁用内联压缩
"hoodie.compact.inline.max.delta.commits": "5" # 内联compaction启用时的阈值;当前禁用内联,不会仅凭此项自动执行
}
# 数据写入(与CoW类似)
df_initial.write \
.format("hudi") \
.options(**mor_options) \
.mode("overwrite") \
.save("/path/to/hudi/mor_table")
# 频繁的增量更新(MoR的优势场景)
for i in range(10):
update_data = [
{"user_id": 1, "name": "张三", "age": 25 + i, "city": "北京"},
{"user_id": 2, "name": "李四", "age": 30 + i, "city": "上海"}
]
df_update = spark.createDataFrame(update_data)
df_update.write \
.format("hudi") \
.options(**mor_options) \
.mode("append") \
.save("/path/to/hudi/mor_table")
查询类型
info 🔍 MoR查询类型理解
类比理解:就像查阅一本经常更新的法律条文,你可以选择:
- 完整版查询(快照查询):把主条文和所有修正案都看一遍,得到最准确但耗时的结果
- 快速版查询(只读优化查询):只看主条文,快速获取信息但可能遗漏最新修正
专业表述:这里比较MoR表的两种常用读取模式,并非列举全部查询类型。快照查询读取当前可见文件切片,按需合并基础文件与日志;只读优化查询仅读取基础文件,可能遗漏尚未合入基础文件的变更,性能优势需按工作负载验证。
1. 快照查询 (Snapshot Query)
# 查询最新的完整数据视图
snapshot_df = spark.read \
.format("hudi") \
.option("hoodie.datasource.query.type", "snapshot") \
.load("/path/to/hudi/mor_table")
2. 只读优化查询 (Read Optimized Query)
# 只查询基础文件,性能更好但可能不是最新数据
read_optimized_df = spark.read \
.format("hudi") \
.option("hoodie.datasource.query.type", "read_optimized") \
.load("/path/to/hudi/mor_table")
优势与局限
优势:
- ✅ 更新写入开销通常较低:更新可追加日志,但实际表现取决于写入路径与表服务配置
- ✅ 支持高频更新:适合实时数据摄入场景
- ✅ 可配置并发写入:仍可能发生文件组级冲突,需按版本配置并发模式、锁与冲突处理;MoR本身不保证无冲突
局限:
- ❌ 查询性能复杂:需要合并基础文件和日志文件
- ❌ 存储开销大:需要额外的日志文件存储
- ❌ 压缩必要:需要定期压缩以优化性能
📊 表类型选择指南
性能对比
| 特性 | Copy on Write | Merge on Read |
|---|---|---|
| 写入延迟 | 更新通常需重写受影响基础文件 | 更新通常可追加日志,仍有其他写入路径 |
| 查询性能 | 无日志合并开销,需实测 | 快照查询可能需合并;只读优化可避免合并 |
| 存储开销 | 受文件版本保留与清理策略影响 | 还需考虑日志及compaction期间的空间 |
| 并发写入 | 需配置并发控制,可能冲突 | 需配置并发控制,可能冲突 |
| compaction需求 | 不需要日志合并型compaction,仍需其他表维护 | 通常需安排compaction以控制日志积累 |
选择建议
example 🎯 选择CoW的场景
分析型工作负载:
- 数据主要用于报表和分析
- 查询频率高,对查询性能要求严格
- 写入频率相对较低(如日批次更新)
示例:
- 每日用户行为分析报表
- 月度销售数据分析
- 历史数据归档查询
example 🎯 选择MoR的场景
实时数据摄入:
- 需要高频率的数据更新
- 对写入延迟要求高
- 可以接受查询时的额外计算开销
示例:
- 实时用户画像更新
- 实时风控数据处理
- IoT设备状态监控
🗂️ 索引机制详解
索引的作用
索引的主要作用是快速定位需要更新的数据文件,避免全表扫描。
info 📇 索引机制理解
类比理解:就像图书馆的目录卡片系统,不用翻遍整个图书馆找一本书,而是先查目录卡片确定书在哪个区域哪个书架,然后直接去那里找。索引让数据定位从"大海捞针"变成"按图索骥"。
专业表述:Hudi索引机制通过维护record key到文件位置的映射关系,实现了对数据更新操作的高效定位。当执行UPSERT操作时,索引帮助快速确定哪些文件包含需要更新的记录,避免了全表扫描的性能开销。
# 没有索引的更新过程
"""
1. 扫描所有数据文件
2. 在每个文件中查找目标记录
3. 更新找到的记录
4. 重写相关文件
"""
# 有索引的更新过程
"""
1. 通过索引快速定位目标文件
2. 只处理包含目标记录的文件
3. 更新并重写这些文件
"""
索引类型详解
1. Bloom Filter索引(是否默认需按引擎与版本确认)
工作原理:
- 为每个文件创建布隆过滤器
- 快速判断记录是否可能存在于文件中
- 减少需要扫描的文件数量
# Bloom Filter索引配置
bloom_index_options = {
"hoodie.index.type": "BLOOM",
"hoodie.index.bloom.num_entries": "60000", # 布隆过滤器条目数
"hoodie.index.bloom.fpp": "0.000000001", # 误报率
"hoodie.bloom.index.parallelism": "100", # 并行度
"hoodie.bloom.index.prune.by.ranges": "true", # 启用范围剪枝
"hoodie.bloom.index.use.caching": "true" # 启用缓存
}
优势:
- 内存占用小
- 查询速度快
- 可利用记录键范围信息进行候选文件剪枝,不等同于通用范围查询索引
局限:
- 存在误报(但无漏报)
- 不支持范围查询
- 需要调优参数
2. Simple索引
工作原理:
- 读取已有记录键及其位置,与待写入记录进行关联以定位记录
- 为写入记录标记已有文件位置,并非持久化的简单哈希索引
- 适用性取决于扫描与关联成本,需结合数据规模测试
# Simple索引配置
simple_index_options = {
"hoodie.index.type": "SIMPLE",
"hoodie.simple.index.parallelism": "100",
"hoodie.simple.index.use.caching": "true"
}
3. HBase索引
工作原理:
- 使用外部HBase存储记录键到文件位置的映射
- 用于写入时的记录定位,不是通用复杂查询索引;规模适用性需测试
- 需要额外的HBase集群,并确认目标Hudi版本与引擎支持
# HBase索引配置
hbase_index_options = {
"hoodie.index.type": "HBASE",
"hoodie.index.hbase.table": "hudi_index_table",
"hoodie.index.hbase.zkquorum": "localhost:2181",
"hoodie.index.hbase.zkport": "2181",
"hoodie.index.hbase.update.partition.path": "true"
}
4. Flink State索引
工作原理:
- 利用Flink的状态后端存储索引
- 适合流式处理场景
- 与Flink深度集成
# Flink State索引配置(在Flink程序中)
flink_state_options = {
"hoodie.index.type": "FLINK_STATE",
"hoodie.index.state.ttl": "1800000" # 状态TTL(毫秒)
}
索引性能优化
# 索引性能优化配置
index_optimization = {
# 启用索引缓存
"hoodie.bloom.index.use.caching": "true",
# 调整布隆过滤器参数
"hoodie.index.bloom.num_entries": "60000",
"hoodie.index.bloom.fpp": "0.000000001",
# 启用范围剪枝
"hoodie.bloom.index.prune.by.ranges": "true",
# 调整并行度
"hoodie.bloom.index.parallelism": "200",
# 元数据相关索引配置(非增量索引开关,配置键及语义需按版本核对)
"hoodie.index.bloom.use.metadata": "true"
}
⏰ 时间线管理
时间线概念
时间线是Hudi中的核心概念,记录了表的所有变更历史。
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
timeline
title ⏰ Hudi时间线示例
section 数据操作历史
t1 : commit (插入)
t2 : commit (更新)
t3 : commit (删除)
t4 : compaction (日志合并;完成后通常表现为commit)
t5 : clean (清理)
时间线操作类型
1. Commit操作
# 提交操作会创建新的时间点
commit_options = {
"hoodie.datasource.write.operation": "upsert",
"hoodie.datasource.write.table.name": "test_table"
}
2. Compaction操作
# 压缩操作会创建新的时间点
compaction_options = {
"hoodie.compact.inline": "true",
"hoodie.compact.inline.max.delta.commits": "5"
}
3. Clean操作
# 清理操作会创建新的时间点
clean_options = {
"hoodie.clean.automatic": "true",
"hoodie.clean.async": "true",
"hoodie.cleaner.commits.retained": "3"
}
时间旅行查询
# 查询特定时间点的数据
def time_travel_query(spark, table_path, target_time):
"""
时间旅行查询函数
Args:
spark: SparkSession
table_path: 表路径
target_time: 目标时间点
"""
df = spark.read \
.format("hudi") \
.option("hoodie.datasource.query.type", "snapshot") \
.option("as.of.instant", target_time) \
.load(table_path)
return df
# 使用示例
historical_df = time_travel_query(
spark,
"/path/to/hudi/table",
"20241201120000"
)
增量查询
# 增量查询实现
def incremental_query(spark, table_path, start_time, end_time):
"""
增量查询函数
Args:
spark: SparkSession
table_path: 表路径
start_time: 开始时间
end_time: 结束时间
"""
df = spark.read \
.format("hudi") \
.option("hoodie.datasource.query.type", "incremental") \
.option("hoodie.datasource.read.begin.instanttime", start_time) \
.option("hoodie.datasource.read.end.instanttime", end_time) \
.load(table_path)
return df
# 使用示例:查询最近一小时的变化
recent_changes = incremental_query(
spark,
"/path/to/hudi/table",
"20241201110000",
"20241201120000"
)
📁 数据文件组织
文件结构
| 路径 | 说明 |
|---|---|
hudi_table/.hoodie/ |
元数据目录 |
hudi_table/.hoodie/hoodie.properties |
表属性 |
.hoodie/下的时间线文件(具体位置依表版本而异) |
提交等操作记录,不是统一的.commits/子目录 |
时间线中的.inflight状态文件 |
进行中的操作,不是统一的.inflight/子目录 |
hudi_table/.hoodie/.aux/ |
辅助文件 |
hudi_table/city=北京/ |
分区目录 |
hudi_table/city=北京/base_file_001.parquet |
基础文件 |
hudi_table/city=北京/.log_file_001.log |
日志文件(MoR) |
hudi_table/city=上海/base_file_003.parquet |
基础文件 |
hudi_table/city=上海/base_file_004.parquet |
基础文件 |
文件命名规则
# Hudi 文件命名规则
"""
基础文件:
{fileId}_{writeToken}_{instantTime}.parquet
日志文件:
.{fileId}_{baseInstantTime}.log.{logVersion}_{writeToken}
"""
# 示例
example_files = {
"base_file": "abc123_1-0-1_20241201120000.parquet",
"log_file": ".abc123_20241201120000.log.1_1-0-1"
}
🔧 配置最佳实践
性能优化配置
# 生产配置讨论示例:参数需按版本、工作负载与资源验证,不可直接作为推荐模板
production_config = {
# 基础配置
"hoodie.table.name": "production_table",
"hoodie.datasource.write.recordkey.field": "id",
"hoodie.datasource.write.partitionpath.field": "date",
# 索引配置
"hoodie.index.type": "BLOOM",
"hoodie.index.bloom.num_entries": "60000",
"hoodie.index.bloom.fpp": "0.000000001",
# 并行配置
"hoodie.insert.shuffle.parallelism": "400",
"hoodie.upsert.shuffle.parallelism": "400",
"hoodie.delete.shuffle.parallelism": "400",
# 压缩配置
"hoodie.compact.inline": "false",
"hoodie.compact.inline.max.delta.commits": "5",
"hoodie.compaction.strategy": "org.apache.hudi.table.action.compact.strategy.LogFileSizeBasedCompactionStrategy",
# 清理配置
"hoodie.clean.automatic": "true",
"hoodie.clean.async": "true",
"hoodie.cleaner.commits.retained": "3",
# 内存配置
"hoodie.memory.merge.max.size": "1073741824", # 1GB
"hoodie.memory.spillable.map.path": "/tmp/hudi-spillable-map",
# 写入配置
"hoodie.datasource.write.operation": "upsert",
"hoodie.datasource.write.table.type": "MERGE_ON_READ",
"hoodie.datasource.write.precombine.field": "ts"
}
监控配置
# 监控和指标配置
monitoring_config = {
# 启用指标
"hoodie.metrics.on": "true",
"hoodie.metrics.reporter.type": "PROMETHEUS",
# 元数据与Hive同步配置(不是日志配置)
"hoodie.datasource.meta.sync.enable": "true",
"hoodie.datasource.hive_sync.enable": "true",
# ZooKeeper锁配置(不是心跳配置;完整配置需按版本确认)
"hoodie.write.lock.provider": "org.apache.hudi.client.transaction.lock.ZookeeperBasedLockProvider",
"hoodie.write.lock.zookeeper.url": "localhost:2181",
"hoodie.write.lock.zookeeper.port": "2181"
}
🎯 小结
通过本章节的学习,我们深入了解了:
- Hudi的分层架构和各组件的职责分工
- CoW和MoR表类型的原理、优缺点和选择策略
- 索引机制的设计和不同索引类型的特点
- 时间线管理和时间旅行查询的实现
- 数据文件组织和存储优化策略
abstract 📚 下一步学习
掌握了Hudi的架构原理后,接下来我们将学习Hudi环境搭建与配置,为实际操作做准备。
🔗 相关资源
💡 记住:架构理解是使用Hudi的关键,不要急于追求完美理解每个细节。先掌握核心概念,在实践中逐步加深理解。每个架构设计都有其权衡,理解这些权衡有助于在实际项目中做出正确的选择。