spark任务调度FIFO和FAIR的详解

版权声明:原创文章,转载请注明出处 https://blog.csdn.net/xianpanjia4616/article/details/84405145

今天我们主要来分析一下spark的任务调度,Spark中的调度模式主要有两种:FIFO和FAIR。默认情况下Spark的调度模式是FIFO(先进先出),谁先提交谁先执行,后面的任务需要等待前面的任务执行。而FAIR(公平调度)模式支持在调度池中为任务进行分组,不同的调度池权重不同,任务可以按照权重来决定执行顺序。spark的调度模式可以通过spark.scheduler.mode进行设置。

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

schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)

目前Spark中有两种可调度的实体,Pool和TaskSetManager。Pool是一个调度池,Pool里面还可以有子Pool,Spark中的rootPool即根节点默认是一个无名的Pool。

/***TaskSchedulerImpl的初始化方法*/
def initialize(backend: SchedulerBackend) {
    this.backend = backend
    // temporarily set rootPool name to empty
    rootPool = new Pool("", schedulingMode, 0, 0)
    schedulableBuilder = {
    schedulingMode match {
        case SchedulingMode.FIFO =>
            new FIFOSchedulableBuilder(rootPool)
        case SchedulingMode.FAIR =>
            new FairSchedulableBuilder(rootPool, conf)
    }
    }
    schedulableBuilder.buildPools()
}

从上面可以看到会根据不同的mode创建不同的调度池,分别为FIFOSchedulableBuilder和FairSchedulableBuilder两种,在最后面调用了schedulableBuilder.buildPools(),接下来我们来看两者都是怎么实现的。

override def buildPools() {
    // nothing
  }

可以看到FIFOSchedulableBuilder这个里面其实什么都没有做.

override def buildPools() {
    var is: Option[InputStream] = None
    try {
      is = Option {
        schedulerAllocFile.map { f =>
          new FileInputStream(f)
        }.getOrElse {
          Utils.getSparkClassLoader.getResourceAsStream(DEFAULT_SCHEDULER_FILE)
        }
      }
      //根据配置文件创建buildFairSchedulerPool
      is.foreach { i => buildFairSchedulerPool(i) }
    } finally {
      is.foreach(_.close())
    }

    // finally create "default" pool
    buildDefaultPool()
  }

可以看到FairSchedulableBuilder会读取FAIR的配置文件,默认是在SPARK_HOME/conf/fairscheduler.xml,也可以通过参数spark.scheduler.allocation.file设置用户自定义配置文件。

调度的排序接口如下所示,就是对两个可调度的对象进行比较。

private[spark] trait SchedulingAlgorithm {
    def comparator(s1: Schedulable, s2: Schedulable): Boolean
}

其实现类为FIFOSchedulingAlgorithm、FairSchedulingAlgorithm

/**
 * FIFO排序的实现,主要因素是优先级、其次是对应的Stage
 * 优先级高的在前面,优先级相同,则靠前的stage优先
 */
private[spark] class FIFOSchedulingAlgorithm extends SchedulingAlgorithm {
    override def comparator(s1: Schedulable, s2: Schedulable): Boolean = {
    //一般来说优先级越小优先级越高
    val priority1 = s1.priority
    val priority2 = s2.priority
    var res = math.signum(priority1 - priority2)
    if (res == 0) {
        //如果优先级相同,那么Stage靠前的优先
        val stageId1 = s1.stageId
        val stageId2 = s2.stageId
        res = math.signum(stageId1 - stageId2)
    }
    if (res < 0) {
        true
    } else {
       false
    }
 }
}

1.先比较priority,在FIFO中该优先级实际上是Job ID,越早提交的job的jobId越小,priority越小,优先级越高。
2.若priority相同,则说明是同一个job里的TaskSetMagager,则比较StageId,StageId越小优先级越高。

