Yarn调度器的理解与优化

1 调度器的简介与概述

1.1 调度器的基础架构

  • 资源调度器是YARN最核心的组件之一,它是插拔式可配的,它定义了一整套接口规范以便用户可按照需要实现自己的调度器;

  • YARN自带了FifoScheduler(先入先出调度器)、CapacityScheduler(容量调度器)FairScheduler(公平调度器)三种常用资源调度器;
    在这里插入图片描述

  • 目前较多使用的调度器是FairScheduler , FairScheduler 它是多用户调度器,基于队列为单位划分资源,每个队列可设定一定比例的资源最低保证和使用上限,同时每个用户也可设定一定的资源使用上限以防止资源滥用;当一个队列的资源有剩余时,可暂时将剩余资源共享给其他队列,目标就是在整个时间线上让所有的applications平均的获取资源。

这块可以通过插拔特性和事件处理器两个角度来分析调度器在YARN中的基础架构。

1.1.1 插拔式组件

ResourceManager在初始化时会根据用户配置项(yarn-site.xml)来创建资源调度器对象。

protected ResourceScheduler createScheduler() {
  //获取调度器配置类名
    String schedulerClassName = conf.get(YarnConfiguration.RM_SCHEDULER,
        YarnConfiguration.DEFAULT_RM_SCHEDULER);
    LOG.info("Using Scheduler: " + schedulerClassName);
    try {
      Class<?> schedulerClazz = Class.forName(schedulerClassName);
      if (ResourceScheduler.class.isAssignableFrom(schedulerClazz)) {
        return (ResourceScheduler) ReflectionUtils.newInstance(schedulerClazz,
            this.conf);
      } else {
        throw new YarnRuntimeException("Class: " + schedulerClassName
            + " not instance of " + ResourceScheduler.class.getCanonicalName());
      }
    } catch (ClassNotFoundException e) {
      throw new YarnRuntimeException("Could not instantiate Scheduler: "
          + schedulerClassName, e);
    }
  }

所有资源调度器都需要实现ResourceScheduler(继承YarnScheduler和Recoverable)接口,具体代码如下:

public interface YarnScheduler extends EventHandler<SchedulerEvent> {
  /**获取队列信息
   * Get queue information
   * @param queueName queue name
   * @param includeChildQueues include child queues?
   * @param recursive get children queues?
   * @return queue information
   * @throws IOException
   */
  public QueueInfo getQueueInfo(String queueName, boolean includeChildQueues,
      boolean recursive) throws IOException;

  /**获取当前用户的队列ACL权限
   * Get acls for queues for current user.
   * @return acls for queues for current user
   */
  public List<QueueUserACLInfo> getQueueUserAclInfo();

  /**
   * Get the whole resource capacity of the cluster.
   * @return the whole resource capacity of the cluster.
   */
  @LimitedPrivate("yarn")
  @Unstable
  public Resource getClusterResource();

  /**获取调度器最少可分配的资源
   * Get minimum allocatable {@link Resource}.
   * @return minimum allocatable resource
   */
  public Resource getMinimumResourceCapability();

  /**获取调度器最多可分配的资源
   * Get maximum allocatable {@link Resource} at the cluster level.
   * @return maximum allocatable resource
   */
  public Resource getMaximumResourceCapability();

  public Resource getMaximumResourceCapability(String queueName);

  /**获取当前集群可用节点的数量*/
  public int getNumClusterNodes();

  /**AM与调度器的核心API*/
  
  Allocation
  allocate(ApplicationAttemptId appAttemptId,
      List<ResourceRequest> ask,
      List<SchedulingRequest> schedulingRequests,
      List<SchedulingRequest> deleteSchedulingRequests,
      List<ContainerId> release,
      List<String> blacklistAdditions,
      List<String> blacklistRemovals,
      List<ContainerResourceChangeRequest> increaseRequests,
      List<ContainerResourceChangeRequest> decreaseRequests);
  //接口较多
  ···········

  /**
   * 获取调度失败归因
   * @param applicationId 需要获取调度失败归因的App
   * @return 调度失败归因信息
   */
  ScheduleResult getScheduleResult(ApplicationId applicationId);
}
public interface Recoverable {
  //RM重启调用此借口恢复调度器的信息
  public void recover(RMState state) throws Exception;
}

总结来说,完成一个调度器需要实现以上接口内容。

1.1.2 事件处理器

