Spark源码分析之六:Task调度(二)

        话说在《Spark源码分析之五:Task调度(一)》一文中,我们对Task调度分析到了DriverEndpoint的makeOffers()方法。这个方法针对接收到的ReviveOffers事件进行处理。代码如下:

// Make fake resource offers on all executors
    // 在所有的executors上提供假的资源(抽象的资源,也就是资源的对象信息,我是这么理解的)
    private def makeOffers() {
      // Filter out executors under killing
      // 过滤掉under killing的executors
      val activeExecutors = executorDataMap.filterKeys(executorIsAlive)
      
      // 利用activeExecutors中executorData的executorHost、freeCores,构造workOffers,即资源
      val workOffers = activeExecutors.map { case (id, executorData) =>
        // 创建WorkerOffer对象
        new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
      }.toSeq
      
      // 调用scheduler的resourceOffers()方法,分配资源,并调用launchTasks()方法,启动tasks
      // 这个scheduler就是TaskSchedulerImpl
      launchTasks(scheduler.resourceOffers(workOffers))
    }
        代码逻辑很简单,一共分为三步:

        第一,从executorDataMap中过滤掉under killing的executors,得到activeExecutors;

        第二,利用activeExecutors中executorData的executorHost、freeCores,获取workOffers,即资源;

        第三,调用scheduler的resourceOffers()方法,分配资源,并调用launchTasks()方法,启动tasks:这个scheduler就是TaskSchedulerImpl。

        我们逐个进行分析,首先看看这个executorDataMap,其定义如下:

private val executorDataMap = new HashMap[String, ExecutorData]
        它是CoarseGrainedSchedulerBackend掌握的集群中executor的数据集合,key为String类型的executorId,value为ExecutorData类型的executor详细信息。ExecutorData包含的主要内容如下:

        1、executorEndpoint:RpcEndpointRef类型,RPC终端的引用,用于数据通信;

        2、executorAddress:RpcAddress类型,RPC地址,用于数据通信;

        3、executorHost:String类型,executor的主机;

        4、freeCores:Int类型,可用处理器cores;

        5、totalCores:Int类型,处理器cores总数;

        6、logUrlMap:Map[String, String]类型,日志url映射集合。

        这样,通过executorDataMap这个集合我们就能知道集群当前executor的负载情况,方便资源分析并调度任务。那么executorDataMap内的数据是何时及如何更新的呢?go on,继续分析。
        对于第一步中,过滤掉under killing的executors,其实现是对executorDataMap中的所有executor调用executorIsAlive()方法中,判断是否在executorsPendingToRemove和executorsPendingLossReason两个数据结构中,这两个数据结构中的executors,都是即将移除或者已丢失的executor。

        第二步,在过滤掉已失效或者马上要失效的executor后,利用activeExecutors中executorData的executorHost、freeCores,构造workOffers,即资源,这个workOffers更简单,是一个WorkerOffer对象,它代表了系统的可利用资源。WorkerOffer代码如下:

/**
 * Represents free resources available on an executor.
 */
private[spark]
case class WorkerOffer(executorId: String, host: String, cores: Int)
        而最重要的第三步,先是调用scheduler.resourceOffers(workOffers),即TaskSchedulerImpl的resourceOffers()方法,然后再调用launchTasks()方法将tasks加载到executor上去执行。

        我们先看下TaskSchedulerImpl的resourceOffers()方法。代码如下:

