从源码的角度带你走进MapReduce的世界!!!

权利的游戏
花絮:今天周六,明天04.15《权力的游戏第八季》开播,期待ing…

前言

        本文主要内容:
                  一、角色及功能
                  二、MapTask调用解析
                  三、ReduceTask调用解析

一、角色及功能

1、客户端(Client)——任务提交

Internal method for submitting jobs to the system.The job submission process involves:

Checking the input and output specifications of the job.
1、检查作业输入输出的指定
输入路径一定要存在,输出路径一定不存在
Computing the InputSplits for the job.
2、计算作业的输入切片
Setup the requisite accounting information for the DistributedCache of the job, if necessary.
3、如果需要,设置需要的计数器信息和分布式缓存信息
Copying the job’s jar and configuration to the map-reduce system directory on the distributed file-system.
4、将作业的jar包以及配置文件发送到分布式文件系统的目录中
Submitting the job to the JobTracker and optionally monitoring it’s status.
5、将作业提交给ResourceManager并监控作业的执行进度和状态

以上操作都是客户端做的。
执行流程图
                                             图一    执行流程图

源码分析
默认情况下,计算切片要用到TextInputFormat类,这个类是默认的
/** An {@link InputFormat} for plain text files.  Files are broken into lines.
 * Either linefeed or carriage-return are used to signal end of line.  Keys are the position in the file, and values are the line of text.. */

input.getSplits()方法来源于FileInputFormat

FileInputFormat(378行):

	long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
										1						1L
	minSize = 1
	
    long maxSize = getMaxSplitSize(job);
	maxSize = Long.MAX_VALUE  2^63-1
	
	java代码:
		//设置切片最小大小, 默认值是1L
		conf.setLong(FileInputFormat.SPLIT_MINSIZE, 2L);
		//设置切片的最大大小,默认值为Long的最大值
		conf.setLong(FileInputFormat.SPLIT_MAXSIZE, 64*1024*1024L);
	
	
获取block大小:
 long blockSize = file.getBlockSize();
 计算切片大小:
 long splitSize = computeSplitSize(blockSize, minSize, maxSize);
 
 long splitSize = computeSplitSize(128MB, 1, maxSize);
 
 long splitSize = computeSplitSize(128MB, 1, Long.MAX_VALUE);
		Math.max(minSize, Math.min(maxSize, blockSize));  
		                   切片大小最大也就block大小
		Math.max(1, Math.min(Long.MAX_VALUE, 128MB));
		Math.max(1, 128MB);
splitSize = 128MB

结论:默认情况下,一个block对应一个切片

