MAPREDUCE简介

MAPREDUCE框架结构及核心运行机制
  一个完整的mapreduce程序在分布式运行时有三类实例进程:
   1.MRAppMaster:负责整个程序的过程调度及状态协调
   2.mapTask:负责map阶段的整个数据处理流程
   3.ReduceTask:负责reduce阶段的整个数据处理流程
   
MAPREDUCE程序运行流程
  1.一个mr程序启动的时候,最先启动的是MRAppMaster,MRAppMaster启动后根据本次job的描述信息,计算出需要的maptask实例数量,然后向集群申请机器启动相应数量的maptask进程。
  2.maptask进程启动之后,根据给定的数据切片范围进行数据处理
   (1)利用客户指定的inputformat来获取RecordReader读取数据,形成输入KV对(RecordReader的read方法,默认的格式TextInputFormat) K:默认是mapreduce读到的一行文本的起始偏移量,V:默认是mapreduce读取到的一行文本的内容。
   (2)将输入KV对传递给客户定义的map()方法,做逻辑运算,并将map()方法输出的KV对收集到缓存区,(写到OutPutCollector输出收集器,然后到环形缓冲区,环形缓冲区默认超过80%开始溢出,缓冲区中数据分区且有序)。
   (3)将缓存中的KV对按照K分区排序后不断溢写到磁盘文件
  3.MRAppMaster监控到所有maptask进程任务完成之后,会根据客户指定的参数启动相应数量的reducetask进程,并告知reducetask进程要处理的数据范围(数据分区)。
  4.reducetask进程启动之后,根据MRAppMaster告知的待处理数据所在位置,从若干台maptask运行所在机器上获取到若干个maptask输出结果文件,并在本地进行重新归并排序,然后按照相同key的KV为一个组(调用compareTo方法,比较相邻的k),调用客户定义的reduce方法进行逻辑运算,并收集运算输出的结果KV,然后调用客户指定的OutPutFormat将结果数据输出到外部存储。(RecordWriter的write方法,默认格式是TextOutPutFormat)
  注意:中间可能伴随有combiner操作,combiner操作运行的前提是不能影响最终的结果,combiner的优点是优化网络之间传输的数据量。
  maptask并行度(一个split对应一个maptask)
   一个job的map阶段并行度由客户端在提交job时决定。
   而客户端对map阶段并行度的规划的基本逻辑为:将待处理数据执行逻辑切片(即按照一个特定切片大小,将待处理数据划分成逻辑上的多个split),然后每一个split分配一个mapTask并行实例处理。(由FileInputFormat实现类的getSplit()方法完成切片)。
  reducetask并行度
  reducetask的并行度同样影响整个job的执行并发度和执行效率,但与maptask的并发数由切片数决定不同,Reducetask数量的决定是可以直接手动设置:
  默认值是1,手动设置成4。
  job.setNumReduceTasks(4);
  注意: reducetask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个reducetask。
  尽量不要运行太多的reduce task。对大多数job来说,最好reduce的个数最多和集群中的reduce slots(任务槽)持平,或者比集群的reduce slots小。这个对于小集群而言,尤其重要。
  Map slots总数=集群节点数×mapred.tasktracker.map.tasks.maximum
  Reduce slots总数=集群节点数×mapred.tasktracker.reduce.tasks.maximum

	补充:
		1.reduce任务的数量并非由输入数据的大小决定,而是特别指定的。
		  可以设定mapred.tasktracker.map.task.maximum和mapred.tasktracker.reduce.task.maximum属性的值来指定map和reduce的数量
		2.reduce最优个数与集群中可用的reduce任务槽相关,总槽数由节点数乘以每个节点的任务槽
		3.本地作业运行器上,只支持0个或者1个reduce任务
		4.在一个tasktracker上能同时运行的任务数取决于一台机器有多少个处理器,还与相关作业的cpu使用情况有关,
		  经验法则是任务数(包括map和reduce)处理器的比值为1到2.
		5.如果有多个reduce任务,则每个map输出通过哈希函数分区,每个分区对应一个reduce任务
内置数据类型
	BooleanWritable:标准布尔型数值
	ByteWritable:单字节数值
	DoubleWritable:双字节数值
	FloatWritable:浮点数
	IntWritable:整型数
	LongWritable:长整型数
	Text:使用UTF8格式存储的文本
	NullWritable:当中的key或value为空时使用

