[Spark源码剖析] Task的调度与执行源码剖析

Spark源码阅读02-Spark核心原理之调度算法 终极手撕架构师的学习笔记:分布式+微服务+开源框架+性能优化《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-S88bpB5M-1712068764237)] 阅读详情

转载请标明出处:http://blog.youkuaiyun.com/bigbigdata/article/details/47801483

本文基于Spark 1.3.1,Standalone模式

一个Spark Application分为stage级别和task级别的调度,stage级别的调度已经用DAGScheduler划分stage和DAGScheduler提交stage两片文章进行源码层面的说明,本文将从源码层面剖析task是如何被调度和执行的。

函数调用流程

先给出task调度的总体函数调用流程,并说明每个关键函数是干嘛的。这样一开始就在心里有个大概的流程图,便于之后的理解。

//< DAGScheduler调用该taskScheduler.submitTasks提交一个stage对应的taskSet,一个taskSet包含多个task
TaskSchedulerImpl.submitTasks(taskSet: TaskSet)
    //< TaskScheduler(实际上是TaskSchedulerImpl)为DAGScheduler提交的每个taskSet创建一个对应的TaskSetManager对象,TaskSetManager用于调度同一个taskSet中的task
    val manager = TaskSchedulerImpl.createTaskSetManager(taskSet, maxTaskFailures)
    //< 将新创建的manager加入到调度树中,调度树由SchedulableBulider维护。有FIFO、Fair两种实现
    SchedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)
        //< 触发调用CoarseGrainedSchedulerBackend.reviveOffers(),它将通过发送事件触发makeOffers方法调用
        CoarseGrainedSchedulerBackend.reviveOffers()
            //< 此处为发送ReviveOffers事件
            driverEndpoint.send(ReviveOffers)
        //< 此处为接收事件并处理
        CoarseGrainedSchedulerBackend.receive 
            CoarseGrainedSchedulerBackend.makeOffers
                //< 查找各个节点空闲资源(这里是cores),并返回要在哪些节点上启动哪些tasks的对应关系,用Seq[Seq[TaskDescription]]表示
                TaskSchedulerImpl.resourceOffers
            //< 启动对应task
            CoarseGrainedSchedulerBackend.launchTasks
                executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask)))

看了上述流程可能不那么明白,没关系,不明白才要往下看。

TaskSchedulerImpl.submitTasks(…)

在Spark 1.3.1版本中,TaskSchedulerImpl是TaskScheduler的唯一实现。submitTasks函数主要作用如下源码及注释所示:

  1. 为taskSet创建对应的TaskSetManager对象。TaskManager的主要功能在于对Task的细粒度调度,比如
    • 决定在某个executor上是否启动及启动哪个task
    • 为了达到Locality aware,将Task的调度做相应的延迟
    • 当一个Task失败的时候,在约定的失败次数之内时,将Task重新提交
    • 处理拖后腿的task
  2. 调用SchedulerBackend.makeOffers进入下一步
override def submitTasks(taskSet: TaskSet) {
  val tasks = taskSet.tasks
  logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks")
  this.synchronized {
    //< 为stage对应的taskSet创建TaskSetManager对象
    val manager = createTaskSetManager(taskSet, maxTaskFailures)
    //< 建立taskset与TaskSetManager的对应关系
    activeTaskSets(taskSet.id) = manager

    //< TaskSetManager会被放入调度池(Pool)当中。
      schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)

    //< 设置定时器,若task还没启动,则一直输出未分配到资源报警(输出警告日志)
    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()
          }
        }
      }, STARVATION_TIMEOUT_MS, STARVATION_TIMEOUT_MS)
    }
    hasReceivedTask = true
  }

  //< 将处触发调用SchedulerBackend.makeOffers来为tasks分配资源,调度任务
  backend.reviveOffers()
}

基于事件模型的调用

下面源码及注释展示了CoarseGrainedSchedulerBackend是如何通过事件模型来进一步调用的。其中ReviveOffers事件有两种触发模式:

  1. 周期性触发的,默认1秒一次
  2. reviveOffers被TaskSchedulerImpl.reviveOffers()调用
  override def reviveOffers() {
    driverEndpoint.send(ReviveOffers)
  }

  override def receive: PartialFunction[Any, Unit] = {
  //< 此处省略n行代码

  case ReviveOffers =>
    makeOffers()

  //< 此处省略n行代码
  }

CoarseGrainedSchedulerBackend.makeOffers()

