36 Spark之TaskScheduler Spark shell案例运行日志详解、TaskScheduler和SchedulerBackend、FIFO与FAIR、Task运行时本地性算法详

本节课内容:

1.     TaskSchedulerBackend与SchedulerBackend

2.     FIFO与FAIR两种调度模式

3.     Task数据本地性资源的分配


一、Scheduler运行过程(Spark-shell角度)

1.启动Spark-shell

      

 

当我们spark-shell本身的时候命令终端返回来的主要是ClientEndpoint和SparkDeploySchedulerBakcend。这是因为此时还没有任何应用程序Job的触发,这是启动Application本身而已,所以主要就是实例化SparkContext并注册当前的应用程序给Master,并从集群中获得ExecutorBackend的计算资源;(这就是为什么启动时日志没有DriverEndpoint信息的原因,因为此时应用程序内部还未发生具体计算资源的调度)

2.TaskScheduler运行时机

       DAGScheduler划分好Stage后,会通过TaskSchedulerImpl中的TaskSetManager来管理当前要运行的Stage中的所有的任务TaskSet,TaskSetManager会根据locality aware来为Task奉陪计算资源,监控Task的执行状态。(例如重试、慢任务以及进行推测式执行等)

 

二、TaskScheduler与SchedulerBackend

      1.底层调度的总流程

(1)TaskScheduler提交Tasks

       TaskScheduler#submitTasks方法主要作用是将TaskSet加入到TaskSetManager中进行管理。

override def submitTasks(taskSet: TaskSet) {
    val tasks = taskSet.tasks
    logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks")
    this.synchronized {
	//创建TaskSetManager,并设置最大失败重试次数
      val manager = createTaskSetManager(taskSet, maxTaskFailures)
      val stage = taskSet.stageId
	//记录Stage中提交的TaskSetManager
      val stageTaskSets =
        taskSetsByStageIdAndAttempt.getOrElseUpdate(stage, new HashMap[Int, TaskSetManager])
      stageTaskSets(taskSet.stageAttemptId) = manager
	//如果重复提交同一个TaskSet或者Tasks不在当前的TaskSet中则会报错
      val conflictingTaskSet = stageTaskSets.exists { case (_, ts) =>
        ts.taskSet != taskSet && !ts.isZombie
      }
      if (conflictingTaskSet) {
        throw new IllegalStateException(s"more than one active taskSet for stage $stage:" +
          s" ${stageTaskSets.toSeq.map{_._2.taskSet.id}.mkString(",")}")
      }
	//添加TaskManager到调度队列中,schedulableBuilder是应用程序级别的调度器
      schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)<span style="white-space:pre">	</span>//1 
	//为慢任务启动备份任务
      if (!isLocal && !hasReceivedTask) {
        starvationTimer.scheduleAtFixedRate(new TimerTask() {
          override def run() {
            if (!hasLaunchedTask) {
              logWarning("Initial job has not accepted any resources; " +
                "check your cluster UI to ensure that workers are registered " +
                "and have sufficient resources")
            } else {
              this.cancel()
            }
          }<pre name="code" class="plain"> // default scheduler is FIFO
	  private val schedulingModeConf = conf.get("spark.scheduler.mode", "FIFO")  (TaskSchedulerImpl)

}, STARVATION_TIMEOUT_MS, STARVATION_TIMEOUT_MS) } hasReceivedTask = true }//调用SparkDeploySchedulerBackend分配具体计算资源 backend.reviveOffers() //2 }

 Task执行提交失败后会重试,Task的默认重试次数为4次。 

def this(sc: SparkContext) = this(sc, sc.conf.getInt("spark.task.maxFailures", 4))  (TaskSchedulerImpl)

(2)添加TaskSetManager

       SchedulerBuilder(根据SchedulerMode的不同,FIFO与FAIR实现不同)#addTaskSetManger方法会确定TaskSetManager的调度顺序,然后按照TaskSetManager的locality aware来确定每个Task具体运行在那个ExecutorBackend中。

