计算能力调度器支持多个队列,每个队列可配置一定的资源量,每个队列采用 FIFO 调度策略,为了防止同一个用户的作业独占队列中的资源,该调度器会对同一用户提交的作业所占资源量进行限定。调度时,首先按以下策略选择一个合适队列:计算每个队列中正在运行的任务数与其应该分得的计算资源之间的比值(即比较空闲的队列),选择一个该比值最小的队列;然后按以下策略选择该队列中一个作业:按照作业优先级和提交时间顺序选择,同时考虑用户资源量限制和内存限制。CapacityTaskScheduler的系统结构图如下:
为了使读者能够更好的理解计算能力调度器CapacityTaskScheduler的工作原理及流程,先附上与计算能力调度器相关联的类图(看不清可点击查看原图):
1.计算能力调度器CapacityTaskScheduler的启动
JobTracker节点在启动的过程中会自动的调用TaskScheduler的start()方法,而在CapacityTaskScheduler的start()方法中主要会做如下这些事情:
1).从QueueManager中获取集群配置的所有队列;
2).获取每一个队列配置的计算资源量(占整个集群计算能力的百分比),以及该队列中用户最少占用的资源量(占该队列计算能力的百分比);
3).对于没有明确配置计算资源的队列,将集群中剩余的计算资源平均分配给它们;
4).初始化两个任务调度器mapScheduler、reduceScheduler;
5).将作业队列管理器jobQueuesManager注册到JobTracker节点的监听器队列中,好接收客户端提交来的作业;
6).创建/初始化/启动作业初始化管理器initializationPoller;
2.作业队列管理器JobQueuesManager工作流程
作业队列管理器JobQueuesManager是对JobInProgressListener的实现,它被任务调度器注册到了JobTracker的监听器队列中,当JobTracker收到一个新提交的作业时,就会把该作业对应的JobInProgress通知给注册到它上面的所有监听器。当JobQueuesManager收到这个新的JobInProgress之后,就会把它放到该作业所属的队列的QueueInfo的WaitingJobs集合中,以等待初始化管理器将其交给该队列所属的作业初始化器来初始化。同时,作业队列管理器也会把这个新的JobInProgress通知给任务调度器(CapacityTaskScheduler),任务调度器收到之后就会立马更新该作业所属队列中提交用户提交的作业数量。
当一个JobInProgress的JobSetup任务被JobTracker调度并成功执行之后,该JobInProgress就处于RUNNING状态,同时JobTracker也会把这一状态改变时间通知给它的所有监听器。当JobQueuesManager收到这个时间之后,就会把这个JobInProgress添加到所属队列的QueueInfo的RunningJobs集合中,以等待任务调度器来调度其任务。
同样,当JobInProgress被完成时(无论成功、失败、kill),JobQueuesManager也会收到JobTracker的通知,在收到这个信息之后,JobQueuesManager就会把该从作业所属的队列的QueueInfo的WaitingJobs、Running集合中删除该作业,同时通知给任务调度器(CapacityTaskScheduler),然后任务调度器更新提交该作业的用户在队列中提交的作业数量。
总之,JobQueuesManager对每一个作业队列的管理分为两部分,一部分是等待初始化的作业,另一部分是等待调度的作业,其中,前者供作业初始化管理器使用,后者供任务调度器使用。每一次无论是选择初始化队列中的作业,还是调度队列中的作业,都是由限制的,这个限制来源于该队列配置的计算资源量、每个用户可占用的最大计算资源、每个用户可初始化的最大作业数量、队列可初始化作业的用户数量等。有限制,就必然有选择/调度的先后顺序,如果这个队列支持优先级,则这个顺序就依赖于作业的优先级,否则,就依赖于作业的提交先后顺序。
3.作业初始化管理器JobInitializationPoller
作业初始化管理器采用轮询的方式从JobQueuesManager的各个队列中选取合适数量的JobInProgress交给对应的作业初始化器初始化。JobInitializationPoller在初始化的时候就决定了哪些队列的作业交给那一个作业初始化器来完成其初始化。JobInitializationPoller在每一个轮询的过程中主要干两件事:一.清理已经初始化的作业;二.从每一个队列中选择合适的作业交给对应的作业初始化器。这个选择过程如下:
void init(Set<String> queues, CapacitySchedulerConf capacityConf) {
for (String queue : queues) {
int userlimit = capacityConf.getMinimumUserLimitPercent(queue);
int maxUsersToInitialize = ((100 / userlimit) + MAX_ADDITIONAL_USERS_TO_INIT);
int maxJobsPerUserToInitialize = capacityConf.getMaxJobsPerUserToInitialize(queue);
QueueInfo qi = new QueueInfo(queue, maxUsersToInitialize, maxJobsPerUserToInitialize);
jobQueues.put(queue, qi);
}
...
}
void selectJobsToInitialize() {
for (String queue : jobQueues.keySet()) {
//从该队列中选取一批待初始化的作业
ArrayList<JobInProgress> jobsToInitialize = getJobsToInitialize(queue);
printJobs(jobsToInitialize);
//获取该队列对应的作业初始化器
JobInitializationThread t = threadsToQueueMap.get(queue);
for (JobInProgress job : jobsToInitialize) {
t.addJobsToQueue(queue, job);//将作业交给初始化器
}
}
}
ArrayList<JobInProgress> getJobsToInitialize(String queue) {
QueueInfo qi = jobQueues.get(queue);
ArrayList<JobInProgress> jobsToInitialize = new ArrayList<JobInProgress>();
// 队列中允许初始化的最大用户数量.
int maximumUsersAllowedToInitialize = qi.maxUsersAllowedToInitialize;
int maxJobsPerUserAllowedToInitialize = qi.maxJobsPerUserToInitialize;
int maxJobsPerQueueToInitialize = maximumUsersAllowedToInitialize * maxJobsPerUserAllowedToInitialize;
int countOfJobsInitialized = 0;
HashMap<String, Integer> userJobsInitialized = new HashMap<String, Integer>();
Collection<JobInProgress> jobs = jobQueueManager.getWaitingJobs(queue);
for (JobInProgress job : jobs) {
String user = job.getProfile().getUser();
int numberOfJobs = userJobsInitialized.get(user) == null ? 0 : userJobsInitialized.get(user);
// If the job is already initialized then add the count against user then continue.
if (initializedJobs.containsKey(job.getJobID())) {
userJobsInitialized.put(user, Integer.valueOf(numberOfJobs + 1));
countOfJobsInitialized++;
continue;
}
boolean isUserPresent = userJobsInitialized.containsKey(user);
if (!isUserPresent && userJobsInitialized.size() < maximumUsersAllowedToInitialize) {
// this is a new user being considered and the number of users
// is within limits.
userJobsInitialized.put(user, Integer.valueOf(numberOfJobs + 1));
jobsToInitialize.add(job);
initializedJobs.put(job.getJobID(),job);
countOfJobsInitialized++;
} else if (isUserPresent && numberOfJobs < maxJobsPerUserAllowedToInitialize) {
userJobsInitialized.put(user, Integer.valueOf(numberOfJobs + 1));
jobsToInitialize.add(job);
initializedJobs.put(job.getJobID(),job);
countOfJobsInitialized++;
}
/*
* if the maximum number of jobs to initalize for a queue is reached
* then we stop looking at further jobs. The jobs beyond this number
* can be initialized.
*/
if(countOfJobsInitialized > maxJobsPerQueueToInitialize) {
break;
}
}
return jobsToInitialize;
}
从上面的代码中可以看出,在每一次的轮询过程中为每一个队列挑选的待初始化作业是有限制的,这个限制来自用户的数量和每一个用户的作业数量,也就是说对每一个队列,每次只能最多选择maxUsersToInitialize 个用户的作业,而同时每一个用户作业的数量不得多于maxJobsPerUserToInitialize个。
刚才说到,JobInitializationPoller在初始化的时候就已经建立了作业队列与作业初始化器的映射关系,即某一队列中的所有作业就只能交给一个具体的后台工作线程JobInitializationThread来负责初始化。至于负责作业初始化工作的后台线程的数量可以通过配置项mapred.capacity-scheduler.init-worker-threads来配置,但很明显这个配置值不能超过配置的队列数量(即使超过了,最后也会取队列的数量)。
4.作业的调度
CapacityTaskScheduler给TaskTracker节点调度作业任务的过程很简单,因为它并不直接调度作业的任务,而是交给它的两个内部任务调度器,一个负责调度作业的Map型任务,另一个负责调度作业的Reduce型任务。值得注意的是,计算能力调度器每次最多只能给TaskTracker节点分配一个Map或Reduce任务。这个过程由于很简单,所以不作解释而直接贴出代码:
/*
* 1.根据集群当前的计算资源来更新每一个队列的计算能力
* 2.根据当前作业的完成情况,来更新每一个队列中正在运行的任务数量,占用的计算资源,以及
* 每个用户占用的计算资源
*/
private synchronized void updateQSIObjects(int mapClusterCapacity, int reduceClusterCapacity) {
// if # of slots have changed since last time, update.
// First, compute whether the total number of TT slots have changed
for (QueueSchedulingInfo qsi: queueInfoMap.values()) {
// compute new capacities, if TT slots have changed
if (mapClusterCapacity != prevMapClusterCapacity) {
qsi.mapTSI.capacity = (int)(qsi.capacityPercent*mapClusterCapacity/100);
}
if (reduceClusterCapacity != prevReduceClusterCapacity) {
qsi.reduceTSI.capacity = (int)(qsi.capacityPercent*reduceClusterCapacity/100);
}
// reset running/pending tasks, tasks per user
qsi.mapTSI.resetTaskVars();
qsi.reduceTSI.resetTaskVars();
// update stats on running jobs
for (JobInProgress job :jobQueuesManager.getRunningJobQueue(qsi.queueName)) {
if (job.getStatus().getRunState() != JobStatus.RUNNING) {
continue;
}
int numMapsRunningForThisJob = mapScheduler.getRunningTasks(job);
int numReducesRunningForThisJob = reduceScheduler.getRunningTasks(job);
int numMapSlotsForThisJob = mapScheduler.getSlotsOccupied(job);
int numReduceSlotsForThisJob = reduceScheduler.getSlotsOccupied(job);
job.setSchedulingInfo(String.format(JOB_SCHEDULING_INFO_FORMAT_STRING, Integer.valueOf(numMapsRunningForThisJob), Integer.valueOf(numMapSlotsForThisJob), Integer.valueOf(numReducesRunningForThisJob), Integer.valueOf(numReduceSlotsForThisJob)));
qsi.mapTSI.numRunningTasks += numMapsRunningForThisJob;
qsi.reduceTSI.numRunningTasks += numReducesRunningForThisJob;
qsi.mapTSI.numSlotsOccupied += numMapSlotsForThisJob;
qsi.reduceTSI.numSlotsOccupied += numReduceSlotsForThisJob;
Integer i = qsi.mapTSI.numSlotsOccupiedByUser.get(job.getProfile().getUser());
qsi.mapTSI.numSlotsOccupiedByUser.put(job.getProfile().getUser(),Integer.valueOf(i.intValue() + numMapSlotsForThisJob));
i = qsi.reduceTSI.numSlotsOccupiedByUser.get(job.getProfile().getUser());
qsi.reduceTSI.numSlotsOccupiedByUser.put(job.getProfile().getUser(), Integer.valueOf(i.intValue() + numReduceSlotsForThisJob));
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("updateQSI: job %s: run(m)=%d, "
+ "occupied(m)=%d, run(r)=%d, occupied(r)=%d, finished(m)=%d,"
+ " finished(r)=%d, failed(m)=%d, failed(r)=%d, "
+ "spec(m)=%d, spec(r)=%d, total(m)=%d, total(r)=%d", job
.getJobID().toString(), Integer
.valueOf(numMapsRunningForThisJob), Integer
.valueOf(numMapSlotsForThisJob), Integer
.valueOf(numReducesRunningForThisJob), Integer
.valueOf(numReduceSlotsForThisJob), Integer.valueOf(job
.finishedMaps()), Integer.valueOf(job.finishedReduces()), Integer
.valueOf(job.failedMapTasks),
Integer.valueOf(job.failedReduceTasks), Integer
.valueOf(job.speculativeMapTasks), Integer
.valueOf(job.speculativeReduceTasks), Integer
.valueOf(job.numMapTasks), Integer.valueOf(job.numReduceTasks)));
}
/*
* it's fine walking down the entire list of running jobs - there
* probably will not be many, plus, we may need to go through the
* list to compute numSlotsOccupiedByUser. If this is expensive, we
* can keep a list of running jobs per user. Then we only need to
* consider the first few jobs per user.
*/
}
}
prevMapClusterCapacity = mapClusterCapacity;
prevReduceClusterCapacity = reduceClusterCapacity;
}
public synchronized List<Task> assignTasks(TaskTrackerStatus taskTracker) throws IOException {
TaskLookupResult tlr;
ClusterStatus c = taskTrackerManager.getClusterStatus();
int mapClusterCapacity = c.getMaxMapTasks();
int reduceClusterCapacity = c.getMaxReduceTasks();
int maxMapTasks = taskTracker.getMaxMapTasks();
int currentMapTasks = taskTracker.countMapTasks();
int maxReduceTasks = taskTracker.getMaxReduceTasks();
int currentReduceTasks = taskTracker.countReduceTasks();
updateQSIObjects(mapClusterCapacity, reduceClusterCapacity);
if ((maxReduceTasks - currentReduceTasks) > (maxMapTasks - currentMapTasks)) {
reduceScheduler.updateCollectionOfQSIs();
tlr = reduceScheduler.assignTasks(taskTracker);
if (TaskLookupResult.LookUpStatus.TASK_FOUND == tlr.getLookUpStatus()) {
Task _task = tlr.getTask();
return Collections.singletonList(_task);
}
else if((TaskLookupResult.LookUpStatus.TASK_FAILING_MEMORY_REQUIREMENT == tlr.getLookUpStatus() || TaskLookupResult.LookUpStatus.NO_TASK_FOUND == tlr.getLookUpStatus()) && (maxMapTasks > currentMapTasks)) {
mapScheduler.updateCollectionOfQSIs();
tlr = mapScheduler.assignTasks(taskTracker);
if(TaskLookupResult.LookUpStatus.TASK_FOUND == tlr.getLookUpStatus()) {
Task _task = tlr.getTask();
return Collections.singletonList(_task);
}
}
}
else {
mapScheduler.updateCollectionOfQSIs();
tlr = mapScheduler.assignTasks(taskTracker);
if (TaskLookupResult.LookUpStatus.TASK_FOUND == tlr.getLookUpStatus()) {
Task _task = tlr.getTask();
return Collections.singletonList(_task);
}
else if ((TaskLookupResult.LookUpStatus.TASK_FAILING_MEMORY_REQUIREMENT == tlr.getLookUpStatus() || TaskLookupResult.LookUpStatus.NO_TASK_FOUND == tlr.getLookUpStatus()) && (maxReduceTasks > currentReduceTasks)) {
reduceScheduler.updateCollectionOfQSIs();
tlr = reduceScheduler.assignTasks(taskTracker);
if(TaskLookupResult.LookUpStatus.TASK_FOUND == tlr.getLookUpStatus()) {
Task _task = tlr.getTask();
return Collections.singletonList(tlr.getTask());
}
}
}
return null;
}
下面就来详细的看看这两个内部的任务调度器mapScheduler、reduceScheduler的工作原理。实际上,从上面的类图中可以看出,它们的调度机制是完全一样的,即都继承了TaskSchedulingMgr的核心方法assignTasks()。这里首先要谈的是TaskSchedulingMgr对作业的调度主要分两个层次,一个是队列级别,一个是作业级别。具体的调度策略是:先从优先级最高的队列中按照作业优先级从高到低的顺序遍历每一个作业,知道找到一个合适的任务为止,如果在该队列中没有找到,则从优先级次最高的队列中按照上述的规则查找,...。TaskSchedulingMgr实现任务调度的具体过程如下:
/*检查一个用户已经占用的计算资源是否超出了它所在队列的预先设置限制*/
private boolean isUserOverLimit(JobInProgress j, QueueSchedulingInfo qsi) {
int currentCapacity;
TaskSchedulingInfo tsi = getTSI(qsi);
//作业所属队列总的计算资源
if (tsi.numSlotsOccupied < tsi.capacity) {
currentCapacity = tsi.capacity;
}
else {
currentCapacity = tsi.numSlotsOccupied + getSlotsPerTask(j);
}
//计算队列中每个用户能够占用计算资源的限制
int limit = Math.max((int)(Math.ceil((double)currentCapacity/(double)qsi.numJobsByUser.size())), (int)(Math.ceil((double)(qsi.ulMin*currentCapacity)/100.0)));
String user = j.getProfile().getUser();
if (tsi.numSlotsOccupiedByUser.get(user) >= limit) {
return true;
}
else {
return false;
}
}
/*
* 从某个队列中调度一个合适的任务
*/
private TaskLookupResult getTaskFromQueue(TaskTrackerStatus taskTracker, QueueSchedulingInfo qsi) throws IOException {
for (JobInProgress j : scheduler.jobQueuesManager.getRunningJobQueue(qsi.queueName)) {
//该作业还无法运行
if (j.getStatus().getRunState() != JobStatus.RUNNING) {
continue;
}
//检查提交该作业的用户在该作业所属队列中已占用的计算资源是否超出了限制
if (isUserOverLimit(j, qsi)) {
continue;
}
//检查集群配置的执行单个Map/Reduce任务内存是够满足当前作业的要求
if (scheduler.memoryMatcher.matchesMemoryRequirements(j, type, taskTracker)) {
Task t = obtainNewTask(taskTracker, j);
if (t != null) {
return TaskLookupResult.getTaskFoundResult(t);
} else {
continue;
}
} else {
if (getPendingTasks(j) != 0 || hasSpeculativeTask(j, taskTracker)) {
return TaskLookupResult.getMemFailedResult();
}
}
}
//如果程序执行到这里,说明限制条件太严格了,所以应该师徒降低分配标准来尽量的分配队列中的一个任务
for (JobInProgress j : scheduler.jobQueuesManager.getRunningJobQueue(qsi.queueName)) {
if (j.getStatus().getRunState() != JobStatus.RUNNING) {
continue;
}
if (scheduler.memoryMatcher.matchesMemoryRequirements(j, type, taskTracker)) {
Task t = obtainNewTask(taskTracker, j);
if (t != null) {
return TaskLookupResult.getTaskFoundResult(t);
} else {
continue;
}
} else {
if (getPendingTasks(j) != 0 || hasSpeculativeTask(j, taskTracker)) {
return TaskLookupResult.getMemFailedResult();
}
}
return TaskLookupResult.getNoTaskFoundResult();
}
/*给一个TaskTracker节点调度一个任务*/
private TaskLookupResult assignTasks(TaskTrackerStatus taskTracker) throws IOException {
printQSIs();
//一次遍历每一个队列,直到找到一个合适的任务
for (QueueSchedulingInfo qsi : qsiForAssigningTasks) {
if (0 == getTSI(qsi).capacity) {
continue;
}
TaskLookupResult tlr = getTaskFromQueue(taskTracker, qsi);
TaskLookupResult.LookUpStatus lookUpStatus = tlr.getLookUpStatus();
if (lookUpStatus == TaskLookupResult.LookUpStatus.NO_TASK_FOUND) {
continue;
}
if (lookUpStatus == TaskLookupResult.LookUpStatus.TASK_FOUND) {
return tlr;
}
else if (lookUpStatus == TaskLookupResult.LookUpStatus.TASK_FAILING_MEMORY_REQUIREMENT) {
return tlr;
}
}
return TaskLookupResult.getNoTaskFoundResult();
}
对于队列之间的优先级由队列的空闲程度决定的,也就是越空闲的队列,它的优先级就越高,以避免大量地浪费集群的计算资源,同时从空闲的队列中调度任务成功的概率也高。而每个队列内部的作业之间也存在着优先级,如果该队列没有开启作业优先级(用户在提交作业时可以设置手动设置作业的优先级,在Hadoop内部,作业的优先级共分为 5级),则这个优先级策略就是作业提交的越早,其优先级就越高;如果队列支持客户端的优先级设置,那么这时的优先级策略就和JobQueueTaskScheduler所采用的优先级策略是一样的了,其配置项如下:
<property>
<name>mapred.capacity-scheduler.queue.queueName.supports-priority</name>
<value>true/false</value>
</property>
计算能力调度器CapacityTaskScheduler的调度策略可以看出,任何一个TaskTracker节点在向JobTracker发送心跳包之后最多只能被分配一个任务,这样带来的一个缺点就是:当集群的规模很大且计算节点的硬件配置较高时或应用场景中短作业居多时,就会出现许多计算节点在大多数时间里处于空闲的状态,从而浪费了集群的计算资源。另外,它也没有考虑计算节点的负载均衡。
这里再补充一次关于为JobTracker节点配置任务调度器的方法:只需在JobTracker的配置文件(mapred-site.xml)中作如下配置即可:
<property>
<name>mapred.jobtracker.taskScheduler</name>
<value>任务调度器实现类的全限定名,如org.apache.hadoop.mapred.CapacityTaskScheduler</value>
</property>
Hadoop 调度的未来开发
既然Hadoop 调度器是可插入式的,那么您会看到针对某个独特集群部署而开发的新调度器。有两种正在开发的调度器(来自 Hadoop事项列表),分别是自适应调度器和学习调度器。学习调度器 (MAPREDUCE-1349)可用来在出现多种工作负载情况下维持利用率水平。目前,此调度器的实现重点关注 CPU 平均负载,网络和磁盘 I/O的利用率仍处于计划之中。自适应调度器 (MAPREDUCE-1380)重点关注根据性能和用户定义的业务目标自动调整某个作业。(此段落属于网络转载)