直接设置,最好是大于minSize小于等于blocksize
maxSize = getLong(SPLIT_MAXSIZE
只要设置比1大,它就说了算
minSize = getLong(SPLIT_MINSIZE,

切片中包含的信息:
	1、文件的路径
	2、偏移量
	3、切片长度
	4、此切片所在的block块所在的主机名称

JobSubmitter中的197int maps = writeSplits(job, submitJobDir);
conf.setInt(MRJobConfig.NUM_MAPS, maps);

结论:切片的数量对应于map任务的数量
结论

        默认情况下,一个block对应一个切片,切片的数量对应于map任务的数量。

2、HDFS

存储配置文件/jar包/切片信息

3、ResourceManager

调度NodeManager,让NM分配一个容器,运行MRAppMaster程序;
RM会调度NodeManager分配容器,用于运行map任务或者reduce任务。

4、NodeManager

分配容器,根据MRAppMaster的请求,运行指定的任务,map任务或者reduce任务
当任务运行结束,要通知MRAppMaster,MRAppMaster做后续的处理

5、MRAppMaster

负责计算的监控/失败重试/计算调度;
收集HDFS上的配置文件/jar包/切片信息;
分析出需要运行多少个Map任务,多少个reduce任务;
向RM发送请求,让RM分配容器,运行map任务;
当map任务完成5%之后,为reduce申请容器。

6、YarnChild

        MrAppmaster运行程序时向resouce manager 请求的maptask/reduceTask。也是运行程序的容器。其实它就是一个运行程序的进程。

二、MapTask调用解析

MapReduce工作流程
                                        图二        MapReduce工作流程

1、Task

        YarnChild,由NodeManager通过脚本启动,YarnChild的main方法中调用Task的run方法,task可以是maptask也可以是reducetask
Task中,163行, taskFinal.run(job, umbilical); // run the task

2、MapTask

        在MapTask中,run方法调用了它自己的runNewMapper方法;784行MapTask调用了mapper的run方法

3、该run方法在哪里?

        在MapTask的runNewMapper中,
                创建了任务上下文对象;
                创建了Mapper对象,这个mapper对象是我们自己设置的;
                创建了FileInputFormat对象,默认是TextInputFormat.class的对象;
                创建了切片对象,用于读取输入的内容;
                创建了读取切片内容的输入流对象:NewTrackingRecordReader
                创建了输出对象;
        reduce任务可以没有 (runNewMapper 763行)
        创建Map任务的上下文件对象,开始调用map方法进行map任务的计算,重写的map方法中的context参数的几个方法:

context
context.nextKeyValue()
mapContext.nextKeyValue();
context.getCurrentKey()
mapContext.getCurrentKey();
context.getCurrentValue()
mapContext.getCurrentValue();
context.write(key, value)
mapContext.write(key, value);

为什么不直接调用mapContext的方法而要转一下?
	MapContext mapContext
	Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context

        因为Mapper中run方法要求必须是Context类型的,而Mapper中Context,是一个抽象类,可以将MapContext的实现跟Mapper中的Context进行解耦,此时四个方法如何实现,只需要看MapContext中是如何实现的。

(1)、KeyValueLineRecordReader(key,value的默认切分方式)

"hello beijing\tI'm OK"
	
	key="hello beijing"
	value="I'm OK"
	
	如果有多个制表符,则以第一个为准
	
	<hello, 4>
	<beijing, 3>
	
	hello\t4
	beijing\t3

(2)、LineRecordReader(默认的方式)

        以行的偏移量为key,行的字符串为value的读取方式,MapTask中755行,input输入流是NewTrackingRecordReader对象。

(3)、NewTrackingRecordReader是如何读取的?

        mapper.run(mapperContext);中用到nextKeyValue()方法,这个方法是mapperContext的,mapperContext代理了mapContext的;mapContext代理了input的,input是NewTrackingRecordReader对象;所以要看NewTrackingRecordReader对象中的nextKeyValue方法。

MapTask 512行:real是真正读取的
748行说明了默认的InputFormat是哪个实现类
JobContextImpl类中175行说明了默认情况下使用TextInputFormat作为
inputFormat的实现类

LineRecordReader
        该类中116行,判断如果当前map处理的切片不是第一个切片,则永远丢掉第一行.该类180行说明,只要不是最后一个切片,则永远多读一行,将block块有可能切开的一行拼成整体。

结论:map读取数据的时候,使用LineRecordReader来读取在nextKeyValue方法中读取

(4)、context.write方法要找MapContextImpl的父类

TaskInputOutputContextImpl	该类87行:write方法
该write方法用的是output的write方法,该output是MapContextImpl
构造器中实例化父类的时候传过去的参数

在MapContextImpl构造器中的该参数是谁?
在实例化MapContextImpl的时候传的参数是哪个

MapTask类767行实例化出来的NewOutputCollector
NewOutputCollector类中的write方法:
711行:
  collector.collect(key, value,
                    partitioner.getPartition(key, value, partitions));
NewOutputCollector类中697行
如果分区数是1,则直接返回0分区
否则使用默认分区器计算分区:
	默认分区器是哪个?
	在JobContextImpl的235行,如果用户设置,则使用用户设置的
	否则使用HashPartitioner
	
	默认如何计算分区?
	(key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;

4 、环形缓冲区

NewOutputCollector类用于写map的输出数据到环形缓冲区
这个类通过collector的对象输出数据
collector是谁?在NewOutputCollector类695行进行初始化

MapTask第382行的方法用于创建collector对象,默认情况下,这个对象
是MapOutputBuffer类的对象 387行

MapOutputBuffer类的collect方法:

MapTask 965行 环形缓冲区默认阈值0.8 80%
当环形缓冲区写到80%后,开始向磁盘溢写数据
MapTask 967行,sortmb指的是环形缓冲区大小,默认100MB
981行将MB单位换算为Byte单位,在983行实例化出了缓存,byte数组

MapOutputBuffer类的collect方法:
        1065行开始执行collect方法,将键值对序列化好之后写到缓存中,在MapOutputBuffer的1255行compare方法先按照分区比较,再按照key比较,要排序就要比较大小‘看comparator如何实现,在1001行有赋值:

5、比较器

        要么key的类型系统提供并注册到WritableComparator类的map集合中,要么自己定义,自己注册;Text.class 本身提供了比较器,并且注册到WritableComparator类map集合中了。

6、Writable接口、WritableComparable接口

WritableComparator类中65行
如果没有注册过比较器,比如像自定义的key类型
WritableComparator会直接创建一个WritableComparator对象,其中
用到了自定义的key的类型
要求自定义的key类型实现WritableComparable接口
自定义key类型的时候需要实现compareTo方法,在该方法中自定义比较
规则。

7、排序是如何排序的?

        的978行,默认使用QuickSort快排,1593行进行排序进行快排的时候,调用了MapOutputBuffer的compare方法‘compare方法用到了key类型注册的比较器,或者自定义的比较器’还可以继承WritableComparator类,重写compare方法,调用父类的有参构造器。

三、ReduceTask调用解析

        ReduceTask376行使用shuffle插件从map端抽取数据,最终返回一个迭代器

384、385、386行获取map输出key的类型和value的类型以及比较器
该比较器默认就是map输出key的比较器,和溢写排序的时候用的比较器一样

可以对一个job设置分组比较器的类,用于键值对分组,进行reduce计算
job.setGroupingComparatorClass(cls);

在自定义分组比较器的时候,可以继承WritableComparator类,重写
compare方法,如果是0,表示两个键值对是一组中的数据

context:
context.nextKey()
reducerContext封装了reduceContext
nextKey方法就在reduceContext中
ReduceContextImpl类有nextkey方法
实际上ReduceContextImpl类中有这四个方法
        context.getCurrentKey()
        context.getValues()
        context.write(key, outValue);

在WritableDeserializer中的63行有一个反序列化方法
该方法中,传过来的参数就是返回的参数

在reduce方法中,没有必要遍历所有的数据
如果数据有一部分没有用到,则nextkey方法会自动丢弃,数据不处理

猜你喜欢

转载自blog.csdn.net/weixin_42312342/article/details/89279734
今日推荐