private[spark] class FairSchedulingAlgorithm extends SchedulingAlgorithm {
    override def comparator(s1: Schedulable, s2: Schedulable): Boolean = {
    //最小共享,可以理解为执行需要的最小资源即CPU核数,其他相同时,所需最小核数小的优先调度
    val minShare1 = s1.minShare
    val minShare2 = s2.minShare
    //运行的任务的数量
    val runningTasks1 = s1.runningTasks
    val runningTasks2 = s2.runningTasks
    //是否有处于挨饿状态的任务,看可分配的核数是否少于任务数,如果资源不够用,那么处于挨饿状态
    val s1Needy = runningTasks1 < minShare1
    val s2Needy = runningTasks2 < minShare2

     //最小资源占用比例,这里可以理解为偏向任务较轻的   
    val minShareRatio1 = runningTasks1.toDouble / math.max(minShare1, 1.0).toDouble
    val minShareRatio2 = runningTasks2.toDouble / math.max(minShare2, 1.0).toDouble

     //权重,任务数相同,权重高的优先
    val taskToWeightRatio1 = runningTasks1.toDouble / s1.weight.toDouble
    val taskToWeightRatio2 = runningTasks2.toDouble / s2.weight.toDouble
    var compare: Int = 0

     //挨饿的优先
    if (s1Needy && !s2Needy) {
        return true
    } else if (!s1Needy && s2Needy) {
        return false
    } else if (s1Needy && s2Needy) {
        //都处于挨饿状态则,需要资源占用比小 的优先
        compare = minShareRatio1.compareTo(minShareRatio2)
    } else {
        //都不挨饿,则比较权重比,比例低的优先
        compare = taskToWeightRatio1.compareTo(taskToWeightRatio2)
   }

  if (compare < 0) {
        true
   } else if (compare > 0) {
    false
   } else {
  //如果都一样,那么比较名字,按照字母顺序比较,不考虑长度,所以名字比较重要
    s1.name < s2.name
  }
 }
}

1.调度池运行的task数小于minShare的优先级比不小于的优先级要高。
2.若两者运行的task个数都比minShare小,则比较minShare使用率,使用率约低优先级越高。
3.若两者的minShare使用率相同,则比较权重使用率,使用率约低优先级越高。
4.若权重也相同,则比较名字。

这一块还是稍微有点难理解的.如JobA和JobB的Task数量相同都是10,A的minShare是2,B的是5,那占用比为5和2,显然B的占用比更小,贪心的策略应该给B先调度处理;

那理论上的东西就先大致介绍这么多,下面我们就具体看一个demo,看下具体的写法.

SPARK_HOME/conf/fairscheduler.xml

<allocations>
  <pool name="production">
    <schedulingMode>FAIR</schedulingMode>
    <weight>4</weight>
    <minShare>5</minShare>
  </pool>
  <pool name="test">
    <schedulingMode>FIFO</schedulingMode>
    <weight>2</weight>
    <minShare>3</minShare>
  </pool>
</allocations>

schedulingMode:FIFO 或 FAIR,FIFO是默认的策略。
weight:每个池子分配资源的权重,默认情况下所有的权重为1。
minShare:最小资源,CPU核的数量,默认为0。在进行资源分配时,总是最先满足所有池的minShare,再根据weight分配剩下的资源。

代码如下,我这是一个sparksql的demo;

package spark

import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SQLContext
import org.apache.spark.{SparkConf, SparkContext}

/**
  * spark的fair任务调度;
  */
object sparkSqlDemo {
  case class Person(name:String,age:Int,city:String)
  Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)
  def main(args: Array[String]): Unit = {
    val s = System.currentTimeMillis()
    val conf = new SparkConf().setAppName("sparksql")
    conf.set("spark.scheduler.mode","FAIR")
    conf.set("spark.driver.allowMultipleContexts","true")
    val sc = new SparkContext(conf)
    sc.setLocalProperty("spark.scheduler.pool", "production")
    val context = new SQLContext(sc)
    val peopleRDD = sc.textFile("hdfs://master:9000/jason/test.txt")
      .map(_.split(" "))
      .filter(x=> !x.isEmpty)
      .map(x => Person(x(0), x(1).toInt,x(2)))
    import context.implicits._
    val df = peopleRDD.toDF
    df.createOrReplaceTempView("people")
    val query_df = context.sql("select * from people")
    println("第一个执行完了")
    query_df.show()
    val count_query = context.sql("select count(1) from people")
    println("第二个执行完了")
    count_query.show()

    val test_1 = context.sql("select sum(age) from people")
    println("第三个执行完了")
    test_1.show()

    val test_2 = context.sql("select avg(age) from people")
    println("第四个执行完了")
    test_2.show()

    val test_3 = context.sql("select min(age),max(age),avg(age) from people")
    println("第五个执行完了")
    test_3.show()
    val e = System.currentTimeMillis()
    println("总共用时:" + (e-s))
  }
}

然后我们提交这个任务,会在yarn上面看到如下的:

从这张图可以看到scheduling Mode是FAIR,这就说明spark的使用的是公平调度,然后点击stages,会看到下面的情况:

从这图可以看到我们刚才那个配置文件里面的pool,包括权重等配置.可以看到下面的stage都是运行在我们配置的调度池里面

今天就先写到这里吧,困了,有时间再接着完善一下.

如果有写的不对的地方,欢迎大家指正,如果有什么疑问,可以加QQ群:340297350,谢谢

猜你喜欢

转载自blog.csdn.net/xianpanjia4616/article/details/84405145