Yarn Fair Scheduler详解

2,737 阅读11分钟

参考:www.cnblogs.com/qcloud1001/…

一 Fair Scheduler整体结构

Fair Scheduler运行流程就是Resource Manager启动FairSchedulerSchedulerDispatcher两个服务,各自负则update线程和handle线程。

update线程: 1. 更新瞬时队列资源Instantaneous fair share。 2. 在开启抢占功能的情况下判断各个leaf队列是否需要抢占资源

handle线程: 处理事件响应,比如集群增减节点,队列增减app,app更新container等(涉及到Steady fair share)

二 FairScheduler队列继承模块

yarn通过树形结构来管理队列。yarn通过树形结构来管理队列。从管理资源角度来看,树的根节点root队列(FSParentQueue)、非根节点(FSParentQueue)、叶子节点(FSLeaf)和app任务(FSAppAttempt,公平调度器角度的App)都是抽象的资源。它们都实现了Schedulable接口,都是一个可调度资源对象。它们都有自己的fair share(队列的资源量)方法,weight属性(权重)、minShare属性(最小资源量)、maxShare属性(最大资源量),priority属性(优先级)、resourceUsage属性(资源使用量属性)以及资源需求量属性(demand),同时也都实现了preemptContainer抢占资源的方法,assignContainer方法(为一个ACCEPTED的APP分配AM的container)。

三 资源分配(Steady fair share与Instantaneous fair share)

Steady Fair Share:是每个队列内存资源量的固定理论值。Steady Fair Share在RM初期工作后不再轻易改变,只有后续在增加节点(addNode)时才会重新计算。RM的初期工作也是handle线程把集群的每个节点添加到调度器中(addNode)。

Instantaneous Fair Share:是每个队列的内存资源量的实际值,是在动态变化的。yarn里的fair share如果没有专门指代,都是指的的Instantaneous Fair Share。

1 Steady fair share计算方式

handle线程如果接收到NODE_ADDED事件,会去调用addNode方法。

private synchronized void addNode(RMNode node) {
    FSSchedulerNode schedulerNode = new FSSchedulerNode(node, usePortForNodeName);
    nodes.put(node.getNodeID(), schedulerNode);
    //将该节点的内存加入到集群总资源
    Resources.addTo(clusterResource, schedulerNode.getTotalResource());
    //更新available资源
    updateRootQueueMetrics();
    //更新一个container的最大分配,就是UI界面里的MAX
    updateMaximumAllocation(schedulerNode, true);

    //设置root队列的steadyFair=clusterResource的总资源
    queueMgr.getRootQueue().setSteadyFairShare(clusterResource);
    //重新计算SteadyShares
    queueMgr.getRootQueue().recomputeSteadyShares();
    LOG.info("Added node " + node.getNodeAddress() +
            " cluster capacity: " + clusterResource);
}

recomputeSteadyShares使用广度优先遍历计算每个队列的内存资源量,直到叶子节点。
public void recomputeSteadyShares() {
    //广度遍历整个队列树
    //此时getSteadyFairShare 为clusterResource
    policy.computeSteadyShares(childQueues, getSteadyFairShare());
    for (FSQueue childQueue : childQueues) {
childQueue.getMetrics().setSteadyFairShare(childQueue.getSteadyFairShare());
        if (childQueue instanceof FSParentQueue) {
            ((FSParentQueue) childQueue).recomputeSteadyShares();
        }
    }
}

computeSteadyShares方法计算每个队列应该分配到的内存资源,总体来说是根据每个队列的权重值去分配,权重大的队列分配到的资源更多,权重小的队列分配到得资源少。但是实际的细节还会受到其他因素影响,是因为每队列有minResourcesmaxResources两个参数来限制资源的上下限。computeSteadyShares最终去调用computeSharesInternal方法。

computeSharesInternal方法概括来说就是通过二分查找法寻找到一个资源比重值R(weight-to-slots),使用这个R为每个队列分配资源(在该方法里队列的类型是Schedulable,再次说明队列是一个资源对象),公式是steadyFairShare=R * QueueWeights

computeSharesInternal是计算Steady Fair ShareInstantaneous Fair Share共用的方法,根据参数isSteadyShare来区别计算。

