Read OSS

Apache Spark 代码库导航:架构与模块全景图

中级

前置知识

  • 具备分布式系统的基本概念
  • 熟悉 Scala 语法(case class、trait、模式匹配)
  • 了解构建工具的基本使用(Maven 或 SBT)

Apache Spark 代码库导航:架构与模块全景图

Apache Spark 是目前规模最大、影响力最深远的开源数据处理引擎之一。它的代码库横跨约 40 个 Maven 模块,支持四种编程语言,并能协调数千台机器上的计算任务。对于初次接触这个仓库的开发者来说,面对如此庞大的代码,很容易不知从何下手。

本文的目标是为你建立一张清晰的认知地图。我们将全面梳理 monorepo 的结构,从底层工具库到 SQL 优化器逐层追踪模块依赖,跟踪 spark-submit 到应用程序启动的完整路径,并深入理解 Spark 4.x 引入的 Classic 与 Connect 两种执行模式之间的架构分野。读完本文,你将清楚地知道每个关注点对应哪个目录,以及各模块之间如何协作。

Monorepo 结构与模块组织

Spark 的仓库是一个经典的 monorepo:所有代码集中在一个 Git 仓库中,由根目录的单个 POM 文件统一构建。根目录的 pom.xml 列举了所有模块:

graph TD
    subgraph "Common Utilities"
        A[common/sketch]
        B[common/kvstore]
        C[common/network-common]
        D[common/network-shuffle]
        E[common/unsafe]
        F[common/utils]
        G[common/variant]
    end

    subgraph "Core Engine"
        H[core]
    end

    subgraph "SQL Stack"
        I[sql/api]
        J[sql/catalyst]
        K[sql/core]
        L[sql/hive]
        M[sql/pipelines]
    end

    subgraph "Connect"
        N[sql/connect/common]
        O[sql/connect/server]
        P[sql/connect/client/jvm]
    end

    subgraph "Connectors"
        Q[connector/kafka-0-10-sql]
        R[connector/avro]
        S[connector/protobuf]
    end

    subgraph "Resource Managers"
        T[resource-managers/kubernetes]
        U[resource-managers/yarn]
    end

    C --> H
    E --> H
    H --> J
    I --> K
    J --> K
    K --> O
    K --> L

这些模块可以归纳为六个逻辑分组:

