標籤:hadoop 分散式運算 mapreduce 輸入輸出格式 shuffle
大資料時代之hadoop(一):hadoop安裝
大資料時代之hadoop(二):hadoop指令碼解析
大資料時代之hadoop(三):hadoop資料流(生命週期)
大資料時代之hadoop(四):hadoop Distributed File System(HDFS)
hadoop的核心分為兩塊,一是分布式儲存系統-hdfs,這個我已經在上一章節大致講了一下,另一個就是hadoop的計算架構-mapreduce。
mapreduce其實就是一個移動式的基於key-value形式的分散式運算架構。
其計算分為兩個階段,map階段和reduce階段,都是對資料的處理,由於其入門非常簡單,但是若想理解其中各個環節及實現細節還是有一定程度的困難,因此我計劃在本文中只是挑幾個mapreduce的核心來進行分析講解。
1、MapReduce驅動程式預設值
編寫mapreduce程式容易入手的其中一個原因就在於它提供了一些了的預設值,而這些預設值剛好就是供開發環境設定而設定的。雖然容易入手,但還是的理解mapreduce的精髓,因為它是mapreduce的引擎,只有理解了mapreduce的核心,當你在編寫mapreduce程式的時候,你所編寫的程式才是最終穩重的,想要的程式。廢話少說,見下面代碼:
public int run(String[] args) throws IOException { JobConf conf = new JobConf(); /** *預設的輸入格式,即mapper程式要處理的資料的格式,hadoop支援很多種輸入格式,下面會詳細講解, *但TextInputFormat是最常使用的(即普通文字檔,key為LongWritable-檔案中每行的開始位移量,value為Text-文本行)。 **/ conf.setInputFormat(org.apache.hadoop.mapred.TextInputFormat.class); /** *真正的map任務數量取決於輸入檔案的大小以及檔案塊的大小 **/ conf.setNumMapTasks(1); /** *預設的mapclass,如果我們不指定自己的mapper class時,就使用這個IdentityMapper 類 **/ conf.setMapperClass(org.apache.hadoop.mapred.lib.IdentityMapper.class); /** * map 任務是由MapRunner負責啟動並執行,MapRunner是MapRunnable的預設實現,它順序的為每一條記錄調用一次Mapper的map()方法,詳解代碼 --重點 */ conf.setMapRunnerClass(org.apache.hadoop.mapred.MapRunner.class); /** * map任務輸出結果的key 和value格式 */ conf.setMapOutputKeyClass(org.apache.hadoop.io.LongWritable.class); conf.setMapOutputValueClass(org.apache.hadoop.io.Text.class); /** * HashPartitioner 是預設的分區實現,它對map 任務運行後的資料進行分區,即把結果資料劃分成多個塊(每個分區對應一個reduce任務)。 * HashPartitioner是對每條 記錄的鍵進行雜湊操作以決定該記錄應該屬於哪個分區。 * */ conf.setPartitionerClass(org.apache.hadoop.mapred.lib.HashPartitioner.class); /** * 設定reduce任務個數 */ conf.setNumReduceTasks(1); /** *預設的reduce class,如果我們不指定自己的reduce class時,就使用這個IdentityReducer 類 **/ conf.setReducerClass(org.apache.hadoop.mapred.lib.IdentityReducer.class); /** * 任務最終輸出結果的key 和value格式 */ conf.setOutputKeyClass(org.apache.hadoop.io.LongWritable.class); conf.setOutputValueClass(org.apache.hadoop.io.Text.class); /** * 最終輸出到文字檔類型中 */ conf.setOutputFormat(org.apache.hadoop.mapred.TextOutputFormat.class);/*]*/ JobClient.runJob(conf); return 0; }
我要說的大部分都包含在了代碼的注釋裡面,除此之外,還有一點:由於java的泛型機制有很多限制:類型擦除導致運行過程中類型資訊並非一直可見,所以hadoop需要明確設定map,reduce輸入和結果類型。
上面比較重要的就是MapRunner這個類,它是map任務啟動並執行引擎,預設實現如下:
public class MapRunner<K1, V1, K2, V2> implements MapRunnable<K1, V1, K2, V2> { private Mapper<K1, V1, K2, V2> mapper; private boolean incrProcCount; @SuppressWarnings("unchecked") public void configure(JobConf job) { //通過反射方式取得map 執行個體 this.mapper = ReflectionUtils.newInstance(job.getMapperClass(), job); //increment processed counter only if skipping feature is enabled this.incrProcCount = SkipBadRecords.getMapperMaxSkipRecords(job)>0 && SkipBadRecords.getAutoIncrMapperProcCount(job); } public void run(RecordReader<K1, V1> input, OutputCollector<K2, V2> output, Reporter reporter) throws IOException { try { // allocate key & value instances that are re-used for all entries K1 key = input.createKey(); V1 value = input.createValue(); while (input.next(key, value)) { // map pair to output//迴圈調用map函數 mapper.map(key, value, output, reporter); if(incrProcCount) { reporter.incrCounter(SkipBadRecords.COUNTER_GROUP, SkipBadRecords.COUNTER_MAP_PROCESSED_RECORDS, 1); } } } finally { mapper.close(); } } protected Mapper<K1, V1, K2, V2> getMapper() { return mapper; }}
要相信,有些時候還是看源碼理解的更快!
2、shuffle
shuffle過程其實就是從map的輸出到reduce的輸入過程中所經曆的步驟,堪稱mapreduce的“心臟”,分為3個階段,map端分區、reduce端複製、reduce排序(合并)階段。
2.1、map端分區
由於在mapreduce計算中,有多個map任務和若干個reduce任何,而且各個任務都可能處於不同的機器裡面,所以如何從map任務的輸出到reduce的輸入是一個痛點。
map函數在產生輸出時,並不是簡單的寫到磁碟中,而是利用緩衝的形式寫入到記憶體,並出於效率進行預排序,過程如:
在寫磁碟之前,線程首先根據reduce的個數將輸出資料劃分成響應的分區(partiton)。在每個分區中,後台線程按鍵進行內排序,如果有個一combiner,它會在排序後的輸出上運行。
2.2、reduce端複製階段
由於map任務的輸出檔案寫到了本地磁碟上,並且劃分成reduce個數的分區(每一個reduce需要一個分區),由於map任務完成的時間可能不同,因此只要一個任務完成,reduce任務就開始複製其輸出,這就是reduce任務的複製階段。如所示。
2.3、reduce端排序(合并)階段
複製完所有map輸出後,reduce任務進入排序階段(sort phase),這個階段將合并map輸出,維持其順序排序,如所示。
3、輸入與輸出格式
隨著時間的增加,資料的增長也是指數級的增長,且資料的格式也越來越多,對大資料的處理也就越來越困難,為了適應能夠處理各種各樣的資料,hadoop提供了一系列的輸入和輸出格式控制,其目的很簡單,就是能夠解析各種輸入檔案,併產生需要的輸出格式資料。
但是不管處理哪種格式的資料,都要與mapreduce結合起來,才能最大化的發揮hadoop的有點。
這部分也是hadoop的核心啊!
3.1、輸入分區與記錄
在講HDFS的時候,說過,一個輸入分區就是由單個map任務處理的輸入塊,一個分區的大小最好與hdfs的塊大小相同。
每個分區被劃分成若干個記錄,每個記錄就是一個索引值對,map一個接一個的處理每條記錄。
在資料庫常見中,一個輸入分區可以對應一個表的若干行,而一條記錄對應一行(DBInputFormat)。
輸入分區在hadoop中表示為InputSplit介面,有InputFormat建立的。
InputFormat負責產生輸入分區並將他們分割成記錄,其只是一個介面,具體任務有具體實現去做的。
3.2、FileInputFormat
FileInputFormat是所有使用檔案作為其資料來源的InputFormat實現的基類,它提供了兩個功能:一個定義哪些檔案包含在作業的輸入中;一個為輸入檔案產生分區的實現。把分區割成基類的作業有其子類實現,FileInputFormat是個抽象類別。
FileInputFormat實現了把檔案分區的功能,但它是怎麼來實現了呢?需要先說三個參數:
屬性名稱 |
類型 |
預設值 |
描述 |
mapred.min.split.size |
Int |
1 |
一個檔案分區的最小位元組數 |
mapred.max.split.size |
Long |
Long.MAX_VALUE |
一個檔案分區的最大位元組數 |
dfs.block.size |
long |
64M |
HDFS中塊大小 |
分區的大小有一個公式計算(參考FileInputFomat類的computeSplitSize()方法)
max(minimumSize,min(maximumSize,blockSize))
預設情況下: minimumSize < blockSize < maximumSize
FileInputFormat只分割大檔案,即檔案大小超過塊大小的檔案。
FileInputFormat產生的InputSplit是一整個檔案(檔案太小,未被分區,整個檔案當成一個分區,供map任務處理)或該檔案的一部分(檔案大,被分區)。
3.3、常用的InputFormat實現
小檔案與CombineFileInputFormat
雖然hadoop適合處理大檔案,但在實際的情況中,大量的小檔案處理是少不了的,因此hadoop提供了一個CombineFileInputFormat,它針對小檔案而設計的,它把多個檔案打包到一個分區中一般每個mapper可以處理更多的資料。
TextInputFormat
hadoop預設的InputFormat,每個記錄的鍵是檔案中行的位移量,值為行內容。
KeyValueInputFormat
適合處理設定檔,檔案中行中為key value格式的,如key=value類型的檔案 ,key即為行中的key,value即為行中的value。
NLineInputFormat
也是為處理文字檔而開發的,它的特點是為每個map任務收到固定行數的輸入,其他與TextInputFormat相似。
SequenceFileInputFormat(二進位輸入)
hadoop的循序檔格式儲存格式儲存二進位的索引值對序列,由於循序檔裡面儲存的就是map結構的資料,所以剛好可以有SequenceFileInputFormat 來進行處理。
DBInputFormat
顧名思義,用於使用jdbc從關聯式資料庫中讀取資料。
多種輸入
MultipleInputs類可以用來處理多種輸入格式的資料,如輸入資料中包含文本類型和二進位類型的,這個時候就可以用 MultipleInputs來指定某個檔案有哪種輸入類型和哪個map函數來解析。
3.4、輸出格式
既然有輸入格式,就有輸出格式,與輸入格式對應。
預設的輸出格式是TextOutputFormat,它把記錄寫成文本行,索引值對可以是任意類型, 索引值對中間預設用定位字元分割。
3.5、hadoop特性
除了上面幾點之外,還有計數器、排序、串連等需要關注,詳細待後續吧。。。
大資料時代之hadoop(五):hadoop 分散式運算架構(MapReduce)