MapReduce
1. 概述
MapReduce 是一个分布式运算程序的编程框架,是用户开发“基于 Hadoop 的数据分析应用”的核心框架。
MapReduce 核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个 Hadoop 集群上。
优点:
1)MapReduce 易于编程
它简单的实现一些接口,就可以完成一个分布式程序。也就是说你写一个分布式程序,跟写一个简单的串行程序是一模一样的。用户只需要关心业务逻辑,实现一些简单的接口就可以了
2)良好的扩展性
可以动态增加服务器,解决计算资源不够的问题
3)高容错性
MapReduce 设计的初衷就是使程序能够部署在廉价的 PC 机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由 Hadoop 内部完成的。
4)适合 PB 级以上海量数据的离线处理
可以实现上千台服务器集群并发工作,提供数据处理能力。
缺点:
1)不擅长实时计算
MapReduce 无法像 MySQL 一样,在毫秒或者秒级内返回结果。(MapReduce基于磁盘)
2)不擅长流式计算
流式计算的输入数据是动态的,而 MapReduce 的输入数据集是静态的,不能动态变化。 这是因为 MapReduce 自身的设计特点决定了数据源必须是静态的。 (Spark Streaming、Flink擅长)
3)不擅长 DAG(有向无环图)计算
即多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。
在这种情况下,MapReduce 并不是不能做,而是使用后,因为每个 MapReduce 作业的输出结果都会写入到磁盘,会造成大量的磁盘 IO,导致性能非常的低下。 (Spark擅长)
1.1 MapReduce 进程
一个完整的MapReduce 程序在分布式运行时有三类实例进程:
(1)MrAppMaster:负责整个程序的过程调度及状态协调。 (MRAppMaster是MapReduce的ApplicationMaster实现)
(2)MapTask:负责 Map阶段的整个数据处理流程。
(3)ReduceTask:负责 Reduce 阶段的整个数据处理流程。
注意:MapTask和ReduceTesk都是进程级别的! 进程名是Yarnchild;
2. 序列化
定义:
序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输。
反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。
为什么要序列化?
不同节点要通过网络进行传输,网络传输就必须是字节流!所以要通过序列化将对象转化为字节流再进行传输;
传过去之后反序列化才能够使用对象;
为什么不用 Java 的序列化?
Java 的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。
所以, Hadoop 自己开发了一套序列化机制(Writable)。
注意:
Hadoop的序列化继承Writable,而Spark使用Kryo 轻量级序列化框架;
3. 核心框架原理
3.1 InputFormat 数据输入
默认使用的实现类是:TextInputFormat;
TextInputFormat 的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为key,行内容作为 value 返回。
切片与 MapTask 并行度决定机制
- 数据切片是 MapReduce 程序计算输入数据的单位,一个切片会对应启动一个 MapTask进程从而决定并行度。
- 切片的个数 = MapTesk进程个数
- 默认切片大小和BlockSize块大小(128mb)保持一致,效率最高;
- 不同的文件分开单独切片 !
注意:
- MapTask是并行执行!
- MapTask和ReduceTask都是进程级别,名字是
Yarnchild
;
提交job流程
- 建立连接,判断是本地模式还是集群模式;
- 客户端提交job给ResourceManager,ResourceManager返回job id和资源提交路径;
- Client提交jar包、切片信息和配置文件到指定的资源提交路径;
- Client提交完资源后,向ResourceManager申请运行MrAppMaster;
FileInputFormat 切片机制
- 每个文件单独切片;
- 默认情况下,切片大小=blocksize ;
- 根据blocksize来切片,每次都要判断切完剩下的部分是否大于块的1.1倍,如果剩下的小于1.1倍就不切了;
- 一个切片对应一个MapTask,MapTask是并行执行的!
- 一个MapTask需要约1g内存,一个cpu,开销较大;
- 切片公式:
computeSplitSize = Math.max(minSize, Math.min(maxSize, blockSize));
要使切片小于128,将 maxSize调小 。
要使切片大于128,将minSize调大。
问:块大小blockSize能不能调整?
答:不能!块是实实在在存储在物理储存上的
CombineTextInputFormat切片机制
框架默认的 TextInputFormat 切片机制是每个文件单独切片,都会交给一个 MapTask,(一个切片对应一个MapTask),这样如果有大量小文件,就会产生大量的MapTask,一个MapTask需要约1g内存,一个cpu,开销较大,处理效率极其低下。
CombineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中;
通过设置切片最大值:
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); // 4mb
让多个小文件合并为少量的切片;
3.2 MapTask 工作流程
0)提交Job:
1.客户端提交job给ResourceManager,ResourceManager返回job id和资源提交路径;
2.Client提交jar包、切片信息和配置文件到指定的资源提交路径;
3.Client提交完资源后,向ResourceManager 申请 运行MrAppMaster(整个任务运行的资源节点);
1)Read阶段:
4.MrAppMaster开启;
5.MrappMaster会读取客户端提供的信息,主要是job.split 切片信息,会根据切片个数开启对应个数的MapTask;
6.MapTask启动后,使用TextInputFormat的recodReader读取文件;
2)Map阶段:
7.recordReader读完之后将K、V返回给Mapper (mapper里面是业务逻辑);
8.Map逻辑完了之后,通过context.write进行collect收集,默认再使用HashPartitioner进行分区处理;
3)collect + shuffle:
9.数据从Mapper输出,先getPartition进行分区(默认是对key取hashCode然后对ReduceTask数量取模,分区个数对应ReduceTask个数)并标记分区,然后到【内存中】的outputCollector环形缓冲区。环形缓冲区的作用是为了批量收集Map的结果(环形缓冲区默认大小100M)
10.当缓冲区的数据达到80%的时候,将数据溢写到磁盘—【溢写之前】快速排序(shuffle第一次排序);
11.按照分区溢写,【溢写后】产生多个溢写文件,使用归并排序保证分区内有序(Shuffle阶段第二次排序),等待reduceTask来拉取;
3.3 ReduceTask 工作流程
1)拉取阶段+merge
1.ReduceTask会从MapTask对应的分区通过HTTP方式拉取分区数据 ,拉过来之后先放在内存,内存不够再溢写到磁盘上;
(切片对应MapTask ,分区对应ReduceTask)
2.数据拉取过来后进行归并排序,形成ReduceTask内的全局排序的文件;
2)reduce + output
3.reducer进行业务逻辑处理;
4.使用outputformat将reducer的数据写到HDFS;
3.4 shuffle工作流程
- 数据通过context.write收集,经过getpartition标记分区,因为数据后续处理都是按照分区来处理的;
- 【将数据标记分区后】,数据进入环形缓冲区,默认100mb;达到80%就开始溢写到磁盘;
- 【溢写前】快速排序;
- 【溢写后】归并排序,保证分区内有序;
- 可选Combine预聚合,减少数据量;
- ReduceTask根据分区从MapTask拉取数据,再进行归并排序;
3.5 Combiner预聚合(可选)
作用:
1.Map数量一般多于Reduce,而使用Combine可以减少数据量,Combiner对每一个MapTask的输出进行局部汇总,以减小网络传输量以提高效率;
2.可以减少数据倾斜 !
哪里可以使用 Combiner ?
Combiner的输出是Reducer的输入,即对MapTask的输出结果进行汇总,要在【ReduceTask拉取数据之前】使用;
使用 Combiner 的前提?
Combiner 绝不能改变最终的计算结果!
Combiner 只应该用于那种 Reduce 的输入 key/value与输出 key/value 类型完全一致,且不影响最终结果的场景
3.6 分区
目的:
把不同数据输出到不同reduceTask 最终到输出不同文件中;
默认分区原则:
按照key的hashCode % reduceTask 数量 = 分区号;
源码:
注意事项:
-
ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致;
-
ReduceTask 默认值 就是1,所以输出文件个数为一个;
-
如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜;
-
ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个ReduceTask;
-
如果分区数不是1,但是ReduceTask为1,是否执行分区过程?
答案是:不执行分区过程,因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1。不大于1,则默认不会走getPartition方法,所有数据写到0分区。
3.7 Map Join
当数据量非常庞大时,如果都在Reduce端进行处理,那么资源不够用!则可以在Map阶段进行表的合并。
1)使用场景
Map Join 适用于一张表十分小、一张表很大的场景。 (将小表缓存到内存当中)
2)优点
思考:在 Reduce 端处理过多的表,非常容易产生数据倾斜。怎么办?
在 Map 端缓存多张表,提前处理业务逻辑,这样增加 Map 端业务,减少 Reduce 端数据的压力,尽可能的减少数据倾斜。
3)具体办法:采用 DistributedCache
①在 Mapper 的 setup 阶段,将小表文件读取到缓存集合中。
②在 Driver 驱动类中加载缓存:
//缓存普通文件到 Task 运行节点。
job.addCacheFile(new URI(“file:///e:/cache/pd.txt”));
//如果是集群运行,需要设置 HDFS 路径
job.addCacheFile(new URI(“hdfs://hadoop102:8020/cache/pd.txt”));
4. 压缩格式
写多读少—优先考虑压缩比,用Gzip
写少读多—优先考虑速度,用Snappy、Lzo
-
Snappy
不支持切片;
速度快 -
Gzip
不支持切片;
压缩比高,
速度慢 -
Lzo
支持切片;
速度快;
压缩率一般; -
Bzip2
支持切片;
压缩率高;
速度慢;
压缩选择位置:
输入端:
当文件小于块大小128mb的时候就不用切片,所以不考虑切片性能。(默认情况下,切片大小=blocksize )
Mapper输出:
跨服务器通信,不需要切片!考虑速度即可。
Reduce输出端:
取决于需求,即输出数据在哪里用。