FIFO
FIFO(先进先出)方式调度Job,每个Job被切分成多个Stage。第一个Job优先获取所有可用资源,接下来第二个Job再获取剩余可用资源。(每个Stage对应一个TaskSetManager)
优先级(Priority): 在DAGscheduler创建TaskSet时使用JobId作为优先级的值。FIFO调度算法实现如下所示
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) {
val stageId1 = s1.stageId
val stageId2 = s2.stageId
res = math.signum(stageId1 - stageId2)
}
if (res < 0) {
true
} else {
false
}
}
}
由源码可知,FIFO依据JobId进行挑选较小值。因为越早提交的作业,JobId越小。
对同一个作业(Job)而言,越早生成的Stage,其StageId越小。有依赖关系的多个Stage之间,DAGScheduler会控制Stage是否会被提交到调度队列中(若其依赖的Stage未执行完前,此Stage不会被提交),其调度顺序可通过此来保证。但若某Job中有两个无入度的Stage的话,则先调度StageId小的Stage
FAIR
FAIR共享模式调度下,Spark以在多Job之间轮询方式为任务分配资源,所有的任务拥有大致相当的优先级来共享集群的资源。FAIR调度模型如下图:
Fair调度队列可存在多个调度队列,且队列呈树型结构,用户使用sc.setLocalProperty(“spark.scheduler.pool”, “poolName”)来指定要加入的队列,默认情况下会加入到buildDefaultPool。每个队列中还可指定自己内部的调度策略,且Fair还存在一些特殊的属性:
以上参数,可通过conf/fairscheduler.xml文件配置调度池的属性。Fair调度算法实现:
private[spark] class FairSchedulingAlgorithm extends SchedulingAlgorithm {
override def comparator(s1: Schedulable, s2: Schedulable): Boolean = {
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
}
}
}
由源码可知,未满足minShare规定份额的资源的队列或任务集先执行;如果所有均不满足minShare的话,则选择缺失比率小的先调度;如果均不满足,则按执行权重比进行选择,先调度执行权重比小的。如果执行权重也相同的话则会选择StageId小的进行调度(name=“TaskSet_”+ taskSet.stageId.toString)。以此为标准将所有TaskSet进行排序, 然后选出优先级最高的进行调度。
默认情况下,Spark的调度程序以FIFO方式运行作业。每个job会被划分成很多stage,在第一个job运行完成之后,第二个job才会去执行。如果在队列头部的job不需要使用集群的全部资源,那么后面的job可以立即执行。队列头部的job很大的话,其余的job必须推迟执行。
从Spark 0.8开始,也可以在作业之间配置公平的共享。在公平分享下,Spark以“循环”方式在任务之间分配tasks,使所有job获得大致相等的集群资源份额。这意味着长job运行期间提交的短job,也可以立即获取到资源,并且仍然可以获得良好的响应时间,而无需等待长job完成。此模式最适合多用户。要启用公平调度程序,只需在配置SparkContext时:
val conf = new SparkConf().setMaster(...).setAppName(...)
conf.set("spark.scheduler.mode", "FAIR")
val sc = new SparkContext(conf)
Spark的分配资源主要就是 executor、cpu、memory。分配资源,首先要了解机器有多大的内存,多少个cpu core,就根据这个实际情况去设置。一个cpu对应2-3task合理。
Standalone 模式
如果每台机器可用内存是4G,2个cpu core,20台机器,
那可以设置:20个executor,每个executor4G内存,2个cpu core(资源最大化利用)。
yarn 模式下
根据spark要提交的资源队列资源来考虑,如果所在队列资源为500G内存,100个cpu core。
可以设置50个executor;每个executor10G内存2个cpu
增加每个executor的cpu core,也是增加了执行的并行能力。 原本20个executor,每个才2个cpu core。能够并行执行的task数量,就是40个task。如果现在每个executor的cpu core,增加到了5个。能够并行执行的task数量,就是100个task。执行的速度,提升了2.5倍。如果executor数量比较少,那么能够并行执行的task数量就比较少,就意味着Application的并行执行的能力就很弱。
增加每个executor的内存量。 增加了内存量以后,对性能的提升有几点:
Master资源调度通过schedule()方法实现,步骤如下:
Driver向Master进行Application注册的时候,Master注册完之后,会调用schedule()方法,进行资源调度。schedule()源码如下:
private def schedule(): Unit = {
// 首先判断master状态不是alive的话,直接返回,也就是说standby是不会进行资源调度的
if (state != RecoveryState.ALIVE) { return }
// Drivers take strict precedence over executors
// Random.shuffle的原理主要是遍历整个ArrayBuffer,随机交换从后往前的两个位置的数
// 对传入集合中的元素进行随机打乱
val shuffledWorkers = Random.shuffle(workers) // Randomization helps balance drivers
// 取出worker中之前所有注册上来的worker,进行过滤,worker必须是alive状态
for (worker <- shuffledWorkers if worker.state == WorkerState.ALIVE) {
// 首先调度Driver。
// 为什么要调度Driver,什么情况下会注册Driver,调度Driver?
// 其实只有用yarn-cluster模式提交的时候,才会注册Driver,因为Standalone和yarn-client模式
// 都直接在本地启动Driver,不会注册Driver,更不会让master调度Driver了
// 遍历等待调度的Driver
for (driver <- waitingDrivers) {
// 如果当前worker的空闲内存量,大于等于Driver需要的内存,
// 并且worker的空闲cpu core,大于Driver所需的cpu数量
// Driver是在Worker上启动的。。因此Worker节点的内存和CPU需要能够让Driver运行
if (worker.memoryFree >= driver.desc.mem && worker.coresFree >= driver.desc.cores) {
// 启动Driver
launchDriver(worker, driver)
// 从缓存中移除
waitingDrivers -= driver
}
}
}
// 对worker的调度机制,在worker上启动executor
startExecutorsOnWorkers()
}
从上述源码中可以看出,首先对所有已注册的worker进行随机打散;接着进行遍历,去除不是alive状态的worker,首先对Driver进行调度,为什么一开始要调度Driver?因为只有在yarn-cluster模式下才需要调度Driver,在这个模式下,YARN需要找一个NodeManager来启动Driver,因此需要在已注册的worker节点集合中寻找满足条件的worker,来启动Driver。由于standalone和yarn-client都是在本地启动Driver,所以无需进行调度。
调度完Driver之后,就正式开始进行executor的调度了,调用了方法startExecutorsOnWorkers(),源码如下:
private def startExecutorsOnWorkers(): Unit = {
// Right now this is a very simple FIFO scheduler. We keep trying to fit in the first app
// in the queue, then the second app, etc.
// Application的调度机制,默认采用的是spreadOutApps调度算法
// 首先遍历waitingApps中的appInfo,并且过滤出,还需要调度的core的app,
// 说白了,就是处理app需要的cpu core
for (app <- waitingApps if app.coresLeft > 0) {
// 这是脚本文件中的 --executor-cores 这个参数
val coresPerExecutor: Option[Int] = app.desc.coresPerExecutor
// Filter out workers that don't have enough resources to launch an executor
// 过滤掉没有足够资源启动的worker
// 从worker中过滤出状态为alive的worker,并且这个worker的资源能够被Application使用
// 然后按照剩余cpu数量倒叙排序,从大到小排序
val usableWorkers = workers.toArray.filter(_.state == WorkerState.ALIVE)
.filter(worker => worker.memoryFree >= app.desc.memoryPerExecutorMB &&
worker.coresFree >= coresPerExecutor.getOrElse(1))
.sortBy(_.coresFree).reverse
// cpu core 和 memory 资源分配
val assignedCores = scheduleExecutorsOnWorkers(app, usableWorkers, spreadOutApps)
// Now that we've decided how many cores to allocate on each worker, let's allocate them
// 给每个worker分配完资源给application之后
// 遍历每个worker节点
for (pos <- 0 until usableWorkers.length if assignedCores(pos) > 0) {
// 启动executor
allocateWorkerResourceToExecutors(
app, assignedCores(pos), coresPerExecutor, usableWorkers(pos))
}
}
}
这里的调度机制默认是采用FIFO的调度算法。首先在等待调度队列的App中取出一个Application,并且过滤出还需要调度core的App(还没有调度完的App);接着读取App中的coresPerExecutors参数,代表了每个executor被分配的多少个cpu core(spark-submit脚本中可设置的参数 --executor-cores);然后从worker中过滤出状态为alive的worker,然后对这些worker再过滤出它们的内存和cpu core能够启动Application的worker,也就是对存活着的worker中过滤出有足够资源去启动Application的worker,并按照cpu core的大小降序排序,下一步使用scheduleExecutorsOnWorkers()方法,给每个worker分配资源,最后使用allocateWorkerResourceToExecutors()启动executor。
下面看看,worker节点怎么被分配executor,scheduleExecutorsOnWorkers()源码如下:
private def scheduleExecutorsOnWorkers(
app: ApplicationInfo,
usableWorkers: Array[WorkerInfo],
spreadOutApps: Boolean): Array[Int] = {
// --executor-cores app中每个executor被分配的cores
val coresPerExecutor = app.desc.coresPerExecutor
// 每个worker最少被分配的cpu core,默认就是coresPerExecutor
val minCoresPerExecutor = coresPerExecutor.getOrElse(1)
// 如果没有设置--executor-cores 参数的话,就默认分配一个executor
val oneExecutorPerWorker = coresPerExecutor.isEmpty
// --executor-memory 每个executor要被分配的内存大小
val memoryPerExecutor = app.desc.memoryPerExecutorMB
// 可用worker的个数
val numUsable = usableWorkers.length
// 创建一个空数组,存储了要分配给每个worker的cpu core数量
val assignedCores = new Array[Int](numUsable) // Number of cores to give to each worker
// 每个worker上分配几个executor
val assignedExecutors = new Array[Int](numUsable) // Number of new executors on each worker
// 获取需要分配的core的数量,取app剩余还需要分配的cpu core数量 和 worker总共可用CPU core数量的最小值
// 如果worker资源总数不够,那么只能先分配这么多cpu core
// app.coresLeft = requestedCores - coresGranted,
// 其中requestedCores代表app需要分配多少个cpu core,coresGranted代表当前集群worker节点已经被分配了多少个core
var coresToAssign = math.min(app.coresLeft, usableWorkers.map(_.coresFree).sum)
/** Return whether the specified worker can launch an executor for this app. */
// 判断当前worker剩余 cpu core 和 memory是否能够分配给app运行
def canLaunchExecutor(pos: Int): Boolean = {
// 只要当前剩余的cpu core,还没有被分配完,这个标志位就是true
val keepScheduling = coresToAssign >= minCoresPerExecutor
// 当前worker剩余的core是否能够分配给App
val enoughCores = usableWorkers(pos).coresFree - assignedCores(pos) >= minCoresPerExecutor
// If we allow multiple executors per worker, then we can always launch new executors.
// Otherwise, if there is already an executor on this worker, just give it more cores.
// 每个worker资源足够的情况下,可以启动多个executor,
// 否则的话,就给当前启动的这个worker足够的core和memory
val launchingNewExecutor = !oneExecutorPerWorker || assignedExecutors(pos) == 0
if (launchingNewExecutor) {
// 当前节点已分配出去的内存
val assignedMemory = assignedExecutors(pos) * memoryPerExecutor
// 剩余内存是否足够分配
val enoughMemory = usableWorkers(pos).memoryFree - assignedMemory >= memoryPerExecutor
val underLimit = assignedExecutors.sum + app.executors.size < app.executorLimit
keepScheduling && enoughCores && enoughMemory && underLimit
} else {
// 在一个worker持续分配资源
keepScheduling && enoughCores
}
}
// 过滤掉资源不够的worker
var freeWorkers = (0 until numUsable).filter(canLaunchExecutor)
while (freeWorkers.nonEmpty) {
freeWorkers.foreach { pos =>
var keepScheduling = true
while (keepScheduling && canLaunchExecutor(pos)) {
// 当前worker内存和core资源足够分配给一个app
// 可用worker可用core减去已分配的core
coresToAssign -= minCoresPerExecutor
// pos节点worker已被分配出去多少core
assignedCores(pos) += minCoresPerExecutor
// If we are launching one executor per worker, then every iteration assigns 1 core
// to the executor. Otherwise, every iteration assigns cores to a new executor.
if (oneExecutorPerWorker) {
// 一个worker点一个executor
assignedExecutors(pos) = 1
} else {
// 资源足够的情况下,一个worker可以分配多个executor
assignedExecutors(pos) += 1
}
// Spreading out an application means spreading out its executors across as
// many workers as possible. If we are not spreading out, then we should keep
// scheduling executors on this worker until we use all of its resources.
// Otherwise, just move on to the next worker.
// 如果是spreadOutApps模式下,那么就每个worker在分配executor后,接着就到下一个worker上分配
// 循环分配,保证每个可用worker都可以分配到executor
// 如果不是,那么就给当前这个executor一直分配core,直到这个executor所在的节点资源
// 已经都分配完了。
if (spreadOutApps) {
keepScheduling = false
}
}
}
// 过滤掉资源不足的worker
freeWorkers = freeWorkers.filter(canLaunchExecutor)
}
assignedCores
}
从上面代码中可以看出,Master的资源调度算法主要有两个:一个是SpreadOut算法,另一个是非SpreadOut算法。区别从源码中就可以看出来,SpreadOut算法,是将executor尽可能的分配到较多的worker节点上,这样做的好处是,每个节点都能工作,防止资源浪费,而第二种就是将executor尽可能少的分配到worker,直到这个worker资源不足,才到下一个worker上分配资源。这里注意,这个算法相对老版本的算法做了优化,老版本中分配core的单位是1个,而这里则是按照spark-submit脚本中配置的–executor-cores,为单位进行分配,这里要注意。