Yarn的资源管理器本质是一个事件处理器,需要具有能处理外部6种Scheduler-EventType事件的能力,对应进行语义处理,如下图:

在这里插入图片描述

具体事件对应的含义可总结为以下表格:

事件具体操作
NODE_ADDED表示集群增加了一个节点资源调度器会触发增加节点资源量的动作。
NODE_REMOVED表示集群移除了节点,资源调度器会触发资源更新,可分配资源移除对应节点资源量。
APPLICATION_ADDED表示RM收到一个新的app作业,资源管理器需要新声明一个数据结构,存储资源需求。
APPLICATION_REMOVED表示特定app作业运行结束,
CONTAINER_EXPIREDconatiner被分配给某个AM后,如果在一定时间内没有使用该Conatiner,则会被回收再分配。
NODE_UPDATERM收到NM通过心跳机制汇报信息后,会出发该事件,可能会触发container释放,因此该事件会触发资源重新分配,所以该事件是核心时间

1.2 资源表示模型

Yarn支持内存与cpu两种资源类型的管理和分配,这里忽略对机器学习场景下gpu的调度。通过动态资源分配的方式,NM会在启动时向RM注册,其中信息就包括可分配的cpu和内存量,主要是配置项来实现。

配置项含义
yarn.nodemanager.resource.memory-mb可分配的无力内存总量。默认为8G。
yarn.nodemanager.vmem-pmem-ratio单位物理内存量对应最多可使用的虚拟内存量,默认值为2.1,则表示每使用1MB物理内存,可最多转化为2.1的虚拟内存总量。
yarn.nodemanager.resource.cpu-vcores可分配的虚拟cpu个数,默认为8。为了更细粒度的切分cpu的资源性能分配,可以将每个物理cpu切分为虚拟cpu。但是需要注意的是,用户设置的cpu数必须是整数。

为了更好的来分配资源,yarn内部包含一些调度语义,这一操作可能带来性能问题。语义一般包含在特定节点(node)或特定机架(rack)分配container,黑名单部分节点,请求归还部分资源。

在语义调度方面,笔者认为做的并不是很好,存在性能与语义广度的问题,这也是与k8s的差距所在之一,促使在机器学习场景下,由yarn转向开k8s。

1.3 资源调度模型

1.3.1 双层资源调度模型

Yarn采用的是双层调度的模型:在第一层,RM中的调度器分配的具体资源给相应的AM;在第二层,AM进一步将资源分配给其内部的各个任务。对于调度器本身,主要关注第一层的调度问题。Yarn的资源调度分配过程是异步的,资源调度器将资源分配给应用程序,并不是发送给对应的AM,而是存放在特定的缓冲区,等待AM的周期性心跳来获取。

一句话总结,Yarn采用的是pull-based通信模型,而不是push-based模型。

我们可以将调度流程总结为以下步骤:

  1. NM通过周期性心跳进行汇报节点信息
  2. RM为NM返回心跳答应,举例包括释放的container列表信息
  3. RM接收到来自NM的信息后,会触发NODE_UPDATE事件
  4. ResourceScheduler收到NODE_UPDATE事件后,会按照相关策略将资源分配给各应用程序,这里包括第二步释放的container,并将结果存储到相应的数据结构到中。
  5. 应用程序的AM会向RM发送周期性心跳,以领取新分配的container。
  6. RM收到AM心跳信息后,将分配的container以心跳应答的形式返回给AM。
  7. AM收到新分配的container列表后,会将这些Conatiner进一步分配给其内部的各个任务。

资源调度器资源分配流程,可参考下图:
在这里插入图片描述

资源分配流程图,资源调度器主要关注第四步,第七步由AM,即用户程序,自己决策。

1.3.2 资源保证机制

一般资源保证的讨论场景是在资源不足的情况下,对于资源保证策略可以分为两类,如下表:

分配策略相应操作缺陷
增量式分配(Incremental placement)资源无法全部满足时,优先为应用程序预留一个节点上的资源,当累计释放的空闲资源满足应用程序时,提供资源保证。资源预留可能会导致资源浪费,降低集群资源利用率。
一次性分配(all-or-nothing)同上场景下,暂时放弃当前资源直到出现一个节点可以全部满足资源需求。一次性资源分配会产生饿死现象,即应用程序可能永远分配不到满足资源需求的节点

总结:YARN采取了增量式的分配方式,避免饿死现象发生。对于gpu的分配,通过节点attribute和作业参数可以达到一次性分配的效果。

1.3.3 资源分配算法

