MapReduce流程分析(R1)

thamsyangsw發表於2014-02-27
轉載地址:http://blog.csdn.net/jackydai987/article/details/6227365

接觸Hadoop已經1年了,一直沒時間好好學習下。這幾天打算好好研究下Hadoop.本來是想打算改寫下TextInputFormat。看了原始碼後,反而更迷糊了。所以乾脆連MapReduce的整個流程寫下來。也當為這幾天的學習作個總結。

先來一個我們常寫的main函式。

Configuration conf = new Configuration();

              String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();

              if (otherArgs.length != 2){

                     System.exit(2);

              }

              Job job = new Job(conf, "wordcount");

             

              job.setJarByClass(mywordcount.class);

             

              job.setInputFormatClass(TextInputFormat.class);

             

              job.setOutputKeyClass(Text.class);

              job.setOutputValueClass(IntWritable.class);

             

              job.setMapperClass(wordcountMapper.class);

              job.setReducerClass(wordcountReduce.class);

              job.setCombinerClass(wordcountReduce.class);

             

              FileInputFormat.setInputPaths(job, new Path(otherArgs[0]));

              FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));

             

              job.waitForCompletion(true);

上述程式我就不分析。直接來分析下執行流程。

master節點的NameNode, SecondedNameNode,JobTrackerslaves節點的DataNode, TaskTracker都已經啟動後,JobTracker一直在等待JobClient透過RPC提交作業,TaskTracker一直透過RPC JobTracker傳送心跳heartbeat詢問有沒有任務可做。

而主程式中透過job.waitForCompletion(true)函式透過呼叫JobClient.runJob()函式將MapReduce作業交與JobTrack進行執行。

1JobClient.runJob()

runjob會根據使用者的設定(job.setInputFormatClass())來將需要輸入的資料劃分成小的資料集,同時返回劃分後split想的相應資訊(這裡的split路徑資訊我估計應該是hdfs裡的路徑和偏移,因為我們要處理的資料早應該上傳到了hdfs系統)。同時根據split設定MapTask的個數。獲取了上面資訊後,就將執行任務所需要的全部資料、資訊全部上傳至HDFS。上傳的內容主要包括三個包: job.jar, job.splitjob.xml

job.xml: 作業配置,例如Mapper, Combiner, Reducer的型別,輸入輸出格式的型別等。
job.jar: jar
,裡面包含了執行此任務需要的各種類,比如 Mapper,Reducer等實現。

job.split:
檔案分塊的相關資訊,比如有資料分多少個塊,塊的大小(預設64m)等。

進行完上述工作後,本地的工作就完成了,函式jobSubmitClient.submitJob(jobId)呼叫真正的JobTracker執行Task

2:JobTracker.submitJob()

JobTracker接收到新的job請求(即submitJob()函式被呼叫)後,會建立一個JobInProgress物件並透過它來管理和排程任務。JobInProgress在建立的時候會初始化一系列與任務有關的引數,呼叫FileSystem,把在JobClient端上傳的所有任務檔案下載到本地的檔案系統中的臨時目錄裡。這其中包括上傳的*.jar檔案包、記錄配置資訊的xml、記錄分割資訊的檔案。

JobTracker的建構函式中,會生成一個taskScheduler成員變數,來進行Job的排程, 預設為JobQueueTaskScheduler,也即按照FIFO的方式排程任務。而在offerService函式中也為JobTracker(也即taskSchedulertaskTrackerManager)註冊了兩個Listener

  • JobQueueJobInProgressListener jobQueueJobInProgressListener用於監控job的執行狀態
  • EagerTaskInitializationListener eagerTaskInitializationListener用於對Job進行初始化

EagerTaskInitializationListenerjobAdded函式就是向jobInitQueue中新增一個JobInProgress物件,於是自然觸發了此Job的初始化操作。由JobInProgressinitTasks函式完成:

3initTasks()

任務Task分兩種: MapTask reduceTask,它們的管理物件都是TaskInProgress

initTasks函式,會透過JobClientreadSplitFile()獲得已分解的輸入資料的RawSplit列表,然後透過這個列表建立相應的TaskInProgress(MapTask)同時還會讀取相應資料塊所在DataNode的主機名(透過FileSplitgetLocations()函式獲取)。建立完TaskInProgress後,就會呼叫createCache()方法為這些TaskInProgress物件產生一個未執行任務的Map快取nonRunningMapCache。當TaskTrackerJobTrack中傳送心跳,請求任務時,就會去直接去這個快取中取任務。

