Yarn中资源调度模型及分配

资源调度模型

  •  在YARN的资源分配过程中,其采用了双层资源调度模型:
    • 在第一层中,ResourceManager中的资源调度器将资源分配给各个ApplicationMaster;
    • 在第二层中,ApplicationMaster再进一步将资源分配给它内部的各个任务;
  • YARN的资源分配过程是异步的,也就是说,资源调度器将资源分配给一个应用程序后,它不会立刻push给对应的ApplicationMaster,而是暂时放到一个缓冲区中,等待ApplicationMaster通过周期性的心跳主动来取(pull-based通信模型)
  • YARN采用了增量资源分配机制(当应用程序申请的资源暂时无法保证时,为应用程序预留一个节点上的资源直到累计释放的空闲资源满足应用程序需求),这种机制会造成浪费,但不会出现饿死现象;
  • YARN资源调度器采用了主资源公平调度算法,DRF的基本设计思想则是将最大最小化公平算法应用于主资源上,进而将多维资源调度问题转化为单资源调度问题,即DRF总是最大化所有主资源中最小的;

双层资源调度模型

  在Yarn中,其资源分配及调度的过程及示意图如下:

  1. NodeManager通过周期性心跳汇报节点信息。
  2. ResourceManager为NodeManager返回一个心跳应答,包括需释放的Container列表等信息。
  3. ResourceManager收到来自NodeManager的信息后,会触发一个NODE_UPDATE事件。
  4. ResourceScheduler收到NODE_UPDATE事件后,会按照一定的策略将该节点上的资源(步骤2中有释放的资源)分配给各应用程序,并将分配结果放到一个内存数据结构中。
  5. 应用程序的ApplicationMaster向ResourceManager发送周期性的心跳,以领取最新分配的Container。
  6. ResourceManager收到来自ApplicationMaster心跳信息后,为它分配的container将以心跳应答的形式返回给ApplicationMaster。
  7. ApplicationMaster收到新分配的container列表后,会将这些Container进一步分配给它内部的各个任务。

       资源调度器关注的是步骤4中采用的策略,即如何将节点上空闲的资源分配给各个应用程序,至于步骤7中的策略,则完全由用户应用程序自己决定。

资源保证机制

   在分布式计算中,资源调度器需要选择合适峰资源保证这样的机制,当应用程序申请的资源暂时无法保证时:

  1. 是优先为应用程序预留一个节点上的资源直到累计释放的空闲资源满足应用程序需求(称为“增量资源分配”)
  2. 还是暂时放弃当前资源直到出现一个节点剩余资源一次性满足应用程序需求(称为“一次性资源分配”)

   这两种机制均存在优缺点,对于增量资源分配来说,资源预留会导致资源浪费,降低集群资源利用率;而一次性资源分配则会产生饿死现象,即应用程序可能永远等不到满足资源需求的节点出现。在YARN中其采用了增量资源分配机制,尽管这种机制会造成浪费,但不会出现饿死现象。

资源分配算法

        YARN资源调度器采用了主资源公平调度算法(Dominant Resource Fairness,DRF),该算法扩展了最大最小公平(max-min fairness)算法,使其能够支持多维资源的调度。由于DRF被证明非常适合应用于多资源和复杂需求的环境中,因此被越来越多的系统采用,包括Apache Mesos。

        在DRF算法中,将所需份额(资源比例)最大的资源称为主资源,而DRF的基本设计思想则是将最大最小公平算法应用于主资源上,进而将多维资源调度问题转化为单资源调度问题,即DRF总是最大化所有主资源中最小的,其算法伪代码如下:

      示例:考虑一个有9个cpu和18GB的系统,有两个用户:用户A的每个任务都请求(1CPU,4GB)资源;用户B的每个任务都请求(3CPU,4GB)资源。如何为这种情况构建一个公平分配策略?如下,通过列不等式方程可以解得给用户A分配3份资源,用户B分配2份资源是一个很好的选择。

     其使用根据DRF的思路,分配的过程如下表所示,注意,每一次选择为哪个资源分配的决定,取决于上次分配之后,目前dominant share最小的那个用户可以得到下一次的资源分配。

     最终可以看到在这个例子中,用户A的CPU占总CPU 1/9,MEM占总MEM的 2/9,而用户B的CPU占1/3,MEM占2/9,所以A的主资源为内存,B的主资源为CPU。基于这点,DRF会最大化A的内存的最小化分配,并会最大化B的CPU的最小分配。

