Hadoop-MapReduce
MapReduce是一個框架,我們可以使用它來編寫應用程序,以可靠的方式並行地處理大量商品硬件羣集上的大量數據。
什麼是MapReduce?
MapReduce是一種基於java的分佈式計算的處理技術和程序模型。 MapReduce算法包含兩個重要任務,即Map和Reduce。Map採用一組數據並將其轉換爲另一組數據,其中各個元素被分解爲元組(鍵/值對)。其次,reduce任務,它將map的輸出作爲輸入,並將這些數據元組合併成一組較小的元組。作爲MapReduce名稱的順序,reduce任務總是在map作業之後執行。
MapReduce的主要優點是易於在多個計算節點上擴展數據處理。在MapReduce模型下,數據處理原語稱爲映射器和縮減器。將數據處理應用程序分解爲映射器和簡化器有時並不重要。但是,一旦我們以MapReduce形式編寫應用程序,擴展應用程序以在集羣中運行數百,數千甚至數萬臺機器只是一種配置更改。這種簡單的可擴展性是吸引許多程序員使用MapReduce模型的原因。
算法
-
通常MapReduce範例是基於將計算機發送到數據所在的位置!
-
MapReduce程序在三個階段執行,即map階段,shuffle階段和reduce階段。
-
Map 階段 :
映射或映射器的作業是處理輸入數據。一般來說,輸入數據是以文件或目錄的形式存儲在Hadoop文件系統(HDFS)中。輸入文件逐行傳遞到映射器函數。映射器處理數據並創建幾個小塊的數據。
-
Reduce 階段 :
這個階段是Shuffle階段和Reduce階段的組合。 Reducer的工作是處理來自映射器的數據。處理後,它產生一組新的輸出,將存儲在HDFS中。
-
-
在MapReduce作業期間,Hadoop將Map和Reduce任務發送到集羣中的相應服務器。
-
該框架管理數據傳遞的所有細節,例如發出任務,驗證任務完成,以及在節點之間複製集羣周圍的數據。
-
大多數計算髮生在節點上,本地磁盤上的數據減少了網絡流量。
-
完成給定任務後,集羣收集並減少數據以形成適當的結果,並將其發送回Hadoop服務器。
術語
- PayLoad:應用程序實現Map和Reduce功能,並形成作業的核心。
- Mapper: 映射器將輸入鍵/值對映射到一組中間鍵/值對。
- NamedNode :管理Hadoop分佈式文件系統(HDFS)的節點。
- DataNode :在任何處理髮生之前提前呈現數據的節點。
- MasterNode : JobTracker運行並接受來自客戶端的作業請求的節點。
- SlaveNode : Map和Reduce程序運行的節點。
- JobTracker :計劃作業並跟蹤將作業分配給任務跟蹤器。
- Task Tracker : 跟蹤任務並向JobTracker報告狀態。
- Job :程序是跨數據集的Mapper和Reducer的執行。
- Task :在一個數據片段上執行Mapper或Reducer。
- Task Attempt :嘗試在SlaveNode上執行任務的特定實例。
MapReduce - 讀取數據
通過InputFormat決定讀取的數據的類型,然後拆分成一個個InputSplit,每個InputSplit對應一個Map處理,RecordReader讀取InputSplit的內容給Map
- InputFormat
決定讀取數據的格式,可以是文件或數據庫等
功能
- 驗證作業輸入的正確性,如格式等
- 將輸入文件切割成邏輯分片(InputSplit),一個InputSplit將會被分配給一個獨立的Map任務
- 提供RecordReader實現,讀取InputSplit中的"K-V對"供Mapper使用
方法
List getSplits(): 獲取由輸入文件計算出輸入分片(InputSplit),解決數據或文件分割成片問題
RecordReader createRecordReader(): 創建RecordReader,從InputSplit中讀取數據,解決讀取分片中數據問題
InputSplit
代表一個個邏輯分片,並沒有真正存儲數據,只是提供了一個如何將數據分片的方法
Split內有Location信息,利於數據局部化
一個InputSplit給一個單獨的Map處理
public abstract class InputSplit {
/**
* 獲取Split的大小,支持根據size對InputSplit排序.
*/
public abstract long getLength() throws IOException, InterruptedException;
/**
* 獲取存儲該分片的數據所在的節點位置.
*/
public abstract String[] getLocations() throws IOException, InterruptedException;
}
RecordReader
將InputSplit拆分成一個個<key,value>對給Map處理,也是實際的文件讀取分隔對象</key,value>
問題
大量小文件如何處理
CombineFileInputFormat可以將若干個Split打包成一個,目的是避免過多的Map任務(因爲Split的數目決定了Map的數目,大量的Mapper Task創建銷燬開銷將是巨大的)
怎麼計算split的
通常一個split就是一個block(FileInputFormat僅僅拆分比block大的文件),這樣做的好處是使得Map可以在存儲有當前數據的節點上運行本地的任務,而不需要通過網絡進行跨節點的任務調度
通過mapred.min.split.size, mapred.max.split.size, block.size來控制拆分的大小
如果mapred.min.split.size大於block size,則會將兩個block合成到一個split,這樣有部分block數據需要通過網絡讀取
如果mapred.max.split.size小於block size,則會將一個block拆成多個split,增加了Map任務數(Map對split進行計算並且上報結果,關閉當前計算打開新的split均需要耗費資源)
先獲取文件在HDFS上的路徑和Block信息,然後根據splitSize對文件進行切分( splitSize = computeSplitSize(blockSize, minSize, maxSize) ),默認splitSize 就等於blockSize的默認值(64m)
public List<InputSplit> getSplits(JobContext job) throws IOException {
// 首先計算分片的最大和最小值。這兩個值將會用來計算分片的大小
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
// generate splits
List<InputSplit> splits = new ArrayList<InputSplit>();
List<FileStatus> files = listStatus(job);
for (FileStatus file: files) {
Path path = file.getPath();
long length = file.getLen();
if (length != 0) {
FileSystem fs = path.getFileSystem(job.getConfiguration());
// 獲取該文件所有的block信息列表[hostname, offset, length]
BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length);
// 判斷文件是否可分割,通常是可分割的,但如果文件是壓縮的,將不可分割
if (isSplitable(job, path)) {
long blockSize = file.getBlockSize();
// 計算分片大小
// 即 Math.max(minSize, Math.min(maxSize, blockSize));
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
long bytesRemaining = length;
// 循環分片。
// 當剩餘數據與分片大小比值大於Split_Slop時,繼續分片, 小於等於時,停止分片
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, splitSize, blkLocations[blkIndex].getHosts()));
bytesRemaining -= splitSize;
}
// 處理餘下的數據
if (bytesRemaining != 0) {
splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining, blkLocations[blkLocations.length-1].getHosts()));
}
} else {
// 不可split,整塊返回
splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts()));
}
} else {
// 對於長度爲0的文件,創建空Hosts列表,返回
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
// 設置輸入文件數量
job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
LOG.debug("Total # of splits: " + splits.size());
return splits;
}
分片間的數據如何處理
split是根據文件大小分割的,而一般處理是根據分隔符進行分割的,這樣勢必存在一條記錄橫跨兩個split
解決辦法是隻要不是第一個split,都會遠程讀取一條記錄。不是第一個split的都忽略到第一條記錄
public class LineRecordReader extends RecordReader<LongWritable, Text> {
private CompressionCodecFactory compressionCodecs = null;
private long start;
private long pos;
private long end;
private LineReader in;
private int maxLineLength;
private LongWritable key = null;
private Text value = null;
// initialize函數即對LineRecordReader的一個初始化
// 主要是計算分片的始末位置,打開輸入流以供讀取K-V對,處理分片經過壓縮的情況等
public void initialize(InputSplit genericSplit, TaskAttemptContext context) throws IOException {
FileSplit split = (FileSplit) genericSplit;
Configuration job = context.getConfiguration();
this.maxLineLength = job.getInt("mapred.linerecordreader.maxlength", Integer.MAX_VALUE);
start = split.getStart();
end = start + split.getLength();
final Path file = split.getPath();
compressionCodecs = new CompressionCodecFactory(job);
final CompressionCodec codec = compressionCodecs.getCodec(file);
// 打開文件,並定位到分片讀取的起始位置
FileSystem fs = file.getFileSystem(job);
FSDataInputStream fileIn = fs.open(split.getPath());
boolean skipFirstLine = false;
if (codec != null) {
// 文件是壓縮文件的話,直接打開文件
in = new LineReader(codec.createInputStream(fileIn), job);
end = Long.MAX_VALUE;
} else {
// 只要不是第一個split,則忽略本split的第一行數據
if (start != 0) {
skipFirstLine = true;
--start;
// 定位到偏移位置,下次讀取就會從偏移位置開始
fileIn.seek(start);
}
in = new LineReader(fileIn, job);
}
if (skipFirstLine) {
// 忽略第一行數據,重新定位start
start += in.readLine(new Text(), 0, (int) Math.min((long) Integer.MAX_VALUE, end - start));
}
this.pos = start;
}
public boolean nextKeyValue() throws IOException {
if (key == null) {
key = new LongWritable();
}
key.set(pos);// key即爲偏移量
if (value == null) {
value = new Text();
}
int newSize = 0;
while (pos < end) {
newSize = in.readLine(value, maxLineLength, Math.max((int) Math.min(Integer.MAX_VALUE, end - pos), maxLineLength));
// 讀取的數據長度爲0,則說明已讀完
if (newSize == 0) {
break;
}
pos += newSize;
// 讀取的數據長度小於最大行長度,也說明已讀取完畢
if (newSize < maxLineLength) {
break;
}
// 執行到此處,說明該行數據沒讀完,繼續讀入
}
if (newSize == 0) {
key = null;
value = null;
return false;
} else {
return true;
}
}
}
MapReduce - Mapper
主要是讀取InputSplit的每一個Key,Value對並進行處理
public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
/**
* 預處理,僅在map task啓動時運行一次
*/
protected void setup(Context context) throws IOException, InterruptedException {
}
/**
* 對於InputSplit中的每一對<key, value>都會運行一次
*/
@SuppressWarnings("unchecked")
protected void map(KEYIN key, VALUEIN value, Context context) throws IOException, InterruptedException {
context.write((KEYOUT) key, (VALUEOUT) value);
}
/**
* 掃尾工作,比如關閉流等
*/
protected void cleanup(Context context) throws IOException, InterruptedException {
}
/**
* map task的驅動器
*/
public void run(Context context) throws IOException, InterruptedException {
setup(context);
while (context.nextKeyValue()) {
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
cleanup(context);
}
}
public class MapContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> extends TaskInputOutputContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
private RecordReader<KEYIN, VALUEIN> reader;
private InputSplit split;
/**
* Get the input split for this map.
*/
public InputSplit getInputSplit() {
return split;
}
@Override
public KEYIN getCurrentKey() throws IOException, InterruptedException {
return reader.getCurrentKey();
}
@Override
public VALUEIN getCurrentValue() throws IOException, InterruptedException {
return reader.getCurrentValue();
}
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
return reader.nextKeyValue();
}
}
MapReduce - Shuffle
對Map的結果進行排序並傳輸到Reduce進行處理 Map的結果並不是直接存放到硬盤,而是利用緩存做一些預排序處理 Map會調用Combiner,壓縮,按key進行分區、排序等,儘量減少結果的大小 每個Map完成後都會通知Task,然後Reduce就可以進行處理。
Map端
當Map程序開始產生結果的時候,並不是直接寫到文件的,而是利用緩存做一些排序方面的預處理操作
每個Map任務都有一個循環內存緩衝區(默認100MB),當緩存的內容達到80%時,後臺線程開始將內容寫到文件,此時Map任務可以繼續輸出結果,但如果緩衝區滿了,Map任務則需要等待
寫文件使用round-robin方式。在寫入文件之前,先將數據按照Reduce進行分區。對於每一個分區,都會在內存中根據key進行排序,如果配置了Combiner,則排序後執行Combiner(Combine之後可以減少寫入文件和傳輸的數據)
每次結果達到緩衝區的閥值時,都會創建一個文件,在Map結束時,可能會產生大量的文件。在Map完成前,會將這些文件進行合併和排序。如果文件的數量超過3個,則合併後會再次運行Combiner(1、2個文件就沒有必要了)
如果配置了壓縮,則最終寫入的文件會先進行壓縮,這樣可以減少寫入和傳輸的數據
一旦Map完成,則通知任務管理器,此時Reduce就可以開始複製結果數據
Reduce端
Map的結果文件都存放到運行Map任務的機器的本地硬盤中
如果Map的結果很少,則直接放到內存,否則寫入文件中
同時後臺線程將這些文件進行合併和排序到一個更大的文件中(如果文件是壓縮的,則需要先解壓)
當所有的Map結果都被複制和合並後,就會調用Reduce方法
Reduce結果會寫入到HDFS中
Hadoop - IO
輸入文件從HDFS進行讀取.
輸出文件會存入本地磁盤.
Reducer和Mapper間的網絡I/O,從Mapper節點得到Reducer的檢索文件.
使用Reducer實例從本地磁盤迴讀數據.
Reducer輸出- 回傳到HDFS.
串行化
傳輸、存儲都需要
Writable接口
Avro框架:IDL,版本支持,跨語言,JSON-linke
壓縮
能夠減少磁盤的佔用空間和網絡傳輸的量
Compressed Size, Speed, Splittable
gzip, bzip2, LZO, LZ4, Snappy
要比較各種壓縮算法的壓縮比和性能
重點:壓縮和拆分一般是衝突的(壓縮後的文件的block是不能很好地拆分獨立運行,很多時候某個文件的拆分點是被拆分到兩個壓縮文件中,這時Map任務就無法處理,所以對於這些壓縮,Hadoop往往是直接使用一個Map任務處理整個文件的分析)
Map的輸出結果也可以進行壓縮,這樣可以減少Map結果到Reduce的傳輸的數據量,加快傳輸速率
完整性
磁盤和網絡很容易出錯,保證數據傳輸的完整性一般是通過CRC32這種校驗法
每次寫數據到磁盤前都驗證一下,同時保存校驗碼
每次讀取數據時,也驗證校驗碼,避免磁盤問題
同時每個datanode都會定時檢查每一個block的完整性
當發現某個block數據有問題時,也不是立刻報錯,而是先去Namenode找一塊該數據的完整備份進行恢復,不能恢復才報錯