0x00 系列文章目录
- 源码走读-Yarn-ResourceManager01-基础概念
- 源码走读-Yarn-ResourceManager02-RM的启动-脚本
- 源码走读-Yarn-ResourceManager03-RM的启动之RM详解
- 源码走读-Yarn-ResourceManager04-RM调度之FairScheduler
- 源码走读-Yarn-ResourceManager05-MR任务提交-客户端侧分析
- 源码走读-Yarn-ResourceManager06-MR任务提交-服务端分析
- 源码走读-Yarn-ResourceManager07-ShutdownHookManager
- 源码走读-Yarn-ResourceManager08-总结
0x04 RM调度之FairScheduler
RM对NM的调度分为心跳触发调度
和持续调度
,我们先从心跳调度开始讲。因为本文的主题是讲RM,这里就不讲NM启动过程了,放在另一篇文章里分析。我们直接从AsyncDispatcher
讲起。
4.1 AsyncDispatcher
AsyncDispatcher
的内部类GenericEventHandler
会处理一个EventType: NODE_UPDATE
的NodeUpdateSchedulerEvent
,然后走我们熟悉的事件处理流程,最后交给ResourceManager
的内部类SchedulerEventDispatcher
处理,:
4.2 SchedulerEventDispatcher
接收到NODE_UPDATE
事件后,会交给调度器FairScheduler
的handle
方法处理。
4.3 FairScheduler
因为事件是NODE_UPDATE
类型,所以会执行handle
方法中以下代码:
case NODE_UPDATE:
if (!(event instanceof NodeUpdateSchedulerEvent)) {
throw new RuntimeException("Unexpected event type: " + event);
}
// NodeUpdateSchedulerEvent是SchedulerEvent的子类
NodeUpdateSchedulerEvent nodeUpdatedEvent = (NodeUpdateSchedulerEvent)event;
nodeUpdate(nodeUpdatedEvent.getRMNode());
break;
// 处理从node来的update事件
private synchronized void nodeUpdate(RMNode nm) {
long start = getClock().getTime();
if (LOG.isDebugEnabled()) {
LOG.debug("nodeUpdate: " + nm + " cluster capacity: " + clusterResource);
}
eventLog.log("HEARTBEAT", nm.getHostName());
// 根据NM的NodeId 取出FSSchedulerNode
// FSSchedulerNode是配置了FairScheuler策略时 继承自SchedulerNode,从调度器角度表示的一个节点
FSSchedulerNode node = getFSSchedulerNode(nm.getNodeID());
// 拿到并清除在NM心跳间隔期间积累的container变更
List<UpdatedContainerInfo> containerInfoList = nm.pullContainerUpdates();
// 存放新启动的container
List<ContainerStatus> newlyLaunchedContainers = new ArrayList<ContainerStatus>();
// 存放已结束的container
List<ContainerStatus> completedContainers = new ArrayList<ContainerStatus>();
for(UpdatedContainerInfo containerInfo : containerInfoList) {
newlyLaunchedContainers.addAll(containerInfo.getNewlyLaunchedContainers());
completedContainers.addAll(containerInfo.getCompletedContainers());
}
// 将应该新启动的Container启动起来
for (ContainerStatus launchedContainer : newlyLaunchedContainers) {
containerLaunchedOnNode(launchedContainer.getContainerId(), node);
}
// 处理应该结束的Container
for (ContainerStatus completedContainer : completedContainers) {
ContainerId containerId = completedContainer.getContainerId();
LOG.debug("Container FINISHED: " + containerId);
completedContainer(getRMContainer(containerId),
completedContainer, RMContainerEventType.FINISHED);
}
if (continuousSchedulingEnabled) {
if (!completedContainers.isEmpty()) {
attemptScheduling(node);
}
} else {
// 尝试调度该node
attemptScheduling(node);
}
long duration = getClock().getTime() - start;
// 统计指标
fsOpDurations.addNodeUpdateDuration(duration);
}
下面看看attemptScheduling
:
synchronized void attemptScheduling(FSSchedulerNode node) {
if (rmContext.isWorkPreservingRecoveryEnabled()
&& !rmContext.isSchedulerReadyForAllocatingContainers()) {
return;
}
final NodeId nodeID = node.getNodeID();
if (!nodes.containsKey(nodeID)) {
// The node might have just been removed while this thread was waiting
// on the synchronized lock before it entered this synchronized method
LOG.info("Skipping scheduling as the node " + nodeID +
" has been removed");
return;
}
// 分配新的conatiner
// 1. 检查app预留情况
// 2. 如果没有预留就开始地调度
boolean validReservation = false;
FSAppAttempt reservedAppSchedulable = node.getReservedAppSchedulable();
if (reservedAppSchedulable != null) {
validReservation = reservedAppSchedulable.assignReservedContainer(node);
}
if (!validReservation) {
// No reservation, schedule at queue which is farthest below fair share
// 没有预留时,
int assignedContainers = 0;
// 已分配资源,初始为空资源
Resource assignedResource = Resources.clone(Resources.none());
// 得到当前可分配的最大资源量
Resource maxResourcesToAssign =
Resources.multiply(node.getAvailableResource(), 0.5f);
while (node.getReservedContainer() == null) {
boolean assignedContainer = false;
// 这一步很关键,开始尝试分配该node的container资源
// 最后得到是这一次准备分配的资源
Resource assignment = queueMgr.getRootQueue().assignContainer(node);
if (!assignment.equals(Resources.none())) {
// 分配成功,就增加统计量
assignedContainers++;
assignedContainer = true;
// 将此次分配的累加到已分配的资源中
Resources.addTo(assignedResource, assignment);
}
// 分配不成功就退出循环
if (!assignedContainer) { break; }
// 判断是否应该继续调度 比如未开启yarn.scheduler.fair.assignmultiple
if (!shouldContinueAssigning(assignedContainers,
maxResourcesToAssign, assignedResource)) {
break;
}
}
}
// 更新root队列指标
updateRootQueueMetrics();
}
4.4 FSParentQueue
我们接着看FSParentQueue
的assignContainer
方法:
@Override
public Resource assignContainer(FSSchedulerNode node) {
Resource assigned = Resources.none();
// 如果不允许分配比如超出配额限制就直接返回了
if (!assignContainerPreCheck(node)) {
return assigned;
}
// 在对子队列排序时需要持有写锁
writeLock.lock();
try {
// FairSharePolicy分配策略:
// 通过加权公平共享份额来实现调度比较。以外,低于其最低份额的调度计划比刚好达到最低份额的调度计划优先级更高。
// 在比较低于最小份额的那些调度计划时,参考的标准是低于最小份额的比例。
// 比如,作业A具有10个任务的最小份额中的8个,而作业B具有100个最小份额中的50个
// 则接下来调度作业B,因为B拥有50%的最小份额 而A处于占80%
// 在比较超过最小份额的调度计划时,参考的标准是 运行中任务数/权重
// 如果所有权重都相等,就把槽位给拥有最少任务数量的作业
// 否则,拥有更高权重的作业分配更多
// 对子队列列表按FairSharePolicy来排序
Collections.sort(childQueues, policy.getComparator());
} finally {
writeLock.unlock();
}
// 在排序和遍历排序后的子队列列表间隙会释放锁
// 那么这个队列列表在以下情况可能被修改:
// 1.将一个子队列添加到子队列列表末尾,那么显然这不会影响container分配
// 2.移除一个子队列,这样可能还挺好,我们就不分配资源给很快被移除的队列
readLock.lock();
try {
// 以下开始遍历子队列分配资源,只要一次分配成功就退出循环
for (FSQueue child : childQueues) {
// 这里有可能是FSParentQueue FSLeafQueue FSAppAttempt,都实现自Schedulable接口
assigned = child.assignContainer(node);
if (!Resources.equals(assigned, Resources.none())) {
break;
}
}
} finally {
readLock.unlock();
}
// 最后返回分配到的资源
return assigned;
}
4.5 FSLeafQueue
这个小节我们看看FairScheduelr中的叶子队列,首先是assignContainer方法:
@Override
public Resource assignContainer(FSSchedulerNode node) {
Resource assigned = Resources.none();
if (LOG.isDebugEnabled()) {
LOG.debug("Node " + node.getNodeName() + " offered to queue: " +
getName() + " fairShare: " + getFairShare());
}
if (!assignContainerPreCheck(node)) {
return assigned;
}
Comparator<Schedulable> comparator = policy.getComparator();
writeLock.lock();
try {
Collections.sort(runnableApps, comparator);
} finally {
writeLock.unlock();
}
// 在这个间隙我们释放锁可以有高效的性能、避免死锁
// 未排序的、可运行的App可能在这个时候直接添加
// 但我们可以接受,因为这种可能性极低
readLock.lock();
try {
// 遍历app列表
for (FSAppAttempt sched : runnableApps) {
// 判断待分配的app黑名单中是否包含该node,如果包含就分配下一个app
if (SchedulerAppUtils.isBlacklisted(sched, node, LOG)) {
continue;
}
// node尝试分配container资源给该app
assigned = sched.assignContainer(node);
// 如果分配成功就跳出循环
if (!assigned.equals(Resources.none())) {
if (LOG.isDebugEnabled()) {
LOG.debug("Assigned container in queue:" + getName() + " " +
"container:" + assigned);
}
break;
}
}
} finally {
readLock.unlock();
}
// 返回分配到的资源
return assigned;
}
4.6 小结
本章主要讲解了FairScheduler对Job的处理流程,其实理清了思路还是挺清晰的。
下一章,我们开始学习源码走读-Yarn-ResourceManager05-MR任务提交-客户端侧分析。