一、架构

Spark 框架的核心是一个计算引擎,整体来说,它采用了标准的 master-slave 结构。上图中的 Driver 表示 master ,负责管理整个集群中的作业任务调度;Executor 则是 slave,负责实际执行任务;
1.1 Driver
Spark 驱动器节点,运行 Application 的 main() 函数,负责实际代码的执行工作。它会创建SparkContext,SparkContext 负责和 Cluster Manager 通信,进行资源申请、任务分配和监控等;当 Executor 部分运行完毕后,Driver 负责将 SparkContext 关闭;
Spark 作业执行时主要负责:
- 将用户程序转化为作业(job)
- 在 Executor 之间调度任务(task)
- 跟踪 Executor 的执行情况
- 通过 UI 展示查询运行情况
1.1.1 SparkContext
SparkContext 是 Spark 功能的主要入口。其代表与 Spark 集群的连接,能够用来在集群上创建 RDD、累加器、广播变量。每个 JVM 里只能存在一个处于激活状态的 SparkContext,在创建新的 SparkContext 之前必须调用 stop() 来关闭之前的 SparkContext。在 SparkContext 的初始化过程中,Spark 会分别创建 DAGScheduler 作业和 TaskScheduler 任务调度两级调度模块。
1.1.1.1 RDD
弹性分布式数据集,是 Spark 的核心结构,Spark 的基本运算单元,可以通过一系列算子进行操作;Spark 的所有算子都是基于 RDD 来执行的,不同的场景会有不同的 RDD 实现类,但是都可以进行相互转换,RDD 执行过程中会形成 DAG 图,然后形成 lineage(血统) 保证容错性;从物理层面来看,RDD 存储的是 block 和 node 之间的映射;RDD 在逻辑上是一个 HDFS 文件,在抽象是一种元素集合,包含了数据。它是可被分区的,分为多个分区,每个分区在集群的不同节点上,从而让 RDD 中的数据可以被并行操作;RDD 有以下特性:
- 弹性:
存储弹性 :内存与磁盘自动切换;
容错弹性 :数据丢失可以自动恢复;
计算弹性 :计算出错有重试机制;
分片弹性:可以根据需要重新分片;
- 分布式:数据存储在大数据集群不同节点上;
- 数据集:RDD 封装了计算逻辑,并不保存数据,所以对 RDD 的操作并不会改变数据本身,改变的只是 RDD 提供的数据副本;
- 数据抽象:RDD 是一个抽象类,需要子类具体实现;
- 不可变:RDD 可以类似看作 String ,是不可改变的,只能产生新的 RDD;
- 可分区、并行计算;
RDD 的缺陷:
- 不支持细粒度的写和更新操作:Spark 写数据是粗粒度的,所谓粗粒度就是批量写入数据,目的是为了提高效率,但是 Spark 读数据是细粒度的,也就是一条一条读;
- 不支持增量迭代计算;
1.1.1.2 DAG
有向无环图,描述了 RDD 的依赖关系;