hadoop中使用DominantResourceCalculator实现了cup、memory这两种主资源的公平调度;代码如下:

package org.apache.hadoop.yarn.util.resource;

import org.apache.hadoop.classification.InterfaceAudience.Private;
import org.apache.hadoop.classification.InterfaceStability.Unstable;
import org.apache.hadoop.yarn.api.records.Resource;

/**
 * A {@link ResourceCalculator} which uses the concept of  
 * <em>dominant resource</em> to compare multi-dimensional resources.
 *
 * Essentially the idea is that the in a multi-resource environment, 
 * the resource allocation should be determined by the dominant share 
 * of an entity (user or queue), which is the maximum share that the 
 * entity has been allocated of any resource. 
 * 
 * In a nutshell, it seeks to maximize the minimum dominant share across 
 * all entities. 
 * 
 * For example, if user A runs CPU-heavy tasks and user B runs
 * memory-heavy tasks, it attempts to equalize CPU share of user A 
 * with Memory-share of user B. 
 * 
 * In the single resource case, it reduces to max-min fairness for that resource.
 * 
 * See the Dominant Resource Fairness paper for more details:
 * www.cs.berkeley.edu/~matei/papers/2011/nsdi_drf.pdf
 */
@Private
@Unstable
public class DominantResourceCalculator extends ResourceCalculator {
  
  @Override
  public int compare(Resource clusterResource, Resource lhs, Resource rhs) {
    
    if (lhs.equals(rhs)) {
      return 0;
    }
    
    if (isInvalidDivisor(clusterResource)) {
      //除数为0,cup和memory一大一小
      if ((lhs.getMemory() < rhs.getMemory() && lhs.getVirtualCores() > rhs
          .getVirtualCores())
          || (lhs.getMemory() > rhs.getMemory() && lhs.getVirtualCores() < rhs
              .getVirtualCores())) {
        return 0;
      } else if (lhs.getMemory() > rhs.getMemory()
          || lhs.getVirtualCores() > rhs.getVirtualCores()) {
        return 1;
      } else if (lhs.getMemory() < rhs.getMemory()
          || lhs.getVirtualCores() < rhs.getVirtualCores()) {
        return -1;
      }
    }
    //比较主资源
    float l = getResourceAsValue(clusterResource, lhs, true);
    float r = getResourceAsValue(clusterResource, rhs, true);
    
    if (l < r) {
      return -1;
    } else if (l > r) {
      return 1;
    } else {
      //主资源相等,比较副资源占比
      l = getResourceAsValue(clusterResource, lhs, false);
      r = getResourceAsValue(clusterResource, rhs, false);
      if (l < r) {
        return -1;
      } else if (l > r) {
        return 1;
      }
    }
    
    return 0;
  }

  /**
   * Use 'dominant' for now since we only have 2 resources - gives us a slight
   * performance boost.
   * 
   * Once we add more resources, we'll need a more complicated (and slightly
   * less performant algorithm).
   */
  protected float getResourceAsValue(
      Resource clusterResource, Resource resource, boolean dominant) {
    // Just use 'dominant' resource
    return (dominant) ?
        Math.max(
            (float)resource.getMemory() / clusterResource.getMemory(), 
            (float)resource.getVirtualCores() / clusterResource.getVirtualCores()
            ) 
        :
          Math.min(
              (float)resource.getMemory() / clusterResource.getMemory(), 
              (float)resource.getVirtualCores() / clusterResource.getVirtualCores()
              ); 
  }
  
  @Override
  public int computeAvailableContainers(Resource available, Resource required) {
    return Math.min(
        available.getMemory() / required.getMemory(), 
        available.getVirtualCores() / required.getVirtualCores());
  }

  @Override
  public float divide(Resource clusterResource, 
      Resource numerator, Resource denominator) {
    return 
        getResourceAsValue(clusterResource, numerator, true) / 
        getResourceAsValue(clusterResource, denominator, true);
  }
  
  @Override
  public boolean isInvalidDivisor(Resource r) {
    if (r.getMemory() == 0.0f || r.getVirtualCores() == 0.0f) {
      return true;
    }
    return false;
  }

  @Override
  public float ratio(Resource a, Resource b) {
    return Math.max(
        (float)a.getMemory()/b.getMemory(), 
        (float)a.getVirtualCores()/b.getVirtualCores()
        );
  }

