一、场景

不适用:

1、如果你的作业是从main函数从头执行到结尾,中间没有其他线程调用spark的action操作,是不需要开启公平调度器,因为用户这个程序本身就是需要顺序执行,spark默认调度是FIFO,如下:

sc.makeRDD(List("Hello Scala", "Hello Spark"))
          .flatMap(_.split(" "))
          .map((_,1))
          .reduceByKey(_+_)
          .collect()

2、如果你的作业中经常会有大任务出现,比如某个action操作直接把所有 executor-cores资源吃死,这种情况即便使用公平调度器也无法调度,因为公平调度是建立在集群有资源的前提下

3、在资源足够的前提下,只是想让作业并行执行,让程序运行速度更快些,而不考虑任务优先级问题,比如不希望一个action执行后main函数阻塞,想另起一个线程执行其他action操作,则默认FIFO就可以

如下图示例:总资源4core,main函数的action占用2core且阻塞【代表执行大任务】, 此时新线程的action操作也占用2core【代表执行小任务】,通过日志发现,小任务可以执行,并且是在大任务执行过程中执行完毕,故这类无需考虑优先级的场景,在资源足够的前提下仅需开启新线程即可

object SparkFIFO {

    var sc: SparkContext = _
    var rdd: RDD[String] = _

    def main(args: Array[String]): Unit = {
        val sparConf = new SparkConf()
          .setMaster("local[4]")
          .setAppName("FIFO")
        sc = new SparkContext(sparConf)
        rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"), 2).cache()

        threadAction()
        // main线程
        wordcount()

        sc.stop()
    }

    def threadAction(): Unit = {
        new Thread(() => {
            Thread.sleep(2000) // 停止1s让main函数先执行
            rdd.flatMap(_.split(" ")).map(_ => println("------- threadAction ------")).count()
        }).start()
    }

    def wordcount(): Unit = { // 让main函数卡主,检测threadAction函数是否会执行
        rdd.flatMap(_.split(" ")).map(_ => {
            println("------- main ------")
            Thread.sleep(1000 * 10)
        }).collect()
    }

}
22/07/03 15:54:13 INFO MemoryStore: Block rdd_0_1 stored as values in memory (estimated size 88.0 B, free 2004.6 MiB)
22/07/03 15:54:13 INFO MemoryStore: Block rdd_0_0 stored as values in memory (estimated size 88.0 B, free 2004.6 MiB)
22/07/03 15:54:13 INFO BlockManagerInfo: Added rdd_0_1 in memory on 192.168.88.106:49733 (size: 88.0 B, free: 2004.6 MiB)
22/07/03 15:54:13 INFO BlockManagerInfo: Added rdd_0_0 in memory on 192.168.88.106:49733 (size: 88.0 B, free: 2004.6 MiB)
------- main ------
------- main ------
22/07/03 15:54:14 INFO SparkContext: Starting job: count at SparkFIFO.scala:30
22/07/03 15:54:14 INFO DAGScheduler: Got job 1 (count at SparkFIFO.scala:30) with 2 output partitions
22/07/03 15:54:14 INFO DAGScheduler: Final stage: ResultStage 1 (count at SparkFIFO.scala:30)
22/07/03 15:54:14 INFO DAGScheduler: Parents of final stage: List()
22/07/03 15:54:14 INFO DAGScheduler: Missing parents: List()
22/07/03 15:54:14 INFO DAGScheduler: Submitting 2 missing tasks from ResultStage 1 (MapPartitionsRDD[4] at map at SparkFIFO.scala:30) (first 15 tasks are for partitions Vector(0, 1))
22/07/03 15:54:14 INFO TaskSchedulerImpl: Adding task set 1.0 with 2 tasks
22/07/03 15:54:14 INFO TaskSetManager: Starting task 0.0 in stage 1.0 (TID 2, 192.168.88.106, executor driver, partition 0, PROCESS_LOCAL, 7363 bytes)
22/07/03 15:54:14 INFO TaskSetManager: Starting task 1.0 in stage 1.0 (TID 3, 192.168.88.106, executor driver, partition 1, PROCESS_LOCAL, 7363 bytes)
22/07/03 15:54:14 INFO Executor: Running task 0.0 in stage 1.0 (TID 2)
22/07/03 15:54:14 INFO Executor: Running task 1.0 in stage 1.0 (TID 3)
22/07/03 15:54:14 INFO BlockManager: Found block rdd_0_1 locally
22/07/03 15:54:14 INFO BlockManager: Found block rdd_0_0 locally
------- threadAction ------
------- threadAction ------
------- threadAction ------
------- threadAction ------
22/07/03 15:54:14 INFO Executor: Finished task 0.0 in stage 1.0 (TID 2). 1004 bytes result sent to driver
22/07/03 15:54:14 INFO Executor: Finished task 1.0 in stage 1.0 (TID 3). 1004 bytes result sent to driver
22/07/03 15:54:14 INFO TaskSetManager: Finished task 0.0 in stage 1.0 (TID 2) in 57 ms on 192.168.88.106 (executor driver) (1/2)
22/07/03 15:54:14 INFO TaskSetManager: Finished task 1.0 in stage 1.0 (TID 3) in 57 ms on 192.168.88.106 (executor driver) (2/2)
22/07/03 15:54:14 INFO TaskSchedulerImpl: Removed TaskSet 1.0, whose tasks have all completed, from pool 
22/07/03 15:54:14 INFO DAGScheduler: ResultStage 1 (count at SparkFIFO.scala:30) finished in 0.090 s
22/07/03 15:54:14 INFO DAGScheduler: Job 1 is finished. Cancelling potential speculative or zombie tasks for this job
22/07/03 15:54:14 INFO TaskSchedulerImpl: Killing all running tasks in stage 1: Stage finished
22/07/03 15:54:14 INFO DAGScheduler: Job 1 finished: count at SparkFIFO.scala:30, took 0.115021 s
------- main ------
------- main ------
22/07/03 15:54:33 INFO Executor: Finished task 1.0 in stage 0.0 (TID 1). 932 bytes result sent to driver
22/07/03 15:54:33 INFO Executor: Finished task 0.0 in stage 0.0 (TID 0). 975 bytes result sent to driver
22/07/03 15:54:33 INFO TaskSetManager: Finished task 1.0 in stage 0.0 (TID 1) in 20871 ms on 192.168.88.106 (executor driver) (1/2)
22/07/03 15:54:33 INFO TaskSetManager: Finished task 0.0 in stage 0.0 (TID 0) in 20936 ms on 192.168.88.106 (executor driver) (2/2)
适用