有向无环图并不是真正意义的图形,而是由 Spark 程序直接映射成的数据流的高级抽象模型,描述了 RDD 的依赖关系;
当 RDD 遇到 Action 算子时,会将之前的所有算子形成一个 DAG ,也就是 RDD Graph,再在 Spark 中转化为 Job ,提交到集群执行。一个 APP 中可以包含多个 Job;
1.1.1.3 DAGScheduler
DAGScheduler 是 Spark 中高层调度层,它实现了面向阶段的调度。它将 Job 划分成一个或多个 Stage,并把 Stage 分成一个或多个 Task,当完成 Task 的创建后将 Task 按 TaskSet 的方式发送个 TaskScheduler;
如果某个阶段的任务失败,DAGScheduler 会重新提交该阶段。如果某个阶段丢失了,它会重新提交该阶段以及所有依赖的阶段;
DAGScheduler 的工作流程
-
提交作业:当用户在程序中调用一个行动操作(如 count、save 等)时,Spark 会创建一个作业并提交给 DAGScheduler。
-
划分阶段:DAGScheduler 从最终的 RDD 出发,逆向遍历 RDD 的依赖链,遇到宽依赖就划分一个新的阶段。这样会得到一个或多个阶段,每个阶段包含一系列连续的窄依赖转换。
-
提交阶段:DAGScheduler 按照阶段的依赖关系依次提交阶段。如果一个阶段依赖的父阶段已经执行完成,那么该阶段就可以提交了。DAGScheduler 会为每个阶段创建一个任务集(TaskSet),每个任务对应一个分区。然后,DAGScheduler 将这个任务集提交给 TaskScheduler。
-
任务调度与执行:TaskScheduler 负责将任务分配到集群的 Executor 上执行。TaskScheduler 会考虑数据本地性,尽量将任务分配到数据所在的节点上。
-
监控任务执行:DAGScheduler 监控任务的执行情况。如果某个任务失败,DAGScheduler 会重试该任务(重试次数可配置)。如果重试次数用尽,或者阶段因 shuffle 数据丢失而无法计算,DAGScheduler 会重新提交该阶段以及所有依赖的阶段。
-
作业完成:当所有阶段都成功执行完毕后,作业完成。
1.1.1.4 TaskScheduler
TaskScheduler 是 Spark 中负责将 DAGScheduler 划分出来的 TaskSet(一组任务)调度到集群上的组件。负责接收 DAGScheduler 提交来的 TaskSet,按照一定的调度策略将任务分发到 Executor 上执行,并处理任务失败的情况。
1.1.1.5 SparkEnv
Spark 执行环境对象,其中包括与众多 Executor 指向相关的对象;
1.2 Cluster Manager
ClusterManager 是 Spark 资源管理系统的核心组件,负责分配和管理集群资源。standalone 模式中即 Master 节点,控制整个集群;yarn 模式中则是 JobManager;
1.2.1 工作流程
-
提交应用程序 :用户通过
spark-submit提交应用程序; -
资源请求:Driver 向 ClusterManager 请求资源;
-
启动 Executor:ClusterManager 在 Worker 节点上启动 Executor 进程;
-
任务执行:Driver 将任务发送到 Executor 执行;
-
资源释放:应用程序完成后,释放资源;
1.3 Worker
Spark 的计算节点;
1.3.1 Executor
集群中 Worker 节点上一个执行 Application 的 JVM 进程,负责在 Spark 作业中运行具体任务(Task),任务彼此之间相互独立,并负责将数据存在内存或磁盘上,每个 Application 都有各自独立的一批 Executor;
Spark 应用启动时,Executor 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有 Executor 节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他 Executor 节点上继续运行;
Executor 的两个核心功能:
- 负责运行组成 Spark 应用的任务,并将结果返回给 Driver 进程
- 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在 Executor 进程内的,因此任务可以在运行时充分利用缓存数据加速运算;
二、相关概念
2.1 Job
一个 RDD Graph 触发的作业,包含多个 Task 组成的并行计算,往往由 Spark Action 算子触发,一个 Application 种往往会产生多个 Job,一个 Job 包含多个 RDD 及作用于相应 RDD 上的各种 Operation;在 SparkContext 中通过 runJob() 方法向 Spark 提交 Job;
2.2 Stage
每个 Job 会根据 RDD 的宽依赖关系被切分成多个 Stage,每个 Stage 中包含一组相同的 Task,这一组 Task 也叫 TaskSet;每个 Job 会被拆分成多组 Task,作为一个TaskSet,这个 TaskSet 就称为 Stage;Stage 的划分和调度是由 DAGScheduler 来负责的, Stage 分为最终 Stage(Result Stage) 和非最终的 Stage(Shuffle Map Stage),Stage 的边界就是发生 Shuffle 的地方;
2.3 Task
被送到 Executor上的一个工作单元,一个分区对应一个 Task,Task 执行 RDD 中对应 Stage 中包含的算子。Task 被封装好后放入 Executor 的线程池中执行;Task的调度和管理是由 TaskScheduler 负责;
一个 Job 包含多个 Stage,一个 Stage 包含多个 Task;
2.4 宽依赖和窄依赖
窄依赖和宽依赖的概念主要用在两个地方:
- 一个是容错中相当于 Redo 日志的功能;
- 另一个是在调度中构建 DAG 作为不同 Stage 的划分点;
2.4.1 宽依赖
即 shuffle 依赖,指父 RDD 的每个 Partition 都可能被多个子 RDD Partition 使用,子 RDD Partition 通常对应所有的父 RDD Partition;
宽依赖的函数有 groupByKey、join(父 RDD 一个分区的数据进入到多个不同子 RDD 的分区)、partitionBy;
宽依赖的作用:
划分 Stage 的依据,对于宽依赖,必须等到上一阶段的计算完成才能计算下一阶段;
2.4.2 窄依赖
窄依赖表示每一个父 RDD 的 Partition 最多被子 RDD 的一个 Partition 使用,
窄依赖我们形象的比喻为独生子女;
2.5 Spark 容错机制
2.5.1 lineage(血统容错)机制
如果一节点死机了,而且运算窄依赖,则只要把丢失的父 RDD 分区重新计算即可,不依赖与其他节点,没有数据计算冗余适合用于窄依赖关系。而宽依赖需要父 RDD 的所有分区都存在,重算数据的开销就很大,并且有冗余计算(问题节点的每个父 RDD 分区并不是都给丢失的子 RDD,但数据也会计算)。
2.5.2 checkpoint 机制
辅助 lineage 做容错,lineage 过长会造成容错成本过高,这样不如在中间点做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的 RDD 开始重新做 lineage,可以减少开销。
应用场景:
- DAG 中 lineage 过长;
- 在宽依赖上做 checkpoint 获得的收益更大;
三、程序提交流程
大致的流程如下图所示:

