1.Spark 调度系统包含哪些组件?
Spark 的进程模型是一个 Master/Slave 结构,其中包含了 Master、Worker、Driver 以及 Executor 这四种进程。一个 Application
便是依靠这 4 种进程通力协作完成的,其部署架构图如下所示:
之所以一个 Job 能有条不紊的完成计算,是因为 Spark 的调度系统在起作用(这里特指 Standalone 资源管理器)。在 Standalone 模式下,Spark 框架把计算资源抽象为 Executor,并用于任务调度,一个 Executor 包含若干个 CPU
和 Memory
。一般来说,CPU
的个数就决定了 Executor 内的任务并行度。
一个 Application 提交后,先生成 Job 的逻辑执行图;然后根据依赖关系,划分 Stage,转化成实际的 Task 的物理执行图;最后交给 Executor 以并行的计算方式付诸执行。在这个过程中,调度系统尤为重要,没有它这个过程无法进行。在 Standalone 的资源调度系统中,主要包含以下三个核心成员:
核心成员 | 所在进程 | 作用 |
---|---|---|
DAGScheduler | Driver | 划分 Stage,构建 Task |
SchedulerBackend | Driver | 提供计算资源(CPU 和 Memory) |
TaskScheduler | Driver | 整合资源,调度 Task |
分布式计算的精髓在于把数据依赖图转换为实际可执行的物理执行图,然后以并行的方式交付执行。DAGScheduler
主要负责计算任务的拆解,划分 Stage,构建 Task,类似于一个部门主管,把项目的任务逐级分解;
SchedulerBackend
通过与 ExecutorBackend
通信,掌握了集群中的所有资源情况( CPU 和 Memory )。SchedulerBackend
就好比某个公司的人力资源总监,ExecutorBackend
类似 分公司的人力主管,一起为公司的运作,提供人力资源( CPU 和 Memory )。
DAGScheduler
这边有“活儿”,SchedulerBackend
这边有资源,TaskScheduler
把 “活儿” 和资源进行整合,类似把合适的 ”活“ 派给合适的人,从而完成任务调度。下图是调度系统的协作流程图:
下面我们一次介绍 DAGScheduler
、ScheduleBackend
、TaskScheduler
这三个核心成员。
2.DAGSchedulear 如何工作?
一个 application 要转换为逻辑执行图,再转换为实际的物理执行图才可以并行的付诸执行。这个转换的过程就是由 DAGScheduler
完成。
这个过程可以分成 2 个环节:第一个环节 DAGScheduler
根据转换算子,以惰性的方式构建数据依赖图;第二个环节以 Action 算子为起点,从后向前回溯 DAG ,以 shuffle 为边界,划分 Stage。
干说有点懵,我们还是 wordcount 例子来给大家解释,它的物理执行图如下:
第一个环节,DAGScheduler
根据转换算子形成了图中的 RDD 数据依赖图(粉色)。
对于第二个环节,Spark 在运行的过程中,会有个 Action 算子触发作业的从头计算,此时,DAGScheduler
便会以 shuffle 为边界,去划分 Stage。
如上图,DAGScheduler
便会以 count
算子为起点,从图中的最后,依次把 DAG 中的 RDD 划入到第一个 Stage,也就是图中的蓝色。
直到遇到 reduceByKey
算子,因为该算子会引入 shuffle ,所以从最后一个 RDD ,到 reduceByKey
处所产生的 RDD 便划为整个 Job 的第一个 Stage。
到这里还没完,咱们的 DAGScheduler
会继续向前回溯,直到整个数据依赖图的最开始,都没有碰到 shuffle 依赖,此时便会把这其中的 RDD 划入第二个 Stage,也就是图中紫色部分。
你以为结束了嘛?到这里,只是把 Stage 创建完毕,还要进行 Stage 提交。DAGScheuler
还是从后向前,以递归的方式依次提交 Stage。
例如,在上图中一共划分了 2 个 Stage,DAGScheduler
会先提交 Stage1(蓝色),在提交时,发现 Stage1 依赖的父 Stage0 (紫色) 还没有执行,这个时候便会先提交父 Stage0。
当 Stage0 执行完毕后,再次提交 Stage 1。对于提交的每个 Stage ,DAGScheduler
会根据 RDD partitions 属性创建分布式任务集合 TaskSet,一般来说,有多少个分区, TaskSet 中就有多少个 task。
生成了 Task 任务后,DAGScheduler
便会以 TaskSet 为粒度,提交给 TaskScheduler 去调度执行。到此,我们就把 DAGScheduler
的职责说明白了:
-
根据代码中的转换算子,构建数据依赖图,就是 DAG,有向无环图。
-
以 shuffle 为边界,划分 Stage,并且基于 Stage 创建分布式任务集合 TaskSets,并将一个个 TaskSets 提交给 TaskScheduler,进行调度
但是,在调度执行前,要先了解集群中的资源分布,这就要仰仗 SchedulerBackend
, 它掌握了集群中的所有资源。
3.SchedulerBackend 是什么玩意?
前文说了,SchedulerBackend
就好比一家公司的人力资源总监,实时掌握了整个集群中的资源信息。那么它是怎么做到的呢?
其实,SchudelerBackend
维护了一个名叫 ExecutorDataMap 的数据结构,记录了每个 Executors 的资源情况。
它是一种给哈希结构,key 为 Executor id,value 是 ExecutorData 类,该类封装了如 RPC 地址,主机地址,可用 cpu 数,满配 cpu 数等信息。
Executor id | ExecutorData |
---|---|
executor 1 | {rpc:192.xx.xx, host: 11.123.xx availCores : 22, totalCores: 50} |
executor 2 | {rpc:192.xx.xx, host: 11.123.xx availCores : 22, totalCores: 50} |
有了这个资源 ”小本本“,ScheduleBackend
就可以随时给任务提供计算资源,提供的资源以 WorkerOffer 为粒度,这个类封装了 Executor ID,主机地址和 CPU 核心数等信息,WorkerOffer 用来表示一份可用于调度任务的空闲资源。
在任务调度的过程中,SchedulerBackend
可以提供多个 WorkOffer 用于计算,我们都知道 ScheduleBackend
是 Master 节点上 Driver 端的成员,它是如何掌握所有节点的资源信息的?
SchedulerBackend
带领了一帮小弟,就是 Worker 节点上的 ExecutorBackend
,它会周期性的向 SchedulerBackend
发送心跳信息,汇报当前节点上的资源信息,所以 SchedulerBackend
便可以随时更新自己维护的那份资源 ”小本本“,从而掌握了集群中的所有资源信息。
4.为啥 TaskScheduler 是“中介”?
我们已经知道 DAGScheduler
划分 Stage,创建了 task,手中有一大堆的 ”活儿“,而 SchedulerBackend
维护了集群中的资源 ”小本本“ 。
相当于有了可以干活的人力,接下来,就是 TaskScheduler
把合适的 ”活儿“ ,派给合适的 “工人”,就好像职场中介一样,这个过程我们称为任务调度。
职场中介 TaskScheduler
也不是随便派活儿的,那他派活的原则是什么呢?
其实 TaskScheduler 会按照任务的本地倾向性,筛选出 TaskSets 中合适调度的 task。
要想理解任务的本地倾向性,我们先从 Task 这个任务说起,到底什么是 Task?我们先来看看它的关键属性:
属性名 | stage Id | stageAttempId | taskBinary | partition | locs |
---|---|---|---|---|---|
属性含义 | task 所在的 Stage | 失败重试编号 | 任务代码的二进制文件 | task 所在的 RDD 分区 | 本地倾向性 |
在这张表中,stage Id 和 stageAttempId 记录了 task 与 Stage 之间的关系;
taskBinary 则封装了我们自己写的代码逻辑,partition 表示的是 task 所对应的分区,locs 属性以字符串的方式记录了该任务倾向的计算节点或者 Executor 进程。
前文说了,Task 的个数与分区个数一致,每个 Task 在被创建的时候,DAGSchduler
便会根据数据分区的物理地址,给 Task 设置 locs 属性。locs 属性就记录了数据分区是在那个节点上,或者说是在哪个 Executor 进程。
所以,TaskScheduler
在调度的时候,便会根据 Task locs 这个属性,把它调度到相应的节点或者 Executor 进程。
从上面的分析可知,每个 Task 任务在被创建的时候都会被附上 locs 属性,也就是说每个 Task 都是自带本地倾向性。
目前,Spark 一共有四类本地倾向性,分别是:
-
PROCESS_LOCAL
:这种类型要求对应的数据分区在某个进程中有副本; -
NODE_LOCAL
:节点的本地性倾向,它要求数据在当前节点上存有副本即可; -
RACK_LOCAL
:要求数据分区存在同一机架; -
ANY
: 这个相当于没有要求,Task 随便被调度到哪里都 ok。
上面这张图是 TaskScheduler 的调度逻辑,拿到 WorkOffer 后,会去遍历 TaskSet,优先调度 PROCESS_LOCAL 的 Task,再是 NODE_LOCAL,依次到最后的 ANY。
这里可能又会有小伙伴感到疑惑了,为什么要设置这么多种本地倾向性类型?为什么是 PROCESS_LOCAL 先调度?
其实啊,本地倾向性指的是计算任务(也就是用户写的代码)与分区数据之间的关系。再加上 Spark 调度系统的核心是数据不动,代码动。
也就是说,在任务调度的过程中,TaskScheduler
会把代码发送到数据所在的位置,而尽量让数据待在原地不动,这样尽可能减少网络 IO,毕竟数据分发要比代码分发更重。
所以设置这个本地倾向性,主要是为了保证数据不动,TaskScheduler
根据每一个 Task
的 locs 属性,把它发送到与 Task
locs
属性所对应的节点上或进程中。
讲到这里,我们已经把 TaskScheduler
的职责讲清楚了,它就像是一个职场中介,根据 WorkerOffer 与每一个 Task 的 locs 属性,挑选出合适的 Task。
挑选出合适的 Task 之后,TaskScheduler
便会把 Task 通过 LaunchTask 消息,发送给 SchedulerBackend
, SchedulerBackend
同样在拿到这些 Task后,也是通过 LaunchTask 消息,把 Task 进一步分发给 ExecutorBackend
。
ExecutorBackend
在拿到 Task 之后,便开始把任务派给线程池中处理进行任务执行,每个线程处理一个 Task。
每当线程处理完毕,这些线程会通过 ExecutorBackend
向 Driver 端的 SchedulerBackend
发送 StatusUpdate 事件,告知 Task 的运行状态,然后 SchedulerBackend
会把事件再汇报给 DAGScheduler
。
当然了,对于一个 TaskSet ,只有当所有的 Task 都完成任务调度和任务执行,当前这个 Stage 才算完成,从而进入下一个 Stage 的计算,最后所有的 Stage 完成后,整个 Spark 作业才算结束。
最后总结一下:TaskScheduler
的主要职责:会按照任务的本地倾向性,筛选出 TaskSets 中合适调度的 task。