MapReduce入门编程及源码详解

文章目录

        • 1 入门编程WordCount
        • 2 MR Job提交源码分析
            • Class Job
            • Job.waitForCompletion
            • job.submit
        • 3 MR Map阶段过程详解
          • 3.1 MapTask类解读
          • 3.2 InputFormat
            • getSplits
            • createRecordReader
          • 3.3 Mapper
          • 3.4 OutputCollector
            • NewOutputCollector
            • MapOutputBuffer
        • 4 MR Reduce阶段过程详解
          • 4.1 ReduceTask类解读
          • 4.2 ShuffleConsumerPlugin
          • 4.3 Shuffle-Copy
          • 4.4 Shuffle-Merge
          • 4.5 Shuffle-Sort
          • 4.6 Reducer
          • 4.7 OutputFormat

1 入门编程WordCount

mapper

package wordcount;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

public class WCMapper extends Mapper<LongWritable, Text,Text,LongWritable> {
    private Text KeyOut = new Text();
    private final static LongWritable valueOut = new LongWritable(1);

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

        //将读取的一行内容根据分隔符进行切分
        String[] words =value.toString().split("\\s+");

        for(String word:words){

            KeyOut.set(word);
            //输出单词
            context.write(new Text(word),valueOut);
        }
    }
}

reducer

package wordcount;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.io.Text;

import java.io.IOException;

public class WCReducer extends Reducer <Text, LongWritable,Text,LongWritable>{

    private LongWritable result = new LongWritable();
    @Override
    protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {

        //统计变量
        long count = 0;

        //遍历一组数据,取出该组所有的value
        for (LongWritable value:values){
            count += value.get();
        }
        result.set(count);

        context.write(key,result);

    }
}

driver

package wordcount;


import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;

import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class WCdriver {
    public static void main(String[] args) throws Exception{
        //配置文件对象
        Configuration conf = new Configuration();

        //创建作业实例
        Job job = Job.getInstance(conf, WCdriver.class.getSimpleName());

        //设置作业驱动类
        job.setJarByClass(WCdriver.class);

        //设置作业mapper reducer类
        job.setMapperClass(WCMapper.class);
        job.setReducerClass(WCReducer.class);

        //设置作业mapper阶段输出key value数据类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(LongWritable.class);

        //设置作业reducer阶段的输出key value数据类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(LongWritable.class);

        //配置作业输入路径
        FileInputFormat.addInputPath(job,new Path("E:\\InAndOut\\hadoop\\Input\\word.txt"));

        //配置作业输出路径
        Path out = new Path("E:\\InAndOut\\hadoop\\Output\\WC");
        FileOutputFormat.setOutputPath(job,out);

        //判断输出路径是否存在
        FileSystem fs = FileSystem.get(conf);
        if (fs.exists(out)){
            fs.delete(out,true);
        }

        //提交作业并等待执行完成
        boolean resultFlag = job.waitForCompletion(true);

        //程序退出
        System.exit(resultFlag?0:1);

    }
}

注意事项

  • hadoop序列化数据类型实在org.apache.hadoop.io包下的,导错包会导致编译错误。

  • 配置作业输入( FileInputFormat)输出(FileOutputFormat)路径时,这两个类必须得是org.apache.hadoop.mapreduce.lib包下的类,导错包会出现编码错误。

  • 输出路径为文件夹,因为输出的不是一个文件,还有相应的矫正文件,所以得指定为文件夹。

2 MR Job提交源码分析
Class Job

概述

  • 作为使用java语言编写的MapReduce程序,其入口方法为main方法。
  • 在MapReduce main方法中,整个核心围绕在Job类,中文通常称之为作业。

格式

public class WordCountDriver_v1 {
public static void main(String[] args) throws Exception {
……
Job job = Job.getInstance(conf, WordCountDriver_v1.class.getSimpleName());	job.setJarByClass(WordCountDriver_v1.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
boolean resultFlag = job.waitForCompletion(true);
	……
  }
  }

作用