之所以要做的这么复杂,是因为队列不是单纯的按照比例来分配资源的(单纯按权重比例,需要maxR,minR都不设置。maxR的默认值是0x7fffffff,minR默认值是0)。如果设置了maxR,minR,按比例分到的资源小于minR,那么必须满足minR。按比例分到的资源大于maxR,那么必须满足maxR。因此想要找到一个R(weight-to-slots)来尽可能满足:

R*(Queue1Weights + Queue2Weights+...+QueueNWeights) <=totalResource
R*QueueWeights >= minShare
R*QueueWeights <= maxShare

computeSharesInternal详细来说分为四个步骤:

  1. 确定可用资源:totalResources = min(totalResources-takenResources(fixedShare), totalMaxShare)
  2. 确定R上下限
  3. 二分查找法逼近R
  4. 使用R设置fair Share
private static void computeSharesInternal(
        Collection<? extends Schedulable> allSchedulables,
        Resource totalResources, ResourceType type, boolean isSteadyShare) {

    Collection<Schedulable> schedulables = new ArrayList<Schedulable>();
    //第一步
    //排除有固定资源不能动的队列,并得出固定内存资源
    int takenResources = handleFixedFairShares(
            allSchedulables, schedulables, isSteadyShare, type);

    if (schedulables.isEmpty()) {
        return;
    }
    // Find an upper bound on R that we can use in our binary search. We start
    // at R = 1 and double it until we have either used all the resources or we
    // have met all Schedulables' max shares.
    int totalMaxShare = 0;
    //遍历schedulables(非固定fixed队列),将各个队列的资源相加得到totalMaxShare
    for (Schedulable sched : schedulables) {
        int maxShare = getResourceValue(sched.getMaxShare(), type);
        totalMaxShare = (int) Math.min((long)maxShare + (long)totalMaxShare,
                Integer.MAX_VALUE);
        if (totalMaxShare == Integer.MAX_VALUE) {
            break;
        }
    }
    //总资源要减去fiexd share
    int totalResource = Math.max((getResourceValue(totalResources, type) - takenResources), 0);
    //队列所拥有的最大资源是有集群总资源和每个队列的MaxResource双重限制
    totalResource = Math.min(totalMaxShare, totalResource);
    //第二步:设置R的上下限
//计算资源权值比的最大值,通过从1.0不断翻倍的方式来尝试,终止条件是resourceUsedWithWeightToResourceRatio()返回值不小于可分配的总资源量
    double rMax = 1.0;
    while (resourceUsedWithWeightToResourceRatio(rMax, schedulables, type)
            < totalResource) {
        rMax *= 2.0;
    }

    //第三步:二分法逼近合理R值
    //使用二分查找法在0到rMax之间确定一个值使得resourceUsedWithWeightToResourceRatio()的返回值最逼近可分配的总资源量
    double left = 0;
    double right = rMax;
    for (int i = 0; i < COMPUTE_FAIR_SHARES_ITERATIONS; i++) {
        double mid = (left + right) / 2.0;
        int plannedResourceUsed = resourceUsedWithWeightToResourceRatio(mid, schedulables, type);
        if (plannedResourceUsed == totalResource) {
            right = mid;
            break;
        } else if (plannedResourceUsed < totalResource) {
            left = mid;
        } else {
            right = mid;
        }
    }
    //第四步:使用R值设置,确定各个非fixed队列的fairShare,意味着只有活跃队列可以分资源
    // Set the fair shares based on the value of R we've converged to
    for (Schedulable sched : schedulables) {
        if (isSteadyShare) {
            setResourceValue(computeShare(sched, right, type),
                    ((FSQueue) sched).getSteadyFairShare(), type);
        } else {
            setResourceValue(computeShare(sched, right, type), sched.getFairShare(), type);
        }
    }
}

a 确定可用资源

handleFixedFairShares方法来统计出所有fixed队列的fixedShare相加,并且fixed队列排除掉不得瓜分系统资源(minShare)。yarn确定fixed队列的标准如下:

