Spark 任务调度和FIFO、FAIR调度源码分析

Application调度

Spark每个Application拥有对应的SparkContext.SparkContext维持整个应用的上下文信息。通过主节点的分配获得独立的一组Executor JVM进程执行任务。Executor空间内的不同应用之间是不共享 的,一个Executor在一个时间段内只能分配给一个应用使用。如果多用户需要共享集群资源,一般依据集群管理者的静态配置资源分配规则。

默认情况下,用户向以Standalone模式运行的Spark集群提交的应用使用FIFO(先进先 出)的顺序进行调度。每个应用会独占所有可用节点的资源。用户可以通过配置以下参数静态分配资源:

  • spark.cores.max:一个应用可以在整个集群申请的CPU core数。默认使用参数spark.deploy.defaultCores(默认是Int.Max,用程序可以使用所有当前可以获得的CPU资源)决定的可用核数。(这个参数对Yarn模式不起作用)
  • spark.executor.cores:该参数用以设置每个executor使用的CPU资源,standalone模式下是worker节点上所有可用的CPU的资源。
  • spark.executor.memory:该参数指定每个executor的内存,默认是 1G。该配置项默认单位是MB,也可以显示指定单位 (如2g,8g)
  • num-executor:executor的数量,不存在此参数,可以通过spark.cores.max/ executor-cores推导出来。

当以Yarn模式运行Spark应用时,可以配置下列参数:

  • spark.executor.cores:该参数用以设置每个executor使用的CPU资源,在 ON YARN模式下,默认是1

  • spark.executor.memory:该参数指定每个executor的内存,默认是 1G。该配置项默认单位是MB,也可以显示指定单位 (如2g,8g)

  • spark.executor.instances(–num-executors):application的executor数量,该选项默认值是2。

Spark的应用接收提交和FIFO调度的代码如下,从源码中可以看到,Master先统计可用资源,然后在waitingDrivers的队列中通过FIFO 方式为App分配资源和指定Worker启动Driver执行应用。
在这里插入图片描述

Job调度

在Spark应用程序内部,用户通过不同线程提交的Job可以并行运行,这里所说的Job就 是Spark Action(如count、collect等)算子触发的整个RDD DAG为一个Job。

FIFO模式

在默认情况下,Spark的调度器以FIFO(先进先出)方式调度Job的执行。每个Job被切分为多个Stage。第一个Job优先获取所有可用的资源,接下来第二个Job再 获取剩余资源。以此类推。如果第一个Job很大,占用所有资源,则第二个 Job就需要等待第一个任务执行完,释放空余资源,再申请和分配Job。
在这里插入图片描述

在算法执行中,先看优先级**,TaskSet的优先级是JobID**,因为先提交的JobID小,所以 就会被更优先地调度,这里相当于进行了两层排序,先看是否是同一个Job的Taskset,不同 Job之间的TaskSet先排序。

最后执行的stageId最小为0,最先应该执行的stageId最大。但是这里的调度机制是优 先调度Stageid小的。在DAGScheduler中控制Stage是否被提交到队列中,如果还有父母 Stage未执行完,则该stage的Taskset不会提交到调度池中,这就保证了虽然最先做的stage 的id大,但是排序完,由于后面的还没提交到调度池中,所以会先执行。

**每个job被触发时,都会首先进行DAGScheduler进行stage(TasksetManager)调度。stage(TasksetManager)是否加入调度池和前一个job是否占用所有资源没有关系,只与是否还有父母stage未执行完有关。**即使前一个job正在占用所有资源,下一个job的stage还是可以进入调度池,但是由于优先级jobid小于前一个job的stage,所以只能等待前一个job的stage执行结束。当一个stage(TaskSetManager)执行完毕之后,会从调度池中删除已经执行过的TaskSetManager。然后通知DAGSchedulerlooking for newly runnable stages,将其加入调度池。

FAIR模式

FAIR共享模式调度下,Spark在多Job之间以轮询(round robin)方式为任务分配资源,所有 的任务拥有大致相当的优先级来共享集群的资源。这就意味着当一个长任务正在执行时,短 任务仍可以分配到资源,提交并执行,并且获得不错的响应时间。用户可以通过配置 spark.scheduler.mode方式来让应用以FAIR模式调度。

FAIR调度器支持将Job分组加入 调度池中调度,用户可以同时针对不同优先级对每个调度池配置不同的调度权重。当使用Fair调度模式时,会在rootPool调度池下创建子pool。子pool的创建根据配置文件来创建,默认FIFO模式。
如果配置文件中没有name为default的调度池,则会自动创建default pool。在默认情况下,每个调度池拥有相同的优先级来共享整个集群的资源。

