环境 :本地开发 (Windows) / 生产环境
核心场景:大数据量炸裂 (Explode)、大字典关联 (Join)、多路输出
第一章:核心代码模板 (最佳实践)
本模板解决了"同一份数据源,需要分流处理(一份未匹配,一份匹配并排序)"时的重复计算问题。
1.1 完整代码逻辑
java
// 1. 初始化 SparkSession (包含本地磁盘优化配置)
SparkSession spark = SparkSession.builder()
.appName("ForeignFamilyFieldCompleter")
.master("local[*]")
// 【关键配置】防止本地 C 盘爆满,指定大容量盘作为临时目录
.config("spark.local.dir", "F:\\spark_temp")
// 【关键配置】针对小文件过多的场景,降低打开文件的预估成本(默认4MB太大了),促进自动合并
.config("spark.sql.files.openCostInBytes", "102400") // 100KB
.config("spark.sql.shuffle.partitions", "500") // 根据数据量调整,本地跑大任务建议调大
.getOrCreate();
// 2. 读取与预处理 (Explode)
Dataset<Row> fg = spark.read().parquet("F:\\data\\fg_family_test\\input\\...");
Dataset<Row> fg_exploded = fg.withColumn("family_info_cn", explode(col("family_info")))
.filter(col("family_info_cn").startsWith("CN")); // String类型直接操作,勿用点号访问
// 3. 读取字典
Dataset<Row> latestDict = spark.read().parquet("...");
// 4. 核心 Join (最昂贵的步骤)
// 本地跑 5G 字典,内存不足以支撑 Broadcast,使用默认的 SortMergeJoin 更稳定
Dataset<Row> joinedData = fg_exploded.as("left")
.join(
latestDict.as("right"),
// 建议使用 col("...").equalTo(...) 并在 Join 后 drop 掉右边的主键,防止列名歧义
col("left.family_info_cn").equalTo(col("right.dict_pub_num")),
"left_outer"
);
// =======================================================
// 【核心优化点】持久化缓存
// =======================================================
// 原因:joinedData 将被后续两个任务共用。如果不缓存,Join 过程会被计算两次。
// 策略:MEMORY_AND_DISK。内存放不下自动溢写到 F 盘,保证任务不崩。
joinedData.persist(StorageLevel.MEMORY_AND_DISK());
try {
// 5. 任务分支 A:提取未关联数据 (用于日志/补录)
Dataset<Row> unMatched = joinedData.filter(col("right.dict_pub_num").isNull());
// 【输出优化】未匹配数据通常是少数。
// 使用 repartition(1) 强制合并成 1 个文件,避免产生数千个 KB 级的小文件。
unMatched.repartition(1).write().mode("overwrite")
.parquet("F:\\data\\fg_family_test\\unMatchedData");
// 6. 任务分支 B:关联数据开窗取最新 (主业务)
// 此时 Spark 直接读取缓存,跳过 Read -> Explode -> Join 步骤,速度极大提升。
Dataset<Row> matched = joinedData.filter(col("right.dict_pub_num").isNotNull());
WindowSpec windowSpec = Window.partitionBy(col("left.publication_number"))
.orderBy(col("right.publication_date").desc_nulls_last());
Dataset<Row> finalResult = matched
.withColumn("rn", row_number().over(windowSpec))
.where(col("rn").equalTo(1))
.drop("rn")
.drop("dict_pub_num"); // 清理多余列
// 【输出优化】根据数据总量控制文件数 (5G数据 / 128MB ≈ 40个文件)
finalResult.coalesce(40).write().mode("overwrite")
.parquet("F:\\data\\fg_family_test\\output\\20260124");
} finally {
// 7. 【必须】手动释放资源
joinedData.unpersist();
}
// 8. 【调试技巧】本地跑完暂停,为了看 Spark UI
// System.out.println("任务结束,请访问 http://localhost:4040 查看 UI");
// Thread.sleep(1000000);
第二章:关键性能参数调优
2.1 Shuffle Partitions (洗牌分区数)
- 参数名 :
spark.sql.shuffle.partitions - 默认值: 200
- 原理: 决定了 Join、GroupBy、Window 操作后数据被切分成多少份。
- 如何设置:
- 本地极小数据 : 设为 10-20 (减少调度开销,跑得更快)。
- 生产/大数据 (当前场景) : 设为 500 ~ 1000。
- 计算公式 :
Shuffle Stage 总数据量 / 目标单分区大小(128MB)。 - 例如 : Join 产生了 50GB 数据 ->
50 * 1024 / 128 ≈ 400。宁大勿小。
2.2 小文件自动合并 (File Packing)
- 参数名 :
spark.sql.files.maxPartitionBytes(默认 128MB) - 隐形杀手 :
spark.sql.files.openCostInBytes(默认 4MB) - 问题: Spark 默认认为打开一个文件等于读 4MB 数据。如果你有 10000 个 1KB 的小文件,Spark 估算的开销是 40GB,从而切分出数千个 Task。
- 解决 : 在 SparkSession 中将
openCostInBytes调小 (如102400即 100KB),让 Spark 更积极地将小文件打包到一个 Partition 中。
第三章:Coalesce vs Repartition (文件数控制)
这是控制输出文件数量、合并碎片的关键操作。
| 特性 | Coalesce(N) | Repartition(N) |
|---|---|---|
| 是否 Shuffle | 否 (不走网络,不做排序) | 是 (全量洗牌,数据重新打散) |
| 主要用途 | 减少分区数、合并小文件 | 增加分区数、解决数据倾斜 |
| 性能 | 极快 (本地合并) | 较慢 (涉及磁盘IO和网络) |
| 典型场景 1 | 读取大量小文件后,Task 数巨多,每个 Task 跑几毫秒。使用 coalesce 合并 Task。 |
输出最终结果时,希望每个文件大小均匀 (如 128MB)。 |
| 典型场景 2 | 大幅 filter 过滤后,剩下的数据很稀疏。 |
上游计算逻辑复杂,数据严重倾斜,必须重新洗牌。 |
| 避坑指南 | **不要在极小数据量写入时用 coalesce(1)**,如果上游并行度高,这会导致所有压力压在一个节点上。此时改用 repartition(1)。 |
第四章:持久化策略 (Cache/Persist)
4.1 什么时候用?
当一个经过复杂计算(如 Join、Explode)得到的 DataFrame,被后续代码使用了两次及以上时(如一个用于统计 count,一个用于写入 write)。
4.2 级别选择
| 级别 | StorageLevel.MEMORY_AND_DISK |
StorageLevel.MEMORY_AND_DISK_SER |
|---|---|---|
| 描述 | 优先存内存,存不下写磁盘。存的是 Java 对象。 | 优先存内存,存不下写磁盘。存的是序列化后的字节。 |
| 优点 | 读取速度最快,CPU 开销小。 | 极度节省内存,对 GC (垃圾回收) 非常友好。 |
| 缺点 | 占用内存大,易导致 Full GC。 | 读取时需要反序列化,多一点 CPU 开销。 |
| 适用场景 | 本地开发 (简单粗暴,防止 OOM)。 | 生产环境 (推荐,防止拖垮集群)。 |
4.3 风险提示
- 磁盘满: 本地运行时,溢写的数据会存到临时目录。如果 C 盘/F 盘空间不足,会导致任务失败。
- 忘记释放 : 任务结束前必须调用
.unpersist(),否则资源一直被占用。
第五章:本地调试与故障排查
5.1 C 盘爆满 (No Space Left on Device)
- 原因 : Spark 本地模式默认将 Shuffle 中间文件和溢写数据存放在系统临时目录 (
%TEMP%)。 - 解决:
java
.config("spark.local.dir", "F:\\spark_temp") // 指定空间大的盘
5.2 任务跑得慢
- 没有 Cache: 检查是否同一个 DF 被用了两次,导致重复计算。
- 没有 Broadcast : 检查 Join 的右表是否很小 (<1GB)。如果是,使用
broadcast(dict)。 - 任务数过多 : UI 显示数万个 Task?调整
openCostInBytes或使用coalesce。 - 任务数过少 : UI 显示只有几个 Task 但每个都要跑很久?调大
shuffle.partitions。
5.3 怎么看 Spark UI(本地默认端口4040)
- 进不去 : 也就是网页打不开。原因通常是程序跑完退出了。解决办法:在
main结束前加Thread.sleep(1000000);。 - 看哪里:
- Shuffle Read/Write: 如果这个数字很大 (如 >10GB),说明发生了物理 Join。
- Tasks (Running): 如果 Running 数很少 (如 5),说明受限于本地 CPU 核心数。
- Duration (Max vs Median) : 如果 Max 时间远大于 Median,说明发生了数据倾斜。
写在最后 :
Spark 调优的核心在于**"平衡"**------在内存与磁盘之间平衡(Cache),在小文件数量与并发度之间平衡(Coalesce),在 Shuffle 开销与数据分布之间平衡(Partitions)。掌握了这份手册里的参数和策略,你就掌握了 Spark 开发的主动权!