Apache Spark 开发与调优实战手册 (Java / Spark 2.x)

环境 :本地开发 (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 开发的主动权!

相关推荐
无心水5 小时前
【任务调度:数据库锁 + 线程池实战】3、 从 SELECT 到 UPDATE:深入理解 SKIP LOCKED 的锁机制与隔离级别
java·分布式·科技·spring·架构
编程小白gogogo6 小时前
苍穹外卖图片不显示解决教程
java·spring boot
舟舟亢亢6 小时前
算法总结——二叉树【hot100】(上)
java·开发语言·算法
百锦再6 小时前
Java中的char、String、StringBuilder与StringBuffer 深度详解
java·开发语言·python·struts·kafka·tomcat·maven
努力努力再努力wz7 小时前
【Linux网络系列】:TCP 的秩序与策略:揭秘传输层如何从不可靠的网络中构建绝对可靠的通信信道
java·linux·开发语言·数据结构·c++·python·算法
yy.y--8 小时前
Java数组逆序读写文件实战
java·开发语言
BD_Marathon9 小时前
IDEA创建多级包时显示在同一行怎么办
java·ide·intellij-idea
亓才孓9 小时前
【Exception】CONDITIONS EVALUATION REPORT条件评估报告
java·开发语言·mybatis
硅基动力AI10 小时前
如何判断一个关键词值不值得做?
java·前端·数据库
重生之后端学习10 小时前
78. 子集
java·数据结构·算法·职场和发展·深度优先