文章目录
- 一、MapReduce基础入门
- 1.为什么要MapReduce
- 2.MapReduce优缺点
- 3.MapReduce进程结构
- 4.MapReduce程序运行流程分析
- 二、MapReduce框架原理
- 1.工作流程
- 2.InputFormat
- 3.MapTask
- 4.Combiner
- 5.Shuffle
- 6.ReduceTask
- 7.OutputFormat
一、MapReduce基础入门
- MapReduce是一个分布式运算程序的编程框架,是用户开发“基于hadoop的数据分析应用”的核心框架;
- MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
1.为什么要MapReduce
- 海量数据在单机上处理因为硬件资源限制,无法胜任。
- 而一旦将单机版程序扩展到集群来分布式运行,将极大增加程序的复杂度和开发难度。
- 引入MapReduce框架后,开发人员可以将绝大部分工作集中在业务逻辑的开发上,而将分布式计算中的复杂性交由框架来处理,进而提高开发效率。
2.MapReduce优缺点
优点
- 易于编程
它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的 PC 机器运行。也就是说你写一个分布式程序,跟写一个简单的串行程序是一模一样的。 就是因为这个特点使得 MapReduce 编程变得非常流行。 - 扩展性强
当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。 - 高容错性
MapReduce 设计的初衷就是使程序能够部署在廉价的 PC 机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上面上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由 Hadoop 内部完成的。 - 高吞吐量
适合 PB 级以上海量数据的离线处理。这里加红字体离线处理,说明它适合离线处理而不适合在线处理。比如像毫秒级别的返回一个结果,MapReduce 很难做到。
缺点
- 不适合实时计算
MapReduce 无法像 Mysql 一样,在毫秒或者秒级内返回结果。 - 不适合流式计算
流式计算的输入数据时动态的,而 MapReduce 的输入数据集是静态的,不能动态变化。这是因为 MapReduce 自身的设计特点决定了数据源必须是静态的。 - 不适合DAG(有向图)计算
多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce 并不是不能做,而是使用后,每个MapReduce 作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下。
3.MapReduce进程结构
一个完整的mapreduce程序在分布式运行时有三类实例进程:
- MRAppMaster:负责整个程序的过程调度及状态协调
- mapTask:负责map阶段的整个数据处理流程
- ReduceTask:负责reduce阶段的整个数据处理流程
4.MapReduce程序运行流程分析
1.在MapReduce程序读取文件的输入目录上存放相应的文件。
2.客户端程序在submit()方法执行前,获取待处理的数据信息,然后根据集群中参数的配置形成一个任务分配规划。
3.客户端提交job.split、jar包、job.xml等文件给yarn,yarn中的ResourceManager启动MRAppMaster。
4.MRAppMaster启动后根据本次job的描述信息,计算出需要的maptask实例数量,然后向集群申请机器启动相应数量的maptask进程。5.maptask利用客户指定的inputformat来读取数据,形成输入KV对。
6.maptask将输入KV对传递给客户定义的map()方法,做逻辑运算。
7.map()运算完毕后将KV对收集到maptask缓存。
8.maptask缓存中的KV对按照K分区排序后不断写到磁盘文件。9.MRAppMaster监控到所有maptask进程任务完成之后,会根据客户指定的参数启动相应数量的reducetask进程,并告知reducetask进程要处理的数据分区。
10.Reducetask进程启动之后,根据MRAppMaster告知的待处理数据所在位置,从若干台maptask运行所在机器上获取到若干个maptask输出结果文件,并在本地进行重新归并排序,然后按照相同key的KV为一个组,调用客户定义的reduce()方法进行逻辑运算。
11.Reducetask运算完毕后,调用客户指定的outputformat将结果数据输出到外部存储。
二、MapReduce框架原理
1.工作流程
(1)split阶段
- 首先MapReduce会根据要运行的大文件来进行split,每个输入分片(input split)针对一个map任务,输入分片(input split)存储的并非数据本身,而是一个分片长度和一个记录数据位置的数组。
- 通过InputFormat指定读入规则(默认TextInputFormat),调用RecordReader中的read()方法按行读
将行偏移量(行号)和这行内容组成一个文件片,以此内容作为用户自定义的Mapper类中map()方法的入口数据 - 输入分片(input split)通常和HDFS的块(block)关系很密切,HDFS(Hadoop2.x版本)的默认块大小是128MB,如果运行的大文件是128x10MB,MapReduce会分为10个MapTask,每个MapTask都尽可能运行在块(block)所在的DataNode上,体现了移动计算不移动数据的思想。
(2)map阶段
- 执行用户自定义的Mapper类中的map()方法,MapTask接受输入分片(input split),调用map()方法对入口数据进行处理,再通过context.write将处理后的信息以键值对的方式进行输出到OutputCollector中。
(3)Shuffle阶段
主要负责将map端生成的数据传递给reduce端
- OutputCollector将收集的键值对发送到环形缓冲区中,环形缓冲区的默认大小为100MB。
- 环形缓冲区的数据量累计达到80%时,会将数据溢出本地磁盘文件,如果文件较大,这个过程中可能会溢出多个小文件,并且每个小文件中的数据根据分区和键进行排序。
- 多个溢出文件再根据分区和键进行归并排序合并到一个大文件中。
- 当所有的MapTask完成任务后,ReduceTask启动。
(4)Reduce阶段
- 每一个ReduceTask根据自身的分区号从各个的MapTask节点上提取属于当前分区的数据。
- 数据收集完成之后,ReduceTask再根据键进行归并排序合并成一个大文件,大文件根据键有序。
- 调用GroupingComparator对大文件里的数据进行分组,从文件中取出一个一个的键值对group,调用用户自定义的reduce()方法进行逻辑处理。
- 最后通过OutputFormat(默认TextOutputFormat)方法将结果数据写到part-r-000**结果文件中。
Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上缓冲区越大,磁盘IO次数就越少,执行速度就越快。
缓冲区的大小可以通过在mapred-site.xml中设置mapreduce.task.io.sort.mb的值来改变,默认为100M。
2.InputFormat
- InputFormat 的主要功能就是确定每一个 MapTask 需要读取哪些数据以及如何读取数据的问题
- 每一个 map 读取哪些数据由 InputSplit(数据切片)决定,如何读取数据由 RecordReader 来决定。
- InputFormat 实现类有很多,开发比较常用的是文件类型(FileInputFormat)和数据库类型(DBInputFormat),这里主要用到的是 FileInputFormat 。
FileInputFormat切片机制
- 计算切片大小:Math.max(minSize, Math.min(maxSize, blockSize))
默认情况下,splitsize=blocksize
minsize(切片最小值)
默认值:1
配置参数: mapreduce.input.fileinputformat.split.minsize
若minsize>blocksize,则splitsize=minsize
maxsize(切片最大值)
默认值:Long.MAXValue
配置参数:mapreduce.input.fileinputformat.split.maxsize
若maxsize<blocksize,则splitsize=maxsize
- 冗余机制:每次切片时,都要判断剩余部分的内容大小,若小于块的1.1倍则划分到一块切片。
- 切片信息会写到一个切片规划文件中。
- 整个切片的核心过程在 FileInputFormat 类中的 getSplit() 方法中完成。
- 数据切片只是在逻辑上对输入数据进行分片,并不会在磁盘上将其切分成分片进行存储。InputSplit 只记录了分片的元数据信息,比如起始位置、长度以及所在的节点列表等。
- 提交切片规划文件到 yarn 上,MRAppMaster根据切片规划文件计算开启MapTask个数。
- 获取切片信息 API,可以使用 MapTask 上下文对象获取切片信息。
// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
// 获取切片的文件名称
String name = inputSplit.getPath().getName();
FileInputFormat 默认切片规则:
(1)简单地按照文件的内容长度进行切片
(2)切片大小,默认等于 block 大小
(3)切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
选择并发数的影响因素:
(1)运算节点的硬件配置
(2)运算任务的类型:CPU密集型还是IO密集型
(3)运算任务的数据量
3.MapTask
一个 job 的 map 阶段 MapTask 并行度(个数),由客户端提交 job 时的切片个数决定。
4.Combiner
- Combiner 是MR程序中 Mapper 和 Reducer 之外的一种组件,其父类就是 Reducer 。
- Combiner 是在每一个 MapTask 所在的节点运行,Reducer 是接收全局所有 Mapper 的输出结果。
- Combiner 的作用就是对每一个 MapTask 的输出进行局部汇总,以减少网络传输量和磁盘IO流的读写。
- Combiner 能够应用的前提是不能影响最终的业务逻辑,并且 Combiner 的输出 KV 要跟 Reducer 的输入 KV 类型对应起来
- 自定义 Combiner 步骤:
1.自定义一个类继承 Reducer,重写 reduce() 方法
public class WordcountCombiner extends Reducer<Text, IntWritable, Text, IntWritable>{
@Override
protected void reduce(Text key, Iterable<IntWritable> values,Context context)
throws IOException,InterruptedException {
......
}
}
2.在 job 驱动类中设置:
job.setCombinerClass(WordCountCombiner.class);
5.Shuffle
Partition 分区
- 默认 Partition 分区:
//默认分区是根据 key 的 hashCode 对 ReduceTasks 个数取模得到的,用户没法控制哪个 key 存储到哪个分区。
public class HashPartitioner<K, V> extends Partitioner<K, V> {
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
- 自定义 Partition 步骤:
1.自定义类继承 Partitioner,重写 getPartition() 方法
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text key, FlowBean value, int numPartitions) {
......
}
}
2.在 job 驱动类中设置:
job.setPartitionerClass(CustomPartitioner.class);
3.自定义 partition 后,要根据自定义 partitioner 的逻辑设置相应数量的 ReduceTasks:
job.setNumReduceTasks(5);
如果 Reduce 任务数 > 分区数,则会多产生几个空的输出文件part-r-000xx;
如果1 < Reduce 任务数 < 分区数,则有一部分分区数据无处安放,会Exception;
如果 Reduce 任务数 = 1,则不管 Map 端输出多少个分区文件,最终结果都交给一个 ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
6.ReduceTask
一个 job 的 reduce 阶段 ReduceTask 并行度(个数),可以直接手动设置。
//默认值是1,手动设置为4
job.setNumReduceTasks(4);
7.OutputFormat
- OutputFormat 是 MapReduce 输出的基类,所有 MapReduce 输出都实现了 OutputFormat 接口。
- 默认的输出格式是 TextOutputFormat,它把每条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat 调用 toString()方法把它们转换为字符串。