3.1 Yarn client 模式

- 在 Yarn client 模式下,通过 spark-submit 提交程序后,会在 client 服务器运行 main() 函数,启动 Dirver 进程;
- Driver 开始构建并初始化 SparkContext ;
- SparkContext 向 ClusterManager(ResourceManager)注册,并申请运行 Executor 的资源(内核和内存);
- ClusterManager 根据 SparkContext 提出的申请和 Worker(NodeManager) 的心跳报告,来决定在哪个 Worker 上启动 Executor;
- Worker 节点收到请求后会启动 Executor;
- Executor 向 SparkContext 注册,这样 Driver 就知道哪些 Executor 运行该应用;
- SparkContext 构建 DAG 图,DAG Scheduler 将 DAG 图分解成多个 Stage ,并把每个 Stage 的 TaskSet 发送给 Task Scheduler ;
- Executor 向 SparkContext 申请 Task ,Task Secheduler 将 Task发送给 Executor,同时 SparkContext 将程序代码发送给 Executor;
- Task 在 Executor 上运行,把运行结果反馈给 Task Scheduler,然后再反馈给 DAG Scheduler,运行完毕后写入数据;
- SparkContext 向 ClusterManager 注销并释放所有资源;
3.2 Yarn cluster 模式

- 在 Yarn cluster 模式下,通过 spark-submit 提交任务后,会启动一个临时进程;
- 临时进程向 ClusterManager(ResourceManager) 通信申请启动 ApplicationMaster(Driver);
- ClusterManager分配 container,并通知 NodeManager 启动 ApplicationMaster,此时的 ApplicationMaster 就是 Driver;
- NodeManager 启动 Driver;
- Driver 启动后开始运行用户 main() 函数;
- Driver开始构建 SparkContext;
- SparkContext 向 ClusterManager注册 Application 并申请运行 Executor 的资源;
- ClusterManager收到 Driver 的资源申请后会分配合适的 Worker 节点;
- Worker 节点启动 Executor 进程;
- Executor 进程启动后会向 SparkContext 反向注册;
- SparkContext 构建 DAG 图,DAG Scheduler 将 DAG 图分解成多个 Stage ,并把每个 Stage 的 TaskSet 发送给 Task Scheduler ;
- Executor 向 SparkContext 申请 Task ,Task Secheduler 将 Task发送给 Executor,同时 SparkContext 将程序代码发送给 Executor;
- Task 在 Executor 上运行,把运行结果反馈给 Task Scheduler,然后再反馈给 DAG Scheduler,运行完毕后写入数据;
- SparkContext 向 ClusterManager 注销并释放所有资源;