文章目录
概述
在spark中调度器(Scheduler)有两种:
- TaskScheduler(低级的调度器接口):负责实际每个具体Task的物理调度执行。
- DAGScheduler(高级的调度器接口):负责将Task拆分成不同Stage的具有依赖关系(包含RDD的依赖关系)的多批任务,然后提交给TaskScheduler进行具体处理。
DAGScheduler会根据各个RDD之间的依赖关系形成一个DAG,并根据ShuffleDependency来进行stage的划分。stage包含多个tasks,个数由该stage的finalRDD决定,stage里面的task完全相同。DAGScheduler完成stage的划分后基于每个Stage生成TaskSet,并提交给TaskScheduler。TaskScheduler负责具体的task的调度,在Executor上启动执行task。
基本概念
- Job:提交给调度器的顶层工作组件,一次RDD Action生成的一个或多个Stage所组成的一次计算作业。举例说明,当用户调用一个action操作,如count,则一个Job会通过submitJob方式被提交。每个Job可能需要执行多个stage来生成中间和结果数据。
- Stage:用来计算Job中间和结果数据的tasks的集合,其中每个task在同一个RDD的不同partition上执行同样的函数。Stages是在shuffle边缘被分离,这会产生一个依赖等待(即必须等待前一个stage完成后才能拉取其输出数据)。有两种类型的stages:ResultStage是执行一个action操作的最后的一个stage,用来生成最终结果;而ShuffleMapStage会为一个shuffle操作生成map输出文件,即中间数据。当这些jobs重用同一个RDD,则Stages会在多个jobs间共享。
- Task:在集群上运行的基本单位。一个Task负责处理RDD的一个partition。RDD的多个patition会分别由同一个TaskSet的不同Task去处理。Tasks是独立工作的单元,每个将会被发送到一台机器上执行。
- TaskSet任务集:一组关联的,但是互相之间没有Shuffle依赖关系的任务所组成的任务集,一个stage对应生成一个TaskSet任务集。
- Cache跟踪:DAGScheduler找到哪些RDDs已经被cache了来避免重计算它们,而且同样地记住哪些ShuffleMapStages已经生成了输出文件来避免重建一个shuffle的map侧计算任务。
- Preferred locations:优化的任务计算位置,DAGScheduler同样基于潜在RDDs的最优化位置,或缓存及shuffle数据的位置,来计算在哪里执行stage中的每个task。
- Cleanup:所有数据结构会被清除当依赖于它们运行的jobs完成后,以防止在一个长期运行的应用中内存泄漏。
主要功能
- 接收用户提交的job;
- 将job划分为不同stage的DAG图,记录哪些RDD、Stage被物化存储,并在每一个stage内产生一系列的task,并封装成TaskSet;
- 结合当前的缓存情况,决定每个Task的最佳位置(移动计算而不是移动数据,任务在数据所在的节点上运行),将TaskSet提交给TaskScheduler;
- 重新提交Shuffle输出丢失的Stage给TaskScheduler(不是由shuffle输出丢失造成的Stage内部的错误,DAGScheduler是不管的,由TaskScheduler负责尝试重新提交task执行);
DAGScheduler类说明
SparkContext中创建DAGScheduler的代码如下,创建DAGScheduler时将SparkContext自身的引用传递进去了。
// 为什么用volatile修饰?并行提交job时,保持多线程内的可见性?
@volatile private var _dagScheduler: DAGScheduler = _
_dagScheduler = new DAGScheduler(this)
由DAGScheduler类定义可以看出DAGScheduler持有了SparkContext,TaskScheduler以及MapOutTrackerMaster和BlockManagerMaster等。DAGScheduler的数据结构主要维护jobId和stageId的关系、Stage、ActiveJob,以及缓存的RDD的partitions的位置信息等。
private[spark] class DAGScheduler(
private[scheduler] val sc: SparkContext,
private[scheduler] val taskScheduler: TaskScheduler,
listenerBus: LiveListenerBus,
mapOutputTracker: MapOutputTrackerMaster,
blockManagerMaster: BlockManagerMaster,
env: SparkEnv,
clock: Clock = new SystemClock())
extends Logging {
// 用于生成jobId(taskId是在taskschedulerImpl中生成,shuffleId和RddId在SparkContext中生成)
private[scheduler] val nextJobId = new AtomicInteger(0)
// 生成stageId
private val nextStageId = new AtomicInteger(0)
// jobid到Stage id集合的映射
private[scheduler] val jobIdToStageIds = new HashMap[Int, HashSet[Int]]
// StageId到stage的映射
private[scheduler] val stageIdToStage = new HashMap[Int, Stage]
/**
* 来自shuffle dependency ID到ShuffleMapStage的映射,将会生成对应依赖的数据。只包含当前运行job部分的
* stage(当jobs要求的shuffle stage完成后,这个映射将会被移除,并且这个shuffle数据的唯一记录将会
* 被放在MapOutputTracker中)。
*/
private[scheduler] val shuffleIdToMapStage = new HashMap[Int, ShuffleMapStage]
// jobId对应的ActiveJob(正在运行的job)集合
private[scheduler] val jobIdToActiveJob = new HashMap[Int, ActiveJob]
// 需要运行的Stage的集合,依赖stage还没有完成的stage集合
private[scheduler] val waitingStages = new HashSet[Stage]
// 正在运行的Stage集合
private[scheduler] val runningStages = new HashSet[Stage]
// 由于拉取数据失败而需要重新提交的stage集合
private[scheduler] val failedStages = new HashSet[Stage]
private[scheduler] val activeJobs = new HashSet[ActiveJob]
// 包含每个RDD的partitions被缓存的位置。
// 这个映射的key是RDD的id,而它的value是用partition分区值索引的数组。
// 数组的每个值是缓存的RDD分区partition的位置集合。
// 所以访问这个映射的操作应该被用synchronizing进行监控(具体查看问题SPARK-4454)。
private val cacheLocs = new HashMap[Int, IndexedSeq[Seq[TaskLocation]]]
// 为了跟踪记录失败的结点,我们使用MapOutputTracker的时间值,它和每个task一起发送。
// 当我们检测到一个结点失败,我们记录下当前的时间值和失败的executor,为新tasks增加它的值,并利用它来忽视
// 丢失的ShuffleMapTask的结果。
// TODO: 关于失败时间点的垃圾收集信息,当我们知道没有更多的丢失的消息需要被检测。
private val failedEpoch = new HashMap[String, Long]
// 是一个典型的生产者和消费者模式,提供事件的缓冲与异步处理
// 为什么不在dagscheduler中直接生成缓冲队列?因为用一个EventLoop抽象类定义缓冲队列
// 的模板,而在子类中具体实现onReceive方法,实现具体的实现,这样可扩展性强
// 这样就不用在想扩展事件处理方法时去继承DAGScheduler类,更加灵活方便