  • 在Job类源码注释中,针对Job的作用做了描述:Job类允许用户配置作业,提交作业,控制作业执行以及查询作业状态。用户创建MapReduce应用程序,通过Job描述作业的各个方面,然后提交作业并监视其进度。
  • 通常,我们把定义描述Job所在的主类(含有main方法的类)称之为MapReduce程序的驱动类。
Job.waitForCompletion

概述

  • 客户端驱动程序最后执行了Job.waitForCompletion方法;从名字上可以看出该方法的功能是等待MapReduce程序执行完毕。

  • 点击进入Job.waitForCompletion方法内部:

    • 在判断状态state可以提交Job后,执行submit方法提交作业。

    • monitorAndPrintJob()方法会不断的刷新获取job运行的进度信息,并打印。verbose为true表明要实时监视打印进度,该参数由用户提交程序时指定。

    • isSuccessful用于判断Job的最终状态是否成功完成,返回布尔类型结果。

job.submit

概述:submit方法核心分为两个方面:一是跟MR运行集群环境建立连接;二是提交MR程序到集群运行。

集群建立连接

  • connect方法:整个connect方法的核心是创建cluster对象实例。

  • Cluster类中最重要的两个成员变量是客户端通信协议提供者ClientProtocolProvider、客户端通信协议ClientProtocol,其实例叫做client,依托ClientProtocolProvider的create()方法产生。

  • ClientProtocolProvider分为LocalClientProtocolProvider(本地模式),YarnClientProtocolProvider(yarn集群模式)。通过new Cluster–>this–>initialize可以发现,会根据配置信息,调用不同的ClientProtocolProvider创建不同的ClientProtocol。

  • ClientProtocol是与集群进行通信的客户端通信协议,其实例叫做client,有两种不同的具体实现:Yarn模式的YARNRunner、Local模式的LocalJobRunner

  • 在ClientProtocol中,定义了很多方法,用户可以使用这些方法进行job的提交、杀死、或是获取一些程序状态信息。

提交MR程序到集群:

  • 在job.submit方法的最后,调用了submitter.submitJobInternal方法进行作业的提交

  • JobSubmitter作业提交器共有四个类成员变量,分别为:

    • 文件系统FileSystem实例jtFs:用于操作作业运行需要的各种文件等;

    • 客户端通信协议ClientProtocol实例submitClient:用于与集群交互,完成作业提交、作业状态查询等。

    • 提交作业的主机名submitHostName;

    • 提交作业的主机地址submitHostAddress。

  • submitJobInternal实现了提交作业的全逻辑。包括输出规范性检测、作业属性参数设置、作业准备区创建与准备资源提交(依赖资源、job.split、job.xml)、最终提交作业等。

  • submitJobInternal–真正提交作业,分为两种模式提交程序,本地模式运行提交任务、YARN模式运行提交任务。

3 MR Map阶段过程详解

整体概述

  • 待处理目录下所有文件逻辑上被切分为多个Split文件,一个Split被一个MapTask处理;
  • 通过InputFormat按行读取内容返回键值对,调用map(用户自己实现的)方法进行处理;
  • 结果交给OutputCollector输出收集器,对输出结果key进行分区Partition,然后写入内存缓冲区;
  • 当缓冲区快满的时候,将缓冲区的数据以一个临时文件的方式溢写Spill存放到磁盘,溢写时排序Sort;
  • 最终对这个map task产生的所有临时文件做合并Merge,生成最终的Map正式输出文件。
3.1 MapTask类解读

概述

  • 在MapReduce程序中,初登场的阶段叫做Map阶段,Map阶段运行的task叫做maptask。
  • MapTask类作为maptask的载体,调用的就是类的run方法,开启Map阶段任务。

第一层调用(MapTask.run)

在MapTask.run方法的第一层调用中,有下面两个重要的代码段。

