- 概述
- 线上事故描述、日志分析和解决方案
- 事故现象描述
- 日志分析
- 解决方案
- Yarn资源预留机制原理解析
- 资源预留概述
- 为满足预留条件的Container进行资源预留
- 结束预留,开始运行
- 总结
概述
我们线上2.7.2版本的Yarn集群在一个周六的早上开始发生大量任务挂起。由于周末在家,未能及时发现。5个小时以后同事手动kill掉部分应用,系统立刻恢复正常应用。由于急于立刻恢复应用,因此未能保存当时yarn的应用堆栈和内存dump,只有INFO级别的日志文件可供排查原因。我们通过hadoop社区与hadoop的开发者交流,同时根据日志、结合代码,最终确认问题原因来自于yarn的资源预留特性。本文一方面描述我们线上集群发生问题的现象、日志以及我们的解决方案,同时,我们从代码层面去分析yarn的资源预留机制的实现原理;
线上事故描述、日志分析和解决方案
事故现象描述
我们的yarn监控报警系统会每分钟通过yarn的Rest API请求它的队列状态并保存为日志,同时,如果发现队列发生严重的应用堆积将报警。通过分析yarn本身的ResourceManager日志和我们的监控系统日志,我们发现,集群从上午09:00开始出现问题,直到下午14:00手动kill掉部分应用集群恢复运行,发生以下主要现象:
- 全集群范围应用堆积:系统逐渐发生资源堆积,而且,应用堆积发生在所有队列,即,不是某一个队列的资源紧张问题;
- 集群可用资源充裕:全集群应用堆积发生过程中,集群整体资源内存占用率为60%(650G/1100G);
- 集群资源完全静止:事故发生以后,我们的监控日志显示,yarn的资源占用率完全不变了,即,没有任何资源的释放和资源的申请。观察事故发生以后的yarn ResourceManager日志,发现除了有新应用提交的日志,没有应用运行、资源释放的任何日志;
- ResourceManager虚拟机没有Full GC:由于是整个集群范围内的应用堆积,我们怀疑ResourceManager可能会发生了长时间的GC导致虚拟机停顿。因此使用gcutil,发现gc正常。同时,部分现象也与Full GC的猜测不符合,比如,Full GC只会导致资源调度效率降低,但是不会导致停止资源调度。同时,所有的NodeManager与ResourceManager通信正常,这说明ResourceManager可以及时处理NodeManager心跳等请求,另外,故障期间新的应用能够成功提交,只是一直处在accepted 状态,无法变为RUNNING .
- kill掉少量应用以后系统立刻恢复:故障过程中,yarn集群持续保持60%的资源占用率不变,所有队列均有剩余资源,没有任何container的产生和释放。当kill掉少量应用以后,5个小时内积累的大量的pending应用开始正常竞争系统可用资源,集群的空闲资源立刻被充分利用,直到这些应用运行完毕。
- 故障期间应用几乎没有日志:查看发生故障期间正在运行、提交却等待运行的应用日志,我们发现,故障发生的5个小时期间,应用几乎没有任何日志。在此期间yarn的日志也仅仅有新应用提交相关日志,资源的分配、释放从日志层面看在故障期间几乎没发生。
如果从资源调度角度考虑,yarn的资源分配会让任何一个应用申请的资源都不会超过其所在队列的资源限制,即,如果是某一个应用导致的问题,那么绝对不可能导致整个集群应用均发生pending。因此,初步怀疑是Yarn ResourceManager 本身的问题,这个问题可能是由于错误的RM配置导致的。
我们从jira上试图寻找问题答案,找到与我们遇到的问题类似的一些issue,最终,我把可能的原因开始缩小到yarn的资源预留上。
- YARN-4618:这里提到的问题,发生在系统有大量的pending的container在等待资源,因此计算系统总的pending 资源的时候发生了整型溢出,从而永远不再进行资源分配,pending container永远无法运行;我们的集群是由12台配置为(128G,32vCores)的节点组成,不存在这种大量存在的container的现象,因此排除;
- YARN-4270:提出了对资源预留的一个improvement,目的是防止集群的全部节点的资源全部预留导致整个集群的资源不可用,因此添加了改进方案,任何时候只能允许集群一定比例的节点被预留,默认是1;
- YARN-4477:它指出YARN-4270中的改进方案本身带来了新的bug,会导致yarn的FairScheduler进行调度的时候陷入死循环无法退出,因此自然所有后续调度都无法进行了
我通过直接google首先看到的是YARN-4618,在确认与我遇到的问题不匹配。没办法,只好在官方jira上反馈了我的问题,在回复里别人怀疑我遇到的问题可能是YARN-4477提到的问题。但是我很快发现,YARN-4477中的问题是2.8.0版本的hadoop的问题,是由于YARN-4270对资源预留的改进方案导致的,而我的hadoop版本是2.7.2,不存在这个改进方案。因此,我开始将注意力转向YARN-4270,也就是说,是否我遇到的问题刚好就是YARN-4270提到了这个问题,即系统全部节点被预留导致即使集群有空闲资源,新的应用也无法使用,即应用被饿死的情况。
重新回到Yarn ResourceManager的日志上。
日志分析
通过搜索Yarn ResourceManager中的reserve关键字,得到以下日志:
2017-11-11 09:00:30,343 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.106 app_id=application_1507795051888_183354
2017-11-11 09:00:30,346 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.105 app_id=application_1507795051888_183354
2017-11-11 09:00:30,401 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.84 app_id=application_1507795051888_183354
2017-11-11 09:00:30,412 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.85 app_id=application_1507795051888_183354
2017-11-11 09:00:30,535 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.102 app_id=application_1507795051888_183354
2017-11-11 09:00:30,687 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.86 app_id=application_1507795051888_183354
2017-11-11 09:00:30,824 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.108 app_id=application_1507795051888_183354
2017-11-11 09:00:30,865 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.104 app_id=application_1507795051888_183354
2017-11-11 09:00:30,991 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.103 app_id=application_1507795051888_183354
2017-11-11 09:00:31,232 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.107 app_id=application_1507795051888_183354
2017-11-11 09:00:31,249 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.101 app_id=application_1507795051888_183354
2017-11-11 09:00:34,547 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.100 app_id=application_1507795051888_183358
2017-11-11 09:01:06,277 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.100 app_id=application_1507795051888_183342
2017-11-11 09:01:16,525 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.100 app_id=application_1507795051888_183342
2017-11-11 09:01:25,348 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.100 app_id=application_1507795051888_183342
2017-11-11 09:01:28,351 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.100 app_id=application_1507795051888_183342
2017-11-11 09:02:29,658 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.100 app_id=application_1507795051888_183342
2017-11-11 09:04:14,788 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.100 app_id=application_1507795051888_183376
2017-11-11 09:04:26,307 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.100 app_id=application_1507795051888_183380
2017-11-11 09:04:51,200 INFO org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSAppAttempt: Making reservation: node=10.120.117.100 app_id=application_1507795051888_183383
从日志上可以看到,在事故发生以前的几分钟内,我们由12台服务器组成的集群,全部被预定,其中,有11台机器被application_1507795051888_183354
这个应用所预定预定,剩下的一台10.120.117.100服务器被另外几个应用多次预定,最终,所有服务器全部被预定,我们集群的每一台服务器在满足当前被预定的应用的运行条件以前,不再分配资源给任何其它的应用或者新提交的应用。这样,我们整个yarn集群陷入无限等待的死锁状态,即,在集群尚有剩余资源的情况下,由于当前还没有任何一台机器能够运行某个大应用的container,因此yarn对这台服务器的资源进行只释放,不分配的操作,以期待最终这台服务器能够容纳这个大应用的container运行,但是可惜大应用的资源始终无法得不到满足,新提交的应用也不再被分配资源,系统资源分配停止。
我们最终找到了application_1507795051888_183354
这个应用的来源,这是一个我们配置的crontab任务,该crontab会在每周六9:00提交一个pyspark应用到yarn上执行,我们直接看到了这个提交应用的参数:
from pyspark.sql import HiveContext
conf = SparkConf()\
.set("spark.driver.maxResultSize", "2g")\
.set('spark.driver.memory', '64g')\
.set('spark.executor.memory','64g')\
.set('spark.driver.cores','4')
无论是driver还是executor,都设置为64g,同时,我们每一台NodeManager的相关配置如下:
<property>
<name>yarn.nodemanager.resource.memory-mb</name>
<value>102400</value>
</property>
<property>
<name>yarn.scheduler.maximum-allocation-mb</name>
<value>64000</value>
</property>
yarn.nodemanager.resource.memory-mb
配置了每台服务器提供给yarn的内存资源,而yarn.scheduler.maximum-allocation-mb
配置了单个Container的最大资源量。由于pyspark设置的executor和driver memory都在yarn.scheduler.maximum-allocation-mb范围之内,因此,yarn在运行队列为executor创建的container将是64g,由于集群每一台服务器的剩余资源都不足64g,因此这个spark应用的任何一个driver和executor都无法运行,因此进行预订,最终将我们的集群全部预定完,集群陷入死锁。
简单解决方案
对于2.7.2版本的hadoop,还没有实现YARN-4270提出的防止集群全部被预留的improvement,因此,为了避免这样的情况发生,我们至少可以进行以下工作:
1.选择关闭资源预留机制;
2.最大container的大小设置得越大,某个container预定但永远无法得到执行的概率就越大,因此,我们应该根据我们单台集群的资源容量以及我们集群平时的运行负载,将yarn.scheduler.maximum-allocation-mb
设置为一个较小的合理值,防止资源预留导致的死锁发生。
3.当然,在资源提交这一层,我们也必须做好提交审查。导致问题的这段python代码是数据挖掘工程师写的,他们对集群的资源管理不是很了解,对分布式计算也不甚清楚,自然很难设置合适的提交参数,更不用说在应用运行起来以后进行调优了。
4.如果服务器的物理资源可以控制,那么单台服务器的内存和CPU与container的最大允许值相比应该足够大,比如,如果集群每台服务器运行时负载都在50%以上,最大Container是92G,而每台服务器的物理内存是100G,那么服务器被预定就成为大概率事件,而同样的平均负载和最大Container值,如果我们每台服务器的物理内存是200G,服务器被预留的概率则会大大降低,我在上文中讨论的服务器全部节点都被预定的情况也就几乎不会发生了。
Yarn资源预留机制原理解析
资源预留概述
资源预留发生在应用向Yarn进行资源申请的时候。yarn的资源预留机制是一种资源保证机制,它发生在应用程序申请的资源无法得到满足的时候。即,当应用程序申请的资源此刻无法满足但是未来只要有一定的资源释放就可以得到满足的情况下,yarn会面临两种选择:
- 优先为这个应用程序预留一个节点上的资源,直到累计释放的空闲资源满足了应用的需求。这种资源的分配方式叫做增量资源分配,即Incremental placement;
- 暂时放弃当前节点资源,直到某个节点的资源一次性满足应用程序的需求。这种分配方式叫做一次性资源分配,即all-or-nothing;
两种分配方式各有优缺点,对于增量资源分配而言,资源预留机制会导致资源浪费,集群资源利用率低,而一次性资源分配虽然在发现资源无法满足某个应用需求的时候及时放弃,但是,这个应用有可能永远得不到自己请求的资源因而永远无法运行,即饿死现象;
因此,yarn最终还是采用了增量资源分配机制,尽管会造成一定的资源浪费,但是不会出现饿死现象,大小应用都有平等的运行机会;
Yarn的一次资源预留,精确来讲,是将 某一个container 在 某个服务器节点上 做资源预留;它发生在服务器节点的可用资源无法满足container所需要的资源的情况下。资源预留发生在Yarn对应的调度器为应用的资源请求进行分配的过程中,如果大家对Yarn的资源分配原理和机制感兴趣,可以参考我的博客:《Yarn资源请求处理和资源分配原理解析》,下文不再对Yarn资源分配的过程和原理做讲解。
注意,请将集群节点资源和队列资源区分开。我们通过队列的配置文件fair-scheduler.xml/capacity-scheduler.xml定义了资源队列,这个队列的资源实际上是对应到各个实际的服务器节点上的内存和CPU资源的。比如,一个队列的容量是100G,代表提交到这个队列的应用所使用的内存最大不可以超过100G,但是,队列对于这些应用具体运行在哪些服务器上、在每台服务器上消耗了多少资源是不限制的,有可能,所有的container都运行在了某一台服务器上,占用了这台服务区100G资源,或者,这些应用运行在了10台服务器上,消耗每台服务器10G资源。
任何一个Yarn应用都提交的container请求如果超过了所在队列的剩余可用资源,这个container是不会被分配也不会被预留的,只有当这个container请求满足其所在队列的剩余资源,却无法在某个节点上运行,才会发生资源预留。
比如,我们的队列配置是这样的:
简单起见我们只考虑内存,忽视cpu资源。
为满足预留条件的Container进行资源预留
如果我们发现当前服务器节点的剩余资源无法满足这个container的资源请求,这时候就需要为这个container进行资源预留。注意,只有当这个container的资源请求满足队列的剩余可用资源,即,所在队列可用资源能够运行这个container,但是,当前服务器节点的剩余资源却不够用来运行这个container。
因此,如果这个container直接超过了队列的剩余资源,是不会进行资源预留的,直接就分配失败了。
当yarn进行资源分配的时候,如果某个请求满足预留条件,就会调用FSAppAttempt.reserve()
为这个container在这个服务器上进行预留:
private void reserve(Priority priority, FSSchedulerNode node,
Container container, boolean alreadyReserved) {
//查看FSAppAttempt.assignReservedContainer(),可以看到,对于一个已经进行了reservation的节点,会试图将这个 reservation变成allocation,
if (!alreadyReserved) { //如果这不是一个已经reserve过的attempt,即这个attempt从来都没有做过reservation
getMetrics().reserveResource(getUser(), container.getResource());
RMContainer rmContainer =
super.reserve(node, priority, null, container);
node.reserveResource(this, priority, rmContainer);//为这个节点进行reserve操作
} else { //如果这是已经已经处在预留状态的container,则继续保持预留状态
RMContainer rmContainer = node.getReservedContainer();
super.reserve(node, priority, rmContainer, container);
node.reserveResource(this, priority, rmContainer);//如果已经进行了reserve操作,则这里相当于更新一下节点的reserve操作
}
}
FSAppAttemt是FairScheduler层面的应用抽象,每一个FSAppAttemt对象代表了一个被调度的应用,每一个FSAppAttempt对象存放了对调度器FairScheduler的引用,以及当前自己占用的资源量(fair share),已经抢占的资源等等,每一个FSAppAttempt同时都是一个SchedulerApplicationAttempt,SchedulerApplicationAttempt则存放了当前application的所有container的详细信息(活跃的container的信息、处于预定状态的container的信息、ApplicationMaster的信息)等等;
因此,如果一个container变为预定状态,即一个container在某个服务器节点上进行预留,那么在应用层面和在服务器节点层面,都会有状态的改变:
- 在应用本身层面,SchedulerApplicationAttempt会通过
SchedulerApplicationAttempt.reserve()
方法更新这个container的状态、预定信息等:
public synchronized RMContainer reserve(SchedulerNode node, Priority priority,
RMContainer rmContainer, Container container) {
// Create RMContainer if necessary
if (rmContainer == null) {
rmContainer =
new RMContainerImpl(container, getApplicationAttemptId(),
node.getNodeID(), appSchedulingInfo.getUser(), rmContext);
//将这个预留的container的信息增加到currentReservation中,currentReservation中记录了当前
//这个application所有处于预留状态的资源总量
Resources.addTo(currentReservation, container.getResource());
// Reset the re-reservation count
resetReReservations(priority);
} else {
// Note down the re-reservation
addReReservation(priority);
}
//产生一个RMContainerReservedEvent,通知这个处于reserve状态的container
rmContainer.handle(new RMContainerReservedEvent(container.getId(),
container.getResource(), node.getNodeID(), priority));
//更新reservedContainers,reservedContainers保存了每一个Priority的预留信息,即预留节点和预留的container之间的对应关系
Map<NodeId, RMContainer> reservedContainers =
this.reservedContainers.get(priority);
if (reservedContainers == null) {
reservedContainers = new HashMap<NodeId, RMContainer>();
this.reservedContainers.put(priority, reservedContainers);
}
reservedContainers.put(node.getNodeID(), rmContainer);
return rmContainer;
}
在这里, RMContainer的实现类是RMContainerImpl,每一个RMContainerImpl对象是ResourceManager用来管理的一个container的抽象。RMContainer从创建、到运行到最后运行完毕销毁,会经历各种状态,因此,每一个RMContainer同时也是一个EventHanlder,它维护了一组状态机,状态机定义了相应container事件发生的时候响应的处理行为以及状态的切换。
关于RM端container的状态定义和状态转换关系,大家可以参考这篇博客:[RMContainer状态机分析](“http://dongxicheng.org/mapreduce-nextgen/yarnmrv2-resource-manager-rmcontainer-state-machine/“)。
rmContainer.handle(new RMContainerReservedEvent(container.getId(), container.getResource(), node.getNodeID(), priority))
生成了一个RMContainerReservedEvent事件交给RMContainer进行处理,RMContainerReservedEvent的事件类型是RMContainerEventType.RESERVED,RMContainer通过预先定义的状态机,规定了对这个事件的处理逻辑:
.addTransition(RMContainerState.NEW, RMContainerState.RESERVED,
RMContainerEventType.RESERVED, new ContainerReservedTransition())
// Transitions from RESERVED state
.addTransition(RMContainerState.RESERVED, RMContainerState.RESERVED,
RMContainerEventType.RESERVED, new ContainerReservedTransition())
可以看到,当发生RMContainerEventType.RESERVED
类型的事件,RMContainer的状态会从NEW状态转换成RESERVED状态(即从新建状态变成预留状态),或者从RESERVED状态变成RESERVED状态(即一个已经处在预留状态的container,调度器会不断尝试对其进行分配以摆脱预留,当然,如果分配失败,则还是处于RESERVED状态),同时,通过ContainerReservedTransition()这个状态转换器进行这个事件的处理逻辑:
private static final class ContainerReservedTransition extends
BaseTransition {
@Override
public void transition(RMContainerImpl container, RMContainerEvent event) {
RMContainerReservedEvent e = (RMContainerReservedEvent)event;
container.reservedResource = e.getReservedResource();
container.reservedNode = e.getReservedNode();
container.reservedPriority = e.getReservedPriority();
}
}
可以看到,ContainerReservedTransition()转换处理器只是更新了当前container的相关预留信息。
- 在服务器节点层面,如果该服务器节点container预留,会调用
FSSchedulerNode.reserveResource()
方法来更新这个节点的相关信息。FSSchedulerNode是RM层面对某个节点的抽象,一个FSSchedulerNode对象对应了一个服务器节点:
@Override
public synchronized void reserveResource(
SchedulerApplicationAttempt application, Priority priority,
RMContainer container) {
// Check if it's already reserved
RMContainer reservedContainer = getReservedContainer();
if (reservedContainer != null) {//如果这个节点上已经有reservation
// Sanity check
if (!container.getContainer().getNodeId().equals(getNodeID())) {//如果这个container试图再次在这个节点上进行reservation,则抛出异常
//略
}
//一个节点不可以为超过两个application进行reservation
if (!reservedContainer.getContainer().getId().getApplicationAttemptId()
.equals(container.getContainer().getId().getApplicationAttemptId())) {
//略
}
//略
}
setReservedContainer(container);
this.reservedAppSchedulable = (FSAppAttempt) application;
}
从代码中可以看到,如果节点被预留,会首先检查这个节点是否已经被预留,因为yarn规定:一个服务器在同一时间只可以被一个container预留。如果校验成功,可以预留,则更新节点对象的相关预留信息。
这样,一个container在某台服务器节点上进行了预留,并且container的状态被更新,被预留的服务器节点关于预留的信息被设置。在这个预留取消之前,这个服务器节点上将不会再分配任何新的container。
同时,每一次资源分配,都会检查这个服务器上的可用资源是否可以满足被预留的container的资源需求,如果满足,则结束预留,container从RESERVED状态变为ALLOCATED状态,然后,经过与ApplicationMaster的通信,这个container最终将在这个服务器上得到运行。
结束预留,开始运行
那么,当一个资源被预留的节点上的部分container运行完毕,资源被释放,使得它上面之前进行资源预留的container可以运行了,即,一个container的状态什么时候可以从预留状态转换为运行状态呢?对于连续调度和心跳调度,container状态从RESERVED
到ALLOCATED
的状态转换都是通过调用FSAppAttemtp.assignReservedContainer()
实现的。assignReservedContainer()
只是对已经处于预定状态的container进行一次尝试分配,分配可能会失败,如果失败,container依然保持为RESERVED状态。
我的博客《Yarn资源请求处理和资源分配原理解析》中讲解Yarn资源分配的两种模式,即,(1)FairScheduler使用一个独立的线程、不需要等待服务器节点通过心跳汇报自身状态的连续调度方式,以及,(2)只有当服务器通过心跳汇报自身状态时才会触发对这个节点资源的调度的心跳调度方式。两种调度方式下资源预留的运行逻辑没有任何不同,只是将一个container变为预留状态、或者从预留状态变为分配状态的时机不同:对于连续调度,当调度时机到来,他们都会调用这个被调度的应用的 FSAppAttempt.attemptScheduling(FSSchedulerNode node)
方法尝试进行资源分配,两种调度方式的判断逻辑是一致的,即:
- 如果这个节点不是一个预留的节点,即没有被container预留,则会进行正常的资源分配;即,从多个请求的FSAppAttempt中通过我们FairScheduler配置的policy,选择一个FSAppAttempt进行运行;
- 如果这个节点被某个container所预留,则尝试将这个已经预留的container在这个节点上进行分配;为某个处于预留状态的container进行资源分配是通过调用
FSAppAttempt.assignReservedContainer()
进行的。显然,这次分配只会将这个预留的container这它预留的节点上进行尝试分配,不会尝试其他节点。
/**
* 当一个container已经处于预留状态,这个方法被调用,用来尝试对这个处于预留状态的container进行分配,
* 即,尝试将这个container从reserved状态变成allocation状态
* @param node
* Node that the application has an existing reservation on
*/
public Resource assignReservedContainer(FSSchedulerNode node) {
RMContainer rmContainer = node.getReservedContainer(); //获取这个处于预留状态的container
Priority priority = rmContainer.getReservedPriority();
// 确认当前这个priority还有请求的资源,如果没有了,这个预留也没必要了,需要取消预留
if (getTotalRequiredResources(priority) == 0) {
unreserve(priority, node); //进行unreserve操作
return Resources.none();
}
// Fail early if the reserved container won't fit.
// Note that we have an assumption here that there's only one container size
// per priority.
if (!Resources.fitsIn(node.getReservedContainer().getReservedResource(),
node.getAvailableResource())) { //如果发现这个节点的可用资源还是满足不了这个预定的container的资源需求,则此次分配失败
return Resources.none();
}
//发现节点可用资源已经大于这个container的资源请求,则可以尝试进行资源预留
return assignContainer(node, true);
}
assignReservedContainer()
的核心工作,是检查当前节点的剩余资源是否依然小于这个预留的container的请求资源,如果是,那么这次分配直接快速失败,而如果发现节点的可用资源已经大于container的请求资源,那么就调用assignContainer(FSSchedulerNode node, boolean reserved)
进行分配。
FSAppAttempt.assignContainer(FSSchedulerNode node, boolean reserved)
方法进行资源分配是不区分当前container的状态是RESERVED还是NEW的状态的,它是对资源请求进行container分配(无论是否已经预定)的方法,使用参数reserved区分是对一个预留的container进行分配还是对一个新的container进行分配。对于处于预留状态的container,对如果分配成功,Container将从RESERVED 变为 ALLOCATED状态,如果分配失败,则RESERVED可能依然保持为RESERVED状态,或者回到NEW状态,具体的状态流转,可以参考下图:
我在我的博客《Yarn资源请求处理和资源分配原理解析》以FairScheduler为例详细介绍了Yarn的资源分配原理,大家可以看到 FSAppAttempt.assignContainer(FSSchedulerNode node, boolean reserved)
方法的详细解析。
总结
Yarn的资源预留机制可能在某些情况下造成集群的资源死锁,即,虽然队列有剩余资源,但是,由于单个container请求的资源量较大,所有服务器的剩余资源都无法满足其运行需求,因此发生预留。如果这样的container很多,极端情况下,可能所有服务器都处于预定状态,造成服务器资源死锁,Yarn虽然看似状态正常,但是已经停止服务。这种状态下,我们分析停止服务过程中的日志,不会看到任何异常信息,只有分析停止服务前的日志才能看到原因,即每一个服务器节点都已经被某个container预留,新的 应用提交以后无法申请到资源,而这些预留的container又永远无法摆脱预留状态。
Yarn对预留的处理,只是一种标记,即,将某个服务器标记为被某个container预留。任何一个被某个container预留的服务器,在取消预留或者被预留的container被成功分配之前,这个节点不会为其它container分配资源,只会一次次尝试为这个预留的container分配资源。而正常的没有被预留的服务器节点,则是根据我们定义的优先级(一个资源队列节点的孩子节点之间的优先级、同一个队列节点中应用的优先级、应用内container的优先级)选择出一个资源请求,在这个节点上分配一个container运行这个资源请求。
总之,当集群的资源利用率一直处于较高状态,Yarn的很多边界情况被触发的概率会大大增加,集群会出现较多的奇怪问题,变得非常不稳定。70%或以下左右的平均资源利用率会让集群处于比较好的运行状态。我们的FairScheduler调度器默认使用单一资源调度,进行资源调度的时候不考虑CPU消耗,如果集群负载较多,会经常发生服务器的CPU负载极高,甚至导致服务器宕机。