Yarn资源请求处理和资源分配原理解析

来源:互联网 发布:企业家it 编辑:程序博客网 时间:2024/06/05 13:36
  • 概述
  • FairScheduler的资源调度原理和代码
    • FairScheduler的调度概览
    • 两种调度时机-心跳调度以及持续调度
    • 开始进行资源调度
    • 判断这个application是否适合在这个节点上分配资源运行
    • YARN请求资源时的locality和relaxility限定
    • 资源分配:assignContainer()
    • Parent节点调用FSParentQueue.assignContainer()方法进行资源分配
    • Leaf节点通过调用FSLeafQueue.assignContainer()方法进行资源分配
    • 应用通过调用FSAppAttemt.assignContainer()方法进行资源分配
      • 根据Priority优先级排序以及资源请求的Priority简介
      • 分配前的校验
      • 判断资源分配的locality并进行资源分配
  • 总结

概述

在我的上一篇《Yarn FairScheduler 的资源预留机制导致集群停止服务事故分析》中介绍了我们由12台服务器、每台资源容量(100G,32 vCores)组成的yarn集群由于资源预留导致宕机的一次事故。资源预留是Yarn进行资源分配过程中为了让大的应用不至于被小的饿死而进行资源预定的分配方式。本文就从原理和代码层面详细介绍Yarn的资源分配流程。

下图是我们向yarn提交任务到最后任务以一个一个container的形式运行起来的大致过程。我们通过客户端向Yarn提交计算任务,Yarn会首先为我们的任务生成一个ApplicationMaster,AM负责对整个应用的资源进行管理,包括资源的申请、释放、任务中container的调度和运行。 关于ApplicationMaster与ResourceManager之间进行通信的详细机制,大家可以参考我的博客《YARN ApplicationMaster与ResourceManager之间基于applicationmaster_protocol.proto协议的allocate()接口源码解析》 ,本文不再重复讲解。本文将讲解的资源调度,就是ApplicationMaster向ResourceManager请求资源以后,Yarn的ResourceManager委托我们所配置的调度器(FairScheduler/CapacityScheduler)决定是否分配资源、分配多少资源的过程。当然,ApplicationMaster本身也是运行在一个container中的,也是进行调度的。下文将进行讲解。

RM分配Container的流程

FairScheduler的资源调度原理和代码

FairScheduler的调度概览

我们所配置的调度器FairScheduler是运行在ResourceManager内的调度算法。Hadoop的官方文档详细介绍了FairScheduler的使用方法。

FairScheduler的资源队列之间存在层级关系,树的根节点的代表了整个集群的资源,根节点的名字叫做root。我们定义Yarn的资源队列,就是在root下面定义其子节点,比如,root.queue1、root.queue2等等,即,实际上,资源队列之间形成了资源队列树。FSParentQueue对象代表了树中的非叶子节点,FSLeafQueue代表了树的叶子节点。由于我们提交的任何一个应用都需要运行在某个队列中,因此,叶子节点下面还挂载了正在该队列上运行的应用,Yarn使用FSAppAttempt来作为一个运行时的应用在ResourceManager端的抽象,因此,这些FSAppAttempt对象都挂载在对应的叶子节点下面。

使用树恰当地表达对集群资源进行划分时所需要的隔离关系和层级关系。比如,我们一个集群资源给公司的所有部门提供计算服务,部门与部门之间有可能是有平级部门的兄弟关系,平级部门之间的资源需要相互隔离,也有可能是上下级部门的关系,上级部门有权利使用下级任何部门的资源。因此,使用树来抽象资源,既可以实现平级部门之间的资源隔离,也能够表达上下级部门之间资源的包含关系。

资源队列树

上图就是这个资源树的示意图。FairScheduler通过这样一棵资源树,维护了整个集群的资源使用情况。资源树中的每一个叶子节点都挂载了此时正在这个队列上运行、或者正在申请运行的应用。FairScheduler只需要对这个树进行适当遍历,就可以知道任何一个资源队列当前有哪些应用在运行、队列当前还剩下多少资源、已经使用多少资源等。

通过这样一个资源树,资源调度的过程就变成了这样一个不断进行的过程:从树中取出一个资源请求,如果这个资源请求不违背队列的最大资源量等限制,也能够在某个服务器上运行(这个服务器剩余资源可供运行这个请求的资源),那么,就让这个请求运行在对应的服务器上,即创建对应的container。实际上,分配了这个container以后,会在ApplicationMaster某一次心跳响应中返回这个分配结果,ApplicationMaster知道资源分配成功,就与对应的NodeManager通信,请求该NodeManager将对应的Container(比如运行Mapper或者Reducer的Container或者运行Spark executor 的Container)在其节点上启动,NodeManager收到请求,如果验证通过,则启动对应的Container。

那么,一次调度的发生是如何被触发的呢?这就涉及到两种调度时机,即心跳调度和持续调度。

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

心跳调度是最早的yarn的调度方式,在2.3版本以前的yarn只支持心跳调度。Yarn的NodeManager会通过心跳的方式定期向ResourceManager汇报自身状态,当NodeManager向ResourceManager汇报了自身资源情况(比如,当前可用资源,正在使用的资源,已经释放的资源),这个RPC会触发ResourceManager调用nodeUpdate()方法,这个方法为这个节点进行一次资源调度,即,从维护的Queue中取出合适的应用的资源请求(合适 ,指的是这个资源请求既不违背队列的最大资源使用限制,也不违背这个NodeManager的剩余资源量限制)放到这个NodeManager上运行。这种调度方式一个主要缺点就是调度缓慢,当一个NodeManager即使已经有了剩余资源,调度也只能在心跳发送以后才会进行,不够及时

在 YARN-1010中引入了连续资源调度机制,不用等待NodeManager向ResourceManager发送心跳才进行任务调度,而是由一个独立的线程进行实时的资源分配等调度,与NodeManager的心跳出发的调度相互异步并行进行。当心跳到来,只需要把调度结果通过心跳响应告诉对应的NodeManager即可。