4、需要:如果你的spark application是作为一个服务启动的,SparkContext 7*24小时长时间存在,然后服务每次接收到一个请求,就用一个子线程去执行一系列的RDD算子以及代码来触发job的执行,且会根据请求来判断执行优先级,这种场景是适合公平调度器的

比方说用户有读写请求,就可以根据读写请求创建两个资源池,读的请求权重要高一些,这样就可以优先执行读请求

注意:spark的FAIR中可以设置多个pool,但是这和yarn上的queue不一样!spark的pool没有真正实现资源上的隔离,而是执行taskSet的先后顺序,在spark调用taskSetManager执行时会先将待执行的所有的Pool和Pool中的taskSetManager进行排序,而这个排序才是fair的核心,同样TaskScheduler也是根据这个排序后的顺序执行的!

二、介绍

spark runJob作业提交的时候有两种调度策略:FIFO/FAIR 。

这两种调度可以通过sparkConf配置进行选择,那么这两个调度是如何实现的?接下来看一下代码

**在DAGScheluer对job划分好stage并以TaskSet的形式提交给TaskScheduler后,TaskScheduler的实现类TaskSchedulerImpl会为每个TaskSet创建一个TaskSetMagager对象,并将该对象添加到调度池中:

spark fatjar 冲突 spark fair_spark

可以看出这里有一个schedulableBuilder变量,此变量是在创建TaskSchedulerImpl对象的时候通过initialize函数创建的:

spark fatjar 冲突 spark fair_List_02

根据代码可以看出会根据conf配置的schedulingMode来创建不同的调度模式
而schedulingMode是根据spark.scheduler.mode配置得到的,不设置默认是FIFO

spark fatjar 冲突 spark fair_大数据_03

spark fatjar 冲突 spark fair_spark_04

那么为什么要有两个调度策略呢?这是因为当我们需要作业根据优先级来执行的时候,就需要使用fair调度策略了,如果没有设置则默认按照先进先出的顺序调用

注意:这种调度是spark-driver端sparkContext的调度,并不是yarn上的调度!

spark fatjar 冲突 spark fair_spark_05

若想配置公平调度器,参考官网:https://spark.apache.org/docs/3.2.1/job-scheduling.html#fair-scheduler-pools

使用FAIR公平调度器注意事项:

1、fair公平调度器默认是读取fairscheduler.xml 配置文件,根据配置来创建多个pool,因为fair公平就是对多个池子、taskSetManager之间公平,如果使用默认的fairscheduler.xml配置文件则默认会有三个池子:production 、 test、 还有一个默认的defualt池子。

spark fatjar 冲突 spark fair_java_06