  1. map阶段的任务划分(根据是否有reduce阶段来决定如何划分)
  2. 运行Mapper类

第二层调用(runNewMapper)准备部分

runNewMapper内第一大部分代码为maptask运行的准备部分,其主要逻辑是创建maptask运行时需要的各种对象。

  • Input Split 切片信息;
  • InputFormat、LineRecordReader 读取数据组件;
  • Mapper 处理数据组件;
  • OutputCollector 输出收集器;
  • taskContext、 mapperContext上下文对象;

第二层调用(runNewMapper)工作部分

(1)如何从切片读取数据(initialize逻辑)

  • 根据切片信息读取数据获得输入流in

  • 判断切片是否被压缩,使用的压缩算法是否为可切分算法。

  • 判断自己是否属于第一个切片,如果不是,舍弃第一行数据不处理。

  • 最终读取数据的实现在in.readLine方法中。默认行为是:根据回车换行符一行一行读取数据,返回键值对。

    • key:每一行起始位置偏移量

    • value:这一行的内容

(2)调用map方法处理数据,就是用户重写方法map()来实现业务逻辑的地方。

(3)如何调用OutputCollector收集map输出的结果

  • createSortingCollector创建map输出收集器是最复杂的一部分,因为和后续环形缓冲区操作有关。
  • 进入createSortingCollector方法。注意collector的默认实现是MapOutputBuffer
3.2 InputFormat

概述

  • 整个MapReduce以InputFormat开始,其负责读取待处理的数据。默认的实现叫做TextInputFormat

  • InputFormat核心逻辑体现在两个方面:

    • 一是:如何读取待处理目录下的文件。一个一个读?还是一起读?

    • 二是:读取数据的行为是什么以及返回什么样的结果?是一行一行读?还是按字节读?

  • 可以说,不同的实现有不同的处理逻辑。

getSplits

概述

  • maptask的并行度问题,指的是map阶段有多少个并行的task共同处理任务。
  • map阶段并行度由客户端在提交job时决定,即客户端提交job之前会对待处理数据进行逻辑切片。切片完成会形成切片规划文件(job.split),每个逻辑切片最终对应启动一个maptask。
  • 逻辑切片机制由FileInputFormat实现类的getSplits()方法完成。

MapTask切片机制(逻辑规划):

