3. Compaction核心原理

Compaction 这个词,说白了就是「把零碎的东西收拾整齐」。

我在做数据湖仓项目时,经常遇到一个场景:早上跑批任务,发现某个表的小文件数量已经超过 10 万个。查询慢、元数据膨胀、NameNode 压力大——所有问题都指向同一个根源:文件碎片太多。

Compaction 就是来解决这个问题的。它不只是合并文件那么简单,背后涉及三个核心环节:文件合并机制、数据重写与排序、元数据更新流程。这三个环节环环相扣,任何一个出问题,整个 Compaction 都可能翻车。

3.1 文件合并机制:把小文件「揉」成大文件

文件合并是 Compaction 最直观的动作。你想想看,一个表每天产生几千个几 KB 的小文件,查询时扫描这些文件的开销,比扫描数据本身还大。

核心目标:将多个小文件合并为 1 个或少数几个大文件,减少文件数量,提升 I/O 效率。

合并策略通常分两种:

  • 全量合并(Full Compaction):把分区内所有文件重新合并。效果最好,但代价也最大。我建议只在数据稳定后执行,比如凌晨低峰期。
  • 增量合并(Incremental Compaction):只合并新产生的小文件,或者合并那些「已经比较小」的文件。效率高,但需要更精细的策略控制。

我在项目中遇到过一种情况:某个实时写入的表,每 5 分钟生成一批小文件。如果每天做一次全量合并,凌晨的合并任务要跑 2 小时,严重影响下游依赖。后来改成增量合并,每次只合并过去 1 小时的文件,合并时间压缩到 10 分钟以内。

经验之谈:合并粒度不是越大越好。我一般建议目标文件大小控制在 256MB ~ 1GB 之间,具体取决于你的 HDFS 块大小和查询引擎的并行度。

3.2 数据重写与排序:不只是「拼在一起」

很多人以为 Compaction 就是把文件拼起来。其实没那么简单。

合并过程中,数据会被重新写入。这意味着你可以趁机做很多优化:

  1. 排序优化:按分区键、排序键重新排列数据。比如 Iceberg 的 Compaction 会按分区字段排序,这样查询时能利用分区裁剪,减少扫描量。
  2. 列裁剪与压缩:重新写入时可以选择更高效的压缩算法。我习惯在 Compaction 阶段把 Snappy 换成 Zstandard,压缩比能提升 30% 以上。
  3. 数据清理:删除过期数据、去重、合并更新操作。这在 CDC 场景下特别有用。

举个例子,Hudi 的 Compaction 会把 MOR(读时合并)表的 log 文件和 base 文件合并,同时把更新记录「回写」到 base 文件中。这个过程本质上就是一次数据重写。

注意:数据重写会带来额外的 I/O 和计算开销。如果 Compaction 频率过高,反而会拖累整体性能。我曾经见过一个团队每 10 分钟做一次 Compaction,结果集群资源全耗在合并上了,查询反而变慢。

3.3 元数据更新流程:让查询「看见」新文件

文件合并完了,数据重写完了,接下来最关键的一步:更新元数据。

元数据是查询引擎的「地图」。如果地图不更新,查询引擎还是去读那些已经被合并掉的小文件,那 Compaction 就白做了。

元数据更新流程一般分三步:

步骤 操作 说明
1 提交新文件列表 将合并后的大文件信息写入元数据层
2 标记旧文件为删除 不立即物理删除,而是标记为「可清理」
3 清理旧文件 在后续的清理任务中物理删除旧文件

以 Iceberg 为例,它的元数据更新是通过 Snapshot 机制实现的。每次 Compaction 完成后,会生成一个新的 Snapshot,指向新的 manifest 文件。查询引擎只需要读取最新的 Snapshot,就能「看到」合并后的文件。

关键点:元数据更新必须是原子操作。要么全部成功,要么全部回滚。否则会出现「部分查询读到新文件,部分查询读到旧文件」的不一致问题。

我遇到过最坑的一次:某个团队在 Compaction 过程中,先删除了旧文件,再提交新文件列表。结果提交失败,旧文件已经没了,数据直接丢失。嗯,从那以后我定了一条铁律:先提交新文件,再标记旧文件,最后物理删除。

3.4 Compaction 核心流程全景图

下面这张图把整个 Compaction 的核心流程串起来了。你可以看到文件合并、数据重写、元数据更新这三个环节是如何衔接的。

Compaction 核心流程全景图 Compaction 核心流程全景图 1. 文件合并 2. 数据重写与排序 3. 元数据更新 扫描小文件列表 分组与合并策略 生成合并计划 读取旧文件数据 排序与压缩 写入新文件 提交新文件列表 标记旧文件为删除 生成新 Snapshot 关键原则 • 先提交新文件,再标记旧文件,最后物理删除 • 元数据更新必须是原子操作,失败需回滚 • 目标文件大小建议控制在 256MB ~ 1GB

3.5 避坑指南:我踩过的那些坑

Compaction 看起来简单,实际落地时坑不少。我把自己踩过的几个典型问题列出来,希望能帮你少走弯路。

坑 1:元数据更新不是原子操作

我曾经在一个项目中,Compaction 任务先删除了旧文件,再提交新文件列表。结果提交失败,旧文件已经没了,数据直接丢失。修复花了整整两天。

解决方案:使用两阶段提交协议,先提交新文件,再标记旧文件为删除,最后物理清理。

坑 2:Compaction 频率过高

有个团队每 10 分钟做一次 Compaction,结果集群资源全耗在合并上了,查询反而变慢。更糟的是,频繁的元数据更新导致 Snapshot 数量爆炸,元数据查询都变慢了。

解决方案:根据数据写入频率和查询延迟要求,合理设置 Compaction 间隔。我一般建议每小时或每天一次。

坑 3:忽略排序优化

很多人在 Compaction 时只合并文件,不重新排序。结果查询性能提升有限。其实 Compaction 是重新排序的最佳时机,因为数据已经被读出来了,排序的额外开销很小。

解决方案:在 Compaction 时按查询模式重新排序数据。比如按时间分区后,再按用户 ID 排序,能显著提升点查性能。

3.6 小结

Compaction 的核心原理,说白了就是三件事:合并文件、重写数据、更新元数据。这三件事环环相扣,缺一不可。

我个人习惯在每次 Compaction 任务完成后,检查三个指标:文件数量是否减少、查询延迟是否下降、元数据大小是否可控。如果这三个指标都正常,说明 Compaction 策略是健康的。