Spark任务调度概述
本文讲述了Spark任务调度的实现框架,概要分析了Spark从Job提交到Task创建并提交给Worker的整个过程。并对Spark任务调度相关的概念进行了介绍。
任务调度总体流程
从设计层面来说,Spark把任务的执行过程划分成多个阶段,每个阶段由一个处理对象来进行处理,并把处理完成的结果传递个下一个处理对象进行处理。
这个过程如下图1所示:
图1 任务调度总体流程抽象
我们知道RDD是懒加载,一般来说Spark RDD的转换函数(transformation)不会执行任何动作,而当Spark在执行RDD的Action函数时,Spark调度器(Scheduler)会根据RDD的依赖关系构建执行的DAG图并提交一个Spark作业(Job)。
Job由很多的Stage构成,这些Stage是在转换中实现最终RDD所需数据的步骤。每个Stage由一组在执行器(executor)上并行计算的任务(task)构成。
图1从概念上,展示了Job->Stage->Task->TaskRunner线程的创建过程:
1)通过SparkContext或SparkSession来执行应用程序。
2)在Spark应用程序中,每个Action操作都会生成并提交一个Job。该动作是由SparkContext中的runJob系列函数来完成的。
3)在Job会根据是否存在宽转换(也就是:是否存在shuffle依赖)为标准来创建Stage。创建Stage的动作是由DAGScheduler来完成的(后面的文章会详细分析创建Stage的过程)。
4)Stage会创建出一系列的Task,这些Task由TaskScheduler提交到各个执行器(Executor)中 。
5)Executor会为每个Task启动一个TaskRunner线程来执行。
任务调度实现框架
前面从抽象概念上分析了任务执行的全过程,下面从实现层面来分析任务调度的全流程。
从实现层面来讲,Spark任务调度的整个过程如下图所示:
图2 任务调度实现流程
1)DAG图的构建
用户编写Spark应用程序,该应用程序可能包括一个或多个RDD的操作。在《RDD转换操作原理》一节分析过,RDD的每个转换操作(Transformations操作)会形成一个新的RDD,多个RDD之间会形成依赖关系,这就是RDD的DAG。可以参考"RDD依赖的实现原理"和"RDD的血缘(Lineage)"章节。
2)Stage的划分和提交
Spark会把DAG交给一个后台服务:DAGScheduler。它会根据DAG中RDD的相互依赖关系,按是否为shuffle依赖(宽依赖)为边界,来划分Stage(后面会有专门的文章分析如何划分Stage)。一个DAG可能被划分成一个或多个Stage,多个Stage也是相互依赖的,Stage的依赖也是一个DAG图。
3)创建Task和TaskSet,并传给TaskScheduler
DAGScheduler会根据需要计算的分区列表和Stage的类型,来为每个分区创建一个Task,然后为该每个Stage创建一个TaskSet,并把该TaskSet传递给对应运行模式的TaskScheduler对象(不同运行模式,TaskScheduler实现类不同)。
4)Task的提交和执行
TaskScheduler对象接收到TaskSet后,会检查Executor端是否有足够的资源来执行本次提交的TaskSet,检查时会考虑资源调度的算法,目前有两种资源调度算法:FIFO(先进先出调度)和FAIR(公平调度),默认是FIFO。若资源足够,则会根据SchedulerBackend后台的DriverEndpoint(RPC环境)来向Worker端对应的Executor来发送LaunchTask(TaskDescription)消息来执行任务。
其实消息不是直接传递给Executor的,而是每个Executor都对应一个后台服务:CoarseGrainedExecutorBackend,消息是由该后台服务接收,并传递Executor对象的函数,此时Executor就可以开始创建TaskRunner线程,向任务线程池中提交了。
至此,任务线程正式启动了。Executor会把任务执行的状态,通过CoarseGrainedExecutorBackend传递给SchedulerBackend后台,这样Driver端就能实时得知每个Executor的任务执行状态了。
任务提交的这个过程如下图所示:
任务调度的基本概念
DAG(Direct Acyclic Graph)
在Spark任务调度的较高层面,Spark会根据RDD的依赖关系,以Shuffle依赖为边界来划来划分出一个个的相互依赖的Stage,这些Stage构成了一个有向无环图(DAG)。
DAGScheduler
DAGScheduler实现面向阶段(Stage)的高层次的调度。 它为每个Job计算出一个由Stage组成的DAG,跟踪RDD和Stage的输出,并找到运行Job的最小执行计划。
然后,它将阶段(Stage)创建为TaskSet传给TaskScheduler,再交由TaskScheduler来处理TaskSet。
ActiveJob
每个Action操作(比如:count())都会触发一个Job的提交。Job由ActiveJob类来表示。
从逻辑上划分有两种Job,一种是触发Action后用来计算结果。一种是,map阶段的Job,用来计算map端的输出数据。每个Job可以被划分成多个Stage。
Stages
Spark的DAGScheduler把一个Job划分成多个相互依赖的Stage。它会以RDD的Shuffle依赖为边界来进行Stage的划分。也就是说,在遍历RDD的依赖关系时,只要遇到Shuffle依赖就会创建一个新的Stage,这样同一个Stage中的依赖都是窄依赖,所以同一个Stage中的任务可以通过pipeline方式执行。
有两种类型的Stage:
- ResultStage:用来计算触发的Action操作的结果;
- ShuffleMapStage:是执行DAG的中间阶段,主要是为shuffle产生数据。它们会在每次shuffle操作之前执行,可能会包括多个pipeline操作。
TaskSet
TaskSet是Task的集合。进一步来说,Spark会通过DAGScheduler为每个Stage生成一个TaskSet。所以,一个TaskSet是一个Stage的所有Task的集合。
TaskSet会传给TaskScheduler,由TaskScheduler来进一步处理。
Task
每个TaskSet由一组Task组成。Task是Spark任务执行层次的最小执行单元,在Executor端每个单元对应一个执行线程。一个TaskSet中的Task,会在不同的分区数据上执行,但一个Task不能在多个Executor上执行。
但是,每个执行器(executor)都有一个动态分配器的资源数用来运行任务,并且可以在其生命周期内并发运行多个任务。
Spark会为每个分区创建一个Task,所以,每个Stage的Task数,对应于该Stage的输出RDD的分区数。
Taskscheduler
任务(Task)调度器,它接收从DAGScheduler传过来的每个阶段(Stage)的任务集(TaskSet),并负责将任务发送到Worker去执行,若Task运行失败则进行重试。
实现层面,Spark定义了一个org.apache.spark.scheduler.Taskscheduler的接口,该接口的唯一实现类是org.apache.spark.scheduler.TaskSchedulerImpl。
不同模式的任务调度的实现,需要通过一个调度后台来完成,调度后台需要实现接口:SchedulerBackend。
SchedulerBackend
调度系统的后台服务模块,主要负责向资源管理器申请集群资源。调度器后台服务在SparkContext创建时启动。
不同模式的任务调度后台的实现不同。实现调度后台服务时,都必须实现接口:SchedulerBackend。
调度器后台服务的大部分功能是在父类:CoarseGrainedSchedulerBackend中完成的,比如:向Executor端发起执行Task的指令等。不同模式的任务调度后台其实是继承了和复用了CoarseGrainedSchedulerBackend类中的功能实现。
调度后台的实现后面会有文章专门进行分析。
CoarseGrainedExecutorBackend
Executor端的后台服务,每个Executor都对应一个CoarseGrainedExecutorBackend后台服务。该后台服务用来启动和控制Executor,它和Driver端的SchedulerBackend后台通信,接收并处理来自Driver端SchedulerBackend的命令。例如:当有一个Task集合需要执行时,Driver端的SchedulerBackend发送给该服务模块的命令是:LaunchTask。此时,CoarseGrainedExecutorBackend会检查Executor是否已经启动,若已经启动,则会启动线程池来执行提交的Task。
另外,该后台服务把Executor的状态信息汇报给Driver端。所以,CoarseGrainedExecutorBackend就像是一个Executor的代理服务。
总结
本文介绍了Spark任务调度框架的基本概念,并对任务调度框架中的各个成员进行了简要的说明。后续的文章将详细分析Spark任务调度框架中的各个部分的具体实现原理。