在没有外部干预的情况下,新提交的任务放入default pool中进行调度。如果用户也可 以自定义调度池,通过在SparkContext中配置参数spark.scheduler.pool创建调度池。
在这里插入图片描述
这样配置了这个参数的线程每次提交的任务都是放入这个池中进行调度。如果用户不想再使用这个调度池,可以通过调用 SparkContext的方法来终止这个调度池的使用。
在这里插入图片描述

配置调度池

用户可以通过配置文件自定义调度池的属性。每个调度池支持下面3个配置参数。

  • 调度模式(schedulingMode):用户可以选择FIFO或者FAIR方式进行调度。
  • 权重(Weight):这个参数控制在整个集群资源的分配上,这个调度池相对其他调 度池优先级的高低。例如,如果用户配置一个指定的调度池权重为3,那么这个调度池将会 获得相对于权重为1的调度池3倍的资源。
  • minShare:配置minShare参数(这个参数代表多少个CPU核),这个参数决定整体 调度的调度池能给待调度的调度池分配多少资源就可以满足调度池的资源需求,剩余的资源 还可以继续分配给其他调度池。

用户可以通过conf/fairscheduler.xml文件配置调度池的属性,同时需要在程序的
SparkConf对象中配置属性。
在这里插入图片描述

在这里插入图片描述

FIFO和FAIR调度源码分析

TaskScheuduler.initialize

TaskScheuduler首先初始化调度池,根绝spark.scheduler.mode调度模式创建不同的调度池,默认FIFO。

def initialize(backend: SchedulerBackend) {
    this.backend = backend   
    rootPool = new Pool("", schedulingMode, 0, 0)
    schedulableBuilder = {
      schedulingMode match {
        case SchedulingMode.FIFO =>
          new FIFOSchedulableBuilder(rootPool)
        case SchedulingMode.FAIR =>
          new FairSchedulableBuilder(rootPool, conf)
      }
    }
    schedulableBuilder.buildPools()
  }
SchedulableBuilder

FIFO
FIFO模式什么都不做,实现默认的schedulerableBUilder方法,建立的调度池也为空,addTasksetmaneger也是调用默认的;

private[spark] class FIFOSchedulableBuilder(val rootPool: Pool)
  extends SchedulableBuilder with Logging {

  override def buildPools() {
    // nothing
  }

  override def addTaskSetManager(manager: Schedulable, properties: Properties) {
    rootPool.addSchedulable(manager)
  }
}

Fair
fair模式则重写了buildpools的方法,读取默认路径 $SPARK_HOME/conf/fairscheduler.xml文件,也可以通过参数spark.scheduler.allocation.file设置用户自定义配置文件。根据配置文件,创建子pool添加到rootPool当中(rootPool.addSchedulable)。子pool的创建根据配置文件来创建,默认FIFO模式。
如果配置文件中没有name为default的调度池,则会自动创建default pool。

override def buildPools() {
  var fileData: Option[(InputStream, String)] = None
  try {
    fileData = schedulerAllocFile.map { f =>
      val fis = new FileInputStream(f)
      logInfo(s"Creating Fair Scheduler pools from $f")
      Some((fis, f))
    }.getOrElse {
      val is = Utils.getSparkClassLoader.getResourceAsStream(DEFAULT_SCHEDULER_FILE)
      if (is != null) {
        logInfo(s"Creating Fair Scheduler pools from default file: $DEFAULT_SCHEDULER_FILE")
        Some((is, DEFAULT_SCHEDULER_FILE))
      } else {
        logWarning("Fair Scheduler configuration file not found so jobs will be scheduled in " +
          s"FIFO order. To use fair scheduling, configure pools in $DEFAULT_SCHEDULER_FILE or " +
          s"set $SCHEDULER_ALLOCATION_FILE_PROPERTY to a file that contains the configuration.")
        None
      }
    }

    fileData.foreach { case (is, fileName) => buildFairSchedulerPool(is, fileName) }
  } catch {
    case NonFatal(t) =>
      val defaultMessage = "Error while building the fair scheduler pools"
      val message = fileData.map { case (is, fileName) => s"$defaultMessage from $fileName" }
        .getOrElse(defaultMessage)
      logError(message, t)
      throw t
  } finally {
    fileData.foreach { case (is, fileName) => is.close() }
  }
    // finally create "default" pool
  buildDefaultPool()
}
private def buildFairSchedulerPool(is: InputStream, fileName: String) {
  val xml = XML.load(is)
  for (poolNode <- (xml \\ POOLS_PROPERTY)) {

    val poolName = (poolNode \ POOL_NAME_PROPERTY).text

    val schedulingMode = getSchedulingModeValue(poolNode, poolName,
      DEFAULT_SCHEDULING_MODE, fileName)
    val minShare = getIntValue(poolNode, poolName, MINIMUM_SHARES_PROPERTY,
      DEFAULT_MINIMUM_SHARE, fileName)
    val weight = getIntValue(poolNode, poolName, WEIGHT_PROPERTY,
      DEFAULT_WEIGHT, fileName)

    rootPool.addSchedulable(new Pool(poolName, schedulingMode, minShare, weight))

    logInfo("Created pool: %s, schedulingMode: %s, minShare: %d, weight: %d".format(
      poolName, schedulingMode, minShare, weight))
  }
}