默认的调度顺序为FIFO;Spark应用程序目前支持两种调度模式FIFO和FAIR可以通过Spark-env.sh中的Spark.Scheduler.mode来进行具体的设置

 // default scheduler is FIFO
  private val schedulingModeConf = conf.get("spark.scheduler.mode", "FIFO")  (TaskSchedulerImpl)

//在1处调用addTaskSetManager
def addTaskSetManager(manager: Schedulable, properties: Properties)


//FIFO模式下的,addTaskSetManager
override def addTaskSetManager(manager: Schedulable, properties: Properties) {
    rootPool.addSchedulable(manager)
  }

//直接将可调度对象TaskSetManager加入SchedulerQueue的尾端。
override def addSchedulable(schedulable: Schedulable) {
    require(schedulable != null)
    schedulableQueue.add(schedulable)
    schedulableNameToSchedulable.put(schedulable.name, schedulable)
    schedulable.parent = this
  }

//FAIR模式下,addTaskSetManager
override def addTaskSetManager(manager: Schedulable, properties: Properties) {
    var poolName = DEFAULT_POOL_NAME
	//获得根节点的默认调度池的引用
    var parentPool = rootPool.getSchedulableByName(poolName)
    if (properties != null) {
	//根据优先级获得父可调度对象pool的引用。
      poolName = properties.getProperty(FAIR_SCHEDULER_PROPERTIES, DEFAULT_POOL_NAME)
      parentPool = rootPool.getSchedulableByName(poolName)
      if (parentPool == null) {
        //如果父可调度对象不存在,则根据应用程序配置信息创建之
        parentPool = new Pool(poolName, DEFAULT_SCHEDULING_MODE,
          DEFAULT_MINIMUM_SHARE, DEFAULT_WEIGHT)
		//最为根节点default pool的孩子加入default pool中
        rootPool.addSchedulable(parentPool)
        logInfo("Created pool %s, schedulingMode: %s, minShare: %d, weight: %d".format(
          poolName, DEFAULT_SCHEDULING_MODE, DEFAULT_MINIMUM_SHARE, DEFAULT_WEIGHT))
      }
    }
	
	//与FIFO类似,在每个父pool中采用队列形式,将TaskSetManager加入队尾。
    parentPool.addSchedulable(manager)
    logInfo("Added task set " + manager.name + " tasks to pool " + poolName)
  }

(1)   CoarseGrainedSchedulerBackend分配资源

CoarseGrainedSchedulerBackend#reviveOffers方法,给DriverEndpoint发送ReviveOffers消息;ReviveOffers本身是一个空的case object对象,只是起到触发底层调度的左右,在有Task提交或者资源变动时,就会发送ReviveOffers消息。每提交一个Stage都要申请一次资源,发送一个ReviveOffers消息。

	//3处调用的方法
 override def reviveOffers() {
    driverEndpoint.send(ReviveOffers)
  }

ReviveOffers相当于触发器,在资源变化时触发。

TaskScheduler要负责为Task分配计算资源(分配的是程序启动时向Master申请的集群资源),根据计算的本地性原则确定Task具体要运行在哪个ExecutorBackend中。

(4)接收ReviveOffers消息与分配资源

       在DriverEndpoint接收ReviveOffers消息并路由到MakeOffers方法中;在MakeOffers方法中首先准备好所有可用于计算的workOffers(代表了应用程序从Master获得的Executor中所有可用的Core信息)。

//CoarseGrainedSchedulerBackend.DriverEndpoint#receive
override def receive: PartialFunction[Any, Unit] = {
	//省略部分代码
      case ReviveOffers =>
        makeOffers()
}


 //在逻辑上,让所有Executor都成为计算资源的提供者
    private def makeOffers() {
      // 过滤掉挂掉的Executor
      val activeExecutors = executorDataMap.filterKeys(executorIsAlive)
      //生成有所有aliver的Executor元信息组成的序列
	val workOffers = activeExecutors.map { case (id, executorData) =>
        new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
      }.toSeq
	//生成资源分配的二位数组,并以此为基础进行Tasks加载、执行
      launchTask(scheduler.resourceOffers(workOffers)) // 3  4
    }

       (a)resourceOffers方法

       调用TaskSchedulerImpl#resourceOffers方法,为每一个Task具体分配计算资源,其输入是ExecutorBackend机器上可用的cores,输出是TaskDescription二维数组,在其中定义了每个Task具体运行在在哪个ExecutorBackend。

 