JobInProgress也會建立Reduce的監控物件- TaskInProgress,而這個的數量就是根據使用者在程式裡的設定,預設的是一個。同樣地,initTasks()也會透過createCache()方法產生nonRunningReduceCache成員。JobInProgress再接著進行清理工作。最後再記錄一下Job的執行日誌。

至此Job的初始化就全部完成。

4TaskTracker

TaskTracker從啟動後,就每隔一定的時間向JobTracker傳送一次心跳(預設10s,傳送的內容包括自己的當前狀態,當滿足一定狀態時就可以向JobTracker申請新的任務。如Map Task Reduce Task都還有執行的能力)透過transmitHeartBeat()傳送心跳後再接受JobTracker返回的HeartbeatResponse。然後呼叫HeartbeatResponsegetActions()函式獲得JobTracker傳過來的所有指令即一個TaskTrackerAction陣列。再遍歷這個陣列,就可以知道需要完成的事情。(這些事情可能是LaunchTaskAction 執行新任務、 KillTaskAction 結束一個任務)如果有分配好的任務將其加入佇列,呼叫addToTaskQueue,如果是map task則放入mapLancher(型別為TaskLauncher),如果是reduce task則放入reduceLancher(型別為TaskLauncher)

5 JobTrackerheartbeat()

 JobTracker是透過heartbeat()函式來接受TaskTracker的心跳,如果TaskTracker是請求任務的指令。Heartbeat()函式就會呼叫預設的任務排程器(JobQueueTaskScheduler)來分配任務。先計算MapReduce的剩餘工作量,再計算每個TaskTracker應有的工作量。如果TaskTracker上執行的map task數目小於平均的工作量,則向其分配map  task。分配完Map Task後再分配Reduce task.而這裡有一個函式findNewMapTask()就是從nonRunningMapCachenonRunningReduceCache中查詢出map  taskTaskInProgressReduce task. TaskInProgress再返回給TaskTracker

findNewMapTask()從近到遠一層一層地尋找,首先是同一節點,然後在尋找同一機櫃上的節點,接著尋找相同資料中心下的節點,直到找了maxLevel層結束。這樣的話,在JobTrackerTaskTracker派發任務的時候,可以迅速找到最近的TaskTracker,讓它執行任務。(透過尋找本任務split所在的DataNode,然後判斷髮送心跳的TaskTracker和本任務split所在的DataNode是不是同一主機。如果是則分配這個MapTask給傳送心跳的Tracker,如果不是者返回null不進行分配。)

再呼叫localizeJob()進行真正的初始化(TaskTracker上的Task)。而localizeJob又呼叫TaskLauncher

6TaskLauncher

TaskLauncher是一個執行緒就是從上面的佇列中取出TaskInProgress然後呼叫startNewTask(TaskInProgress tip)來啟動一個task

這裡又會再次將Task有關的資料包、資訊包從HDFS複製回本地檔案系統包括:job.splitjob.xml以及job.jar,當所有的資源複製回來後,就呼叫launchTaskForJob()開始執行Task.

launchTaskForJob函式又呼叫launchTask()

7launchTask()

 launchTask()函式首先透過createRunner()函式是建立MapTaskRunner來啟動子程式和建立ReduceTaskRunner來啟動子程式。TaskRunner負責將一個任務放到一個程式裡面來執行。它會呼叫run()函式來處理。run()函式會初始化一系列環境變數等。最後生成一個新程式並執行即runChild

8 Child程式

真正的map taskreduce task都是在Child程式中執行的。Child程式會執行Task.

9MapTask

如果是MapTaskMapTask.run()首先向TaskTracker彙報情況,再設定Mapper的輸出格式。接著讀取input split,按照其中的資訊,生成RecordReader來讀取資料。這其中會生成一個MapRunnable

,而MapRunnable要完成的任務就時透過RecordReadernext函式讀取迴圈從split中讀取交給map函式進行處理,然後使用OutputCollector收集每次處理對後得到的新的.

10:OutputCollector

OutputCollector的作用是收集每次呼叫map後得到的新的kv對,寧把他們spill到檔案或者放到記憶體,以做進一步的處理,比如排序,combine等。

