MapReduce框架的優勢是可以在集羣中並行運行mapper和reducer任務,那如何確定mapper和reducer的數量呢,或者說如何以編程的方式控製作業啓動的mapper和reducer數量呢?在《Hadoop-2.4.1學習之Mapper和Reducer》中曾經提及建議reducer的數量爲(0.95~1.75 ) * 節點數量 * 每個節點上最大的容器數,並可使用方法Job.setNumReduceTasks(int),mapper的數量由輸入文件的大小確定,且沒有相應的setNumMapTasks方法,但可以通過Configuration.set(JobContext.NUM_MAPS, int)設置,其中JobContext.NUM_MAPS的值爲mapreduce.job.maps,而在Hadoop的官方網站上對該參數的描述爲與MapReduce框架和作業配置巧妙地交互,並且設置起來更加複雜。從這樣一句含糊不清的話無法得知究竟如何確定mapper的數量,顯然只能求助於源代碼了。
在hadoop中MapReduce作業通過JobSubmitter類的submitJobInternal(Jobjob, Cluster cluster)方法向系統提交作業(該方法不僅設置mapper數量,還執行了一些其它操作如檢查輸出格式等,感興趣的可以參考源代碼),在該方法中與設置mapper有關的代碼如下:
- int maps = writeSplits(job, submitJobDir);
- conf.setInt(MRJobConfig.NUM_MAPS, maps);
- LOG.info(”number of splits:” + maps);
方法writeSplits返回mapper的數量,該方法的源代碼如下:
- private int writeSplits(org.apache.hadoop.mapreduce.JobContext job,Path jobSubmitDir)
- throws IOException,InterruptedException, ClassNotFoundException {
- JobConf jConf = (JobConf)job.getConfiguration();
- int maps;
- if (jConf.getUseNewMapper()) {
- maps = writeNewSplits(job, jobSubmitDir);
- } else {
- maps = writeOldSplits(jConf, jobSubmitDir);
- }
- return maps;
- }
在該方法中,根據是否使用了新版本的JobContext而使用不同的方法計算mapper數量,實際情況是jConf.getUseNewMapper()將返回true,因此將執行writeNewSplits(job,jobSubmitDir)語句,該方法的源代碼如下:
- Configuration conf = job.getConfiguration();
- InputFormat<?, ?> input = ReflectionUtils.newInstance(job.getInputFormatClass(), conf);
- List<InputSplit> splits = input.getSplits(job);
- T[] array = (T[]) splits.toArray(new InputSplit[splits.size()]);
- // sort the splits into order based on size, so that the biggest
- // go first
- Arrays.sort(array, new SplitComparator());
- JobSplitWriter.createSplitFiles(jobSubmitDir, conf, jobSubmitDir.getFileSystem(conf), array);
- return array.length;
通過上面的代碼可以得知,實際的mapper數量爲輸入分片的數量,而分片的數量又由使用的輸入格式決定,默認爲TextInputFormat,該類爲FileInputFormat的子類。確定分片數量的任務交由FileInputFormat的getSplits(job)完成,在此補充一下FileInputFormat繼承自抽象類InputFormat,該類定義了MapReduce作業的輸入規範,其中的抽象方法List<InputSplit> getSplits(JobContext context)定義瞭如何將輸入分割爲InputSplit,不同的輸入有不同的分隔邏輯,而分隔得到的每個InputSplit交由不同的mapper處理,因此該方法的返回值確定了mapper的數量。下面將分爲兩部分學習該方法是如何在FileInputFormat中實現的,爲了將注意力集中在最重要的部分,對日誌輸出等信息將不做介紹,完整的實現可以參考源代碼。
首先是第一部分,該部分代碼計算了最大InputSplit和最小InputSplit的值,如下:
- long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
- long maxSize = getMaxSplitSize(job);
其中的getMinSplitSize和getMaxSplitSize方法分別用於獲取最小InputSplit和最大InputSplit的值,對應的配置參數分別爲mapreduce.input.fileinputformat.split.minsize,默認值爲1L和mapreduce.input.fileinputformat.split.maxsize,默認值爲Long.MAX_VALUE,十六進制數值爲 0x7fffffffffffffffL,對應的十進制爲9223372036854775807,getFormatMinSplitSize方法返回該輸入格式下InputSplit的下限。以上數字的單位都是byte。由此得出minSize的大小爲1L,maxSize的大小爲Long.MAX_VALUE。
其次是生成InputSplit的第二部分。在該部分將生成包含InputSplit的List,而List的大小爲InputSplit的數量,進而確定了mapper的數量。其中重要的代碼爲:
- if (isSplitable(job, path)) {
- long blockSize = file.getBlockSize();
- long splitSize = computeSplitSize(blockSize, minSize, maxSize);
- long bytesRemaining = length;
- 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) {
- int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
- splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
- blkLocations[blkIndex].getHosts()));
- }
- }
blockSize的值爲參數dfs.blocksize的值,默認爲128M。方法computeSplitSize(blockSize, minSize, maxSize)根據blockSize,minSize,maxSize確定InputSplit的大小,源代碼如下:
- Math.max(minSize, Math.min(maxSize, blockSize))
從該代碼並結合第一部分的分析可以得知,InputSplit的大小取決於dfs.blocksiz、mapreduce.input.fileinputformat.split.minsize、mapreduce.input.fileinputformat.split.maxsize和所使用的輸入格式。在輸入格式爲TextInputFormat的情況下,且不修改InputSplit的最大值和最小值的情況,InputSplit的最終值爲dfs.blocksize的值。
變量SPLIT_SLOP的值爲1.1,決定了當剩餘文件大小多大時停止按照變量splitSize分割文件。根據代碼可知,當剩餘文件小於等於1.1倍splitSize時,將把剩餘的文件做爲一個InputSplit,即最後一個InputSplit的大小最大爲1.1倍splitSize。