默认的defualt池子是FIFO的模型,创建这个池子的目的是当用户代码中没有指定使用哪个池子的时候,就会放入默认池子中,注意:要防止出现这种情况,因为这将会是FIFO的模式,相当于fair公平调度器完全没有发挥出作用。

spark fatjar 冲突 spark fair_List_07

spark fatjar 冲突 spark fair_大数据_08

看了fairscheduler.xml配置文件大家可能会疑惑,我既然已经使用了公平调度器了,那为何配置上的test池子可以设置为FIFO的模式呢?

这是因为公平调度器允许:一个池子内部也可以根据不同的模型进行排序,比方上图中的两个调度池:test/producetion,这两个池子之间可以通过weight和minShare来进行比较的,production的优先级高于test,但test池子内部则按照先进先出的优先级进行排序,而production内部按照TaskSet参数来进行公平调度规则比较

2、spark要开启公平调度器:

spark fatjar 冲突 spark fair_java_09

3、用户代码中要手动设置线程使用的池名称,如下图:如果不设置池名称,则默认所有任务都提交给defualt调度池!那么只有一个default池子有作业,而default池子默认是FIFO, 至此公平调度器就成了一个摆设,无法发挥作用

spark fatjar 冲突 spark fair_spark fatjar 冲突_10

spark fatjar 冲突 spark fair_List_11

三、 示例

fairscheduler.xml文件:故意调大production1的权重

<allocations>
  <pool name="production1">
    <schedulingMode>FAIR</schedulingMode>
    <weight>2</weight>
    <minShare>2</minShare>
  </pool>
  <pool name="production2">
    <schedulingMode>FAIR</schedulingMode>
    <weight>1</weight>
    <minShare>2</minShare>
  </pool>
</allocations>

代码:循环调用threadAction函数并创建新的线程执行action

object SparkFair {

    var sc: SparkContext = _
    var rdd: RDD[String] = _
    val pools = List("production1", "production2", "production1", "production2", "production2", "production1")

    def main(args: Array[String]): Unit = {
        val sparConf = new SparkConf()
          .setMaster("local[4]")
          .setAppName("Fair")
          .set("spark.scheduler.mode", "FAIR")
          .set("spark.scheduler.allocation.file", "src/main/resources/fairscheduler.xml")
        sc = new SparkContext(sparConf)
        rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"), 2).cache()

        pools.foreach(threadAction)

        // main线程
        wordcount()

        Thread.sleep(10000000)
        sc.stop()
    }

    def threadAction(pool: String): Unit = {
        new Thread(() => {
            sc.setLocalProperty("spark.scheduler.pool", pool)
            action()
        }).start()
    }

    def action(): Unit = {
        rdd.flatMap(_.split(" ")).collect()
    }

}

查看日志:可以看到main现成的action放入到了defualt-pool中,由此可见sparkcontext的sc.setLocalProperty(“spark.scheduler.pool”, pool)设置是作用在每个线程的

spark fatjar 冲突 spark fair_java_12

然后看sparkUI:可以看出production1优先执行

spark fatjar 冲突 spark fair_大数据_13

四、 源码

1、通过上面initialize函数可以看出scheduler.initialize(backend)的initialize方法对schedulableBuilder进行了实例化。可以看到程序会根据配置来创建不同的调度池,schedulableBuilder有两种实现,分别是FIFOSchedulableBuilder和FairSchedulableBuilder,接着后面调用了schedulableBuilder.buildPools(),我们来看两者都是怎么实现的。

spark fatjar 冲突 spark fair_List_14

spark fatjar 冲突 spark fair_大数据_15

可以看出其构建过程就是可以看到FairSchedulableBuilder的buildPools方法中会先去读取FAIR模式的配置文件默认位于SPARK_HOME/conf/fairscheduler.xml,也可以通过参数spark.scheduler.allocation.file设置;

然后根据配置文件中的pool标签构件好pool池子并放入到rootPool池子中,rootPool是所有调度策略必须有的顶层池,在TaskSchedulerImpl类初始化时就会创建

spark fatjar 冲突 spark fair_java_16

用户自定义配置文件FAIR可以配置多个调度池,即rootPool里面还是一组Pool,每个Pool中都可以放很多作业

从上到下看池子应该是这样的:

spark fatjar 冲突 spark fair_List_17

图片上可以看出Pool中包含着TaskSetManagr作业,那么是如何包含的呢?,如下图

在Pool中有一个队列,队列泛型是Schedulable类,而Schedulable类是Pool和TaskManager的父类,故有了上图的结构

spark fatjar 冲突 spark fair_spark_18

不过一般来讲rootPool下面只有一层的pool,如果按照默认的fairscheduler.xml配置文件的话,rootPool下面只有三个pool: 一个test调度池,一个默认default调度池和一个production调度池

