Hadoop之MapReduce详细工作流程

InputFormat阶段

根据输入格式:InputFormat的实现类
①切片, getSplit()
②使用输入格式的RR读取数据, createRecordReader()

具体实现类有:

  1. 默认的TextInputFormat
    场景: 普通的文本格式数据来源
    切片: 采用默认的切片策略,以文件为单位,先判断文件是否可切,如果可切,循环以片大小为单位切片!
    不可切,整个文件作为1片!

    RR : LineRecordReader(将一行封装为一个key-value)
    LongWritable key: 行的偏移量
    Text value: 行的内容

  2. NLineInputFormat
    场景: 适合一行的内容特别多,在Map阶段map()处理的逻辑非常复杂!
    根据行数自定义切片的大小!

     切片:	可以设置以文件为单位,每N行作为一个切片!
     
     RR :  LineRecordReader(将一行封装为一个key-value)
     		LongWritable	key: 行的偏移量
     		Text  value:  行的内容
    
  3. KeyValueTextInputFormat
    场景: 一行的内容的格式 为 key-value,方便地将key,value拆分封装

     切片:  采用默认的切片策略,以文件为单位,先判断文件是否可切,如果可切,循环以片大小为单位切片!
     		不可切,整个文件作为1片!
    
     RR :  KeyValueRecordReader(将一行封装为一个key-value)
     		Text	key:  行的分隔符之前的部分内容
     		Text  value:   行的分隔符之后的部分内容
    
  4. CombineTextInputFormat
    场景: 输入目录中小文件过多,可以将多个小文件设置到一个切片中!

     切片:  ①根据maxSize对每个文件进行逻辑切片,切分为若干part
     		②将多个part组合,知道超过maxSize,这些part作为一个切片
     		
     
     RR :  LineRecordReader(将一行封装为一个key-value)
     		LongWritable	key: 行的偏移量
     		Text  value:  行的内容	
    

这里以FileInputFormat源码来看怎么计算切片大小的:

 long splitSize = Math.max(minSize, Math.min(maxSize, blockSize));//切片大小计算规则
   
 maxSize值为:  mapreduce.input.fileinputformat.split.maxsize默认为Long的最大值;
 minSize值为:  mapreduce.input.fileinputformat.split.minsize默认为1L;
 blockSize默认为块大小,hadoop2默认为128M;
 根据默认的策略策略,可以调整切片的大小:
	调整切片大小大于块大小:调整minSize 大于块大小
	调整切片大小小于块大小:调整maxSize 小于块大小

默认切片大小等于块大小,主要为了减少在运行MR时,大量的跨机器读取切片内容带来额外的网络IO

自定义输入格式,根据自己业务重写createRecordReader及isSplitable方法:

		   /*
			* 1. 改变切片策略,一个文件固定切1片,通过指定文件不可切
			* 2. 提供RR ,这个RR读取切片的文件名作为key,读取切片的内容封装到bytes作为value
			*/
			public class MyInputFormat extends FileInputFormat {
			
			@Override
			public RecordReader createRecordReader(InputSplit split, TaskAttemptContext context)
					throws IOException, InterruptedException {
				return new MyRecordReader();
			}
		
			// 重写isSplitable
			@Override
			protected boolean isSplitable(JobContext context, Path filename) {
					return false;
			}
			
		}

Map阶段

编织规范:自己map类继承Mapper类,重写map方法:

					  /**
					  * 单词统计map
					  */
						public class wcMap extends Mapper<LongWritable, Text, Text, IntWritable> {
						    private Text out_key=new Text();
						    private IntWritable out_value=new IntWritable(1);
						
						    @Override
						    protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context) throws IOException,     InterruptedException {
						        String[] split = value.toString().split("\t");
						        for(String item:split){
						            out_key.set(item);
						            context.write(out_key, out_value);
						        }
						    }
						}