fair模式也重写了addtaskmanager方法。

override def addTaskSetManager(manager: Schedulable, properties: Properties) {
  val poolName = if (properties != null) {
      properties.getProperty(FAIR_SCHEDULER_PROPERTIES, DEFAULT_POOL_NAME)
    } else {
      DEFAULT_POOL_NAME
    }
  var parentPool = rootPool.getSchedulableByName(poolName)
  if (parentPool == null) {
    // we will create a new pool that user has configured in app
    // instead of being defined in xml file
    parentPool = new Pool(poolName, DEFAULT_SCHEDULING_MODE,
      DEFAULT_MINIMUM_SHARE, DEFAULT_WEIGHT)
    rootPool.addSchedulable(parentPool)
    logWarning(s"A job was submitted with scheduler pool $poolName, which has not been " +
      "configured. This can happen when the file that pools are read from isn't set, or " +
      s"when that file doesn't contain $poolName. Created $poolName with default " +
      s"configuration (schedulingMode: $DEFAULT_SCHEDULING_MODE, " +
      s"minShare: $DEFAULT_MINIMUM_SHARE, weight: $DEFAULT_WEIGHT)")
  }
  parentPool.addSchedulable(manager)
  logInfo("Added task set " + manager.name + " tasks to pool " + poolName)
}

在rootPool找到对应的子pool,调用子pool的addSchedulable方法,将TaskSetManager添加到子pool中的调度队列 schedulableQueue当中。(FIFO中是调用rootPool的addSchedulable方法)。

override def addSchedulable(schedulable: Schedulable) {
  require(schedulable != null)
  schedulableQueue.add(schedulable)
  schedulableNameToSchedulable.put(schedulable.name, schedulable)
  schedulable.parent = this
}
Pool.getSortedTaskSetQueue

Pool类中的getSortedTaskSetQueue就是用来获取调度池中排序过后的TaskSet。

override def getSortedTaskSetQueue: ArrayBuffer[TaskSetManager] = {
  val sortedTaskSetQueue = new ArrayBuffer[TaskSetManager]
  val sortedSchedulableQueue =
    schedulableQueue.asScala.toSeq.sortWith(taskSetSchedulingAlgorithm.comparator)
  for (schedulable <- sortedSchedulableQueue) {
    sortedTaskSetQueue ++= schedulable.getSortedTaskSetQueue
  }
  sortedTaskSetQueue
}

通过SchedulingAlgorithm的comparator对schedulableQueue进行排序。schedulableQueue中元素的类型为Schedulable,其有两个子类:Pool和TaskSetManager。FIFO的rootPool的schedulableQueue中元素的TaskSetManager。Fair的rootPool的schedulableQueue中元素为子pool,子pool中的schedulableQueue中元素为TaskSetManager。

所以在Fair模式,会进行两次排序。先对rootPool中的子pool进行排序(排序算法为FairSchedulingAlgorithm),然后对每个子pool中的TaskSetManager再进行排序(排序算法由子pool的调度模式决定)。

SchedulingAlgorithm

SchedulingAlgorithm的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
    }
  }
}

调度规则:

  1. 对s1和s2两个可调度任务的优先级(jobID)进行比较,其中优先级的值越小表示优先级越高;
  2. 当s1和s2的优先级值相等的时候,进一步比较s1和s2所属的Stage的身份标识,stageId小的,优先级更高;
  3. 如果s1的优先级值小于s2的优先级值,则优先调度s1,否则调度s2;

SchedulingAlgorithm的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
    }
  }
}

调度规则:

  1. 如果s1中处于运行状态的Task的数量小于s1的minShare,并且s2中处于运行状态的Task的数量大于s2的minShare,则优先调度s1;
  2. 如果s1中处于运行状态的Task的数量大于等于s1的minShare,并且s2中处于运行状态的Task的数量小于s2的minShare,则优先调度s2;
  3. 如果s1中处于运行状态的Task的数量小于s1的minShare,并且s2中处于运行状态的Task的数量也小于s2的minShare,那么进一步对minShareRatio1和minShareRatio2进行比较。如果minShareRatio1小于minShareRatio2,则优先调度s1;反之,调度s2。如果minShareRatio1和minShareRatio2相等,则进行s1和s2的名字比较,小的优先进行调度;minShareRatio:正在运行的任务数量和minShare数量之间的比值
  4. 如果s1中处于运行状态的Task的数量大于等于s1的minShare,并且s2中处于运行状态的Task的数量大于等于s2的minShare,那么再对taskToWeightRatio1和taskToWeightRatio2进行比较。较小值,优先调度。如果taskToWeightRatio1和taskToWeightRatio2相等,还需要对s1和s2的名字进行比较,较小者,优先调度。taskToWeightRatio:正在运行的任务数量与任务权重之间的比值