该函数非常重要,它将集群的资源以Offer的方式发给上层的TaskSchedulerImpl。TaskSchedulerImpl调用scheduler.resourceOffers获得要被执行的Seq[TaskDescription],然后将得到的Seq[TaskDescription]交给CoarseGrainedSchedulerBackend分发到各个executor上执行

    def makeOffers() {
      launchTasks(scheduler.resourceOffers(executorDataMap.map { case (id, executorData) =>
        new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
      }.toSeq))
    }

为便于理解makeOffers调用及间接调用的各个流程,将该函数实现分为三个step来分析,这需要对源码的表现形式做一点点改动,但并不会有任何影响。

Step1: val seq = executorDataMap.map { case (id, executorData) => new WorkerOffer(id, executorData.executorHost, executorData.freeCores) }.toSeq

executorDataMap是HashMap[String, ExecutorData]类型,在该HashMap中key为executor id,value为ExecutorData类型(包含executor的host,RPC信息,TotalCores,FreeCores信息)

//< 代表一个executor上的可用资源(这里仅可用cores)
private[spark]
case class WorkerOffer(executorId: String, host: String, cores: Int)

这段代码,返回HashMap[executorId, WorkerOffer]。每个WorkerOffer包含executor的id,host及其上可用cores信息。

Step2: val taskDescs = scheduler.resourceOffers( seq )

拿到集群里的executor及其对应WorkerOffer后,就要开始第二个步骤,即找出要在哪些Worker上启动哪些task。这个过程比较长,也比较复杂。让我来一层层拨开迷雾。

我把val taskDescs = scheduler.resourceOffers( seq )即TaskSchedulerImpl.resourceOffers(offers: Seq[WorkerOffer]),返回的是Seq[Seq[TaskDescription]] 类型,来看看其实现:

def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized {
    //< 标记每个slave为alive并记录它们的hostname
    var newExecAvail = false
    //< 此处省略更新executor,host,rack信息代码;这里会根据是否有新的executor更新newExecAvail的值

    //< 为了负载均衡,打乱offers顺序,Random.shuffle用于将一个集合中的元素打乱
    val shuffledOffers = Random.shuffle(offers)
    //< 事先创建好用于存放要在各个worker上launch的 List[workerId, ArrayBuffer[TaskDescription]]。
    //< 由于task要使用的cores并不一定为1,所以每个worker上要launch得task并不一定等于可用的cores数
    val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))
    //< 每个executor上可用的cores
    val availableCpus = shuffledOffers.map(o => o.cores).toArray

    //< 返回排序过的TaskSet队列,有FIFO及Fair两种排序规则,默认为FIFO,可通过配置修改
    val sortedTaskSets = rootPool.getSortedTaskSetQueue
    for (taskSet <- sortedTaskSets) {
      //< 如果有新的executor added,更新TaskSetManager可用的executor
      if (newExecAvail) {
        taskSet.executorAdded()
      }
    }

    var launchedTask = false
    //< 依次取出排序过的taskSet列表中的taskSet;
    //< 对于每个taskSet,取出其tasks覆盖的所有locality,从高到低依次遍历每个等级的locality;
    //< 取出了taskSet及本次要处理的locality后,根据该taskSet及locality遍历所有可用的worker,找出可以在各个worker上启动的task,加到tasks:Seq[Seq[TaskDescription]]中
    for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) {
      do {
        //< 获取tasks,tasks代表要在哪些worker上启动哪些tasks
        launchedTask = resourceOfferSingleTaskSet(
            taskSet, maxLocality, shuffledOffers, availableCpus, tasks)
      } while (launchedTask)
    }

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

结合代码,概括起来说,Step2又可以分为4个SubStep:

  • 【SubStep1】: executor, host, rack等信息更新
  • 【SubStep2】: 随机打乱workers。目的是为了分配tasks能负载均衡,分配tasks时,是从打乱的workers的序列的0下标开始判断是否能在worker上启动task的
  • 【SubStep3】: RootPool对它包含的所有的TaskSetManagers进行排序并返回已排序的TaskSetManager数组。这里涉及到RootPool概念及如何排序,将会在下文展开说明
  • 【SubStep4】: 对于RootPool返回的排序后的ArrayBuffer[TaskSetManager]中的每一个TaskSetManager,取出其包含的tasks包含的所有locality。根据locality从高到低,对于每个locality,遍历所有worker,结合延迟调度机制,判断TaskSetManager的哪些tasks可以在哪些workers上启动。这里比较需要进一步说明的是“延迟调度机制”及如何判断某个TaskSetManager里的tasks是否有可以在某个worker上启动

