MapReduce计算框架重要类体系图
--环境结构
|--------JobConf
|--------JobClient
|--------InputFormat
|--------FileInputFormat
|--------TextInputFormat
|--------RecordReader
|--------LineRecordReader
|--------OutPutFormat
|--------FileOutputFormat
|--------TextOutputFormat
|--------RecordReader
|--------LineRecordWriter
--数据处理引擎
|--------MapperTask. run()
|--------MapperRunner
|--------MapOutputCollector
|--------MapOutputBuffer
|--------SpillThread
|--------MapTask.mergeParts()
|--------ReduceTask.run()
|--------MapOutPutCollector
|--------MapOutputCopier
_____________________________________________________________________________________
JobConf介绍
JobConf是用户描述一个job的接口。下面的信息是MapReduce过程中一些较关键的定制信息:
MapTask介绍
MapTask启动和执行一个Map任务,初始化、启动map任务,调用的是run()方法。
run()方法源码骨架:
run方法里调用了runOldMapper()方法。这个方法是用于执行用户自定义Mapper类里的Map()方法。(M/R 2.0版本调用的是runNewMapper()方法)
runOldMapper()方法源码骨架:
InputSplit inputSplit ……
RecordReader<INKEY,INVALUE> in ……
MapRunnable<INKEY,INVALUE,OUTKEY,OUTVALUE> runner =ReflectionUtils.newInstance(job.getMapRunnerClass(), job);
MapOutputCollector<OUTKEY, OUTVALUE> collector ……
方法作用释义:
1.这个方法会解析job里的InputSplit信息,InputSplit信息里封装了一个Map任务处理的文件切片信息,如文件切片所在的节点位置信息,处理的start位置以及处理的长度length(可以类比Zebra的FileSpilt)
2.拿到文件切片信息之后,怎么来读取文件内容呢?——>解析job指定的RecordReader,Hadoop默认的是LineRecordReader(按行读取,把每行头位置信息的偏移量作为key,每行的内容作为Value)。
3.MapRunnalbe的作用是,根据反射机制,拿到用户(程序员)自定义的Mapper类,拿到这个类之后,就可以拿到用户的输出map<key,value>了。
细节说明:MapRunnable是一个接口,那实际上这里runner对象是它的实现类:MappRunner类的对象。
_____________________________________________________________________________________
MapRunner介绍
MapperRunner类源码骨架:
private Mapper<K1, V1, K2, V2> mapper;
public void configure(JobConf job) {
this.mapper = ReflectionUtils.newInstance(job.getMapperClass(), job);
}
public void run(RecordReader<K1, V1> input, OutputCollector<K2, V2> output,Reporter reporter){
while (input.next(key, value)) {
mapper.map(key, value, output);
}
}
方法释义:MapRunner通过指定的Redcord读取器,读取input split的内容(默认是一行一行读)。每读取到一行,就调用一次map方法,并获得每次map方法的输出map<k,v>
_____________________________________________________________________________________
MapOutputCollector介绍
4.MapOutputCollector是一个接口,作用是收集每次调用map后得到的新的kv对,然后把他们spill到文件或者放到内存,以做进一步的处理,比如分区、排序,combine等(就是shuffle过程)。
即MapOutputCollector是处理输出map的类。
细节说明:MapOutputCollector 其中有两个子类:MapOutputBuffer和DirectMapOutputCollector。 DirectMapOutputCollector用在不需要Reduce阶段的时候。(比如在做利润排序案例的时候,没有reducer)
如果Mapper后续有reduce任务,系统会使用MapOutputBuffer做为实现类,MapOutputBuffer使用了一个缓冲区对输出map对进行缓存。
默认缓存大小是100mb,当缓存中数据量达到80%的时候,发生spill过程,溢写到本地磁盘上。
_____________________________________________________________________________________
MapOutputBuffer介绍
MapOutputBuffer骨架源码:
public static class MapOutputBuffer<K extends Object, V extends Object> implements MapOutputCollector<K, V>, IndexedSortable {
private int partitions; //分区数量,reduce的数量,如果用户不设置,默认是1个
private Class<K> keyClass;//输出map的key类型
private Class<V> valClass;//输出map的value类型
private RawComparator<K> comparator;//比较器,用于排序
final SpillThread spillThread = new SpillThread();//是一个线程类,负责spill溢写到磁盘的过程
final BlockingBuffer bb = new BlockingBuffer();//用于存放map输出结果的缓冲区
public synchronized void collect(K key, V value, final int partition){
startSpill();//开始溢写线程,这个方法会启动SpillThread线程开始溢写。溢写的条件是缓冲区里的数据达到总大小的80%。默认是缓冲区是100mb。这个是可以在配置文件里配置的
keySerializer.serialize(key);//将输出map<K,V>的Key值序列化
valSerializer.serialize(value);//将输出map<K,V>的Value值序列化,key和value序列化后,将其数据溢写到本地文件里
}
//溢写具体是由SpilThread这个线程类来负责的
protected class SpillThread extends Thread {
sortAndSpill();//需要spill时调用函数sortAndSpill,按照partition和key做排序。默认使用的是快速排序QuickSort算法。使用的比较器,默认是WritableComparable。Hadoop的基本数据类型都是实现了这个WritableComparable接口。
}
//用户在结束map处理后,已经没有数据再输出到缓冲区,但缓存中还有数据没有刷到磁盘上,需要将缓存中的数据 flush到磁盘上,这个动作就是由MapOutputBuffer的flush来完成。
public void flush(){
}
}
_____________________________________________________________________________________
MapTask.mergeParts()方法源码骨架:
private void mergeParts() {
final Path[] filename = new Path[numSpills];//记录了生成的Spill文件信息
//循环得到每个Spill文件名字以及长度
for(int i = 0; i < numSpills; i++) {
filename[i] = mapOutputFile.getSpillFile(i);
finalOutFileSize += rfs.getFileStatus(filename[i]).getLen();
}
//将多个Spill文件内容通过finalOut输出流写出到finalOutputFile文件,这个文件就是最后的结果文件
//此外,merge后的文件是一个已分区且已排好序的文件
FSDataOutputStream finalOut=rfs.create(finalOutputFile, true, 4096);
sortPhase.addPhases(partitions);
Writer<K, V> writer = new Writer<K, V>(job, finalOut, keyClass, valClass, codec,
spilledRecordsCounter);
//如果用户未指定有Combineg过程,直接执行merge合并
//如果用户指定了有Combine过程,但是如果spill文件数量小于3,在merger阶段也不会发生combine
if (combinerRunner == null || numSpills < minSpillsForCombine) {
Merger.writeFile(kvIter, writer, reporter, job);
} else {
//如果用户指定了Comibine,并spill文件数量大于3,则会发生Combine,然后进行merge
combineCollector.setWriter(writer);
combinerRunner.combine(kvIter, combineCollector);
}
sortPhase.startNextPhase();
finalOut.close();
//Merge之后,把Spill文件删掉
for(int i = 0; i < numSpills; i++) {
rfs.delete(filename[i],true);
}
}
_____________________________________________________________________________________
ReduceTask介绍
ReduceTask用于初始化和执行Reduce任务。
初始化和启动reduce任务的方法入口是:run()方法
run()方法源码骨架:
void run(final JobConf job, final TaskUmbilicalProtocol umbilical){
//定义了reduce一共有三个阶段,分别是:copy、sort、reduce阶段
copyPhase = getProgress().addPhase("copy");
sortPhase = getProgress().addPhase("sort");
reducePhase = getProgress().addPhase("reduce");
TaskReporter reporter = startReporter(umbilical);//设置并启动report进程以便和TaskTracker进程通信,汇报reudce任务的执行情况
Class combinerClass = conf.getCombinerClass();//获取CombinerClass
shuffleConsumerPlugin.init(shuffleContext);// shuffleConsumerPlugin.init()方法用于初始化copy阶段所依赖的运行环境,以及创建Merge管理器。copy阶段就是从各个Map任务服务器那里,去拷贝map的输出文件自己分区的数据到reduce任务节点上。这一过程,我们也称之为Fetch。当reduce节点拿到所有的输出文件之后,如果文件数量太多,(hadoop默认是的数量时10)会进行文件的合并,这个过程称为merge。并且在文件合并时,会按key进行排序。
rIter = shuffleConsumerPlugin.run();//shuffleConsumerPlugin.run()方法就是执行fetch过程、merge过程、以及sort过程。并且,把最后的结果封装成: [key1,Iterable<>] [key2,Iterable<>]这样的形式,封装到rIter 对象里。
mapOutputFilesOnDisk.clear();//reduce把各个map任务节点上的文件merger完后之后会生成新文件,则原来的那些就没用了,所以删掉
sortPhase.complete(); //sortpahse完成,copyphase阶段是在先于sortphase完成
setPhase(TaskStatus.Phase.REDUCE); //更新当前状态,进入reduce阶段。
statusUpdate(umbilical);//通过rpc告知JobTracker当前reduce任务的执行阶段。
Class keyClass = job.getMapOutputKeyClass();//获取输出key值类型
Class valueClass = job.getMapOutputValueClass(); //获取reduce输出value类型
RawComparator comparator = job.getOutputValueGroupingComparator();//获取比较器
//这个是执行reduce 合并的方法入口
runOldReducer(job, umbilical, reporter, rIter, comparator, keyClass, valueClass);
done(umbilical, reporter);//以上三个阶段都完成后,reduce任务结束,做一些资源清理工作,并最后向JobTracker发送一次统计报告,然后结束Reporter通信线程。
}
执行Reduce任务的方法入口是:runOldReducer()方法 (M/R 1.x版本API)
_____________________________________________________________________________________
runOldReducer()方法源码骨架:
void runOldReducer(){
Reducer<INKEY,INVALUE,OUTKEY,OUTVALUE> reducer = ReflectionUtils.newInstance(job.getReducerClass(), job);//得到用户定义的Reduce实现类
String finalName = getOutputName(getPartition());//得到分区数量,hadoop默认分区是1。
RecordWriter<OUTKEY, OUTVALUE> finalOut =……//确定reduce结果输出器,Hadoop默认是LineRecordWriter,输出形式:reduce输出map.key Tab reduce输出map.value
reduce输出map.key Tab reduce输出map.value ……
OutputCollector<OUTKEY,OUTVALUE> collector = new OutputCollector<OUTKEY,OUTVALUE>() {
public void collect(OUTKEY key, OUTVALUE value)
{
finalOut.write(key, value);//
}
};
ReduceValuesIterator<INKEY,INVALUE> values=rIter;//将rlter里的数据拿出来。
while (values.more()) {
reducer.reduce(values.getKey(), values, collector, reporter);
values.nextKey();
}//循环执行reduce.reduce方法,并把每次reduce的输出map结果通过Collector,写出到结果文件里。直到所有遍历完所有的key值后,退出reduce方法。
reducer.close();//reduce阶段结束
}
复制线程(MapOutputCopier)
Map输出复制器ReduceCopier在其内部采用多线程的方式来从其它的TaskTracker上通过http协议请求的复制属于自己的Map任务输出结果。至于复制线程的个数可在配置文件mapred-site.xml中配置,对应的配置项:mapred.reduce.parallel.copies,为了提高reduce及整个Hadoop的效率,这个值应该设置和该作业的Map任务数差不多。每一个复制线程都从scheduledCopies 中获取一个任务来执行,在接受目标TaskTracker传过来的Map输出数据时,它会根据当前内存情况决定将该数据存放在村内还是磁盘。