一張圖瞭解MapReduce全流程

先上圖

 

目錄

 

〇、Job提交流程

0.WordCount源碼:

1.waitForCompletion

2.submit

3.submitJobInternal

一、getSplits:輸入文件分片

二、RecordReader:讀取文件

三、Map


〇、Job提交流程

 

0.WordCount源碼:

public class WordCount {
	public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
		private final static IntWritable one = new IntWritable(1);
		private Text word = new Text();
		public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
			StringTokenizer itr = new StringTokenizer(value.toString());
			while (itr.hasMoreTokens()) {
				word.set(itr.nextToken());
				context.write(word, one);
			}
		}
	}
	public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
		private IntWritable result = new IntWritable();
		public void reduce(Text key, Iterable<IntWritable> values, Context context)
				throws IOException, InterruptedException {
			int sum = 0;
			for (IntWritable val : values) {
				sum += val.get();
			}
			result.set(sum);
			context.write(key, result);
		}
	}
	public static void main(String[] args) throws Exception {
		Configuration conf = new Configuration();
		String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
		if (otherArgs.length < 2) {
			System.err.println("Usage: wordcount <in> [<in>...] <out>");
			System.exit(2);
		}
		Job job = Job.getInstance(conf, "word count");
		job.setJarByClass(WordCount.class);
		job.setMapperClass(TokenizerMapper.class);
		job.setCombinerClass(IntSumReducer.class);
		job.setReducerClass(IntSumReducer.class);
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(IntWritable.class);
		for (int i = 0; i < otherArgs.length - 1; ++i) {
			FileInputFormat.addInputPath(job, new Path(otherArgs[i]));
		}
		FileOutputFormat.setOutputPath(job, new Path(otherArgs[otherArgs.length - 1]));
		System.exit(job.waitForCompletion(true) ? 0 : 1);
	}
}

1.waitForCompletion

我們在自己寫的MR程序中通過org.apache.hadoop.mapreduce.Job來創建Job,配置好之後通過waitForCompletion方法來提交Job並打印MR執行過程的log。waitForCompletion源碼及註釋如下:

    public boolean waitForCompletion(boolean verbose) throws IOException, InterruptedException,ClassNotFoundException {
    if (state == JobState.DEFINE) {
      submit(); //判斷狀態state爲DEFINE狀態,則可以提交Job後,執行submit()方法。
    }
    if (verbose) { //verbose表示是否打印Job運行信息
      monitorAndPrintJob(); //不斷的刷新獲取job運行的進度信息,並打印。
    } else {
      // 從配置裏取得輪訓的間隔時間,來分析當前job是否執行完畢
      int completionPollIntervalMillis = 
        Job.getCompletionPollInterval(cluster.getConf());
      while (!isComplete()) {
        try {
          Thread.sleep(completionPollIntervalMillis);
        } catch (InterruptedException ie) {
        }
      }
    }
    return isSuccessful();
  }

2.submit

其中調用的函數submit()源碼及註釋如下:

    public void submit() 
        throws IOException, InterruptedException, ClassNotFoundException {
    ensureState(JobState.DEFINE); // 確保當前的Job的狀態是處於DEFINE,否則不能提交Job。
    setUseNewAPI(); // 啓用新的API,即org.apache.hadoop.mapreduce下的Mapper和Reducer
    connect(); // Connect方法會產生一個JobClient實例,用來和JobTracker通信。
    final JobSubmitter submitter = // 構造提交器
        getJobSubmitter(cluster.getFileSystem(), cluster.getClient());
    status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
      public JobStatus run() throws IOException, InterruptedException, 
      ClassNotFoundException {
        return submitter.submitJobInternal(Job.this, cluster); // 提交
      }
    });
    state = JobState.RUNNING;
    LOG.info("The url to track the job: " + getTrackingURL());
   }

3.submitJobInternal

提交函數 submitJobInternal() 源碼太長,就不貼了,它主要乾了以下事情:

1.checkSpecs:檢查輸出目錄,如果已存在則報錯
2.getStagingDir:初始化Job執行過程中會用到的文件的存放路徑
3.getHostAddress/Name:獲取和設置提交job機器的地址和主機名
4.getNewJobID:獲取JobID
5.從HDFS的NameNode獲取驗證用的Token,並將其放入緩存。攜帶這個Token就可以去NameNode查詢task運行情況
6. copyAndConfigureFiles:上傳命令中配置的文件,比如我們打的WordCount.jar
7.writeSplits:對輸入文件分片,將分片信息寫入HDFS中
8.submitJob:正式提交Job到Yarn

至此,Job已經正式提交到Yarn去運行了。

參考博客:mapreduce job提交流程源碼級分析: https://www.cnblogs.com/lxf20061900/p/3643581.html

一、getSplits:輸入文件分片

假設我們有一個大小爲200M的文件,裏面每行是一個單詞。在上面的Job提交流程中,有一步就是對輸入文件進行分片。

默認情況下我們調用的是TextInputFormat類來對文件進行分片,分片函數getSplits繼承自它的抽象父類FileInputFormat,以下是FileInputFormat.getSplits()函數的流程圖。

(1)遍歷輸入文件

(2)如果文件大小=0,則新增一個長度爲0的split;否則到第3步

(3)判斷文件是否可切分(默認返回True),如果不可切分,則新增一個和文件相同大小的split(即把這個文件全部放到一個split中),否則到第4步

(4)計算一個切片的大小,是minSize,maxSize,blockSize這三個數中的不大不小的那個

(5)如果文件剩餘帶下/splitSize>1.1,則切分出長度爲splitSize的大小的一個切片;否則把剩下的全部都作爲一個切片(這樣做是爲了防止只剩129M的時候被切成了128M+1M,防止了過多小文件的產生)

(6)重複1-5直到文件遍歷結束,返回所有切分信息。

minSize 可通過 mapreduce.input.fileinputformat.split.minsize 來設置

maxSize 可通過mapreduce.input.fileinputformat.split.maxsize 來設置

blockSize在Hadoop2中默認128M,Hadoop1中默認64M

FileSplit主要屬性如下圖:

從上圖可以看出一個split只是記錄了一個文件的位置、開始、結束等信息,只是邏輯上的一個分片,並不是真正的切出來這樣的一個文件放在磁盤上。

所以經過上面切分之後,我們得到了兩個split,第一個是0~128M,第二個是128~200M。

 

二、RecordReader:讀取文件

InputFormat除了對輸入文件進行切片,還有一個重要的作用就是讀取輸入文件,轉化爲key-value形式的數據傳遞給Map來處理。InputFormat.createRecordReader()會返回一個RecordReader實例,然後調用RecordReader中的方法對文件進行讀取。以LineRecordReader爲例,主要變量和函數如下:

其中start、end記錄了當前split的開始和結束,pos記錄了當前讀取的位置。nextKeyValue會判斷是否還有下一個k-v,如果有將會獲取下一個k-v,然後調用getCurrentKey()獲取當前key,getCurrentValue獲取當前value。LineRecordReader輸出的key是偏移量,value是每一行的內容。所以輸入文件的轉化過程如下:

三、Map

Mapper的驅動函數如下:

run(Context context) throws IOException, InterruptedException {
    setup(context);
    try {
      while (context.nextKeyValue()) {
        map(context.getCurrentKey(), context.getCurrentValue(), context);
      }
    } finally {
      cleanup(context);
    }
  }

可以看出,context是RecordReader的一層封裝,調用的都是RecordReader中的函數。獲取到k-v之後就傳遞給我們自己寫的map函數,輸出新的k-v。這一步的轉化過程如下:

四、環形緩衝區

Mapper的輸出去向如何呢?我們在map()中通過Context.write(k,v)來輸出計算好的k-v,通過outputCollector收集之後寫到寫到環形緩衝區中。

環形內緩衝區就是內存中一塊連續的地址,我們從它的一端寫數據,也就是Mapper輸出的k-v,另一端寫這些數據的索引,包括第index個k-v、屬於第partition個分區、key的起始位置keystart、value的起始位置valuestart。數據和索引是根據equator區分的,這個equator在發生溢寫之後是可以變化的。

 

這個環形緩衝區的默認大小是100M,當這個環形數組的數據存儲量達到80%的時候就開始執行splill溢寫操作,它會鎖定這80%的內存數據,並把這些數據給它寫到本地磁盤上。在溢寫的時候,剩下的20%依然可以接受來自Mapper的數據。

上面所說的partition表示這個k-v屬於第幾個分區,是通過Partitioner.getPartition()確定的,默認是按照key的哈希值&int最大值,然後對reduce個數取模。當達到80%開始溢寫的時候,有幾個分區就會產生幾個分區文件。在寫入文件之前,還會對同一個分區中的數據按照key進行快速排序,然後把排好序的文件寫到文件中。

假如我們有兩個reduce,第一個80%會產生兩個分區文件,分區1和分區2。當第二次達到80%的時候會再產生兩個分區文件,分區3和分區4。如果此時map執行結束了,同一個map產生的這四個分區文件還會按照相同的partition進行合併,合併的時候會進行歸併排序以保證合併後的文件也是有序的。上面這些操作圖示如下:

這是在我們沒有配置Combiner的情況下的執行方式。如果我們配置了Combiner,則會在spill到磁盤的時候對相同key的數據進行合併,四個分區文件進行合併的時候對相同的key也會執行合併操作。所以有combiner的時候執行過程如下:

接下來詳細看一下Combiner。Combiner繼承於Reducer,是對同一key的value列表進行處理。Combiner是對同一key的部分value進行操作,而Reducer是對同一key的所有value進行操作。所以我們只能在部分操作不影響總體操作的時候才能使用Combiner,比如最大值、最小值等。不能用的情況有平均值、中位數等。

Combiner的好處有:它會先對對map輸出的結果進行一次合併,減少了map和reduce節點中的數據傳輸量。同時map階段已經對部分數據進行了合併,減少了reduce要處理的數據量。

 

 

 

 

 

未完待續。。。

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