下面,就对SubStep3及SubStep4进行展开说明

【SubStep3】

SubStep3的职责是”RootPool对它包含的所有的TaskSetManagers进行排序并返回已排序的TaskSetManager数组”。那么什么是RootPool呢?每个Spark Application包含唯一一个TaskScheduler对象,该TaskScheduler对象包含唯一一个RootPool,Spark Application包含的所有Job的所有stage对应的所有未完成的TaskSetManager都会保存在RootPool中,完成后从RootPool中remove。RootPool为org.apache.spark.scheduler.Pool类型,称作调度池。Pool的概念与YARN中队列的概念比较类似,一个队列可以包含子队列,相对的一个Pool可以包含子Pool;YARN队列的叶子节点即提交到该队列的Application,Pool的叶子节点即分配到该Pool的TaskSetManager。Pool根据调度模式的不同,分为FIFO及Fair。FIFO模式下只有一层Pool,不同于YARN的队列可以n多层,Pool的Fair调度模式下,只能有三层:RootPool,RootPool的子Pools,子Pools的叶子节点(即TaskSetManager)。

不同的调度模式添加叶子节点的实现是一样的,如下:

  override def addSchedulable(schedulable: Schedulable) {
    require(schedulable != null)
    //< 当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素
    schedulableQueue.add(schedulable)
    schedulableNameToSchedulable.put(schedulable.name, schedulable)
    schedulable.parent = this
  }

Schedulable类型的参数schedulable包含成员val parent: Pool,即父Pool,所以在添加TaskSetManager到Pool的时候就指定了父Pool。对于FIFO,所有的TaskSetManager的父Pool都是RootPool;对于Fair,TaskSetManager的父Pool即RootPool的某个子Pool。

不同的模式,除了Pool的层级结构不同,对它包含的TaskSetManagers进行排序时使用的算法也不同。FIFO对应FIFOSchedulingAlgorithm类,Fair对应FairSchedulingAlgorithm()类

  var taskSetSchedulingAlgorithm: SchedulingAlgorithm = {
    schedulingMode match {
      case SchedulingMode.FAIR =>
        new FairSchedulingAlgorithm()
      case SchedulingMode.FIFO =>
        new FIFOSchedulingAlgorithm()
    }
  }

当Pool.getSortedTaskSetQueue被调用时,就会用到该排序类,如下:

  //< 利用排序算法taskSetSchedulingAlgorithm先对以本pool作为父pool的子pools做排序,再对排序后的pool中的每个TaskSetManager排序;
  //< 得到最终排好序的 ArrayBuffer[TaskSetManager]
  override def getSortedTaskSetQueue: ArrayBuffer[TaskSetManager] = {
    var sortedTaskSetQueue = new ArrayBuffer[TaskSetManager]
    val sortedSchedulableQueue =
      schedulableQueue.toSeq.sortWith(taskSetSchedulingAlgorithm.comparator)
    //< FIFO不会调到这里,直接走到下面的return  
    for (schedulable <- sortedSchedulableQueue) {
      sortedTaskSetQueue ++= schedulable.getSortedTaskSetQueue
    }
    sortedTaskSetQueue
  }

FIFO排序类中的比较函数的实现很简单:
1. Schedulable A和Schedulable B的优先级,优先级值越小,优先级越高
2. A优先级与B优先级相同,若A对应stage id越小,优先级越高

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

Pool及TaskSetManager都继承于Schedulable,来看下它的定义:

private[spark] trait Schedulable {
  var parent: Pool
  // child queues
  def schedulableQueue: ConcurrentLinkedQueue[Schedulable]
  def schedulingMode: SchedulingMode
  def weight: Int
  def minShare: Int
  def runningTasks: Int
  def priority: Int
  def stageId: Int
  def name: String

    //< 省略若干代码
}

可以看到,Schedulable包含weight(权重)、priority(优先级)、minShare(最小共享量)等属性。其中:
* weight:权重,默认是1,设置为2的话,就会比其他调度池获得2x多的资源,如果设置为-1000,该调度池一有任务就会马上运行
* minShare:最小共享核心数,默认是0,在权重相同的情况下,minShare大的,可以获得更多的资源

对于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
    }
  }
}

结合以上代码,我们可以比较容易看出Fair调度模式的比较逻辑:
1. 正在运行的task个数小于最小共享核心数的要比不小于的优先级高
2. 若两者正在运行的task个数都小于最小共享核心数,则比较minShare使用率的值,即runningTasks.toDouble / math.max(minShare, 1.0).toDouble,越小则优先级越高
3. 若minShare使用率相同,则比较权重使用率,即runningTasks.toDouble / s.weight.toDouble,越小则优先级越高
4. 如果权重使用率还相同,则比较两者的名字

