有必要了解的大資料知識(二) Hadoop

有夢想的老王發表於2021-03-17

前言

接上文,複習整理大資料相關知識點,這章節從MapReduce開始...

MapReduce介紹

MapReduce思想在生活中處處可見。或多或少都曾接觸過這種思想。MapReduce的思想核心是“分而治之”,適用於大量複雜的任務處理場景(大規模資料處理場景)。

Map負責“分”,即把複雜的任務分解為若干個“簡單的任務”來並行處理。可以進行拆分的前提是這些小任務可以平行計算,彼此間幾乎沒有依賴關係。
Reduce負責“合”,即對map階段的結果進行全域性彙總。
MapReduce執行在yarn叢集

  1. ResourceManager
  2. NodeManager

這兩個階段合起來正是MapReduce思想的體現。

MapReduce設計思想和架構

MapReduce是一個分散式運算程式的程式設計框架,核心功能是將使用者編寫的業務邏輯程式碼和自帶預設元件整合成一個完整的分散式運算程式,併發執行在Hadoop叢集上。

Hadoop MapReduce構思:
分而治之
對相互間不具有計算依賴關係的大資料,實現並行最自然的辦法就是採取分而治之的策略。平行計算的第一個重要問題是如何劃分計算任務或者計算資料以便對劃分的子任務或資料塊同時進行計算。不可分拆的計算任務或相互間有依賴關係的資料無法進行平行計算!
統一構架,隱藏系統層細節
如何提供統一的計算框架,如果沒有統一封裝底層細節,那麼程式設計師則需要考慮諸如資料儲存、劃分、分發、結果收集、錯誤恢復等諸多細節;為此,MapReduce設計並提供了統一的計算框架,為程式設計師隱藏了絕大多數系統
層面的處理細節。
MapReduce最大的亮點在於通過抽象模型和計算框架把需要做什麼(what need to do)與具體怎麼做(how to do)分開了,為程式設計師提供一個抽象和高層的程式設計介面和框架。程式設計師僅需要關心其應用層的具體計算問題,僅需編寫少量的處理應用本身計算問題的程式程式碼。如何具體完成這個平行計算任務所相關的諸多系統層細節被隱藏起來,交給計算框架去處理:從分佈程式碼的執行,到大到數千小到單個節點叢集的自動排程使用。
構建抽象模型:Map和Reduce
MapReduce借鑑了函式式語言中的思想,用Map和Reduce兩個函式提供了高層的並行程式設計抽象模型
Map: 對一組資料元素進行某種重複式的處理;
Reduce: 對Map的中間結果進行某種進一步的結果整理。
Map和Reduce為程式設計師提供了一個清晰的操作介面抽象描述。MapReduce
處理的資料型別是鍵值對。
MapReduce中定義瞭如下的Map和Reduce兩個抽象的程式設計介面,由使用者去程式設計實現:
Map: (k1; v1) → [(k2; v2)]
Reduce: (k2; [v2]) → [(k3; v3)]

MapReduce 框架結構
一個完整的mapreduce程式在分散式執行時有三類例項程式:

  1. MRAppMaster 負責整個程式的過程排程及狀態協調
  2. MapTask 負責map階段的整個資料處理流程
  3. ReduceTask 負責reduce階段的整個資料處理流程

MapReduce程式設計規範

MapReduce 的開發一共有八個步驟, 其中 Map 階段分為 2 個步驟,Shuffle 階段 4個步驟,Reduce 階段分為 2 個步驟

Map 階段 2 個步驟

  1. 設定 InputFormat 類, 將資料切分為 Key-Value(K1和V1) 對, 輸入到第二步
  2. 自定義 Map 邏輯, 將第一步的結果轉換成另外的 Key-Value(K2和V2) 對, 輸出結果

Shuffle 階段 4 個步驟

  1. 對輸出的 Key-Value 對進行分割槽
  2. 對不同分割槽的資料按照相同的 Key 排序
  3. (可選) 對分組過的資料初步規約, 降低資料的網路拷貝
  4. 對資料進行分組, 相同 Key 的 Value 放入一個集合中

Reduce 階段 2 個步驟

  1. 對多個 Map 任務的結果進行排序以及合併, 編寫 Reduce 函式實現自己的邏輯, 對輸入的 Key-Value 進行處理, 轉為新的 Key-Value(K3和V3)輸出
  2. 設定 OutputFormat 處理並儲存 Reduce 輸出的 Key-Value 資料

轉換為程式碼,例子如下

Map階段

public class WordCountMapper extends Mapper<Text,Text,Text, LongWritable> {