了解了这个以后我们再来看看任务是如何放进pool中的,如下图:task通过addTaskSetManager函数放入

spark fatjar 冲突 spark fair_spark

然后看addTaskSetManager函数:FIFO就是直接放到RootPool里面了,可想而知FIFO就只有一层,但FAIR则会根据用户的配置[sc.setLocalProperty(“spark.scheduler.pool”, “test”)]来选择放入哪个Pool中

spark fatjar 冲突 spark fair_java_20

spark fatjar 冲突 spark fair_spark fatjar 冲突_21

最终将作业放入指定pool的队列中

spark fatjar 冲突 spark fair_java_22


那这队列放进去后如何使用呢?在 taskScheduler 的 submitTasks 方法中会为每个 taskSet 创建一个 TaskSetManager,用于管理 taskSet。然后向调度池中添加该 TaskSetManager,最后会调用 backend.reviveOffers() 方法为 task 分配资源。

spark fatjar 冲突 spark fair_List_23

下面主要看 backend.reviveOffers() 这个方法,在提交模式是 yarn-cluster 模式下,实际上是调用 YarnClusterSchedulerBackend 的 reviveOffers 方法,实则调用的是其父类 CoarseGrainedSchedulerBackend 的 reviveOffers 方法,这个方法是向 driverEndpoint 发送一个 ReviveOffers 消息。

spark fatjar 冲突 spark fair_java_24

DriverEndpoint 收到信息后会调用 makeOffers 方法:

spark fatjar 冲突 spark fair_大数据_25

接下来会调用resourceOffers

spark fatjar 冲突 spark fair_spark fatjar 冲突_26


在resourceOffers函数中,会调用根rootpool的getSortedTaskSetQueue函数,将根目录下的所有pool进行排序!然后将排序好的TaskSet按照先后顺序执行,这便是调度使用的关键,那么接下来看getSortedTaskSetQueue函数

spark fatjar 冲突 spark fair_spark_27

getSortedTaskSetQueue函数中有两个重要信息:

第一:是先将rootPool下面的所有pool按照taskSetSchedulingAlgorithm规则进行排序。

第二:将第一层排好序的pool通过for循环将其pool内部的所有TaskSetManager再排序

第三:将这些排好序的taskSetManager累加到ArrayBuffer后返回

注意:这里有一个重点就是taskSetSchedulingAlgorithm的规则适用于pool 和 TaskSetManaer!

spark fatjar 冲突 spark fair_java_28

接下来看一下taskSetSchedulingAlgorithm是啥,可以看到其实是pool的排序规则【FIFO/FAIR】,每个pool的排序规则是根据fairscheduler.xml文件中schedulingMode标签决定的

spark fatjar 冲突 spark fair_spark_29


spark fatjar 冲突 spark fair_大数据_30

第一层rootPool的排序规则是根据用户配置调度模式决定的,默认是FIFO,FIFO就只有rootPoll一层,那么Fair的话rootPool就是根据FIFOSchedulingAlgorithm进行排序的, 并且其下面还有其他pool

spark fatjar 冲突 spark fair_java_31

接下来看这两个的排序规则:

spark fatjar 冲突 spark fair_spark fatjar 冲突_32

FIFO由于只有RootPool这一层,故实际上就是比较两个TaskSetManager的jobId

spark fatjar 冲突 spark fair_java_33

而Fair比较器则规则则多了,主要还是根据饥饿原则进行比较

spark fatjar 冲突 spark fair_大数据_34

那么再回到getSortedTaskSetQueue函数中,第一层rootPool根据Fair比较之后,会再根据不同的Pool内部进行比较,拿默认的配置文件来说,会有一个test调度池,一个默认default调度池和一个production调度池;

这第一层顺序:production > test > default

随之而来的就是各个pool中内部TaskSetManager的排序

production调度池是Fair模式,故内部按照TaskSetManager的参数来按照Fair饥饿模式排序:

spark fatjar 冲突 spark fair_大数据_35

test调度池是FIFO模式,故会按照TaskManager的priority【jobId】顺序排序

default调度池是FIFO模式,故也会按照TaskManager的priority【jobId】顺序排序

看到获取规则后,再回到resourceOffers函数中,可以发现当内部排好序的taskSet集合给到TaskSchedulerImpl后会一次调用并执行,至此spark调度策略就完成了

spark fatjar 冲突 spark fair_java_36


如果pool中的taskSet已经完成了,那么外界TaskScheduler会调用pool的函数removeSchedulable来清除pool中的taskSetManager

spark fatjar 冲突 spark fair_List_37

spark fatjar 冲突 spark fair_大数据_38

以上就是spark调度器的设计思路,可以根据代码对照理解;