3. 小文件合并:小文件产生原因、合并策略、合并工具与参数调优
小文件问题,可以说是 Iceberg 生产环境里最让人头疼的「慢性病」。我见过不少团队,表里几万个文件,查询慢得像蜗牛,元数据操作动不动就超时。说白了,小文件不治理,存储和计算成本都会失控。
这一节,我们就把小文件的来龙去脉、合并手段、以及参数怎么调,一次性讲透。
3.1 小文件是怎么产生的?
先看看源头。小文件不是凭空冒出来的,常见的场景有这么几个:
- 流式写入太频繁:Kafka 实时入湖,每几分钟就提交一次,每次只写几十 MB。我遇到过一天产生 2 万多个小文件的案例。
- 分区粒度过细:按小时甚至按分钟分区,每个分区数据量很少,文件自然就小了。
- 并发写入冲突:多个 Spark 任务同时写同一张表,每个任务各自写自己的文件,合并前谁也不管谁。
- 更新/删除操作:Iceberg 的 MOR(Merge-on-Read)模式会产生 delete files,积累多了也是小文件。
- 历史遗留:从 Hive 迁移过来的表,原本就有大量小文件,Iceberg 不会自动帮你合并。
核心结论:小文件的本质是「写入频率」和「数据量」不匹配。频率越高、单次数据量越小,小文件就越多。
3.2 合并策略:什么时候合并?合并成多大?
合并不是无脑跑一个命令就完事。你需要根据业务场景选策略。我个人习惯把合并策略分成三种:
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 定时批量合并 | 离线数仓、T+1 报表 | 简单可控,对查询影响小 | 实时性差,数据延迟高 |
| 增量合并 | 近实时流处理 | 延迟低,文件数可控 | 实现复杂,需配合 compaction |
| 按分区合并 | 分区数据量差异大 | 只合并热分区,节省资源 | 冷分区可能被忽略 |
至于合并成多大,我建议你参考 HDFS 的块大小。128MB 到 256MB 是一个比较稳妥的范围。文件太小,NameNode 压力大;文件太大,并行度又不够。
我的经验:目标文件大小设为 256MB 时,查询性能和元数据开销能达到比较好的平衡。如果你的表经常做全表扫描,可以适当放大到 512MB。
3.3 合并工具:Iceberg 的 RewriteDataFiles
Iceberg 官方提供了 RewriteDataFilesAction,这是最核心的合并工具。它底层用的是 Spark,但封装得比较友好。
基本用法是这样的:
import org.apache.iceberg.spark.actions.SparkActions
SparkActions.get()
.rewriteDataFiles(table)
.option("target-file-size-bytes", "268435456") // 256MB
.option("max-concurrent-file-group-rewrites", "10")
.execute()
这段代码会扫描表的所有数据文件,把小于目标大小的文件合并成大文件。嗯,这里要注意:合并过程会重写数据,所以会占用额外的存储空间和计算资源。
3.4 参数调优:让合并跑得更稳
参数调优是门手艺活。我踩过不少坑,下面这几个参数是我觉得最关键的:
| 参数名 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
target-file-size-bytes |
512MB | 256MB | 合并后的目标文件大小 |
min-file-size-bytes |
75MB | 100MB | 小于此值的文件才会被合并 |
max-concurrent-file-group-rewrites |
50 | 10-20 | 并发合并的组数,太大容易撑爆资源 |
rewrite-job-order |
none | bytes-asc | 按文件大小升序处理,优先合并小文件 |
partial-progress-enabled |
false | true | 启用部分进度,避免单次合并时间过长 |
避坑指南:我曾经把 max-concurrent-file-group-rewrites 设成 50,结果集群资源被打满,其他查询全挂了。建议从 10 开始,观察集群负载再逐步调高。
3.5 合并流程全景图
下面这张图展示了小文件合并的完整流程,从扫描到提交,每一步都有讲究。
3.6 实战建议:合并频率怎么定?
合并频率没有标准答案,但你可以参考这个原则:文件数不要超过分区数据量的 10 倍。比如一个分区有 10GB 数据,目标文件 256MB,那文件数应该在 40 个左右。如果超过 400 个,就该合并了。
我一般这样安排:
- 离线表:每天凌晨跑一次合并,配合 ETL 调度。
- 实时表:每 1-2 小时跑一次增量合并,用
partial-progress-enabled=true避免单次耗时过长。 - 大表(TB 级):按分区合并,只合并最近 7 天的热分区,历史分区一个月合并一次。
小技巧:合并前先用 table.snapshot().summary() 查看当前文件数,再决定要不要跑合并。如果文件数不多,就别浪费资源了。
3.7 避坑总结
最后,把我踩过的坑列出来,你遇到了可以直接对照:
- 合并期间 OOM:
max-concurrent-file-group-rewrites设太大,Spark Executor 内存不够。建议从 5 开始试。 - 合并后查询变慢:目标文件设得太大,导致扫描并行度下降。256MB 是个安全起点。
- 合并任务卡住:表有并发写入,导致提交冲突。可以开启
partial-progress-enabled,或者错开写入窗口。 - 存储空间暴涨:合并期间新旧文件同时存在,磁盘占用翻倍。确保集群有足够的临时空间。
小文件合并不是一次性工作,而是一个持续治理的过程。把参数调好、频率定好,你的 Iceberg 表才能跑得又快又省。