解决 Spark SQL 报错:NoSuchMethodError TaskMetrics.externalAccums
2025-03-30 14:07:01
搞定 SparkSQL 报错:java.lang.NoSuchMethodError: TaskMetrics.externalAccums()
刚开始学用 Java 操作 SparkSQL,跑个简单的任务就遇到了 java.lang.NoSuchMethodError: 'scala.collection.mutable.ArrayBuffer org.apache.spark.executor.TaskMetrics.externalAccums()'
这个错,是不是有点懵?别急,这个问题挺常见的,通常跟依赖库的版本没对齐有关。这篇博客就带你看看问题出在哪,以及怎么解决它。
看这报错信息:
23/10/02 10:48:10 ERROR Utils: uncaught error in thread spark-listener-group-appStatus, stopping SparkContext
java.lang.NoSuchMethodError: 'scala.collection.mutable.ArrayBuffer org.apache.spark.executor.TaskMetrics.externalAccums()'
at org.apache.spark.sql.execution.ui.SQLAppStatusListener.onTaskEnd(SQLAppStatusListener.scala:179)
at org.apache.spark.scheduler.SparkListenerBus.doPostEvent(SparkListenerBus.scala:45)
... (其余堆栈信息省略) ...
Exception in thread "spark-listener-group-appStatus" java.lang.NoSuchMethodError: 'scala.collection.mutable.ArrayBuffer org.apache.spark.executor.TaskMetrics.externalAccums()'
at org.apache.spark.sql.execution.ui.SQLAppStatusListener.onTaskEnd(SQLAppStatusListener.scala:179)
... (其余堆栈信息省略) ...
Exception in thread "main" java.lang.IllegalStateException: Cannot call methods on a stopped SparkContext.
... (其余堆栈信息省略) ...
at org.example.Main.main(Main.java:23)
报错信息很明确:NoSuchMethodError
,找不到 org.apache.spark.executor.TaskMetrics
类里的 externalAccums()
方法。而且这个错发生在 Spark 内部的监听器 SQLAppStatusListener
处理任务结束事件(onTaskEnd
)的时候。最终,这导致 SparkContext
被异常停止,后续操作也就失败了,抛出了 IllegalStateException: Cannot call methods on a stopped SparkContext
。
刨根问底:为啥会出现这个错误?
java.lang.NoSuchMethodError
这个错误,十有八九是 Java 虚拟机 (JVM) 在运行时找不到某个特定方法的签名 。这通常发生在以下情况:
- 编译时依赖版本与运行时依赖版本不一致 :你的代码编译时依赖了一个版本的库(比如
spark-core
A 版),这个库里有某个类X
和方法m()
。但实际运行时,类路径 (classpath) 上是另一个版本的库(比如spark-core
B 版),这个 B 版里的类X
可能没有方法m()
,或者m()
方法的参数、返回类型变了。当代码尝试调用m()
时,JVM 就傻眼了,抛出NoSuchMethodError
。 - 依赖冲突 :你的项目可能引入了多个库,这些库又各自依赖了同一个基础库的不同版本(例如,项目依赖 A 库和 B 库,A 依赖
common-lib
1.0,B 依赖common-lib
2.0)。构建工具(如 Maven 或 Gradle)会根据规则选择一个版本加载到运行时 classpath。如果加载的版本恰好是缺少你需要的方法的那个版本,就会出错了。
在这个具体的 TaskMetrics.externalAccums()
错误里,最可能的原因是 项目中 spark-sql
模块的版本和 spark-core
模块的版本,或者它们依赖的 Scala 库版本不匹配 。
我们来看看你的 pom.xml
:
<dependencies>
<!-- Spark Core -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>3.5.0</version>
</dependency>
<!-- Spark SQL -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.12</artifactId>
<version>3.5.0</version>
<scope>compile</scope> <!-- 注意:这里 scope 设为 compile 可能不是最佳实践,后面会提到 -->
</dependency>
<!-- Hadoop HDFS (可选,取决于是否与 HDFS 交互) -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>3.3.6</version>
</dependency>
<!-- opencsv (似乎与 Spark 问题无关) -->
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.5.1</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>compile</scope> <!-- 通常设为 provided 或 optional -->
</dependency>
<!-- !!! 显式指定 Scala 库版本,这可能是问题的关键 !!! -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.12.0</version>
</dependency>
</dependencies>
spark-core
和 spark-sql
都用了 3.5.0
版本,并且都是针对 Scala 2.12
构建的(看 _2.12
后缀)。这看起来没毛病。
但是!你还显式地引入了 scala-library
,并且指定了版本 2.12.0
。 Spark 3.5.0 虽然是为 Scala 2.12 构建的,但它内部依赖的 scala-library
的具体 补丁 版本可能不是 2.12.0
(这是一个非常早期的 2.12 版本)。Spark 的不同模块在编译时是基于某个特定的 Scala 补丁版本的。当你强制使用 2.12.0
时,很可能与 Spark 3.5.0 内部实际依赖和编译时使用的 Scala 版本(比如可能是 2.12.15
或更高)产生了二进制不兼容,导致 NoSuchMethodError
。
TaskMetrics
类是 spark-core
的一部分,而 SQLAppStatusListener
是 spark-sql
的一部分,后者在运行时调用了前者的方法。如果这两个模块依赖的基础 Scala 版本不匹配,就可能出现这种找不到方法的情况。
对症下药:解决方案来了
既然分析了原因,解决起来就目标明确了:确保所有相关的 Spark 和 Scala 依赖版本协调一致。
方案一:移除显式 Scala 依赖,让 Spark 自己搞定
原理:
Maven 或 Gradle 这样的构建工具,能够处理传递性依赖。当你引入 spark-core_2.12
和 spark-sql_2.12
时,它们会自动将正确版本的 scala-library
作为依赖拉取下来。你显式指定一个非常老的 2.12.0
版本,反而会覆盖掉 Spark 期望的版本,导致不兼容。最好的办法是,相信 Spark 的依赖管理,让它自己带入需要的 scala-library
版本。
操作步骤:
-
修改
pom.xml
:
将<dependency>
部分中关于scala-library
的那一段整个删掉。<dependencies> <!-- Spark Core --> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.12</artifactId> <version>3.5.0</version> <!-- 如果你是在 Spark 集群环境运行,或者使用 spark-submit 提交, 通常会将 Spark 相关依赖 scope 设为 provided,避免打包进最终的 fat jar。 本地运行可以不设置或用 compile。 --> <!-- <scope>provided</scope> --> </dependency> <!-- Spark SQL --> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-sql_2.12</artifactId> <version>3.5.0</version> <!-- 同上,根据运行环境调整 scope --> <!-- <scope>provided</scope> --> </dependency> <!-- Hadoop HDFS --> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-hdfs</artifactId> <version>3.3.6</version> <!-- Hadoop 依赖也可能根据环境设为 provided --> <!-- <scope>provided</scope> --> </dependency> <!-- opencsv --> <dependency> <groupId>com.opencsv</groupId> <artifactId>opencsv</artifactId> <version>5.5.1</version> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> <!-- Lombok 通常在编译后就不需要了,设为 provided 或 optional 比较合适 --> <scope>provided</scope> </dependency> <!-- 删除了显式的 scala-library 依赖 --> </dependencies>
-
清理并重新构建项目:
使用 Maven 命令清理旧的构建结果并重新构建:# 在项目根目录下执行 mvn clean package # 或者在 IDE 中执行 Maven Clean 和 Maven Install/Package
-
再次运行你的 Java 代码:
执行org.example.Main
类。
验证:
如果还想确认 Spark 到底依赖了哪个版本的 Scala,可以在项目目录下运行 Maven 命令查看依赖树:
mvn dependency:tree | findstr scala-library # Windows
mvn dependency:tree | grep scala-library # Linux / macOS
你会看到类似这样的输出(具体版本号可能略有不同,但应该是 2.12.x 的某个较新补丁版):
[INFO] | +- org.apache.spark:spark-core_2.12:jar:3.5.0:compile
[INFO] | | +- org.scala-lang:scala-library:jar:2.12.18:compile
[INFO] | +- org.apache.spark:spark-sql_2.12:jar:3.5.0:compile
[INFO] | | +- org.scala-lang:scala-library:jar:2.12.18:compile
... (可能还有其他依赖引入 scala-library)
只要所有 Spark 模块最终解析到的 scala-library
版本是一致的,问题通常就能解决。
进阶技巧:使用 Spark BOM (Bill of Materials)
对于管理一堆 Spark 相关模块的版本,特别是当你使用的模块越来越多时,一个更省事的方法是使用 Spark 的 BOM。BOM 文件定义了一组互相兼容的依赖版本。
在 pom.xml
的 <dependencyManagement>
部分引入 Spark BOM:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-bom_2.12</artifactId>
<version>3.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 引入 Spark Core 时不用再写版本号了 -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<!-- <scope>provided</scope> -->
</dependency>
<!-- 引入 Spark SQL 时也不用写版本号 -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.12</artifactId>
<!-- <scope>provided</scope> -->
</dependency>
<!-- 其他非 Spark 的依赖照常写 -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>3.3.6</version> <!-- 这个不是 Spark BOM 管理的 -->
<!-- <scope>provided</scope> -->
</dependency>
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.5.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
</dependencies>
这样,所有受 BOM 管理的 Spark 模块版本都会自动对齐到 3.5.0
,也包括了它们依赖的 Scala 版本,更加方便维护。
方案二:排查并解决传递依赖冲突
原理:
即使你没显式引入 scala-library
,项目中的其他依赖(比如某些 Hadoop 相关库、或者其他大数据生态的库)也可能引入不同版本的 scala-library
或者与 Spark 依赖的其他基础库(如 Jackson、Netty 等)产生冲突。虽然在这个 specific error (TaskMetrics.externalAccums
) 里 Scala 版本是最常见的罪魁祸首,但排查一下总是好的。
操作步骤:
-
分析依赖树:
运行mvn dependency:tree
命令,仔细查看输出。重点关注:- 有没有多个不同版本的
scala-library
被引入? - 有没有多个不同版本的
spark-*
模块被引入?(比如,一个依赖引入了 Spark 2.x,而你用的是 3.x) - 有没有常见的冲突库,如 Jackson (
com.fasterxml.jackson.core
)、Netty (io.netty
)、Guava (com.google.guava
) 等,存在多个不兼容的版本?
- 有没有多个不同版本的
-
排除冲突依赖:
如果发现了冲突,比如某个库some-other-library
引入了一个旧版本的scala-library
,你可以使用 Maven 的<exclusions>
标签来阻止这个传递依赖被引入。<dependency> <groupId>com.example</groupId> <artifactId>some-other-library</artifactId> <version>1.2.3</version> <exclusions> <exclusion> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> </exclusion> <!-- 如果它还引入了其他冲突库,也可以在这里排除 --> <!-- <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> --> </exclusions> </dependency>
这样一来,
some-other-library
就不会再把那个可能引起冲突的scala-library
带进来了,你的项目会使用由 Spark 依赖(或你自己指定的)版本。
安全建议:
别滥用 <exclusions>
。在排除一个依赖前,最好先搞清楚为什么会有冲突,以及排除它会不会影响 some-other-library
本身的功能。有时升级 some-other-library
到一个更新的、与你的 Spark 版本兼容的版本是更好的选择。
方案三:确认运行环境的一致性 (如果不是本地运行)
原理:
这个问题也可能发生在你把代码打包好,然后提交到 Spark 集群(如 YARN、Kubernetes 或 Standalone 模式)上运行的时候。如果你本地开发和编译时用的 Spark 版本 (比如 3.5.0),但集群上部署的 Spark 版本是别的 (比如 3.2.0),那么运行时就会加载集群上的 Spark 库,这跟你编译时的库版本不同,同样会触发 NoSuchMethodError
。
操作步骤:
-
检查集群 Spark 版本:
登录到 Spark 集群的管理节点或任一工作节点,执行spark-shell --version
或spark-submit --version
,确认集群实际使用的 Spark 和 Scala 版本。 -
保持版本一致:
确保你项目pom.xml
中使用的 Spark 版本 (3.5.0
) 和 Scala 版本 (_2.12
) 与集群环境完全一致。如果不一致,要么修改你的pom.xml
以匹配集群版本,要么升级集群的 Spark 版本。 -
打包方式检查 (
scope:provided
):
当你把应用打包成 JAR 提交到集群时,Spark 核心库(spark-core
,spark-sql
等)和 Scala 库通常应该在pom.xml
中设置为<scope>provided</scope>
。这意味着这些库由集群环境提供,不应被打进你的应用程序 JAR 包中。如果错误地将它们打包进去了(比如使用了maven-shade-plugin
但没有正确配置排除规则),可能会导致 classpath 上存在两个不同版本的 Spark 或 Scala 库,从而引发冲突。检查你的打包插件配置,确保 Spark 和 Scala 的核心库没有被包含在最终的 fat jar/uber jar 里。
总结一下
遇到 NoSuchMethodError: TaskMetrics.externalAccums()
这类问题,多半是依赖版本没整明白。按照下面的思路去排查一般都能解决:
- 首要检查: 确保你没有在
pom.xml
里画蛇添足,强制指定scala-library
版本。删掉它,让 Spark 自己带入兼容的版本。这是最常见的原因和最有效的解决办法。 - 其次检查: 使用
mvn dependency:tree
查看完整的依赖关系,看看有没有其他库引入了不兼容版本的 Spark 组件、Scala 或其他基础库。必要时使用<exclusions>
。 - 最后考虑 (如果适用): 如果你是在集群环境运行,检查你的项目构建版本和集群部署的 Spark 版本是不是完全一致,并确认打包时没有把
provided
的库打进去。
希望这些分析和步骤能帮你顺利解决这个拦路虎,继续愉快的 Spark 开发之旅!