FileInputFormat切片机制
	1.切片定义在InputFormat类中的getSplit()方法
	2.FileInputFormat中默认的切片机制:
	  (1)简单地按照文件的内容长度进行切片
	  (2)切片大小,默认等于block大小
	  (3)切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
		比如待处理数据有两个文件:
			file1.txt    320M
			file2.txt    10M
		经过FileInputFormat的切片机制运算后,形成的切片信息如下:  
			file1.txt.split1--  0~128M
			file1.txt.split2--  128~256M
			file1.txt.split3--  256~320M
			file2.txt.split1--  0~10M
	3.FileInputFormat中切片的大小的参数配置
	通过分析源码,在FileInputFormat中,计算切片大小的逻辑:Math.max(minSize, Math.min(maxSize, blockSize));  切片主要由这几个值来运算决定
		minsize:默认值:1  
			配置参数: mapreduce.input.fileinputformat.split.minsize    
		maxsize:默认值:Long.MAXValue  
			配置参数:mapreduce.input.fileinputformat.split.maxsize
		因此,默认情况下,切片大小=blocksize
		maxsize(切片最大值):
			参数如果调得比blocksize小,则会让切片变小,而且就等于配置的这个参数的值
		minsize (切片最小值):
			参数调的比blockSize大,则可以让切片变得比blocksize还大
	选择并发数的影响因素:
		1.运算节点的硬件配置
		2.运算任务的类型:CPU密集型还是IO密集型
		3.运算任务的数据量
	注意
		1.如果有大量小文件,会产生大量的小切片,就会产生大量的maptask运行.
		解决:从源头上解决,文件合并后再上传;如果小文件实在合并不了,可以写一个MapReduce程序先对小文件进行合并;
			 可以用另一种InputForamt:CombinerInputFormat(它可以将多个文件划分到一个切片中)
		2.当切片切割剩下的长度<1.1splitSize是,就直接把直到文件末尾的范围都作为一个切片
Combiner
	1.combiner是MR程序中Mapper和Reducer之外的一种组件
	2.combiner组件的父类就是Reducer
	3.combiner和reducer的区别在于运行的位置:
		Combiner是在每一个maptask所在的节点运行
		Reducer是接收全局所有Mapper的输出结果
	4.combiner的意义就是对每一个maptask的输出进行局部汇总,以减小网络传输量
		具体实现步骤
			1.自定义一个combiner继承Reducer,重写reduce方法
			2.在job中设置:job.setCombinerClass(CustomCombiner.class)
	5.combiner能够应用的前提是不能影响最终的业务逻辑,而且,combiner的输出kv应该跟reducer的输入kv类型要对应起来
Shuffle
	1.mapreduce中,map阶段处理的数据如何传递给reduce阶段,是mapreduce框架中最关键的一个流程,这个流程就叫shuffle
	2.shuffle: 洗牌、发牌——(核心机制:数据分区,排序,缓存)
	3.具体来说:就是将maptask输出的处理结果数据,分发给reducetask,并在分发的过程中,对数据按key进行了分区和排序
	
	shuffle是MR处理流程中的一个过程,它的每一个处理步骤是分散在各个map task和reduce task节点上完成的,整体来看,分为3个操作
		1.分区partition
		2.Sort根据key排序
			排序(sort)如果你自定义了key的数据类型要求你的类一定是WriteableCompartor的子类,不想继承WriteableCompartor,
			至少实现Writeable,这时你就必须在job上设置排序比较器job.setSortCmpartorClass(MyCompartor.class);
			而MyCompartor.class必须继承RawCompartor的类或子类
		3.Combiner进行局部value的合并
		4.分组
			分组(group)分区时会调用分组器,把同一分区中的相同key的数据对应的value制作成一个iterable,并且会在sort。
			在job上设置分组器。Job.setGroupCompartorClass(MyGroup.class)MyGroup.class必须继承RawCompartor的类跟子类
	详细流程
		1.maptask收集我们的map()方法输出的kv对,放到内存缓冲区中(环形缓冲区)
		2.从内存缓冲区不断溢出本地磁盘文件(这个过程叫做spill),可能会溢出多个文件(超过80%开始溢出,另外20%内存可以继续写入数据)
		3.多个溢出文件会被合并成大的溢出文件
		4.在溢出过程中,及合并的过程中,都要调用partitoner进行分组和针对key进行排序
		5.reducetask根据自己的分区号,去各个maptask机器上取相应的结果分区数据
		6.reducetask会取到同一个分区的来自不同maptask的结果文件,reducetask会将这些文件再进行合并(归并排序)
		7.合并成大文件后,shuffle的过程也就结束了,后面进入reducetask的逻辑运算过程
		 (从文件中取出一个一个的键值对group,调用用户自定义的reduce()方法)
		注意:Shuffle中的缓冲区大小会影响到mapreduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快 
		  缓冲区的大小可以通过参数调整,  参数:io.sort.mb  默认100M
