Spark项目打包与依赖管理实战:从本地开发到集群部署
2026/8/14 1:19:27 网站建设 项目流程

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 本地开发环境清单

  1. Java:Spark 3.x 通常需要 Java 8 或 11。用java -version确认。
  2. Scala(可选但推荐):如果你用 Scala 开发,建议安装与 Spark 发行版匹配的 Scala 版本(如 Spark 3.3+ 常用 Scala 2.12)。用scala -version检查。
  3. 构建工具:二选一即可,我个人更推荐 Maven,因为生态更通用,遇到问题网上资料多。
    • Maven:安装并配置MAVEN_HOME。用mvn -v确认。
    • sbt:sbt 在 Scala 项目中更常见,但下载依赖可能较慢。
  4. 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-corespark-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插件。

  1. project/plugins.sbt中添加:addSbtPlugin(“com.eed3si9n” % “sbt-assembly” % “2.1.1”)
  2. build.sbt中可添加合并策略(避免冲突):
    assemblyMergeStrategy in assembly := { case PathList("META-INF", xs @ _*) => MergeStrategy.discard case x => MergeStrategy.first }
  3. 打包命令:sbt assembly
  4. 产出在target/scala-2.12/目录下,名为spark-demo-sbt-assembly-1.0.jar

4.3 验证打包结果

不要想当然认为打包成功就万事大吉。验证分两步:

  1. 检查 JAR 包内容

    jar tf target/spark-demo-1.0-SNAPSHOT-shaded.jar | grep -E “(mysql|yourcompany)” | head -20

    这个命令能列出 JAR 包中包含mysql(你的第三方依赖)和yourcompany(你的代码)的文件,确认它们都被打包进去了。

  2. 本地使用 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 生产级存档的进阶要点

  1. 依赖管理精细化

    • 避免依赖冲突:使用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 本身的依赖不被打包。集群环境已经提供了这些库的不同版本,混入你的包中极易引发冲突。
  2. 资源文件与配置

    • 如果你的应用需要读取配置文件(如application.conflog4j.properties),需要决定是打包进 JAR,还是放在集群的固定路径。
    • 打包进 JAR:使用getClass.getResourceAsStream(“/config.conf”)读取。
    • 放在外部:通过--files参数提交,在代码中用SparkFiles.get(“filename”)获取路径。
    spark-submit ... --files hdfs:///path/to/config.conf
  3. 日志与调试

    • cluster模式下,Driver 和 Executor 的日志需要通过 YARN 命令查看:yarn logs -applicationId <appId>
    • 在打包前,建议在本地将日志级别调到INFODEBUG跑一遍,确保没有隐藏的警告或异常。
  4. 处理敏感信息

    • 绝对不要将数据库密码、API Key 等硬编码在代码或打包进 JAR 的配置文件中。
    • 使用 Spark 的--conf参数传递,或从环境变量、集群安全的配置服务中读取。

6. 常见问题排查清单

当你的存档提交失败时,按这个顺序排查,能节省大量时间:

  1. ClassNotFoundException/NoSuchClassDefFoundError

    • 第一步:确认缺失的类是否属于 Spark、Hadoop 自身。如果是,检查pom.xml中对应依赖的scope是否为provided。在集群上,这些类应由集群环境提供。
    • 第二步:如果是第三方库(如 MySQL 驱动、JSON 解析库),检查其依赖的scope是否为compile(默认),并且是否被打包进了 fat-jar(用jar tf命令验证)。
    • 第三步:检查是否有依赖冲突,导致正确的类被覆盖。使用mvn dependency:tree -Dverbose分析。
  2. object spark is not a member of package org.apache

    • 这是编译错误,不是运行时错误。100% 是构建配置问题
    • 检查pom.xmlspark-core的 artifactId 后缀(如_2.12)是否与scala.version属性匹配。
    • 检查 IDE 是否正确地导入了 Maven 或 sbt 项目(需要点击“重新导入所有 Maven 项目”)。
    • 在命令行执行mvn clean compile,看是否能编译通过。
  3. 任务卡住,不执行也不报错

    • 检查资源申请是否合理(内存、核心数),是否超过队列或集群限制。
    • 检查--master地址是否正确,网络是否通畅。
    • 查看 YARN ResourceManager 的 Web UI,确认应用是否被接受,资源是否分配。
    • 检查 Executor 日志,看是否在初始化阶段卡住(如连接外部数据库失败)。
  4. 本地运行成功,集群提交失败

    • 环境差异:这是最常见原因。集群的 Java 版本、Spark 版本、Hadoop 版本是否与你本地一致?尤其是 Hadoop 版本,可能影响 HDFS 和 YARN 的兼容性。
    • 数据路径:本地代码中使用的文件路径(如file:///home/data)在集群中不存在。应使用 HDFS 路径(hdfs://...)或确保文件已分发。
    • 权限问题:提交作业的用户是否有权限读写 HDFS 路径、执行 YARN 队列?
  5. JAR 包太大,上传缓慢

    • 严格使用providedscope 排除 Spark/Hadoop 依赖。
    • 使用maven-shade-pluginsbt-assembly<filters>或合并策略,排除不必要的文件(如文档、源码)。
    • 考虑将不变的、公共的第三方依赖提前放到集群每个节点的固定路径,并通过--jars参数引用,而不是全部打进一个包。

7. 从存档到持续集成与部署

对于正式项目,存档不应该是一个手动过程。应该集成到 CI/CD 流水线中。

  1. 版本化:每次打包的 JAR 名称应包含版本号或 Git Commit ID,便于追溯。例如spark-demo-${git.commit.id.abbrev}.jar
  2. 自动化测试:在 CI 中,除了单元测试,可以加入一个使用spark-submit在本地local模式下运行 fat-jar 的集成测试,作为存档是否有效的最终关卡。
  3. 自动上传:打包成功的 JAR,自动上传到公司的 Maven 私库或 HDFS 上的固定发布目录。
  4. 配置管理:将 Spark 提交参数(如 executor 内存、核心数)提取到配置文件(如application.yaml)中,与代码分离。通过 CI 流程为不同环境(测试、生产)注入不同的配置。

最后的核心建议:Spark 存档的成功,90% 依赖于清晰、正确的项目依赖管理和构建配置。不要急于写复杂的业务逻辑(比如spark数据分析案例spark 图谱),先用一个像 WordCount 这样的简单例子,把从编码、打包、本地测试、集群提交的完整链路彻底跑通。这个基础打牢了,后续引入再复杂的库(比如处理dgx spark这样的 GPU 加速场景,或集成muse spark 1.2这类特定工具库)都会顺畅得多。把每次存档都当作一次可重复、自动化的发布流程来对待,是走向生产稳定的关键一步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询