Spark Core 开发环境全链路配置指南:从 JDK 到 IDEA 一文搞定

Spark Core 之 Spark 基于开发工具的详细配置讲解

摘要:JDK → Scala → Spark → Maven → IDEA 全链路安装配置,完整 pom.xml 可直接复制使用,WordCount 源码逐行解析,10 个高频错误排查,4 张原创架构图,四阶段学习路线。面向 Java、大数据及 AI 开发工程师,确保读完即可动手搭建 Spark 开发环境。

一、开发环境四层架构总览

Spark 开发环境从底层到上层分为四个层级,每层对下层有严格的版本约束:

![Spark 开发环境四层架构](./diagrams/de-01-stack.png)


层级组件关键约束
Layer 0: OSLinux/macOS/WindowsWindows 需 winutils.exe
Layer 1: RuntimeJDK 8 + Scala 2.12.15Spark 3.x = Scala 2.12
Layer 2: BuildMaven 3.6+ / SBT 1.5+shade-plugin 必配
Layer 3: IDEIntelliJ IDEA + Scala PluginRun Config -Dspark.master=local[4]
Layer 4: Deploylocal / YARN / K8s / Standaloneclient 调试 / cluster 生产

二、JDK 安装配置

Spark 3.x 官方推荐 JDK 8(1.8.0_311+),Spark 3.3+ 开始试验性支持 JDK 11 和 17。生产环境强烈建议 JDK 8

Linux(Ubuntu/Debian)

# 安装 OpenJDK 8
sudo apt update
sudo apt install openjdk-8-jdk -y

# 验证安装
java -version
# 输出示例: openjdk version "1.8.0_312"

# 确认 JAVA_HOME
echo $JAVA_HOME
# 若为空,追加到 ~/.bashrc:
echo 'export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64' >> ~/.bashrc
source ~/.bashrc

macOS

# 通过 Homebrew 安装
brew install openjdk@8

# 设置 JAVA_HOME(追加到 ~/.zshrc)
echo 'export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)' >> ~/.zshrc
source ~/.zshrc

Windows

1. 下载 [Oracle JDK 8](https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html) 安装包

2. 安装到 C:\Program Files\Java\jdk1.8.0_xxx

3. 系统环境变量:JAVA_HOME = C:\Program Files\Java\jdk1.8.0_xxx

4. Path 追加 %JAVA_HOME%\bin

⚠️ 不要用 JDK 11+ 运行 Spark 3.2 以下版本,否则直接报 `java.lang.NoClassDefFoundError`。版本对照:

>

| Spark 版本 | 推荐 JDK | Scala 版本 |
|-----------|---------|-----------|
| 2.4.x | JDK 8 | 2.11.12 |
| 3.0 - 3.2 | JDK 8 | 2.12.15 |
| 3.3+ | JDK 8 / 11 | 2.12.15 / 2.13.8 |
| 3.4+ | JDK 8 / 11 / 17 | 2.12.17 / 2.13.8 |

三、Scala 安装与版本匹配

铁律:你代码中的 Scala 版本 **必须与 Spark 编译时的 Scala 版本完全一致**。

Linux 安装

# 方式1:apt 安装(推荐)
sudo apt install scala -y

# 方式2:手动下载
wget https://downloads.lightbend.com/scala/2.12.15/scala-2.12.15.tgz
tar -xzf scala-2.12.15.tgz -C /opt/
echo 'export SCALA_HOME=/opt/scala-2.12.15' >> ~/.bashrc
echo 'export PATH=$PATH:$SCALA_HOME/bin' >> ~/.bashrc
source ~/.bashrc

# 验证
scala -version
# 输出: Scala code runner version 2.12.15

如何确定正确的 Scala 版本?

# 启动 spark-shell,看第一行日志
spark-shell
# 日志第一行会显示:
# Using Scala version 2.12.15 (OpenJDK 64-Bit Server VM, Java 1.8.0_312)

四、Spark 安装配置

4.1 下载与解压

# 下载 Spark 3.3.0(预编译 Hadoop 3.x 版本)
wget https://archive.apache.org/dist/spark/spark-3.3.0/spark-3.3.0-bin-hadoop3.tgz

# 解压到 /opt
tar -xzf spark-3.3.0-bin-hadoop3.tgz -C /opt/

# 创建软链接(方便版本切换)
sudo ln -s /opt/spark-3.3.0-bin-hadoop3 /opt/spark

