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触发策略决策流程 数据写入完成 是否热分区(写入频繁)? 是 高频检查:每10分钟 文件数 > 200 或 平均文件大小 < 64MB 否 低频检查:每天1次 文件数 > 500 或 平均文件大小 < 32MB 触发Compaction 说明:热分区高频检查,冷分区低频检查,差异化触发避免资源浪费

最后说一句:触发策略不是配完就不管了。数据量在变,写入模式在变,你的策略也要跟着调。我每季度会复盘一次Compaction的触发记录,看看有没有误触发或者漏触发的情况。嗯,这活儿不复杂,但很值得做。