/**
   * Called by cluster manager to offer resources on slaves. We respond by asking our active task
   * sets for tasks in order of priority. We fill each node with tasks in a round-robin manner so
   * that tasks are balanced across the cluster.
   *
   * 被集群manager调用以提供slaves上的资源。我们通过按照优先顺序询问活动task集中的task来回应。
   * 我们通过循环的方式将task调度到每个节点上以便tasks在集群中可以保持大致的均衡。
   */
  def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized {
    
    // Mark each slave as alive and remember its hostname
    // Also track if new executor is added
    // 标记每个slave节点为alive活跃的,并且记住它的主机名
    // 同时也追踪是否有executor被加入
    var newExecAvail = false
    
    // 循环offers,WorkerOffer为包含executorId、host、cores的结构体,代表集群中的可用executor资源
    for (o <- offers) {
      
      // 利用HashMap存储executorId->host映射的集合
      executorIdToHost(o.executorId) = o.host
      
      // Number of tasks running on each executor
      // 每个executor上运行的task的数目,这里如果之前没有的话,初始化为0
      executorIdToTaskCount.getOrElseUpdate(o.executorId, 0)
      
      // 每个host上executors的集合
      // 这个executorsByHost被用来计算host活跃性,反过来我们用它来决定在给定的主机上何时实现数据本地性
      if (!executorsByHost.contains(o.host)) {// 如果executorsByHost中不存在对应的host
        
        // executorsByHost中添加一条记录,key为host,value为new HashSet[String]()
        executorsByHost(o.host) = new HashSet[String]()
        
        // 发送一个ExecutorAdded事件,并由DAGScheduler的handleExecutorAdded()方法处理
        // eventProcessLoop.post(ExecutorAdded(execId, host))
        // 调用DAGScheduler的executorAdded()方法处理
        executorAdded(o.executorId, o.host)
        
        // 新的slave加入时,标志位newExecAvail设置为true
        newExecAvail = true
      }
      
      // 更新hostsByRack
      for (rack <- getRackForHost(o.host)) {
        hostsByRack.getOrElseUpdate(rack, new HashSet[String]()) += o.host
      }
    }

    // Randomly shuffle offers to avoid always placing tasks on the same set of workers.
    // 随机shuffle offers以避免总是把任务放在同一组workers上执行
    val shuffledOffers = Random.shuffle(offers)
    
    // Build a list of tasks to assign to each worker.
    // 构造一个task列表,以分配到每个worker
    val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))
    
    // 可以使用的cpu资源
    val availableCpus = shuffledOffers.map(o => o.cores).toArray
    
    // 获得排序好的task集合
    // 先调用Pool.getSortedTaskSetQueue()方法
    // 还记得这个Pool吗,就是调度器中的调度池啊
    val sortedTaskSets = rootPool.getSortedTaskSetQueue
    
    // 循环每个taskSet
    for (taskSet <- sortedTaskSets) {
      // 记录日志
      logDebug("parentName: %s, name: %s, runningTasks: %s".format(
        taskSet.parent.name, taskSet.name, taskSet.runningTasks))
      
      // 如果存在新的活跃的executor(新的slave节点被添加时)
      if (newExecAvail) {
        // 调用executorAdded()方法
        taskSet.executorAdded()
      }
    }

    // Take each TaskSet in our scheduling order, and then offer it each node in increasing order
    // of locality levels so that it gets a chance to launch local tasks on all of them.
    // NOTE: the preferredLocality order: PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY
    var launchedTask = false
    
    // 按照位置本地性规则调度每个TaskSet,最大化实现任务的本地性
    // 位置本地性规则的顺序是:PROCESS_LOCAL(同进程)、NODE_LOCAL(同节点)、NO_PREF、RACK_LOCAL(同机架)、ANY(任何)
    for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) {
      do {
        // 调用resourceOfferSingleTaskSet()方法进行任务集调度
        launchedTask = resourceOfferSingleTaskSet(
            taskSet, maxLocality, shuffledOffers, availableCpus, tasks)
      } while (launchedTask)
    }

    // 设置标志位hasLaunchedTask
    if (tasks.size > 0) {
      hasLaunchedTask = true
    }
    
    return tasks
  }
         首先来看下它的主体流程。如下:

        1、设置标志位newExecAvail为false,这个标志位是在新的slave被添加时被设置的一个标志,下面在计算任务的本地性规则时会用到;

        2、循环offers,WorkerOffer为包含executorId、host、cores的结构体,代表集群中的可用executor资源:

            2.1、更新executorIdToHost,executorIdToHost为利用HashMap存储executorId->host映射的集合;

            2.2、更新executorIdToTaskCount,executorIdToTaskCount为每个executor上运行的task的数目集合,这里如果之前没有的话,初始化为0;

            2.3、如果新的slave加入:

                2.3.1、executorsByHost中添加一条记录,key为host,value为new HashSet[String]();

                2.3.2、发送一个ExecutorAdded事件,并由DAGScheduler的handleExecutorAdded()方法处理;

                2.3.3、新的slave加入时,标志位newExecAvail设置为true;

            2.4、更新hostsByRack;

        3、随机shuffle offers(集群中可用executor资源)以避免总是把任务放在同一组workers上执行;

        4、构造一个task列表,以分配到每个worker,针对每个executor按照其上的cores数目构造一个cores数目大小的ArrayBuffer,实现最大程度并行化;

        5、获取可以使用的cpu资源availableCpus;

        6、调用Pool.getSortedTaskSetQueue()方法获得排序好的ta

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值