Spark的动态资源分配算法

前言

《Spark RPC框架详解》这篇文章中,介绍了Spark RPC通信的基本流程。可以看到,Spark中的Driver通过Stage调度生成了物理执行计划,这个物理执行计划包括了所有需要运行的Task,以及,最关键的,这些Task希望运行的节点信息,我们叫做Locality Preference,即本地性偏好。
但是在Yarn的场景下,资源是以Executor(Container)为单位进行调度,整个Container的粒度与Task不一一对应,Container的生命周期与Task不一一对应。在这种场景下,动态资源调度的基本任务就是

  • 根据这些Task以及Task的本地性偏好,向Yarn申请Container作为Executor以执行Task;这里最重要的,是在资源申请中表达出对应的Task的位置偏好。
  • 在申请完资源以后,根据Task的本地性偏好,将Task调度到申请到的资源上面去。这里最重要的,是尽量满足Task的本地性偏好。

本文将详细讲解这个过程。
对于资源申请算法的基本流程,以及将Task和资源进行匹配的基本流程,本文都用实际例子进行讲解。

基于任务需求进行资源请求的整体过程

向Yarn请求资源是由客户端向ApplicationMaster申请,然后ApplicationMaster向Yarn发起请求的,而不是客户端直接向Yarn申请的。
资源是为了服务于Task的运行,Task的生成显然是Driver端负责的,Driver会根据物理执行计划生成的Task信息发送给ApplicationMaster,ApplicationMaster根据这些Task的相关信息进行资源申请。

ApplicationMaster启动以后,会有一个独立线程不断通过调用YarnAllocator.allocateResources()进行持续的资源更新(查看ApplicationMaster的launchReporterThread()方法)。这里叫资源更新,而不叫资源申请,因为这里的操作包括新的资源的申请,旧的无用的Container的取消,以及Blocklist Node的更新等多种操作。
总而言之,ApplicationMaster作为客户端和Yarn的中间方,其资源申请的方法allocateResource()在逻辑上的功能为:

  1. 粒度转换: 将Task级别的资源请求,转换为Container(Executor级别的资源请求)。这是一个游戏到粗的粒度的转换。
  2. 维度转换: Driver发过来的资源请求是资源的最终全局状态,而Yarn 的 API 要求的针对每一个Container进行增量请求。因此,allocateResources()会将Driver发送过来资源请求的最终状态,对比当前系统已经运行、分配未运行、已经发送请求但是还没有分配资源等等已经存在的状态,确定一个发送给Yarn的增量请求状态。这是一个全量到增量的维度的转换。
  3. 角度转换: Driver发过来的每个Task都带有各自Task的Locality,而发送给Yarn的Container请求又是带有Locality需求的Container需求。这是一个从Task到Container的角度的转换。

ApplicationMaster端的allocateResources()方法的基本流程在代码YarnAllocator.allocateResources()中:

   ---------------------------------- YarnAllocator ------------------------------------
  def allocateResources(): Unit = synchronized {
   
   
    
    updateResourceRequests() // 与Yarn进行资源交互
    
    val allocateResponse = amClient.allocate(progressIndicator) // 从Yarn端获取资源结果,包括新分配的、已经结束的等等

    val allocatedContainers = allocateResponse.getAllocatedContainers()
	handleAllocatedContainers(allocatedContainers.asScala) // 处理新分配的Container
    val completedContainers = allocateResponse.getCompletedContainersStatuses() // 处理已经结束的Container
     processCompletedContainers(completedContainers.asScala)
    }
  }

ApplicationMaster端的allocateResources()的基本流程如下图所示:
在这里插入图片描述

  1. 生成资源请求,即将Driver发送过来的全量的、Task粒度的资源请求和Host偏好信息,转换为对Yarn的、以Executor为粒度的资源请求
    updateResourceRequests()
    
  2. 将资源请求发送给Yarn并从Yarn上获取分配结果(基于Yarn的异步调度策略,这次获取的记过并非本次资源请求的分配结果)以进行后续处理:
    val allocatedContainers = allocateResponse.getAllocatedContainers()
    handleAllocatedContainers(allocatedContainers.asScala)
    val completedContainers = allocateResponse.getCompletedContainersStatuses()
    processCompletedContainers(completedContainers.asScala)
    