  @Override
  public Resource divideAndCeil(Resource numerator, int denominator) {
    return Resources.createResource(
        divideAndCeil(numerator.getMemory(), denominator),
        divideAndCeil(numerator.getVirtualCores(), denominator)
        );
  }

  @Override
  public Resource normalize(Resource r, Resource minimumResource,
                            Resource maximumResource, Resource stepFactor) {
    int normalizedMemory = Math.min(
      roundUp(
        Math.max(r.getMemory(), minimumResource.getMemory()),
        stepFactor.getMemory()),
      maximumResource.getMemory());
    int normalizedCores = Math.min(
      roundUp(
        Math.max(r.getVirtualCores(), minimumResource.getVirtualCores()),
        stepFactor.getVirtualCores()),
      maximumResource.getVirtualCores());
    return Resources.createResource(normalizedMemory,
      normalizedCores);
  }

  @Override
  public Resource roundUp(Resource r, Resource stepFactor) {
    return Resources.createResource(
        roundUp(r.getMemory(), stepFactor.getMemory()), 
        roundUp(r.getVirtualCores(), stepFactor.getVirtualCores())
        );
  }

  @Override
  public Resource roundDown(Resource r, Resource stepFactor) {
    return Resources.createResource(
        roundDown(r.getMemory(), stepFactor.getMemory()),
        roundDown(r.getVirtualCores(), stepFactor.getVirtualCores())
        );
  }

  @Override
  public Resource multiplyAndNormalizeUp(Resource r, double by,
      Resource stepFactor) {
    return Resources.createResource(
        roundUp(
            (int)Math.ceil(r.getMemory() * by), stepFactor.getMemory()),
        roundUp(
            (int)Math.ceil(r.getVirtualCores() * by), 
            stepFactor.getVirtualCores())
        );
  }

  @Override
  public Resource multiplyAndNormalizeDown(Resource r, double by,
      Resource stepFactor) {
    return Resources.createResource(
        roundDown(
            (int)(r.getMemory() * by), 
            stepFactor.getMemory()
            ),
        roundDown(
            (int)(r.getVirtualCores() * by), 
            stepFactor.getVirtualCores()
            )
        );
  }

}

资源调度器

Capacity Scheduler

       Capacity Scheduler是Yahoo开发的多用户调度器,它以队列为单位,每个队列可设定一定比例的资源最低保证和使用上限,同时每个用户也可以设定一定的资源使用上限以防止资源滥用。当一个队列的资源有剩余时,可暂时将剩余资源共享给其他队列;Capacity Scheduler具有如下几个特点:

  1. 容量保证:管理员可以为每个队列设置资源的最低保证和使用上限,而所有提交到该队列的应用程序共享这些资源;
  2. 灵活性:当一个队列的资源有剩余时,可暂时将剩余资源共享给其他队列,一旦该队列有闲的应用程序提交,则其他队列释放的资源会归还该队列;
  3. 多重租赁:支持多用户共享集群和多应用程序同时运行;
  4. 安全性保证:每个队列都有严格的ACL列表规定他的访问用户;
  5. 动态更新配置文件:管理员可根据需要动态修改各种配置参数,以实现集群的在线管理;

Capacity Scheduler配置参数

  • 资源分配相关参数
    • capacity: 队列的容量(百分比)
      maximum-capacity: 队列资源的使用上限(百分比)
      minimum-user-limit-percent:每个用户最低资源保障(百分比)
      user-limit-factor: 每个用户最多可使用的资源量(百分比)
  • 限制应用程序数目的配置参数
    • maximum-applications: 集群或者队列总同时处于等待和运行状态的应用程序数目上限,默认10000
      maximum-am-resource-percent: 集群中用于运行应用程序ApplicationMaster的资源比例上限,
                                                          该参数通常用于限制处于活动状态的应用程序的数目,默认0.1(10%)
  • 队列访问和权限控制参数
    • state: 队列状态stopped/running,可以实现队列的优雅退出
      acl_submit_application: 限定哪些用户或者用户组可向给定的队列中提交应用程序,具有继承性
      acl_administer_queue: 为队列指定一个管理员

      从以上常见的参数可知,Capacity Scheduler将整个系统资源划分为若干个队列,并且每个队列有较为严格的资源控制;通过这些控制Capacity Scheduler将hadoop集群在逻辑上划分为多个拥有相对独立资源的子集群,而这些子集群可以共享大集群的资源。

