1. 文件大小与Spark任务分片机制:深入理解Spark如何根据文件大小决定分区数,以及分区数对并行度的影响
做Spark调优这么多年,我见过最多的新手误区就是——上来就问“并行度设多少合适?”其实,并行度的根,在文件分片里。你想想看,Spark读文件时,分区数怎么来的?说白了,就是文件大小除以一个叫splitSize的值,再向上取整。
今天咱们就把这个机制彻底掰开揉碎。
1.1 核心概念:分区、分片与并行度
先理清三个词:
- 分区(Partition):Spark里最小的并行执行单元。一个分区对应一个Task。
- 分片(Split):文件在HDFS/本地文件系统上的逻辑切分。一个分片通常对应一个分区。
- 并行度:同时执行的Task数量。它由分区数决定,但受限于集群资源。
我习惯把分区比作“工位”,Task就是“工人”。工位多了,工人才能多干活。但工位太少,工人就得排队等——这就是数据倾斜和资源浪费的根源。
核心公式:
分区数 = ceil( 输入数据总大小 / 分区目标大小 )
其中“分区目标大小”由 spark.sql.files.maxPartitionBytes 控制,默认128MB。
1.2 文件大小如何决定分区数?
Spark读取文件时,会经历以下步骤:
- 获取文件列表:列出输入路径下的所有文件。
- 计算每个文件的分片数:对每个文件,用
FileInputFormat的getSplits()方法计算。 - 合并小文件分片:如果文件很小,Spark会尝试合并多个小文件的分片,避免产生过多小分区。
- 生成分区:每个分片最终变成一个Partition。
举个例子:
// 假设有3个文件:
// file1: 256MB
// file2: 128MB
// file3: 10MB
// 默认 maxPartitionBytes = 128MB
// file1 分成 2 个分区 (256/128=2)
// file2 分成 1 个分区 (128/128=1)
// file3 分成 1 个分区 (10MB < 128MB,但单独一个分区)
// 总分区数 = 2 + 1 + 1 = 4
嗯,这里要注意:小文件不会跨文件合并。每个文件至少一个分区,哪怕它只有1KB。这就是小文件问题的根源之一。
1.3 分区数对并行度的影响
分区数直接决定了Task数量。而Task数量,又决定了你能用多少CPU核心。
| 分区数 | 可用CPU核心数 | 实际并行Task数 | 效果 |
|---|---|---|---|
| 10 | 100 | 10 | 资源浪费,90个核心闲置 |
| 200 | 100 | 100 | 理想状态,核心全用上 |
| 500 | 100 | 100 | Task调度开销增大,但可接受 |
| 2000 | 100 | 100 | 调度开销显著,GC压力大 |
我曾经在一个项目中,发现集群有200个核心,但分区数只有20。结果每个Task处理的数据量巨大,执行时间超长,而大部分核心都在空转。调整 maxPartitionBytes 后,分区数变成200,执行时间直接缩短了5倍。
经验法则:分区数建议设置为集群可用CPU核心数的2~3倍。这样既能充分利用资源,又能应对数据倾斜时的Task straggler问题。
1.4 文件大小分布对分区的实际影响
现实中的文件,大小往往不均匀。咱们看几种典型场景:
场景一:大文件 + 小文件混合
假设你有1个1GB的大文件和100个1MB的小文件。默认128MB分区:
- 大文件:1GB / 128MB = 8个分区
- 小文件:100个文件 × 1分区/文件 = 100个分区
- 总分区数:108个
你看,100个小文件贡献了92%的分区,但数据量只占10%。这会导致大量Task处理极少数据,调度开销却一点不少。
场景二:所有文件大小一致
10个文件,每个128MB。分区数 = 10。每个Task处理128MB数据,非常均匀。这是最理想的情况。
场景三:单个超大文件
1个文件10GB。分区数 = 10GB / 128MB ≈ 80个分区。每个Task处理128MB,也很均匀。但要注意,如果文件是压缩格式(如gzip),它可能无法切分,只能一个分区处理整个文件——这就是另一个坑了。
避坑指南:我曾经遇到一个生产事故——一个gzip压缩的5GB日志文件,Spark只生成1个分区,单个Task处理5GB数据,跑了2小时还没跑完。后来改成不压缩的文本文件,分区数变成40个,10分钟就跑完了。
所以,可切分的压缩格式(如snappy、lzo、bzip2)比不可切分的(如gzip、zip)更适合Spark。
1.5 核心参数与调优建议
Spark提供了几个参数来控制分区行为:
| 参数名 | 默认值 | 作用 | 我的建议 |
|---|---|---|---|
spark.sql.files.maxPartitionBytes |
134217728 (128MB) | 每个分区的最大字节数 | 根据集群内存和CPU核心数调整,通常64MB~256MB |
spark.sql.files.openCostInBytes |
4194304 (4MB) | 小文件合并的“代价阈值”,小于此值的文件会被合并 | 如果小文件多,可以调大到16MB或32MB |
spark.sql.adaptive.coalescePartitions.enabled |
true (Spark 3.0+) | AQE自动合并小分区 | 建议开启,能自动优化分区数 |
spark.sql.adaptive.coalescePartitions.parallelismFirst |
true | 合并分区时是否优先保证并行度 | 如果资源充足,保持默认 |
一句话总结:文件大小决定分区数,分区数决定并行度。并行度不够,资源浪费;并行度太高,调度开销大。找到平衡点,就是调优的关键。
1.6 知识体系图:文件大小与分片机制
下面这张图,把整个流程串起来了:
从这张图可以清楚看到:文件大小和数量,经过分片计算和小文件合并后,最终决定了分区数。而分区数,直接决定了并行执行的Task数量。
1.7 实战建议
最后,给你几个我在项目中反复验证过的建议:
- 控制文件大小:尽量让每个文件在64MB~256MB之间。太小了分区多,太大了Task处理慢。
- 减少小文件:如果小文件不可避免,调大
openCostInBytes让Spark自动合并。 - 开启AQE:Spark 3.0+ 的 Adaptive Query Execution 能自动合并小分区,省心不少。
- 选对压缩格式:优先用snappy或lzo,别用gzip。
- 监控分区数:在Spark UI的Stages页面,看一眼“Number of Tasks”就知道分区数是否合理。
嗯,这一章的内容就到这儿。记住一句话:分区数不是越大越好,也不是越小越好,而是要匹配你的资源和数据。