Mapreduce中的序列化
	Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,header,继承体系...),
	不便于在网络中高效传输;所以,hadoop自己开发了一套序列化机制(Writable),精简,高效
		
	代码验证两种序列化差异
		public class TestSeri {
			public static void main(String[] args) throws Exception {
				//定义两个ByteArrayOutputStream,用来接收不同序列化机制的序列化结果
				ByteArrayOutputStream ba = new ByteArrayOutputStream();
				ByteArrayOutputStream ba2 = new ByteArrayOutputStream();

				//定义两个DataOutputStream,用于将普通对象进行jdk标准序列化
				DataOutputStream dout = new DataOutputStream(ba);
				DataOutputStream dout2 = new DataOutputStream(ba2);
				ObjectOutputStream obout = new ObjectOutputStream(dout2);
				//定义两个bean,作为序列化的源对象
				ItemBeanSer itemBeanSer = new ItemBeanSer(1000L, 89.9f);
				ItemBean itemBean = new ItemBean(1000L, 89.9f);

				//用于比较String类型和Text类型的序列化差别
				Text atext = new Text("a");
				// atext.write(dout);
				itemBean.write(dout);

				byte[] byteArray = ba.toByteArray();

				//比较序列化结果
				System.out.println(byteArray.length);
				for (byte b : byteArray) {

					System.out.print(b);
					System.out.print(":");
				}

				System.out.println("-----------------------");

				String astr = "a";
				// dout2.writeUTF(astr);
				obout.writeObject(itemBeanSer);

				byte[] byteArray2 = ba2.toByteArray();
				System.out.println(byteArray2.length);
				for (byte b : byteArray2) {
					System.out.print(b);
					System.out.print(":");
				}
			}
		}
排序
	MR默认的排序规则如下
		1.如果key为封装int的IntWritable类型,那么MapReduce按照数字大小对key排序
		2.如果key为封装为String的Text类型,那么MapReduce按照字典顺序对字符串排序
	总结:
		排序是MR在运行过程中的必经步骤,是MR的天然特性。
		不同的数据类型,排序规则略有不同。
		我们可以利用MR的排序,完成我们的业务需求
partitioner
	Mapreduce中会将map输出的kv对,按照相同key分组,然后分发给不同的reducetask
	  默认的分发规则为:根据key的hashcode%reducetask数来分发
	
	实现
		如果要按照我们自己的需求进行分组,则需要改写数据分发(分组)组件Partitioner
		  自定义一个CustomPartitioner继承抽象类:Partitioner
		然后在job对象中,设置自定义partitioner: job.setPartitionerClass(CustomPartitioner.class)
数据倾斜问题
	数据倾斜在MapReduce编程模型中十分常见,用最通俗易懂的话来说,
	  数据倾斜无非就是大量的相同key被partition分配到一个分区里,造成了'一个人累死,其他人闲死'的情况,
	  这种情况是我们不能接受的,这也违背了并行计算的初衷,首先一个节点要承受着巨大的压力,
	  而其他节点计算完毕后要一直等待这个忙碌的节点,也拖累了整体的计算时间,可以说效率是十分低下的
	不同的数据字段可能的数据倾斜一般有两种情况:
		1.一种是唯一值非常少,极少数值有非常多的记录值(唯一值少于几千)
		2.一种是唯一值比较多,这个字段的某些值有远远多于其他值的记录数,但是它的占比也小于百分之一或千分之一
	解决方案
		1.增加jvm内存,这适用于第一种情况(唯一值非常少,极少数值有非常多的记录值(唯一值少于几千)),
		  这种情况下,往往只能通过硬件的手段来进行调优,增加jvm内存可以显著的提高运行效率。
		2.增加reduce的个数,这适用于第二种情况(唯一值比较多,这个字段的某些值有远远多于其他值的记录数,
		  但是它的占比也小于百分之一或千分之一),我们知道,这种情况下,最容易造成的结果就是大量相同key被partition到一个分区,
		  从而一个reduce执行了大量的工作,而如果我们增加了reduce的个数,这种情况相对来说会减轻很多,毕竟计算的节点多了,就算工作量还是不均匀的,那也要小很多
		3.自定义分区,这需要用户自己继承partition类,指定分区策略,这种方式效果比较显著
		4.重新设计key,有一种方案是在map阶段时给key加上一个随机数,有了随机数的key就不会被大量的分配到同一节点(小几率),待到reduce后再把随机数去掉即可
		5.使用combinner合并,combinner是在map阶段,reduce之前的一个中间阶段,在这个阶段可以选择性的把大量的相同key数据先进行一个合并,可以看做是local reduce,
		  然后再交给reduce来处理,这样做的好处很多,即减轻了map端向reduce端发送的数据量(减轻了网络带宽),
		  也减轻了map端和reduce端中间的shuffle阶段的数据拉取数量(本地化磁盘IO速率),推荐使用这种方法

你可能感兴趣的:(MAPREDUCE简介)