本文想要了解一个问题,如果多个线程通过同一个SparkSession提交作业,不同线程间的作业是怎么调度的,工程中Spark使用的是FIFO模式。
单个Spark作业详细的运行流程可见之前写的那篇文章《Spark-Job执行流程分析》。这里简单提一下,一个action操作会被DAGScheduler根据Shuffle关系拆分成多个stage,同一个action中的stage是串行执行的。每个stage会创建一批task,称为TaskSet。TaskSet会交给TaskScheduler进行调度,分配到各个Executor进程中执行。TaskSet中的task是最小的计算单元,每个task对应计算一个Partition中的数据。
从DAGSchedule中的taskScheduler.submitTasks(new TaskSet(tasks.toArray, stage.id, stage.latestInfo.attemptId, jobId, properties))这一行开始看进去。该方法最终会将TaskSet封装成TaskSetManager,然后提交到调度池中调度执行:
然后会Driver端会发送backend.reviveOffers消息执行任务,任务会经历如下三个方法:
TaskSchedulerImpl.resourceOffers()选择要执行的任务,给任务资源分配===> resourceOfferSingleTaskSet()根据数据本地性进行单个TaskSet任务资源分配===> TaskSetManager.resourceOffer()分配task执行
TaskSetManager之间的调度
TaskSetManager之间的调度就发生在上面的resourceOffers()方法中,在resourceOfferSingleTaskSet()方法执行之前。之前的文章中有提到过,Spark中有两种调度池:FIFO 和 FAIR。
FIFO:后提交的TaskSetManager等待先提交的TaskSetManager执行完毕才能执行(当然如果前一个任务CPU核数没有用完的话,多余的CPU可以用来执行后续的任务)。
FAIR:通过fairscheduler.xml来配置,根据其中的weight、minShare配置项进行任务调度。下图是官方自带FAIR调度池配置的例子:
minShare:调度池最小资源数(cores,即CPU核数),默认为0
weight:定的是任务的权重(weight为2的分配到的资源为weight为1的两倍,但是是在满足了minShare之后,多于的CPU资源才会按照weight的比例进行分配)
如果是FIFO模式,那么内部只有一个pool。如果是FAIR模式,内部可以将任务指定提交到不同的Pool中,实现不同Pool之间的CPU资源隔离。可以通过参数spark.scheduler.allocation.file设置用户自定义配置文件
排序算法在SchedulingAlogrithm.scala类中:
对于FIFO模式,先比较优先级,优先级高(编号小,在FIFO模式下就是jobId)的先运行。优先级相同的情况下再比较stageId,stageId小的先运行。
所以对于多个Job来说,如果CPU核数不充足的情况下,肯定是先运行完毕其中的一个Job,再运行下一个(从后面单个TaskSet调度的代码中可以看到,CPU核数充足的话,多个Job是可以同时运行的)。
FAIR计算就比较复杂一点,
根据runningTask minShare weight三者综合来判断优先级
对于TaskSetManager1和TaskSetManager2来说
1.如果其中某一个的runningTask < minShare(最少任务运行数量),比如TaskSetManager1, 那么就先运行TaskSetManager1(任务数少)
2.如果都满足最少任务运行数量了,那么比较runningTasks/minShare的值,谁小说明使用的资源少,先运行资源少的那个(使用资源占比少)
3.如果满足资源比较,再比较权重runningTasks/weight的值,权重越高,这个值越小,先让权重高(值小)的先运行
4.还是不能就只能比较两个stage的名字了(比较stage的Id编号)
对TaskSetManager排好序之后,接下来就是对SortedTaskSet(TaskSet == TaskSetManager)逐个进行调度了。
ps:逐个调度并不代表TaskSetManager不能并行执行。
TaskSetManager内部的Task调度
结合TaskSet排序以及本地性将Tasks分配给各个Executor:
按照TaskSet的数据本地性,逐个给TaskSet分配资源,知道所有CPU都分配完毕,然后开始运行Task。即这些待运行的Task不一定是同一个TaskSet的。但是属于同一个TaskSet的task,它们的数据本地性是同一个级别的。如下所示:
Task的本地性分为5个级别:按照PROCESS_LOCAL(本进程) -> NODE_LOCAL(本节点) -> NO_PREF -> RACK_LOCAL -> ANY 的顺序从TaskSetManager中选择Task去这个executor上面执行。
总结:
所以针对开篇提出来的问题可以得出如下结论,使用FIFO模式多线程提交多个job时:
1.总CPU资源大于所有job需要的CPU资源总和,那么多个job是可以完全并行执行的
2.如果总CPU资源小于某个job的CPU资源总和并且这个job满足当前数据本地性的task数量仍大于总CPU数量,那么只能运行一个job
3.如果总CPU资源大于一个job需要的CPU资源数量,小于两个job的CPU数量,那么第一个job的Task可以完全执行,第二个job只能执行部分Task
什么情况下会使用到FAIR模式呢?
比如本人当前的项目,所有的任务都是通过同一个SparkSession提交的,有一种任务会运行耗时很久,而其他的任务运行相对较快。
在当前使用FIFO模式的情况下,如果耗时久的任务先运行的话,后续的任务都要等待这个任务运行完毕才能运行,不满足用户要求
如果使用FAIR模式对CPU资源进行隔离,虽然运行慢的任务会变的更慢,但是那些运行快的任务就会获得CPU资源从而快速的运行完毕。
说明下 粗粒度资源分配 VS 细粒度资源分配:
粗粒度资源申请(Spark):
将所有Application所要用到的资源申请完毕,才会进行任务的调度。当所有的task执行完成后才会释放这部分资源。
优点:由于所有的资源都申请完毕,所以任务运行速度就快了。
缺点:直到最后一个task执行完成才会释放资源,集群的资源无法充分利用。当数据倾斜时更严重。
细粒度资源申请(MapReduce):
Application执行之前不需要先去申请资源,而是直接执行,让job中的每一个task在执行前自己去申请资源,task执行完成就释放资源。
优点:集群的资源可以充分利用。
缺点:task自己去申请资源,task启动变慢,任务运行的速度就相应的变慢了
参考:
https://blog.csdn.net/LINBE_blazers/article/details/92801646(TaskSchedulerImpl对执行结果的处理)
https://www.cnblogs.com/SysoCjs/p/11466243.html(TaskSet获取时的排序算法)
https://yq.aliyun.com/articles/680995(Spark中的资源调度)
https://cloud.tencent.com/developer/article/1004904(Scheduler内部调度原理)
https://blog.csdn.net/LINBE_blazers/article/details/92396898(单个TaskSet中的调度)
https://blog.csdn.net/zwgdft/article/details/88349295(Spark并行执行多个job)