Spark项目打包与依赖管理实战:从本地开发到集群部署

📅 2026/8/14 1:19:33
Spark项目打包与依赖管理实战:从本地开发到集群部署
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 本地开发环境清单JavaSpark 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确认。sbtsbt 在 Scala 项目中更常见但下载依赖可能较慢。IDEIntelliJ IDEA安装 Scala 插件或 VS Code 等。IDEA 对 Maven/sbt 项目支持最好。2.2 创建 Maven 项目骨架这是最稳妥的起点。你可以用 IDE 新建 Maven 项目或者用命令行mvn archetype:generate -DgroupIdcom.yourcompany -DartifactIdspark-demo -DarchetypeArtifactIdmaven-archetype-quickstart -DinteractiveModefalse然后最关键的一步是修改pom.xml。这个文件定义了项目的所有依赖和打包方式。下面是一个针对 Spark 3.4 的pom.xml核心部分示例project ... modelVersion4.0.0/modelVersion groupIdcom.yourcompany/groupId artifactIdspark-demo/artifactId version1.0-SNAPSHOT/version properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target spark.version3.4.0/spark.version scala.version2.12.18/scala.version !-- 与 Spark 发行版 Scala 版本一致 -- /properties dependencies !-- Spark Core 依赖scope 为 provided -- dependency groupIdorg.apache.spark/groupId artifactIdspark-core_2.12/artifactId version${spark.version}/version scopeprovided/scope /dependency !-- Spark SQL 依赖如果需要 -- dependency groupIdorg.apache.spark/groupId artifactIdspark-sql_2.12/artifactId version${spark.version}/version scopeprovided/scope /dependency !-- 其他第三方依赖如连接 MySQL -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version !-- 注意这类依赖 scope 通常是 compile要打进包 -- /dependency /dependencies build plugins !-- 指定 Scala 版本和编译插件如果用 Scala 写代码 -- plugin groupIdnet.alchim31.maven/groupId artifactIdscala-maven-plugin/artifactId version4.8.1/version executions execution goals goalcompile/goal goaltestCompile/goal /goals /execution /executions configuration scalaVersion${scala.version}/scalaVersion /configuration /plugin !-- Maven 编译插件 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration source8/source target8/target /configuration /plugin /plugins /build /project关键点解释spark-core和spark-sql的scopeprovided/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 的scopeprovided/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的buildplugins部分添加plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.0/version executions execution phasepackage/phase goals goalshade/goal /goals configuration filters filter artifact*:*/artifact excludes !-- 排除签名文件避免冲突 -- excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude excludeMETA-INF/*.RSA/exclude /excludes /filter /filters !-- 可选指定主类这样提交时不用再指定 --class -- transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.yourcompany.sparkdemo.SimpleWordCount/mainClass /transformer /transformers /configuration /execution /executions /plugin打包命令在项目根目录下执行mvn clean package -DskipTests成功后在target/目录下你会找到两个 JARspark-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 clusterDriver 程序在 YARN 的某个容器中运行适合生产。client模式则 Driver 运行在提交任务的机器上方便看日志但提交机器挂了任务就失败。--num-executors、--executor-cores、--executor-memory根据你的数据量和集群资源调整。不要一上来就申请最大资源先从小规模测试。JAR 包路径通常需要先上传到 HDFS 或集群所有节点都能访问的共享存储。5.2 生产级存档的进阶要点依赖管理精细化避免依赖冲突使用mvn dependency:tree查看依赖树排除传递性冲突。在pom.xml中可以使用exclusions。dependency groupIdsome.group/groupId artifactIdsome-artifact/artifactId versionX.Y.Z/version exclusions exclusion groupIdconflict.group/groupId artifactIdconflict-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分析。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这类特定工具库都会顺畅得多。把每次存档都当作一次可重复、自动化的发布流程来对待是走向生产稳定的关键一步。