对于Fair调度模式,需要先对RootPool的各个子Pool进行排序,再对子Pool中的TaskSetManagers进行排序,使用的算法都是FairSchedulingAlgorithm.FairSchedulingAlgorithm。

到这里,应该说清楚了整个SubStep3的流程。

【SubStep4】

SubStep4说白了就是已经知道了哪些worker上由多少可用cores了,然后要决定要在哪些worker上启动哪些tasks:

//< 事先创建好用于存放要在各个worker上launch的 List[workerId, ArrayBuffer[TaskDescription]]。
//< 由于task要使用的cores并不一定为1,所以每个worker上要launch得task并不一定等于可用的cores数
val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))

var launchedTask = false
for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) {
  do {
    //< 获取tasks,tasks代表要在哪些worker上启动哪些tasks
    launchedTask = resourceOfferSingleTaskSet(
        taskSet, maxLocality, shuffledOffers, availableCpus, tasks)
  } while (launchedTask)
}

从for循环可以看到,该过程对排好序的taskSet数组的每一个元素,从locality优先级从高到低(taskSet.myLocalityLevels返回该taskSet包含的所有task包含的locality,按locality从高到低排列,PROCESS_LOCAL最高)取出locality,以取出的taskSet和locality调用TaskSchedulerImpl.resourceOfferSingleTaskSet,来看下它的实现(为方便阅读及理解,删去一些代码):

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

  //< 获取每个worker上要执行的tasks序列
  for (i <- 0 until shuffledOffers.size) {
    val execId = shuffledOffers(i).executorId
    val host = shuffledOffers(i).host
    if (availableCpus(i) >= CPUS_PER_TASK) {
      try {
        for (task <- taskSet.resourceOffer(execId, host, maxLocality)) {
          //< 将获得要在index为i的worker上执行的task,添加到tasks(i)中;这样就知道了要在哪个worker上执行哪些tasks了
          tasks(i) += task

          availableCpus(i) -= CPUS_PER_TASK
          assert(availableCpus(i) >= 0)
          launchedTask = true
        }
      } catch {
        case e: TaskNotSerializableException =>
          return launchedTask
      }
    }
  }
  return launchedTask
}

resourceOfferSingleTaskSet拿到worker可用cores,taskSet和locality后
1. 遍历每个worker的可用cores,如果可用cores大于task需要的cores数(即CPUS_PER_TASK),进入2
2. 调用taskSet.resourceOffer(execId, host, maxLocality)获取可在指定executor上启动的task,若返回非空,把返回的task加到最终的tasks: Seq[ArrayBuffer[TaskDescription]]中,该结构保存要在哪些worker上启动哪些tasks
3. 减少2中分配了task的worker的可用cores及更新其他信息

从以上的分析中可以看出,要在某个executor上启动哪个task最终的实现在TaskSetManager.resourceOffer中,由于该函数比较长,我将函数分过几个过程来分析

首先来看第一段:

//< 如果资源是有locality特征的
if (maxLocality != TaskLocality.NO_PREF) {
  //< 获取当前taskSet允许执行的locality。getAllowedLocalityLevel随时间变化而变化
  allowedLocality = getAllowedLocalityLevel(curTime)
  //< 如果允许的locality级别低于maxLocality,则使用maxLocality覆盖允许的locality
  if (allowedLocality > maxLocality) {
    // We're not allowed to search for farther-away tasks
    //< 临时将允许的locality级别降低到资源允许的最高locality级别
    allowedLocality = maxLocality
  }
}

要判断task能否在worker上启动,除了空闲资源是否达到task要求外,还需要判断本地性,即locality。locality从高到低共分为PROCESS_LOCAL, NODE_LOCAL,RACK_LOCAL及ANY。若taskSet带有locality属性,则通过getAllowedLocalityLevel函数获得该taskSet能容忍的最低界别locality。

getAllowedLocalityLevel中:
1. 如果taskset刚刚被提交,taskScheduler开始第一轮对taskset中的task开始提交,那么当时currentLocalityIndex为0,直接返回可用的最好的本地性;如果是在以后的提交过程中,那么如果当前的等待时间超过了一个级别,就向后跳一个级别
2. getAllowedLocalityLevel方法返回的是当前这次调度中,能够容忍的最差的本地性级别,在后续步骤的搜索中就只搜索本地性比这个级别好的情况
3. 随着时间的推移,撇开maxLocality配置不谈,对于本地性的容忍程度越来越大。