# 环境变量
echo 'export SPARK_HOME=/opt/spark' >> ~/.bashrc
echo 'export PATH=$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbin' >> ~/.bashrc
echo 'export PYSPARK_PYTHON=python3' >> ~/.bashrc
source ~/.bashrc

# 验证
spark-shell --version

4.2 spark-defaults.conf(全局默认配置)

# $SPARK_HOME/conf/spark-defaults.conf
# ===== 序列化 =====
spark.serializer org.apache.spark.serializer.KryoSerializer
# 注册自定义类(避免 Kryo "Class not registered" 错误)
spark.kryo.classesToRegister com.example.MyClass

# ===== Shuffle =====
# Shuffle 分区数(默认 200,大集群可调大)
spark.sql.shuffle.partitions 200

# ===== 动态资源分配(YARN/K8s) =====
spark.dynamicAllocation.enabled true
spark.dynamicAllocation.minExecutors 2
spark.dynamicAllocation.maxExecutors 50

# ===== History Server =====
spark.eventLog.enabled true
spark.eventLog.dir hdfs://namenode:8020/spark-history
spark.history.fs.logDirectory hdfs://namenode:8020/spark-history

# ===== 压缩 =====
spark.sql.adaptive.enabled true
spark.sql.adaptive.coalescePartitions.enabled true
spark.io.compression.codec snappy

4.3 spark-env.sh(环境级配置)

# $SPARK_HOME/conf/spark-env.sh
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
export HADOOP_CONF_DIR=/opt/hadoop/etc/hadoop
export SPARK_WORKER_MEMORY=8g
export SPARK_WORKER_CORES=4
export SPARK_MASTER_HOST=master-node

五、Maven 安装与完整 pom.xml

5.1 安装 Maven

# Linux
sudo apt install maven -y

# macOS
brew install maven

# 验证
mvn -version
# Apache Maven 3.8.6

5.2 完整 pom.xml(可直接复制使用)

以下是一个 生产级 Spark 项目 pom.xml,涵盖 Spark Core + Spark SQL、Kryo 序列化、maven-shade-plugin 打包等配置:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>spark-project</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<!-- ===== 统一版本管理(关键!避免传递依赖冲突) ===== -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<scala.version>2.12.15</scala.version>
<scala.binary.version>2.12</scala.binary.version>
<spark.version>3.3.0</spark.version>
<jackson.version>2.13.4</jackson.version>
<!-- maven 插件版本 -->
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<!-- ===== 依赖管理 ===== -->
<dependencies>
<!-- Spark Core — scope=provided(集群已有,不打入 fat jar) -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_${scala.binary.version}</artifactId>
<version>${spark.version}</version>
<scope>provided</scope>
</dependency>

<!-- Spark SQL — scope=provided -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_${scala.binary.version}</artifactId>
<version>${spark.version}</version>
<scope>provided</scope>
</dependency>

<!-- Scala 标准库 — scope=provided(集群已有) -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
<scope>provided</scope>
</dependency>

<!-- ===== 第三方依赖 — scope=compile(集群没有,需打入 fat jar) ===== -->
<!-- MySQL JDBC -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>

<!-- JSON 处理 — 锁定版本避免与 Spark 内置版本冲突 -->
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-scala_${scala.binary.version}</artifactId>
<version>${jackson.version}</version>
</dependency>

<!-- 单元测试 -->
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.binary.version}</artifactId>
<version>3.2.12</version>
<scope>test</scope>
</dependency>
</dependencies>

<!-- ===== 构建插件 ===== -->
<build>
<plugins>
<!-- 1. Scala 编译插件(必须先于 Java 编译) -->
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>4.6.3</version>
<executions>
<execution>
<id>scala-compile-first</id>
<phase>process-resources</phase>
<goals>
<goal>add-source</goal>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scala-test-compile</id>
<phase>process-test-resources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<configuration>
<scalaVersion>${scala.version}</scalaVersion>
<args>
<arg>-target:jvm-1.8</arg>
</args>
</configuration>
</plugin>

<!-- 2. Maven 编译插件(Java 编译) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>

<!-- 3. maven-shade-plugin — 打包 Fat JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<!-- 排除签名文件(否则报 SecurityException) -->
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<!-- 合并 SPI services(Kryo/FST 等序列化框架依赖) -->
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/services/org.apache.spark.sql.sources.DataSourceRegister</resource>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.WordCount</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

