4. Compaction原理:MOR表数据写入与读取路径、Compaction的必要性、Compaction策略
4.1 MOR表的核心机制:写入与读取路径
MOR(Merge-On-Read)表的核心设计思想是将写入成本与读取成本分离。写入时只追加增量数据,读取时再合并基线数据与增量数据。理解这一机制,是掌握Compaction原理的前提。
MOR表在文件系统层面维护两类数据文件:Base File(列式文件,如Parquet)与Log File(行式文件,如Avro)。每次写入操作(Insert/Update/Delete)都会产生新的Log File片段,而Base File保持不变。下图展示了MOR表的数据写入与读取路径:
上图揭示了MOR表的核心矛盾:写入越快,Log文件积累越多,读取时需要合并的数据量就越大。写入路径追求低延迟,将变更追加到Log;读取路径则必须遍历所有Log片段与Base File进行合并,才能返回最新数据。这种设计在写入密集型场景下优势明显,但若Log无限增长,读取性能将急剧恶化。
4.2 Compaction的必要性:性能衰减的临界点
Compaction的本质是将Log File中的增量数据合并到新的Base File中,并清理已合并的Log片段。其必要性可以从三个维度理解:
| 维度 | 不执行Compaction的后果 | Compaction后的效果 |
|---|---|---|
| 读取性能 | 每次查询需合并数十甚至上百个Log片段,CPU与I/O开销线性增长 | 只需读取单个Base File,查询延迟降低数倍至数十倍 |
| 存储效率 | Log文件冗余存储同一Key的多次变更,占用额外空间 | 合并后只保留最新版本,存储空间显著下降 |
| 文件数量 | 小文件数量持续膨胀,NameNode/元数据压力增大 | 文件数量收敛,元数据开销降低,适合小文件合并场景 |
hoodie.compaction.inline.max.delta.commits=5)时,读取延迟可能从毫秒级恶化到秒级甚至分钟级,且Hudi的FileGroup清理机制(Clean)也无法回收被Log占用的空间,最终导致存储与查询双双失控。
4.3 Compaction策略:Inline、Async与Schedule
Hudi提供了三种Compaction触发与执行策略,适用于不同场景。理解它们的差异是调优的前提。
4.3.1 Inline Compaction(内联压缩)
Inline Compaction在写入事务提交之前同步执行。当满足触发条件时(如提交次数达到阈值),当前写入任务会暂停,先执行Compaction,再完成本次写入提交。
-- 开启Inline Compaction,每5次提交触发一次
hoodie.compaction.inline=true
hoodie.compaction.inline.max.delta.commits=5
优点:实现简单,无需额外部署组件;Compaction与写入在同一事务中,一致性保障容易。
缺点:Compaction耗时较长时,会阻塞写入任务,导致写入延迟尖峰。适合写入频率低、数据量小的场景。
4.3.2 Async Compaction(异步压缩)
Async Compaction将Compaction任务从写入路径中剥离,由独立的Spark作业或线程在后台执行。写入任务只负责生成Compaction计划(Plan),不等待执行结果。
-- 开启异步Compaction
hoodie.compaction.async.enable=true
hoodie.compaction.async.max.delta.commits=5
-- 使用独立Spark作业执行(生产推荐)
spark-submit --class org.apache.hudi.utilities.HoodieCompactor \
--master yarn \
--driver-memory 4g \
--executor-memory 8g \
--executor-cores 4 \
hudi-utilities-bundle.jar \
--base-path /user/hudi/table \
--table-name my_table \
--schedule-compaction \
--instant-time 20250101000000
优点:Compaction不阻塞写入,写入延迟稳定;可独立调整Compaction的计算资源。
缺点:需要额外运维Spark作业;若后台任务执行不及时,Log仍会积累。适合写入压力大、对延迟敏感的生产环境。
4.3.3 Schedule + 手动执行(调度模式)
该策略将Compaction拆分为两个阶段:先调度(Schedule)生成计划,再在合适时机执行(Execute)。调度阶段只做标记,不执行实际合并,因此开销极小。
-- 步骤1:仅生成Compaction计划(不执行)
hoodie.compaction.schedule.enable=true
hoodie.compaction.schedule.max.delta.commits=5
-- 步骤2:手动触发执行(可在业务低峰期)
spark-submit --class org.apache.hudi.utilities.HoodieCompactor \
--master yarn \
hudi-utilities-bundle.jar \
--base-path /user/hudi/table \
--table-name my_table \
--execute-compaction \
--instant-time 20250101000000
优点:执行时机完全可控,可避开业务高峰;调度与执行解耦,便于自动化编排。
缺点:需要额外的调度系统配合(如Airflow、DolphinScheduler)。适合有明确维护窗口、需要精细控制资源使用的场景。
4.4 策略选型与案例复盘
三种策略并非互斥,实际生产中可以组合使用。以下是一个典型的选型决策流程:
- 评估写入频率与延迟要求:若写入QPS高且对P99延迟敏感,优先排除Inline。
- 评估运维能力:是否有独立Spark集群资源?能否接受额外运维成本?
- 评估数据新鲜度要求:若下游对数据可见性要求高(分钟级),Async需保证执行频率不低于写入频率的1/5。
- 组合策略:例如“Async为主 + Schedule兜底”,平时异步执行,若Log积累超过紧急阈值则手动调度一次。
hoodie.compaction.inline=true。业务增长后,每次Compaction耗时约3分钟,导致写入P99延迟从200ms飙升至4秒,频繁触发超时。调整为Async策略后,写入延迟恢复稳定;但发现后台Compactor执行频率不足,Log文件在高峰期积累至40+个片段,查询延迟上升。最终采用“Async + 基于Log数量的动态调度”方案:当未合并的Log片段超过20个时,自动触发一次Compaction,同时将Compactor的Spark资源从4核提升至8核。优化后,写入P99稳定在300ms以内,查询P95延迟控制在1.5秒内,存储空间节省约35%。
Compaction策略的选择本质上是在写入延迟、查询性能、运维复杂度之间寻找平衡。没有放之四海而皆准的配置,必须结合业务的实际写入模式、查询SLA和集群资源进行动态调整。建议在生产环境通过压测对比不同策略下的延迟分布,并持续监控Log文件数量与Compaction耗时,才能找到最优解。