继续返回TaskSetManager.resourceOffer中,获得taskSet能容忍的最差locality后,与maxLocality比较去较差的locality作为最终的
能容忍的最差locality。

进入第二段:

dequeueTask(execId, host, allowedLocality) match {
  case Some((index, taskLocality, speculative)) => {
    //< 进行各种信息更新操作

    addRunningTask(taskId)

    // We used to log the time it takes to serialize the task, but task size is already
    // a good proxy to task serialization time.
    // val timeTaken = clock.getTime() - startTime
    val taskName = s"task ${info.id} in stage ${taskSet.id}"
    sched.dagScheduler.taskStarted(task, info)
    return Some(new TaskDescription(taskId = taskId, attemptNumber = attemptNum, execId,
      taskName, index, serializedTask))
  }
  case _ =>
}

可以看到,第二段首先调用了函数dequeueTask,如果返回不为空,说明为指定的worker分配了task;这之后,进行各种信息更新,将taskId加入到runningTask中,并通知DAGScheduler,最后返回taskDescription。来看看dequeueTask的实现:

private def dequeueTask(execId: String, host: String, maxLocality: TaskLocality.Value)
    : Option[(Int, TaskLocality.Value, Boolean)] =
  {
    //< dequeueTaskFromList: 该方法获取list中一个可以launch的task,同时清除扫描过的已经执行的task。其实它从第二次开始首先扫描的一定是已经运行完成的task,因此是延迟清除
    // 同一个Executor,通过execId来查找相应的等待的task
    for (index <- dequeueTaskFromList(execId, getPendingTasksForExecutor(execId))) {
      return Some((index, TaskLocality.PROCESS_LOCAL, false))
    }

    // 通过主机名找到相应的Task,不过比之前的多了一步判断
    if (TaskLocality.isAllowed(maxLocality, TaskLocality.NODE_LOCAL)) {
      for (index <- dequeueTaskFromList(execId, getPendingTasksForHost(host))) {
        return Some((index, TaskLocality.NODE_LOCAL, false))
      }
    }


    if (TaskLocality.isAllowed(maxLocality, TaskLocality.NO_PREF)) {
      // Look for noPref tasks after NODE_LOCAL for minimize cross-rack traffic
      for (index <- dequeueTaskFromList(execId, pendingTasksWithNoPrefs)) {
        return Some((index, TaskLocality.PROCESS_LOCAL, false))
      }
    }

    // 通过Rack的名称查找Task
    if (TaskLocality.isAllowed(maxLocality, TaskLocality.RACK_LOCAL)) {
      for {
        rack <- sched.getRackForHost(host)
        index <- dequeueTaskFromList(execId, getPendingTasksForRack(rack))
      } {
        return Some((index, TaskLocality.RACK_LOCAL, false))
      }
    }

    // 查找那些preferredLocations为空的,不指定在哪里执行的Task来执行
    if (TaskLocality.isAllowed(maxLocality, TaskLocality.ANY)) {
      for (index <- dequeueTaskFromList(execId, allPendingTasks)) {
        return Some((index, TaskLocality.ANY, false))
      }
    }

    // find a speculative task if all others tasks have been scheduled
    // 最后没办法了,拖的时间太长了,只能启动推测执行了
    dequeueSpeculativeTask(execId, host, maxLocality).map {
      case (taskIndex, allowedLocality) => (taskIndex, allowedLocality, true)}
  }

从该实现可以看出,不管之前获得的能容忍的最差locality(即allowedLocality)有多低,每次dequeueTask都是以PROCESS_LOCAL->…->allowedLocality顺序来判断是否可以以该locality启动task,而并不是必须以allowedLocality启动task。这也增大了启动task的机会。

到这里应该大致说清楚了Step2中的各个流程。

Step3: launchTasks( taskDescs )

得到要在哪些worker上启动哪些task后,将调用launchTasks来启动各个task,实现如下:

def launchTasks(tasks: Seq[Seq[TaskDescription]]) {
  for (task <- tasks.flatten) {
    val ser = SparkEnv.get.closureSerializer.newInstance()
    //< 序列化task
    val serializedTask = ser.serialize(task)
    //< 若序列化后的task的size大于等于Akka可用空间
    if (serializedTask.limit >= akkaFrameSize - AkkaUtils.reservedSizeBytes) {
      val taskSetId = scheduler.taskIdToTaskSetId(task.taskId)
      scheduler.activeTaskSets.get(taskSetId).foreach { taskSet =>
        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)
          //< 中止taskSet,标记为已完成;同时将该taskSet的状态置为isZombie(Zombie:僵尸)
          taskSet.abort(msg)
        } catch {
          case e: Exception => logError("Exception in error callback", e)
        }
      }
    }
    else {
      //< 若序列化后的task的size小于Akka可用空间,减去对应executor上的可用cores数并向对应的executor发送启动task消息
      val executorData = executorDataMap(task.executorId)
      executorData.freeCores -= scheduler.CPUS_PER_TASK
      executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask)))
    }
  }
}