执行分析:
Job会根据切片个数来创建TaskMap,因此:split个数 = TaskMap个数
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
map阶段流程图:
在这里插入图片描述

		(1)Read 阶段:MapTask 通过用户编写的 RecordReader,
		从输入 InputSplit 中解析出 一个个 key/value。 
		(2)Map 阶段:该节点主要是将解析出的 key/value 交给用
		户编写 map()函数处理,并 产生一系列新的 key/value。 
		(3)Collect 收集阶段:在用户编写 map()函数中,当数据处
		理完成后,一般会调用 OutputCollector.collect()输出结果。在
		该函数内部,它会将生成的 key/value 分区(调用 Partitioner),
		并写入一个环形内存缓冲区中。 
		(4)Spill 阶段:即“溢写”,当环形缓冲区满后,MapReduce 会
		将数据写到本地磁盘上, 生成一个临时文件。需要注意的是,将
		数据写入本地磁盘之前,先要对数据进行一次本地排 序,并在必
		要时对数据进行合并、压缩等操作。 溢写阶段详情:
		      步骤 1:利用快速排序算法对缓存区内的数据进行排序,排序
		      方式是,先按照分区编号 Partition 进行排序,然后按照 key 
		      进行排序。这样,经过排序后,数据以分区为单位聚集在 一起,
		      且同一分区内所有数据按照 key 有序。
		      步骤 2:按照分区编号由小到大依次将每个分区中的数据写入
		      任务工作目录下的临时文 件 output/spillN.out(N 表示当前溢写次数)中。
		      如果用户设置了 Combiner,则写入文件之 前,对每个分区中的数据进
		      行一次聚集操作。 
		      步骤 3:将分区数据的元信息写到内存索引数据结构 SpillRecord 中,其
		      中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩
		      后数据大小。如果当前内存索引大 小超过 1MB,则将内存索引写到文件 
		      output/spillN.out.index 中。 
		(5)Combine 阶段:当所有数据处理完成后,MapTask 对所有临时文件进行
		一次合并, 以确保最终只会生成一个数据文件。 当所有数据处理完后,MapTask 
		会将所有临时文件合并成一个大文件,并保存到文件 output/file.out 中,同时生成
		相应的索引文件 output/file.out.index。 在进行文件合并过程中,MapTask 以分区
		为单位进行合并。对于某个分区,它将采用多 轮递归合并的方式。每轮合并
		 io.sort.factor(默认 10)个文件,并将产生的文件重新加入待 合并列表中,
		 对文件排序后,重复以上过程,直到最终得到一个大文件。 让每个 MapTask 
		 最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量 小文件
		 产生的随机读取带来的开销。

Shuffle阶段

发生阶段在mapTask跟reduceTask之间,具体流程如图:
在这里插入图片描述

	1. 阶段定义
			MapTask:   map--------sort
			map:   Mapper.map()中将输出的key-value写出之前
			sort: Mapper.map()中将输出的key-value写出之后
			
	2. sort
		①当在map()将输出的key-value写出后,记录是会被Partitioner计算一个分区号
		②计算后,记录被收集到一个缓冲区(MapOutPutBuffer)
		③收集线程负责向缓冲区收集数据,缓冲区初始值为100M,当使用到80%阈值,
			唤醒溢写线程,溢写线程会将缓冲区已经收集的数据溢写到磁盘
		④在溢写前,会对缓冲区中的数据进行排序(快速排序),在排序时,只通过比较key进行排序
		⑤排序后,按照分区,依次将数据写入到磁盘的临时文件的若干分区中
		⑥每次溢写都会生成一个临时文件,当所有的数据都溢写完成之后,会将所有的临时文件片段合并为一个总的最终的文件
		⑦在合并时,将所有的临时文件的相同分区的数据,进行合并,合并后再对所有的数据进行排序(归并排序)
		⑧最终生成一个结果文件,这个文件分为若干分区,每个分区的数据已经按照key进行了排序,等待reduceTask的shuffle线程来拷贝数据

