3、合并策略基础:核心思想、粒度选择与时机把握
小文件合并这事儿,说白了就是「把零钱换成整钞」。你想想看,HDFS 存一个小文件就要记一条元数据,NameNode 内存就那么点,文件一多直接撑爆。我早年接手过一个离线数仓,光用户行为日志就攒了上千万个小文件,NameNode 的 GC 停顿能把人逼疯。嗯,合并就是来解决这个问题的。
3.1 合并的核心思想
合并的本质很简单:把多个小文件拼成一个大文件。但这里有个关键点——合并不是简单的文件拼接,你得考虑后续怎么读。
核心原则:合并后的文件大小要匹配底层存储的块大小(比如 HDFS 默认 128MB),同时要保证数据在逻辑上仍然可分割、可并行读取。
我个人习惯把合并策略拆成三个维度来看:
- 空间维度:文件要多大才合适?太小浪费元数据,太大又影响并行度。
- 时间维度:什么时候该合并?是攒够一批就合,还是等系统空闲再合?
- 逻辑维度:哪些文件可以合在一起?同分区、同表、还是同数据源?
这三个维度缺一不可。我在项目中见过有人只盯着文件大小,结果把不同分区的数据合到一个文件里,查询时全表扫描,性能反而更差。说白了,合并不是目的,让查询更快才是。
3.2 合并的粒度选择
粒度选择是合并策略里最容易踩坑的地方。选大了,合并效果不明显;选小了,合并本身的开销比收益还大。
3.2.1 文件大小粒度
我一般按以下标准来定目标文件大小:
| 存储类型 | 块大小 | 建议合并目标 | 说明 |
|---|---|---|---|
| HDFS | 128MB | 64MB ~ 256MB | 略小于块大小或整数倍,避免跨块读取 |
| HDFS | 256MB | 128MB ~ 512MB | 大块场景下适当放大 |
| 对象存储 | 无固定块 | 16MB ~ 64MB | 对象存储的元数据开销较小,但请求次数有限 |
| 本地文件系统 | 4KB | 1MB ~ 16MB | 本地场景下主要减少 inode 消耗 |
这里有个经验值:合并后的文件数量控制在 1000 个以内,对大多数查询引擎来说是比较舒服的范围。我曾经遇到一个 Spark 任务,读取 10 万个小文件,光是打开文件句柄就花了 3 分钟。合并到 500 个大文件后,读取时间降到 10 秒。
3.2.2 分区粒度
按分区合并是最常见的做法。每个分区的文件独立合并,互不干扰。这样做的好处是:
- 合并操作可以并行执行,不同分区互不影响
- 查询时按分区裁剪,只读需要的分区
- 单个分区出问题不影响全局
但要注意,分区粒度过细也有问题。比如按小时分区,一天 24 个分区,每个分区只有几百 KB 数据,合并后还是小文件。我建议:分区粒度要和数据量匹配。如果每小时数据量不到 1GB,就别按小时分区了,按天分区更合理。
3.2.3 表粒度
有些场景下,整张表的数据量都不大,比如维度表、配置表。这时候按表粒度合并就够了。我一般这样判断:
- 如果整张表数据量 < 1GB,直接合并成 1 个文件
- 如果 1GB ~ 10GB,合并成 8~16 个文件
- 如果 > 10GB,按分区粒度处理
小技巧:对于经常全表扫描的小表,合并成单个文件反而能利用顺序读的优势,比多个小文件快得多。
3.3 合并的时机选择
时机选不对,合并反而变成负担。我总结了三类常见的合并时机策略:
3.3.1 定时合并
最直接的方式——每天凌晨跑一个合并任务。适合数据写入有明确周期、对实时性要求不高的场景。
- 优点:简单可控,不影响白天业务
- 缺点:如果白天写入量暴增,小文件会堆积到晚上才处理
- 适用场景:离线数仓、T+1 报表
我曾经维护过一个定时合并任务,每天凌晨 2 点跑。结果有一天业务方搞了个大促,数据量翻了 10 倍,凌晨合并跑了 4 个小时还没跑完,直接影响了第二天的早报。从那以后,我学会了加一个「文件数阈值」的兜底逻辑。
3.3.2 阈值触发合并
设定一个阈值,比如「分区内文件数超过 1000 个」或「小文件总大小超过 5GB」,一旦触发就立即合并。
| 阈值类型 | 推荐值 | 触发动作 |
|---|---|---|
| 文件数量 | 500 ~ 2000 | 启动合并任务 |
| 文件总大小 | 块大小的 2~4 倍 | 启动合并任务 |
| 文件平均大小 | < 块大小的 1/10 | 启动合并任务 |
阈值触发的好处是响应及时,但要注意避免「合并风暴」——多个分区同时触发合并,把集群资源打满。我一般会加一个全局并发限制,比如同时最多跑 5 个合并任务。
3.3.3 写入时合并(在线合并)
在数据写入的同时做合并。比如 Flink 的 StreamingFileSink 就支持按文件大小或时间滚动,写入过程中自动合并小文件。
- 优点:实时性好,小文件不会堆积
- 缺点:增加写入延迟,对写入性能有影响
- 适用场景:实时流处理、近实时数仓
注意:在线合并要小心「小文件抖动」——刚合并完的大文件,如果后续又有少量数据写入,可能会被再次拆分。建议设置一个「冷却期」,合并后的一段时间内不再对该文件做操作。
3.4 合并策略的决策流程
说了这么多,到底怎么选?我画了一张决策流程图,你可以对照着来:
3.5 合并策略的避坑指南
最后分享几个我踩过的坑,希望能帮你少走弯路:
坑1:合并后文件过大
我曾经把一个小表合并成 2GB 的单个文件,结果下游的 Spark 任务只有一个分区在读,跑得比合并前还慢。合并不是越大越好,要兼顾并行度。
坑2:忽略合并本身的开销
合并任务要读数据、写数据,本身就会消耗 IO 和 CPU。如果小文件本身只有几 KB,合并的读写开销可能比省下来的元数据开销还大。我一般建议:文件平均大小小于块大小的 1/20 时,才值得合并。
坑3:合并后数据倾斜
按某个字段合并时,如果该字段分布不均匀,会导致合并后的大文件大小差异巨大。比如按用户 ID 合并,活跃用户的数据可能是一个文件的 10 倍。这时候要考虑加一个「二次散列」或者「范围分区」来均衡。
我的习惯:每次上线合并策略前,先用一个小分区做灰度测试。跑一遍合并前后的查询,对比耗时和资源消耗。数据不会骗人,效果好不好,一跑便知。