5.3 pom.xml 配置逐段说明

5.4 构建与打包

配置区关键点常见错误
``统一管理版本号,避免传递依赖冲突版本号分散在 `` 中,改一处漏十处
`spark-*` 依赖**scope=provided**(集群已有)写成 compile → 打包后与集群冲突 → ClassNotFoundException
第三方依赖scope 默认 compile,打入 fat jar把 provided scope 的依赖也打进去 → 签名冲突
`scala-maven-plugin`**必须先于 java 编译**(process-resources 阶段)用 maven-compiler-plugin 编译 scala → 编译失败
`shade-plugin` filters排除 `META-INF/*.SF/*.DSA/*.RSA`不排除 → `SecurityException: Invalid signature file`
`shade-plugin` transformers`AppendingTransformer` 合并 services 文件不配置 → Kryo/FST 序列化 SPI 不工作
# 清理 + 编译 + 打包
mvn clean package

# 只编译不测试(加速)
mvn clean package -DskipTests

# 查看依赖树(排查冲突)
mvn dependency:tree

# 查看合并后的有效 POM
mvn help:effective-pom

六、IDEA 项目搭建与本地调试

6.1 创建项目

IntelliJ IDEA → File → New → Project from Existing Sources
→ 选择包含 pom.xml 的目录 → Import as Maven Project
→ 等待依赖下载完成

6.2 配置 Run Configuration(断点调试 Spark)

Run → Edit Configurations → + → Application

Name: WordCount (local)
Main class: com.example.WordCount
VM options: -Dspark.master=local[4] -Xms512m -Xmx1024m
Program args: data/input.txt data/output/
Environment: HADOOP_HOME=/opt/hadoop
`-Dspark.master=local[4]` 表示本地模式,4 个线程模拟 Spark 集群。此时 Driver 和 Executor 在同一 JVM 中,可以直接在 `flatMap` / `reduceByKey` 内部打断点!

6.3 Windows 额外配置

// 在 main() 方法开头添加(仅 Windows 需要)
System.setProperty("hadoop.home.dir", "C:\\hadoop");