我们通过yarn.scheduler.fair.continuous-scheduling-enabled来配置是否打开连续调度功能。默认情况下该功能关闭。

  /**    ContinuousSchedulingThread负责进行持续的资源调度,与NodeManager的心跳产生的调度同时进行   */  private class ContinuousSchedulingThread extends Thread {    @Override    public void run() {      while (!Thread.currentThread().isInterrupted()) {        try {          continuousSchedulingAttempt();          Thread.sleep(getContinuousSchedulingSleepMs());//睡眠很短的一段时间进行下一轮调度        } catch (InterruptedException e) {          //略        }      }    }  }

FairScheduler.initScheduler()方法中构造了ContinuousSchedulingThread线程对象:

      if (continuousSchedulingEnabled) {         // start continuous scheduling thread        schedulingThread = new ContinuousSchedulingThread();        schedulingThread.setName("FairSchedulerContinuousScheduling");        schedulingThread.setDaemon(true);      }

FairScheduler.serviceStart()方法中启动了ContinuousSchedulingThread线程。该线程每一轮执行完毕,会通过我们配置的yarn.scheduler.fair.continuous-scheduling-sleep-ms睡眠一段时间,然后进行下一轮调度,默认情况下,这个时间是5ms,这个时间间隔接近实时。而对于心跳调度方式,心跳时间间隔通过yarn.nodemanager.heartbeat.interval-ms配置,默认值 1000ms。我把连续调度的每一次执行叫做一轮,在下文中我们可以看到,每一轮,会遍历当前集群的所有节点,挨个对这个节点进行一次调度,即,取出合适的请求,分配到这个节点上运行。

这就是持续调度线程的初始化和启动机制 ,其初始化和启动时伴随着我们的FairScheduler调度器的初始化和启动同时进行的,是FairScheduler的一种调度优化机制。

无论是NodeManager心跳时触发调度,还是通过ContinuousSchedulingThread进行实时、持续触发,他们对某个节点进行一次调度的算法和原理是公用的,都是通过synchronized void attemptScheduling(FSSchedulerNode node)来在某个节点上进行一次调度,方法的参数代表了准备进行资源分配的节点。两种触发机制不同的地方只有两个:

  1. 调度时机:心跳调度仅仅发生在收到了某个NodeManager的心跳信息的情况下,持续调度则不依赖与NodeManager的心跳通信,是连续发生的,当心跳到来,会将调度结果直接返回给NodeManager;
  2. 调度范围:心跳调度机制下,当收到某个节点的心跳,就对这个节点且仅仅对这个节点进行一次调度,即谁的心跳到来就触发对谁的调度,而持续调度的每一轮,是会遍历当前集群的所有节点,每个节点依次进行一次调度,保证一轮下来每一个节点都被公平的调度一次

开始进行资源调度

这是连续调度方式的一轮调度开始的入口:

void continuousSchedulingAttempt() throws InterruptedException {  long start = getClock().getTime();  List<NodeId> nodeIdList = new ArrayList<NodeId>(nodes.keySet());  //进行调度以前,先对节点根据剩余资源的多少进行排序,从而让资源更充裕的节点先得到调度  //这样我们更容易让所有节点的资源能够被均匀分配,而不会因为某些节点总是先被调度所以总是比  //后调度的节点的资源使用率更高  synchronized (this) {    Collections.sort(nodeIdList, nodeAvailableResourceComparator);  }  // 遍历所有节点,依次对每一个节点进行一次调度  for (NodeId nodeId : nodeIdList) {    //FSSchedulerNode是FairScheduler视角下的一个节点    FSSchedulerNode node = getFSSchedulerNode(nodeId);    try {      //判断      if (node != null && Resources.fitsIn(minimumAllocation,          node.getAvailableResource())) {        attemptScheduling(node);      }    } catch (Throwable ex) { //这每次调度过程中如果发生异常,这个异常将被捕获,因此不会影响在其它节点上进行调度      //异常处理,略      }    }  }  long duration = getClock().getTime() - start;  fsOpDurations.addContinuousSchedulingRunDuration(duration);}

continuousSchedulingAttempt()会遍历所有节点,依次进行资源调度,这里的调度,就是试图找出一个container请求放到这个服务器上运行。为了让整个集群的资源分配在服务器节点之间能够更均匀,调度以前通过资源比较器对节点按照资源余量从多到少排序,从而让资源更充裕的先被调度,这样做更有利于让所有节点的资源使用量达到均衡,而不至于由于某些节点的序号排在前面而总是被先调度,造成资源调度倾斜

关于比较器,使用的是NodeAvailableResourceComparator比较器,我们跟踪比较器的代码看到,实际上比较资源的时候只考虑了内存,没有考虑vCores等其它资源。

对于每一个节点,如果节点的资源剩余量大于yarn.scheduler.minimum-allocation-mb所配置的最小调度量才会对这个节点进行调度。如果满足要求,attemptScheduling(node)就开始针对这个节服务器进行调度了,即,把选出合适的资源请求,分配到这个节点上。上面说过,如果是心跳调度模式,也是通过这个方法对发送心跳的节点进行资源调度的。

synchronized void attemptScheduling(FSSchedulerNode node) {  //略  // Assign new containers...  // 1. Check for reserved applications  // 2. Schedule if there are no reservations  FSAppAttempt reservedAppSchedulable = node.getReservedAppSchedulable();  if (reservedAppSchedulable != null) { //如果这个节点上已经有reservation    Priority reservedPriority = node.getReservedContainer().getReservedPriority();    FSQueue queue = reservedAppSchedulable.getQueue();    //如果这个节点被这个应用预定,这里就去判断这个应用是不是有能够分配到这个node上到请求,如果有这样到请求,并且,没有超过队列到剩余资源,那么,就可以把这个预定的资源尝试进行分配(有可能分配失败)      //而如果发现这个应用没有任何一个请求适合在这个节点运行,或者,当前队列的剩余资源已经不够运行这个预留的、还没来得及执行的container,那么这个container就没有再预留的必要了    if (!reservedAppSchedulable.hasContainerForNode(reservedPriority, node)        || !fitsInMaxShare(queue,        node.getReservedContainer().getReservedResource())) {      //如果这个被预留的container已经不符合运行条件,就没有必要保持预留了,直接取消预留,让出资源      reservedAppSchedulable.unreserve(reservedPriority, node);      reservedAppSchedulable = null;    } else {      //对这个已经进行了reservation对节点进行节点分配,当然,有可能资源还是不足,因此还将处于预定状态      node.getReservedAppSchedulable().assignReservedContainer(node);    }  }  if (reservedAppSchedulable == null) {这个节点还没有进行reservation,则尝试进行assignment    // No reservation, schedule at queue which is farthest below fair share    int assignedContainers = 0;    while (node.getReservedContainer() == null) { //如果这个节点没有进行reservation,那么,就尝试      boolean assignedContainer = false;      if (!queueMgr.getRootQueue().assignContainer(node).equals(          Resources.none())) { //尝试进行container的分配,并判断是否完全没有分配到并且也没有reserve成功        assignedContainers++; //如果分配到了资源,或者预留到了资源,总之不是none        assignedContainer = true;      }      if (!assignedContainer) { break; }      if (!assignMultiple) { break; }      if ((assignedContainers >= maxAssign) && (maxAssign > 0)) { break; }    }  }  updateRootQueueMetrics();}

在对这个节点进行调度的时候,会先判断这个节点是不是一个被预留的节点,预留和非预留的节点的处理方式是不同的,具体流程是:

  • 如果这个服务器是一个被某个container预留的服务器,那么,需要对这种状态进行处理,以选择(1)剥夺预留、(2)保持预留状态或者(3)将预留状态转换成分配状态:
    • 剥夺预留: 如果发现这个被预留的应用没有任何一个请求适合在这个节点上运行,另外,由于每一个应用都有所在的资源队列,因此如果我们发现资源队列的剩余资源已经小于这个应用的预留资源,那么,这个预留已经没有存在的必要了,需要取消预留权;从这里我们可以看到,如果一个container在这个节点上是预留状态,说明队列的剩余资源肯定满足container的资源需求,只是服务器剩余资源无法运行这个container;
    • 尝试分配资源:如果我们发现这个在这个节点预留资源的应用的确可以在这个节点运行,并且,预留的资源也的确在队列剩余资源的允许范围内,即,还有预留的必要性,那么,就可以尝试进行一次资源分配了,当然,这个资源分配有可能还是失败,如果失败就已然保持预留状态,这个为预留的container进行 资源分配尝试 的过程是在FSAppAttempt.assignReservedContainer()中进行的;
  • 如果这个节点是一个正常未预留的节点,那么就可以进行正常的资源分配了。

判断这个application是否适合在这个节点上分配资源运行

在判断是进行预留权剥夺还是资源分配尝试的时候,需要判断这个预留资源的应用是否有任何一个请求适合在这个节点上运行,以及队列剩余资源是否允许这个预留的container的存在。在这里有必要详细解释什么叫做适合在这个节点上运行,这是通过

这是通过hasContainerForNode()方法进行判断的:

  /**   * Whether this app has containers requests that could be satisfied on the   * given node, if the node had full space.   * 关于这个方法的判断条件,为什么如果anyRequest==null就直接返回false,这是因为applicationMaster在   * 为应用申请资源的时候,如果是NODE_LOCAL,顺便也会创建这个节点对应的RACK的RACK_LOCAL的请求和offswitch的请求   * 这个可以看MRAppMaster发送请求的时候所使用的RMContainerRequestor.addContainerReq()和ApplicationMaster通过   * 调用AMRMClientImpl.addContainerRequest()申请资源的过程   * 或者查看董西成的博客http://dongxicheng.org/mapreduce-nextgen/yarnmrv2-mrappmaster-containerallocator/   */  public boolean hasContainerForNode(Priority prio, FSSchedulerNode node) {    //查找这个优先级下面目前三种请求,一种是没有任何本地化限制的请求,一种是限制为本地机架的请求,一种是限制为本节点内的请求    ResourceRequest anyRequest = getResourceRequest(prio, ResourceRequest.ANY); //所有请求,对机架和节点没有要求    ResourceRequest rackRequest = getResourceRequest(prio, node.getRackName());//在这个节点所在机架上的请求    ResourceRequest nodeRequest = getResourceRequest(prio, node.getNodeName());  //在这个节点上的请求    return        // There must be outstanding requests at the given priority:        anyRequest != null && anyRequest.getNumContainers() > 0 &&            // If locality relaxation is turned off at *-level, there must be a            // non-zero request for the node's rack:            (anyRequest.getRelaxLocality() ||                (rackRequest != null && rackRequest.getNumContainers() > 0)) &&            // If locality relaxation is turned off at rack-level, there must be a            // non-zero request at the node:            (rackRequest == null || rackRequest.getRelaxLocality() ||                (nodeRequest != null && nodeRequest.getNumContainers() > 0)) &&            // The requested container must be able to fit on the node:            Resources.lessThanOrEqual(RESOURCE_CALCULATOR, null,                anyRequest.getCapability(), node.getRMNode().getTotalCapability());  }

YARN请求资源时的locality和relaxility限定

ApplicationMaster客户端向ResourceManager资源申请,会把自己的资源需求构造为一个ResourceRequest对象发送给RM。一个ResourceRequest包含了若干个相同Capability的container的请求的集合,包含以下元素:

  • Capability: 单个Container的资源量,由<mem,vCore>组合构成,是一个Resource类的实现;

  • Containers Number : Container的数量,整个请求的资源量为 container number * capability

  • Resource Name:资源名称(某个节点的ip、某个机架的ip或者是*代表任意),这里叫做名称有些让人难以理解,其实是限制这个ResourceRequest对象资源运行的locality,这里有必要非常具体的讲解YARN的locality。ApplicationMaster客户端在提交应用的时候,有时候会对container运行的位置提出限制,比如,由于某些数据在服务器node1上,因此ApplicationMaster客户端希望用来处理这些数据的container就运行在这个服务器上,这个要求运行在某个特定节点的本地化叫做NODE_LOCAL,也可以,要求运行在某个固定的机架rack1上,这种机架的本地化叫做RACK_LOCAL,或者,也许没有要求(OFF_SWITCH)。因此,Resource Name可以是某个节点的ip(NODE_LOCAL),或者某个机架的ip(RACK_LOCAL),或者是通配符*(OFF_SWITCH);

    本地化分为三种,定义在NodeType中:

    public enum NodeType {NODE_LOCAL(0), //请求规定了必须运行在某个的服务器节点RACK_LOCAL(1), //请求规定了必须运行在某个机架上OFF_SWITCH(2); //这个请求对本地化没有要求public int index;private NodeType(int index) {  this.index = index;}}

  • relaxLocality:是否允许某种本地化限制松弛到更低的要求,比如,当某个ResourceRequest要求其container运行在node1,但是node1的资源剩余量始终无法满足要求,那么需要进行本地化松弛,可以放弃必须运行在服务器node1的要求,改为只要求运行在这个服务器node1所在的机架rack1上就行。

    我们用一个例子来讲解请求一个NODE_LOCAL或者RACK_LOCAL是怎样进行的。

    如果某一个Container的运行有本地化需求,比如,这个Container要求只在node1上运行,那么,它其实会为这个Container创建多个不同的locality的请求:

    资源名称         内存         cpu     松弛度<“node1”,      “memory:1G”, 1,      true>  //发送resourcename为node1的请求,relaxLocality=true<“rack1”,      “memory:1G”, 1,      false/true> //同时发送node1所在的rack的请求,是否松弛relaxLocality的值<“*”,          “memory:1G”, 1,      false/true>  //同时发送off-switch请求,是否松弛relaxLocality的值

    如果这个请求值允许在node1上运行,那么relaxLocality==false,即rack1和off-switch的请求中的松弛变量都是false,那么,当node1请求无法满足,RM试图将请求降级到rack1或者off-switch的时候,检查他们的relaxLocality,发现是false,降级不被允许,只能继续等待直到node1满足条件,或者node1始终无法分配资源,资源分配失败。

总之,ApplicationMaster 在请求container的时候,如果本地化需求是NODE_LOCAL或者RACK_LOCAL,都会为一个container同时发送多个不同的resource name的请求,RM最终只会选择一个请求进行执行,并且,尽量满足NODE_LOCAL,如果不满足就只能RACK_LOCAL,最后只能运行OFF_SWITCH,总之根据可用资源的情况和请求中设置的允许的松弛程度决定是否分配资源。

在这里,我们可以深刻体会到ApplicationMaster的api是多么复杂

有了对资源本地化(locality)和本地化松弛(relaxility)的理解,我们就可以看懂hasContainerForNode() 方法用来判断这个应用的资源请求当前是否可以运行在这台服务器上了:

  1. 如果连OFF_SWITCH的请求都没有,那么这个应用肯定不需要运行了。我们在上面说过,无论是NODE_LOCAL、RACK_LOCAL抑或本来就是OFF_SWITCH的请求,都会发送OFF_SWITCH的请求,因此,如果发现一个应用连OFF_SWITCH的请求都没有,就没有必要再考虑其他的了。

  2. 如果OFF_SWITCH的relaxLocality是打开的(允许松弛到OFF_SWITCH的级别),或者虽然是关闭的(不允许松弛到OFF_SWITCH)但是有RACK_LOCAL的请求存在

  3. 如果RACK_LOCAL级别的relaxLocality是打开的(允许松弛到RACK_LOCAL级别),或者,虽然RACK_LOCAL级别的relaxLocality是关闭的(不允许松弛到RACK_LOCAL),但是却有NODE_LOCAL级别的请求

  4. 这个资源请求能够在这个节点上运行,即节点剩余资源足够运行这个请求

如果以上条件都满足,hasContainerForNode()返回true,则说明这个在这个节点上进行预订的app或许可以从预定状态变成分配状态了,因此,尝试对这个预定进行分配。而如果hasContainerForNode()返回false,说明这个预定的container实际上不可以在这个服务器上运行,因此没有必要继续空占资源,防止资源被长期无效占用。尝试将app在节点上预定的资源进行allocate的过程,是调用的FSAppAttempt.assignReservedContainer()方法,其实最终也是调用private Resource assignContainer(FSSchedulerNode node, boolean reserved)方法,尝试将这个container在这个节点上进行分配,后面一个参数标记了这个节点是否被预定了。

可见,资源分配的关键方法,就是assignContainer()方法。在进行container分配的过程中,会发生分配成功、或者将资源请求转变为预留状态的过程。我们将具体讲解assignContainer()方法。

资源分配:assignContainer()

我在资源调度实现机制概览介绍了Yarn 对队列配置的树形结构。树的每一个节点(注意这里的节点指的是树的节点,不是服务器节点)都是一个资源的抽象,比如,这个节点代表的最大资源、已使用资源、空闲未使用资源。

树的非叶子节点(FSParentQueue)和叶子节点(FSLeafQueue)都是资源集合,代表了一定的资源,不同的是,非叶子节点(FSParentQueue)只是用来代表他所管理的叶子节点的集合,叶子节点是应用具体运行的队列,因此叶子节点上挂载了正在它上面运行的应用的集合。FSParentQueue和FSLeafQueue都实现了assignContainer()方法,用来在这个队列上进行资源分配。每一次调度,都是从root节点开始,通过递归方式,调用这个节点的assignContainer()方法,尝试为挂载的请求进行一次分配,一旦分配成功则退出递归。我们分别来看FSParentQueue和FSLeafQueue对assignContainer()方法的实现。

下图是进行一轮资源分配(只有连续调度的一轮分配会对所有服务器挨个进行一次分配,心跳调度只对心跳节点进行一次分配,但是原理相同)的概图。从图中可以看到,为某一个节点进行资源分配,就是对资源树进行递归搜索,依次从资源树最上层的root节点、到普通的非叶子节点、再到叶子节点、然后是应用、最后是应用的某个资源请求,然后将选出的请求分配到对应的服务器上的过程。

Yarn资源分配概图

下面,我们就依次从代码角度讲解这个资源分配过程。

Parent节点调用FSParentQueue.assignContainer()方法进行资源分配

我在介绍连续调度和心跳调度 的时候,讲到Yarn是遍历所有的服务器,然后对于每一个服务器,对资源树进行一次递归搜索,选出请求在这个服务器上执行。

这是FSParentQueue.assignContainer()方法:

  public Resource assignContainer(FSSchedulerNode node) {    Resource assigned = Resources.none();    // If this queue is over its limit, reject    if (!assignContainerPreCheck(node)) {      return assigned;    }    Collections.sort(childQueues, policy.getComparator());    for (FSQueue child : childQueues) { //从这个for循环可以看出来这是广度优先遍历      assigned = child.assignContainer(node); //childQueus有可能是FSParentQueue或者FSLeafQueue      if (!Resources.equals(assigned, Resources.none())) { //如果成功分配到了资源,那么,没有必要再去兄弟节点上进行资源分配了        break;      }    }    return assigned;  }

我们从代码中可以看出,FSParentQueue其实并没有进行实际的资源分配,只是不断遍历子节点直到遇到叶子节点才会尝试进行分配,因此资源分配代码实际上是在FSChildQueue.assignContainer()中执行的。

同时,我们必须看到FSParentQueue.assignContainer()中对FSParentQueue的子节点的遍历并不是随机排序的,而是通过对应的policy定义了排序规则:

 Collections.sort(childQueues, policy.getComparator());

这里的Policy就是FairScheduler的官方文档上介绍的不同的Policy,对于FSParentQueue.assignContainer()中使用Policy进行子队列的排序,决定了对某个父节点的多个子节点进行资源分配的顺序。我在我的另外一篇文章将会介绍不同的Policy,在这里我们忽略策略问题。

同时,我们从for循环的退出规则可以看出,递归遍历过程中,一旦分配成功,for循环即退出,这轮分配结束,代码退出到continuousSchedulingAttempt()的for循环处,开始对下一个节点的剩余资源进行尝试分配。即,一轮分配中,对某个服务器,只会进行一个请求的分配。这样也是为了达到平均分配的结果,避免多个请求都堆积分配到某一个服务器而其它服务器却空闲的情况。

Leaf节点通过调用FSLeafQueue.assignContainer()方法进行资源分配

通过从root(FSParentQueue)节点递归调用assignContainer(),最终将到达最终叶子节点的assignContainer()方法,才真正开始进行分配:

/** * 尽量将自己的app分配到某个节点上,FSLeafQueue会遍历自己目前所有的runnableApps,然后逐个尝试进行分配,只要有一个分配成功就退出 * @param node 等待进行container分配的节点 * @return */@Overridepublic Resource assignContainer(FSSchedulerNode node) {  Resource assigned = Resources.none();  if (!assignContainerPreCheck(node)) {    return assigned;  }  Comparator<Schedulable> comparator = policy.getComparator();//根据对应的policy提供的排序器对apps进行排序  writeLock.lock();  try {    Collections.sort(runnableApps, comparator);  } finally {    writeLock.unlock();  }  // Release write lock here for better performance and avoiding deadlocks.  // runnableApps can be in unsorted state because of this section,  // but we can accept it in practice since the probability is low.  readLock.lock();  try {    for (FSAppAttempt sched : runnableApps) { //对排序完成的资源一次进行调度,即对他们进行资源分配尝试      if (SchedulerAppUtils.isBlacklisted(sched, node, LOG)) {        continue;      }      assigned = sched.assignContainer(node); //这里的sched应该是FSAppAttempt      if (!assigned.equals(Resources.none())) { //如果发现进行了资源分配,即,不管是进行了预留,还是进行了实际的分配,都跳出循环        break;      }    }  } finally {    readLock.unlock();  }  return assigned;}

在进行分配以前,通过调用assignContainerPreCheck(node)判断能否在当前这个leaf queue上往这个节点进行资源分配,即,如果这个队列的已使用资源小于这个队列的最大可使用资源(还有剩余资源)并且这个节点没有被预定,那么才可以继续往下走进行分配:

  /**   * 判断当前的这个Queue是否能够进行一次资源分配,即,   * 如果这个队列已经使用的资源小于最大资源并且这不是一个被预定的节点才能进行分配,否则,不可以再分配container了   */  protected boolean assignContainerPreCheck(FSSchedulerNode node) {    if (!Resources.fitsIn(getResourceUsage(),        scheduler.getAllocationConfiguration().getMaxResources(getName()))        || node.getReservedContainer() != null) {      return false;    }    return true;  } public static boolean fitsIn(Resource smaller, Resource bigger) {    return smaller.getMemory() <= bigger.getMemory() &&        smaller.getVirtualCores() <= bigger.getVirtualCores();  }

同样,我们看到,和FSParentQueue.assignContainer()中对挂载的子节点进行排序一样,使用Policy对挂载的FSAppAttempt进行了排序。显然,根据不同的Policy定义的比较器,决定了对Application进行资源分配的先后顺序。

在完成了队列资源检查并对这个叶子节点下面挂载的应用进行了排序,就开始遍历这些应用并尝试将某个应用的请求分配到当前的这个yarn服务器节点。同样,我们从for循环可以看到,一旦有成功分配,循环立刻退出。这种限制一轮分配最多只进行一次成功分配,是为了请求被均匀分配到服务器节点,以及每个队列都得到及时的请求分配,而不至于在某一轮分配中将请求全部分配到某个节点,或者,某个资源队列进行了多次调度,其它队列却一次都没有被调度。

这样,FSLeafQueue.assignContainer()最终通过调用具体的Application的assignContainer()方法实现调度。在RM端,每个Application使用FSAppAttemt对象来表示。

应用通过调用FSAppAttemt.assignContainer()方法进行资源分配

这是FSAppAttemt.assignContainer()方法:

private Resource assignContainer(FSSchedulerNode node, boolean reserved) { //返回一个基于Priority进行排序的Priority集合  Collection<Priority> prioritiesToTry = (reserved) ?      Arrays.asList(node.getReservedContainer().getReservedPriority()) :      getPriorities();  // For each priority, see if we can schedule a node local, rack local  // or off-switch request. Rack of off-switch requests may be delayed  // (not scheduled) in order to promote better locality.  synchronized (this) {    for (Priority priority : prioritiesToTry) {      if (getTotalRequiredResources(priority) <= 0 ||          !hasContainerForNode(priority, node)) {        continue;      }      addSchedulingOpportunity(priority);      // Check the AM resource usage for the leaf queue      if (getLiveContainers().size() == 0 && !getUnmanagedAM()) {        if (!getQueue().canRunAppAM(getAMResource())) {          return Resources.none();        }      }      ResourceRequest rackLocalRequest = getResourceRequest(priority,          node.getRackName());      ResourceRequest localRequest = getResourceRequest(priority,          node.getNodeName());      if (localRequest != null && !localRequest.getRelaxLocality()) {        LOG.warn("Relax locality off is not supported on local request: "            + localRequest);      }      NodeType allowedLocality;      if (scheduler.isContinuousSchedulingEnabled()) { //如果使用的是持续调度,那么需要根据当前的时间确认当前对这个priority可以采取的本地化水平        allowedLocality = getAllowedLocalityLevelByTime(priority,            scheduler.getNodeLocalityDelayMs(),            scheduler.getRackLocalityDelayMs(),            scheduler.getClock().getTime()); //根据时间去决定允许的本地策略是NODE_LOCAL/RACK_LOCAL/OFF_SWITCH      } else {        allowedLocality = getAllowedLocalityLevel(priority,            scheduler.getNumClusterNodes(),            scheduler.getNodeLocalityThreshold(), //yarn.scheduler.fair.locality.threshold.node ,这是一个从0到1之间的小数,代表,我必须经过多少次失败的调度,才能允许将本地化策略降级到RACK_LOCAL            scheduler.getRackLocalityThreshold());//yarn.scheduler.fair.locality.threshold.node,这是一个从0到1之间的小数,代表,我必须经过多少次失败的调度,才能允许将本地化策略降级到OFF_SWITCH      }      if (rackLocalRequest != null && rackLocalRequest.getNumContainers() != 0          && localRequest != null && localRequest.getNumContainers() != 0) { //如果NODE_LOCAL/RACK_LOCAL都不是空的,那么进行NODE_LOCAL级别的调度        return assignContainer(node, localRequest,            NodeType.NODE_LOCAL, reserved);      }      if (rackLocalRequest != null && !rackLocalRequest.getRelaxLocality()) {        continue;      }      if (rackLocalRequest != null && rackLocalRequest.getNumContainers() != 0          && (allowedLocality.equals(NodeType.RACK_LOCAL) ||          allowedLocality.equals(NodeType.OFF_SWITCH))) { //如果RACK_LOCAL的请求不是空的并且允许的本地化策略是RACK_LOCAL/OFF_SWITCH,则进行RACK_LOCAL调度        return assignContainer(node, rackLocalRequest,            NodeType.RACK_LOCAL, reserved);      }      ResourceRequest offSwitchRequest =          getResourceRequest(priority, ResourceRequest.ANY); //否则,进行OFF_SWITCH调度      if (offSwitchRequest != null && !offSwitchRequest.getRelaxLocality()) {        continue;      }      if (offSwitchRequest != null &&          offSwitchRequest.getNumContainers() != 0) {        if (!hasNodeOrRackLocalRequests(priority) ||            allowedLocality.equals(NodeType.OFF_SWITCH)) {          return assignContainer(              node, offSwitchRequest, NodeType.OFF_SWITCH, reserved); //进行OFF_SWITCH调度        }      }    }  }  return Resources.none();}

根据Priority优先级排序以及资源请求的Priority简介

每一个FSAppAttempt保存了自己当前所有container请求的Priority的list,通过调用getPriorities()获取了按照Priority(优先级)排序的container请求的list,我们来看这个对Priority进行排序的比较器:

  //一个排序的HashSet,用来保存当前这个applicaiton的所有请求的优先级  final Set<Priority> priorities = new TreeSet<Priority>(      new org.apache.hadoop.yarn.server.resourcemanager.resource.Priority.Comparator());public static class Comparator   implements java.util.Comparator<org.apache.hadoop.yarn.api.records.Priority> {    @Override    public int compare(org.apache.hadoop.yarn.api.records.Priority o1, org.apache.hadoop.yarn.api.records.Priority o2) {      return o1.getPriority() - o2.getPriority(); //从比较器来看,值越小优先级越高    }  }

这些container请求的Priority是不同类型的应用的ApplicationMaster自己决定的。以MapReduce为例,MapReduce的任务的ApplicationMaster实现类是MRAppMaster,它委托RMContainerAllocator向远程的ApplicationMasterService请求资源,MR的任务类型包括Map任务、Reduce任务和Fail Map任务(失败需要重试的map),我们看在RMContainerAllocator中对这三种类型的任务的优先级定义:

static final Priority PRIORITY_FAST_FAIL_MAP;static final Priority PRIORITY_REDUCE;static final Priority PRIORITY_MAP;//略static {  PRIORITY_FAST_FAIL_MAP = RecordFactoryProvider.getRecordFactory(null).newRecordInstance(Priority.class);  PRIORITY_FAST_FAIL_MAP.setPriority(5);  PRIORITY_REDUCE = RecordFactoryProvider.getRecordFactory(null).newRecordInstance(Priority.class);  PRIORITY_REDUCE.setPriority(10);  PRIORITY_MAP = RecordFactoryProvider.getRecordFactory(null).newRecordInstance(Priority.class);  PRIORITY_MAP.setPriority(20);}

可以看到,FAST_FAIL_MAP任务、REDUCE任务和MAP任务的Priority分别是5,10,15,数字越小,优先级越高,因此在FSAppAttempt.assignContainer()中这个请求就会优先被考虑,即,如果有失败的Map任务,这个失败的Map任务优先执行,其次是Reduce任务,最后是正常的Map任务。实际运行情况下,MR任务总是先产生map任务,因此先只提交map任务运行,然后如果有reduce任务,reduce任务将优先于现有的map任务运行,而失败的map任务由于需要重试,其它reduce任务可能在等待这个失败的map任务执行完才能进入下一个阶段,因此它的优先级最高。

获得排序后的Priority,就可以按照优先级,遍历这个Priority的list,取出对应Priority的container请求,尝试进行资源分配。

分配前的校验

对于某一个priority的请求,在进行资源分配之前,会进行以下检查:

  • 确认这个priority的确有请求存在,因为FSAppAttemtp除了保存所有请求的Priority的list,还保存了每一个Priority到请求的对应关系,即Priority到ResourceRequest的对应关系。我在 YARN请求资源时的locality和reality限定 这一节讲过RequestRequest对象的结构;

  • 再次确认这个priority对应的请求的确适合在这个节点上运行,同样是调用hasContainerForNode()方法进行判断了,我在判断这个application是否适合在这个节点上分配资源运行 这一节专门讲过请求的本地化特性以及hasContainerForNode()的判断机制;

  • 如果当前准备分配的container是ApplicationMaster的container,那么这个container的分配需要判断是否满足maxAMShare的配置限制;maxAMShare是FairScheduler用来限制一个队列的资源最多可用来运行ApplicationMaster而不是具体job的比例。我们具体来看看这一部分代码:

    //如果liveContainer==0,并且这个Application的AM是一个managedAM,那么在分配container的时候必须考虑//是否超过了当前队列的maAMShare配置的最大AM比例值if (getLiveContainers().size() == 0 && !getUnmanagedAM()) {if (!getQueue().canRunAppAM(getAMResource())) {  return Resources.none();}} /* * 判断当前队列是否能够运行一个ApplicationMaster应用 * @param amResource 需要运行的am的资源量 * @return true if this queue can run */public boolean canRunAppAM(Resource amResource) {  float maxAMShare = //获取队列的maxAMShare参数,即队列允许用来运行AM的资源总量      scheduler.getAllocationConfiguration().getQueueMaxAMShare(getName());  if (Math.abs(maxAMShare - -1.0f) < 0.0001) { //如果配置的值为-1.0f,则说明没有限制    return true;  }  //计算队列中可以用来运行AM的最大资源量,即,用队列的FairShare * maxAMShare,这里的fair share指的是instaneous fair share值  Resource maxAMResource = Resources.multiply(getFairShare(), maxAMShare);  Resource ifRunAMResource = Resources.add(amResourceUsage, amResource); //计算如果运行了这个am以后这个队列所有的am的资源使用量  return !policy      .checkIfAMResourceUsageOverLimit(ifRunAMResource, maxAMResource);  //对于默认的FairSharePolicy,判断如果运行了这个am,是否超过了maxAMShare的限制}

    了解一下什么叫做 unmanaged AM:yarn中ApplicationMaster分为两种,一种是unmanaged am,这种ApplicationMaster独立运行在yarn之外,虽然需要与Yarn RM进行通信和进行资源申请,但是其本身运行所需要的资源不是yarn分配的;另外一种叫做managed AM,即客户端在向yarn提交应用,会先申请ApplicationMaster的启动请求,yarn会分配一个container来运行ApplicationMaster,然后ApplicationMaster会向yarn进行Map/Reduce job的资源申请以及job的管理;

    对于managed am,yarn通过maxAMShare对资源队列中用来运行ApplicationMaster的资源进行了限制,这是为了防止发生资源死锁:如果一个队列中太多的资源都用来运行ApplicationMaster,即队列被很多小的应用充斥,应用都在运行,此时队列资源耗尽,所有ApplicationMaster都申请不到新的container,因此不断等待,新的应用又无法申请新的资源。这段代码就是判断当前的这个container是不是AppliationMaster的container,如果是,则必须满足maxAMShare的资源限制。

  • 在完成了以上校验,即确认可以进行container的分配,就开始进行资源分配。

判断资源分配的locality并进行资源分配

yarn需要确认当前这一个分配是进行的NODE_LOCAL/RACK_LOCAL/OFF_SWITCH中的哪种分配。显然,从优先级角度,NODE_LOCAL>RACK_LOCAL>OFF_SWITCH,因为Yarn是最希望能够满足container的NODE_LOCAL请求的,当然,如果不能满足,则只能进行本地化降级,这种降级不是第一次发现无法满足NODE_LOCAL就立刻进行的,而是稍作延迟,如果还是无法按照原本地化标准执行,则对本地化进行降级,因为,NODE_LOCAL现在满足不了,但是也许过一会儿就可以满足了,我们来看:

        ResourceRequest rackLocalRequest = getResourceRequest(priority,            node.getRackName()); //获取这个应用请求这个节点所在机架的RACK_LOCAL的请求        ResourceRequest localRequest = getResourceRequest(priority,            node.getNodeName()); //获取这个应用请求这个节点的NODE_LOCAL的请求        NodeType allowedLocality;        if (scheduler.isContinuousSchedulingEnabled()) { //如果使用的是持续调度,那么需要根据当前的时间确认当前对这个priority可以采取的本地化水平          allowedLocality = getAllowedLocalityLevelByTime(priority,              scheduler.getNodeLocalityDelayMs(),              scheduler.getRackLocalityDelayMs(),              scheduler.getClock().getTime()); //根据时间去决定允许的本地策略是NODE_LOCAL/RACK_LOCAL/OFF_SWITCH        } else {          allowedLocality = getAllowedLocalityLevel(priority,              scheduler.getNumClusterNodes(),              scheduler.getNodeLocalityThreshold(), //yarn.scheduler.fair.locality.threshold.node ,这是一个从01之间的小数,代表,我必须经过多少次失败的调度,才能允许将本地化策略降级到RACK_LOCAL              scheduler.getRackLocalityThreshold());//yarn.scheduler.fair.locality.threshold.node,这是一个从0到1之间的小数,代表,我必须经过多少次失败的调度,才能允许将本地化策略降级到OFF_SWITCH        }

Yarn会通过调用getAllowedLocalityLevelByTime()或者getAllowedLocalityLevel()判断当前允许的本地化策略,他们分别从时间层面和重试次数层面决定是不是已经经历了足够长时间的等待或者足够多次的等待,如果等待时间够长,或者失败次数哦够多,就只能尝试进行本地化降级了。

getAllowedLocalityLevelByTime()是用在连续性调度方式的,是从时间层面确定当前运行的本地化,思想是,如果距离上一次Container的分配时间已经超过了阈值,说明这个NODE_LOCAL的调度已经失败了一段时间,因此需要立刻降级:

  /**   * 返回允许的进行container调度的本地化水平,参数nodeLocalityDelayMs、rackLocalityDelayMs分别代表对NODE_LOCAL和RACK_LOCAL的   * 进行降级前需要延迟的时间阈值   */  public synchronized NodeType getAllowedLocalityLevelByTime(Priority priority,          long nodeLocalityDelayMs, long rackLocalityDelayMs,          long currentTimeMs) {    //默认NODE_LOCAL和RACK_LOCAL的延迟调度时间是-1,代表我们的FairSchedulerd调度器    // if not being used, can schedule anywhere    if (nodeLocalityDelayMs < 0 || rackLocalityDelayMs < 0) { //如果NODE_LOCAL或者RACK_LOCAL允许off-swtich本地级别的调度,则直接返回OFF_SWITCH的本地级别      return NodeType.OFF_SWITCH;    }    //默认的本地级别是NODE_LOCAL    if (! allowedLocalityLevel.containsKey(priority)) { //      allowedLocalityLevel.put(priority, NodeType.NODE_LOCAL);      return NodeType.NODE_LOCAL;    }    NodeType allowed = allowedLocalityLevel.get(priority); //获取这个优先级运行的LOCAL级别    // if level is already most liberal, we're done    if (allowed.equals(NodeType.OFF_SWITCH)) { //如果这个LOCAL级别直接允许OFF_SWITCH,那就直接OFF_SWITCH      return NodeType.OFF_SWITCH;    }    //如果这个LOCAL级别不允许OFF_SWITCH调度,就需要根据RACK_LOCAL或者NODE_LOCAL以及超时时间进行判断最终是进行NODE_LOCAL、RACK_LOCAL还是OFF_SWITCH的本地级别    // check waiting time    long waitTime = currentTimeMs;    if (lastScheduledContainer.containsKey(priority)) { //如果上一次调度的container还有这个优先级的 , 则用当前时间减去上一个container的调度时间从而获得等待时间      waitTime -= lastScheduledContainer.get(priority);    } else {      waitTime -= getStartTime();//否则,等待时间就是从这个application启动的时间到现在的时间    }    long thresholdTime = allowed.equals(NodeType.NODE_LOCAL) ?            nodeLocalityDelayMs : rackLocalityDelayMs; //RACK_LOCAL或者NODE_LOCAL等待的阈值    //如果目前的等待时间已经超过了thresholdTime    if (waitTime > thresholdTime) { //如果等待时间超过了阈值      if (allowed.equals(NodeType.NODE_LOCAL)) { //        allowedLocalityLevel.put(priority, NodeType.RACK_LOCAL);//将NODE_LOCAL降级为RACK_LOCAL        resetSchedulingOpportunities(priority, currentTimeMs);      } else if (allowed.equals(NodeType.RACK_LOCAL)) {//将RACK_LOCAL降级为OFF_SWITCH        allowedLocalityLevel.put(priority, NodeType.OFF_SWITCH);        resetSchedulingOpportunities(priority, currentTimeMs);      }    }    //如果等待时间还没有超时,那就不会对locality进行降级,原来是什么,现在还是什么    return allowedLocalityLevel.get(priority);  }

getAllowedLocalityLevel()是从重试次数角度去考虑的,用在心跳调度方式,即,如果发现针对这个请求的重试次数已经超过了我们的FairScheduler规定的次数,则没有必要再等待了,直接降级处理:

/** * 给出当前集群的节点数量以及在进行本地化降级前失败的调度的次数阈值(这个阈值是一个比例值,即失败的次数占集群节点规模的比例), * 返回允许调度对当前这个Priority的container请求进行调度的locality, */public synchronized NodeType getAllowedLocalityLevel(Priority priority,    int numNodes, double nodeLocalityThreshold, double rackLocalityThreshold) {  // upper limit on threshold  if (nodeLocalityThreshold > 1.0) { nodeLocalityThreshold = 1.0; }  if (rackLocalityThreshold > 1.0) { rackLocalityThreshold = 1.0; }  // If delay scheduling is not being used, can schedule anywhere  if (nodeLocalityThreshold < 0.0 || rackLocalityThreshold < 0.0) { //如果我们没有打开延迟调度策略,那么,直接就用OFF_SWITCH    return NodeType.OFF_SWITCH;  }  //如果已经配置了延迟调度,则根据  // 默认的本地化策略是NODE_LOCAL  if (!allowedLocalityLevel.containsKey(priority)) { //如果这个优先级目前还没有保存在allowedLocalityLevel,则使用默认的NODE_LOCAL的本地化策略,因为我们总是希望进行NodeLocal的调度    allowedLocalityLevel.put(priority, NodeType.NODE_LOCAL);    return NodeType.NODE_LOCAL;  }  NodeType allowed = allowedLocalityLevel.get(priority); //如果当前优先级已经有了对应的允许的本地策略略,则根据允许的本地策略  // If level is already most liberal, we're done  if (allowed.equals(NodeType.OFF_SWITCH)) return NodeType.OFF_SWITCH; //乳沟直接就允许OFF_SWITCH,就OFF_SWITCH  double threshold = allowed.equals(NodeType.NODE_LOCAL) ? nodeLocalityThreshold :    rackLocalityThreshold; //获取允许的本地级别对应的阈值  // Relax locality constraints once we've surpassed threshold.  if (getSchedulingOpportunities(priority) > (numNodes * threshold)) { //如果超过了阈值,则需要进行本地化降级    if (allowed.equals(NodeType.NODE_LOCAL)) {      allowedLocalityLevel.put(priority, NodeType.RACK_LOCAL); //将本地化策略从NODE_LOCAL降级为RACK_LOCAL      resetSchedulingOpportunities(priority);    }    else if (allowed.equals(NodeType.RACK_LOCAL)) {      allowedLocalityLevel.put(priority, NodeType.OFF_SWITCH); //将本地化策略从RACK_LOCAL降级为OFF_SWITCH      resetSchedulingOpportunities(priority);    }  }  //如果还没有达到阈值,则该是什么本地化就还是什么本地化  return allowedLocalityLevel.get(priority);}

由此可以看到,连续调度和心跳调度对于本地化降级的处理思想其实是相同的,都是在降级之前进行适当的延迟。不同之处只是降级时机的判断标准不同

连续调度的降级时机是在时间层面判断距离上一次成功调度的时间是否已经超过了FairScheduler所规定的阈值,如果是,则可以立刻进行降级了,我们通过这两个配置项控制时间阈值:

而心跳调度的降级时机是在失败调度的次数层面,即,连续失败的次数是否已经超过了指定的阈值(这个阈值其实是一个比例值,乘以集群节点数,就是允许的失败次数),如果超过了,则需要降级,我们通过这两个配置项控制这个比例阈值:

在完成了本地化判断以后,就可以开始为container分配资源了,即调用private Resource assignContainer( FSSchedulerNode node, ResourceRequest request, NodeType type, boolean reserved),用来在确认了本地化策略后进行container的资源分配。assignContainer(...)的响应代码虽然比较繁杂,但是难度都不大,因此不做列出。

下图描述了 assignContainer()方法分配这个container的流程:

assignContainer()的具体流程

最后一个参数reserved标记当前是否是在给一个处于reserved状态的container分配资源,如果是一个reserved container,则不需要新创建container,而是直接把这个预留态的container从RMContainerState.RESERVED状态变成RMContainerState.ALLOCATED,而如果不是,则需要新创建container,并直接从RMContainerState.NEW状态变为RMContainerState.ALLOCATED状态,这个我们可以从RMContainerImpl中定义的container的状态机看到:

  private static final StateMachineFactory<RMContainerImpl, RMContainerState,                                            RMContainerEventType, RMContainerEvent>    stateMachineFactory = new StateMachineFactory<RMContainerImpl,        RMContainerState, RMContainerEventType, RMContainerEvent>(      RMContainerState.NEW)    // Transitions from NEW state    .addTransition(RMContainerState.NEW, RMContainerState.ALLOCATED,        RMContainerEventType.START, new ContainerStartedTransition())    //略    .addTransition(RMContainerState.RESERVED, RMContainerState.ALLOCATED,        RMContainerEventType.START, new ContainerStartedTransition())

可以看到,如果发生了RMContainerEventType.START事件,处于RMContainerState.NEW或者RMContainerState.RESERVED状态的container都会变成RMContainerState.ALLOCATED状态,同时,这个状态机的hook类是ContainerStartedTransition,会被调用。关于RM端container的状态定义和状态转换关系,大家可以参考这篇博客:《RMContainer状态机分析》。

ContainerStartedTransition判断当前的container是否是AM container,如果是,则需要开始ApplicationMaster的启动过程,并更新整个Appliation Attempt的状态(Appliation Attempt是整个Application的运行时的实例),因为,如果这个container是整个ApplicationMaster的container,那么整个Appliation Attempt的状态就发生变化,比如,如果这个continer是AM的container并且分配成功,那么整个Appliation Attempt的状态将从SCHEDULED变为 ALLOCATED_SAVING状态。关于Application Attempt的状态转换,大家可以参考《RMAppAttempt状态机分析》。

因此,ApplicationMaster Container和普通的执行具体Map/Reduce任务的Container分配以后的处理流程是不同的:

  • 如果是ApplicationMaster的Container被成功分配,这个分配事件最终会注册给ResourceManager的ApplicationMasterLaucher,ApplicationMasterLaucher维护着一个独立的线程,不断检测是否有新的ApplicationMaster的启动事件,如果有,就会通过ContainerManagementProtocol协议与container所在的服务器通信,直接在对应的节点上把这个ApplicationMaster进程运行起来,AppAttempt的状态因此从ALLOCATED变为LAUCHED;ApplicationMaster运行起来以后,开始进行普通的container的请求;

  • 如果是普通的container的请求,是ApplicationMaster向ResourceManager请求资源,ResourceManager会分配对应的Container并将分配结果告知ApplicationMaster,ApplicationMaster会和对应的NodeManager通信,启动这个container,这个通信也是基于ContainerManagementProtocol协议进行的。

RM分配Container的流程

如果这个启动的container是普通的container,那么只是会更新RM端每个应用的container信息等等。下一次收到了ApplicationMaster的心跳,就会把这些新分配的container信息返回给ApplicationMaster,ApplicationMaster接着会在对应的NodeManager上启动这些container。

从上图中还可以看到,如果这个container请求满足队列剩余资源,即队列剩余可用资源大于container需要的资源,但是,这个服务器节点的剩余资源却不足以运行这个container,就需要进行资源预留。关于资源预留,大家可以参考我专门讲解资源预留机制的博客:《Yarn FairScheduler 的资源预留机制导致集群停止服务事故分析》。

总结

Yarn通过资源树的形式来对整个集群的资源进行横向划分和纵向层级划分,很好地表达了企业应用中需要表达的部门之间的资源隔离以及上下级部门之间的资源分层逻辑。

Yarn的资源调度不是阻塞式的,即,不是ApplicationMaster发起资源请求、Yarn处理请求然后进行资源分配然后将分配结果通过本次响应返回给ApplicationMaster。每次ApplicationMaster发起请求,Yarn会把这些请求挂载到上文提到的资源队列树中。然后,通过连续调度方式,以一个独立的线程,每隔一段时间,对集群中所有的服务器进行一轮调度,或者,某个NodeManager的心跳信息到来以后触发对这个心跳服务器的资源分配。然后,当收到某个AppliationMaster的资源请求以后,就将当前已经进行了成功分配的分配结果通过这次请求的Response返回给AM,显然,这次返回的分配成功的container完全可能是上一次所请求的资源的container。

由此可见,AM和RM之前的资源请求通信,是一种心跳式的通信,AM的心跳定时发出,如果有新的请求,心跳就携带这些请求,否则不携带任何请求,如果有新的分配结果,心跳的响应就会携带回这些结果,否则,不返回任何新的container的分配结果。

Yarn将资源请求的本地化层级分为节点内、机架内和集群内三个本地化级别,满足某些应用希望数据计算和数据本身在同一节点,或者在同一机架的需求。对于节点内或者机架内的请求无法满足,Yarn采用延迟调度的方式,即过一段时间再尝试满足这种本地化需求。如果失败时间或者失败次数超过限制,就进行本地化降级,比如,如果重试N次或者N分钟发现仍然无法将请求分配到应用所指定的node,则尝试分配到这个node相同的机架,或者,如果重试N次或者N分钟发现仍然无法将请求分配到应用所指定的机架,则只好将应用分配到集群内任何可用的节点上。

原创粉丝点击