//3处调用resourceOffers,该方法输入为一个Executor的列表,输出为一个
//TaskDescription的二位数组
def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized {
    // 每个slave节点为alive并且记录其hostname
    // 如果有新的slave节点加入,对其进行追踪。
    var newExecAvail = false
    for (o <- offers) {
      executorIdToHost(o.executorId) = o.host
      executorIdToTaskCount.getOrElseUpdate(o.executorId, 0)
      if (!executorsByHost.contains(o.host)) {
		//如果有新的Executors即新的slave节点加入
        executorsByHost(o.host) = new HashSet[String]()
	//通知DAGScheduler添加Executors
        executorAdded(o.executorId, o.host)
		//标记有新的Executor可用
        newExecAvail = true
      }
	//更新可用的节点信息
      for (rack <- getRackForHost(o.host)) {
        hostsByRack.getOrElseUpdate(rack, new HashSet[String]()) += o.host
      }
    }

    //利用随机打乱offers的方式(round-robin manner)分配计算资源Executor,避免了Task
	//集中分配到某些机器上。
    val shuffledOffers = Random.shuffle(offers)
    //为每一个worker创建一个tasks分配的列表,参见下图
    val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))
    val availableCpus = shuffledOffers.map(o => o.cores).toArray
	//获取按照调度策略排好序的TaskSetManager
    val sortedTaskSets = rootPool.getSortedTaskSetQueue
    for (taskSet <- sortedTaskSets) {
      logDebug("parentName: %s, name: %s, runningTasks: %s".format(
        taskSet.parent.name, taskSet.name, taskSet.runningTasks))
      if (newExecAvail) {
	//如果有新的slave中Executor可用,需要重新计算该TaskSetManager的就近原则
        taskSet.executorAdded()
      }
    }

    //为从rootPool里获取的TaskSetManager列表分配资源。分配的原则是就近原则,优先分配顺
    //序为PROCESS_LOCAL、NODE_LOCAL、NO_PREF、RACK_LOCAL、ANY
    var launchedTask = false
    for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels){   //7
      do {
        launchedTask = resourceOfferSingleTaskSet(
            taskSet, maxLocality, shuffledOffers, availableCpus, tasks)
      } while (launchedTask)
    }

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

  图36-1 worker与tasks及availableCpus对应关系

TaskDescription中以确定好Task具体运行在那个ExecutorBackend上;而确定Task具体运行在那个ExecutorBackend的算法是由TaskSetManager的resourceOffers方法决定。


//为每一个TaskSetManager分配资源
private def resourceOfferSingleTaskSet(
      taskSet: TaskSetManager,
      maxLocality: TaskLocality,
      shuffledOffers: Seq[WorkerOffer],
      availableCpus: Array[Int],
      tasks: Seq[ArrayBuffer[TaskDescription]]) : Boolean = {
    var launchedTask = false
	//顺序遍历当前存在的Executor
    for (i <- 0 until shuffledOffers.size) {
	//获得Executor的Id和hostname
      val execId = shuffledOffers(i).executorId
      val host = shuffledOffers(i).host
	//该Executor可以被分配任务核心实现,通过调用TaskSetManager来为Executor分配Task
      if (availableCpus(i) >= CPUS_PER_TASK) {
		//保证可用的Cores个数不小于Task运行所需的最小cores个数,CPUS_PER_TASK默认
		//为1
        try {
		//获取最高级别的本地性,并记录executor与Task的运行对应关系
          for (task <- taskSet.resourceOffer(execId, host, maxLocality)^) {
            tasks(i) += task
            val tid = task.taskId
            taskIdToTaskSetManager(tid) = taskSet
            taskIdToExecutorId(tid) = execId
            executorIdToTaskCount(execId) += 1
            executorsByHost(host) += execId
            availableCpus(i) -= CPUS_PER_TASK
            assert(availableCpus(i) >= 0)
            launchedTask = true
          }
        } catch {
          //Task未进行序列化
          case e: TaskNotSerializableException =>
            logError(s"Resource offer failed, task set ${taskSet.name} was not serializable")
            return launchedTask
        }
      }
    }
	//返回信息:Task是否已分配好资源。
    return launchedTask
  }