1) max-min fairness(最大最小公平算法)

我们总会面临这样的问题,需要给一组用户分配一些稀缺资源,站在资源分配者的角度,他们具有等价的权利获取资源,但实际上,一些用户可能获取较少的资源就能够满足需要,这样看来,他们对资源的获取又不是完全等价的,似乎不适合去平分资源,因此就有了最大最小公平算法,这种算法分为带权和非权值两种,以下逐一介绍。

最大最小公平算法定义如下(不带权):

1、资源按照需求递增的顺序进行分配;

2、不存在用户获得的资源超过自身的需求;

3、对于未满足的用户,等价分享剩余资源;

算法实现逻辑(不带权):

首先假定用户集合有n个用户,1到n,他们对资源的需求已经排序完毕,满足s1<s2< … <sn,资源总量为S。

1、将资源S/n分配给需求最小的1用户,这很可能已经超出了用户1的需求;

2、将超出的部分回收,再次将(S-s1)/(n-1)的资源分配给用户2,依次次重复上述过程,直到某一次分给该用户的资源不满足该用户的需求;

3、假定当分配到第k个用户时,分配到的资源不能达到该用户的需求,那么就将剩下的资源,平均分配给未获得资源的所有用户,至此,分配任务结束。

【还有一种说法,是先将资源整体平分,再从小到大,将超出的资源平分给资源没有得到满足的用户,这两中做法的结果是一致的】

不带权举例

有用户组G,该组中有4个用户,资源需求分别为2,2.6,4,5,资源总量为10。避免长篇大论,这里直接以图的形式给出。

在这里插入图片描述

上面提到的是最基本的分配原则,但实际上往往并不是这么简单,每个用户往往具有不同的权重,因此就有了分配原则的扩展,带权重的最大最小分配原则。

最大最小化公平算法定义如下(带权):

1、通过权重实现分配的标准化;

2、不存在用户得到的资源超过自己的需求;

3、未得到满足的用户,按照权重共享资源;

带权值举例

有用户组G,该组中有4个用户,资源需求分别为2,4,4,10,权重分别为4,2.5,1,0.5资源总量为16。

1、首先对权重进行标准化,将最小权重设置为1,则权重变为8,5,2,1,总和为16。将总资源分为16等分,四个用户分别得到8,5,2,1。

2、用户1多获得了6份资源,用户2多获得了1份资源,用户3、4资源不满足,因此,将多出来的7份资源再按照权重分配给用户3、4,用户3,4分别再获得7*(2/3)、7*(1/3)份资源;

3、目前为止,用户3获得6.666份资源,用户4获得3.333,将用户3多出的资源再分配给用户4,完成分配。

具体过程如下图所示:

在这里插入图片描述

2)DRF(dominant resource fairness算法

DRF是一种通用的多资源的max-min fairness分配策略。DRF背后的直观想法是在多环境下一个用户的资源分配应该由用户的dominant share(主导份额的资源)决定,dominant share是在所有已经分配给用户的多种资源中,占据最大份额的一种资源。简而言之,DRF试图最大化所有用户中最小的dominant share。例如用户A运行CPU密集的任务而用户B运行内存密集的任务,DRF会试图均衡用户A的CPU资源份额和用户B的内存资源份额。在单个资源的情形下,那么DRF就会退化为max-min fairness。

DRF有四种主要特性,分别是:sharing incentive、strategy-proofness、Pareto efficiency和envy-freeness。DRF是通过确保系统的资源在用户之间是静态和均衡地进行分配来提供sharing incentive,用户不能获得比其他用户更多的资源。此外,DRF是strategy-proof,用户不能通过谎报其资源需求来获得更多的资源。DRF是Pareto-efficient,在满足其他特性的同时,分配所有可以利用的资源,不用取代现有的资源分配。最后,DRF是envy-free,用户不会更喜欢其他用户的资源分配。

一句话总结:确定主资源(资源需求比例最高的),将最大最小公平算法运用在主资源上,将多资源问题转化为单资源问题,核心是针对用户作业,DRF总是最大化主资源最小的作业。

应用实例

举例:考虑一个有9个cpu和18GB的系统,有两个用户:用户A的每个任务都请求(1CPU,4GB)资源;用户B的每个任务都请求(3CPU,4GB)资源。如何为这种情况构建一个公平分配策略?
.

在这里插入图片描述

.
通过列不等式方程可以解得给用户A分配3份资源,用户B分配2份资源是一个很好的选择。DRF的算法伪代码为:
.
在这里插入图片描述
.

使用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的最小分配。
.在这里插入图片描述

3)fair

