目錄
MapReduce的核心思想
上面的圖片展示了四個階段:Input, Map, Reduce, Output,這四個階段就是MapReduce的核心過程
Input/Output只是任何系統都有的階段,現只看Map/Reduce
Map階段將輸入的文件按照業務規則解析爲一個個的k-v對,Reduce階段在Map計算輸出的k-v對上進行繼續計算,
比方說彙總這些k-v對,計算出有多少個這樣的k-v對。它所做的事就是把一件分爲多個階段來做,把一個大的問題分成很多的
小問題,可以將mapreduce看作是一種設計模式,一種解決解決某些場景問題的編程方法。比如在python中就提供了mapreuce
的編程模式
# map/filter 的使用
# 對傳入的參數,每一個應用傳入的運算
def fun1(x):
return x*2 + 5
list1 = [4, 8, 9, 1, 3]
list2 = map(fun1, list1)
# reduce的應用,reduce函數需要單獨導入
# 化簡函數
from functools import reduce
f = lambda x, y : x + y
total = reduce(f, list2)
如果說這個問題不是很大,那麼利用這個模式在一臺計算機上就可以解決這個問題了。
但如果這個如果很大,我們還是要使用這個mapreduce模式,那麼這時我們就需要使用很多臺計算機去解決這個問題了,
這就要使用到Hadoop MapReduce(分佈式的計算框架)
Hadoop MapReduce
Hadoop MapReduce是一個分佈式的計算框架,它的核心功能還是利用mapreduce的編程模式計算解決計算的問題。
但是因爲它是分佈式的,因而引入很多的輔助功能去解決因爲分佈式所帶來的問題,比如文件的存儲,網絡I/O通信等。
具體過程如下
InputFormat
public abstract class InputFormat<K, V> {
/**
* Logically split the set of input files for the job.
*
* <p>Each {@link InputSplit} is then assigned to an individual {@link Mapper}
* for processing.</p>
*
* <p><i>Note</i>: The split is a <i>logical</i> split of the inputs and the
* input files are not physically split into chunks. For e.g. a split could
* be <i><input-file-path, start, offset></i> tuple. The InputFormat
* also creates the {@link RecordReader} to read the {@link InputSplit}.
*
* @param context job configuration.
* @return an array of {@link InputSplit}s for the job.
*/
public abstract
List<InputSplit> getSplits(JobContext context
) throws IOException, InterruptedException;
/**
* Create a record reader for a given split. The framework will call
* {@link RecordReader#initialize(InputSplit, TaskAttemptContext)} before
* the split is used.
* @param split the split to be read
* @param context the information about the task
* @return a new record reader
* @throws IOException
* @throws InterruptedException
*/
public abstract
RecordReader<K,V> createRecordReader(InputSplit split,
TaskAttemptContext context
) throws IOException,
InterruptedException;
}
InputFormat 常見的接口實現類
TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定義InputFormat等
TextInputFormat
TextInputFormat 是默認的 InputFormat,每條記錄是一行輸入。鍵是LongWritable 類型,存儲該行在整個文件中的字節偏移量。 值是這行的內容,不包括任何行終止符(換行符合回車符),它被打包成一個 Text 對象
KeyValueTextInputFormat
每一行均爲一條記錄, 被分隔符(缺省是tab(\t))分割爲key(Text),value(Text)。
可以通過 mapreduce.input.keyvaluelinerecordreader.key.value,separator屬性來設定分隔符。 它的默認值是一個製表符
NLineInputFormat
通過 TextInputFormat 和 KeyValueTextInputFormat,每個 Mapper 收到的輸入行數不同。行數取決於輸入分片的大小和行的長度。 如果希望 Mapper 收到固定行數的輸入,需要將 NLineInputFormat 作爲 InputFormat。與 TextInputFormat 一樣, 鍵是文件中行的字節偏移量,值是行本身。N 是每個 Mapper 收到的輸入行數。N 設置爲1(默認值)時,每個 Mapper 正好收到一行輸入。 mapreduce.input.lineinputformat.linespermap 屬性實現 N 值的設定
自定義InputFormat
1-自定義一個類繼承FileInputFormat
2-改寫RecordReader,實現一次讀取一個完整文件封裝爲KV
3-在輸出時使用SequenceFileOutPutFormat輸出合併文件
Job提交過程
org.apache.hadoop.mapreduce.Job
waitCompletion(true)
submit() --> new JobSubmitter()
submitJobInternal()
//創建提交數據數據的臨時目錄
JobSubmissionFiles.getStagingDir(cluster, conf);
// 獲取jobid,並創建job路徑
JobID jobId = submitClient.getNewJobID();
job.setJobID(jobId);
Path submitJobDir = new Path(jobStagingArea, jobId.toString());
//
copyAndConfigureFiles(job, submitJobDir);
// 計算切片,並形成邏輯切分計劃
int maps = writeSplits(job, submitJobDir);
// FileInputFormat
List<InputSplit> splits = input.getSplits(job);
//計算切片大小
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
Math.max(minSize, Math.min(maxSize, blockSize));
//將job信息(切片計劃xml)寫入提交目錄
writeConf(conf, submitJobFile);
// 向yarn提交job
submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
FileInputFormat切片大小的參數配置
在FileInputFormat中,計算切片大小的邏輯:Math.max(minSize, Math.min(maxSize, blockSize));
切片主要由這幾個值來運算決定
mapreduce.input.fileinputformat.split.minsize=1 默認值爲1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默認值Long.MAXValue
因此,默認情況下,切片大小=blocksize。
maxsize(切片最大值):參數如果調得比blocksize小,則會讓切片變小,而且就等於配置的這個參數的值。
minsize(切片最小值):參數調的比blockSize大,則可以讓切片變得比blocksize還大。
FileInputFormat在切分文件時是按文件來切分的,因此不論這個文件的大小,該文件都至少有一個split,但文件小時極易產生
小文件問題這些就會產生大量很小的split,可以通過預處理將小文件合併爲大文件或使用CombineTextInputFormat
獲取切片信息API
// 根據文件類型獲取切片信息,可用於在map中判斷讀入的是哪個文件
FileSplit inputSplit = (FileSplit) context.getInputSplit();
// 獲取切片的文件名稱
String name = inputSplit.getPath().getName();
MapTask的個數
MapTask的個數由split的個數決定,因此要特別注意小文件問題
Partitioner
默認是HashPartitioner
public class HashPartitioner<K2, V2> implements Partitioner<K2, V2> {
public HashPartitioner() {
}
public void configure(JobConf job) {
}
public int getPartition(K2 key, V2 value, int numReduceTasks) {
return (key.hashCode() & 2147483647) % numReduceTasks;
}
}
自定義Partitioner
*/
public class YearPartitioner extends Partitioner<CompKey, NullWritable> {
@Override
/**
* @param numPartitions the total number of partitions.
*/
public int getPartition(CompKey compKey, NullWritable nullWritable, int numPartitions) {
int year = compKey.getYear();
if(year < 1993) {
return 0;
}
else if(year < 1996) {
return 1;
}
else {
return 2;
}
}
}
在job驅動中,設置自定義partitioner:
job.setPartitionerClass(CustomPartitioner.class);
自定義partition後,要根據自定義partitioner的邏輯設置相應數量的reduce task
job.setNumReduceTasks(3);
注意:
如果reduceTask的數量> getPartition的結果數,則會多產生幾個空的輸出文件part-r-000xx;
如果1<reduceTask的數量<getPartition的結果數,則有一部分分區數據無處安放,會Exception;
如果reduceTask的數量=1,則不管mapTask端輸出多少個分區文件,最終結果都交給這一個reduceTask,最終也就只會產生一個結果文件 part-r-00000;
示例
(1)job.setNumReduceTasks(1);會正常運行,只不過會產生一個輸出文件
(2)job.setNumReduceTasks(2);會報錯
(3)job.setNumReduceTasks(6);大於5,程序會正常運行,會產生空文件
排序
1.部分排序(分區內有序,這是mapreduce框架默認的)
key自動排序
2.全排序
自定義分區(在某個範圍內的數據就放到一個分區內)
對key的排序
3.二次排序(定義組合key, 定義新的Map端排序,定義新的Reduce端的 GroupingComparator)
對value的排序。
將value整合到key
Combiner
可以簡單的理解爲是map端的reduce操作,使用的也是Reducer,可以減少map端輸出文件的大小
class WordCountReducerWithCombiner1 extends Reducer<Text, IntWritable, Text, IntWritable> {
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int amount = 0;
for(IntWritable iw : values) {
amount += iw.get();
}
context.write(key, new IntWritable(amount));
}
}
Reduce
// reduceTask的個數可以在Driver中直接設置
job.setNumReduceTasks(4);
注意
(1)reducetask=0 ,表示沒有reduce階段,輸出文件個數和map個數一致。
(2)reducetask默認值就是1,所以輸出文件個數爲一個。
(3)如果數據分佈不均勻,就有可能在reduce階段產生數據傾斜
(4)reducetask數量並不是任意設置,還要考慮業務邏輯需求,有些情況下,需要計算全局彙總結果,就只能有1個reducetask。
(5)具體多少個reducetask,需要根據集羣性能而定。
(6)如果分區數不是1,但是reducetask爲1,是否執行分區過程。答案是:不執行分區過程。因爲在maptask的源碼中,執行分區的前提是先判斷reduceNum個數是否大於1。不大於1肯定不執行
Reduce的過程
Copy階段(shuffle 階段)
ReduceTask從各個MapTask上遠程拷貝一片數據,並針對某一片數據,如果其大小超過一定閾值,則寫到磁盤上,否則直接放到內存中。
Merge階段
在遠程拷貝數據的同時,ReduceTask啓動了兩個後臺線程對內存和磁盤上的文件進行合併,以防止內存使用過多或磁盤上文件過多。
Sort階段
按照MapReduce語義,用戶編寫reduce()函數輸入數據是按key進行聚集的一組數據。爲了將key相同的數據聚在一起,Hadoop採用了基於排序的策略。由於各個MapTask已經實現對自己的處理結果進行了局部排序,因此,ReduceTask只需對所有數據進行一次歸併排序即可。
Reduce階段
reduce()函數將計算結果寫到HDFS上。
OutputFormat
OutputFormat是MapReduce輸出的基類,所有實現MapReduce輸出都實現了 OutputFormat接口,常用實現類
TextOutputFormat
默認的輸出格式是TextOutputFormat,它把每條記錄寫爲文本行。它的鍵和值可以是任意類型,因爲TextOutputFormat調用toString()方法把它們轉換爲字符串。
SequenceFileOutputFormat
SequenceFileOutputFormat將它的輸出寫爲一個順序文件。如果輸出需要作爲後續 MapReduce任務的輸入,這便是一種好的輸出格式,因爲它的格式緊湊,很容易被壓縮。
自定義OutputFormat
爲了實現控制最終文件的輸出路徑,可以自定義OutputFormat。
要在一個mapreduce程序中根據數據的不同輸出兩類結果到不同目錄,這類靈活的輸出需求可以通過自定義outputformat來實現。
自定義OutputFormat步驟
(1)自定義一個類繼承FileOutputFormat。
(2)改寫recordwriter,具體改寫輸出數據的方法write()。
MapReduce開發
輸入數據接口InputFormat
默認使用的實現類是:TextInputFormat
TextInputFormat的功能邏輯是:一次讀一行文本,然後將該行的起始偏移量作爲key,行內容作爲value返回
CombineTextInputFormat可以把多個小文件合併成一個切片處理,提高處理效率。
用戶還可以自定義InputFormat。
邏輯處理接口:Mapper
用戶根據業務需求實現其中三個方法:map() setup() cleanup ()
Partitioner分區
有默認實現 HashPartitioner,邏輯是根據key的哈希值和numReduces來返回一個分區號;key.hashCode()&Integer.MAXVALUE % numReduces
如果業務上有特別的需求,可以自定義分區。
Comparable排序
當我們用自定義的對象作爲key來輸出時,就必須要實現WritableComparable接口,重寫其中的compareTo()方法。
部分排序:對最終輸出的沒一個文件進行內部排序。
全排序:對所有數據進行排序,通常只有一個Reduce。
二次排序:排序的條件有兩個。
Combiner合併
Combiner合併可以提高程序執行效率,減少io傳輸。但是使用時必須不能影響原有的業務處理結果。
reduce端分組:Groupingcomparator
reduceTask拿到輸入數據(一個partition的所有數據)後,首先需要對數據進行分組,其分組的默認原則是key相同,然後對每一組kv數據調用一次reduce()方法,並且將這一組kv中的第一個kv的key作爲參數傳給reduce的key,將這一組數據的value的迭代器傳給reduce()的values參數。
利用上述這個機制,我們可以實現一個高效的分組取最大值的邏輯。
自定義一個bean對象用來封裝我們的數據,然後改寫其compareTo方法產生倒序排序的效果。然後自定義一個Groupingcomparator,將bean對象的分組邏輯改成按照我們的業務分組id來分組(比如訂單號)。這樣,我們要取的最大值就是reduce()方法中傳進來key。
邏輯處理接口:Reducer
用戶根據業務需求實現其中三個方法: reduce() setup() cleanup ()
輸出數據接口:OutputFormat
默認實現類是TextOutputFormat,功能邏輯是:將每一個KV對向目標文本文件中輸出爲一行。
自定義OutputFormat。