private static int getFairShareIfFixed(Schedulable sched,
                                       boolean isSteadyShare, ResourceType type) {

    //如果队列的maxShare<=0  则是fixed队列,fixdShare=0
    if (getResourceValue(sched.getMaxShare(), type) <= 0) {
        return 0;
    }

    // 如果是计算Instantaneous Fair Share,并且该队列内没有APP再跑,
    // 则是fixed队列,fixdShare=0
    if (!isSteadyShare &&
            (sched instanceof FSQueue)&& !((FSQueue)sched).isActive()) {
        return 0;
    }

    //如果队列weight<=0,则是fixed队列
    //如果对列minShare<=0, fixdShare=0,否则fixdShare=minShare
    if (sched.getWeights().getWeight(type) <= 0) {
        int minShare = getResourceValue(sched.getMinShare(), type);
        return (minShare <= 0) ? 0 : minShare;
    }

    return -1;
}

b 确定R上下限

R的下限为1.0,R的上限是由resourceUsedWithWeightToResourceRatio方法来确定。该方法确定的资源值W,第一步中确定的可用资源值T:W>=T时,R才能确定。

//根据R值去计算每个队列应该分配的资源
private static int resourceUsedWithWeightToResourceRatio(double w2rRatio, Collection<? extends Schedulable> schedulables, ResourceType type) {
//这里可以理解为一个假设情形下的资源分配总和,给每一个schedulable分配相应的份额,再累加起来,好像一次资源分配大演练
    int resourcesTaken = 0;
    for (Schedulable sched : schedulables) {
        int share = computeShare(sched, w2rRatio, type);
        resourcesTaken += share;
    }
    return resourcesTaken;
}
private static int computeShare(Schedulable sched, double w2rRatio,
                                ResourceType type) {
    //share=R*weight,type是内存
    double share = sched.getWeights().getWeight(type) * w2rRatio;
//保证应得份额在最小份额与最大份额之间
share = Math.max(share, getResourceValue(sched.getMinShare(), type));
    share = Math.min(share, getResourceValue(sched.getMaxShare(), type));
    return (int) share;
}

c 二分查找法逼近R

满足下面两个条件中的一个即可终止二分查找: W == T(步骤2中的rightmid) 超过25次(COMPUTE_FAIR_SHARES_ITERATIONS)

d 使用R设置fair share

设置fair share时,可以看到区分了Steady Fair ShareInstantaneous Fair Share

for (Schedulable sched : schedulables) {
      if (isSteadyShare) {
          setResourceValue(computeShare(sched, right, type),
                  ((FSQueue) sched).getSteadyFairShare(), type);
      } else {
          setResourceValue(computeShare(sched, right, type), sched.getFairShare(), type);
      }
  }

2 instaneous fair share计算方式

该计算方式与steady fair的计算调用栈是一致的,最终都要使用到computeSharesInternal方法,唯一不同的是计算的时机不一样。steady fair只有在addNode的时候才会重新计算一次,而Instantaneous Fair Share是由update线程定期去更新。

此处强调的一点是,在上文中我们已经分析如果是计算Instantaneous Fair Share,并且队列为空,那么该队列就是fixed队列,也就是非活跃队列,那么计算fair share时,该队列是不会去瓜分集群的内存资源。

而update线程的更新频率就是由yarn.scheduler.fair.update-interval-ms来决定的。 见UpdateThread

3 maxAMShare

集群资源还没用满,但是还有app在排队?

handle线程如果接收到NODE_UPDATE事件,如果(1)该node的机器内存资源满足条件,(2)并且有ACCEPTED状态的Application,那么将会为该待运行的APP的AM分配一个container,使该APP在所处的queue中跑起来。但在分配之前还需要一道检查canRuunAppAM。能否通过canRuunAppAM,就是由maxAMShare参数限制。

public boolean canRunAppAM(Resource amResource) {
    //默认是0.5f
    float maxAMShare =
            scheduler.getAllocationConfiguration().getQueueMaxAMShare(getName());
    if (Math.abs(maxAMShare - -1.0f) < 0.0001) {
        return true;
    }
    //该队的maxAMResource=maxAMShare * fair share(Instantaneous Fair Share)
    Resource maxAMResource = Resources.multiply(getFairShare(), maxAMShare);
    //amResourceUsage是该队列已经在运行的App的AM所占资源累加和
    Resource ifRunAMResource = Resources.add(amResourceUsage, amResource);
    //查看当前ifRunAMResource是否超过maxAMResource
    return !policy
            .checkIfAMResourceUsageOverLimit(ifRunAMResource, maxAMResource);
}

队列中运行的APP为An,每个APP的AM占用资源为R

ACCEPTED状态(待运行)的APP的AM大小为Ram

队列的fair shareQueFS