并下载 [winutils.exe](https://github.com/steveloughran/winutils) 放到 C:\hadoop\bin\


七、完整代码示例:WordCount + 生产级 ETL 模板

7.1 WordCount(Scala 版本,逐行注释)

package com.example

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

/**
* Spark Core WordCount 示例
*
* 提交命令:
* spark-submit --master yarn --deploy-mode cluster \
* --executor-memory 2G --executor-cores 2 --num-executors 5 \
* --class com.example.WordCount spark-project-1.0.jar \
* hdfs:///input/words.txt hdfs:///output/wc
*/
object WordCount {

def main(args: Array[String]): Unit = {
// 1. 参数校验
if (args.length < 2) {
System.err.println("Usage: WordCount <inputPath> <outputPath>")
System.exit(1)
}
val Array(inputPath, outputPath) = args

// 2. 创建 SparkConf(生产环境所有配置通过 spark-submit --conf 传入)
val conf = new SparkConf()
.setAppName("WordCount") // 作业名(显示在 Web UI)
// .setMaster("local[4]") // 本地调试时取消注释
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.registerKryoClasses(Array(classOf[WordCount])) // 注册自定义类

// 3. 创建 SparkContext(一个 JVM 只能有一个)
val sc = new SparkContext(conf)

try {
// 4. 读取输入文件 → RDD[String]
// textFile 是 Transformation,惰性求值,此处不触发计算
val lines: RDD[String] = sc.textFile(inputPath)

// 5. 核心计算逻辑 — Transformation 链
val wordCounts: RDD[(String, Int)] = lines
.flatMap(line => line.split("\\s+")) // 分词 → RDD[String]
.filter(word => word.nonEmpty) // 过滤空字符串
.map(word => (word.toLowerCase, 1)) // 转小写并计数 → RDD[(String, Int)]
.reduceByKey(_ + _) // 按 key 聚合(Map 端预聚合)→ RDD[(String, Int)]

// 6. Action — 触发计算,将结果写入文件
wordCounts.saveAsTextFile(outputPath)

// 7. 打印 Top 10 到 Driver 日志
val top10: Array[(String, Int)] = wordCounts
.sortBy(_._2, ascending = false) // 按计数降序
.take(10) // Action — 取前 10 到 Driver

println("=" * 50)
println("Top 10 Words:")
top10.foreach { case (word, count) =>
println(f" $word%-20s $count%5d")
}
println("=" * 50)

} finally {
// 8. 无论成功或异常,必须关闭 SparkContext 释放资源
sc.stop()
}
}
}

7.2 代码要点逐行解析

7.3 生产级 ETL 模板(Spark SQL + DataFrame)

行/段说明易错点
`SparkConf()`配置对象,链式调用`.setAppName()` 必调,否则 UI 显示混乱
`.registerKryoClasses()`注册自定义类到 Kryo不注册 → `Class is not registered` 异常
`new SparkContext(conf)`每个 JVM 只能创建一个重复创建 → `Only one SparkContext may be running`
`sc.textFile()`**Transformation**,惰性求值此时不读取数据,只构建 DAG
`flatMap → filter → map`三个连续的窄依赖 TransformationPipeline 在同一个 Stage 内执行,无 Shuffle
`reduceByKey(_ + _)`**宽依赖 Transformation**,触发 ShuffleMap 端先本地聚合(Combine),减少网络 IO
`saveAsTextFile()`**Action** — 触发整个 DAG 执行输出目录不能已存在,否则报错
`sortBy + take(10)`Action — 将结果拉到 Driver数据量大时慎用 `collect()`,用 `take(N)`
`sc.stop()`放在 `finally` 块不调用 → 资源泄露 → Web UI 端口不释放
package com.example

import org.apache.spark.sql.{SparkSession, DataFrame}
import org.apache.spark.sql.functions._

/**
* Spark SQL ETL 模板
* 场景:从 MySQL 读取数据 → 清洗转换 → 写入 HDFS Parquet
*/
object ETLJob {

def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.appName("Daily ETL Job")
.config("spark.sql.shuffle.partitions", "200")
.enableHiveSupport() // 启用 Hive 支持(可选)
.getOrCreate()

try {
// 1. 从 MySQL 读取源数据
val sourceDF: DataFrame = spark.read
.format("jdbc")
.option("url", "jdbc:mysql://mysql-host:3306/mydb")
.option("dbtable", "orders")
.option("user", "etl_user")
.option("password", sys.env("MYSQL_PASSWORD")) // 密码从环境变量取
.load()

// 2. 数据清洗与转换
val cleanedDF = sourceDF
.filter(col("amount") > 0) // 过滤无效金额
.filter(col("status").isin("PAID", "SHIPPED")) // 只保留有效状态订单
.dropDuplicates("order_id") // 按 order_id 去重
.withColumn("year_month", date_format(col("created_at"), "yyyy-MM"))
.withColumn("amount_taxed", col("amount") * 0.9) // 计算税后金额

// 3. 按月份汇总
val summaryDF = cleanedDF
.groupBy("year_month")
.agg(
count("order_id").as("order_count"),
sum("amount_taxed").as("total_amount"),
avg("amount_taxed").as("avg_amount")
)
.orderBy("year_month")

// 4. 写入 HDFS(Parquet 格式,Snappy 压缩)
summaryDF.write
.mode("overwrite")
.format("parquet")
.option("compression", "snappy")
.save("hdfs:///data/warehouse/order_summary/")

} finally {
spark.stop()
}
}
}

八、部署模式全景对比

![Spark 部署模式对比](./diagrams/de-03-deploy.png)

spark-submit 生产命令模板

模式--master适用场景Driver 位置资源管理
**Local**`local[4]`开发、调试、单元测试本地 JVM
**Standalone**`spark://host:7077`小团队、不依赖 Hadoop提交节点(client) 或 Worker(cluster)Spark 自带
**YARN**`yarn`**生产 Hadoop 集群首选**提交节点(client) 或 NodeManager(cluster)YARN RM
**Kubernetes**`k8s://https://api:6443`云原生、容器化PodK8s Scheduler
spark-submit \
--master yarn \
--deploy-mode cluster \
--name "Daily-ETL-Job" \
--class com.example.ETLJob \
--executor-memory 4G \
--executor-cores 2 \
--num-executors 10 \
--driver-memory 2G \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
--conf spark.sql.shuffle.partitions=200 \
--conf spark.dynamicAllocation.enabled=true \
--conf spark.eventLog.enabled=true \
--jars mysql-connector-java-8.0.30.jar \
--files log4j.properties \
spark-project-1.0-SNAPSHOT.jar \
arg1 arg2
⚠️ `--deploy-mode cluster`:Driver 运行在 YARN 集群内,避免提交节点宕机导致整个作业失败。生产环境必须用 cluster 模式

