我們知道,MRv1主要由編程模型(MapReduce API)、資源管理與作業控制模塊(由JobTracker和TaskTracker組成)和數據處理引擎(由MapTask和ReduceTask組成)三部分組成。而YARN出現后,資源管理模塊則交由YARN實現,這樣為了讓MapReduce框架運行在YARN上,僅需實現一個ApplicationMaster組件完成作業控制模塊功能即可,其他部分,包括編程模型和數據處理引擎等,可直接采用MRv1原有的實現。下面將詳細介紹MapReduce On YARN(即MRv2)的基本架構、模塊組成以及各模塊的實現。
MapReduce On YARN與MRv1在編程模型和數據處理引擎方面的實現是一樣的,唯一的不同是運行時環境。不同于MRv1中由JobTracker和TaskTracker構成的運行時環境,MapReduce On YARN的運行時環境由YARN與ApplicationMaster構成,這種新穎的運行時環境使得MapReduce可以與其他計算框架運行在一個集群中,從而達到共享集群資源、提高資源利用率的目的。隨著YARN的成熟與完善,MRv1的獨立運行模式將被MapReduce On YARN取代。
MRAppMaster是MapReduce的ApplicationMaster實現,它使得MapReduce應用程序可以直接運行于YARN之上。在YARN中,MRAppMaster負責管理MapReduce作業的生命周期,包括作業管理、資源申請與再分配、Container啟動與釋放、作業恢復等。本節將介紹MRAppMaster基本構成。
如圖8-1所示,MRAppMaster主要由以下幾種組件/服務構成。
ContainerAllocator。與ResourceManager通信,為MapReduce作業申請資源。作業的每個任務資源需求可描述為5元組<Priority,hostname,capability,containers,relax_locality >,分別表示作業優先級、期望資源所在的host、資源量(當前支持內存和CPU兩種資源)、Container數目、是否松弛本地性。ContainerAllocator周期性通過RPC與ResourceManager通信,而ResourceManager則通過心跳應答的方式為之返回已經分配的Container列表、完成的Container列表等信息
ClientService。ClientService是一個接口,由MRClientService實現。MRClientService實現了MRClientProtocol協議,客戶端可通過該協議獲取作業的執行狀態(不必通過ResourceManager)和控制作業(比如殺死作業、改變作業優先級等)。
Job。Job表示一個MapReduce作業,與MRv1的JobInProgress功能一樣,負責監控作業的運行狀態。它維護了一個作業狀態機,以實現異步執行各種作業相關的操作。
Task。Task表示一個MapReduce作業中的某個任務,與MRv1中的TaskInProgress功能類似,負責監控一個任務的運行狀態。它維護了一個任務狀態機,以實現異步執行各種任務相關的操作
TaskAttempt。TaskAttempt表示一個任務運行實例,它的執行邏輯與MRv1中的MapTask和ReduceTask運行實例完全一致,實際上,它直接使用了MRv1的數據處理引擎,但經過了一些優化,正是由于它與MRv1的數據處理引擎一樣,它對外提供的編程接口也與MRv1完全一致,這意味著MRv1的應用程序可直接運行在YARN之上。
TaskCleaner。TaskCleaner負責清理失敗任務或者被殺死任務使用的目錄和產生的臨時結果(可統稱為垃圾數據),它維護了一個線程池和一個共享隊列,異步刪除任務產生的垃圾數據。
Speculator。Speculator完成推測執行功能。當同一個作業的某個任務運行速度明顯慢于其他任務時,Speculator會為該任務啟動一個備份任務,讓它與原任務同時處理同一份數據,誰先計算完成則將誰的結果作為最終結果,并將另一個任務殺掉。該機制可有效防止那些“拖后腿”任務拖慢整個作業的執行進度
ContainerLauncher。ContainerLauncher負責與NodeManager通信,以啟動一個Container。當ResourceManager為作業分配資源后,ContainerLauncher會將任務執行相關信息填充到Container中,包括任務運行所需資源、任務運行命令、任務運行環境、任務依賴的外部文件等,然后與對應的NodeManager通信,要求它啟動Container
TaskAttemptListener。TaskAttemptListener負責管理各個任務的心跳信息,如果一個任務一段時間內未匯報心跳,則認為它死掉了,會將其從系統中移除。同MRv1中的TaskTracker類似,它實現了TaskUmbilicalProtocol協議,任務會通過該協議匯報心跳,并詢問是否能夠提交最終結果
JobHistoryEventHandler。JobHistoryEventHandler負責對作業的各個事件記錄日志,比如作業創建、作業開始運行、一個任務開始運行等,這些日志會被寫到HDFS的某個目錄下,這對于作業恢復非常有用。當MRAppMaster出現故障時,YARN會將其重新調度到另外一個節點上。為了避免重新計算,MRAppMaster首先會從HDFS上讀取上次運行產生的日志,以恢復已經運行完成的任務,進而能夠只運行尚未運行完成的任務
事件與事件處理器
YARN使用了基于事件驅動的異步編程模型,它通過事件將各個組件聯系起來,并由一個中央異步調度器統一將各種事件分配給對應的事件處理器。在MRAppMaster中,每種組件是一個事件處理器,當MRAppMaster啟動時,它們會以服務的形式注冊到MRAppMaster的中央異步調度器上,并告訴調度器它們處理的事件類型。這樣當出現某一種事件時,MRAppMaster會查詢<事件,事件處理器>表,并將該事件分配給對應的事件處理器
MapReduce客戶端
MapReduce客戶端是MapReduce用戶與YARN(和MRAppMaster)進行通信的唯一途徑,通過該客戶端,用戶可以向YARN提交作業,獲取作業的運行狀態和控制作業(比如殺死作業、殺死任務等)。MapReduce客戶端涉及兩個RPC通信協議
- ApplicationClientProtocol。在YARN中,ResourceManager實現了ApplicationClient-Protocol協議,任何客戶端需使用該協議完成提交作業、殺死作業、改變作業優先級等操作
- MRClientProtocol。當作業的ApplicationMaster成功啟動后,它會啟動MRClient-Service服務,該服務實現了MRClientProtocol協議,從而允許客戶端直接通過該協議與ApplicationMaster通信以控制作業和查詢作業運行狀態,以減輕Resource-Manager負載。
在YARN中,應用程序(作業)的運行過程包括兩個步驟:啟動Application-Master和運行應用程序(作業)內部的各類任務,其中,ApplicationMaster是由ResourceManager直接與NodeManager通信而啟動的,在它啟動起來之前,客戶端只能與ResourceManager交互以查詢作業相關信息,在它啟動起來之前,客戶端只能與ResourceManager交互以查詢作業相關信息。一旦作業的ApplicationMaster成功啟動,客戶端可直接與它交互以查詢作業信息和控制作業。接下來,我們分別介紹這兩個通信協議
- ApplicationClientProtocol協議
MapReduce客戶端通過該協議與ResourceManager通信,以提交應用程序和查詢集群信息。ResourceManager用Application表示用戶提交的作業,并提供了以下接口供用戶使用:
public interface ApplicationClientProtocol {
public GetNewApplicationResponse getNewApplication( //獲取一個Application ID
GetNewApplicationRequest request)throws YarnRemoteException;
public SubmitApplicationResponse submitApplication( //提交一個Application
SubmitApplicationRequest request) throws YarnRemoteException;
public KillApplicationResponse forceKillApplication( //殺死一個Application
KillApplicationRequest request) throws YarnRemoteException;
public GetApplicationReportResponse getApplicationReport( //獲取Application運行報告
GetApplicationReportRequest request) throws YarnRemoteException;
public GetClusterMetricsResponse getClusterMetrics( //獲取集群所有Metric
GetClusterMetricsRequest request) throws YarnRemoteException;
public GetAllApplicationsResponse getAllApplications( //列出所有Application
GetAllApplicationsRequest request) throws YarnRemoteException;
public GetClusterNodesResponse getClusterNodes( //獲取集群中所有節點
GetClusterNodesRequest request) throws YarnRemoteException;
public GetQueueUserAclsInfoResponse getQueueUserAcls( //獲取用戶訪問控制權限
GetQueueUserAclsInfoRequest request) throws YarnRemoteException;
...
}
MRClientProtocol協議
MRAppMaster實現了MRClientProtocol協議為客戶端提供服務,該協議提供了作業和任務的查詢和控制接口,主要如下:
public interface MRClientProtocol {
...
public GetJobReportResponse getJobReport(GetJobReportRequest request) throws YarnRemoteException;//獲取作業運行報告
public GetTaskReportResponse getTaskReport(GetTaskReportRequest request) throws YarnRemoteException;//獲取所有任務運行報告
public GetTaskAttemptReportResponse getTaskAttemptReport(GetTaskAttemptReportRequest request) throws YarnRemoteException;//獲取所有任務實例的運行報告
public GetCountersResponse getCounters(GetCountersRequest request) throws YarnRemoteException;//獲取所有Counter
public GetTaskAttemptCompletionEventsResponse getTaskAttemptCompletionEvents(GetTaskAttemptCompletionEventsRequest request) throws YarnRemoteException;//獲取所有運行完成的任務
public GetTaskReportsResponse getTaskReports(GetTaskReportsRequest request) throws YarnRemoteException;//獲取所有任務運行報告
public GetDiagnosticsResponse getDiagnostics(GetDiagnosticsRequest request) throws YarnRemoteException;//獲取作業診斷信息
public KillJobResponse killJob(KillJobRequest request) throws YarnRemoteException;//殺死一個作業
public KillTaskResponse killTask(KillTaskRequest request) throws YarnRemoteException;//殺死一個任務
public KillTaskAttemptResponse killTaskAttempt(KillTaskAttemptRequest request) throws YarnRemoteException;//殺死一個任務實例
public FailTaskAttemptResponse failTaskAttempt(FailTaskAttemptRequest request) throws YarnRemoteException;//讓一個任務實例運行失敗
...
}
MRAppMaster工作流程
按照作業大小不同,MRAppMaster提供了三種作業運行模式:本地模式(通常用于作業調試,同MRv1一樣,不再贅述)、Uber模式和Non-Uber 模式。對于小作業,為了降低其延遲,可采用Uber模式,在該模式下,所有Map Task和Reduce Task在同一個Container(MRAppMaster所在Container)中順次執行;對于大作業,則采用Non-Uber 模式,在該模式下,MRAppMaster先為Map Task申請資源,當Map Task運行完成數目達到一定比例后再為Reduce Task申請資源
1)Uber運行模式
為了降低小作業延時,YARN專門對小作業運行方式進行了優化。對于小作業而言,MRAppMaster無須再為每個任務分別申請資源,而是讓其重用一個Container,并按照先Map Task后Reduce Task的運行方式串行執行每個任務。在YARN中,如果一個MapReduce作業同時滿足以下條件,則認為是小作業,可運行在Uber模式下:
- Map Task數目不超過mapreduce.job.ubertask.maxmaps(默認是9)。
- Reduce Task數目不超過mapreduce.job.ubertask.maxmaps(默認是1)。
- 輸入文件大小不超過mapreduce.job.ubertask.maxbytes(默認是一個Block大小)。
- Map Task和Reduce Task需要的資源量不超過MRAppMaster可使用的資源量。
2)Non-Uber運行模式
在大數據環境下,Uber運行模式通常只能覆蓋到一小部分作業,而對于其他大多數作業,仍將運行在Non-Uber模式下。在Non-Uber模式下,MRAppMaster將一個作業的Map Task和Reduce Task分為四種狀態
- pending:剛啟動但尚未向ResourceManager發送資源請求
- scheduled:已經向ResourceManager發送資源請求但尚未分配到資源。
- assigned:已經分配到了資源且正在運行。
- completed:已經運行完成。
對于Map Task而言,它的生命周期為scheduled →assigned→completed;而對于Reduce Task而言,它的生命周期為pending →scheduled →assigned→completed。由于Reduce Task的執行依賴于Map Task的輸出結果,因此,為避免Reduce Task過早啟動造成資源利用率低下,MRAppMaster讓剛啟動的Reduce Task處于pending狀態,以便能夠根據Map Task運行情況決定是否對其進行調度。在YARN之上運行MapReduce作業需要解決兩個關鍵問題:如何確定Reduce Task啟動時機以及如何完成Shuffle功能。
由于YARN中不再有Map Slot和Reduce Slot的概念,且RedouceManager也不知道Map Task與Reduce Task之間存在依賴關系,因此,MRAppMaster自己需設計資源申請策略以防止因Reduce Task過早啟動造成資源利用率低下和Map Task因分配不到資源而“餓死”。MRAppMaster在MRv1原有策略(Map Task完成數目達到一定比例后才允許啟動Reduce Task)基礎上添加了更為嚴格的資源控制策略和搶占策略。總結起來,Reduce Task啟動時機由以下三個參數控制:
- mapreduce.job.reduce.slowstart.completedmaps:當Map Task完成的比例達到該值后才會為Reduce Task申請資源,默認是0.05。
- yarn.app.mapreduce.am.job.reduce.rampup.limit:在Map Task完成前,最多啟動的Reduce Task比例,默認為0.5。
- yarn.app.mapreduce.am.job.reduce.preemption.limit:當Map Task需要資源但暫時無法獲取資源(比如Reduce Task運行過程中,部分Map Task因結果丟失需重算)時,為了保證至少一個Map Task可以得到資源,最多可以搶占的Reduce Task比例,默認為0.5。
按照MapReduce的執行邏輯,Shuffle HTTP Server應該分布到各個節點上,以便能夠支持各個Reduce Task遠程復制數據。然而,由于Shuffle是MapReduce框架中特有的一個處理流程,從設計上講,不應該將它直接嵌到YARN的某個組件(比如NodeManager)中。
YARN采用了服務模型管理各個對象,且多個服務可以通過組合的方式交由一個核心服務進行統一管理。在YARN中,NodeManager作為一種組合服務模式,允許動態加載應用程序臨時需要的附屬服務,利用這一特性,YARN將Shuffle HTTP Server組裝成了一種服務,以便讓各個NodeManager啟動時加載它。
當用戶向YARN中提交一個MapReduce應用程序后,YARN將分兩個階段運行該應用程序:第一個階段是由ResourceManager啟動MRAppMaster;第二個階段是由MRAppMaster創建應用程序,為它申請資源,并監控它的整個運行過程,直到運行成功。如圖8-3所示,YARN的工作流程分為以下幾個步驟:
步驟1 用戶向YARN中提交應用程序,其中包括MRAppMaster程序、啟動MRApp-Master的命令、用戶程序等。
步驟2 ResourceManager為該應用程序分配第一個Container,并與對應的NodeManager通信,要求它在這個Container中啟動應用程序的MRAppMaster。
步驟3 MRAppMaster啟動后,首先向ResourceManager注冊,這樣用戶可以直接通過ResourceManager查看應用程序的運行狀態,之后,它將為內部任務申請資源,并監控它們的運行狀態,直到運行結束,即重復步驟4~7。
步驟4 MRAppMaster采用輪詢的方式通過RPC協議向ResourceManager申請和領取資源。
步驟5 一旦MRAppMaster申請到資源后,則與對應的NodeManager通信,要求它啟動任務。
步驟6 NodeManager為任務設置好運行環境(包括環境變量、JAR包、二進制程序等)后,將任務啟動命令寫到一個腳本中,并通過運行該腳本啟動任務
步驟7 各個任務通過RPC協議向MRAppMaster匯報自己的狀態和進度,以讓MRAppMaster隨時掌握各個任務的運行狀態,從而可以在任務失敗時重新啟動任務
在應用程序運行過程中,用戶可隨時通過RPC向MRAppMaster查詢應用程序的當前運行狀態。
步驟8 應用程序運行完成后,MRAppMaster向ResourceManager注銷并關閉自己。
MR作業生命周期及相關狀態機
MR作業生命周期
在正式介紹作業(Job)生命周期之前,先要了解MRAppMaster中作業表示方式。如圖8-4所示,與MRv1一樣,MRAppMaster根據InputFormat組件的具體實現(通常是根據數據量切分數據),將作業分解成若干個Map Task和Reduce Task,其中每個Map Task處理一片InputSplit數據,而每個Reduce Task則進一步處理Map Task產生的中間結果。每個Map/Reduce Task只是一個具體計算任務的描述,真正的任務計算工作則是由運行實例TaskAttempt完成的,每個Map/Reduce Task可能順次啟動多個運行實例,比如第一個運行實例失敗了,則另起一個實例重新計算,直到這一份數據處理完成或者達到嘗試次數上限;也可能同時啟動多個運行實例,讓它們競爭同時處理一片數據。在MRAppMaster中,上述Job、Task和TaskAttempt的生命周期均由一個有限狀態機描述,其中TaskAttempt是實際進行任務計算的組件,其他兩個只負責監控和管理。正是由于MRAppMaster中的TaskAttempt重用了MRv1中的代碼,MRv1中的MapReduce應用程序可直接運行在YARN上.
作業的創建入口在MRAppMaster類中,如下所示:
public class MRAppMaster extends CompositeService {
public void start() {
...
job = createJob(getConfig());//創建Job
JobEvent initJobEvent = new JobEvent(job.getID(), JobEventType.JOB_INIT);
jobEventDispatcher.handle(initJobEvent);//發送JOB_INI,創建MapTask、ReduceTask
startJobs();//啟動作業,這是后續一切動作的觸發之源
...
}
protected Job createJob(Configuration conf) {
Job newJob =
new JobImpl(jobId, appAttemptID, conf, dispatcher.getEventHandler(),
taskAttemptListener, jobTokenSecretManager, fsTokens, clock,
completedTasksFromPreviousRun, metrics, committer, newApiCommitter,
currentUser.getUserName(), appSubmitTime, amInfos, context);
((RunningAppContext) context).jobs.put(newJob.getID(), newJob);
dispatcher.register(JobFinishEvent.Type.class,
createJobFinishEventHandler());
return newJob;
}
}
之后,MapReduce作業將依次經歷作業/任務初始化和作業啟動兩個階段。
(1)作業/任務初始化
JobImpl首先接收到JOB_INIT事件,然后觸發調用函數InitTransition(),進而導致作業狀態從NEW變為INITED,InitTransition函數主要工作是創建Map Task和Reduce Task,代碼如下
public static class InitTransition
implements MultipleArcTransition<JobImpl, JobEvent, JobState> {
...
createMapTasks(job, inputLength, taskSplitMetaInfo);
createReduceTasks(job);
...
}
其中,createMapTasks函數實現如下:
private void createMapTasks(JobImpl job, long inputLength,
TaskSplitMetaInfo[] splits) {
for (int i=0; i < job.numMapTasks; ++i) {
TaskImpl task =
new MapTaskImpl(job.jobId, i,
job.eventHandler,
job.remoteJobConfFile,
job.conf, splits[i],
job.taskAttemptListener,
job.committer, job.jobToken, job.fsTokens,
job.clock, job.completedTasksFromPreviousRun,
job.applicationAttemptId.getAttemptId(),
job.metrics, job.appContext);
job.addTask(task);
}
}
(2)作業啟動
啟動作業的代碼如下
public class MRAppMaster extends CompositeService {
protected void startJobs() {
JobEvent startJobEvent = new JobEvent(job.getID(), JobEventType.JOB_START);
dispatcher.getEventHandler().handle(startJobEvent);
}
}
JobImpl接收到JOB_START事件后,將執行函數StartTransition(),進而觸發Map Task和Reduce Task的調度執行,同時使得作業狀態從INITED變為RUNNING,具體如下:
public static class StartTransition
implements SingleArcTransition<JobImpl, JobEvent> {
public void transition(JobImpl job, JobEvent event) {
job.scheduleTasks(job.mapTasks);
job.scheduleTasks(job.reduceTasks);
}
}
之后,每個Map Task和Reduce Task負責各自的狀態變化,ContainerAllocator模塊會首先為Map Task申請資源,然后是Reduce Task,一旦一個Task獲取到了資源,就會創建一個運行實例Task Attempt。如果該實例運行成功,則Task運行成功,否則,Task還會啟動下一個運行實例Task Attempt,直到一個Task Attempt運行成功或者達到嘗試次數上限。當所有Task運行成功后,Job運行成功。一個運行成功的作業/任務所經歷的狀態變化(不包含失敗或者被殺死情況)
Task狀態機
Task狀態機維護了一個任務的生命周期,即從創建到運行結束整個過程。一個任務可能存在多次運行嘗試,每次運行嘗試被稱為一個“運行實例”,Task狀態機則負責管理這些運行實例。Task狀態機由TaskImpl類實現,其主要包括7種狀態和9種導致狀態發生變化的事件
(1)Task狀態
Task狀態包括
NEW:任務初始狀態。當一個任務被創建時,狀態被置為NEW
SCHEDULED:任務開始被調度時所處的狀態。該狀態意味著,MRAppMaster開始為任務(向ResourceManager)申請資源,但任務尚未獲取到資源。
RUNNING:任務的一個實例開始運行。該狀態意味著,MRAppMaster已經為該任務申請到資源(Container),并與對應的NodeManager通信成功啟動了Container。需要注意的,在某一時刻,一個任務可能有多個運行實例,且可能存在運行失敗的運行實例
SUCCEEDED:任務的一個實例運行成功。該狀態意味著,該任務成功運行并完成。需要注意的,只要任務的一個實例運行成功,則意味著該任務運行成功。
KILLED:任務被殺死所處的狀態。當一個任務的所有運行實例被殺死后,才認為該任務被殺死。
FAILED:任務運行失敗所處的狀態。每個任務的運行實例數目有一定上限,一旦超過該上限,才認為該任務運行失敗,其中,Map Task運行實例數目上限由參數mapreduce.map.maxattempts指定,默認值為4,Reduce Task運行實例數目上限由參數mapreduce.reduce.maxattempts指定,默認值為4。需要注意的是,一個任務運行失敗并不一定會導致整個作業運行失敗,這同樣取決于作業的錯誤容忍率(默認是0)
TaskAttempt狀態機
在YARN中,任務實例是運行在Container中的,因此,Container狀態變化往往伴隨任務實例的狀態變化,比如任務實例運行完成后,會清理Container占用的空間,而Container空間的清理實際上就是任務實例空間的清理。目前每個任務存在兩種類型的實例:Avataar.VIRGIN和Avataar.SPECULATIVE,分別表示原始任務和備份任務(通過推測執行機制啟動的)。
(1)TaskAttempt狀態
TaskAttmpt狀態包括:
- NEW:任務實例初始狀態。當一個任務實例被創建時,狀態被置為NEW。
- UNASSIGNED:等待分配資源所處的狀態。當一個任務實例被創建后,它會發出一個資源申請請求,等待MRAppMaster為它申請和分配資源
- ASSIGNED:任務實例獲取到一個Container。
- RUNNING:任務實例成功啟動。MRAppMaster將資源分配給某個任務后,會與對應的NodeManager通信,以啟動Container。只要Container啟動成功,則任務實例啟動成功
- COMMIT_PENDING:任務實例等待提交最終結果。任務實例運行完成后,需向MRAppMaster請求提交最終結果,一旦提交成功后,該任務的其他實例將被殺死
- SUCCEEDED:成功清理完成Container空間。任務實例運行完成后,需清理它使用的Container占用的空間,只有該空間清理完成后,才認為任務實例運行完成
- FAILED:任務實例運行失敗后所處的狀態
- KILLED:任務實例被殺死后所處的狀態。
資源申請與再分配
ContainerAllocator是MRAppMaster中負責資源申請和分配的模塊。用戶提交的作業被分解成Map Task和Reduce Task后,這些Task所需的資源統一由ContainerAllocator模塊負責從ResourceManager中申請,而一旦ContainerAllocator得到資源后,需采用一定的策略進一步分配給作業的各個任務。
在YARN中,作業的資源需求可描述為5元組:<priority,hostname,capability,containers,relax_locality >,分別表示作業優先級、期望資源所在的host、資源量(當前支持內存與CPU兩種資源)、Container數目、是否松弛本地性,比如
<10, "node1", "memory:1G,CPU:1", 3,true>//優先級是一個正整數,優先級值越小,優先級越高
<10, "node2", "memory:2G,CPU:1", 1, false>//1個必須來自node2上大小為2GB內存、1個CPU的Container(不能來自node2所在的機架或者其他節點)
<2, "*", "memory:1G,CPU:1", 20, false> //*表示這樣的資源可來自任意一個節點,即不考慮數據本地性
ContainerAllocator周期性通過心跳與ResourceManager通信,以獲取已分配的Container列表、完成的Container列表、最近更新的節點列表等信息,而ContainerAllocator根據這些信息完成相應的操作。
1.資源申請過程分析
當用戶提交作業之后,MRAppMaster會為之初始化,并創建一系列Map Task和Reduce Task,由于Reduce Task依賴于Map task之間結果,所以Reduce Task會延后調度。在ContainerAllocator中,當Map Task數目完成一定比例(由mapreduce.job.reduce.slowstart.completedmaps指定,默認是0.05,即5%)且Reduce Task可允許占用的資源(Reduce Task可占用資源比由yarn.app.mapreduce.am.job.reduce.rampup.limit指定)能夠折合成整數個任務時,才會調度Reduce Task。
考慮到Map Task和Reduce Task之間的依賴關系,因此,它們之間的狀態轉移也是不一樣的,對于Map Task而言,會依次轉移到以下幾個任務集合中:
scheduled->assigned->completed
對于Reduce Task而言,則按照以下流程進行:
pending->scheduled->assigned->completed
其中,pending表示等待ContainerAllocator發送資源請求的任務集合;scheduled表示已經將資源請求發送給RM,但還沒有收到分配的資源的任務集合;assigned是已經收到RM分配的資源的任務集合;completed表示已運行完成的任務集合。
Reduce Task之所以會多出一個pending狀態,主要是為了根據Map Task情況調整Reduce Task狀態(在pending和scheduled中相互轉移)。進一步說,這主要是為了防止Map Task餓死,因為在YARN中不再有Map Slot和Reduce Slot的概念(這兩個概念從一定程度上減少了作業餓死的可能性),只有內存、CPU等真實的資源,需要由ApplicationMaster控制資源申請的順序,以防止可能產生的作業餓死
此外,ContainerAllocator將所有任務劃分成三類,分別是Failed Map Task、Map Task和Reduce Task,并分別賦予它們優先級5、20和10,也就是說,當三種任務同時有資源需求時,會優先分配給Failed Map Task,然后是Reduce Task,最后是Map Task。
總結起來,ContainerAllocator工作流程如下:
步驟1 將Map Task的資源需求發送給RM。
步驟2 如果達到了Reduce Task調度條件,則開始為Reduce Task申請資源。
步驟3 如果為某個Task申請到了資源,則取消其他重復資源的申請。由于在HDFS中,任何一份數據通常有三備份,而對于一個任務而言,考慮到rack和any級別的本地性,它可能會對應7個資源請求,分別是:
<20, "node1", "memory:1G", 1, true>
<20, "node2", "memory:1G", 1, true>
<20, "node3", "memory:1G", 1, true>
<20, "rack1", "memory:1G", 1, true>
<20, "rack2", "memory:1G", 1, true>
<20, "rack3", "memory:1G", 1, true>
<20, "*", "memory:1G", 1, true>
一旦該任務獲取了以上任意一種資源,都會取消其他6個的資源申請。
在作業運行過程中,會出現資源重新申請和資源取消的行為,具體如下:
- 如果任務運行失敗,則會重新為該任務申請資源。
- 如果一個任務運行速度過慢,則會為其額外申請資源以啟動備份任務(如果啟動了推測執行功能)
- 如果一個節點失敗的任務數目過多,則會撤銷對該節點的所有資源的申請請求。
ContainerAllocator實際上是一接口,它只定義了三個事件:CONTAINER_REQ、CONTAINER_DEALLOCATE和CONTAINER_FAILED,分別表示請求Container、釋放Container和Container運行失敗。
ContainerAllocator的實現是RMContainerAllocator,它只接收和處理ContainerAllocator接口中定義的三種事件,它的運行是這三種事件驅動的。
RMContainerAllocator中最核心的框架是維護了一個心跳信息,在RMCommunicator類中的實現如下:
while (!stopped.get() && !Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(rmPollInterval);
try {
heartbeat();
} catch (YarnException e) {
LOG.error("Error communicating with RM: " + e.getMessage() , e);
return;
} catch (Exception e) {
LOG.error("ERROR IN CONTACTING RM. ", e);
continue;
}
lastHeartbeatTime = context.getClock().getTime();
executeHeartbeatCallbacks();
} catch (InterruptedException e) {
LOG.warn("Allocated thread interrupted. Returning.");
return;
}
}
其中,heartbeat()函數的定義(在RMContainerAllocator類中)如下:
protected synchronized void heartbeat() throws Exception {
scheduleStats.updateAndLogIfChanged("Before Scheduling: ");
List<Container> allocatedContainers = getResources();
if (allocatedContainers.size() > 0) {
scheduledRequests.assign(allocatedContainers);
}
…
}
其中,getResources()函數用于向RM發送心跳信息,并處理心跳應答。需要注意的是,大部分情況下,心跳信息中并不包含新的資源請求信息,即空的心跳信息,這種心跳有以下幾個作用:
- 周期性發送心跳,告訴RM自己還活著。
- 周期性詢問RM,以獲取新分配的資源和各個Container運行狀況
assign()函數作用是將收到的Container分配給某個任務,如果這個Container無法分配下去(比如內存空間不夠),則在下次心跳中通知RM釋放該Container,如果Container可以分下去,則會釋放對應任務的其他資源請求,同時會向TaskAttempt發送一個TA_ASSIGNED事件,以通知ContainerLauncher啟動Container。
除了新分配和已經運行完成的Container列表外,ContainerAllocator還會從RM中獲取節點更新列表,這個列表給出了最近發生變化的節點,比如新增節點、不可用節點等,當前ContainerAllocator僅處理了不可用節點,即一旦發現節點不可用,ContainerAllocator會將該節點上正運行的任務的狀態置為被殺死狀態,并重新為這些任務申請資源。
Container啟動與釋放
ContainerLauncher負責與各個NodeManager通信,以啟動或者釋放Container。在YARN中,運行Task所需的全部信息被封裝到Container中,包括所需資源、依賴的外部文件、JAR包、運行時環境變量、運行命令等。ContainerLauncher通過RPC協議ContainerManager與NodeManager通信,以控制Container的啟動與釋放,進而控制任務的執行(比如啟動任務、殺死任務等),ContainerManager協議定義了三個RPC接口,具體如下:
StartContainerResponse startContainer(StartContainerRequest request)
throws YarnRemoteException;//啟動一個container
StopContainerResponse stopContainer(StopContainerRequest request)
throws YarnRemoteException;//停止一個container
GetContainerStatusResponse getContainerStatus(
GetContainerStatusRequest request) throws YarnRemoteException;//獲取一個container運行情況
ContainerLauncher是一個Java接口,它定義了兩種事件
CONTAINER_REMOTE_LAUNCH。啟動一個Container。當ContainerAllocator為某個任務申請到資源后,會將運行該任務相關的所有信息封裝到Container中,并要求對應的節點啟動該Container
CONTAINER_REMOTE_CLEANUP。停止/殺死一個Container。存在多種可能觸發該事件的行為,常見的有:
- 推測執行時一個任務運行完成,需殺死另一個同輸入數據的任務
- 用戶發送一個殺死任務請求;
- 任意一個任務運行結束時,YARN會觸發一個殺死任務的命令,以釋放對應Container占用的資源。
尤其需要注意的是第三種情況,YARN作為資源管理系統,應確保任何一個任務運行結束后資源得到釋放,否則會造成資源泄露。而實現這一要求的可行方法是,任何一個任務結束后,不管它對應的資源是否得到釋放,YARN均會主動顯式檢查和回收資源(container)。
ContainerLauncher接口由ContainerLauncherImpl類實現,它是一個服務,接收和處理來自事件調度器發送過來的CONTAINER_REMOTE_LAUNCH和CONTAINER_REMOTE_CLEANUP兩種事件,它采用了線程池方式并行處理這兩種事件。
對于CONTAINER_REMOTE_LAUNCH事件,它會調用Container.launch()函數與對應的NodeManager通信,以啟動Container(可以同時啟動多個Container),代碼如下:
proxy = getCMProxy(containerMgrAddress, containerID);//構造一個RPC client
ContainerLaunchContext containerLaunchContext =
event.getContainer();
StartContainerRequest startRequest =
StartContainerRequest.newInstance(containerLaunchContext,
event.getContainerToken());
List<StartContainerRequest> list = new ArrayList<StartContainerRequest>();
list.add(startRequest);
StartContainersRequest requestList =
StartContainersRequest.newInstance(list);
//調用RPC函數,獲取返回值
StartContainersResponse response =
proxy.getContainerManagementProtocol().startContainers(requestList);
對于CONTAINER_REMOTE_CLEANUP事件,它會調用Container.kill()函數與對應的NodeManager通信,以殺死Container釋放資源(可以同時殺死多個Container),代碼如下:
proxy = getCMProxy(this.containerMgrAddress, this.containerID);
// kill the remote container if already launched
List<ContainerId> ids = new ArrayList<ContainerId>();
ids.add(this.containerID);
StopContainersRequest request = StopContainersRequest.newInstance(ids);
StopContainersResponse response =
proxy.getContainerManagementProtocol().stopContainers(request);
總之,ContainerLauncherImpl是一個非常簡單的服務,其最核心的代碼組織方式是“隊列+線程池”,以處理事件調度器發送過來的CONTAINER_REMOTE_LAUNCH和CONTAINER_REMOTE_CLEANUP兩種事件。
數據處理引擎
MRAppMaster仍采用了MRv1中的數據處理引擎,分別由數據處理引擎MapTask和ReduceTask完成Map任務和Reduce任務的處理,但相比于MRv1,MRAppMaster對這兩個引擎進行了優化,這些優化主要體現在Shuffle階段,具體如下。
1.Map端—用Netty代替Jetty
MRv1版本中,TaskTracker采用了Jetty服務器處理來自各個Reduce Task的數據讀取請求。由于Jetty采用了非常簡單的網絡模型,因此性能比較低。在Hadoop 2.0中,MRAppMaster改用Netty—另一種開源的客戶/服務器端編程框架,由于它內部采用了Java NIO技術,故其相比Jetty更加高效。Netty社區也比Jetty的更加活躍,且穩定性更好
2.Reduce端—批拷貝
MRv1版本中,在Shuffle過程中,Reduce Task會為每個數據分片建立一個專門的HTTP連接(One-connection-per-map),即使多個分片同時出現在一個TaskTracker上也是如此。為了提高數據復制效率,Hadoop 2.0嘗試采用批拷貝技術:不再為每個Map Task建立一個HTTP連接,而是為同一個TaskTracker上的多個Map Task建立一個HTTP連接,進而能夠一次讀取多個數據分片,具體如圖8-11所示