Capacity Scheduler的实现

  1. 应用程序的初始化:应用程序提交到RM上后,RM会向Capacity Scheduler发送一个APP_ADDED事件,Capacity Scheduler收到之后会创建一个FiCaschedulerApp对象用于跟踪和维护该应用程序的运行时信息,同时将该应用程序提交给对应的叶子队列;
  2. 资源调度:当RM收到来自NM的心跳信息后,会想Capacity Scheduler发送NODE_UPDATE事件,而Capacity Scheduler接收到该事件之后会完成以下操作:
    1. 处理心跳信息:NM发送的心跳信息中有两类信息需要资源资源调度器处理,一类是新启动Container,另一类是运行完成的Container,具体如下:
      1. 新启动的Container:资源调度器需要向RM发送一个RMContainerEventType.LAUNCHED事件,进而将该Container从超时队列中移除;
      2. 运行完成的Container:资源调度器将回收它的资源,然后资源再分配;
    2. 资源分配:用户提交应用程序后,应用程序会为对应的ApplicationMaster申请资源,而资源的表示方式是Container,Container表示信息如下图所示:

       Yarn中采用了三级资源分配策略,当一个节点上有空闲资源的时候,它会依次选择队列,应用程序,Container使用该资源,如下图所示:

  1. Yarn采用层次结构组织队列,将队列转换成树形结构,从根节点开始按照子队列的资源使用率从小到大遍历各个子队列,如果子队列是叶子节点,则进行步骤二寻找合适的Container,否则以该子队列为根节点重复以上过程;
  2. 步骤一选中一个叶子节点后Capacity Scheduler会按照应用程序的提交顺序进行排序,并遍历排序后的应用程序找到一个或者多个最合适的Container;
  3. 当步骤二选中给一个应用程序后,Capacity Scheduler将先满足优先级较高的Container,对于同一优先级的Container根据本地性来选择,它会一次选择node local,rack local和no local的Container;

Fair Scheduler

       Fair Scheduler是FaceBook开发的多用户调度器,同Capacity Scheduler类似,它以队列为单位划分资源,每个队列可以设置一定比例的资源最低保证和使用上限,同时每个用户也可设定一定的资源使用上限以防止资源滥用,当一个队列有资源剩余的时候可以共享给其他队列,Fair Scheduler和Capacity Scheduler的不同之处如下:

  • 资源公平共享:在每个队列中,Fair Scheduler可根据选择按照FiFO,Fair和DRF策略为应用程序分配资源,其中Fair策略是根据一种基于最大最小公平算法实现的资源多路复用方式;默认情况下,每个队列都按照该方式分配资源,如果一个队列中有两个应用程序,则每个应用程序可得到1/2的资源;
  • 支持资源抢占:当某个队列中有剩余资源时,调度器会将这些资源共享给其他队列,而当该队列中有新的应用程序提交时,调度器要为它回收资源,为了降低不必要的计算浪费,调度器会采用先等待再回收的策略,如果等待一段时间后未归还,则从哪些超额使用资源的队列中杀死一部分任务,进而释放资源;
  • 负载均衡:Fair Scheduler提供了一个基于任务数目的负载均衡机制,该机制尽可能的将系统总的任务均匀分配到各个节点;
  • 提高小应用的相应时间:由于采用最大最小公平算法,小应用可以快速获取资源并运行完成;

Fair Scheduler的实现

       Fair Scheduler的实现和Capacity Scheduler基本一致,同Capacity Scheduler不同的是Fair Scheduler提供了更多样化的调度策略,它允许每个队列单独配置调度策略,分别有:FIFO、Fair和DRF;调度策略在队列间和队列内部可以单独设置,对于叶子队列,他设置的调度策略决定了内部的应用程序的调度策略;对于非叶子节点,他设置的调度策略决定了各个子队列间的调度策略;同Capacity Scheduler一样,Fair Scheduler也采用了三级资源分配策略,即当一个节点有空闲资源时,它会依次选择队列,应用程序,Container,这里就不再重复叙述;其实Yarn采用的层次结构组织队列,实际存放应用程序的只有叶子队列,其他队列只是一个逻辑概念,用以辅助计算叶子队列的资源量;

Capacity Scheduler和Fair Scheduler的比较

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值