队列的maxAMResource=maxAMShare * QueFS

ifRunAMResource=A1.R+A2.R+...+An.R+Ram

ifRunAMResource > maxAMResource,则该队列不能接纳待运行的APP

四 资源抢占

ResourceManager. serviceInit() 初始化scheduler配置文件,这里配置的是fairscheduler

protected void serviceInit(Configuration configuration) throws Exception {
        //省略
	scheduler = createScheduler();
scheduler.setRMContext(rmContext);
addIfService(scheduler);
rmContext.setScheduler(scheduler);
        //省略
}

FairScheduler.initscheduler()开启线程
private void initScheduler(Configuration conf) throws IOException {
    synchronized (this) {
        //省略
        //创建更新线程,负责监控队列的状态并伺机进行抢占
        updateThread = new UpdateThread();
        updateThread.setName("FairSchedulerUpdateThread");
        updateThread.setDaemon(true);
        //省略
    }
}

private class UpdateThread extends Thread {

  @Override
  public void run() {
    while (!Thread.currentThread().isInterrupted()) {
      try {
        Thread.sleep(updateInterval);
        long start = getClock().getTime();
        update();
        preemptTasksIfNecessary();
        long duration = getClock().getTime() - start;
        fsOpDurations.addUpdateThreadRunDuration(duration);
      } catch (InterruptedException ie) {
        LOG.warn("Update thread interrupted. Exiting.");
        return;
      } catch (Exception e) {
        LOG.error("Exception in fair scheduler UpdateThread", e);
      }
    }
  }
}

这个线程不断计算集群需要的资源,计算所需资源并抢占

1 抢占方法preemptTasksIfNecessary

/**
 * 检查所有缺乏资源的Scheduler, 无论它缺乏资源是因为处于minShare的时间超过了minSharePreemptionTimeout
 * 还是因为它处于fairShare的时间已经超过了fairSharePreemptionTimeout。在统计了所有Scheduler
 * 缺乏的资源并求和以后,就开始尝试进行资源抢占。
 */
protected synchronized void preemptTasksIfNecessary() {
    if (!shouldAttemptPreemption()) { //检查集群是否允许抢占发生
        return;
    }

    long curTime = getClock().getTime();
    if (curTime - lastPreemptCheckTime < preemptionInterval) {
        return;//还没有到抢占时机,等下一次机会吧
    }
    lastPreemptCheckTime = curTime;

    //初始化抢占参数为none,即什么也不抢占
    Resource resToPreempt = Resources.clone(Resources.none());
    for (FSLeafQueue sched : queueMgr.getLeafQueues()) {
        //计算所有叶子队列需要抢占的资源,累加到资源变量resToPreempt中
        Resources.addTo(resToPreempt, resToPreempt(sched, curTime));
    }
    if (Resources.greaterThan(RESOURCE_CALCULATOR, clusterResource, resToPreempt,
            Resources.none())) { //如果需要抢占的资源大于Resources.none(),即大于0
        preemptResources(resToPreempt);//已经计算得到需要抢占多少资源,那么,下面就开始抢占了
    }
}

a 检查集群是否允许发生抢占

shouldAttemptPreemption()用来从整个集群的层次判断是否应该尝试进行资源抢占,如果整个集群的层次不满足抢占条件,当然就不可以进行抢占:

private boolean shouldAttemptPreemption() {
    if (preemptionEnabled) {//首先检查配置文件是否打开抢占
        return (preemptionUtilizationThreshold < Math.max(
                (float) rootMetrics.getAllocatedMB() / clusterResource.getMemory(),
                (float) rootMetrics.getAllocatedVirtualCores() /
                        clusterResource.getVirtualCores()));
    }
    return false;
}

shouldAttemptPreemption的判断标准主要有两个:

  1. 是否已经开启了抢占:即yarn.scheduler.fair.preemption是否配置为true
  2. 整体集群资源利用率是否已经超过了yarn.scheduler.fair.preemption.cluster-utilization-threshold的配置值。

如果以上条件均满足,则可以进行抢占相关的工作,包括计算需要抢占的资源,以及进行抢占。

b 计算需要抢占的资源大小

FairSchduler.resToPreempt()方法用来计算当前的Schedulable需要抢占的资源的大小,属于FairScheduler的核心方法:

