4. Compaction触发策略:什么时候该做合并?
触发Compaction的时机,说白了就是回答一个问题:什么时候该做合并?
我见过不少团队,上来就配一个定时任务,每天凌晨跑一次。结果呢?小文件照堆,查询越来越慢。触发策略没选对,Compaction就是个摆设。
常见的触发方式有四种:基于文件数量、基于文件大小、基于时间窗口、基于分区级别。咱们一个一个拆开看。
4.1 基于文件数量的触发
这是最直观的方式。你想想看,小文件太多,NameNode压力大,查询时打开的文件句柄也多。所以,当某个分区下的文件数量超过阈值,就该触发Compaction了。
核心逻辑: 文件数 > 阈值 → 触发合并
我在项目中遇到过这样一个场景:某张表的ODS层,每天增量写入产生上千个小文件。我设了个阈值——每个分区文件数超过500就触发。效果很明显,NameNode的RPC负载直接降了40%。
配置示例(以Apache Hudi为例):
-- 基于文件数量的触发配置
hoodie.compact.inline=true
hoodie.compact.inline.max.delta.commits=5
-- 或者用以下方式控制文件数量
hoodie.parquet.small.file.limit=104857600 -- 100MB以下视为小文件
hoodie.copyonwrite.record.size.estimate=1024
hoodie.copyonwrite.insert.split.size=5000000
个人建议: 文件数量阈值不要设得太低。比如设成100,那可能每小时都在做Compaction,反而浪费资源。我一般从500起步,根据实际写入量调整。
4.2 基于文件大小的触发
文件数量多不一定都是小文件。有时候一个分区就几个文件,但每个文件只有几MB,这也要合并。基于文件大小的触发,关注的是文件是否「喂饱」了。
举个例子,你的目标文件大小是256MB。如果某个文件只有50MB,那它就是合并对象。多个小文件合并成一个256MB的大文件,查询效率会好很多。
| 文件大小范围 | 是否触发合并 | 说明 |
|---|---|---|
| < 目标大小的50% | 是 | 强烈建议合并 |
| 目标大小的50%~80% | 可选 | 视资源情况决定 |
| > 目标大小的80% | 否 | 文件已接近理想大小 |
嗯,这里要注意:目标文件大小不是越大越好。我见过有人设成1GB,结果Compaction跑得慢,还容易OOM。一般建议256MB~512MB,兼顾查询和写入性能。
避坑指南: 我曾经在一个实时入湖的场景里,把目标文件大小设成了512MB。结果上游数据量突然暴增,Compaction还没跑完,新数据又来了,最后堆了一堆临时文件。后来我加了「最大文件大小上限」的保护,超过1GB的文件直接跳过合并。
4.3 基于时间窗口的触发
这种策略适合有明确时间规律的数据。比如每天凌晨业务低峰期做一次Compaction,或者每小时做一次增量合并。
时间窗口触发的好处是可预测。你可以在资源空闲时跑Compaction,不影响白天的高峰查询。
常见的配置方式:
-- 基于时间窗口的触发(以Apache Iceberg为例)
spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
-- 定时触发Compaction
-- 每天凌晨2点执行
0 2 * * * /usr/bin/spark-submit --class compaction_job compaction.jar
我个人习惯把时间窗口和文件数量结合起来用。比如:
- 每天凌晨2点:强制做一次全量Compaction
- 每30分钟:检查文件数量,超过阈值就触发增量合并
这样既有兜底,又能及时处理突发的小文件堆积。
4.4 基于分区级别的触发
这个策略比较高级。它不是全局统一触发,而是按分区独立判断。
为什么要按分区?因为不同分区的数据写入模式可能完全不同。比如:
- 热分区(今天的数据):写入频繁,小文件多,需要高频Compaction
- 冷分区(一个月前的数据):几乎不写入,不需要频繁合并
如果对所有分区用同一套策略,要么热分区合并不够,要么冷分区白白浪费资源。
分区级别触发的核心思路: 每个分区独立维护文件列表和统计信息,根据自身状态决定是否触发Compaction。
我记得在某个项目中,我们按天分区。当天分区每10分钟检查一次文件数量,超过200就触发合并;而7天前的分区,每天只检查一次,超过500才触发。这样资源利用率高了很多。
实现方式(伪代码):
// 分区级别触发判断逻辑
for each partition in table:
file_count = getFileCount(partition)
avg_file_size = getAvgFileSize(partition)
partition_age = getPartitionAge(partition)
if partition_age < 1 day:
// 热分区:高频检查
if file_count > 200 or avg_file_size < 64MB:
triggerCompaction(partition)
else:
// 冷分区:低频检查
if file_count > 500 or avg_file_size < 32MB:
triggerCompaction(partition)
4.5 四种策略怎么选?
没有银弹。我一般这样决策:
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 实时写入,小文件多 | 文件数量 + 文件大小 | 快速响应小文件堆积 |
| 定时批量写入 | 时间窗口 | 写入完成后统一合并 |
| 混合负载(热冷分区) | 分区级别 | 差异化处理,节省资源 |
| 资源紧张 | 时间窗口 + 文件大小 | 在低峰期合并,避免资源争抢 |
我的经验: 刚开始做Compaction时,别搞太复杂。先用「文件数量+时间窗口」的组合,跑一段时间看看效果。等摸清了数据规律,再逐步引入分区级别触发。一步到位反而容易出问题。
最后说一句:触发策略不是配完就不管了。数据量在变,写入模式在变,你的策略也要跟着调。我每季度会复盘一次Compaction的触发记录,看看有没有误触发或者漏触发的情况。嗯,这活儿不复杂,但很值得做。