MapOutputCollector 有兩個子類:MapOutputBufferDirectMapOutputCollector DirectMapOutputCollector用在不需要Reduce階段的時候。如果Mapper後續有reduce任務,系統會使用MapOutputBuffer做為輸出, MapOutputBuffer使用了一個緩衝區對map的處理結果進行快取,放在記憶體中. 在適當的時機,緩衝區中的資料會被spill到硬碟中。spillThread執行緒實現將緩衝區的資料寫入硬碟。

向硬碟中寫資料的時機:

1)當記憶體緩衝區不能容下一個太大的kv對時。spillSingleRecord方法。

2)記憶體緩衝區已滿時。SpillThread執行緒。

3Mapper的結果都已經collect了,需要對緩衝區做最後的清理。Flush方法。

11ReduceTask

ReduceTask .run()函式同樣先進行一系列的初始化工作。之後進入正式的工作,主要有這麼三個步驟:CopySortReduce

11.1:copy

copy就是從執行各個Map任務的伺服器那裡,蒐羅map的輸出檔案。

複製的任務的是由ReduceTask.ReduceCopier 類來負責。ReduceCopier先向父TaskTracker詢問此作業個Map任務的完成狀況,獲取到map伺服器的相關資訊後由執行緒MapOutputCopier做具體的複製工作。在複製過來的同時也會做一些歸併排序以減輕後面sort的負擔。

11.2 Sort

排序工作,就相當於上述排序工作的一個延續。它會在所有的檔案都複製完畢後進行。使用工具類Merger歸併所有的檔案。經過這一個流程,一個合併了所有所需Map任務輸出檔案的新檔案產生了。而那些從其他各個伺服器網羅過來的 Map任務輸出檔案,全部刪除了。

11.3Reduce

Reduce任務的最後一個階段。

輸入方面:他會準備根據自定義或預設的KeyClassValueClass構造出Reducer所需的鍵型別, 和值的迭代型別Iterator

輸出方面:它會準備一個OutputCollector收集輸出與MapTask不同,這個OutputCollector更為簡單,僅僅是開啟一個RecordWritercollect一次(排序完成的那個檔案)write一次(寫往HDFS)。

有了輸入,有了輸出,不斷迴圈呼叫自定義的Reducer,最終,Reduce階段完成。

 

寫本文之際參看了很多牛人的大作,在這裡一併感謝。

最後,我還有三個問題沒理解,請教高手解答一下。

Des1:JobtrackerTasktracker分配任務時是先判斷Tasktracker上執行的Task是否小於平均工作量,小於者向其分配Task。(假如我們這裡是MapTask。)然後呼叫函式obtainNewMapTask()中的findNewMapTask()來查詢nonRunningMapCache中的TaskInProgress
findNewMapTask
()函式會從近到遠一層一層地尋找。首先是同一節點,然後在尋找同一機櫃上的節點,接著尋找相同資料中心下的節點,直到找了maxLevel層結束。(這段話是我從網上看到的。沒理解這句話的意思。)
Q1:
我想請問下, findNewMapTask在這裡尋找的是什麼? TaskTracker)我的理解是:透過尋找本任務split所在的DataNode,然後判斷髮送心跳的TaskTracker和本任務split所在的DataNode是不是同一主機。如果是則分配這個MapTask給傳送心跳的Tracker,如果不是者返回null不進行分配。如果我理解錯了,請解釋下。謝謝了。

Des2:
當客戶端提交任務後,首先會透過使用者設定的InputFormat將檔案進行劃分。而hadoop預設的TextInputFormat.class.檢視原始碼知道。TextInputFormat是繼承的FileInputFormat.並且將isSplitable進行了關閉。所以預設的是不對檔案進行劃分。
Q2:
在執行一個MapReduce程式時,原始資料都會提前上傳到HDFS檔案系統,大於64M的檔案都會被劃分儲存到多個DataNode。。假如我有一個128M的檔案上傳到了HDFS,那天檔案應該被劃分成了2份。那麼TextInputFormat在處理輸入時不對檔案進行劃分,在TaskTracker處理檔案時,處理的是64M,還是128M呢?

(這個問題已經解決,應該是64M
Q3
:如果是64M是不是會開啟兩個TaskTracker來處理檔案,又因為TextInputFormat對檔案不進行劃分,所以每個
TaskTracker
上只會開啟一個Map Task來處理Map任務。最後兩個TaskTracker啟動兩個Reduce生成兩個檔案?

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/26613085/viewspace-1095679/,如需轉載,請註明出處,否則將追究法律責任。

相關文章