1. 先搞清楚 Spark 存档到底要解决什么问题
Spark 存档,或者说 Spark 项目归档,核心要解决的是“一次开发,到处运行”的依赖打包问题。很多刚接触 Spark 的朋友,在本地 IDE(比如 IntelliJ IDEA)里写代码跑得好好的,一提交到集群(无论是 YARN、Standalone 还是 Kubernetes)就报ClassNotFoundException或者NoSuchMethodError。这十有八九就是依赖没带对、没带全。
所以,这个“教学”不是教你写 Spark SQL 或者 RDD 算子,而是教你如何把写好的 Spark 应用,连同它所有的“家当”(第三方库、配置文件),打包成一个结实、可移植的“包裹”(通常是 JAR 包),确保它在任何符合版本的 Spark 环境下都能稳定执行。这步做不好,后面的集群部署、任务调度都是空谈。
我一般会建议,无论你是做数据分析、图计算(比如 Spark 图谱),还是机器学习,在动手写业务逻辑之前,先把项目结构和打包方式定下来。这能避免后期 80% 因环境不一致导致的诡异报错,比如那个经典的object spark is not a member of package org.apache,很多时候就是构建工具(sbt 或 Maven)的配置没写对,导致核心 Spark 库都没引入成功。
2. 环境准备与项目骨架搭建
在开始打包之前,得先把“厨房”收拾好。这里的环境包括两部分:一是你本地开发调试的环境,二是你目标运行集群的环境。目标环境通常由运维团队提供,但你需要明确知道它的 Spark 版本、Scala 版本和 Hadoop 版本。
2.1 本地开发环境清单
- Java:Spark 3.x 通常需要 Java 8 或 11。用
java -version确认。 - Scala(可选但推荐):如果你用 Scala 开发,建议安装与 Spark 发行版匹配的 Scala 版本(如 Spark 3.3+ 常用 Scala 2.12)。用
scala -version检查。 - 构建工具:二选一即可,我个人更推荐 Maven,因为生态更通用,遇到问题网上资料多。
- Maven:安装并配置
MAVEN_HOME。用mvn -v确认。 - sbt:sbt 在 Scala 项目中更常见,但下载依赖可能较慢。
- Maven:安装并配置
- IDE:IntelliJ IDEA(安装 Scala 插件)或 VS Code 等。IDEA 对 Maven/sbt 项目支持最好。
2.2 创建 Maven 项目骨架
这是最稳妥的起点。你可以用 IDE 新建 Maven 项目,或者用命令行:
mvn archetype:generate -DgroupId=com.yourcompany -DartifactId=spark-demo -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false然后,最关键的一步是修改pom.xml。这个文件定义了项目的所有依赖和打包方式。下面是一个针对 Spark 3.4+ 的pom.xml核心部分示例:
<project ...> <modelVersion>4.0.0</modelVersion> <groupId>com.yourcompany</groupId> <artifactId>spark-demo</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <spark.version>3.4.0</spark.version> <scala.version>2.12.18</scala.version> <!-- 与 Spark 发行版 Scala 版本一致 --> </properties> <dependencies> <!-- Spark Core 依赖,scope 为 provided --> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.12</artifactId> <version>${spark.version}</version> <scope>provided</scope> </dependency> <!-- Spark SQL 依赖(如果需要) --> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-sql_2.12</artifactId> <version>${spark.version}</version> <scope>provided</scope> </dependency> <!-- 其他第三方依赖,如连接 MySQL --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <!-- 注意:这类依赖 scope 通常是 compile,要打进包 --> </dependency> </dependencies> <build> <plugins> <!-- 指定 Scala 版本和编译插件(如果用 Scala 写代码) --> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <version>4.8.1</version> <executions> <execution> <goals> <goal>compile</goal> <goal>testCompile</goal> </goals> </execution> </executions> <configuration> <scalaVersion>${scala.version}</scalaVersion> </configuration> </plugin> <!-- Maven 编译插件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> </project>关键点解释:
spark-core和spark-sql的<scope>provided</scope>:这表示 Spark 核心库在集群运行时已经提供,打包时不需要打进 JAR 里,可以显著减小 JAR 包体积。这是新手最容易配错的地方之一,如果设成compile,打出来的包会巨大无比。- 第三方依赖(如
mysql-connector-java):这类库集群环境没有,所以必须打进最终的 JAR 包,因此 scope 用默认的compile。 - Scala 版本 (
2.12):必须与 Spark 发行版后缀 (_2.12) 匹配。下错版本就会导致object spark is not a member这类错误。
2.3 创建 sbt 项目骨架(备选)
如果你更习惯 sbt,项目根目录下的build.sbt文件是关键:
name := "spark-demo-sbt" version := "1.0" scalaVersion := "2.12.18" // 匹配 Spark 版本 val sparkVersion = "3.4.0" libraryDependencies ++= Seq( "org.apache.spark" %% "spark-core" % sparkVersion % "provided", "org.apache.spark" %% "spark-sql" % sparkVersion % "provided", "mysql" % "mysql-connector-java" % "8.0.33" )sbt 中的% “provided”作用同 Maven 的<scope>provided</scope>。
3. 编写代码与本地测试
项目骨架搭好,依赖配好,才能开始安心写代码。这里以一个简单的 WordCount 为例,展示标准流程。
3.1 编写一个简单的 Spark 应用
在src/main/scala(或src/main/java)下创建你的主类:
package com.yourcompany.sparkdemo import org.apache.spark.sql.SparkSession object SimpleWordCount { def main(args: Array[String]): Unit = { // 1. 创建 SparkSession,这是 Spark 2.x 后的统一入口 val spark = SparkSession.builder() .appName("Simple WordCount") .master("local[*]") // 本地测试用 local,[*] 表示使用所有可用核心 .getOrCreate() // 2. 设置日志级别,减少控制台噪音 spark.sparkContext.setLogLevel("WARN") // 3. 创建测试数据 val data = Seq("Hello Spark", "Hello World", "Spark is cool") import spark.implicits._ val df = data.toDF("line") // 4. 执行 WordCount val wordsDF = df.selectExpr("explode(split(line, ' ')) as word") val wordCounts = wordsDF.groupBy("word").count() // 5. 输出结果 wordCounts.show() // 6. 停止 SparkSession spark.stop() } }3.2 在 IDE 中本地运行测试
在 IntelliJ IDEA 里,直接右键点击SimpleWordCount对象,选择Run ‘SimpleWordCount’。如果一切配置正确,你应该能在控制台看到输出:
+-----+-----+ | word|count| +-----+-----+ |Hello| 2| |World| 1| |Spark| 2| | is| 1| | cool| 1| +-----+-----+本地测试成功的意义:这证明了你的代码逻辑、项目依赖和基础环境(Java, Scala)是没问题的。这是存档前必须通过的“冒烟测试”。
注意:本地
master(“local[*]”)模式只是为了方便调试。提交到集群时,需要去掉这行,或者通过命令行参数指定--master。
4. 核心环节:打包与存档
本地跑通只是第一步,打包才是存档教学的核心。目标是将你的应用代码和所有非provided的依赖,打包成一个“uber-jar”或“fat-jar”。
4.1 使用 Maven Shade Plugin 打包(推荐)
这是最常用的方式,它会把依赖的类文件“重命名”后合并到一个 JAR 包中,避免依赖冲突。在pom.xml的<build><plugins>部分添加:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.0</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <filters> <filter> <artifact>*:*</artifact> <excludes> <!-- 排除签名文件,避免冲突 --> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> <!-- 可选:指定主类,这样提交时不用再指定 --class --> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.yourcompany.sparkdemo.SimpleWordCount</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin>打包命令:在项目根目录下执行:
mvn clean package -DskipTests成功后,在target/目录下,你会找到两个 JAR:
spark-demo-1.0-SNAPSHOT.jar:原始的、不包含依赖的 JAR。spark-demo-1.0-SNAPSHOT-shaded.jar(或类似名称):这个才是我们需要的 fat-jar,它包含了你的代码和所有compile范围的依赖。
4.2 使用 sbt assembly 打包(sbt项目)
对于 sbt 项目,常用sbt-assembly插件。
- 在
project/plugins.sbt中添加:addSbtPlugin(“com.eed3si9n” % “sbt-assembly” % “2.1.1”) - 在
build.sbt中可添加合并策略(避免冲突):assemblyMergeStrategy in assembly := { case PathList("META-INF", xs @ _*) => MergeStrategy.discard case x => MergeStrategy.first } - 打包命令:
sbt assembly - 产出在
target/scala-2.12/目录下,名为spark-demo-sbt-assembly-1.0.jar。
4.3 验证打包结果
不要想当然认为打包成功就万事大吉。验证分两步:
检查 JAR 包内容:
jar tf target/spark-demo-1.0-SNAPSHOT-shaded.jar | grep -E “(mysql|yourcompany)” | head -20这个命令能列出 JAR 包中包含
mysql(你的第三方依赖)和yourcompany(你的代码)的文件,确认它们都被打包进去了。本地使用 spark-submit 测试 Fat-Jar: 这是最接近生产环境的测试。确保你本地安装了对应版本的 Spark(可以从官网下载预编译版)。
# 假设 spark-submit 在 PATH 中,否则用完整路径 spark-submit \ --master local[*] \ --class com.yourcompany.sparkdemo.SimpleWordCount \ target/spark-demo-1.0-SNAPSHOT-shaded.jar如果能成功运行并输出 WordCount 结果,说明你的存档是真正可用的。如果报错
ClassNotFoundException,大概率是某些关键依赖没打进包(scope 设成了provided)或者合并冲突。
5. 提交到集群与生产级考量
本地验证通过后,就可以提交到真正的 Spark 集群了(如 YARN、Kubernetes 或 Standalone)。这里以 YARN 集群为例。
5.1 基本提交命令
spark-submit \ --master yarn \ --deploy-mode cluster \ # 或 client,取决于你的集群配置和调试需求 --class com.yourcompany.sparkdemo.SimpleWordCount \ --num-executors 4 \ --executor-cores 2 \ --executor-memory 4G \ hdfs://namenode:8020/path/to/your/spark-demo-1.0-SNAPSHOT-shaded.jar \ # 这里可以传递应用参数,对应 main 方法中的 args参数解释:
--master yarn:指定集群管理器。--deploy-mode cluster:Driver 程序在 YARN 的某个容器中运行,适合生产。client模式则 Driver 运行在提交任务的机器上,方便看日志,但提交机器挂了任务就失败。--num-executors、--executor-cores、--executor-memory:根据你的数据量和集群资源调整。不要一上来就申请最大资源,先从小规模测试。- JAR 包路径:通常需要先上传到 HDFS 或集群所有节点都能访问的共享存储。
5.2 生产级存档的进阶要点
依赖管理精细化:
- 避免依赖冲突:使用
mvn dependency:tree查看依赖树,排除传递性冲突。在pom.xml中可以使用<exclusions>。
<dependency> <groupId>some.group</groupId> <artifactId>some-artifact</artifactId> <version>X.Y.Z</version> <exclusions> <exclusion> <groupId>conflict.group</groupId> <artifactId>conflict-artifact</artifactId> </exclusion> </exclusions> </dependency>- 使用
provided范围:确保 Hadoop、Spark 本身的依赖不被打包。集群环境已经提供了这些库的不同版本,混入你的包中极易引发冲突。
- 避免依赖冲突:使用
资源文件与配置:
- 如果你的应用需要读取配置文件(如
application.conf、log4j.properties),需要决定是打包进 JAR,还是放在集群的固定路径。 - 打包进 JAR:使用
getClass.getResourceAsStream(“/config.conf”)读取。 - 放在外部:通过
--files参数提交,在代码中用SparkFiles.get(“filename”)获取路径。
spark-submit ... --files hdfs:///path/to/config.conf- 如果你的应用需要读取配置文件(如
日志与调试:
- 在
cluster模式下,Driver 和 Executor 的日志需要通过 YARN 命令查看:yarn logs -applicationId <appId>。 - 在打包前,建议在本地将日志级别调到
INFO或DEBUG跑一遍,确保没有隐藏的警告或异常。
- 在
处理敏感信息:
- 绝对不要将数据库密码、API Key 等硬编码在代码或打包进 JAR 的配置文件中。
- 使用 Spark 的
--conf参数传递,或从环境变量、集群安全的配置服务中读取。
6. 常见问题排查清单
当你的存档提交失败时,按这个顺序排查,能节省大量时间:
ClassNotFoundException/NoSuchClassDefFoundError:- 第一步:确认缺失的类是否属于 Spark、Hadoop 自身。如果是,检查
pom.xml中对应依赖的scope是否为provided。在集群上,这些类应由集群环境提供。 - 第二步:如果是第三方库(如 MySQL 驱动、JSON 解析库),检查其依赖的
scope是否为compile(默认),并且是否被打包进了 fat-jar(用jar tf命令验证)。 - 第三步:检查是否有依赖冲突,导致正确的类被覆盖。使用
mvn dependency:tree -Dverbose分析。
- 第一步:确认缺失的类是否属于 Spark、Hadoop 自身。如果是,检查
object spark is not a member of package org.apache:- 这是编译错误,不是运行时错误。100% 是构建配置问题。
- 检查
pom.xml中spark-core的 artifactId 后缀(如_2.12)是否与scala.version属性匹配。 - 检查 IDE 是否正确地导入了 Maven 或 sbt 项目(需要点击“重新导入所有 Maven 项目”)。
- 在命令行执行
mvn clean compile,看是否能编译通过。
任务卡住,不执行也不报错:
- 检查资源申请是否合理(内存、核心数),是否超过队列或集群限制。
- 检查
--master地址是否正确,网络是否通畅。 - 查看 YARN ResourceManager 的 Web UI,确认应用是否被接受,资源是否分配。
- 检查 Executor 日志,看是否在初始化阶段卡住(如连接外部数据库失败)。
本地运行成功,集群提交失败:
- 环境差异:这是最常见原因。集群的 Java 版本、Spark 版本、Hadoop 版本是否与你本地一致?尤其是 Hadoop 版本,可能影响 HDFS 和 YARN 的兼容性。
- 数据路径:本地代码中使用的文件路径(如
file:///home/data)在集群中不存在。应使用 HDFS 路径(hdfs://...)或确保文件已分发。 - 权限问题:提交作业的用户是否有权限读写 HDFS 路径、执行 YARN 队列?
JAR 包太大,上传缓慢:
- 严格使用
providedscope 排除 Spark/Hadoop 依赖。 - 使用
maven-shade-plugin或sbt-assembly的<filters>或合并策略,排除不必要的文件(如文档、源码)。 - 考虑将不变的、公共的第三方依赖提前放到集群每个节点的固定路径,并通过
--jars参数引用,而不是全部打进一个包。
- 严格使用
7. 从存档到持续集成与部署
对于正式项目,存档不应该是一个手动过程。应该集成到 CI/CD 流水线中。
- 版本化:每次打包的 JAR 名称应包含版本号或 Git Commit ID,便于追溯。例如
spark-demo-${git.commit.id.abbrev}.jar。 - 自动化测试:在 CI 中,除了单元测试,可以加入一个使用
spark-submit在本地local模式下运行 fat-jar 的集成测试,作为存档是否有效的最终关卡。 - 自动上传:打包成功的 JAR,自动上传到公司的 Maven 私库或 HDFS 上的固定发布目录。
- 配置管理:将 Spark 提交参数(如 executor 内存、核心数)提取到配置文件(如
application.yaml)中,与代码分离。通过 CI 流程为不同环境(测试、生产)注入不同的配置。
最后的核心建议:Spark 存档的成功,90% 依赖于清晰、正确的项目依赖管理和构建配置。不要急于写复杂的业务逻辑(比如spark数据分析案例或spark 图谱),先用一个像 WordCount 这样的简单例子,把从编码、打包、本地测试、集群提交的完整链路彻底跑通。这个基础打牢了,后续引入再复杂的库(比如处理dgx spark这样的 GPU 加速场景,或集成muse spark 1.2这类特定工具库)都会顺畅得多。把每次存档都当作一次可重复、自动化的发布流程来对待,是走向生产稳定的关键一步。