spark源码学习(三)- TaskSchedular 提交taskset过程

背景

     以前一直以为task本地行,将task分配到哪一个executor上是taskschedular做的事情,知道看了DagSchedular和TaskSchedular之后才知道,DagSchedular是负责上层调度,task具体运行在那个executor上,而TaskSchedular是决定task在ececutor上是process级别的本地行还是memory级别的本地行,因为在DagSchedular会有在sparkcontext初始化时候创建的blockManager对象,所以DagSchedular是可以获取rdd.partition缓存信息的,并且决定在那个executor上运行。

     DagSchedular负责为task分配cpu等资源信息,并发送任务到executor上,coarseDrainExecutorBackend中的receive方法就会调用launchTask 运行task,该executor是在sparkcontext初始化的时候从main函数启动的,SchrdularBackEnd拥有所有executor上的cores信息,并会把该信息传递给DagSchedular。


过程

1.起点:taskScheduler.submitTasks()

      DagSchedular进行上层stage划分之后,将stage转换成tasksets,并且向TaskSchedular进行提交,注意,在提交tasksets的时候,DagSchedular已经完成了上层task任务运行的本地性调度,比如某一个task应该在哪个executor上运行,而TaskSchedular是进行底层的本地性,比如:线程级别、内存级别和其他级别。因为DagSchedular内部含有一个blockManager对象,可以获得block信息(rdd)的缓存情况,所以executor级别的本地性是由DagSchedular完成的

taskScheduler.submitTasks(new TaskSet(tasks.toArray, stage.id, stage.latestInfo.attemptId, jobId, properties))

       submit方法如下所示,核心代码由三部分,一个tasksetManager创建,然后是将tasksetManager加入schdularBuilder队列,schdularBuilder队列是在TaskSchdular创建初始化的时候实例化的,存在两种实现:FIFO和FAIR。最后一段核心代码就是使用backend发送触发信息,该触发信号主要是通知SchedularBackend 获取现有的资源,调用TaskSchedular使用资源分配task

override def submitTasks(taskSet: TaskSet) {
    val tasks = taskSet.tasks
    logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks")
    this.synchronized {

      //核心代码1  创建TasksetManager , 创建方式  new create
      val manager = createTaskSetManager(taskSet, maxTaskFailures)



      //省略代码1,conflect manager检测
      

      //核心代码2   将taskSetManager添加进队列  schedulableBuilder初始化注意一下
      schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)

      //此处省略一部分代码2


    }
    //核心代码3  发送触发器
    backend.reviveOffers()
  }

2.SchedularBackend.reviveOffers() 转发消息

     这里的资源信息获取,只是获取已有资源信息的详情,而不是向master申请资源,简单的原理就是遍历现有executor,收集所有可用的cores信息,因为在DagSchedular阶段就已经将task分配到了具体的executor,所以我们只需要遍历一下tasksetManager中的executorId列表,然后去获取一下就行了,具体获取操作是backend对象去执行的。

     reviveOffers代码如下,其中driverEndPoint是SchedularBackEnd一个内部类的实例,负责与executor(包含本机)进行交互,值得注意的是本次函数调用,是Driver节点,也就是本节点,自己调用的driverEndpoint.send方法给自己,在方法内部有很多case条件,可以区分是本机发送的还是executor发送的信息。

override def reviveOffers() {
    driverEndpoint.send(ReviveOffers)
  }

     DriverEndPoint receive接收代码如下:

override def receive: PartialFunction[Any, Unit] = {
      
      //省略其他case

      case ReviveOffers =>
        //本次case
        makeOffers()

      //省略其他case
    }

3.SchedularBackEnd.DriverEndPoint.makeOffers().资源信息获取

    该函数主要作用在于收集现有executor的资源信息,并且将资源信息反向传递给TaskSchedular对象,由TaskSchedular对象进行具体的任务资源匹配。

private def makeOffers() {
      // Make sure no executor is killed while some task is launching on it
      val taskDescs = CoarseGrainedSchedulerBackend.this.synchronized {

        // 获取可用活跃节点和该节点的资源信息:闲置cpu数目
        val activeExecutors = executorDataMap.filterKeys(executorIsAlive)
        val workOffers = activeExecutors.map { case (id, executorData) =>
          new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
        }.toIndexedSeq

        //调用resourceOffers 进行任务资源分配
        scheduler.resourceOffers(workOffers)
      }
      if (!taskDescs.isEmpty) {
        launchTasks(taskDescs)
      }
    }


