文章目录
- 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按行读取内容返回<key,value>键值对,调用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方法的第一层调用中,有下面两个重要的代码段。
- map阶段的任务划分(根据是否有reduce阶段来决定如何划分)
- 运行Mapper类
第二层调用(runNewMapper)准备部分:
runNewMapper内第一大部分代码为maptask运行的准备部分,其主要逻辑是创建maptask运行时需要的各种对象。
- Input Split 切片信息;
- InputFormat、LineRecordReader 读取数据组件;
- Mapper 处理数据组件;
- OutputCollector 输出收集器;
- taskContext、 mapperContext上下文对象;
第二层调用(runNewMapper)工作部分:
(1)如何从切片读取数据(initialize逻辑)
- 根据切片信息读取数据获得输入流in
- 判断切片是否被压缩,使用的压缩算法是否为可切分算法。
- 判断自己是否属于第一个切片,如果不是,舍弃第一行数据不处理。
- 最终读取数据的实现在in.readLine方法中。默认行为是:根据回车换行符一行一行读取数据,返回<key,value>键值对。
- 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,value>键值对类型数据。其中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。
- 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大致分为copy、sort、reduce三个阶段,重点在前两个阶段。
- 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制表符分割的。