> 技术文档 > Apache Spark 的源码

Apache Spark 的源码

Apache Spark 的源码是一个庞大而精密的工程。直接深入每一行代码是不现实的,最好的方式是分层理解,从整体架构到核心模块,再到关键流程。

下面我将为你提供一个全面的、结构化的源码导览。

1. 总体概览:Spark 是什么?

首先,要记住 Spark 的核心定位:一个统一的、用于大规模数据处理的分析引擎

  • 统一:它在一个框架内支持 SQL 查询、流式处理、机器学习和图计算。

  • 大规模:它是一个分布式系统,可以在商用硬件集群上运行。

  • 核心理念

    • RDD (Resilient Distributed Dataset):最早的核心抽象,一个不可变的、可分区、可并行计算的分布式数据集。虽然现在我们更多地使用 DataFrame/Dataset,但底层一切最终都会转换成 RDD。

    • Lazy Evaluation (惰性求值):所有的转换操作(transformations)都只是记录了计算的“计划”,直到遇到一个行动操作(action)时,计算才会真正发生。

    • DAG (Directed Acyclic Graph) Scheduler:Spark 将计算任务组织成一个有向无环图,优化执行路径,并实现容错。

2. 项目目录结构(代码的地图)

打开 Spark 的 GitHub 仓库,你会看到很多目录。理解这些目录的职责是阅读源码的第一步。

Generated code

/├── core/ # ⭐ Spark 的核心!一切的基础├── sql/ # ⭐ Spark SQL, DataFrames, Datasets 和 Catalyst 优化器├── streaming/ # Spark Streaming (包括老的 DStreams 和新的 Structured Streaming)├── mllib/ # 机器学习库 (MLlib)├── graphx/ # 图计算框架├── launcher/ # 启动器模块,负责 `spark-submit`, `spark-shell` 等命令的逻辑├── common/ # 各个模块共用的工具类 (如网络、序列化、日志等)├── resource-managers/ # 资源管理器接口 (YARN, Kubernetes, Mesos)├── connect/ # Spark Connect,新的解耦客户端-服务器架构├── dev/ # 开发者脚本 (构建、测试、发布等)├── examples/ # ⭐ 官方示例代码,学习和调试的绝佳起点└── ... 其他配置、文档等文件

Use code with caution.

建议的阅读顺序:从 core 和 sql 开始,因为它们是现代 Spark 应用的绝对核心。examples 目录是理解 API 如何使用的最佳实践场所。

3. 核心模块详解 (core 模块)

core 模块是 Spark 的心脏,它实现了 Spark 最底层的调度、执行和内存管理。

  • SparkContext.scala:

    • 位置: core/src/main/scala/org/apache/spark/SparkContext.scala

    • 作用: Spark 应用程序的主入口。它代表了与 Spark 集群的连接,可以用来在集群上创建 RDD、累加器和广播变量。一个 JVM 中只能有一个活跃的 SparkContext。

  • RDD.scala:

    • 位置: core/src/main/scala/org/apache/spark/rdd/RDD.scala

    • 作用: RDD 的抽象基类。它定义了 RDD 的五个核心特性:

      1. 一个分区列表 (a list of partitions)。

      2. 一个计算函数,用于在每个分区上计算 (a function for computing each partition)。

      3. 一个对其他 RDD 的依赖关系列表 (a list of dependencies on other RDDs)。

      4. 一个可选的分区器,用于 key-value 型 RDD (an optional Partitioner for key-value RDDs)。

      5. 一个可选的首选位置列表,用于将任务就近分配到数据所在地 (a list of preferred locations to compute each split on)。

    • 你会看到 map, filter, flatMap 等我们熟悉的转换操作都在这里定义。

  • 调度器 (Schedulers):

    • DAGScheduler.scala:

      • 位置: core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala

      • 作用: 负责将 RDD 的依赖关系图 (DAG) 划分成多个阶段 (Stage)。划分的依据是 \"Shuffle\" 操作。遇到一个 Shuffle 依赖,就会创建一个新的 Stage。它将 Stage 提交给 TaskScheduler。

    • TaskScheduler.scala:

      • 位置: core/src/main/scala/org/apache/spark/scheduler/TaskScheduler.scala

      • 作用: 负责将 DAGScheduler 提交来的 Stage 中的任务 (Task) 发送到集群中的 Executor 上执行。它不关心 Stage 之间的依赖关系,只负责任务的启动、重试和状态跟踪。

  • BlockManager.scala:

    • 位置: core/src/main/scala/org/apache/spark/storage/BlockManager.scala

    • 作用: Spark 的存储体系核心。它负责管理 Driver 和 Executor 上的数据块 (Block),这些数据块可以是 RDD 的分区、Shuffle 的中间结果或广播变量。它处理数据在内存、磁盘和远程节点之间的存取。

4. Spark SQL 模块详解 (sql 模块)

这是现代 Spark 最常用、最复杂也最高效的部分。

  • SparkSession.scala:

    • 位置: sql/core/src/main/scala/org/apache/spark/sql/SparkSession.scala

    • 作用: Spark 2.0 之后统一的新入口。它封装了 SparkContext,并提供了操作 DataFrame 和 Dataset 的 API。

  • Dataset.scala / DataFrame:

    • 位置: sql/core/src/main/scala/org/apache/spark/sql/Dataset.scala

    • 作用: DataFrame 是 Dataset[Row] 的一个类型别名。Dataset 是一个分布式的数据集合,它结合了 RDD 的强类型和 Spark SQL 优化引擎的优点。它包含一个逻辑计划 (Logical Plan),描述了计算的步骤。

  • Catalyst 优化器:

    • 位置: sql/catalyst/

    • 作用: 这是 Spark SQL 的大脑,一个基于 Scala 函数式编程构建的可扩展查询优化器。它负责将用户写的 SQL 或 DataFrame/Dataset 操作转换成高效的物理执行计划。

