小文件问题详解
聊完文件大小对 Spark 的整体影响,咱们得先搞清楚一个最基础的问题——到底多大的文件算「小文件」?
说实话,这个问题没有的数字标准。我个人的习惯是:如果一个文件在 HDFS 上的块大小是 128MB,但实际数据只有几 MB 甚至几百 KB,那它就是小文件。更准确地说,当文件数量远大于并行度(分区数)时,小文件问题就来了。
小文件的定义:单个文件大小远小于 HDFS 块大小(通常 128MB),且文件数量远大于 Spark 任务并行度,导致元数据膨胀、调度开销剧增、I/O 效率低下的文件。
小文件产生的根源
小文件不是凭空冒出来的。我在项目中排查过不下 20 次小文件问题,总结下来,根源就三类。
1. 流式写入
这是最常见的元凶。你想想看,Kafka 过来的数据每秒几百条,如果用 saveAsTextFile 或 foreachBatch 每批次直接写 HDFS,每批次生成一个文件。一天下来,几千个文件就出来了。
我记得有一次帮一个实时数仓团队做优化,他们的 Structured Streaming 作业每 10 秒触发一次微批次,每次写一个 parquet 文件。一个月后,HDFS 上某个分区目录下堆了 26 万个文件。NameNode 直接报警,RPC 延迟飙到 5 秒以上。
避坑指南:流式写入时,我建议用 coalesce(1) 或 repartition(1) 合并输出,或者使用 FileSink 的 rolling 策略,按时间或数据量滚动合并。
2. 过度分区
这个坑我自己也踩过。刚用 Spark 时,总觉得分区越多并行度越高,于是写代码时随手就 repartition(10000)。结果呢?每个分区就几百条数据,写出去就是一万个小文件。
过度分区通常发生在:
- 盲目调大
spark.sql.shuffle.partitions:默认 200 其实够用,有人非要设成 2000 - 使用
repartition但没考虑数据量:100MB 数据分 1000 个区,每个区才 100KB - Join 或 Aggregation 后没做合并:Shuffle 完分区数没变,但数据量已经很小了
注意:分区数不是越大越好。分区数 = 数据量 / 目标文件大小(建议 128MB~256MB)。我一般用公式:分区数 = max(总数据量 / 256MB, 集群核心数)。
3. 动态分区插入
这个场景在数仓 ETL 里特别常见。用 INSERT OVERWRITE TABLE PARTITION 时,如果分区键的基数很高(比如按 user_id 分区),每个分区可能只有几条数据,但每个分区都会生成一个文件。
我见过最夸张的例子:一个表按小时分区,但数据源延迟到达,导致每个小时分区下又有几十个文件。最终一个月的分区下堆了 10 万+ 文件。
4. 数据倾斜的副产品
数据倾斜不仅影响性能,还会产生小文件。倾斜的 key 所在分区数据量大,正常分区数据量小。正常分区写出去的文件可能只有几 KB,这就是小文件。
小文件对 Driver 的负面影响
很多人以为小文件只影响 Executor,其实 Driver 才是第一个受害者。
| 影响维度 | 具体表现 | 我见过的案例 |
|---|---|---|
| 元数据膨胀 | Driver 需要维护所有输出文件的元数据(路径、大小、块位置)。10 万个文件,光元数据就要占几百 MB 内存 | 有一次 Driver 堆内存 8GB,GC 频繁,最后 OOM。查下来是 30 万个小文件导致的 |
| Task 调度开销 | 每个小文件对应一个 Task,Driver 要创建、调度、跟踪这些 Task。10 万个 Task 的调度时间可能超过 5 分钟 | 我见过一个作业,实际计算只用了 30 秒,但调度花了 3 分钟 |
| RDD Lineage 膨胀 | 小文件导致 RDD 分区数暴增,DAG 图变得巨大,Driver 序列化和反序列化开销剧增 | 用 toDebugString 看 lineage,打印出来有几千行 |
小文件对 Executor 的负面影响
Executor 层面更直接——全是 I/O 和序列化的开销。
- Task 启动开销占比过高:每个 Task 启动需要序列化、广播变量、建立连接。如果处理时间只有 100ms,启动时间占 200ms,那效率只有 33%
- HDFS I/O 效率低下:小文件意味着随机读取,而不是顺序读取。HDFS 擅长处理大文件的顺序读,小文件的寻道开销占比太大
- 网络传输放大:Shuffle 时,小文件会导致 Map 输出碎片化,Reduce 端需要拉取大量小数据块,网络连接数暴增
- GC 压力增大:每个 Task 都要创建反序列化器、迭代器对象。Task 数量一多,对象创建和销毁频繁,GC 频繁触发
一句话总结:小文件让 Spark 从「计算密集型」变成了「调度密集型 + I/O 密集型」,性能断崖式下跌。
小文件问题的知识体系
下面这张图把整个小文件问题的脉络梳理清楚了。从定义到根源,再到对 Driver 和 Executor 的影响,一目了然。
嗯,这张图把整个小文件问题的脉络都串起来了。从定义到四个根源,再到对 Driver 和 Executor 的具体影响,每个环节都环环相扣。你想想看,如果流式写入不加控制,再叠加上过度分区,那后果就是 Driver 内存爆掉、Executor 忙着启动 Task 而不是算数据。
我曾经帮一个团队排查过这样的问题:他们的流式作业每天产生 5 万个小文件,Driver 的 GC 时间占到了 40%,作业经常失败。最后我们做了三件事——合并小文件、调整分区策略、改用 coalesce 控制输出文件数。效果立竿见影,作业稳定了,性能提升了 3 倍。
我的建议:在日常开发中,养成检查输出文件数的习惯。用 hdfs dfs -count 或 Spark UI 的 Storage 页签看看文件数量和大小分布。如果发现大量小于 10MB 的文件,就要警惕了。