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 开发的主动权!

相关推荐
heartbeat..4 小时前
Redis 中的锁:核心实现、类型与最佳实践
java·数据库·redis·缓存·并发
4 小时前
java关于内部类
java·开发语言
好好沉淀4 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
gusijin4 小时前
解决idea启动报错java: OutOfMemoryError: insufficient memory
java·ide·intellij-idea
To Be Clean Coder4 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
吨~吨~吨~5 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea
你才是臭弟弟5 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
短剑重铸之日5 小时前
《设计模式》第二篇:单例模式
java·单例模式·设计模式·懒汉式·恶汉式
码农水水5 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
summer_du5 小时前
IDEA插件下载缓慢,如何解决?
java·ide·intellij-idea