关于Yarn的一些常见问题
Yarn三种调度策略
理想情况下,应用对Yarn资源的请求应该立刻得到满足,但现实情况资源往往是有限的,特别是在一个很繁忙的集群,一个应用资源的请求经常需要等待一段时间才能的到相应的资源。在Yarn中,负责给应用分配资源的是Scheduler。在Yarn中有三种调度器可以选择:FIFO Scheduler ,Capacity Scheduler,FairScheduler。
-
FIFO Scheduler
FIFO Scheduler把应用按提交的顺序排成一个队列,这是一个先进先出队列,在进行资源分配的时候,先给队列中最头上的应用进行分配资源,待最头上的应用需求满足后再给下一个分配,依次类推。
FIFO Scheduler是最简单也是最容易理解的调度器,不需要任何配置,但它并不适用于共享集群。大的应用可能会占用所有集群资源,这就导致其它应用被阻塞。在共享集群中,更适合采用Capacity Scheduler或Fair Scheduler,这两个调度器都允许大任务和小任务在提交的同时获得一定的系统资源。
图中,在FIFO 调度器中,小任务会被大任务阻塞。 -
Capacity Scheduler
对于Capacity调度器,有一个专门的队列用来运行小任务,但是为小任务专门设置一个队列会预先占用一定的集群资源,这就导致大任务的执行时间会落后于使用FIFO调度器时的时间。
支持多个队列,每个队列可配置一定量的资源,每个采用FIFO的方式调度。为了防止同一个用户的job任务独占队列中的资源,调度器会对同一用户提交的job任务所占资源进行限制。分配新的job任务时,首先计算每个队列中正在运行task个数与其队列应该分配的资源量做比值,然后选择比值最小的队列。其次,按照job任务的优先级和时间顺序,同时要考虑到用户的资源量和内存的限制,对队列中的job任务进行排序执行。多个队列同时按照任务队列内的先后顺序一次执行。
Capacity调度器根据来自NodeManager的信息,将Container放到最适合的节点上。当工作负载可以预见的情况下,Capacity调度器效果最好,因为这有助于分配最小容量。要使调度器有效工作,需要为每个队列分配小于最高预期负载的最小容量。在每个队列内部,使用层次化的FIFO来调度多个应用程序,类似于在独立的FIFO调度其中使用的方式。 -
Fair Scheduler
在Fair调度器中不需要预先占用一定的系统资源,Fair调度器会为所有运行的job动态的调整系统资源。支持多个队列,每个队列可以配置一定的资源,每个队列中的job任务公平共享其所在队列的所有资源。队列中的job任务都是按照优先级分配资源,优先级越高分配的资源越多,但是为了确保公平每个job任务都会分配到资源。优先级是根据每个job任务的理想获取资源量减去实际获取资源量的差值决定的,差值越大优先级越高。
如图所示,当第一个大job提交时,只有这一个job在运行,此时它获得了所有集群资源;当第二个小任务提交后,Fair调度器会分配一半资源给这个小任务,让这两个任务公平的共享集群资源。
需要注意的是,在Fair调度器中,从第二个任务提交到获得资源会有一定的延迟,因为它需要等待第一个任务释放占用的Container。小任务执行完成之后也会释放自己占用的资源,大任务又获得了全部的系统资源。最终的效果就是Fair调度器即得到了高的资源利用率又能保证小任务及时完成。
Fair调度器支持 抢占,可以从ApplicationMaster要回Container。即当单个作业运行时,该作业获得了所有集群资源,当有另一个作业提交后,调度器会要回Container并分配适当的资源给这个作业(要回的在将来被分配的资源的多少取决于作业的大小,公平起见)。在这个过程中后面提交的任务获得资源会有一定延迟,因为需要等待前面任务的Container被ApplicationMaster要回。Fair调度器与Capacity调度器不同的一点就是,Fair调度器不需要事先占用一定的资源,它根据提交的作业动态调整系统资源。
Yarn抢占
在FairScheduler初始化会产生一个叫做updateThread的deamon线程:
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);
}
}
}
}
这个线程负责不断计算集群需要的资源并进行抢占,计算所需资源并抢占,发生在UpdateThread.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);//已经计算得到需要抢占多少资源,那么,下面就开始抢占了
}
}
shouldAttemptPreemption()用来从整个集群的层次判断是否应该尝试进行资源抢占,如果整个集群的层次不满足抢占条件,当然就不可以进行抢占: