Mapreduce計算框架

Mapreduce計算框架

介紹

​ MapReduce是一個計算框架,它的原理是Google的MR並行計算思想,它用於離線處理海量數據,將海量數據的計算任務分發到集羣的多臺機器上,通過並行計算之後再進行合併結果,因此,它就是基於海量數據處理而生的,同時由於它計算的規格化,這些等待處理的數據必須按照一致的格式存儲,而基於MR框架的應用在編寫過程中就利用這個規格去切割、過濾、計算。目前,MapReduce有兩個大版本。

計算原理

​ 上面講了MapReduce的基礎介紹之後,接下來講一下MapReduce的計算過程,如圖:

在這裏插入圖片描述

​ 計算流程可以分爲如下幾個步驟:

  • Input(從本地文件系統、HDFS等文件系統上讀取數據)
  • Splitting(切割數據)
    • 如果原本的數據是在分佈式文件系統上的,就按照數據就近原則,切割成多個Block,其實也就是按行切割。
    • 如果原本數據不是在分佈式文件系統上,將採用部分切割,加載到內存的原則,同樣採用按行切割。
  • Mapping(Mapping其實就是對數據塊進行映射操作,根據鍵進行映射,最後輸出一個key-value)
  • Shuffling(混洗,這是一個介於map與reduce之間的階段,很重要,後面會詳細講)
  • Reduce(聚合操作,主要是對一個基於<K,List>結構的數據進行聚合操作)
  • Merge(把計算結果進行合併)

Shuffle混洗的詳解

混洗的核心機制:就是將 MapTask 輸出的處理結果數據,按照 Partitioner 組件制定的規則分發 給 ReduceTask,並在分發的過程中,對數據按 key 進行了分區和排序,以下就是混洗機制的局部實現:

在這裏插入圖片描述

Spill

Spill過程包括輸出、排序、溢寫、合併等步驟,如圖所示:

在這裏插入圖片描述

分析:

  • Collect就是一個收集操作:每個Map任務不斷地以對的形式把數據輸出到在內存中構造的一個環形數據結構中。使用環形數據結構是爲了更有效地使用內存空間,在內存中放置儘可能多的數據。

    • 這個kvbuffer的環型數據結構,這個結構中存放了數據、索引等信息
    • 那麼,爲何要使用環型數據結構呢?其實很簡單,環型結構的特點在於它的周長是一定的,即分配的內存是一定的,在使用的過程中,類似於一個光盤,上面的指針相當於是磁針,將數據輸出到磁盤之後,新的收集數據又可以繼續在原先的基礎上填充。(不需要銷燬數組後重新申請內存)
  • Sort是一個排序操作:對Collect中的數據進行排序,先把Kvbuffer中的數據按照partition值和key兩個關鍵字升序排序,移動的只是索引數據(不會移動真實的數據,只移動數據對應的索引)排序結果是Kvmeta中數據按照partition爲單位聚集在一起,同一partition內的按照key有序

  • Spill簡稱溢寫:由於kvbuffer是一個環型結構,它總有裝滿的時候,這個時候就需要將對應的數據執行flush disk操作,沖刷到磁盤中進行保存,同時kvbuffer又有了內存空間提供給map任務繼續輸入。

    • Spill線程爲這次Spill過程創建一個磁盤文件:從所有的本地目錄中輪訓查找能存儲這麼大空間的目錄,找到之後在其中創建一個類似於“spill12.out”的文件。Spill線程根據排過序的Kvmeta挨個partition的把數據吐到這個文件中,一個partition對應的數據吐完之後順序地吐下個partition,直到把所有的partition遍歷完。一個partition在文件中對應的數據也叫段(segment)。

      所有的partition對應的數據都放在這個文件裏,雖然是順序存放的,但是怎麼直接知道某個partition在這個文件中存放的起始位置呢?強大的索引又出場了。有一個三元組記錄某個partition對應的數據在這個文件中的索引:起始位置、原始數據長度、壓縮之後的數據長度,一個partition對應一個三元組。然後把這些索引信息存放在內存中,如果內存中放不下了,後續的索引信息就需要寫到磁盤文件中了:從所有的本地目錄中輪訓查找能存儲這麼大空間的目錄,找到之後在其中創建一個類似於“spill12.out.index”的文件,文件中不光存儲了索引數據,還存儲了crc32的校驗數據。(spill12.out.index不一定在磁盤上創建,如果內存(默認1M空間)中能放得下就放在內存中,即使在磁盤上創建了,和spill12.out文件也不一定在同一個目錄下。)

      每一次Spill過程就會最少生成一個out文件,有時還會生成index文件,Spill的次數也烙印在文件名中。索引文件和數據文件的對應關係如下圖所示:

在這裏插入圖片描述

環型結構的運行機制

在這裏插入圖片描述

  • 取環形的一個起始點,同時填充逆時針方向的數據與順時針方向的數據,直到兩個磁針碰撞。
  • 磁針碰撞,即數據填滿(這裏的滿可以是環形結構的一個存儲比例),執行溢寫操作,釋放出來的空間,這個時候就如以上的圖4,取該空閒空間的中間點,又開始向兩端填充數據,重複以上過程,直到map執行完畢。

MapReduce的瓶頸

Map任務總要把輸出的數據寫到磁盤上,即使輸出數據量很小在內存中全部能裝得下,在最後也會把數據刷到磁盤上。 這是MapReduce計算框架的一個瓶頸,導致它不適合處理實時業務,也是Spark框架相對MR框架的優勢。

Merge

Map任務如果輸出數據量很大,可能會進行好幾次Spill,out文件和Index文件會產生很多,分佈在不同的磁盤上。最後把這些文件進行合併的merge過程閃亮登場。

Merge過程怎麼知道產生的Spill文件都在哪了呢?從所有的本地目錄上掃描得到產生的Spill文件,然後把路徑存儲在一個數組裏。Merge過程又怎麼知道Spill的索引信息呢?沒錯,也是從所有的本地目錄上掃描得到Index文件,然後把索引信息存儲在一個列表裏。到這裏,又遇到了一個值得納悶的地方。在之前Spill過程中的時候爲什麼不直接把這些信息存儲在內存中呢,何必又多了這步掃描的操作?特別是Spill的索引數據,之前當內存超限之後就把數據寫到磁盤,現在又要從磁盤把這些數據讀出來,還是需要裝到更多的內存中。之所以多此一舉,是因爲這時kvbuffer這個內存大戶已經不再使用可以回收,有內存空間來裝這些數據了。(對於內存空間較大的土豪來說,用內存來省卻這兩個io步驟還是值得考慮的。(比如Spark))

然後爲merge過程創建一個叫file.out的文件和一個叫file.out.Index的文件用來存儲最終的輸出和索引。

一個partition一個partition的進行合併輸出。對於某個partition來說,從索引列表中查詢這個partition對應的所有索引信息,每個對應一個段插入到段列表中。也就是這個partition對應一個段列表,記錄所有的Spill文件中對應的這個partition那段數據的文件名、起始位置、長度等等。

然後對這個partition對應的所有的segment進行合併,目標是合併成一個segment。當這個partition對應很多個segment時,會分批地進行合併:先從segment列表中把第一批取出來,以key爲關鍵字放置成最小堆,然後從最小堆中每次取出最小的輸出到一個臨時文件中,這樣就把這一批段合併成一個臨時的段,把它加回到segment列表中;再從segment列表中把第二批取出來合併輸出到一個臨時segment,把其加入到列表中;這樣往復執行,直到剩下的段是一批,輸出到最終的文件中。

最終的索引數據仍然輸出到Index文件中。如下圖:

在這裏插入圖片描述

Copy

Reduce任務通過RPC向各個Map任務拖取它所需要的數據。每個節點都會啓動一個常駐的RPC server,其中一項服務就是響應Reduce拖取Map數據。當有MapOutput的RPC請求過來的時候,RPC server就讀取相應的Map輸出文件中對應這個Reduce部分的數據通過網絡流輸出給Reduce。

Reduce任務拖取某個Map對應的數據,如果在內存中能放得下這次數據的話就直接把數據寫到內存中。Reduce要向每個Map去拖取數據,在內存中每個Map對應一塊數據,當內存中存儲的Map數據佔用空間達到一定程度的時候,開始啓動內存中merge,把內存中的數據merge輸出到磁盤上一個文件中。

如果在內存中不能放得下這個Map的數據的話,直接把Map數據寫到磁盤上,在本地目錄創建一個文件,從RPC流中讀取數據然後寫到磁盤,使用的緩存區大小是64K。拖一個Map數據過來就會創建一個文件,當文件數量達到一定閾值時,開始啓動磁盤文件merge,把這些文件合併輸出到一個文件。

有些Map的數據較小是可以放在內存中的,有些Map的數據較大需要放在磁盤上,這樣最後Reduce任務拖過來的數據有些放在內存中了有些放在磁盤上,最後會對這些來一個全局合併。

以上就是Shuffle的全部過程了,可見,在MR框架中,Shuffle相比map、reduce的工作量要大得多。

工作機制

只有深入學習理解了MR的工作機制,才能夠用它來實現複雜的計算以及優化。Hadoop2.0引入了新機制(MR2)

它建立在一個名爲YARN的系統上,該系統主要是負責資源管理、任務調度、追蹤。

Mapreduce1.x

A.架構組成

  • JobTracker(作業追蹤者)

    • 將作業切分成任務:MapTask和ReduceTask
    • 將任務分派給TaskTracker執行
    • 作業的監控,接受心跳信息,如果沒有收到心跳信息,就切換到其他TaskTracker執行任務
  • TaskTracker(任務追蹤者)

    • 具體的任務派發、管理者
    • 與JobTasker之間維持着交流(心跳)
    • 是TaskRunner的管理者。
  • TaskRunner

    • 由TaskTracker創建出來的實例
    • TaskRunner會創建一個新的JVM進程來執行各個MAP、REDUCE任務,採用子進程的方式,避免這些任務的執行影響到TaskTracker本身。
  • 四種任務(在MAP階段有,在REDUCE階段也有)

    • map任務
      • 具體執行內容就是我們實現的map()方法
      • 執行映射操作
    • reduce任務
      • 具體執行內容就是我們實現的reduce()方法
      • 執行聚合操作
    • setup任務
      • 具體執行內容就是我們實現的setup()方法
      • 任務的初始化操作,會在該階段的xx操作執行之前先進行初始化,一般用於做一些閾值,過濾的設置
    • cleanup任務
      • 具體執行內容就是我們實現的cleanup()方法
      • 所有任務執行完之後就會執行清理任務。
           //通過這個可以時刻了解clean任務的執行進度
           float progress=job.cleanupProgress();
    

B.執行流程

整體的執行流程如下圖:

在這裏插入圖片描述

1、作業提交

  • 執行Job.submit,會創建jobsubmiter,即作業提交者。
    • 向JobTracker申請JobID,同時檢查各種輸入、輸出路徑、作業分片計算、需要資源計算,如果出錯,嚮應用程序拋出錯誤;如果正常,通知JobTracker準備執行任務。

2、任務初始化

  • 通過內部的任務列表,交給作業調度器(Scheduler)調度,並且對任務進行初始化,這個過程包括一些狀態、進度信息的初始化。
  • map任務初始化:調度器會去FS種獲取計算好的輸入分片,爲每個分片創建一個map任務(不可控)。
  • reduce任務初始化:調度器通過從jobTracker中拿到job,並且獲取job設置的reduceNumbers,從而初始化對應數量的任務。
       //控制reduceTask的任務數
      job.setNumReduceTasks(4);
  • setup任務:調度器創建setup任務,該任務在各階段的首要執行位置。
  • cleanup任務:創建資源回收任務。

3、任務分配

  • tasktracker通過心跳告知作業追蹤者是否空閒、是否存活,以至於讓jobtracker給它分配任務。
  • 採用”槽位分配“策略:
    • taskTracker中有一個map槽,一個reduce槽,分配時候會優先分配map任務,再去分配reduce任務

4、任務執行

  • 通過FS將作業的JAR文件複製到TaskTracker所在文件系統,實現JAR本地化,同時,tasktracker會將運行所需要的各個文件通過分佈式緩存複製到本地磁盤,創建本地執行目錄,同時創建T…Runner,由它來創建執行任務的子進程。

5、進度狀態更新

  • 層層向上彙報進度、狀態
    • runner->tasktracker->(心跳)jobtracker->(請求)[jobClient->(進程內部調用)app]
  • 整個MR作業可以分爲三個階段:MAP、SHUFFLE、REDUCE
    • 假如reduce執行了1/2,則整體執行了5/6.

6、完成作業

  • 當jobTracker收到最後一個任務的完成通知,就把JOB設置爲成功,這樣在客戶端查詢的過程中,就會知道任務已經完成。
  • jobTracker可以通過指定job.end.notification.url來執行任務通知請求。

Mapreduce2.x

​ Mapreduce2與上一代的差別並不是在mapreduce這個計算框架本身,而是在作業資源管理、調度方面有了巨大的變化,它使用了性能更好、職責更加分明、耦合程度更低的資源管理、調度系統,即Yarn(Yet Another resource Nefotiator)另一種資源協調者。一般在使用的時候通過配置參數mapreduce.framework.name=yarn配置。

A.架構組成

  • ResourceManager:RM
    • 負責集羣資源的統一管理和調度
    • 處理客戶端對作業的請求
    • 監控NM,隨時準備其他NM替換它
  • NodeManager:NM
    • 負責本身節點資源(容器)的管理和使用
    • 定期向RM彙報心跳信息
    • 接受處理RM的命令
    • 接受處理AM的命令
    • 單個節點的資源管理
  • ApplicationMaster:AM
    • 每個應用程序對應一個MRApp或者SparkApp,負責應用程序的管理
    • 向RM申請資源(core、meory),分配給內部task
    • 與NM通信,AM在某個NM中
  • Container
    • 封裝了CPU、Memory等資源的一個容器
    • 任務環境的抽象
  • Client
    • 提交作業
    • 查看作業進度
    • 殺死作業

B.執行流程

在這裏插入圖片描述

1、向YARN提交應用程序

2、RM分配第一個容器,並通知NM要在該容器中啓動AM

3、AM向RM註冊,使得RM可以監控狀態

4、AM以輪詢的方式,通過RPC向RM申請資源,並與NM交流,啓動對應的任務

5、NM爲各個任務設置環境之後,即創建容器,之後啓動任務

6、各個任務通過RPC與AM交流,而用戶可以通過RM實時查看狀態

7、執行完畢之後,AM向RM申請註銷並關閉自己

C.採用Yarn的優勢

Mapreduce1(存在問題)
JobTracker 是Map-reduce的集中處理點,存在單點故障
JobTracker 完成了太多的任務,造成了過多的資源消耗,當map-reduce job非常多的時候,會造成很大的內存開銷,潛在來說,也增加了JobTracker fail的風險,這也是業界普遍總結出老hadoop 的Map-Reduce只能支持4000節點主機的上限
TaskTracker 以map/reduce task的數目作爲資源的表示過於簡單,沒有考慮到cpu/內存的佔用情況,如果兩個大內存消耗的task被調度到了一塊,很容易出現OOM(內存溢出)
TaskTracker端,把資源強制劃分爲map task slot和reduce task slot,如果當系統中只有map task或者只有reduce task的時候,會造成資源的浪費,也就是前面提到過的集羣資源利用的問題。

基於上述的問題:在MR2中採用了YARN系統,基本思想就是將JobTracker兩個主要的功能分離成單獨的組件,這兩個功能是:資源管理、任務調度/監控。

  • ResourceManager有兩個主要組件:Scheduler和ApplicationsManager。
    • Scheduler即是資源的調度器,負責分配資源,管理資源,在YARN中,資源以容器的方式進行管理,這些資源包括CPU、內存、磁盤、網絡等。
    • ApplicationsManager即任務的調度、監控,負責與第一個NM協商啓動AppMaster.
  • NM負責每個結點的資源監控與向Scheduler彙報、同時負責執行應用程序。
  • AppMaster負責從Scheduler協商適當的資源容器,並與NodeManager跟蹤其狀態並監視進度。