  • 首先需要计算出split size切片大小(split size=block size

  • 然后以split size逐个遍历待处理的文件,形成逻辑规划文件。默认情况下,有多少个split就对应启动多少个MapTask。

  • 在getSplits方法中,创建了一个集合splits,用于保存最终的切片信息。生成的切片信息在客户端提交job时,也就是JobSubmitter. writeSplits方法中,把所有切片信息进行排序,大的切片在前,然后序列化到一个文件中,此文件叫做逻辑切片文件(job.split),提交到作业准备区路径下。

  • 在进行逻辑切片的时候,假如说一个文件恰好是129M大小,那么根据默认的逻辑切片规则将会形成一大一小两个切片(0-128 128-129),并且将启动两个maptask。这明显对资源的利用效率不高。因此在设计中,MapReduce时刻会进行bytesRemaining,剩下文件大小,如果剩下的不满足 bytesRemaining/splitSize > SPLIT_SLOP,那么将不再继续split,而是剩下的所有作为一个切片整体。SPLIT_SLOP默认值是1.1。

createRecordReader

概述

  • InputFormat.createRecordReader方法用于创建RecordReader。
  • RecordReader类最终负责读取切片数据,默认实现是LineRecordReader:一行一行按行读取数据。
  • 在LineRecordReader中,核心的方法有: initialize初始化方法,nextKeyValue读取数据方法。

initialize属于LineRecordReader的初始化方法,会被MapTask调用且调用一次。里面描述了如何从切片读取数据。

nextKeyValue方法用于判断是否还有下一行数据以及定义了按行读取数据的逻辑:一行一行读取,返回键值对类型数据。其中key是每行起始位置的offset偏移量,value为这一行的内容。

优化措施:由于文件在HDFS上进行存储的时候,物理上会进行分块存储,可能会导致文件内容的完整性被破坏。比如:一个单词hello被分开成he 和 llo存储在不同的block中,就导致单词计数的结果错误。为了避免这个问题,在实际读取split数据的时候,每个maptask会进行读取行为的调整。

  • 一是:每个maptask都多处理下一个split的第一行数据;

  • 除了第一个,每个maptask都舍去自己的第一行数据不处理。

3.3 Mapper
  • 对于map方法,如果用户不重写,父类中也有默认实现逻辑。其逻辑为:输入什么,原封不动的输出什么,也就意味着不对数据进行任何处理。
  • 此外还要注意,map方法的调用周期、次数取决于父类中run方法。当LineRecordReader. nextKeyValue返回true时,意味着还有数据,LineRecordReader每读取一行数据,返回一个kv键值对,就调用一次map方法。
  • 因此得出结论:mapper阶段默认情况下是基于行处理输入数据的。
3.4 OutputCollector

概述

  • map最终调用context.write方法将结果输出。

  • 至于输出的数据到哪里,取决于MR程序是否有Reducer阶段?

    • 如果有reducer阶段,则创建输出收集器OutputCollector,对结果收集。

    • 如果没有reducer阶段,则创建OutputFormat,默认实现是TextOutputFormat,直接将处理的结果输出到指定目录文件中。

NewOutputCollector
  • 进入NewOutputCollector构造方法,核心方法是createSortingCollector。
  • 此外还确定了程序是否需要进行分区以及分区的实现类是什么。
MapOutputBuffer
  • 在createSortingCollector方法内部,核心是创建具体的输出收集器MapOutputBuffer。
  • MapOutputBuffer就是口语中俗称的map输出的内存缓冲区。
  • 当创建好MapOutputBuffer之后,在返回给MapTask之前对其进行了init初始化。

在MapReduce具有reducetask阶段的时候,maptask的输出并不只是直接输出到磁盘上的;而是被输出收集器首先收集到内存缓冲区,最终持久化到磁盘。这个过程称之为MapReduce在Map端的Shuffle过程。主要包括:

(1)Partitioner 分区

  • 通过debug不断进入发现,最终调用的是MapTask中的Write方法。Write方法中把输出的数据kv通过收集器写入了环形缓冲区,在写入之前这里还进行了数据分区计算。partitioner.getPartition(key, value, partitions)就是计算每个mapper的输出分区编号是多少。注意,只有当reducetask >1的时候。才会进行分区的计算。

  • 默认的分区器在JobContextImpl中定义,是HashPartitioner。默认的分区规则也很简单: key.hashCode() %numReduceTasks。为了避免hashcode值为负数,通过和Integer最大值进行与计算修正hashcode为正。

(2)Circular buffer 内存环形缓冲区

  • 环形缓冲区(Circular buffer)本质就是字节数组,叫做kvbuffer ,默认100M大小。缓冲区的作用是批量收集map方法的输出结果,减少磁盘IO的影响。想一下,一个一个写和一个批次一个批次写,哪种效率高?环形缓冲区里面不仅存放着key、value的序列化之后的数据,还存储着一些元数据,存储key,value对应的元数据的区域,叫kvmeta。

  • 每个key、value都对应一个元数据,元数据由4个int组成:value的起始位置、key的起始位置、partition、value的长度。

  • key/value序列化的数据和元数据在环形缓冲区中的存储是由equator(赤道)分隔的。是相邻不重叠的两个区域。key/value按照索引递增的方向存储,meta则按照索引递减的方向存储,将其数组抽象为一个环形结构之后,以equator为界,key/value顺时针存储,meta逆时针存储。数据的索引叫做bufindex,元数据的索引叫做kvindex。

MapReduce入门编程及源码详解_第1张图片

  • kvindex每次都是向下跳四个“格子”,然后再向上一个格子一个格子地填充四元组的数据。比如kvindex初始位置是-4,当第一个写完之后,(Kvindex+0)的位置存放value的起始位置、(Kvindex+1)的位置存放key的起始位置、(Kvindex+2)的位置存放partition的值、(Kvindex+3)的位置存放value的长度,然后Kvindex跳到-8位置

创建过程

  • 初始化:在MapTask中创建OutputCollector(实现是MapOutputBuffer)的时候,对环形缓冲区进行了初始化的动作。初始化的过程中,主要是构造环形缓冲区的抽象数据结构。包括不限于:设置缓冲区大小、溢出比、初始化kvbuffer|kvmeta、设置Equator标识分界线、构造排序的实现类、combiner、压缩编码等。(MapOutputBuffer.init)

  • 数据收集:收集数据到环形缓冲区核心逻辑有:序列化key到字节数组,序列化value到字节数组,写入该条数据的元数据(起始位置、partition、长度)、更新kvindex。(MapOutputBuffer.collect)

(3)Spill、Sort 溢写、排序

概述:环形缓冲区虽然可以减少IO次数,但是内存总归有容量限制,不能把所有数据一直写入内存,数据最终还是要落入磁盘上存储的,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写。

溢写过程

  • 触发Spill阈值:整个缓冲区有个溢写的比例spill.percent。这个比例默认是0.8。当环形缓冲区的数据已经达到阈值(buffer size * spill percent = 100MB * 0.8 = 80MB),spill线程启动。

  • startSpill():spill线程是由startSpill()方法唤醒的,在进行spill操作的时候,此时map向buffer的写入操作并没有阻塞,剩下20M可以继续使用。(MapOutputBuffer.collect)

  • SpillThread:溢写的线程叫做SpillThread,查看其线程run方法,run中主要是sortAndSpill。每个spill文件都有一个索引,其中包含有关每个文件中分区的信息-分区的开始位置和结束位置。这些索引存储在内存中,叫做SpillRecord,可使用内存量为mapreduce.task.index.cache.limit.bytes,默认情况下等于1MB。如果不足以将SpillRecord存储在内存中,则所有下一个创建的溢出文件的索引都将与溢出文件一起写入磁盘。

  • 溢写数据到临时文件中

  • 更新spillRecord

  • 将内存中的spillRecord写入磁盘变成索引文件

  • Sort排序:在溢写的过程中,会对kvmeta元数据进行排序。排序规则是MapOutputBuffer.compare。先对partition进行排序其次对key值排序。这样,数据首先按分区排序,并且在每个分区内按key对数据排序。Spill线程根据排过序的Kvmeta逐个分区的把数据溢出到磁盘临时文件中,一个partition对应的数据写完之后顺序地写下个partition,直到把所有的partition遍历完。

(4)Merge 合并

  • 每次spill都会在磁盘上生成一个临时文件,如果map的输出结果真的很大,有多次这样的spill发生,磁盘上相应的就会有多个临时文件存在。这样将不利于reducetask处理数据。
  • 合并(merge)会将所有溢出文件合并在一起以确保最终一个maptask对应一个输出结果文件。
  • 一次最多可以合并文件个数由mapreduce.task.io.sort.factor指定,默认10。如果超过将进行多次merge合并。
  • 合并之后的结果还包含索引文件,索引文件描述了数据中分区范围信息,以便reducetask能够轻松获取与其相关的分区数据。

(5)Combiner 规约

作用

  • 对map端的输出先做一次局部合并,以减少在map和reduce节点之间的数据传输量,以提高网络IO性能。
  • 是MapReduce的一种优化手段之一,默认情况下不开启。

生效阶段:当job设置了Combiner,在spill和merge的两个阶段都可能执行。

4 MR Reduce阶段过程详解
  • Reduce大致分为copysortreduce三个阶段,重点在前两个阶段。
  • copy阶段包含一个eventFetcher来获取已完成的map列表,由Fetcher线程去copy数据,到各个maptask那里去拉取属于自己分区的数据。在此过程中会启动两个merge线程,分别为inMemoryMerger和onDiskMerger,分别将内存中的数据merge到磁盘和将磁盘中的数据进行merge。
  • 待数据copy完成之后,开始进行sort阶段,sort阶段主要是执行finalMerge操作,纯粹的sort阶段。
  • 完成之后就是reduce阶段,调用用户定义的reduce函数进行处理。
4.1 ReduceTask类解读

概述

  • 在MapReduce程序中,Map阶段之后进行的叫做Reduce阶段,该运行的task叫做reducetask。
  • ReduceTask类作为reducetask的载体,调用的就是类的run方法,开启reduce阶段任务。

第一层调用(ReduceTask.run)阶段划分:整个reducetask分为3个阶段:copy拉取数据、sort排序数据、reduce处理数据。

第一层调用(ReduceTask.run)Shuffle操作

  • 对于MapReduce程序来说,MapTask输出的结果并不会主动发送给各个ReduceTask;因此需要各个ReduceTask主动到各个Map端拉取属于自己分区的数据。从拉取数据开始到reduce方法处理数据之前,称之为reduce端的shuffle操作。包括copy、merge、sort。

  • 在ReduceTask.run方法中跟shuffle相关的操作,除了shuffle核心任务之外,还创建了reducetask工作相关的一些组件,包括但不限于:

    • codec解编码器
    • CombineOutputCollector输出收集器
    • shuffleConsumerPlugin(负责reduce端shuffle插件)对shuffleConsumerPlugin进行了初始化init、run运行。运行返回的结果就是reduce shuffle之后的全部数据。这是shuffle过程的核心
    • shuffleContext上下文对象
    • GroupingComparator分组比较器。

第一层调用(ReduceTask.run)运行Reducer:shuffle完的结果将进入到reducer进行最终的reduce聚合处理。

第二层调用(runNewReducer)准备部分

默认情况下,框架使用new API来运行,所以将执行runNewReducer()。runNewReducer内第一大部分代码我们称之为reducetask运行的准备部分。其主要逻辑是创建reducetask运行时需要的各种依赖。包括:taskContext上下文、创建用户编写设置的reducer类、outputFormat输出数据组件、ReducerContext上下文。

第二层调用(runNewReducer)工作部分

  • reducer.run:在runNewReducer的代码中,最后还调用了reduer.run方法开始针对shuffle后的数据进行reduce操作。
  • RecordWriter
4.2 ShuffleConsumerPlugin

概述

  • 注意ShuffleConsumerPlugin是一个接口,默认的实现只有一个Shuffle.class。
  • 其完整定义了整个reducer阶段shuffle的完整过程。
  • 在ReduceTask类中,和ShuffleConsumerPlugin相关的操作就两个方法:init初始化、run运行。

init初始化

  • 初始化的过程中,核心逻辑就是创建MergeManagerImpl类。

  • 在MergeManagerImpl类中,核心的有:

    • 确定shuffle时的一些条件;是否允许内存到内存合并;启动两个merge线程,分别为inMemoryMerger和onDiskMerger,分别将内存中的数据merge到磁盘和将磁盘中的数据进行。
  • 确定shuffle时的一些条件参数

  • 启动MemToMemMerge线程,因为fetch来数据首先放入在内存中的,正常情况下在内存中对数据进行合并是最快的。可惜的是,默认情况下,是不开启内存到内存的合并的。

  • 启动inMemoryMerger(内存到磁盘合并)、onDiskMerger(磁盘到磁盘合并)线程

run运行

  • EventFetcher线程
  • Fetcher线程
4.3 Shuffle-Copy

概述

  • Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求maptask获取属于自己的文件。
  • 如果是本地模式运行,启动1个fetcher线程拉取数据,否则启动5个线程并发拉取。

Fetcher线程run方法

(1)获得所有maptask处于PENDING待处理状态的主机。针对maptask的几种状态,在MapHost类中有记载。

(2)copyFromHost

  • 从处于PENDING状态的maptask拉取数据。下面进入copyFromHost方法内部。

  • 建立拉取数据的输入流。

  • 拉取拷贝数据。下面进入copyMapOutput方法看是如何拉取拷贝数据的。

  • 首先进行判断copy过来的数据放置在哪里?优先内存,超过限制放置磁盘。因此获得的mapOutput就有两种具体的实现。然后通过mapOutput.shuffle开始拉取数据。

    • InMemoryMapOutput:把copy来的数据放置到reducetask内存中。

    • OnDiskMapOutput:把copy来的数据放置到磁盘上。

4.4 Shuffle-Merge

概述

  • 对于从属于不同maptask拉取过来的数据,需要进行merge合并成完整的数据,最终调reduce方法进行业务处理。
  • reduce的merge合并分为3种:内存到内存合并、内存到磁盘合并、磁盘到磁盘合并。
  • 哪到哪指的是:合并之前数据在哪里以及合并之后的数据放置在哪里。
  • 其中内存到内存合并,默认不开启,因此我们通常关注后两种合并。
  • 在启动Fetcher线程copy数据过程中已经启动了两个merge线程,分别为inMemoryMerger和onDiskMerger,分别将内存中的数据merge到磁盘和将磁盘中的数据进行merge。
  • 可以从Shuffle.init----> createMergeManager—> new MergeManagerImpl中确定。

inMemoryMerger

  • inMemoryMerger本质是一个MergeThread线程。进入线程run方法。
  • 在内存中合并,合并的结果写入磁盘。

onDiskMerger

  • onDiskMerger本质是一个MergeThread线程。进入线程run方法。

finalMerger

  • 当所有的Fetcher拉取数据结束之后,会进行最终一次合并,最终合并的所有数据保存在kvIter。
  • 可以在shuffle类的run方法中找到逻辑。
4.5 Shuffle-Sort

概述

  • 在合并的过程中,会对数据进行Sort排序。
  • 默认情况下是key的字典序(WritableComparable),如果用户设置比较器,则以用户设置的为准。
4.6 Reducer
  • 当合并、排序结束之后,进入到reduce阶段。开始调用用户编写的reduce方法进行业务逻辑处理。
  • 在runNewReducer方法的最后,调用了reducer.run方法运行reducer。

reducer.run方法

  • 首先在Reduce.run中调用context.nextKey()决定是否进入while循环,然后调用nextKeyValue将key/value的值从input中读出,其次通过context.getValues将Iterator传入reduce中,在reduce中通过Iterator.hasNext查看此key是否有下个value,然后通过Iterator.next调用nextKeyValue去input中读取value。然后循环迭代Iterator,读取input中相同key的value。

  • 也就是说reduce中相同key的value值在Iterator.next中通过nextKeyValue读取的,每调用一次next就从input中读一个value。通俗理解:key相同的被分为一组,一组中所有的value会组成一个Iterable。key则是当前的value与之对应的key。

reducer.reduce方法

对于reduce方法,如果用户不重写,父类中也有默认实现逻辑。其逻辑为:输入什么,原封不动的输出什么,也就意味着不对数据进行任何处理。通常会基于业务需求重新父类的reduce方法。

4.7 OutputFormat

概述

  • reduce阶段的最后是通过调用context.write方法将数据写出的。
  • 负责输出数据的组件叫做OutputFormat,默认实现是TextOutPutFormat
  • 而真正负责写数据的组件叫做LineRecordWriter,Write方法就定义在其中,这一点和输入组件很是类似。LineRecordWriter的行为是一次输出写一行,再有输出换行写。在构造LineRecordWriter的时候,已经设置了输出的key,value之间是以\t制表符分割的。

你可能感兴趣的:(hadoop,mapreduce,hadoop,大数据,java)