4.TaskSchedular.resourceOffers() 回调,使用资源信息分配给task

      此段函数主要作用:

      a.打乱核心资源信息,出发点为了负载均衡

      b.从shecularBuilder队列中获取datsetsManager,以某种策略(FIFO/FAIR)

      c.循环分配资源,resourceOffserSingleTask函数,会遍历一遍executor,分配任务

def resourceOffers(offers: IndexedSeq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized {
    
    //省略一段代码,用于检测是否有新的executor或者需要更新信息

    
    //省略代码,黑名单node节点检测


    //负载金衡操作,打乱executor资源顺序
    val shuffledOffers = shuffleOffers(filteredOffers)
    // Build a list of tasks to assign to each worker.
    val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))
    val availableCpus = shuffledOffers.map(o => o.cores).toArray

    //从队列里获取tasksetManager,该队列是schedulableBUulder创建的
    val sortedTaskSets = rootPool.getSortedTaskSetQueue
   

    //省略代码,将上面检测出的新的executor节点,添加到tasksetManager

    
    //核心代码分配任务资源
    for (taskSet <- sortedTaskSets) {
      var launchedAnyTask = false
      var launchedTaskAtCurrentMaxLocality = false
      for (currentMaxLocality <- taskSet.myLocalityLevels) {
        do {
          launchedTaskAtCurrentMaxLocality = resourceOfferSingleTaskSet(
            taskSet, currentMaxLocality, shuffledOffers, availableCpus, tasks)
          launchedAnyTask |= launchedTaskAtCurrentMaxLocality
        } while (launchedTaskAtCurrentMaxLocality)
      }
      if (!launchedAnyTask) {
        taskSet.abortIfCompletelyBlacklisted(hostToExecutors)
      }
    }

    if (tasks.size > 0) {
      hasLaunchedTask = true
    }
    return tasks
  }

     其中遍历每一个taskSet,并按照LocalityLevels从高到低尝试本地性优先级,resourceOffersSingleTaskSet按照当前遍历到的taskset和本地性条件,分配资源,这段代码里的do-while的作用会在后面提到。

for (taskSet <- sortedTaskSets) {
      var launchedAnyTask = false
      var launchedTaskAtCurrentMaxLocality = false
      for (currentMaxLocality <- taskSet.myLocalityLevels) {
        //为什么需要do-while呢???  后面会提到
        do {       
            launchedTaskAtCurrentMaxLocality = resourceOfferSingleTaskSet(
            taskSet, currentMaxLocality, shuffledOffers, availableCpus, tasks)
          launchedAnyTask |= launchedTaskAtCurrentMaxLocality
        } while (launchedTaskAtCurrentMaxLocality)
      }

5.TaskSchedular.resourceOfferSingleTaskSet()

    这段函数中遍历每一个exrcutor,然后遍历符合当前本地性条件的taskset,但是因为resourceOffers只返回了一个taskset,所以这个函数调用的作用就是,为每一个executor 分配之多一个taskset,这是为了负载均衡,所以调用调用一个该函数,遍历所有的executor,但是不会把所有可分配的task都分配出去,上文中do-while的作用就在于此,每次之多给executor分配一个task,循环多次,保证负载均衡。

private def resourceOfferSingleTaskSet(
      taskSet: TaskSetManager,
      maxLocality: TaskLocality,
      shuffledOffers: Seq[WorkerOffer],
      availableCpus: Array[Int],
      tasks: IndexedSeq[ArrayBuffer[TaskDescription]]) : Boolean = {
    var launchedTask = false


    //遍历每一个executor
    for (i <- 0 until shuffledOffers.size) {
      val execId = shuffledOffers(i).executorId
      val host = shuffledOffers(i).host
      if (availableCpus(i) >= CPUS_PER_TASK) {
        try {

          //贼神奇的代码,这个for循环之循环一次,因为resourceOffer值返回了一个taskset
          for (task <- taskSet.resourceOffer(execId, host, maxLocality)) {
            tasks(i) += task
            val tid = task.taskId
            taskIdToTaskSetManager(tid) = taskSet
            taskIdToExecutorId(tid) = execId
            executorIdToRunningTaskIds(execId).add(tid)
            availableCpus(i) -= CPUS_PER_TASK
            assert(availableCpus(i) >= 0)
            launchedTask = true
          }
        } catch {
          case e: TaskNotSerializableException =>
            logError(s"Resource offer failed, task set ${taskSet.name} was not serializable")
            // Do not offer resources for this task, but don't throw an error to allow other
            // task sets to be submitted.
            return launchedTask
        }
      }
    }
    return launchedTask
  }




总结

额外资料:

其他人写的这方面的文章,下面这篇幅挺好的

https://www.jianshu.com/p/ba11e9aef0a6

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值