resourceOffers算法思想

ResourceOffers确定了Task具体运行在哪个ExecutorBackend上的。算法实现具体如下:

a)     通过Random#shuffle,将计算资源重新洗牌,以寻求计算子奥运的负载均衡。

b)     根据每个ExecutorBackend的cores个数,声明类型为TaskDescription的ArrayBuffer数组。

c)     如果有新的ExecutorBackend分配给我们的Job,此时会调用ExecutorAdded来获得完整的可用的计算资源。

这里说一下,数据本地性的级别(Locality level)的由高到低优先级的次序:PROCESS_LOCAL、NODE_LOCAL、NO_PREF、RACK_LOCAL、ANY,其中NO_PREF是指机器本地性。RACK_LOCAL是机架本地性。

d)     通过下述代码追踪最高级别的的本地性。(见7)

for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) {

每个Task默认采用一个线程来进行计算的。

// 执行Task默认需要一个cores,即一个线程。
val CPUS_PER_TASK = conf.getInt("spark.task.cpus", 1)
通过调用用TaskSetManager#resourceOffer最终确定每个Task具体运行在哪个ExecutorBackend以及具体的Localitylevel。

//5处被调用 TaskSetManager#resourceOffer
def resourceOffer(
      execId: String,
      host: String,
      maxLocality: TaskLocality.TaskLocality)
    : Option[TaskDescription] =
  {//不是僵尸TaskSet,即还可以提交Task的TaskSet
    if (!isZombie) {
      val curTime = clock.getTimeMillis()
     //获得当前的最大本地性级别
      var allowedLocality = maxLocality
     //如果最大本地性级别不是机器本地性
      if (maxLocality != TaskLocality.NO_PREF) {
		//重新计算当前时间节点的最高本地性级别,由于存在延迟调度,所以我们需要根据基于等
	//待时间的延迟调度算法来获取当前的本地性。
        allowedLocality = getAllowedLocalityLevel(curTime)
        if (allowedLocality > maxLocality) {
          //如果得到的加载Task本地性低于原来的maxLocality,则将Task加载的本地性置为
//maxLocality
          allowedLocality = maxLocality
        }
      }
	//根据不同的Task的本地性级别进行不同的处理。
      dequeueTask(execId, host, allowedLocality) match {
		//index表示Task在TaskSet中的下标,taskLocality:本地性,speculative:表示是否
		//是投机产生的,由于其他Task已排定,进而确定该Task。
        case Some((index, taskLocality, speculative)) => {
          // 为Task找到一个executor(也可以认为是为当前executor找到了TaskSet中一个
		//Task),对Task返回信息进行一些登记处理
		//在TaskSet中找到这个Task
          val task = tasks(index)
		//创建Task的id
          val taskId = sched.newTaskId()
          // Do various bookkeeping (???)
          copiesRunning(index) += 1
		//设置尝试提交次数
          val attemptNum = taskAttempts(index).size
		//实例化Task的元信息
          val info = new TaskInfo(taskId, index, attemptNum, curTime,
            execId, host, taskLocality, speculative)
          taskInfos(taskId) = info
          taskAttempts(index) = info :: taskAttempts(index)
          // 为延迟调度策略更新本地性级别
          // NO_PREF不会影响延迟调度相关的变量
          if (maxLocality != TaskLocality.NO_PREF) {
            currentLocalityIndex = getLocalityIndex(taskLocality)
            lastLaunchTime = curTime
          }
          //序列化并返回Task
          val startTime = clock.getTimeMillis()
          val serializedTask: ByteBuffer = try {
            Task.serializeWithDependencies(task, sched.sc.addedFiles, sched.sc.addedJars, ser)
          } catch {
		//task序列化失败,则丢弃整个TaskSet
            case NonFatal(e) =>
              val msg = s"Failed to serialize task $taskId, not attempting to retry it."
              logError(msg, e)
              abort(s"$msg Exception during serialization: $e")
              throw new TaskNotSerializableException(e)
          }
		//Task广播时序列化的大小限制(为什么后面还要序列化,这是广播作用??)
          if (serializedTask.limit > TaskSetManager.TASK_SIZE_TO_WARN_KB * 1024 &&
              !emittedTaskSizeWarning) {
            emittedTaskSizeWarning = true
            logWarning(s"Stage ${task.stageId} contains a task of very large size " +
              s"(${serializedTask.limit / 1024} KB). The maximum recommended task size is " +
  	  s"${TaskSetManager.TASK_SIZE_TO_WARN_KB} KB.")
          }
		//Task加入到运行Task队列中
          addRunningTask(taskId)

          //序列化一些日志处理。
          val taskName = s"task ${info.id} in stage ${taskSet.id}"
          logInfo(s"Starting $taskName (TID $taskId, $host, partition ${task.partitionId}," +
            s"$taskLocality, ${serializedTask.limit} bytes)")
		//向高层调度器DAGScheduler报告Task开始执行
          sched.dagScheduler.taskStarted(task, info)
		//返回封装了TaskDescription的Some类
          return Some(new TaskDescription(taskId = taskId, attemptNumber = attemptNum, execId,
            taskName, index, serializedTask))
        }
        case _ =>
      }
    }
    None
  }

DAGScheduler是从 数据(存储)层面考虑preferedLocation的,而TaskScheduler则是从具体计算Task的角度考虑 计算的本地性的。

f)通过Lanch Task把任务发送给ExecutorBackend去执行。(见4)

 // Launch tasks returned by a set of resource offers
    private def launchTasks(tasks: Seq[Seq[TaskDescription]]) {
      for (task <- tasks.flatten) {
	//对tasks中的所有task进行序列化
        val serializedTask = ser.serialize(task)
		//序列化后的Task的限制
        if (serializedTask.limit >= akkaFrameSize - AkkaUtils.reservedSizeBytes) {
          scheduler.taskIdToTaskSetManager.get(task.taskId).foreach { taskSetMgr =>
            try {
              var msg = "Serialized task %s:%d was %d bytes, which exceeds max allowed: " +
                "spark.akka.frameSize (%d bytes) - reserved (%d bytes). Consider increasing " +
                "spark.akka.frameSize or using broadcast variables for large values."
              msg = msg.format(task.taskId, task.index, serializedTask.limit, akkaFrameSize,
                AkkaUtils.reservedSizeBytes)
		//Task大小超出限制,丢弃TaskSet
              taskSetMgr.abort(msg)
            } catch {
              case e: Exception => logError("Exception in error callback", e)
            }
          }
        }
        else {//否则,Task大小符合要求
		//更新executor信息
          val executorData = executorDataMap(task.executorId)
          executorData.freeCores -= scheduler.CPUS_PER_TASK
		//发送Task序列化后的Task给executor。
          executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask)))
        }
      }
    }

对于任务大小的设置:Task进行广播时,AkkaFrameSize大小是128MB,Akka保留的字节大小为200k,如果任务大于或等于128MB-200K的话,则任务会直接被丢弃掉;如果小于128MB-200K的话会通过CoarseGrainedSchedulerBackend去LaunchTask到具体的ExecutorBackend上。

至此,在Driver端的处理完成了,下一节将会讲解ExecutorBackend端接收Task后的处理。(可参照35图)

-----------------------------------------------------EOF---------------------------------------------------------------------------------------






  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值