分组 目录 职责
Common common/* 底层工具库:网络通信、内存管理、概率数据结构、键值存储
Core core/ RDD 抽象、调度(DAGScheduler、TaskScheduler)、存储(BlockManager)、RPC
SQL sql/api, sql/catalyst, sql/core, sql/hive DataFrame/Dataset API、Catalyst 优化器、查询执行引擎
Connect sql/connect/* 客户端-服务端架构:Protobuf 协议、gRPC 服务端、JVM/JDBC 客户端
Connectors connector/* 数据源集成:Kafka、Avro、Protobuf
Resource Managers resource-managers/* 集群管理器插件:Kubernetes、YARN

此外还有语言绑定(python/R/)、历史遗留模块(streaming/graphx/mllib/)以及构建基础设施(assembly/launcher/examples/repl/)。

提示: common/unsafe 模块是 Spark 的堆外内存层,提供了用于原始内存访问的 Platform 和贯穿整个 SQL 引擎的二进制行格式 UnsafeRow。深入理解这个模块,是进行性能优化工作的关键所在。

三层架构模型

Spark 的代码库遵循严格的分层依赖模型,可以将其理解为三个同心圆:

flowchart TB
    subgraph Layer1["Layer 1: Core Engine"]
        direction LR
        RDD["RDD Abstraction"]
        SCHED["DAGScheduler + TaskScheduler"]
        STORE["BlockManager + Shuffle"]
        RPC["RpcEnv"]
    end

    subgraph Layer2["Layer 2: SQL/Catalyst"]
        direction LR
        PARSE["Parser (ANTLR4)"]
        ANALYZE["Analyzer"]
        OPT["Optimizer"]
        PLAN["Physical Planner"]
    end

    subgraph Layer3["Layer 3: User-Facing API"]
        direction LR
        SESSION["SparkSession"]
        DF["DataFrame / Dataset"]
        CONNECT["Spark Connect (gRPC)"]
    end

    Layer3 --> Layer2
    Layer2 --> Layer1

第一层 — 核心引擎core/)提供分布式计算的基础原语。RDD(弹性分布式数据集)是最核心的抽象:一个不可变、可分区、基于血缘实现容错的数据集合。DAGScheduler 将计算拆解为多个 Stage,TaskScheduler 负责将任务分发到 Executor,BlockManager 则负责存储管理。

第二层 — SQL/Catalystsql/catalyst/sql/core/)是 Spark SQL 的主战场。Catalyst 优化器将 SQL 或 DataFrame 操作经过一条完整的流水线进行转换:解析 → 分析 → 优化 → 物理计划生成。最终输出的物理计划(SparkPlan)通过调用 execute() 方法产生 RDD[InternalRow],从而与第一层的核心引擎衔接。

第三层 — 用户接口层sql/api/sql/connect/)提供开发者日常使用的入口:SparkSessionDataFrameDataset。在 Spark 4.x 中,这一层进一步分化为两种实现:Classic(进程内 JVM 执行)和 Connect(gRPC 客户端-服务端模式)。

这种分层关系由 Maven 模块依赖强制保证。sql/catalyst 依赖 core,但 core 对 SQL 的存在一无所知。正是这种清晰的边界,让 RDD 引擎保持了通用性,同时允许 SQL 层在其之上构建更高层次的抽象。

CLI 入口:从命令行到 JVM

当你执行 spark-submit my-app.jar 时,背后究竟发生了什么?让我们来逐步追踪。

sequenceDiagram
    participant User
    participant spark_submit as bin/spark-submit
    participant spark_class as bin/spark-class
    participant Launcher as o.a.s.launcher.Main
    participant SparkSubmit as SparkSubmit.doSubmit()
    participant App as User's main()

    User->>spark_submit: spark-submit --class MyApp app.jar
    spark_submit->>spark_class: exec spark-class o.a.s.deploy.SparkSubmit "$@"
    spark_class->>Launcher: java -cp ... o.a.s.launcher.Main SparkSubmit ...
    Launcher-->>spark_class: Returns JVM command with full classpath
    spark_class->>SparkSubmit: exec java -cp <full-classpath> SparkSubmit args...
    SparkSubmit->>SparkSubmit: prepareSubmitEnvironment()
    SparkSubmit->>App: Reflectively invoke main()

第一步: bin/spark-submit 是一个极简的 shell 脚本,核心逻辑不超过 8 行。它定位 SPARK_HOME,禁用 Python 哈希随机化,然后将 org.apache.spark.deploy.SparkSubmit 作为类名转交给 bin/spark-class 处理。

第二步: bin/spark-class 承担了 shell 侧的主要工作。它负责定位 Java、从 $SPARK_HOME/jars/* 组装初始 classpath,并调用 org.apache.spark.launcher.Main——这是一个小型 Java 程序,用于输出包含完整 classpath、内存配置和 JVM 参数的最终 java 启动命令。命令输出经过解析后被 exec 执行。

第三步: SparkSubmit.doSubmit() 解析参数并根据操作类型进行分发——SUBMIT、KILL、REQUEST_STATUS 或 PRINT_VERSION。对于 SUBMIT 操作,它会调用 prepareSubmitEnvironment(),这才是真正的核心所在:它确定集群管理器类型(YARN、Kubernetes、Standalone 或 Local),解析部署模式(client 或 cluster),下载依赖 JAR 包,最终通过反射调用用户的 main 方法。

提示: 排查提交问题时,在 spark-submit 命令中加上 --verbose 参数。这会触发 doSubmit() 中的 appArgs.verbose 逻辑,在应用启动前打印出已解析的 classpath、部署模式和所有配置项,大幅提升调试效率。

SparkSession:面向用户的入口

只要写过 Spark SQL 代码,你就一定用过 SparkSession。在 Spark 4.x 中,它是定义在 sql/api 中的一个抽象类:

sql/api/src/main/scala/org/apache/spark/sql/SparkSession.scala#L63-L72

classDiagram
    class SparkSession {
        <<abstract>>
        +sparkContext: SparkContext*
        +version: String
        +sql(query: String): DataFrame
        +read: DataFrameReader
        +createDataFrame(): DataFrame
    }

    class ClassicSparkSession {
        +sparkContext: SparkContext
        -sharedState: SharedState
        -sessionState: SessionState
        +extensions: SparkSessionExtensions
    }

    class ConnectSparkSession {
        -client: SparkConnectClient
        -channel: ManagedChannel
    }

    SparkSession <|-- ClassicSparkSession
    SparkSession <|-- ConnectSparkSession

抽象类 SparkSession 定义了面向用户的 API——sql()readcreateDataFrame() 等。它有两个具体实现:

Classic 模式(sql/core/src/main/scala/org/apache/spark/sql/classic/SparkSession.scala)在同一进程中执行所有操作。它封装了一个 SparkContext,持有一个 SessionState(其中包含解析器、分析器、优化器和执行计划生成器),并直接在 Driver JVM 内执行查询。这是传统的 Spark 执行模型。

Connect 模式则以轻量级 gRPC 客户端的形式运行。DataFrame 操作被序列化为 Protobuf 消息,发送到远端的 Spark Connect 服务端执行。客户端完全感知不到 RDD 或 SparkContext 的存在,只负责接收服务端通过网络返回的 Arrow 格式结果。

这一分离是 Spark 4.x 中最重要的架构决策之一。其背后的动机在于:Classic 的进程内模型将用户应用与 Spark 运行时深度耦合——升级 Spark 就必须重新编译应用。而 Connect 模式实现了客户端与服务端的解耦,可以独立升级服务端,Python、Go 或 Rust 等语言的轻量级客户端也无需内嵌 JVM 即可与 Spark 通信。

注意 sparkContextsharedState 等方法上的 @ClassicOnly 注解——这些 API 仅在 Classic 模式下可用,在 Connect 模式下调用会失败。

构建系统与开发者导航

Spark 支持两套构建系统,各有其定位:

flowchart LR
    subgraph Maven["Maven (CI / Releases)"]
        MPOM[pom.xml] --> MBUILD[mvn package -DskipTests]
        MBUILD --> MJARS[assembly/target/jars/]
    end

    subgraph SBT["SBT (Development)"]
        SBUILD[project/SparkBuild.scala] --> SCOMPILE["build/sbt compile"]
        SCOMPILE --> SINCR[Incremental compilation]
    end

Maven 是官方构建工具,用于 CI 和正式发版。根目录的 pom.xml 定义了所有模块依赖和插件配置,需要完整、可复现的构建时使用它。

SBT 更适合日常开发。正如 AGENTS.md 中所建议的:"优先使用 SBT 而非 Maven,以获得更快的增量编译速度。" SBT 的构建逻辑定义在 project/SparkBuild.scala 中,它将 Maven 模块名映射为 SBT 项目名。例如,sql/core 对应 SBT 项目 sqlsql/catalyst 对应 catalyst

以下是代码库导航中常用的命令:

任务 命令
编译单个模块 build/sbt catalyst/compile
按通配符运行测试 build/sbt 'catalyst/testOnly *AnalyzerSuite'
运行特定测试 build/sbt 'catalyst/testOnly *AnalyzerSuite -- -z "test name"'
进入 SBT 交互式 shell build/sbt 后执行 project catalyst

提示: 探索 Spark 代码时,最好的方式是从入口点出发,沿执行路径向内追踪。SQL 相关代码从 SparkSession 入手,核心引擎内部则从 SparkContext 出发。不要试图通读整个代码库——聚焦于你感兴趣的功能,沿其执行路径深入即可。

下一步

有了这张全景地图,你已经对整个代码库的版图有了清晰的认知。在下一篇文章中,我们将深入探讨应用程序真正启动时发生了什么:SparkContext 内部精心编排的初始化流程、SparkEnv 运行时容器,以及将你的代码转化为在集群中分布式运行的任务的两级调度栈。