今天我们主要来分析一下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,谢谢