逻辑比较简单,先对task进行序列化,若序列化后的task的size大于等于akka可用空间大小,则taskSet标记为已完成并置为Zombie状态;若序列化后的task的size小于akka可用空间大小,则通过发送消息给对应executor启动task

Spark源码和调优简介 Spark Core 作者:calvinrzluo,腾讯 IEG 后台开发工程师本文基于 Spark 2.4.4 版本的源码,试图分析其 Core 模块的部分实现原理,其中如有错误,请指正。为了简化论述,将部... 阅读详情

相关推荐

Spark源码分析之五:Task调度(一)

在前四篇博文中,我们分析了Job提交运行总流程的第一阶段Stage划分与提交,它又被细化为三个分阶段:         1、Job的调度模型与运行反馈;         2、Stage划分;         3、Stage提交:对应TaskSet的生成。         Stage划分与提交阶段主要是由DAGScheduler完成的,而DAGScheduler负责Job的逻辑调度,主要职责

辰辰爸的博客 5492

记一次Spark集群查询速度变慢的问题调查

现象:针对某张表(下文中用A表代替)的查询速度慢了6s       简单介绍一下系统的架构:Spark + Tachyon + glusterfs+mesos,其中glusterfs负责tachyon数据的持久化,Spark从tachyon上加载数据,mesos负责Spark任务调度   通过对比分析Spark:4040页面提供的任务执行信息发现,A表的Spark任务中,出现了Loc...

探索中前进的Rock 854

Spark Task执行流程源码分析系列之三: TaskSetManager分析

概述 TaskSetManager是一个stage的任务集合的抽象,主要进行当前Stage任务集管理,跟踪每一个任务,进行失败重试,推测执行以及基于数据本地性的调度等,对外只提供两个接口,resourceOffer来判断是否在指定的executor上面执行任务,statusUpdate在任务成功或者失败时候,告知任务状态改变。本文将分析源码,解密它是如何进行调度任务执行,推测执行以及失败重试的。 基本属性 我们先来看下TaskSetManager的主要成员变量,主要是任务的管理相关的变量: taskSet

进击的数据小白 1251

Spark中的Scheduler

Spark中的Scheduler scheduler分成两个类型,一个是TaskScheduler与其实现,一个是DAGScheduler。 TaskScheduler:主要负责各stage中传入的task的执行与调度。 DAGScheduler:主要负责对JOB中的各种依赖进行解析,根据RDD的依赖生成stage并通知TaskScheduler执行。 实例生成 TaskSchedule

隔壁老杨的专栏 2346

spark最新版本3.0.0中job划分、调度以及任务的提交执行流程源码分析

文章目录前言一 整个任务的流程图示二 任务的划分、调度和执行1 调用行动算子生成job2 划分阶段并发送Task给Executor3 Executor执行Task 前言 上文我们对spark整个应用运行流程进行了整体的分析,但是后面最后一步关于任务的划分、调度和执行就一笔带过了,今天我们就来对spark任务的执行流程的底层源码分析,搞懂这个会对我们理解spark几个术语有比较清楚的理解了,如: 术语 描述 关系 job 工作 调用一个行动算子就会有一个job生成 stage 阶段 有一

newhandnew413的博客 1288

Spark Task执行流程源码分析系列之四: 任务启动&运行&结果处理

上面几节我们分析了Task的构建和调度,本节我们将聚焦在Task的执行,会涉及到Task是如何启动起来的,具体的不同任务是如何执行的,执行得到的结果是如何处理的。 Task启动 首先我们来看下Task是如何启动的,Driver发送lauchTask消息给Executor,Executor的常驻进程CoarseGrainedExecutorBackend接收到Driver发送来的LaunchTask消息后,会解码任务信息,然后由Executor开始执行任务,Executor会对Task进行封装为TaskR.

进击的数据小白 1174

spark1.2源码学习之任务调度1

正学习spark,记录下我看源码的心得。省的忘掉。

went_java的专栏 889

三维视觉智能焊接系统:免示教技术的工业应用实践