    /**
     * K1-----V1
     * A -----A
     * B -----B
     * C -----C
     *
     * K2-----V2
     * A -----1
     * B -----1
     * C -----1
     *
     * @param key
     * @param value
     * @param context
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void map(Text key, Text value, Context context) throws IOException, InterruptedException {
        context.write(key,new LongWritable(1));
    }
}

Reduce階段

public class WordCountReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
    @Override
    protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
        long count = 0L;
        for (LongWritable value : values) {
            count += value.get();
        }
        context.write(key, new LongWritable(count));
    }
}

shuffle階段,舉一個分割槽的例子:

public class WordCountPartitioner extends Partitioner<Text, LongWritable> {

    @Override
    public int getPartition(Text text, LongWritable longWritable, int i) {
        if (text.toString().length() > 5) {
            return 1;
        }
        return 0;
    }
}

主方法

public class JobMain extends Configured implements Tool {

    public static void main(String[] args) throws Exception {
        ToolRunner.run(new Configuration(),new JobMain(),args);
    }

    @Override
    public int run(String[] strings) throws Exception {
        Job job = Job.getInstance(super.getConf(), "wordcout");
        job.setJarByClass(JobMain.class);
        //輸入
        job.setInputFormatClass(TextInputFormat.class);
        TextInputFormat.addInputPath(job,new Path("/"));
        //map
        job.setMapperClass(WordCountMapper.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(LongWritable.class);

        //shuffle階段
        job.setPartitionerClass(WordCountPartitioner.class);
        job.setNumReduceTasks(2);

        //reduce階段
        job.setReducerClass(WordCountReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(LongWritable.class);

        //輸出
        job.setOutputFormatClass(TextOutputFormat.class);
        TextOutputFormat.setOutputPath(job,new Path("/"));
        return 0;
    }
}

MapTask執行機制

具體步驟:

  1. 讀取資料元件 InputFormat (預設 TextInputFormat) 會通過 getSplits 方法對輸入目錄中檔案進行邏輯切片規劃得到 splits, 有多少個 split 就對應啟動多少個MapTask . splitblock 的對應關係預設是一對一
  2. 將輸入檔案切分為 splits 之後, 由 RecordReader 物件 (預設是LineRecordReader)進行讀取, 以 \n 作為分隔符, 讀取一行資料, 返回 <key,value> . Key 表示每行首字元偏移值, Value 表示這一行文字內容
  3. 讀取 split 返回 <key,value> , 進入使用者自己繼承的 Mapper 類中,執行使用者重寫的 map 函式, RecordReader 讀取一行這裡呼叫一次
  4. Mapper 邏輯結束之後, 將 Mapper 的每條結果通過 context.write 進行collect資料收集. 在 collect 中, 會先對其進行分割槽處理,預設使用 HashPartitioner。
  • MapReduce 提供 Partitioner 介面, 它的作用就是根據 Key 或 Value 及Reducer 的數量來決定當前的這對輸出資料最終應該交由哪個 Reduce task處理, 預設對 Key Hash 後再以 Reducer 數量取模. 預設的取模方式只是為了平均 Reducer 的處理能力, 如果使用者自己對 Partitioner 有需求, 可以訂製並設定到 Job 上。
  1. 接下來, 會將資料寫入記憶體, 記憶體中這片區域叫做環形緩衝區, 緩衝區的作用是批量收集Mapper 結果, 減少磁碟 IO 的影響. 我們的 Key/Value 對以及 Partition 的結果都會被寫入緩衝區. 當然, 寫入之前,Key 與 Value 值都會被序列化成位元組陣列。
  • 環形緩衝區其實是一個陣列, 陣列中存放著 Key, Value 的序列化資料和 Key,Value 的後設資料資訊, 包括 Partition, Key 的起始位置, Value 的起始位置以及Value 的長度. 環形結構是一個抽象概念
  • 緩衝區是有大小限制, 預設是 100MB. 當 Mapper 的輸出結果很多時, 就可能會撐爆記憶體, 所以需要在一定條件下將緩衝區中的資料臨時寫入磁碟, 然後重新利用這塊緩衝區. 這個從記憶體往磁碟寫資料的過程被稱為 Spill, 中文可譯為溢寫. 這個溢寫是由單獨執行緒來完成, 不影響往緩衝區寫 Mapper 結果的執行緒.溢寫執行緒啟動時不應該阻止 Mapper 的結果輸出, 所以整個緩衝區有個溢寫的比例 spill.percent . 這個比例預設是 0.8, 也就是當緩衝區的資料已經達到閾值 buffer size * spill percent = 100MB * 0.8 = 80MB , 溢寫執行緒啟動,鎖定這 80MB 的記憶體, 執行溢寫過程. Mapper 的輸出結果還可以往剩下的20MB 記憶體中寫, 互不影響
  1. 當溢寫執行緒啟動後, 需要對這 80MB 空間內的 Key 做排序 (Sort). 排序是 MapReduce模型預設的行為, 這裡的排序也是對序列化的位元組做的排序
    • 如果 Job 設定過 Combiner, 那麼現在就是使用 Combiner 的時候了. 將有相同 Key 的 Key/Value 對的 Value 加起來, 減少溢寫到磁碟的資料量.Combiner 會優化 MapReduce 的中間結果, 所以它在整個模型中會多次使用
    • 那哪些場景才能使用 Combiner 呢? 從這裡分析, Combiner 的輸出是Reducer 的輸入, Combiner 絕不能改變最終的計算結果. Combiner 只應該用於那種 Reduce 的輸入 Key/Value 與輸出 Key/Value 型別完全一致, 且不影響最終結果的場景. 比如累加, 最大值等. Combiner 的使用一定得慎重, 如果用好, 它對 Job 執行效率有幫助, 反之會影響 Reducer 的最終結果
  2. 合併溢寫檔案, 每次溢寫會在磁碟上生成一個臨時檔案 (寫之前判斷是否有 Combiner),如果 Mapper 的輸出結果真的很大, 有多次這樣的溢寫發生, 磁碟上相應的就會有多個臨時檔案存在. 當整個資料處理結束之後開始對磁碟中的臨時檔案進行 Merge 合併, 因為最終的檔案只有一個, 寫入磁碟, 並且為這個檔案提供了一個索引檔案, 以記錄每個reduce對應資料的偏移量

ReduceTask工作機制

Reduce 大致分為 copy、sort、reduce 三個階段,重點在前兩個階段。copy 階段包含一個 eventFetcher 來獲取已完成的 map 列表,由 Fetcher 執行緒去 copy 資料,在此過程中會啟動兩個 merge 執行緒,分別為 inMemoryMerger 和 onDiskMerger,分別將記憶體中的資料 merge 到磁碟和將磁碟中的資料進行 merge。待資料 copy 完成之後,copy 階段就完成了,開始進行 sort 階段,sort 階段主要是執行 finalMerge 操作,純粹的 sort 階段,完成之後就是 reduce 階段,呼叫使用者定義的 reduce 函式進行處理

詳細步驟:

  1. Copy階段 ,簡單地拉取資料。Reduce程式啟動一些資料copy執行緒(Fetcher),通過HTTP方式請求maptask獲取屬於自己的檔案。
  2. Merge階段 。這裡的merge如map端的merge動作,只是陣列中存放的是不同map端copy來的數值。Copy過來的資料會先放入記憶體緩衝區中,這裡的緩衝區大小要比map端的更為靈活。merge有三種形式:記憶體到記憶體;記憶體到磁碟;磁碟到磁碟。預設情況下第一種形式不啟用。當記憶體中的資料量到達一定閾值,就啟動記憶體到磁碟的merge。與map 端類似,這也是溢寫的過程,這個過程中如果你設定有Combiner,也是會啟用的,然後在磁碟中生成了眾多的溢寫檔案。第二種merge方式一直在執行,直到沒有map端的資料時才結束,然後啟動第三種磁碟到磁碟的merge方式生成最終的檔案。
  3. 合併排序 。把分散的資料合併成一個大的資料後,還會再對合並後的資料排序。
  4. 對排序後的鍵值對呼叫reduce方法 ,鍵相等的鍵值對呼叫一次reduce方法,每次呼叫會產生零個或者多個鍵值對,最後把這些輸出的鍵值對寫入到HDFS檔案中。

Shuffle具體流程

map 階段處理的資料如何傳遞給 reduce 階段,是 MapReduce 框架中最關鍵的一個流程,這個流程就叫 shuffle
shuffle: 洗牌、發牌 ——(核心機制:資料分割槽,排序,分組,規約,合併等過程)

  1. Collect階段 :將 MapTask 的結果輸出到預設大小為 100M 的環形緩衝區,儲存的是key/value,Partition 分割槽資訊等。
  2. Spill階段 :當記憶體中的資料量達到一定的閥值的時候,就會將資料寫入本地磁碟,在將資料寫入磁碟之前需要對資料進行一次排序的操作,如果配置了 combiner,還會將有相同分割槽號和 key 的資料進行排序。
  3. Merge階段 :把所有溢位的臨時檔案進行一次合併操作,以確保一個 MapTask 最終只產生一箇中間資料檔案。
  4. Copy階段 :ReduceTask 啟動 Fetcher 執行緒到已經完成 MapTask 的節點上覆制一份屬於自己的資料,這些資料預設會儲存在記憶體的緩衝區中,當記憶體的緩衝區達到一定的閥值的時候,就會將資料寫到磁碟之上。
  5. Merge階段 :在 ReduceTask 遠端複製資料的同時,會在後臺開啟兩個執行緒對記憶體到本地的資料檔案進行合併操作。
  6. Sort階段 :在對資料進行合併的同時,會進行排序操作,由於 MapTask 階段已經對資料進行了區域性的排序,ReduceTask 只需保證 Copy 的資料的最終整體有效性即可。Shuffle 中的緩衝區大小會影響到 mapreduce 程式的執行效率,原則上說,緩衝區越大,磁碟io的次數越少,執行速度就越快

緩衝區的大小可以通過引數調整, 引數:mapreduce.task.io.sort.mb 預設100M

相關文章