FairScheduler 是 hadoop 中的作业公平调度器,主要是解决当 TT 发送心跳告诉 JT 当前的空闲 slots 时,希望 JT 分配给 TT 相应多个 task ,让 TT 去执行这些 task 。所以 JT 就需要一个调度器来对作业进行调度,选择出作业,然后将作业的 task 分配 TT 去执行。
而 hadoop 中的 task 可以分为 map,reduce,jobsetup,jobcleanup,taskcleanup 这五种 task 。
关于 setup,cleanup task 的信息在
https://issues.apache.org/jira/browse/MAPREDUCE-463 , hadoop-5785
分别是为 job 做准备工作,为 job 做清理工作,清理 task 工作。
一个 job 初始化时,依次产生 map task,reduce task,steup and cleanup task 。
Setup,cleanup 分别有二个,对应于 map 和 reduce 的。
比如 jobcleanup 工作主要是删除输出的临时文件夹,因为 mapreduce 为了保证正确性,数据会先写到输出文件夹下面的 _temporary 文件下 , 如果正确执行的话,最终会将临时文件夹下的数据重命名为真正的输出文件夹下,而这个重命名在 hadoop 中的代价比较小。
Jobstepup 的原理也是一样,即先创建起相应的输出的临时文件夹,用于数据存储。
那么到底分配哪个 job 中的 map,reduce task 给 TT 去执行呢?这就是调度器要做的事情。
首先将用户分成多个组,每个组中的用户是相互独立的,每个组都有一定的容量大小,并且有实际的 slots 数目,根据这些来决定当前哪个组可以分配 task ,然后再到该组中的选择一个用户的 job 来分配。
FairScheduler
继承了 TaskScheduler 类。主有要变量 Configuration, TaskTrackerManager 二个变量,另外有方法:start(),terminate(),assignTasks(TaskTracker) 。
Start : 是由Jobtracker 启动的调用它,用来初始化调度器。
terminate() :停止工作。
assignTasks(TaskTracker) :为某个TT 分配task 队列。
Start
首先,创建日志监听器 eventLog = new FairSchedulerEventLog(); 将调度器的动作写到日志中,用于为后面决策作分析。
添加二个job 监听对象。
taskTrackerManager .addJobInProgressListener( jobListener );
if (! mockMode ) {
eagerInitListener = new EagerTaskInitializationListener(conf);
eagerInitListener .setTaskTrackerManager( taskTrackerManager );
eagerInitListener .start();
taskTrackerManager .addJobInProgressListener( eagerInitListener );
}
JobListener 对象是从调度器中添加或删除job ,继续JobInProgressListener 接口的方法。
public void jobAdded(JobInProgress job) { synchronized (FairScheduler. this ) { eventLog .log( "JOB_ADDED" , job.getJobID()); JobInfo info = new JobInfo( new JobSchedulable(FairScheduler. this , job, TaskType. MAP ), new JobSchedulable(FairScheduler. this , job, TaskType. REDUCE )); infos .put(job, info); poolMgr .addJob(job); // Also adds job into the right PoolScheduable update(); } } |
EagerTaskInitializationListener 对象启动了一个线程池来对job 队列进行实始化工作。
接下来创建组管理和对TT 的负载管理,最后初始化一些环境参数。
poolMgr = new PoolManager( this ); poolMgr .initialize(); loadMgr = (LoadManager) ReflectionUtils.newInstance ( conf.getClass( "mapred.fairscheduler.loadmanager" , CapBasedLoadManager. class , LoadManager. class ), conf); loadMgr .setTaskTrackerManager( taskTrackerManager ); loadMgr .setEventLog( eventLog ); loadMgr .start(); |
最后是启动一个 UpdateThread 的守护线程,另外创建好 servlet ,提供给给用户实时查看相关的调度信息。
if (! mockMode ) { new UpdateThread ().start(); } // Register servlet with JobTracker's Jetty server if ( taskTrackerManager instanceof JobTracker) { JobTracker jobTracker = (JobTracker) taskTrackerManager ; HttpServer infoServer = jobTracker. infoServer ; infoServer.setAttribute( "scheduler" , this ); infoServer.addServlet( "scheduler" , "/scheduler" , FairSchedulerServlet. class ); } |
UpdateThread 线程每隔 mapred.fairscheduler.update.interval 时间去计算调度的相关参数,如每个job 权重,公平性,当前运行的数目等。
然后就是将当前的job 运行参数dump 到log 中,用于分析和决策。
计算需要被抢占的task 数目。
public void run() { while ( running ) { try { Thread.sleep ( updateInterval ); update(); dumpIfNecessary(); preemptTasksIfNecessary(); } catch (Exception e) { LOG .error( "Exception in fair scheduler UpdateThread" , e); } } } } |
JobListener jobAdded()
当jobtracker 有job 提交后,该方法会被调用。
将job 信息存到map 中,同时调用组管理器将job 加入到组中,
最后调用update() 方法更新调度器中的相关参数。
public void jobAdded(JobInProgress job) { synchronized (FairScheduler. this ) { eventLog .log( "JOB_ADDED" , job.getJobID()); JobInfo info = new JobInfo( new JobSchedulable(FairScheduler. this , job, TaskType. MAP ), new JobSchedulable(FairScheduler. this , job, TaskType. REDUCE )); infos .put(job, info); poolMgr .addJob(job); // Also adds job into the right PoolScheduable update() ; } } |
assignTasks()
根据每个组的情况,计算出当前集群的map,reduce 运行数目。
for (Pool pool: poolMgr .getPools()) { runnableMaps += pool.getMapSchedulable().getDemand(); runningMaps += pool.getMapSchedulable().getRunningTasks(); runnableReduces += pool.getReduceSchedulable().getDemand(); runningReduces += pool.getReduceSchedulable().getRunningTasks(); } |
得到集群中所有 TT 加起来总的 map,reduce 的 slots 数目。
int totalMapSlots = getTotalSlots(TaskType. MAP , clusterStatus); int totalReduceSlots = getTotalSlots(TaskType. REDUCE , clusterStatus); |
利用排序算法对各个组进行排序。
// Get the map or reduce schedulables and sort them by fair sharing List<PoolSchedulable> scheds = getPoolSchedulables(taskType); Collections.sort (scheds, new SchedulingAlgorithms.FairShareComparator()); |
对于排好序的组遍历,获得相应的 map,reduce 的 task 。而在调用组的 assignTask() 方法时,会先对组内的 job 队列应用调度算法进行排序,然后再选择一个 job ,调用该 job 的 assignTask() 方法,这样就可以得到某个 job 的 task 任务了。
for (Schedulable sched: scheds) { // This loop will assign only one task eventLog .log( "INFO" , "Checking for " + taskType + " task in " + sched.getName()); Task task = taskType == TaskType. MAP ? sched.assignTask(tts, currentTime, visitedForMap) : sched.assignTask(tts, currentTime, visitedForReduce); if (task != null ) { foundTask = true ; JobInProgress job = taskTrackerManager .getJob(task.getJobID()); eventLog .log( "ASSIGN" , trackerName, taskType, job.getJobID(), task.getTaskID()); // Update running task counts, and the job's locality level if (taskType == TaskType. MAP ) { launchedMap.add(job); mapsAssigned++; runningMaps++; updateLastMapLocalityLevel(job, task, tts); } else { reducesAssigned++; runningReduces++; } // Add task to the list of assignments tasks.add(task); break ; // This break makes this loop assign only one task } // end if(task != null) } // end for(Schedulable sched: scheds) |
update()
这个方法是一个很重要的方法,就是用来 更新公平调度器的相关参数。
如:读取最新的配置参数,删除已经完成或者被kill 掉的job 。
更新job,pool 的最新需求。
计算每个组的map,reduce 的公平性。
更新每个组的时间优先级顺序。
当要将 job 加入到调度器时,以及调度器每隔一段时间会调用这个方法,去更新公平调度器的相关信息。分别在 JobListener 类中的jobAdded() 方法和UpdateThread 守护线程中调用。
管理调度中的所有组,即 Pool 。
记录调度中的每个组,一种映射关系,即组名与组对象。
private Map<String, Pool> pools = new HashMap<String, Pool>();
addJob(JobInProgress job)
当有job 来了,就将其加入到相应的组中。
public synchronized void addJob(JobInProgress job) { getPool(getPoolName(job)).addJob(job); } |
如果该组存在,则直接得到 Pool 对象,然后将 job 加入到组中。
如果组不存在,则创建一个 Pool 对象,将 job 加入到组后,再将组加入到组管理对象的队列中。
public synchronized Pool getPool(String name) { Pool pool = pools .get(name); if (pool == null ) { pool = new Pool( scheduler , name); pool.setSchedulingMode( defaultSchedulingMode ); pools .put(name, pool); } return pool; } |
reloadAllocs()
reloadAllocsIfNecessary()
组名,该组中的job, 以及组内的map,reduce task 的调度算法。
保存该组的job 队列。
private Collection<JobInProgress> jobs = new ArrayList<JobInProgress>();
该组的调度算法。
/** Scheduling mode for jobs inside the pool (fair or FIFO) */
private SchedulingMode schedulingMode ;
组的map,reduce 调度。
private PoolSchedulable mapSchedulable ;
private PoolSchedulable reduceSchedulable ;
addJob()
就是将job 加入到组队列,以及组的调度中。
public void addJob(JobInProgress job) { jobs .add(job); mapSchedulable .addJob(job); reduceSchedulable .addJob(job); } |
负责管理组内 job 的 map,reduce 调度问题。
实现了Schedulable 接口。
主要负责三件事情:
实现了assignTask() 接口,从多个组中分配task 给TT 。
提供了job/pool 信息给调度器,包括当前需求数,当前运行数,每个组的最小共享数,job/pool 的权重以及起始时间,优先级等。
同时对于每个用户来适用于公平调度算法。
addJob(JobInProgress)
PoolSchedulable 分为map 和reduce 二种,在Pool 构造时建立起来。
public Pool(FairScheduler scheduler, String name) { this . name = name; mapSchedulable = new PoolSchedulable(scheduler, this , TaskType. MAP ); reduceSchedulable = new PoolSchedulable(scheduler, this , TaskType. REDUCE ); } |
assignTask()
当公平调度选择了该组后,具体选择组内的哪个 job ,就由 PoolSchedulable 的该方法来完成。
仔细研读一下,步骤如下:
首先得到该组当前运行的 task 数目,如果当前运行数目大于公平调度给的最大 slots 数,那么返回 null 。
否则可以从该组中分配任务,得到该组的调度方式,有二种: FIFO,FAIR 。这个方式是针对 job 来说的。
然后将调度方式应用于当前的组内的所有 JobSchedulable 队列中的job 。
最后遍历所有 JobSchedulable 队列,调用job 的JobSchedulable 对象中的assignTask 方法,从job 中获得task 。
public Task assignTask(TaskTrackerStatus tts, long currentTime, Collection<JobInProgress> visited) throws IOException { int runningTasks = getRunningTasks(); if (runningTasks >= poolMgr .getMaxSlots( pool .getName(), taskType )) { return null ; } SchedulingMode mode = pool .getSchedulingMode(); Comparator<Schedulable> comparator; if (mode == SchedulingMode. FIFO ) { comparator = new SchedulingAlgorithms .FifoComparator(); } else if (mode == SchedulingMode. FAIR ) { comparator = new SchedulingAlgorithms.FairShareComparator(); } else { throw new RuntimeException( "Unsupported pool scheduling mode " + mode); } Collections.sort ( jobScheds , comparator); for (JobSchedulable sched: jobScheds ) { Task task = sched.assignTask (tts, currentTime, visited); if (task != null ) return task; } return null ; } |
Job 的调度管理。
主要变量有:
公平调度对象,job 对象,task 类型,有map,reduce 二种。
private FairScheduler scheduler ; private JobInProgress job ; private TaskType taskType ; private int demand = 0; |
assignTask ()
就是从该job 中,获得相应的map,reduce task 。
public Task assignTask(TaskTrackerStatus tts, long currentTime, Collection<JobInProgress> visited) throws IOException { if (isRunnable()) { visited.add( job ); TaskTrackerManager ttm = scheduler . taskTrackerManager ; ClusterStatus clusterStatus = ttm.getClusterStatus(); int numTaskTrackers = clusterStatus.getTaskTrackers();
// check with the load manager whether it is safe to // launch this task on this taskTracker. LoadManager loadMgr = scheduler .getLoadManager(); if (!loadMgr.canLaunchTask(tts, job , taskType )) { return null ; } if ( taskType == TaskType. MAP ) { LocalityLevel localityLevel = scheduler .getAllowedLocalityLevel( job , currentTime); scheduler .getEventLog().log( "ALLOWED_LOC_LEVEL" , job .getJobID(), localityLevel); // obtainNewMapTask needs to be passed 1 + the desired locality level return job .obtainNewMapTask(tts, numTaskTrackers, ttm.getNumberOfUniqueHosts(), localityLevel.toCacheLevelCap()); } else { return job .obtainNewReduceTask(tts, numTaskTrackers, ttm.getNumberOfUniqueHosts()); } } else { return null ; } } |
该类中有二个类:FairShareComparor ,FifoComparator 。分别实现了公平调度和先来服务算法。
二个类主要实现了一个接口。
public int compare(Schedulable s1, Schedulable s2) {}
这样可以对一个组内的job 进行排序,或者对各个组进行排序。
下面看一下公平调度的比较类 FairShareComparator ,即公平调度器是如何去选择哪个组来分配 task 任务。
判断当前运行的任务数目小于最小共享数目。
如果二个组都小于,则比较最小共享占比。即当前运行的数 / 该组的最小共享数。
如果二个组都大于,则比较权重比。即运行任务数 / 权重系数。
如果二者还是相等,则根据起始时间来比较。
public int compare(Schedulable s1, Schedulable s2) { double minShareRatio1, minShareRatio2; double tasksToWeightRatio1, tasksToWeightRatio2; int minShare1 = Math.min (s1.getMinShare(), s1.getDemand()); int minShare2 = Math.min (s2.getMinShare(), s2.getDemand()); boolean s1Needy = s1.getRunningTasks() < minShare1; boolean s2Needy = s2.getRunningTasks() < minShare2; minShareRatio1 = s1.getRunningTasks() / Math.max (minShare1, 1.0); minShareRatio2 = s2.getRunningTasks() / Math.max (minShare2, 1.0); tasksToWeightRatio1 = s1.getRunningTasks() / s1.getWeight(); tasksToWeightRatio2 = s2.getRunningTasks() / s2.getWeight(); int res = 0; if (s1Needy && !s2Needy) res = -1; else if (s2Needy && !s1Needy) res = 1; else if (s1Needy && s2Needy) res = ( int ) Math.signum (minShareRatio1 - minShareRatio2); else // Neither schedulable is needy res = ( int ) Math.signum (tasksToWeightRatio1 - tasksToWeightRatio2); if (res == 0) { // Jobs are tied in fairness ratio. Break the tie by submit time and job // name to get a deterministic ordering, which is useful for unit tests. res = ( int ) Math.signum (s1.getStartTime() - s2.getStartTime()); if (res == 0) res = s1.getName().compareTo(s2.getName()); } return res; } |
到此,把公平调度的基本框架了解了一下,具体的实现还是要看里面的具体源代码,才能真正掌握具体。另外就是官方的文档, http://hadoop.apache.org/common/docs/r0.20.2/fair_scheduler.html,
以及 Hadoop Fair Scheduler Design Document 。