本文深入探讨了三维视觉智能焊接系统中免示教技术的工业应用实践。该系统通过三维视觉相机实时扫描工件,自动识别焊缝并生成轨迹,将产线换型时间从数小时缩短至几分钟,显著提升了生产柔性。文章详细解析了其核心硬件、算法及工厂部署流程,并展望了与数字孪生、工艺智能化融合的未来趋势。

weixin_29091105的博客 432

Spark2.0.X源码深度剖析之 TaskScheduler之Task划分

微信号:519292115 邮箱:taosiyuan163@163.com 尊重原创,禁止转载!! Spark目前是大数据领域中最火的框架之一,可高效实现离线批处理,实时计算和机器学习等多元化操作,阅读源码有助你加深对框架的理解和认知 Task作为Spark的最小执行单元在DAGScheduler划分好Stage之后会提交给TaskSch

ws0owws0ow的博客 1631

GIS国土工具实战:从地类分析到坐标转换,一站式解决项目难题

本文详细介绍了GIS国土工具在国土整治项目中的高效应用,涵盖地类分析、坐标转换、影像加载等核心功能。通过实战案例展示如何利用该工具快速处理shp与txt文件转换、智能比对三调二调数据,以及优化影像加载策略,显著提升项目效率。特别适合需要处理复杂国土数据的专业人员使用。

weixin_28732953的博客 334

Android Camera性能优化概述

做camera性能优化方面的工作有几年了,想写一篇文章简单介绍一下其中涉及到的具体技术,这篇文章会大概介绍一下涉及到的技术。

weixin_42136255的博客 4023

spark调度流程源码剖析

Spark作为一个优秀的分布式集群内存计算框架,提供了简单接口和丰富的rdd算子供开发者调用。spark的运行速度之所以如此之快,一方面是因为它基于内存,另一方面是因为它对job,state,task的划分并根据算子的shuffle过程将同一端的多个算子操作直接执行一条pipeline,减少了不必要的中间过程的存储消耗。根据官网的spark调度流程,我们看到如下图: 这张图非常简洁,大概

SummerMangoZz的博客 525

利用Nginx反向代理实现GitLab自定义域名的高效配置

本文详细介绍了如何利用Nginx反向代理实现GitLab自定义域名的高效配置。通过统一域名管理、性能优化和安全加固,提升GitLab的访问效率和安全性。文章包含前期准备、GitLab基础配置、Nginx反向代理设置及常见问题排查,帮助开发者快速完成部署。

weixin_28834169的博客 164

从源码剖析一个 Spark WordCount Job 执行的全过程

从源码剖析一个 Spark WordCount Job 执行的全过程 | Mz's Blog WordCount 可以说是分布式数据处理框架的”Hello World”,我们可以以它为例来剖析一个 Spark Job 的执行全过程。 我们要执行的代码为: sc.textFile("hdfs://...").flatMap(_.split(" ")).map((_, 1)).reduceByKey(...

weixin_33754065的博客 200

手把手教你用certificate-manager修复Vcenter证书过期问题(附密码重置技巧)

本文详细解析了Vcenter证书过期引发的503服务不可用、密码错误等典型故障现象,并提供了使用certificate-manager工具进行系统性修复的完整步骤。文章重点阐述了证书体系原理、精准诊断方法,并分享了在SSO验证失败时通过vdcadmintool重置密码的关键技巧,帮助管理员快速恢复平台并建立预防性运维策略。

swift5iosmith的博客 459

差分运放电路输出电压偏置设计解析

差分运算放大电路是信号处理中的基础模块,通过差分对管结构实现共模抑制与差模放大。其核心原理基于运放的虚短虚断特性,当电阻网络满足对称条件时,输出电压与输入差分成正比。在工业测量系统中,由于ADC通常仅支持正电压输入,需要通过偏置电路将双极性信号平移至正电压范围。典型设计采用精密基准电压源和匹配电阻网络,既保持信号完整性又满足系统接口要求。本文以-100V高压测量为例,详细解析了分压网络设计、参考电压选取以及电阻匹配等关键技术要点,为传感器接口电路设计提供实践参考。

weixin_35750747的博客 470

Spark 核心之 Application 和 Job 原理剖析

要点总结层级关系Job 触发每个 Action 算子调用 sc.runJob() 创建新 JobStage 切分遇到 ShuffleDependency 即切分Task 生成Stage 最后一个 RDD 的 Partition 数量 = Task 数金句:Transformation 是"画图纸"(构建 DAG),Action 是"按下启动键"(触发 Job)。一个 Application 可以画无数张图纸,但只有按下的启动键才算数。