5. 一个 Spark SQL 查询的生命周期(关键流程串讲)

让我们以一个简单的查询为例,看看代码是如何在这些模块间流转的:

Generated scala

val df = spark.read.json(\"path/to/data.json\")val result = df.filter($\"age\" > 21).select(\"name\")result.show() // Action

Use code with caution.Scala

  1. API 调用 -> 创建逻辑计划 (Unresolved Logical Plan)

    • 当你写下 df.filter(...).select(...) 时,你正在 Dataset.scala 中调用 API。

    • 这些调用不会立即执行,而是构建一个描述计算目标的未解析逻辑计划树。此时,Spark 还不知道 \"age\" 或 \"name\" 列是否存在或类型是什么。

  2. 分析 (Analysis)

    • 代码入口: sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala

    • Catalyst 的分析器 (Analyzer) 会使用目录 (Catalog)(一个存储所有表和函数元数据的服务)来解析这个计划。它会绑定列名、表名,检查类型,最终生成一个已解析逻辑计划 (Resolved Logical Plan)

  3. 逻辑优化 (Logical Optimization)

    • 代码入口: sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala

    • 一系列基于规则的优化器会应用到逻辑计划上。例如:

      • 谓词下推 (Predicate Pushdown):将 filter 操作尽可能地推向数据源。如果读取的是 Parquet 文件,Spark 可以在文件级别就过滤掉不符合 age > 21 的行组,极大地减少 I/O。

      • 常量折叠 (Constant Folding):如果查询中有 WHERE age = 10 + 20,它会被优化成 WHERE age = 30。

    • 最终得到一个优化后的逻辑计划 (Optimized Logical Plan)

  4. 物理计划 (Physical Planning)

    • 代码入口: sql/core/src/main/scala/org/apache/spark/sql/execution/SparkPlanner.scala

    • Spark Planner 会将优化后的逻辑计划转换成一个或多个物理执行计划 (Physical Plan)。例如,一个逻辑上的 Join 操作可以被实现为多种物理 Join 策略(如 Broadcast Hash Join, Sort Merge Join)。

    • Spark 会使用**成本模型 (Cost-based Optimization)**来选择最优的物理计划。

  5. 代码生成 (Code Generation)

    • 代码入口: sql/core/src/main/scala/org/apache/spark/sql/execution/WholeStageCodegenExec.scala

    • 这是 Spark SQL 性能的秘密武器。Spark 不会像 RDD 那样逐个解释执行每个操作,而是将一个 Stage 内的许多操作合并(fuse)并动态编译成单个 Java 方法的字节码。这消除了大量的虚函数调用,利用了 CPU 寄存器,性能接近手写的专门程序。

  6. 执行 (Execution)

    • 最终的物理计划是一棵 RDD 树。show() 是一个 Action 操作。

    • 这个 Action 会触发 DAGScheduler 将物理计划(RDD 依赖图)切分成 Stages。

    • TaskScheduler 将这些 Stages 中的 Tasks 分发到各个 Executor 上。

    • Executor 上的 Worker 线程执行生成的 Java 字节码,处理数据分区。

    • 结果被收集回 Driver 端并显示出来。

6. 如何开始探索和贡献?

  1. 从 examples 开始

    • 克隆项目:git clone https://github.com/apache/spark.git

    • 编译项目:按照官方文档,使用 dev/make-distribution.sh 或 build/mvn。

    • 在你的 IDE(如 IntelliJ IDEA)中打开项目。

    • 找到 examples 目录下的 SparkPi.scala 或一个简单的 SQL 示例。尝试在 IDE 中运行它,并设置断点,观察 SparkContext 的创建、任务的提交等过程。

  2. 阅读设计文档

    • 很多重要的设计决策和架构演进都在 Spark 的 JIRA 和设计文档中有详细的讨论。例如,搜索 \"Project Tungsten\" 或 \"Catalyst\" 相关的 JIRA ticket。

  3. 关注一个子模块

    • 不要试图一次性理解所有东西。选择一个你感兴趣的模块,比如 Spark on Kubernetes 的支持 (resource-managers/kubernetes),或者一个你常用的 SQL 函数的实现 (sql/catalyst/.../expressions.scala),然后深入研究它。

  4. 尝试修复一个小 Bug

    • 在 JIRA 上找一个标记为 \"Beginner\" 或 \"newbie\" 的 issue,尝试修复它。这是理解代码和贡献流程的最佳方式。

总结

  • 分层架构:Spark 是一个分层的系统,上层 API (DataFrame/SQL) 依赖下层核心引擎 (Core/Scheduler)。

  • Catalyst 是现代 Spark 的灵魂:理解 Catalyst 的分析、优化、物理计划和代码生成流程,就理解了 Spark SQL 高性能的来源。

  • 从宏观到微观:先理解目录结构和模块职责,再跟踪一个核心流程(如一个 SQL 查询的生命周期),最后才深入到具体某个函数的实现细节。

这个庞大的项目凝聚了全球顶级工程师的智慧,探索它本身就是一个非常有价值的学习过程。祝你探索愉快!