这里有几个组件值得了解,我们可以根据业务需要重写:

		一、分区
		1. 分区是在MapTask中通过Partitioner来计算分区号
		
		2. Partitioner的初始化
				①计算总的分区数partitions,取决于用户设置的reduceTask的数量
				②partitions>1,默认尝试获取用户设置Partitioner,如果用户没有定义,那么会使用HashPartitioner
					HashPartitioner根据key的hashcode进行计算,相同的key以及hash值相同的key会分到一个区
					
				②partitions<=1,默认初始化一个Partitioner,这个Partitioner计算的所有的区号都为0
				
		3. 注意
				通常在Job的设置中,希望将数据分为几个区,就设置reduceTask的数量为对应的数量!
				partitions=设置的reduceTask的数量,0<=分区器计算的区号 < partitions
				
		二、排序
		1. 排序是MR框架在shuffle阶段自动进行
		
		2. 在MapTask端发生两次排序,在排序时,用户唯一可以控制的是提供一个key的比较器
				
		3. 设置key的比较器
				①用户可以自定义key的比较器,自定义的比较器必须是一个RawComparator类型的类,
						重点是实现compareTo()方法
				②用户通过key,让key实现WritableComparable接口,系统自动提供一个比较器,
						重点是实现compareTo()方法
						
		4. 排序的分类
				全排序: 对所有的数据进行排序,指生成一个结果文件,这个结果文件整体有序
				部分排序: 最终生成N个结果文件,每个文件内部整体有序
				二次排序:  在对key进行比较时,比较的条件为多个
				辅助排序:  在进入reduce阶段时,通过比较key是否相同,将相同的key分为1组
				
		三、分组
		1. 分组通过分组比较器,对进入reduce的key进行对比,key相同的分为一组,一次性进入Reducer,被调用reduce方法
		
		2. 分组比较器的设置
				①用户可以自定义key的分组比较器,自定义的比较器必须是一个RawComparator类型的类
					重点是实现compareTo()方法
				②如果没有设置key的分组比较器,默认采取在Map阶段排序时,key的比较器
				
		3. Reduce的细节
				在进入reduce(),Reducer会自动实例化一个key,value,这个key-value在Redcuer工作期间,一直是一个不变的对象
				每次迭代,reducer会把读到的新的key-value的属性值赋值给key-value!
				
		四、Combiner
		
		1. Combiner的本质是一个Reducer,对key-value进行合并
		
		2.  Combiner 和 Reducer的区别
				Combiner在shuffle阶段运行
				Reducer在reduce阶段运行
				
		3.  Combiner适用于  +,-操作,不适合 *,/操作
		
		4. Combiner的运行时机
				在MapTask端: ①每次从缓冲区将数据溢写到磁盘之前,如果设置了Combiner,数据会被Combine之后
					再溢写到磁盘
							②在MapTask最后的merge阶段,如果溢写的片段数据>=3,,如果设置了Combiner,在生成
					最终的数据时,也会先执行Combine之后再溢写到磁盘
					
				在ReduceTask端:  ③shuffle线程从多个MapTask读取同一个分区的数据,之后进行合并,在合并时,如果
						shuffle所使用的内存不够,也会将部分数据临时溢写到磁盘,此时如果设置了Combiner,数据会被Combine之后
					再溢写到磁盘
					
		5. Combiner的本质目的是为了减少MR在运行期间的磁盘IO和网络IO

Reduce阶段

reduce业务流程:
在这里插入图片描述

	 (1)Copy 阶段:ReduceTask 从各个 MapTask 上远程拷贝一片数据,
	 并针对某一片数 据,如果其大小超过一定阈值,则写到磁盘上,
	 否则直接放到内存中。
	 (2)Merge 阶段:在远程拷贝数据的同时,ReduceTask 启动了两个
	 后台线程对内存和 磁盘上的文件进行合并,以防止内存使用过多或
	 磁盘上文件过多。 
	 (3)Sort 阶段:按照 MapReduce 语义,用户编写 reduce()函数输入
	 数据是按 key 进行 聚集的一组数据。为了将 key 相同的数据聚在一起,
	 Hadoop 采用了基于排序的策略。由于 各个 MapTask 已经实现对自己
	 的处理结果进行了局部排序,因此,ReduceTask 只需对所有 数据进行一次归并排序即可。 
	 (4)Reduce 阶段:reduce()函数将计算结果写到 HDFS 上。 
	 
	 设置 ReduceTask 并行度(个数) ReduceTask 的并行度同样影响整个 job 
	 的执行并发度和执行效率,但与 MapTask 的并发数由切片数决定不同,
	 Reducetask 数量的决定是可以直接手动设置: //默认值是 1,手动设置为 4
	 job.setNumReduceTasks(4);  

注意事项:
在这里插入图片描述
测试 ReduceTask 多少合适。 (1)实验环境:1 个 Master 节点,16 个 Slave 节点:
在这里插入图片描述

OutputFormat阶段

在这里插入图片描述
在这里插入图片描述
使用,在job阶段设置:

job.setOutputFormatClass(MyOutPutFormat.class);

这里如果没有reduce阶段,会在map阶段根据自定义输出格式输出;
如果设置了reduce阶段,会在reduce阶段输出根据自定义输出格式输出,
map阶段输出是按照MapTask的NewOutputCollector来输出;

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值