https://starzy1990.github.io/ 538

k8s 不同node之间pod ip不通

底层k8s使用calico网络,ipip网络模式,kubevirt启动了2台虚拟机;在这两台虚拟机上再搭建k8s,calico网络,ipip网络模式。说明:我是使用helm安装的calico,改这个模式要改crd资源ippools。其他方式安装的calico修改方法可能不一样。在虚拟机上的k8s上启动了2个pod,分布在不同的node上,这两个pod不能ping通对方的pod ip。修改虚拟机之上的k8s集群的calico网络的ipipMode为Never即可,之前使用的是Always。

weixin_40548182的博客 4056

【数字IC验证进阶】正则表达式(Regular Expression)快速入门指南

在芯片开发过程中,正则表达式的使用非常常见。初次上手晦涩难懂,多用几次爱不释手!

悟已往之不谏 知来者之可追 1440

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

Spark源码解析:深入理解RDD执行机制

Spark作为大数据处理领域的明星框架,其高效的分布式计算能力依赖于RDD这一核心抽象。本文聚焦RDD的执行机制,覆盖从RDD转换操作(Transformation)到动作操作(Action)触发计算的全流程,重点解析DAG调度、Stage划分、任务执行等关键环节的源码实现。本文从生活场景引入RDD概念,逐步解析核心组件(如DAG调度器、TaskScheduler)的协作逻辑,结合源码分析RDD依赖关系、Stage划分规则,最后通过实战案例验证理论,帮助读者建立“概念-源码-实践”的完整认知链。

小程序开发 812

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

Spark Core基础与源码剖析全景手册

Spark Core作用Spark Core是Spark的内核,负责RDD(弹性分布式数据集)管理、任务调度、内存管理和容错机制等,是所有Spark组件的基础。核心特性RDD(弹性分布式数据集):核心数据抽象,支持分布式、不可变、容错。懒加载(Lazy Evaluation):转换操作不会立即执行,触发Action时才真正计算。容错机制:DAG血缘追踪,自动重算丢失分区。内存计算:极大提升大数据处理速度。口诀:RDD弹性,懒加载,血缘容错,快如闪电。

IT技术学习与工作笔记分享 1179

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

Spark源码解析(一):Spark执行流程和脚本

Spark执行流程 Spark带注释源码 对于整个Spark源码分析系列,我将带有注释的Spark源码和分析的文件放在我的GitHub上Spark源码剖析欢迎大家fork和star 过程描述: 1.通过Shell脚本启动Master,Master类继承Actor类,通过ActorySystem创建并启动。 2.通过Shell脚本启动Worker,Worker类继承Actor类,通过A...

Oeljeklaus的博客 6344

Spark DAG的生成和Task的提交,启动,执行源码

Spark DAG的生成和Task的提交 我们在执行spark任务的时候,可能会好奇任务的执行流程是什么,dag是怎么生成的,task是在哪里启动的,driver和executor是怎么通信的,等等。下面我们通过一个简单的spark wordcount任务,来粗略了解下其中的奥秘。 SparkSession的创建 我们在开发spark作业的时候,首先会需要创建spark任务的入口类SparkSession的对象: SparkSession spark =SparkSession.builder().ap

ralph_wren的博客 2029

Spark源码——Spark on YARN Executor执行Task的过程

文章目录入口点第一步:CoarseGrainedExecutorBackend main()和run() 我们知道Spark中的Task是由Executor进程中执行的,那么Executor启动之后,具体是如何执行Task的呢? Executor执行Task的入口为object CoarseGrainedExecutorBackend。CoarseGrainedExecutorBackend是一个管理单个Executor的ExecutorBackend,它是在分配好的Container中其启动Executo

aof 864

Spark源码分析之调度框架详解

文章目录原理概述源码分析Job提交Stage划分Task提交Executor端运行Task Spark的调度框架分为资源调度和任务调度。Spark的资源调度是基于Yarn实现的,包含Driver和Executor资源的申请等,详细过程见博文 Spark源码分析之AM端运行流程(Driver) 和 Spark源码分析之CoarseGrainedExecutorBackend运行流程(Executor);本文主要讲述Spark任务调度框架的原理和源码分析。 原理概述 由于 Spark Scheduler内部原理

Haiwi-Song 1292
上一篇: [Spark源码剖析] DAGScheduler提交stage
下一篇: [spark源码剖析]RDD相关源码阅读笔记
bigbigdata
博客等级 码龄11年 5粉丝 7原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值