/**
 * 计算这个队列允许抢占其它队列的资源大小。如果这个队列使用的资源低于其最小资源的时间超过了抢占超时时间,那么,
 * 应该抢占的资源量就在它当前的fair share和它的min share之间的差额。如果队列资源已经低于它的fair share
 * 的时间超过了fairSharePreemptionTimeout,那么他应该进行抢占的资源就是满足其fair share的资源总量。
 * 如果两者都发生了,则抢占两个的较多者。
 */
protected Resource resToPreempt(FSLeafQueue sched, long curTime) {
    long minShareTimeout = sched.getMinSharePreemptionTimeout();//minSharePreemptionTimeout
    long fairShareTimeout = sched.getFairSharePreemptionTimeout();//fairSharePreemptionTimeout
    Resource resDueToMinShare = Resources.none();//因为资源低于minShare而需要抢占的资源总量
    Resource resDueToFairShare = Resources.none();//因为资源低于fairShare 而需要抢占的资源总量
    if (curTime - sched.getLastTimeAtMinShare() > minShareTimeout) {//时间超过minSharePreemptionTimeout,则可以判断资源是否低于minShare
        //选取sched.getMinShare()和sched.getDemand()中的较小值,demand代表队列资源需求量,即处于等待或者运行状态下的应用程序尚需的资源量
        Resource target = Resources.min(RESOURCE_CALCULATOR, clusterResource,
                sched.getMinShare(), sched.getDemand());
        //选取Resources.none(即0)和 Resources.subtract(target, sched.getResourceUsage())中的较大值,即
        //如果最小资源需求量大于资源使用量,则取其差额,否则,取0,代表minShare已经满足条件,无需进行抢占
        resDueToMinShare = Resources.max(RESOURCE_CALCULATOR, clusterResource,
                Resources.none(), Resources.subtract(target, sched.getResourceUsage()));
    }

    if (curTime - sched.getLastTimeAtFairShareThreshold() > fairShareTimeout) {// //时间超过fairSharePreemptionTimeout,则可以判断资源是否低于fairShare
        //选取sched.getFairShare()和sched.getDemand()中的较小值,demand代表队列资源需求量,即处于等待或者运行状态下的应用程序尚需的资源量
        //如果需要2G资源,当前的fairshare是2.5G,则需要2.5G
        Resource target = Resources.min(RESOURCE_CALCULATOR, clusterResource,
                sched.getFairShare(), sched.getDemand());

        //选取Resources.none(即0)和 Resources.subtract(target, sched.getResourceUsage())中的较大值,即
        //如果fair share需求量大于资源使用量,则取其差额,否则,取0,代表minShare已经满足条件,无需进行抢占
        //再拿2.5G和当前系统已经使用的资源做比较,如果2.5G-usedResource<0, 则使用Resources.none(),即不需要抢占
        //否则,抢占资源量为2.5G-usedResource<0
        resDueToFairShare = Resources.max(RESOURCE_CALCULATOR, clusterResource,
                Resources.none(), Resources.subtract(target, sched.getResourceUsage()));
    }
    Resource resToPreempt = Resources.max(RESOURCE_CALCULATOR, clusterResource,
            resDueToMinShare, resDueToFairShare);
    if (Resources.greaterThan(RESOURCE_CALCULATOR, clusterResource,
            resToPreempt, Resources.none())) {
        String message = "Should preempt " + resToPreempt + " res for queue "
                + sched.getName() + ": resDueToMinShare = " + resDueToMinShare
                + ", resDueToFairShare = " + resDueToFairShare;
        LOG.info(message);
    }
    return resToPreempt;
}

根据Yarn的设计,由于资源抢占本身是一种资源的强行剥夺,会带来一定的系统开销。因此,Yarn会在实际抢占发生前,耐心等待一段时间,以尽量直接使用其它应用释放的资源来使用,而尽量避免使用抢占的方式。 因此,我们在FairScheduler.xml中,需要配置这两个超时时间:

  1. minSharePreemptionTimeout 表示如果超过该指定时间,Scheduler还没有获得minShare的资源,则进行抢占;
  2. fairSharePreemptionTimeout 表示如果超过该指定时间,Scheduler还没有获得fairShare的资源,则进行抢占。

c 开始抢占