可以看到,updateResourceRequests()是资源请求的核心方法,它会负责同Yarn进行通信以进行资源请求。
《超时导致SparkContext构造失败的问题探究》中,我们也介绍过,生成资源请求,其决策过程发生在方法updateResourceRequests()中。我们主要来看updateResourceRequests()方法:

   ---------------------------------- YarnAllocator ------------------------------------
  def updateResourceRequests(): Unit = {
   
   
    // 获取已经发送给Yarn但是待分配的ContainerRequest,计算待分配容器请求的数量
    // 这些ContainerRequest是之前通过调用amClient.addContainerRequest 发送出去的
    val pendingAllocate = getPendingAllocate
    val numPendingAllocate = pendingAllocate.size
    // 还没有发送请求的executor的数量
    val missing = targetNumExecutors - numPendingAllocate -
      numExecutorsStarting.get - numExecutorsRunning.get
    // 还没有发送给Yarn的资源请求
    if (missing > 0) {
   
         
        /**
       * 将待处理的container请求分为三组:本地匹配列表、本地不匹配列表和非本地列表。
       */
      val (localRequests, staleRequests, anyHostRequests) = splitPendingAllocationsByLocality(
        hostToLocalTaskCounts, pendingAllocate)

      // staleRequests 的意思是,ApplicationMaster已经请求了这个Container,
      // 但是这个ContainerRequest所要求的hosts里面没有一个是在 hostToLocalTaskCounts (即task所倾向于)中的,因此,需要取消这个Container Request,因为已经没有意义了
      // cancel "stale" requests for locations that are no longer needed
      staleRequests.foreach {
   
    stale =>
        amClient.removeContainerRequest(stale)
      }
      val cancelledContainers = staleRequests.size
 
      // consider the number of new containers and cancelled stale containers available
      // 将新的container请求,以及刚刚取消的container,作为available container
      val availableContainers = missing + cancelledContainers

      // to maximize locality, include requests with no locality preference that can be cancelled
      // 在availableContainers的基础上,再算上没有任何locality要求的并且还没有分配成功的container
      val potentialContainers = availableContainers + anyHostRequests.size
      // LocalityPreferredContainerPlacementStrategy,计算每一个Container 的Node locality和 Rack locality
      val containerLocalityPreferences = containerPlacementStrategy.localityOfRequestedContainers(
        potentialContainers, numLocalityAwareTasks, hostToLocalTaskCounts,
          allocatedHostToContainersMap, localRequests)

      val newLocalityRequests = new mutable.ArrayBuffer[ContainerRequest]
      // 遍历ContainerLocalityPreferences数组中的每一个ContainerLocalityPreferences
      containerLocalityPreferences.foreach {
   
   
        case ContainerLocalityPreferences(nodes, racks) if nodes != null =>
          newLocalityRequests += createContainerRequest(resource, nodes, racks)// 根据获取的locality,重新创建ContainerRequest请求
      }
      // 除了有locality需求的container以外,还有更多的available container需要被请求,因此对这些container请求也发送出去
      if (availableContainers >= newLocalityRequests.size) {
   
   
        // more containers are available than needed for locality, fill in requests for any host
        for (i <- 0 until (availableContainers - newLocalityRequests.size)) {
   
   
          newLocalityRequests += createContainerRequest(resource, null, null) // 构造ContainerRequest对象
        }
      } else {
   
   
        val numToCancel = newLocalityRequests.size - availableContainers
        // cancel some requests without locality preferences to schedule more local containers
        anyHostRequests.slice(0, numToCancel).foreach {
   
    nonLocal =>
          amClient.removeContainerRequest(nonLocal)
        }

      }
    } else if (numPendingAllocate > 0 && missing < 0) {
   
   
      val numToCancel = math.min(numPendingAllocate, -missing)

      val matchingRequests = amClient.getMatchingRequests(RM_REQUEST_PRIORITY, ANY_HOST, resource)
      matchingRequests.iterator().next().asScala
  		.take(numToCancel).foreach(amClient.removeContainerRequest)
    }
  }