以FS的fair算法为例,按照内存资源使用量比率进行调度,即used_ memory/minShare大小调度,核心思想是按照此算法决定调度顺序。

4)FIFO

以FS的FIFO算法为例,按照优先级高低调度,如优先级相同,则比较提交时间先后顺序调度,特殊情况下,可次级比较队列或者应用程序名称的字符串大小进行调度。

2 FairScheduler

2.1 特性

1)资源公平共享

主要体现在队列维度,可采取FIFO、Fair和DRF策略为应用程序分配资源。

2)支持资源抢占

当某个队列中有剩余资源时,调度器会将这些资源分配给其他队列,而当该队列中新的应用程序提交时,调度器为了尽可能降低不必要的计算浪费,采用了先等待再强制收回的策略。若等待一段时间后尚有未归还资源,则进行资源抢占:从那些超额使用资源的队列杀死一部分任务,进而释放资源。

3)负载均衡

Fair Scheduler提供了基于任务数目的负载均衡机制,该机制尽可能将系统中的任务均匀分配到各个节点上。另外,用户可根据自己的需要设计负载均衡机制。

4)提高小作业响应时间

采用最大最小公平算法,使小作业可以快速获取资源,并运行完成。

2.2 公平调度器的架构

2.2.1 调度器中的核心线程
  • AllocationFileLoaderService:负责公平策略配置文件(fair-scheulder.xml)的热加载
  • ContinuousSchedulingThread:负责进行持续的资源调度,与NodeManager的心跳产生的调度同时进行
  • UpdateThread:更新队列资源需求,执行队列抢占流程等
  • SchedulerEventDispatcherThread: 调度器事件的处理器,处理app新增、app结束、node新增、node移除、node更新等事件

整个调度架构如下图,是多线程异步协作的架构,而为了保证 内部核心数据(Node,Queue,App)的一致性,在主要的流程中加入了FairScheduler的对象锁。尤其核心调度流程是单线程执行的。这意味着container分配是串行的,这是调度器存在性能瓶颈的核心原因。

在这里插入图片描述

2.2.2 调度器中队列和作业的表示
  • 资源队列之间存在层级关系,树的根节点(root)的代表了整个集群的资源

  • FSParentQueue代表了树中的非叶子节点

  • FSLeafQueue代表了树的叶子节点

  • FSAppAttempt是运行时的应用在RM端的抽象,这些FSAppAttempt对象都挂载在对应的叶子节点下面

  • 以上三个类都实现了Schedulable接口是对调度单元的抽象都具有(Name、ResourceUsage、MinShare、MaxShare、Weights、Priority、assignContainer、preemptContainer、fairShare)属性

  • FSSchedulerNode是节点在RM端的抽象,维护着节点资源信息和状态

在这里插入图片描述
在这里插入图片描述

2.3 核心调度流程

1.首先调度器从集群中选取一个节点(node)

2.从树形队列的根节点(root)开始,每层队列都会按照公平策略排序选择其中一个子队列,直到递归遍历到最终的一个叶子队列为止

3.找到叶子队列后按照公平策略排序选择一个app,再为这个app在node上找到适合的资源

某次调度的路径可能是ROOT -> ParentQueueA -> LeafQueueA1 -> App001

4.在队列调度过程中,首先会列预检查队列的资源使用量是否已经超过了队列的Quota

6.再按照公平调度策略对子队列/app进行排序

7.递归调度子队列/app
在这里插入图片描述

2.4 两种调度时机-心跳调度和持续调度

  • 在Yarn2.3以只前有基于NodeManager的心跳调度,NodeManager会定时向ResourceManager发送心跳汇报自身状态和节点资源情况,会出发NODE_UPDATE事件交给FairScheduler处理,在处理NodeUpdate过程会发起一次资源调度

  • 基于心跳的调度不够及时从而导致集群资源浪费,在Yarn2.3 YARN-1010中引入了持续调度机制,由一个独立的线程(ContinuousSchedulingThread)进行实时的资源分配调度,与NodeManager的心跳出发的调度相互异步并行进行,当NM心跳到来只需要把调度结果通过心跳响应告诉对应的NodeManager即可。
    在这里插入图片描述

2.5 源码链路分析

该文以持续调度为例串讲FairScheduler 资源调度全流程实现