/**
 * 基于已经计算好的需要抢占的资源(toPreempt()方法)进行资源抢占。每一轮抢占,我们从root 队列开始,
 * 一级一级往下进行,直到我们选择了一个候选的application.当然,抢占分优先级进行。
 * 依据每一个队列的policy,抢占方式有所不同。对于fair policy或者drf policy, 会选择超过
 * fair share(这里的fair scheduler都是指Instantaneous Fair Share)
 * 最多的ChildSchedulable进行抢占,但是,如果是fifo policy,则选择最后执行的application进行
 * 抢占。当然,同一个application往往含有多个container,因此同一个application内部container
 * 的抢占也分优先级。
 */
protected void preemptResources(Resource toPreempt) {
    long start = getClock().getTime();
    if (Resources.equals(toPreempt, Resources.none())) {
        return;
    }
    //warnedContainers,被警告的container,即在前面某轮抢占中被认为满足被强占条件的container 同样,yarn发现一个container满足被抢占规则,绝对不是立刻抢占,而是等待一个超时时间,试图让app自动释放这个container,如果到了超时时间还是没有,那么就可以直接kill了
    Iterator<RMContainer> warnedIter = warnedContainers.iterator();
    //toPreempt代表依旧需要进行抢占的资源
    while (warnedIter.hasNext()) {
        RMContainer container = warnedIter.next();
        if ((container.getState() == RMContainerState.RUNNING ||
                container.getState() == RMContainerState.ALLOCATED) &&
                Resources.greaterThan(RESOURCE_CALCULATOR, clusterResource,
                        toPreempt, Resources.none())) {
            warnOrKillContainer(container);
            Resources.subtractFrom(toPreempt, container.getContainer().getResource());//抢占到了一个container,则从toPreempt中去掉这个资源
        } else {
            warnedIter.remove();
        }
    }

    try {
        // Reset preemptedResource for each app
        for (FSLeafQueue queue : getQueueManager().getLeafQueues()) {
            queue.resetPreemptedResources();
        }

        //toPreempt代表了目前仍需要抢占的资源,通过不断循环,一轮一轮抢占,toPreempt逐渐减小
        while (Resources.greaterThan(RESOURCE_CALCULATOR, clusterResource,
                toPreempt, Resources.none())) { //只要还没有达到抢占要求
            RMContainer container =
                    getQueueManager().getRootQueue().preemptContainer();
            if (container == null) {
                break;
            } else {
                //找到了一个待抢占的container,同样,警告或者杀死这个container
                warnOrKillContainer(container);
                warnedContainers.add(container);
                //重新计算剩余需要抢占的资源
                Resources.subtractFrom(
                        toPreempt, container.getContainer().getResource());
            }
        }
    } finally {
        // Clear preemptedResources for each app
        for (FSLeafQueue queue : getQueueManager().getLeafQueues()) {
            queue.clearPreemptedResources();
        }
    }

    long duration = getClock().getTime() - start;
    fsOpDurations.addPreemptCallDuration(duration);
}

每一轮抢占,都会通过方法warnOrKillContainer来检查并处理所有的warnedContainers。
protected void warnOrKillContainer(RMContainer container) {
    ApplicationAttemptId appAttemptId = container.getApplicationAttemptId();
    FSAppAttempt app = getSchedulerApp(appAttemptId);
    FSLeafQueue queue = app.getQueue();
    LOG.info("Preempting container (prio=" + container.getContainer().getPriority() +
            "res=" + container.getContainer().getResource() +
            ") from queue " + queue.getName());

    Long time = app.getContainerPreemptionTime(container);

    if (time != null) {
        // if we asked for preemption more than maxWaitTimeBeforeKill ms ago,
        // proceed with kill
        //如果这个container在以前已经被标记为需要被抢占,并且时间已经超过了maxWaitTimeBeforeKill,那么这个container可以直接杀死了
        if (time + waitTimeBeforeKill < getClock().getTime()) {
            ContainerStatus status =
                    SchedulerUtils.createPreemptedContainerStatus(
                            container.getContainerId(), SchedulerUtils.PREEMPTED_CONTAINER);

            // TODO: Not sure if this ever actually adds this to the list of cleanup
            // containers on the RMNode (see SchedulerNode.releaseContainer()).
            completedContainer(container, status, RMContainerEventType.KILL); //执行清理工作
            LOG.info("Killing container" + container +
                    " (after waiting for premption for " +
                    (getClock().getTime() - time) + "ms)");
        }
    } else {
        //把这个container标记为可能被抢占,也就是所谓的container警告,在下一轮或者几轮,都会拿出这个container判断是否超过了maxWaitTimeBeforeKill,如果超过了,则可以直接杀死了。
        // track the request in the FSAppAttempt itself
        app.addPreemption(container, getClock().getTime());
    }
}