对于poo中的TaskSetManager排序:由于TaskSetManager其minshare都为0,所以Fair模式下TaskSetManager进行排序主要根据runTasks/weight,所以可以实现公平的调度。

TaskSetManager调度

Stage划分

Stage的调度是由DAGScheduler完成的。由RDD的有向无环图DAG切分出了Stage的有 向无环图DAG。Stage的DAG通过最后执行的Stage为根进行广度优先遍历,遍历到最开始执 行的Stage执行,如果提交的Stage仍有未完成的父母Stage,则Stage需要等待其父Stage执 行完才能执行。

同时DAGScheduler中还维持了几个重要的Key-Value集合结构,用来记录 Stage的状态,这样能够避免过早执行和重复提交Stage。**waitingStages中记录仍有未执行 的父母Stage,防止过早执行。runningStages中保存正在执行的Stage,防止重复执行。 failedStages中保存执行失败的Stage,**需要重新执行,这里的设计是出于容错的考虑。

在TaskScheduler中将每个Stage中对应的任务进行提交分和调度。:一个应用对应 一个TaskScheduler,也就是这个应用中所有Action触发的Job中的TaskSetManager都是由这 个TaskScheduler调度的。

TaskSetManager调度

每个Stage对应的一个 TaskSetManager通过Stage回溯到最源头缺失的Stage提交到调度池pool中,在调度池中, 这些TaskSetMananger又会根据Job ID排序,先提交的Job的TaskSetManager优先调度,然 后一个Job内的TaskSetManager ID小的先调度,并且如果有未执行完的父母Stage的 TaskSetManager,则是不会提交到调度池中。

Task调度

整体的Task分发由TaskSchedulerImpl来实现,但是Task的调度(本质上是Task在哪个 分区执行)逻辑由TaskSetManager完成。这个类监控整个任务的生命周期,当任务失败时 (如执行时间超过一定的阈值),重新调度,也会通过delay scheduling进行基于位置感知 (locality-aware)的任务调度。TaskSchedulerImpl类有几个主要接口:接口 resourceOffer,作用为判断任务集合是否需要在一个节点上运行。接口statusUpdate,其主 要作用为更新任务状态。

任务的locality由以下两种方式确定。

  1. RDD DAG源头有HDFS等类型的分布式存储,它们内置的数据本地性决定(RDD中 配置preferred location确定)数据存储位置和分区的选取。
  2. 每个其他非源头Stage由于都要进行Shuffle,所以地址以在resourceoffer中进行 round robin来确定,初始提交Stage时,将prefer的位置设置为Nil。但在Stage调度过程中, 内部是通过Narrow dep的祖先Stage确定最佳执行位置的。这样相当于每个RDD的分区都有 prefer执行位置。

任务调度的整个过程(个人理解,可能有误)

  1. 每次DAGScheduler提交任务submitTask时,会调用TaskSchduler的submitTask。TaskSchduler会将其包装为TaskSetManager(一个Stage对应一个TaskSet对应一个TaskSetManager),加入调度池(如果父stage未完成,子stage不会进行调度),然后调用CoarseGrainedSchedulerBackend.reviveOffers()的方法,给DriverEndPoint发送ReviveOffers信息。

  2. DriverEndPoint在receive方法中接收该消息,并调用makeOffers()方法生成资源offer(每一个executor的freeCores生成一个WorkerOffer,意味着executor的空闲资源),然后makeOffers中调用CoarseGrainedSchedulerBackend.resourceOffers,为每一个资源offer分派Task。

  3. resourceOffers中调用rootPool.getSortedTaskSetQueue获得排序之后的任务。将这些任务分派给资源offer。最后调用DriverEndPoint的launchTasks,将分配的Task分发到对应的资源offer上进行执行。没有分派到资源的Task将继续等待。

  4. 当某个Task执行完毕时,会首先调度之前没有获得资源的Task(根据实验的得来,一个Task执行完毕之后会首先从目前的调度池中获取一个Task去执行,然后再判断是否有新的Task加入调度池。),然后通知DAGScheduler某个Task执行完毕,判断此TaskSetManager是否全部执行完成。是,则在调度池中删除该TaskSetManager,并将下一stage加入调度池,进行新一轮的任务调度排序(之前没有分派到资源的等待Task将和新加入的TaskSetManager重新排序)。

参考:
《Spark大数据处理:技术、应用与性能优化》
https://www.imooc.com/article/266855
https://www.imooc.com/article/266854

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值