FSParentQueue ---->FSLeafQueue -----> FSAppAttempt

持续调度方法:continuousSchedulingAttempt

void continuousSchedulingAttempt() throws InterruptedException {
    long start = System.nanoTime();
    List<NodeId> nodeIdList = new ArrayList<NodeId>(nodes.keySet());
    lock.lock();
    try {
    //进行调度前,先对节点根据剩余资源的多少进行排序,从而让资源更充裕的节点先得到调度
  //让所有节点的资源能够被均匀分配,不会因为某些节点总是先被调度,导致先调度的节点比后调度的节点的资源使用率更高
      Collections.sort(nodeIdList, nodeAvailableResourceComparator);
    } catch (IllegalArgumentException e) {
      LOG.error("Error while sort node list", e);
      return;

对节点单次调度:attemptScheduling

void attemptScheduling(FSSchedulerNode node) {
    long startTime = System.nanoTime();
    lock.lock();
    try {
      if (!enterAttemptSchedulingThread) {
        enterAttemptSchedulingThread = true; //开启AttemptSchedulingThread 调度控制
        try {
          this.schedulerHeapSortEnabled =
              this.temporarySchedulerHeapSortEnabled;
          this.attemptSchedulingLockRemoved =
              this.temporaryAttemptSchedulingLockRemoved; //这个几个条件判断与约束

FSParentQueue.assignContainer

 public Resource assignContainer(FSSchedulerNode node) {
    long start = this.scheduler.getMetricsTime();
    boolean isRoot = this.getName().equals("root");
    Resource assigned = Resources.none();

    // If this queue is over its limit, reject
    //1、预检查队列的资源使用量是否已经超过了队列的Quota
    //node labe与队列label是否匹配
    if (!assignContainerPreCheck(node)) {
      if (this.scheduler.getDetailedSchedulerMetricsEnabled()) {
        if (!isRoot) {

遍历子队列进行资源分配调度:FSParentQueue.innerAssign

 private Resource innerAssign(FSSortWrapper<? extends Schedulable> sortedQueues, boolean isRoot, FSSchedulerNode node) {
    Resource assigned = Resources.none();
    if (!scheduler.getSchedulerHeapSortEnabled()) { //如果没有开启堆排序,rm配置是没有开堆排序的
      FSQueue child;
      Schedulable schedulable;
      for (int i = 0; i < sortedQueues.getValidSize(); i++) {
        schedulable = sortedQueues.getSortedArray(i);
        if (schedulable instanceof FSFakeQueue) {
          child = ((FSFakeQueue) schedulable).getOriginFSQueue();
        } else {
          child = (FSQueue) schedulable;
        }
        if(child.getMetrics().getPendingContainers() <= 0){
          continue;
        }
        assigned = assignAndAddMetric(child, assigned, node, isRoot);//遍历到子队列,分配node资源
        if (!Resources.equals(assigned, Resources.none())) {//拿到资源后,则停止遍历
          break;
        }
      }
    } else {
       .....
      }
    }
    return assigned;
  }

FSLeafQueue.assignContainer

 public Resource assignContainer(FSSchedulerNode node) {
    long start = this.scheduler.getMetricsTime();
    Resource assigned = Resources.none();

    if (LOG.isDebugEnabled()) {
      LOG.debug("Node " + node.getNodeName() + " offered to queue: " +
          getName());
    }

    if (!assignContainerPreCheck(node)) {//1、前置检查
      if (this.scheduler.getDetailedSchedulerMetricsEnabled()) {

遍历叶子队列下的作业进行资源分配调度:FSLeafQueue.innerAssign

private Resource innerAssign(FSSortWrapper<? extends Schedulable> sortedApps, FSSchedulerNode node) {
    int index = 0;
    int blackListCount = 0;
    Resource assigned = Resources.none();
    if (!scheduler.getSchedulerHeapSortEnabled()) {
      FSAppAttempt sched;
      Schedulable schedulable;
      for (int i = 0; i < sortedApps.getValidSize(); i++) {
        index++;
        schedulable = sortedApps.getSortedArray(i);
        if (schedulable instanceof FSFakeAppAttempt) {

2.6 源码问题分析

1、调度器是如何对app和队列进行排序的呢?

队列和app排序中都使用了FairShareComparator 比较器,其判定规则如下:

  • 首先计算Schedulable 是否有资源需求进行对比,即resourceUsage<minShare表示有资源需求
  • 其次计算内存资源使用率进行比较,即usageMemory/minShareMemory
  • 再者使用资源权重进行对比,即UsageMemory/weightMemory
  • 如果前面三个条件都相等则会首先比较Schedulable开始时间、最后比较Schedulable名称
 @Override
    public int compare(Schedulable s1, Schedulable s2) {
      double minShareRatio1, minShareRatio2;
      double useToWeightRatio1, useToWeightRatio2;
      Resource u1 = s1.getResourceUsage();
      Resource u2 = s2.getResourceUsage();
      Resource minShare1 = s1.getMinShare();
      Resource minShare2 = s2.getMinShare();
      boolean s1Needy = Resources.lessThan(RESOURCE_CALCULATOR, null,
          u1, minShare1);
      boolean s2Needy = Resources.lessThan(RESOURCE_CALCULATOR, null,
public ResourceWeights getAppWeight(FSAppAttempt app) {
    double weight = 1.0;
    if (sizeBasedWeight) {
      // Set weight based on current memory demand
      weight = Math.log1p(app.getDemand().getMemory()) / Math.log(2);
    }
    weight *= app.getPriority().getPriority();
    if (weightAdjuster != null) {
      // Run weight through the user-supplied weightAdjuster
      weight = weightAdjuster.adjustWeight(app, weight);
    }
    ResourceWeights resourceWeights = app.getResourceWeights();
    resourceWeights.setWeight((float) weight);
    return resourceWeights;
  }

2、在持续调度中是如何选定节点进行资源调度

对于Node选定排序使用了NodeAvailableResourceComparator 比较器

  • 比较节点可使用资源进行排序
 public int compare(NodeId n1, NodeId n2) {
      boolean isN1Exist = nodes.containsKey(n1);
      boolean isN2Exist = nodes.containsKey(n2);
      if (!isN1Exist && !isN2Exist) {
        return 0;
      }
      if (!isN1Exist) {
        return 1;
      }
      if (!isN2Exist) {
        return -1;
      }
      return RESOURCE_CALCULATOR.compare(clusterResource,
          nodes.get(n2).getAvailableResource(),
          nodes.get(n1).getAvailableResource());
    }
  }

2.7、队列、作业异步排序优化

1、调度过程不再进行排序的步骤。

2、独立的线程池处理所有队列的排序,其中每个线程处理一个队列的排序。

3、通过深度克隆队列/作业中用于排序部分的信息,来保证排序过程中数据结构不变。

4、主要由FSSortThreadPool 中的sortRootThread、sortMainThread分别异步对Rm中的队列和作业进行排序

在这里插入图片描述

3 CapacityScheduler

CapacityScheduler与FairScheduler类似,目前FairScheduler支持多种调度策略,可以认为FS覆盖绝大多数其功能,都支持抢占、批量调度,动态加载配置文件等。

这里不做展开介绍,给出CapacityScheduler与FairScheduler的区别,如下表

CapacitySchedulerFairScheduler
设计思想资源按比例分配给各个队列,并添加各种严格的限制防止个别用户或队列独占资源基于最大最小化公平算法将资源分配给各个资源池或者用户
是否支持负载均衡
Container请求的资源粒度最小资源量的整数倍,比如Container请求量是1.5GB,最小资原量是1GB,则自动将请求化为2GB有专门的内存规整化参数控制,粒度更小。Conatienr请求量为1.5GB,规整标准值为128MB,则请求量不变
队列间资源分配方式资源使用率低者优先Fair,FIFO,DRF
队列内部资源分配方式FIFO,DRFFair,FIFO,DRF

4 调度器的性能优化

4.1 确定性能瓶颈

当我们遇到一个问题的时候,需要明确问题的根本原因,根据上面的架构分析,任何一个环节出现问题,都可能导致性能瓶颈,但是YARN本身又没有全面的指标去体现调度所有环节是否正常,我们确定性能瓶颈,实际上是建设调度全流程指标的过程,首先这里给出一个实际场景。

异常作业Pending引发压力的场景

这里给出一个真实的场景,yarn机器规模在1K节点左右,某天突然数据仓库生产延迟,可初步判断是业务影响导致的,通过排查用户作业,最终发现是由于一个用户上线了一个新业务,每天定期提交大量作业到一个资源较小的队列里,导致AppPending数量持续徒增,导致调度器性能出现问题。

初步问题判断

经过代码分析,可理解为AppPending增加会加大resource usage的计算量,从而导致updateDemand(资源需求更新)和attemptScheduling(核心调度函数)执行变慢,而这两个方法由于一致性问题,都锁了FairScheduler对象(synchronized),而AppMaster在执行allocate也需要锁FairScheduler对象,同时在故障期间多次dump stack,发现ApplicationMasterService的allocate在等待锁,且Ops也有小幅度下降。此时问题聚焦在了allocate和FairScheduler对象锁和,为了避免这个问题在发生,需对集群的AppPending添加了报警,同时还添加了ApplicationMasterService的allocate、ResourceTrackerServer的nodeHeartbeat Ops监控。

二次问题判断

此后数据仓库生产又出现延迟,首先分析了作业,没有发现异常作业和明显的作业/任务增长;同时又分析了监控指标:也没有收获(包括新增加的allocate/nodeHeartbeat Ops);此时可以基本判断是调度器的问题:可发现有的作业,队列有资源,但分配到资源却耗费过长,且集群资源只用到了60%。此时具体哪个环节出现问题并不清楚,所以需按照调度的全流程排查可能出问题的点。

4.2 调度全流程指标建设

调度阶段问题分析指标
资源申请社区版本每个队列有资源Pending信息,即未分配给作业的资源请求;但是如果当前队列的资源超过了最大值,这些资源请求是无法参与调度的,如果集群没有剩余资源了,也是无法参与调度,面向调度器来说我们需要有一个有效的资源请求(ValidPending),建立了这个指标:集群在还有剩余资源的情况下,且队列中的使用资源没有超过最大限制,出现的Pending资源量的累加和。有了这个指标可以清晰的认识到是调度性能的问题,还是没有给调度有效的资源需求,经过看VaildPending可以更加确定是调度器的问题。有效的资源请求(ValidPending)
资源获取由于nodeHeartbeat rpc的Ops性能很好,在加上初次场景中的allocate rpc Ops有小幅度降低,优先排查allocate资源获取量是否正常,建设了AmPulledContainers指标,可发现资源分配速率≈AM获取资源速率≈资源释放速率,如下图:在这里插入图片描述AM获取资源速率(AmPulledContainers)
资源分配在check资源分配是否存在问题,会发现nodeHeartbeat rpc的Ops不能代表资源分配效率,首先nodeHeartbeat是异步发送事件(SchedulerEventType.NODE_UPDATE->资源分配),第二是一个节点在执行NODE_UPDATE事件期间,不会在发送这个事件),于是需要建设资源分配的指标(attemptScheduling函数执行效率),如下:在这里插入图片描述这张图表示:一分钟执行attemptScheduling时间的累加和(60G=1min,单位是纳秒),可以看到一分钟已经被核心调度函数用满,监控时间单位使用纳秒的原因是,其它时间单位变化趋势不明显,不利于观察;attemptScheduling函数执行效率
添加attemptScheduling后,可建设各阶段执行效率的指标,包括每层队列的assignContainerPreCheck、sort时间,FSLeafQueue 黑名单检查时间,FSAppAttempt的本地性跳过时间、加锁时间等,通过监控可发现最初时间主要耗费在RootQueue的排序上,如下图:在这里插入图片描述 有了实时的瓶颈监控,就相对容易做针对性优化了。assignContainerPreCheck、sort(分配container的预检查时间和排序时间)
加锁阶段前面讲架构的时候,介绍过调度事件处理都要锁FairScheduler对象,而且都在SchedulerEventDispatcher中处理,如果其它调度事件未来成了瓶颈,也会影响调度性能,因此我们对所有调度事件,添加了Ops/AvgTime监控指标。另外,一个调度器最好有一个单一指标来清晰表达其调度能力,可建设AssignedContainersOps指标用于衡量这个能力(每秒可调度的containers)。每秒可调度的containers(AssignedContainersOps)

最终问题总结

Hadoop早在1.x时代,就可以支持几千节点,为什么在1K规模就遇到扩展性问题呢?这是因为除了与本身规模(节点、队列、App并发)增长带来的压力,还与具体的业务场景有关,即:资源的消费速度。资源的调度过程,可以看成是一个生产者消费者模式,调度器好比生产者,Container好比消费者,集群的总资源好比生产者和消费者中间的缓冲池,在一定的集群规模下,调度的性能要大于,Container消费的速度,否则就会出现问题。在小作业较多的生产场景中,作业小文件很多,这就导致Hive产生的MapReduce Task,读取的文件很小,Task运行时间也很快(平均运行时间在40s左右),这也导致在小规模集群下遇到了这个问题。

4.3 性能优化点

经过上一节的性能瓶颈分析,场景的最初瓶颈在排序上,以下介绍不同层次的瓶颈点变化:

常见优化点相关优化操作
排序参数ResourceUsage计算优化通过监控工具和分析,可精确的发现,排序的时间主要耗费在了参数resoure usage的计算,经过分析发现在每一层排序的时候,都需要对resource usage做大量重复的计算,我们通过保存参数(YARN-5969),增量计算的方式(YARN-4090),1min 排序耗费的时间相比之前提升10X,调度性能提升2X
过滤没有需求的调度动作调度函数不在把CPU吃满时,发现调度函数有较多跳过,进一步分析发现一些Queue/App的min share比较大,导致排序容易靠前,优先调度它们,但实际上他们很多都没有调度需求了,浪费了很多的调度时间,针对没有需求的Queue/App进行了过滤(YARN-6045,社区有一个PATCH只对App没有需求进行了过滤YARN-3547),经过优化:有效利用CPU时间增加30%,调度性能提升2X
本地性跳过调优集群规模大后,任务的本地性大量的跳过,也是很浪费调度性能,通过监控发现本地性跳过很大,优化原则是:适当降低Node本地性,不降低Rack本地性(防止交换机被打满),经过调参:本地性跳过和rack跳过比率,调度性能提升20%
日志优化在Container吞吐很快的场景,每个container的申请、释放、状态变化都要打日志,非常耗费性能,全部修改成DEBUG后(注意要使用:if (LOG.isDebugEnabled()){LOG.debug(“some logs”)},对于高性能Server,log4j本身也是影响性能的),经过优化性能提升50%(YARN-1297 处理得不是太干净,可以看一下ResourceManager日志,在处理一下)
RMNodeFinishedContainersPulledByAMEvent事件优化这个问题轻则影响性能,严重时会导致ResourceManager OOM,问题是太多RMNodeFinishedContainersPulledByAMEvent导致AsyncDispatcher队列挤压过满,导致性能成指数级下降,YARN-5262解决了此问题
排序算法修改成堆排这个问题是在一个相对较小的集群会遇到,场景是App并发1K的时候,75%的作业都落到1-2个LeafQueue,排序性能迅速下降,可把默认的排序算法修改成了堆排,性能提升了2X,在实现heap的时候jdk的PriorityQueue的接口不太好用,可以自己稍微调整一下
独立排序到调度外,排序并行化优化到目前为止,除了调度所必须要花费的时间外,剩下的就是排序时间,而不断的排序的目的是为了加强公平性,但公平性都只是相对的,如:所有用户的资源都在实时变化,计算资源demand的线程以500ms为间隔进行计算都体现了这个相对性,所以我们可把排序拿出调度、并行化排序,通过减少适当的公平性,来增加调度吞吐,具体变更如下: 在这里插入图片描述 首先我们启动了一个线程池,以ms级为间隔不断对调度对象(Queue/App)进行排序,让独立出去的排序做到足够快,尽量减少排序独立后带来的公平性影响,在调度的时候我们将不在进行排序,直接调度;需要注意的是,排序的对象值(resource usage,demand)在不断变化,必须对当前的排序对象做一份快照(clone),否则会排序失败,同时为了保证一致性,我们在每个队列增加了排序读写锁(sort_readlock/sort_writelock),而在调度的时候由于是快照,我们还需要对调度对象做检查,即判断节点和作业是否被删除;这个PATCH上线后,排序几乎不在耗费调度时间。

5 调度器的关联应用

调度器的关联应用有很多,这里抛砖引玉。举几个有代表性的应用,如调度失败归因和SLA服务实现,这两部分在yarn都已经落地,具体内容作为一个未来项。

调度失败归因

总结来讲就是在调度函数当中将失败信息存入一个对应的数据结构,设置对应的原因码,具体信息展示,即信息码的翻译工作交由上层引擎层处理,实现过程肯定更为复杂。

SLA服务实现

这里的sla可以理解为app层面的资源优先调度,可直接实现一个子类SlaPolicy,作为FairsharePolicy的子类,仅重写getComparator()方法,也可以做到只修改排序逻辑,而不替换其它的逻辑。

即:增加SlaPolicy类,重写getComparator()方法。在内部重写新的Comparator逻辑。其类图如下:

在这里插入图片描述

最终达到的效果就是提升资源调度的优先级,达到优先分配。

6 引用

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值