从方法名称可以看到,结果有两种:

  1. 杀死:如果这个container之前已经被标记为待抢占,并且距离标记时间已经超过了waitTimeBeforeKill却依然没有被自己的ApplicationMaster主动释放的container。如果是,那么既然在waitTimeBeforeKill之前已经向其ApplicationMaster发出警告,那么现在FairScheduler失去了耐心,直接杀死这个Container;
  2. 死期未到:如果这个Container之前已经被标记为抢占,但是距离标记时间还不到waitTimeBeforeKill,那么此次侥幸逃脱,下次再进行判断 ;
  3. 标记和警告:如果这个container还从来没有被标记为待抢占,那么这次就进行标记,记录标记时间,下次updateThread到来,这个container会历经被杀死或者暂时死期未到。

completedContainer(container, status, RMContainerEventType.KILL);是一个状态机过程,当前发生的事件是RMContainerEventType.KILL,即发生kill事件,然后ResourceManager端的container实现RMContainerImpl会根据自己的当前状态以及发生的kill事件,得出目标状态;// TODO 待研究

如果warnedContainer被抢占来的资源依然小于toPreempt,那就只好从队列里面选择某些container来抢占,抢占规则就是队列具体定义的Policy。这段逻辑在preemptResources()方法的这段代码里:

try {
    // Reset preemptedResource for each app
    for (FSLeafQueue queue : getQueueManager().getLeafQueues()) {
        queue.resetPreemptedResources();
    }

    //toPreempt代表了目前仍需要抢占的资源,通过不断循环,一轮一轮抢占,toPreempt逐渐减小
    while (Resources.greaterThan(RESOURCE_CALCULATOR, clusterResource,
            toPreempt, Resources.none())) { //只要还没有达到抢占要求
        //通过具体队列的Policy要求,选择一个container用来被抢占
        RMContainer container =
                getQueueManager().getRootQueue().preemptContainer();
        if (container == null) {
            break;
        } else {
            warnOrKillContainer(container);
            //将这个container加入到警告列表,以后每一轮都会检查它是否被释放或者抢占,如果超过了一定时间还是没有被抢占或者主动释放,就可以直接kill并抢占了
            warnedContainers.add(container);
            Resources.subtractFrom(
                    toPreempt, container.getContainer().getResource());
        }
    }
} finally {
    // Clear preemptedResources for each app
    for (FSLeafQueue queue : getQueueManager().getLeafQueues()) {
        queue.clearPreemptedResources();
    }
}

d 寻找被抢占的container

我们从RMContainer container = getQueueManager().getRootQueue().preemptContainer(); 看看具体的Policy是如何决定选择哪个container进行抢占(执行死刑)的。我们选择FairScheduler默认的Policy FairSharePolicy来进行分析。分析的过程是,实际上是沿着队列树按照深度优先,逐渐往下遍历直至找到一个container用来抢占。

FSParentQueue.preemptContainer()采用递归的方式

/**
 * 从root queue开始,找出一个可以被抢占的container进行抢占。
 * 决策和遍历过程实际上是一个递归调用的过程,从root queue开始,不断
 * 由下级队列决定抢占自己下一级的哪个queue或者application或者container
 * 最终,是由LeafQueue选择一个Application,然后Application选择一个
 * Container
 */
@Override
public RMContainer preemptContainer() {
    RMContainer toBePreempted = null;

    // Find the childQueue which is most over fair share
    FSQueue candidateQueue = null;
    Comparator<Schedulable> comparator = policy.getComparator();
    //从自己所有的子队列中选择一个最应该被抢占的队列
    for (FSQueue queue : childQueues) {
        if (candidateQueue == null ||
                comparator.compare(queue, candidateQueue) > 0) {
            candidateQueue = queue;
        }
    }

    // Let the selected queue choose which of its container to preempt
    //选择出来了一个待抢占的队列以后,让这个队列自行决定抢占哪个container,采用递归调用的方式
    if (candidateQueue != null) {
        toBePreempted = candidateQueue.preemptContainer();
    }
    return toBePreempted;
}