九、10 个高频错误排查

排查工具速查

#错误信息根因解决方案
1`NoClassDefFoundError: SparkConf`pom.xml 中 scope 写成 compile改为 **provided**
2`Task not serializable`闭包引用未实现 Serializable 的对象方案A: 实现 Serializable;方案B: 使用 `mapPartitions`
3`Failed to locate winutils`Windows 缺少 hadoop 二进制下载 winutils.exe 到 `%HADOOP_HOME%\bin`
4`scala compilation error`mixed compile 顺序错误scala-maven-plugin 设在 process-resources 阶段
5`SecurityException: invalid signature`jar 包签名文件未排除shade-plugin 排除 `META-INF/*.SF,*.DSA,*.RSA`
6`NoSuchMethodError`多版本依赖冲突`mvn dependency:tree` + shade relocation
7`Container exited with exit code 137`Executor OOM 被 YARN kill加大 `--executor-memory` 或 `spark.executor.memoryOverhead`
8`Class is not registered: com.example.MyClass`Kryo 未注册自定义类`spark.kryo.classesToRegister = com.example.MyClass`
9`Driver 退出无日志`cluster 模式日志在 YARN Logs用 `yarn logs -applicationId ` 查看
10`cache() 后数据没有缓存`cache 是 lazy 的必须调 Action(如 `count()`)才会触发缓存
# 1. 查看依赖树(找冲突)
mvn dependency:tree | grep -E "jackson|guava|netty"

# 2. 查看完整合并 POM
mvn help:effective-pom > effective-pom.xml

# 3. YARN 日志查看
yarn logs -applicationId application_xxx

# 4. 进制 jar 包内容
jar tf spark-project-1.0.jar | head -20

# 5. 检查是否有 provided 依赖被打入
jar tf spark-project-1.0.jar | grep spark-core

十、最佳实践清单


#实践理由
✅ 1`` 统一版本号避免传递依赖冲突,一处改全改
✅ 2Spark/Hadoop/Scala scope=provided集群已有,打包冲突
✅ 3生产启用 Kryo 序列化比 Java 序列化快 10 倍
✅ 4配置通过 `--conf` 传入不同环境不同配置,不硬编码
✅ 5日志用 `--files log4j.properties`不同环境不同级别,独立于代码
✅ 6先在 `local[4]` 测试节省集群排错时间
✅ 7shade 排除签名 + AppendingTransformer必备配置,否则 SPI 不工作

十一、学习路线图

![Spark 学习路线图](./diagrams/de-04-roadmap.png)

阶段时长核心内容输出目标
**Phase 1 基础入门**2-3 周JDK/Scala/Spark 安装 → IDEA 项目搭建 → RDD 算子 → WordCount能独立写第一个 Spark 程序
**Phase 2 核心原理**4-6 周RDD 血统 + DAG → Shuffle 机制 → 内存管理 → spark-submit → Web UI 调优能调优生产作业
**Phase 3 生态拓展**6-8 周Spark SQL + DataFrame → Catalyst → Structured Streaming → MLlib → PySpark成为全栈 Spark 工程师
**Phase 4 架构调优**持续DAGScheduler/TaskScheduler 源码 → 内核调优 → 平台化建设Spark 专家/架构师
Phase 2 和 Phase 3 可以并行——学 Shuffle 的同时就可以写 Spark SQL,理论 + 实战交替是最快路径

写在最后

Spark 开发环境配置是 第一道门槛,也是最容易被跳过的一步。很多人照着网上的 pom.xml 复制粘贴,出了问题不知道从哪里排查。

本文从 JDK → Scala → Spark → Maven → IDEA 全链路拆解,配合完整 pom.xml 模板(可直接复制)、WordCount 源码逐行注释、10 个高频错误排查、4 张架构图、四阶段学习路线。希望能帮你一次性搞定 Spark 开发环境。

环境稳了,代码才能飞。


‍ starzy · AI Data Engineer · blog.starzy.cn · GitHub: starzy1990.github.io

posted @ 2026-07-31 10:17  starzy  阅读(0)  评论(0)    收藏  举报