總體來講,MR2使用YARN的優勢如下:

  • 避免單點故障,分離JobTracker任務
  • 採用Container的形式,對資源進行更好的分配、管理
  • 取消了map task slot與reduce task slot槽位概念,採用完全內存概念,避免空槽浪費

優化思路

TODO

demo

一個計算用戶在過去某個時間段內的充值金額總數的mr應用:

import com.qgailab.ha.hmhb.pretasks.MapreduceLoader;
import com.qgailab.ha.utils.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;
import java.text.ParseException;

//<p>處理的數據格式是:username,2019-05-01 00:00:00,money
/**
 * @author linxu
 * a class which can run mr to compute which user is all top up and top up than the threshold.
 * <p>
 * 1、用於離線計算
 * 2、可以調用mr框架來計算用戶的充值情況
 * </p>
 */
@Slf4j
public class TopUpRecordCollector {
    private final static String JOB_NAME = "TopUpRecordCollection_Job";

    /**
     * @author linxu
     * mapper.
     */
    public static class RecordMapper extends Mapper<Object, Text, Text, IntWritable> {

        /**
         * 界定符
         */
        private String delim;
        /**
         * 時間過濾器
         * tips:
         * 1、設置時間過濾器,可以在map階段過濾某些不需要計算的數據,提高計算速度。
         */
        private String timeFilter;
        /**
         * 使用用戶名充當key
         */
        private Text keyByUserName = new Text();
        /**
         * value爲用戶的充值金額
         */
        private IntWritable topUpAll = new IntWritable(0);
		
        @Override
        protected void setup(Context context) throws IOException, InterruptedException {
            timeFilter = context.getConfiguration().get("filter.time", "2019-05-01 00:00:00");
        }

        /**
         * 分佈式計算
         *
         * @param key     Object : 原文件位置偏移量。
         * @param value   Text : 原文件的一行字符數據。
         * @param context Context : 出參。
         * @throws IOException , InterruptedException
         */
        @Override
        protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
            String originalLine = value.toString();
            String date = originalLine.substring(3, 22);
            try {
                //獲取某個時間之後的數據
                if (DateUtil.compare(date, timeFilter) >= 0) {
                    String username = originalLine.substring(0, 2);
                    keyByUserName.set(username);
                    String money = originalLine.substring(23);
                    Double m = Double.parseDouble(money);
                    double f = m;
                    //處理可能存在邊界問題
                    topUpAll.set((int) f);
                    //構造k-v
                    context.write(keyByUserName, topUpAll);
                }
            } catch (ParseException e) {
                log.error("time parse error! it is :{}", date);
            }
        }
    }

    /**
     * @author linxu
     * reducer
     * reducing 階段的計算任務:
     * 統計每個user的充值金額總數
     */
    public static class ComputeReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
        // Statistical results;統計結果。
        private IntWritable result = new IntWritable();
        private int topUpThreshold;

        @Override
        protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
            int sum = 0;
            for (IntWritable val : values) {
                sum += val.get();
            }
            if (sum < topUpThreshold) {
                log.info("User:{},top Up is not enough.", key.toString());
                return;
            }
            result.set(sum);
            context.write(key, result);
        }

        @Override
        protected void setup(Context context) throws IOException, InterruptedException {
            topUpThreshold = context.getConfiguration().getInt("money.threshold", 480000);
        }

    }

    public static void main(String[] args) {
        try {
          //configuration的加載使用一個加載器實現。
            MapreduceLoader.init();
            Job job = Job.getInstance(MapreduceLoader.getConf(), TopUpRecordCollector.JOB_NAME);
            job.setJarByClass(TopUpRecordCollector.class);
            job.setMapperClass(RecordMapper.class);
            job.setReducerClass(ComputeReducer.class);
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
            //控制reduceTask的任務數
            //job.setNumReduceTasks(4);
            //通過這個可以時刻了解clean任務的執行進度
            //float progress=job.cleanupProgress();
            FileInputFormat.setInputPaths(job, new Path("hdfs://hacluster/tmp/qgr2/topup.txt"));
            //can verify the output path is exist or not.keep safe.
            FileOutputFormat.setOutputPath(job, new Path("hdfs://hacluster/tmp/waitdel"));
            job.waitForCompletion(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章