其基本过程为:

  1. 获取当前Pending的request(已经发送给Yarn但是还没有分配Container的请求),并将这些Pending的请求按照本地性的需求进行切分。这里的基本意图是,当前收到了来自Driver的全局的资源状态信息,而在Yarn上还有一部分之前的资源请求还没有分配Container,那么,会不会这些Pending Requewt中有些Request已经不需要了(满足不了任何一个task的locality需求)

          val (localRequests, staleRequests, anyHostRequests) = splitPendingAllocationsByLocality(
            hostToLocalTaskCounts, pendingAllocate)
    

    切分的过程,就是查看当前在Yarn这一端pending的所有的Container的locality与我们目前需求的所有(全局)的Task的locality的交集:

    -------------------------------------- YarnAllocator ----------------------------------
      private def splitPendingAllocationsByLocality(
          hostToLocalTaskCount: Map[String, Int], // 每一个host到希望分配上去的task的数量
          pendingAllocations: Seq[ContainerRequest] // 还没有分配出去的ContainerRequest
        ): (Seq[ContainerRequest], Seq[ContainerRequest], Seq[ContainerRequest]) = {
         
         
        val localityMatched = ArrayBuffer[ContainerRequest]()
        val localityUnMatched = ArrayBuffer[ContainerRequest]()
        val localityFree = ArrayBuffer[ContainerRequest]()
    
        val preferredHosts = hostToLocalTaskCount.keySet
        // 将当前已经发送给Yarn但是还没有分配的Container的请求进行切分
        pendingAllocations.foreach {
         
          cr =>
          val nodes = cr.getNodes // 这个 ContainerRequest 对节点的要求
          if (nodes == null) {
         
         
            localityFree += cr // 这个ContainerRequest对nodes没有要求,那么就是对本地性没有要求的Container请求
          } else if (nodes.asScala.toSet.intersect(preferredHosts).nonEmpty) {
         
          // 这个Container的本地性要求和task期望分配的hosts集合有交集
            localityMatched += cr // 把这个ContainerRequest添加到localityMatched的ContainerRequest集合中去
          } else {
         
          // 这个Container的本地性要求和task期望分配的hosts集合没有交集
            localityUnMatched += cr // 把这个ContainerRequest添加到localityMatched的ContainerRequest集合中去
          }
        }
        // 切分结果 (localRequests, staleRequests, anyHostRequests)
        (localityMatched.toSeq, localityUnMatched.toSeq, localityFree.toSeq)
      }
    
    }
    

    切分过程的具体流程如下图所示:
    请添加图片描述

    切分的具体流程为 :

    • 如果这个Pending Container没有任何的locality要求,那么就是localityFree Container,即,其实际分配的位置有可能是当前所有tasks所希望的位置,也可能不是,那么这个container就是localityFree container
      if (nodes == null) {
             
             
              localityFree += cr // 这个ContainerRequest对nodes没有要求,那么就是对本地性没有要求的Container请求
            } 
      
    • 如果这个Pending Container有locality 要求,并且这个locality的nodes与当前所有tasks有交集,那么这个Pending Container就被划分为localityMatched,显然,这个Pending Container是不应该被取消的;
      else if (nodes.asScala.toSet.intersect(preferredHosts).nonEmpty) {
             
              // 这个Container的本地性要求和task期望分配的hosts集合有交集
              localityMatched += cr // 把这个ContainerRequest添加到localityMatched的ContainerRequest集合中去
            }
      
    • 如果这个Pending Container有locality要求,但是这个locality中的nodes不在当前所有tasks的locality中的任何一个节点,即这个Pending Container实际分配的位置不可能是任何一个task所倾向于的位置,那么这个Pending Container就是localityUnMatched,显然,localityUnMatched container目前无法放置任何一个task,需要取消掉;
      else {
             
              // 这个Container的本地性要求和task期望分配的hosts集合没有交集
              localityUnMatched += cr // 把这个ContainerRequest添加到localityMatched的ContainerRequest集合中去
            }
      
  2. 对于localityUnmatched container,向Yarn发送请求,取消这种Container,这些被取消的Container在后面会重新申请,以便在申请的资源总量不变的情况下增强资源的本地特性:

     staleRequests.foreach {
         
          stale =>
       amClient.removeContainerRequest(stale)
     }
    
  3. 计算总的Container的数量,包括:

    • Pending Container中刚刚cancel的container的数量,这些Container刚刚取消了,我们可以再次申请这些Container,但是肯定会增强这些新的资源请求的locality,以最大化我们的Task的locality
    • Pending Container中的locality free的Container数量,这些Container可能分配在集群中的任何地方
    • 新增(missing)的Container请求,即当前的总的container请求中除去正在运行(已经有task在运行,numExecutorsRunning)和正在启动(已经分配但是还没分配task,numExecutorsStarting)的,再除去所有的pending的container(numPendingAllocate,是从Yarn的API中获取的数量,已经请求但是还没有分配成功的资源),多出来的Container:
    	// 还没有发送请求的executor的数量
    	val missing = targetNumExecutors - numPendingAllocate -
    	 numExecutorsStarting.get - numExecutorsRunning.get 
    	.....
    	// consider the number of new containers and cancelled stale containers available
    	// 将新的container请求,以及刚刚取消的container,作为available container
    	val availableContainers = missing + cancelledContainers
    	
    	// to maximize locality, include requests with no locality preference that can be cancelled
    	// 在availableContainers的基础上,再算上没有任何locality要求的并且还没有分配成功的container
    	val potentialContainers = availableContainers + anyHostRequests.size
    

    在这里,val availableContainers = missing + cancelledContainers,即available container代表这次可以增量申请的最大的container数量,包括了这次的额外需求,以及刚刚取消的container(取消的container可以重新申请)

  4. 构建Container请求。这里会根据LocalityPreferredContainerPlacementStrategy的localityOfRequestedContainers来构建Container请求,返回Array[ContainerLocalityPreferences],每一个ContainerLocalityPreferences代表了一个带有对应host和rack信息的Container请求:

       val containerLocalityPreferences = containerPlacementStrategy.localityOfRequestedContainers(
         potentialContainers, numLocalityAwareTasks, hostToLocalTaskCounts,
           allocatedHostToContainersMap, localRequests)
    
  5. 根据ContainerLocalityPreferences,转换成Yarn的 ContainerRequest

       containerLocalityPreferences.foreach {
         
         
         case ContainerLocalityPreferences(nodes, racks) if nodes != null =>
           newLocalityRequests += createContainerRequest(resource, nodes, racks)// 根据获取的locality,重新创建ContainerRequest请求
         case _ =>
       }
    
  6. 如果可以申请的Container(available container)的数量大于刚刚计算完locality的Container数量,那么,为了将申请配额用尽,就再申请其相差的部分Container, 保证申请的Container的数量不小于Available Container的数量。

     if (availableContainers >= newLocalityRequests.size) {
         
         
       // more containers are available than needed for locality, fill in requests for any host
       for (i <- 0 until (availableContainers - newLocalityRequests.size)) {
         
         
         newLocalityRequests += createContainerRequest(resource, null, null) // 构造ContainerRequest对象
       }
     }
    
  7. 如果可以申请的Container(available container)的数量小于刚刚计算完locality的Container数量,那么需要取消一部分container:

    else {
         
         
           val numToCancel = newLocalityRequests.size - availableContainers
           // cancel some requests without locality preferences to schedule more local containers
           anyHostRequests.slice(0, numToCancel).foreach {
         
          nonLocal =>
             amClient.removeContainerRequest(nonLocal)
           }
    
  8. 调用Yarn的标准接口addContainerRequest(),将ContainerRequest发送给Yarn(其实这个接口并不会真正将请求发送出去,只会存放在RMAMClient端,真正发送是通过allocate()接口):

    newLocalityRequests.foreach {
         
          request =>
      amClient.addContainerRequest(request)
    } // 在这里发送container的请求,从日志来看,资源请求已经发出来了,Yarn已经处理了
    

所以,从上面可以看到,最关键的方法是LocalityPreferredContainerPlacementStrategy.localityOfRequestedContainers()方法,它根据当前的已有信息(总共的Container需求,有locality需求的task的数量,这些locality分布在每一个task上的数量等),生成一个Array[ContainerLocalityPreferences]数组,数组中的每一个元素代表了一个Container的需求,并包含了其locality的要求信息,然后基于生成的ContainerLocalityPreferences经过转换成ContainerRequest,发送给Yarn。

资源申请的生成过程详解

资源申请的生成,就是根据当前集群运行的基本情况,Task的基本需求,生成Yarn上的资源请求的过程。

资源申请的生成过程的简单例子

在了解其具体实现以前,我们以具体例子的方式,看一下localityOfRequestedContainers()方法的基本实现逻辑,从而对其动机和达成的效果有一个很好的理解,然后,我们再看其实现细节。

  1. 从任务调度去看,看到的是Task以及每个 Task的Locality倾向。比如,现在我们一共需要为30个Task分配资源,其中,20个Task的locality倾向为Host1,Host2,Host3,10个Task的Locality倾向为Host1, Host2, Host4, 因此,对应到每个Host上的Task权重如下表所示:

    Host 1 Host 2 Host 3 Host 4
    20 Tasks 20 20 20
    10 Tasks 10 10 10
    Sum of Tasks 30 30 20 10

    即,20个Task希望分配在Host1,Host2,Host3中的任何一个,有10个Task希望分配在Host1, Host2, Host4中的任何一个。如上表所示,综合来看,所有Task在四台机器上分配的权重是(30, 30, 20,10)

  2. 假设一个Task需要的vCore是1,而一个Container(Executor)有2个vCore,因此,转换成Container以后的结果如下表所示:

    Host 1 Host 2 Host 3 Host 4
    20 Tasks 20 20 20
    10 Tasks 10 10 10
    Sum of Tasks 30 30 20 10
    Sum of Containers 15 15 10 5

    上面的Sum of Containers的数字只是表示一个比例值,并不表示对应的Host上实际需要申请的Container的数量,我们实际需要的总的Container数量才15个。那么,这15个Container需求平均到每台Host上是多少呢?

  3. 比如Host 1的Sum of Container 为15, 所有Host的Sum of Container 是45,因此占比是1/3,所以平均下来分配到Host1上的Container数量应该是 15 * 1/3 = 5。经过向上取整(宁可稍微多分配也不要少分配)以后,每台机器所平均到的15个Container需求是:

    Host 1 Host 2 Host 3 Host 4
    20 Tasks 20 20 20
    10 Tasks 10 10 10
    Sum of Tasks 30 30 20 10
    Sum of Containers 15 15 10 5
    Allocated Container Target 5 5 4 2
  4. 在这里,计算完总的Allocated Container Target以后,需要减去当前已经在该Host上已经存在(正在运行或者在这个Host上pending的Container),因为我们最终发送给Yarn的Container请求是增量请求。假设现在在每一个Host上已经存在的Container数量都是1,即15个Container中有4个Container是已经分配的,那么,减去已经存在的Container数量以后的结果如下表所示,所以,我们需要新申请12个Container:

    Host 1 Host 2 Host 3 Host 4
    20 Tasks 20 20 20
    10 Tasks 10 10 10
    Sum of Tasks 30 30 20 10
    Sum of Containers 15 15 12 6
    Allocated Container Target 5 5 4 2
    Newly Allocated Container 4 4 3 1
  5. 将每个Host的Newly Allocated Container按照比例进行缩放,保证比例最大的那个Host(这里是Host1 和 Host 2)的比例值是需要新申请的Container的数量。在这里,扩大因子应该是 12(Container的总数量)/4(比例最大的Host的Average Allocated Container) = 3 :

    Host 1 Host 2 Host 3 Host 4
    20 Tasks 20 20 20
    10 Tasks 10 10 10
    Sum of Tasks 30 30 20 10
    Sum of Containers 15 15 10 5
    Allocated Container Target 5 5 4 2
    Newly Allocated Container 4 4 3 1
    Round Up 12 12 9 3
  6. 开始发起资源请求。每一个Container请求的Locality中包含的Host如下表所示:

    Host 1 Host 2 Host 3 Host 3
    3 Containers
    6 Containers
    3 Containers

    其含义是:

    • 3个Container请求的Locality是[Host1, Host2, Host3, Host4],即请求Yarn分配3个Container,并且尽量将它们分配在这4个Hosts中。此时剩下的Host比例为[9:9:6:0]
    • 6个Container请求的Locality是[Host1, Host2, Host3],即请求Yarn分配6个Container,并且尽量将它们分配在这3个Hosts中,此时剩下的Host中container比例为[3:3:0]
    • 3个Container请求的Locality是[Host1, Host2],即请求Yarn分配3个Container,并且尽量将它们分配在这2个Hosts中

    这样,所有Host的Container比例就是12:12:9:3,平均到12个需分配的Container以后的比例是4:4:3:1,再加上已经分配在每个host上的1个Container,那么总的Container在每个Host上的比例就是5:5:4:2,这个比例和我们直接根据每个Host的task比例折算成的Container的比例15:15:10:5是大致相近的。
    到了这里,我们可以理解了,为什么我们需要在 步骤5 做Round Up操作,并且Round Up的目标是将目前比例值最大的Host的比例值扩大为当前Container需求的最大值? 因为在步骤6中生成Container请求的时候,比例值最大的Host的比例值肯定是等于需要申请的Container数量的。

资源调度算法的代码解析

上面以实际例子解释了Spark将当前的Task的Locality需求信息转换成Yarn的资源请求的细节。下面,我们结合代码,详细看一下localityOfRequestedContainers()方法的实现细节:

  def localityOfRequestedContainers(
      numContainer: Int, // 需要进行计算的container的数量,包括missing的,cancel掉的(本地性不符合任何task要求的pending container),以及对本地性没有要求的pending的container
      numLocalityAwareTasks: Int, // 对locality有要求的task的数量,这个是Driver端通过stageIdToExecutorPlacementHints计算然后通过RequestExecutor传递过来的

      hostToLocalTaskCount: Map[String, Int], // 在Stage提交了以后,这个map里面保存了从host到期望分配到这个host的task的数量,这个是Driver端通过stageIdToExecutorPlacementHints传递过来的
      allocatedHostToContainersMap: HashMap[String, Set[ContainerId]], // 已经launch起来的host -> container的映射关系
      localityMatchedPendingAllocations: Seq[ContainerRequest] // 对本地性有要求的pending的container
    ): Array[ContainerLocalityPreferences] = {
   
   
    //  预期的从host到期望在上面再launch的新的container数量的映射关系
    val updatedHostToContainerCount = expectedHostToContainerCount(
      numLocalityAwareTasks, hostToLocalTaskCount, allocatedHostToContainersMap,
        localityMatchedPendingAllocations)
    // 希望再launch的所有Host上的container的数量之和,在这里的例子中,是15
    val updatedLocalityAwareContainerNum = updatedHostToContainerCount.values.sum

    // The number of containers to allocate, divided into two groups, one with preferred locality,
    // and the other without locality preference.
    //  没有locality需求的container的数量
    val requiredLocalityFreeContainerNum =
      math.max(0, numContainer - updatedLocalityAwareContainerNum)

    //  有locality需求的container的数量
    val requiredLocalityAwareContainerNum = numContainer - requiredLocalityFreeContainerNum

    val containerLocalityPreferences = ArrayBuffer[ContainerLocalityPreferences]()
    if (requiredLocalityFreeContainerNum > 0) {
   
    // 如果有container是没有locality需求的
      for (i <- 0 until requiredLocalityFreeContainerNum) {
   
   
        containerLocalityPreferences += ContainerLocalityPreferences( // 为这些没有locality需求的container一一创建container需求
          null.asInstanceOf[Array[String]], null.asInstanceOf[Array[String]])
      }
    }

    if (requiredLocalityAwareContainerNum > 0) {
   
    // 如果有container有locality需求
      val largestRatio = updatedHostToContainerCount.values.max // 全局的所有host中最大的container数量
      // Round the ratio of preferred locality to the number of locality required container
      // number, which is used for locality preferred host calculating.
      var preferredLocalityRatio = updatedHostToContainerCount.map {
   
    case(k, ratio) =>
        val adjustedRatio = ratio.toDouble * requiredLocalityAwareContainerNum / largestRatio
        (k, adjustedRatio.ceil.toInt) // 往上取整
      }
      // 每个有locality需求的的Container request,为他们确定对应的hosts和rack
      for (i <- 0 until requiredLocalityAwareContainerNum) {
   
   
        // Only filter out the ratio which is larger than 0, which means the current host can
        // still be allocated with new container request.
        val hosts = preferredLocalityRatio.filter(_._2 > 0).keys.toArray // 还有container可以分配的一个或者多个hosts
        val racks = hosts.map {
   
    h =>
          resolver.resolve(yarnConf, h) // 解析这些host所在的rack
        }.toSet
        // 每一个ContainerLocalityPreferences代表一个Container
        containerLocalityPreferences += ContainerLocalityPreferences(hosts, racks.toArray)

        // Minus 1 each time when the host is used. When the current ratio is 0,
        // which means all the required ratio is satisfied, this host will not be allocated again.
        preferredLocalityRatio = preferredLocalityRatio.map {
   
    case (k, v) => (k, v - 1) }
      }
    }
    // containerLocalityPreferences中的每一项都会变成一个新的Container Request
    containerLocalityPreferences.toArray
  }

其参数的基本含义是:

  • numContainer: Int 需要进行计算的Container的数量,即可能进行分配的Container数量,包括Miss的container(还没有申请的Container),Cancel掉的(本地性不符合任何task要求,因此已经从Yarn上取消的pending container)。同时,还包括Pending Container中对本地性没有要求的Container,这一部分Container也是我们重新申请的对象,以最大化Locality。上文讲到过的updateResourceRequests()方法中的potentialContainers就是传入到该方法的numContainers参数:

       // 将新的container请求,以及刚刚取消的container,作为available container
       val availableContainers = missing + cancelledContainers
    
       // to maximize locality, include requests with no locality preference that can be cancelled
       // 在availableContainers的基础上,再算上没有任何locality要求的并且还没有分配成功的container
       val potentialContainers = availableContainers + anyHostRequests.size
    
  • numLocalityAwareTasks: Int 对locality有要求的task的数量,这个是Driver端通过对stageIdToExecutorPlacementHints计算然后通过RequestExecutor传递过来的数值。已经说过,这是此时的全局状态量,而不是一个增量;

  • hostToLocalTaskCount: Map[String, Int] 在Stage提交了以后,这个map里面保存了从host到期望分配到这个host的task的数量,这个是Driver端通过stageIdToExecutorPlacementHints传递过来的,具体过程是:

    • 在Driver端,ExecutorAllocationManager的onStageSubmitted回调中,会将这个Stage的task preference存放在stageIdToExecutorPlacementHints中。

      ----------------------------------------- ExecutorAllocationManager ----------------------------------------
      override def onStageSubmitted(stageSubmitted: SparkListenerStageSubmitted): Unit = {
             
             
        .....
          // 计算这个stage在每一个host上的task数量
          // Compute the number of tasks requested by the stage on each host
          var numTasksPending = 0
          val hostToLocalTaskCountPerStage = new mutable.HashMap[String, Int]()
          stageSubmitted.stageInfo.taskLocalityPreferences.foreach {
             
              locality =>
            // 对于每一个task的prefered location的list
            numTasksPending += 1
              // 对于这个task的每一个 preferred location
            locality.foreach {
             
              location => // 对于这个locality中的每一个location
             // 这个host上的task的数量+1
             val count = hostToLocalTaskCountPerStage.getOrElse(location.host, 0) + 1
             hostToLocalTaskCountPerStage(location.host) = count
            }
          }
          // 这个map的key是stage id,value是一个元组,记录了这个stage的pending的task的数量,以及从host到task count的map信息
          stageIdToExecutorPlacementHints.put(stageId,
            (numTasksPending, hostToLocalTaskCountPerStage.toMap))
          updateExecutorPlacementHints() 
      
    • 随后,ExecutorAllocationManager会有线程不断将这些信息通过RequestExecutors发送给远程的ApplicationMaster:

        def start(): Unit = {
             
             
          listenerBus.addToManagementQueue(listener)
      
          val scheduleTask = new Runnable() {
             
             
            override def run(): Unit = {
             
             
              schedule() // 这里会根据需要更新numExecutorsTarget的数量,也会调用
            }
          }
          executor.scheduleWithFixedDelay(scheduleTask, 0, intervalMillis, TimeUnit.MILLISECONDS)
          client.requestTotalExecutors(numExecutorsTarget, localityAwareTasks, hostToLocalTaskCount)
        }
      
  • allocatedHostToContainersMap: HashMap[String, Set[ContainerId]] 已经launch起来的host -> container的映射关系。这是updateResource()方法每次通过Yarn的标准API allocate()向Yarn询问以后获取的结果。我们说过,allocate()接口用来向Yarn发送本次的资源请求,并返回当前Yarn为这个Application分配的Container的结果。由于Yarn端的资源分配是异步分配,因此allocate()返回的结果并非是这次请求的资源的分配结果,而是两次相邻的allocate()请求发生之间的新产生的资源分配结果

  • localityMatchedPendingAllocations: Seq[ContainerRequest] 对本地性有要求的pending的container,其在方法splitPendingAllocationsByLocality()中对Pending的Container的Locality状态进行切分后,那些与当前请求的Task的Locality有交集的Pending Container将作为已经存在的Container,整个资源请求的目标,是使得新申请的Container和已经分配的Container加起来,其资源倾向和所有Task的统计倾向尽量匹配,从而最大程度满足Task的本地性需求。

localityOfRequestedContainers()算法的基本过程为:

  1. 计算每一个Host上应该新分配的Container的数量的预期值。由于是新分配的Container的预期值,因此需要先根据每个Host上的预期存在的Container的总的数量,减去该Host上已经存在的Container:

    val updatedHostToContainerCount = expectedHostToContainerCount(
      numLocalityAwareTasks, hostToLocalTaskCount, allocatedHostToContainersMap,
        localityMatchedPendingAllocations)
    

    这里的计算,就是完成下表中从Sum of Tasks(每个机器上分配到的Task的比例) 到 Sum of Containers (每个机器上分配的Container的比例)的转换,然后根据Sum of Containers 减去每台机器上已经分配的Container,就得到了Average Allocated Container Total(每台机器上应该新分配的Container的数量):

    Host 1 Host 2 Host 3 Host 4
    20 Tasks 20 20 20
    10 Tasks 10 10 10
    Sum of Tasks 30 30 20 10
    Sum of Containers 15 15 12 6
    Allocated Container Target 5 5 4 2
    Newly-Allocated Container 4 4 3 1
  2. 根据上面计算的分配结果,统计没有locality需求的Container

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值