FairScheduler是由Facebook公司提出的,爲了解決Facebook要處理生產型作業(數據分析、Hive)、大型批處理作業(數據挖掘、機器學習)、小型交互作業(Hive查詢)的問題。同時滿足不同用戶提交的作業在計算時間、存儲空間、數據流量和響應時間都有不同需求的情況下,使用Hadoop mapreduce框架能夠應對多種類型作業並行執行,使得用戶具有良好的體驗,所以Facebook提出了該算法。
對於FairScheduler的設計思想,大家普遍都認爲它儘可能保證所有的作業都能夠獲得等量的資源份額。即但系統中只有一個作業執行時,它將獨佔集羣所有資源;而當有其它作業被提交時就會有TaskTracker被釋放並分配給新提交的作業,以保證所有的作業都能夠獲得大體相同的計算資源。筆者在分析了Hadoop-0.20.2.0版本的FairScheduler調度器源碼之後,發現這個對FairScheduler的描述有較大的出入,同時還發現它的源碼實現有相當大的問題。所以,本文將從源碼的級別上來詳細的分析Facebook的這個公平份額調度器(FairScheduler)。
FairScheduler設計的相當靈活,用戶可以根據自己的具體需求來對該任務調度器進行擴展,爲了更好的理解它的工作原理及擴展性,有必要先分析一下它的相關類。
1.FairScheduler的啓動
公平份額調度器FairScheduler的start()方法主要負載創建/啓動器內部的組件,同時它還提供了一個基於Web的可視化管理界面,來對FairScheduler進行簡單的管理與動態配置。關於這個可視化的管理不會在本文討論。這個啓動過程的主要步驟如下:
1). 創建/啓動作業初始化器EagerTaskInitializationListener;
2). 將作業初始化器和作業接收器(JobListener)註冊到JobTracker;
3). 創建作業池管理器(PoolManager);
4). 創建/啓動負載均衡器(LoadManager)和任務選擇器(TaskSelector);
5). 創建權重調整器(WeightAdjuster);
6). 根據配置來初始化assignMultiple和sizeBasedWeight的值;
7). 創建/啓動作業調度更新線程(UpdateThread);
8). 啓動基於Web的可視化管理器;
之所以說FairScheduler的設計具有較好的可擴展性,是因爲用戶可以根據自己的具體應用場景來自定義負載均衡器、任務選擇器以及權重調整器,然後配置到FairScheduler中即可。這個配置的方法就是在JobTracker節點的配置文件中配置對應的實現類全路徑名,而他們對應的配置項分別爲:
負載均衡器: mapred.fairscheduler.loadmanager
任務選擇器: mapred.fairscheduler.taskselector
權重調整器: mapred.fairscheduler.weightadjuster
另外,FairScheduler的屬性assignMultiple被用來控制給一個TaskTracker節點分配任務的數量,如果該值配置爲true,則最多可以給一個TaskTracker節點分配一個Map任務和一個Reduce任務,否則最多隻爲其分配一個Map任務或者一個Reduce任務。該值通過mapred.fairscheduler.assignmultiple來設置;屬性sizeBasedWeight被用來在更新作業的權重的時候是否應該考慮該作業尚未完成任務的大小,可通過mapred.fairscheduler.sizebasedweight來設置。2.作業池管理器(PoolManager)
這裏之所以要重點介紹作業池管理器,是因爲它與作業的調度順序休慼相關,確切的來說是它的配置在很到程度上決定作業的調度。這是因爲,FairScheduler從兩個層面上來考慮調度作業,它首先會根據User和Pool的限制條件來選取一定量的作業作爲當前可調度的作業集;然後對這個可調度的作業集進行基於公平度的排序,進而優先調度那些公平度低的作業。這個公平度反映了作業已佔用計算資源與它應該分得的計算資源之間的落差,每一個作業的公平虧欠度不僅取決於這個落差,還取決於作業處於這種資源分配不公平狀態的時間。同時,每一個作業應該分得的計算資源與它的權重以及它所屬Pool的權重有關,也就是說,作業的權重及它所屬Pool的權重越大,那麼它所佔用的計算資源也應該越多。一個作業的權重計算方法如下:
private double calculateRawWeight(JobInProgress job, TaskType taskType) {
if (!isRunnable(job)) {//作業是否在當前可調度的作業集中
return 0;
} else {
double weight = 1.0;
if (sizeBasedWeight) {
// 作業還未完成的任務數量
weight = Math.log1p(runnableTasks(job, taskType)) / Math.log(2);
}
weight *= getPriorityFactor(job.getPriority());//作業的優先級
if (weightAdjuster != null) {
//用戶來調整作業的權重
weight = weightAdjuster.adjustWeight(job, taskType, weight);
}
return weight;
}
}
剛纔說過,FairScheduler會先基於FIFO的策略從User和Pool的限制層面上選擇一批作業作爲當前可調度作業集,這裏的User限制是指在這個可調度作業集中屬於該User的作業數量不能超過他的上限,Pool限制則指在這個可調度作業集中屬於該Pool的作業數量不能超過它的上限,各個User、Pool的限制都保存在PoolManager中,而PoolManager是通過加載配置文件來得到這些限制信息的。而這個配置文件的路徑又可以通過JobTracker節點的配置文件來設置,對應的配置項爲:mapred.fairscheduler.allocation.file,同時在這個配置文件中,還可以指定一個Poll至少可分得集羣中多少Map/Reduce計算資源。關於如何配置User、Pool的限制及Pool的計算資源,感興趣的同學可以參考Hadoop的官網。另外,對於如何指定一個作業屬於哪一個Pool(請注意,這裏的Pool不同於作業所屬的隊列,但可以通過配置讓Pool等價於作業隊列),可以通過作業的配置文件來執行,對應的配置項名則又是由JobTracker節點的配置文件中的mapred.fairscheduler.poolnameproperty項所決定。3.可調度作業集及其狀態的更新
可調度作業集及其狀態的更新主要由兩個事件來觸發,一是用戶新提交了一個作業並添加到調度器中;二是作業調度更新線程(UpdateThread)的定時(500ms)操作。這個操作過程主要包含以下幾個步驟:
1). 基於FIFO的策略從所有已初始化未完成的作業中選取一批作業作爲新的可調度作業集,User和Pool的限制條件即是選擇結束的條件;
2). 更新每一個作業的running以及非running的Map/Reduce任務數量,但對於非可調度的作業,其非running的Map/Reduce任務數量都爲0;
3). 更新每一個可調度作業的全局權重(這個計算方法在稍後會詳細談到);
4). 基於Pool的計算資源和該Pool中可調度作業的全局權重來計算作業應該分配的計算資源(作業最小資源量);
5). 基於集羣的計算資源和所有可調度作業的全局權重來計算作業應該分配的計算資源(作業公平份額量);
不過在用戶新提交的一個作業添加到調度器的處理過程中除了上述操作之外,還有2個額外操作,一是刪除作業集中以完成的作業,二是更新每一個作業的公平虧欠度,它的計算方法如下:
//應該分得計算資源(*FairShare)與實際得到的資源(running*s)之間的差乘以處於這種"不公平"狀態的時間timeDelta
private void updateDeficits(long timeDelta) {
for (JobInfo info: infos.values()) {
info.mapDeficit += (info.mapFairShare - info.runningMaps) * timeDelta;
info.reduceDeficit += (info.reduceFairShare - info.runningReduces) * timeDelta;
}
}
每一個作業池都配置有固定的計算資源(如果在配置文件中沒有明確配置,那麼該Poo的計算資源就默認爲0),因此就需要將該作業池的計算資源分配給該Pool中的當前可調度作業。而Pool中的每一個可調度作業到底要被分配多少個計算資源主要依賴於該作業的全局權重。這種基於Pool的計算資源和作業的全局權重來分配計算資源的方法如下:private void updateMinSlots() {
//Clear old minSlots
for (JobInfo info: infos.values()) {
info.minMaps = 0;
info.minReduces = 0;
}
// 爲每一個Pool中的可調度作業分配計算資源.
PoolManager poolMgr = getPoolManager();
for (Pool pool: poolMgr.getPools()) {
for (final TaskType type: TaskType.values()) {
Set<JobInProgress> jobs = new HashSet<JobInProgress>(pool.getJobs());
//該Pool的計算資源總量
int slotsLeft = poolMgr.getAllocation(pool.getName(), type);
//給該Pool中所有可調度的作業分配計算資源
while (slotsLeft > 0) {
// Figure out total weight of jobs that still need slots
double totalWeight = 0;
for (Iterator<JobInProgress> it = jobs.iterator(); it.hasNext();) {
JobInProgress job = it.next();
//選擇該Pool中還需要計算資源的可調度作業並統計它們的權重和
if (isRunnable(job) && runnableTasks(job, type) > minTasks(job, type)) {
totalWeight += weight(job, type);
} else {
it.remove();
}
}
if (totalWeight == 0) break;
//對於還需要計算資源的可調度作業,根據它們的權重比重把該Pool中剩餘的計算資源分配給他們
int oldSlots = slotsLeft;
for (JobInProgress job: jobs) {
double weight = weight(job, type);
int share = (int) Math.floor(oldSlots * weight / totalWeight);
slotsLeft = giveMinSlots(job, type, slotsLeft, share);
}
if (slotsLeft == oldSlots) {
// No tasks were assigned; do another pass using ceil, giving the
// extra slots to jobs in order of weight then deficit
List<JobInProgress> sortedJobs = new ArrayList<JobInProgress>(jobs);
Collections.sort(sortedJobs, new Comparator<JobInProgress>() {
public int compare(JobInProgress j1, JobInProgress j2) {
double dif = weight(j2, type) - weight(j1, type);
if (dif == 0) // Weights are equal, compare by deficit
dif = deficit(j2, type) - deficit(j1, type);
return (int) Math.signum(dif);
}
});
for (JobInProgress job: sortedJobs) {
double weight = weight(job, type);
int share = (int) Math.ceil(oldSlots * weight / totalWeight);
slotsLeft = giveMinSlots(job, type, slotsLeft, share);
}
if (slotsLeft > 0) {
LOG.warn("Had slotsLeft = " + slotsLeft + " after the final loop in updateMinSlots. This probably means some fair scheduler weights are being set to NaN or Infinity.");
}
break;
}
}//while
}//for
}//for
}
對於通過集羣的計算資源和所有可調度作業的全局權重來最終確定該作業的應該分配的公平份額的算法,筆者認爲這個算法可能存在某些問題而導致很難理解,所以本文不會詳細討論,有知道的博友可以@我。該算法的實現源碼如下:private void updateFairShares(ClusterStatus clusterStatus) {
// Clear old fairShares
for (JobInfo info: infos.values()) {
info.mapFairShare = 0;
info.reduceFairShare = 0;
}
// 計算每一個可調度作業應該分得的每類計算資源.
for (TaskType type: TaskType.values()) {
//選擇未完成並且可調度的作業
HashSet<JobInfo> jobsLeft = new HashSet<JobInfo>();
for (Entry<JobInProgress, JobInfo> entry: infos.entrySet()) {
JobInProgress job = entry.getKey();
JobInfo info = entry.getValue();
if (isRunnable(job) && runnableTasks(job, type) > 0) {
jobsLeft.add(info);
}
}
//獲取整個集羣的計算資源
double slotsLeft = getTotalSlots(type, clusterStatus);
//計算每一個未完成並且可調度作業應該分得的某一類計算資源
while (!jobsLeft.isEmpty()) {
double totalWeight = 0;
//統計所有未完成並可調度作業的權重和
for (JobInfo info: jobsLeft) {
double weight = (type == TaskType.MAP ? info.mapWeight : info.reduceWeight);
totalWeight += weight;
}
boolean recomputeSlots = false;
double oldSlots = slotsLeft; // Copy slotsLeft so we can modify it
for (Iterator<JobInfo> iter = jobsLeft.iterator(); iter.hasNext();) {
JobInfo info = iter.next();
double minSlots = (type == TaskType.MAP ? info.minMaps : info.minReduces);
double weight = (type == TaskType.MAP ? info.mapWeight : info.reduceWeight);
//基於公平性計算該作業應該分配的計算資源
double fairShare = weight / totalWeight * oldSlots;
//對於以Pool的計算爲準來更新作業的公平資源配額
if (minSlots > fairShare) {
if (type == TaskType.MAP) info.mapFairShare = minSlots;
else info.reduceFairShare = minSlots;
slotsLeft -= minSlots;
iter.remove();
recomputeSlots = true;
}
}//for
if (!recomputeSlots) {
// All minimums are met. Give each job its fair share of excess slots.
for (JobInfo info: jobsLeft) {
double weight = (type == TaskType.MAP ? info.mapWeight : info.reduceWeight);
double fairShare = weight / totalWeight * oldSlots;
if (type == TaskType.MAP)
info.mapFairShare = fairShare;
else
info.reduceFairShare = fairShare;
}
break;
}
}//while
}
}
3.作業的全局權重及資源量計算方法
1). 作業原始權重:
2). 作業全局權重:
3). 作業最小資源量:
4). 作業公平份額量(簡化計算方法):
4.作業的調度
當一個公平份額調度器FairScheduler給一個TaskTracker節點分配任務時,它只分配那些可調度作業的任務給當前的計算節點,同時這些可調度作業的優後順序是按照作業的公平虧損度從高到低排序的,也即是說,FairScheduler優先調度那些公平虧損度高的作業,具體的排序算法實現如下:
private class DeficitComparator implements Comparator<JobInProgress> {
private final TaskType taskType;
private DeficitComparator(TaskType taskType) {
this.taskType = taskType;
}
public int compare(JobInProgress j1, JobInProgress j2) {
JobInfo j1Info = infos.get(j1);
JobInfo j2Info = infos.get(j2);
long deficitDif;
boolean j1Needy, j2Needy;
if (taskType == TaskType.MAP) {
//檢查作業實際佔用的計算資源量是否小於它應該獲得的最小計算資源量
j1Needy = j1.runningMaps() < Math.floor(j1Info.minMaps);
j2Needy = j2.runningMaps() < Math.floor(j2Info.minMaps);
//比較兩個作業的公平虧欠度
deficitDif = j2Info.mapDeficit - j1Info.mapDeficit;
} else {
j1Needy = j1.runningReduces() < Math.floor(j1Info.minReduces);
j2Needy = j2.runningReduces() < Math.floor(j2Info.minReduces);
deficitDif = j2Info.reduceDeficit - j1Info.reduceDeficit;
}
if (j1Needy && !j2Needy)
return -1;
else if (j2Needy && !j1Needy)
return 1;
else // Both needy or both non-needy; compare by deficit
return (int) Math.signum(deficitDif);
}
}
FairScheduler在調度某一個具體的作業之前,還會先調用負載均衡器來判斷是否應該給當前的TaskTracker節點分配任務。另外,對於任何一個TaskTracker節點,FairScheduler最多隻爲它分配2個任務,而且最多隻有1個Map任務和1個Reduce任務,也就是給一個TaskTracker節點分配的任務只有4種組合情況:1). 一個任務也沒有;
2). 一個Map任務;
3). 一個Reduce任務;
4). 一個Map任務和一個Reduce任務;
FairScheduler總體的調度原則是,一是保證各個Pool及User先提交的作業先執行完,二是保證所有的作業享有與其權重對應的計算資源量。
何時使用各個調度器
這些調度算法各具針對性。如果正在運行一個大型Hadoop集羣,它具有多個客戶端和不同類型、不同優先級的作業,那麼容量調度器是最好選擇,它可以確保訪問,並能重用未使用的容量並調整隊列中作業的優先級。儘管不太複雜,但無論是小型還是大型集羣,如果由同一個組織使用,工作負載數量有限,那麼公平調度器也能運轉得很好。公平調度可以將容量不均勻地分配給池(作業的),但是它較爲簡單且可配置性較低。公平調度在存在多種作業的情況下非常有用,因爲它能爲小作業和大作業混合的情況提供更快的響應時間(支持更具交互性的使用模型)。(該段落轉載自網絡)