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/Catalyst(sql/catalyst/、sql/core/)是 Spark SQL 的主战场。Catalyst 优化器将 SQL 或 DataFrame 操作经过一条完整的流水线进行转换:解析 → 分析 → 优化 → 物理计划生成。最终输出的物理计划(SparkPlan)通过调用 execute() 方法产生 RDD[InternalRow],从而与第一层的核心引擎衔接。
第三层 — 用户接口层(sql/api/、sql/connect/)提供开发者日常使用的入口:SparkSession、DataFrame、Dataset。在 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()、read、createDataFrame() 等。它有两个具体实现:
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 通信。
注意 sparkContext 和 sharedState 等方法上的 @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 项目 sql,sql/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 运行时容器,以及将你的代码转化为在集群中分布式运行的任务的两级调度栈。