FSParentQueue.preemptContainer()的递归方式来看,寻找被抢占的container的过程,是从队列树的root queue开始,采用深度优先的方式进行!

FairSharePolicy使用的资源比较器是DefaultResourceCalculator,进行资源大小的比较时,只考虑了memory,没有考虑vCore。

因此,我们来看FSLeafQueue.preemptContainer(),LeafQueue的意思是,下面没有子队列。

FSLeafQueue.preemptContainer()
public RMContainer preemptContainer() {
    RMContainer toBePreempted = null;

    // If this queue is not over its fair share, reject
    if (!preemptContainerPreCheck()) {
        return toBePreempted;
    }

    if (LOG.isDebugEnabled()) {
        LOG.debug("Queue " + getName() + " is going to preempt a container " +
                "from its applications.");
    }

    // Choose the app that is most over fair share
    Comparator<Schedulable> comparator = policy.getComparator();
    FSAppAttempt candidateSched = null;
    readLock.lock();
    try {
        //从该叶子队列中的所有application中,选择一个更应该被强占的application
        //如果使用默认Policy FairSharePolicy,那么选择标准就是该Application当前资源
        //的欠缺或者充裕程度,资源越充裕,越可能被选中
        for (FSAppAttempt sched : runnableApps) {
            if (candidateSched == null ||
                    comparator.compare(sched, candidateSched) > 0) {
                candidateSched = sched;
            }
        }
    } finally {
        readLock.unlock();
    }

    // Preempt from the selected app
    if (candidateSched != null) {
        //由于是叶子队列,因此candidateSched肯定是一个APP,即FSAppAttempt对象
        toBePreempted = candidateSched.preemptContainer();
    }
    return toBePreempted;
}

FSLeafQueueFSParentQueue的抢占逻辑是几乎相同的,都是通过递归遍历进行深度优先遍历,唯一的区别,就是FSParentQueue的child是FSParentQueue或者FSLeafQueue,而FSLeafQueue的child是FSAppAttemtp

FSAppAttempt.preemptContainer()

/**
 * 根据优先级,从application的所有container中选择一个container用来被抢占
 */
@Override
public RMContainer preemptContainer() {
    //省略
    RMContainer toBePreempted = null;
    //获取自己所有的running container
    for (RMContainer container : getLiveContainers()) {
        //使用比较器RMContainerComparator选择出一个最应该被抢占的container
        if (!getPreemptionContainers().contains(container) &&
                (toBePreempted == null ||
                        comparator.compare(toBePreempted, container) > 0)) {
            toBePreempted = container;
        }
    }
    return toBePreempted;
}

FSAppAttempt用来决定自己的哪个container拿出来被抢占,采用的是比较器RMContainerComparator:
static class RMContainerComparator implements Comparator<RMContainer>,
        Serializable {
    @Override
    public int compare(RMContainer c1, RMContainer c2) {
        int ret = c1.getContainer().getPriority().compareTo(
                c2.getContainer().getPriority());
        if (ret == 0) {
            return c2.getContainerId().compareTo(c1.getContainerId());
        }
        return ret;
    }
}

可见,规则就是比较优先级,选择一个优先级较低的container,如果优先级相同,则比较containerId并选择一个id比较小的container。

2 资源抢占总结

从以上代码的整体逻辑可以看到,yarn进行资源抢占在计算需要抢占多少资源的时候,是从整个yarn集群的范围内进行计算的,而不是为了满足某一个application的资源而为了它进行单独的抢占。

五 总结

最开始准备这次分享的时候,只是抱着搞清Fair Scheduler参数的想法来理清逻辑。在代码阅读的过程中发现Yarn队列的设计非常精妙。无论是(root)parentQueue , 还是leaf queue ,或者是Application,虽然处在一个tree的不同位置或是级别,但是他们的性质是一样的,都被抽象为Schedulable,因此都需要实现preemptContainer()方法。在决定哪个container被抢占的时候,就可以递归进行。parentQueue交给下面的leafQueu,而LeafQueue则交给下面的Application决定,Applactiion则根据container的优先级,决定哪个container被抢占。

六 TODO

  1. RMContainerImpl状态转换细节;
  2. Yarn FairScheduler的资源预留机制。