返回

解决 Spark SQL 报错:NoSuchMethodError TaskMetrics.externalAccums

java

搞定 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) 在运行时找不到某个特定方法的签名 。这通常发生在以下情况:

  1. 编译时依赖版本与运行时依赖版本不一致 :你的代码编译时依赖了一个版本的库(比如 spark-core A 版),这个库里有某个类 X 和方法 m()。但实际运行时,类路径 (classpath) 上是另一个版本的库(比如 spark-core B 版),这个 B 版里的类 X 可能没有方法 m(),或者 m() 方法的参数、返回类型变了。当代码尝试调用 m() 时,JVM 就傻眼了,抛出 NoSuchMethodError
  2. 依赖冲突 :你的项目可能引入了多个库,这些库又各自依赖了同一个基础库的不同版本(例如,项目依赖 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-corespark-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 的一部分,而 SQLAppStatusListenerspark-sql 的一部分,后者在运行时调用了前者的方法。如果这两个模块依赖的基础 Scala 版本不匹配,就可能出现这种找不到方法的情况。

对症下药:解决方案来了

既然分析了原因,解决起来就目标明确了:确保所有相关的 Spark 和 Scala 依赖版本协调一致。

方案一:移除显式 Scala 依赖,让 Spark 自己搞定

原理:
Maven 或 Gradle 这样的构建工具,能够处理传递性依赖。当你引入 spark-core_2.12spark-sql_2.12 时,它们会自动将正确版本的 scala-library 作为依赖拉取下来。你显式指定一个非常老的 2.12.0 版本,反而会覆盖掉 Spark 期望的版本,导致不兼容。最好的办法是,相信 Spark 的依赖管理,让它自己带入需要的 scala-library 版本。

操作步骤:

  1. 修改 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>
    
  2. 清理并重新构建项目:
    使用 Maven 命令清理旧的构建结果并重新构建:

    # 在项目根目录下执行
    mvn clean package
    # 或者在 IDE 中执行 Maven Clean 和 Maven Install/Package
    
  3. 再次运行你的 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 版本是最常见的罪魁祸首,但排查一下总是好的。

操作步骤:

  1. 分析依赖树:
    运行 mvn dependency:tree 命令,仔细查看输出。重点关注:

    • 有没有多个不同版本的 scala-library 被引入?
    • 有没有多个不同版本的 spark-* 模块被引入?(比如,一个依赖引入了 Spark 2.x,而你用的是 3.x)
    • 有没有常见的冲突库,如 Jackson (com.fasterxml.jackson.core)、Netty (io.netty)、Guava (com.google.guava) 等,存在多个不兼容的版本?
  2. 排除冲突依赖:
    如果发现了冲突,比如某个库 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

操作步骤:

  1. 检查集群 Spark 版本:
    登录到 Spark 集群的管理节点或任一工作节点,执行 spark-shell --versionspark-submit --version,确认集群实际使用的 Spark 和 Scala 版本。

  2. 保持版本一致:
    确保你项目 pom.xml 中使用的 Spark 版本 (3.5.0) 和 Scala 版本 (_2.12) 与集群环境完全一致。如果不一致,要么修改你的 pom.xml 以匹配集群版本,要么升级集群的 Spark 版本。

  3. 打包方式检查 (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() 这类问题,多半是依赖版本没整明白。按照下面的思路去排查一般都能解决:

  1. 首要检查: 确保你没有在 pom.xml 里画蛇添足,强制指定 scala-library 版本。删掉它,让 Spark 自己带入兼容的版本。这是最常见的原因和最有效的解决办法。
  2. 其次检查: 使用 mvn dependency:tree 查看完整的依赖关系,看看有没有其他库引入了不兼容版本的 Spark 组件、Scala 或其他基础库。必要时使用 <exclusions>
  3. 最后考虑 (如果适用): 如果你是在集群环境运行,检查你的项目构建版本和集群部署的 Spark 版本是不是完全一致,并确认打包时没有把 provided 的库打进去。

希望这些分析和步骤能帮你顺利解决这个拦路虎,继续愉快的 Spark 开发之旅!