Hadoop -04 MapReduce介紹

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

決定讀取數據的格式,可以是文件或數據庫等

功能

  1. 驗證作業輸入的正確性,如格式等
  2. 將輸入文件切割成邏輯分片(InputSplit),一個InputSplit將會被分配給一個獨立的Map任務
  3. 提供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找一塊該數據的完整備份進行恢復,不能恢復才報錯

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章