1. 官网介绍:
Hadoop MapReduce is a software framework for easily writing applications which process vast amounts of data (multi-terabyte data-sets) in-parallel on large clusters (thousands of nodes) of commodity hardware in a reliable, fault-tolerant manner.
Hadoop MapReduce是一个易于编写应用程序的软件框架,该应用程序以一种可靠的、容错的方式,在大型的商品硬件集群(数千个节点)上并行处理大量数据(多tb数据集)。
2. 概念理解
MapReduce:分布式的、离线、并行的计算框架
MapReduce是一个分布式运算程序的编程框架,是用户开发“基于hadoop的数据分析应用”的核心框架;
MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并
发运行在一个Hadoop集群上;
与HDFS解决问题的原理类似,HDFS是将大的文件切分成若干小文件,然后将它们分别存储到集群中各个主机中。
同样原理,MapReduce是将一个复杂的运算切分成若个子运算,然后将它们分别交给集群中各个主机,由各个主机并行运算。
一个完整的MapReduce程序在分布式运行时有三类实例进程:
1、MRAppMaster:负责整个程序的过程调度及状态协调
2、MapTask:负责map阶段的整个数据处理流程
3、ReduceTask:负责reduce阶段的整个数据处理流程
3.为何使用MapReduce
海量数据在单机上处理因为硬件资源限制,无法胜任,而一旦将单机版程序扩展到集群来分布式运行,将极大增加程序的复杂度和开发难度。(程序员无法写),引入MapReduce框架后,开发人员可以将绝大部分工作集中在业务逻辑的开发上,而将分布式计算中的复杂性交由框架来处理。
4. MapReduce框架结构及核心运行机制
4.1 结构
-
Job :用户的每一个计算请求称为一个作业。
-
Task:每一个作业,都需要拆分开了,交由多个主机来完成,拆分出来的执行单位就是任务。
Task又分为如下三种类型的任务:
Map:负责map阶段的整个数据处理流程
Reduce:负责reduce阶段的整个数据处理流程
MRAppMaster:负责整个程序的过程调度及状态协调- Map Task
- map引擎
- 解析每条数据记录,传递给用户编写的map()
- 将map()输出数据写入本地磁盘(如果是map-only作业,则直接写入HDFS)
- Reduce Task
- Reduce引擎
- 从Map Task上远程读取输入数据
- 对数据排序
- 将数据按照分组传递给用户编写的reduce
- Map Task
4.2 MapReduce程序运行流程
1、 一个MR程序启动的时候,最先启动的是MRAppMaster,MRAppMaster启动后根据本次job的描述信息,计算出需要的Maptask实例数量,然后向集群申请机器启动相应数量的Maptask进程
2、 Maptask进程启动之后,根据给定的数据切片(哪个文件的哪个偏移量范围)范围进行数据处理,主体流程为:
a) 利用客户指定的Inputformat来获取RecordReader读取数据,形成输入KV对
b) 将输入KV对传递给客户定义的map()方法,做逻辑运算,并将map()方法输出的KV对收集到缓存
c) 将缓存中的KV对按照K分区排序后不断溢写到磁盘文件
3、 MRAppMaster监控到所有Maptask进程任务完成之后(真实情况是,某些Maptask进程处理完成后,就会开始启动Reducetask去已完成的Maptask处fetch数据),会根据客户指定的参数启动相应数量的Reducetask进程,并告知reducetask进程要处理的数据范围(数据分区)
4、 Reducetask进程启动之后,根据MRAppMaster告知的待处理数据所在位置,从若干台Maptask运行所在机器上获取到若干个Maptask输出结果文件,并在本地进行重新归并排序,然后按照相同key的KV为一个组,调用客户定义的reduce()方法进行逻辑运算,并收集运算输出的结果KV,然后调用客户指定的Outputformat将结果数据输出到外部存储
4.3 MapTask并行度决定机制
Maptask的并行度决定map阶段的任务处理并发度,进而影响到整个job的处理速度
那么,MapTask并行实例是否越多越好呢?其并行度又是如何决定呢?
4.3.1 MapTask并行度的决定机制
一个job的map阶段并行度由客户端在提交job时决定
而客户端对map阶段并行度的规划的基本逻辑为:
将待处理数据执行逻辑切片(即按照一个特定切片大小,将待处理数据划分成逻辑上的多个split),然后每一个split分配一个MapTask并行实例处理
4.3.2 FileInputFormat切片机制
-
1、切片定义在InputFormat类中的getSplit()方法
-
2、FileInputFormat中默认的切片机制:
a) 简单地按照文件的内容长度进行切片
b) 切片大小,默认等于block大小
c) 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
比如待处理数据有两个文件:
file1.txt 320M
file2.txt 10M经过FileInputFormat的切片机制运算后,形成的切片信息如下:
file1.txt.split1-- 0~128
file1.txt.split2-- 128~256
file1.txt.split3-- 256~320
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
因此,默认情况下,切片大小=blocksize
maxsize(切片最大值):
参数如果调得比blocksize小,则会让切片变小,而且就等于配置的这个参数的值
minsize (切片最小值):
参数调的比blockSize大,则可以让切片变得比blocksize还大但是,不论怎么调参数,都不能让多个小文件“划入”一个split
-
4、并发数的影响因素:
1、运算节点的硬件配置
2、运算任务的类型:CPU密集型还是IO密集型
3、运算任务的数据量
4.3.3 CombineFileInputFormat
切片机制:
1、能将多个小文件划分为一个切片
2、 能考虑数据本地性,尽可能将一个切片中的文件选择在同一台机器/同一个机架上
使用 : job.setInputFormatClass(CombineTextInputFormat.class)
CombineTextInputFormat.setMin
CombineTextInputFormat.setMax
4.4 ReduceTask并行度的决定
Reducetask的并行度同样影响整个job的执行并发度和执行效率,但与Maptask的并发数由切片数决定不同,Reducetask数量的决定是可以直接手动设置:
//默认值是1,手动设置为4
job.setNumReduceTasks(4);
如果数据分布不均匀,就有可能在reduce阶段产生数据倾斜
注意: reducetask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个reducetask
尽量不要运行太多的reduce task。对大多数job来说,最好rduce的个数最多和集群中的reduce持平,或者比集群的 reduce slots小。这个对于小集群而言,尤其重要。
5. MapReduce示例编写及编程规范
5.1 编程规范
(1)用户编写的程序分成三个部分:Mapper,Reducer,Driver(提交运行mr程序的客户端)
(2)Mapper的输入数据是KV对的形式(KV的类型可自定义)
(3)Mapper的输出数据是KV对的形式(KV的类型可自定义)
(4)Mapper中的业务逻辑写在map()方法中
(5)map()方法(maptask进程)对每一个<K,V>调用一次
(6)Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
(7)Reducer的业务逻辑写在reduce()方法中
(8)Reducetask进程对每一组相同k的<k,v>组调用一次reduce()方法
(9)用户自定义的Mapper和Reducer都要继承各自的父类
(10)整个程序需要一个Drvier来进行提交,提交的是一个描述了各种必要信息的job对象
5.2 wordcount示例编写
需求:在一堆给定的文本文件中统计输出每一个单词出现的总次数
(1)定义一个mapper类
//首先要定义四个泛型的类型
//keyin: LongWritable valuein: Text
//keyout: Text valueout:IntWritable
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
//map方法的生命周期: 框架每传一行数据就被调用一次
//key : 这一行的起始点在文件中的偏移量
//value: 这一行的内容
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//拿到一行数据转换为string
String line = value.toString();
//将这一行切分出各个单词
String[] words = line.split(" ");
//遍历数组,输出<单词,1>
for(String word:words){
context.write(new Text(word), new IntWritable(1));
}
}
}
(2)定义一个reducer类
//生命周期:框架每传递进来一个kv 组,reduce方法被调用一次
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
//定义一个计数器
int count = 0;
//遍历这一组kv的所有v,累加到count中
for(IntWritable value:values){
count += value.get();
}
context.write(key, new IntWritable(count));
}
}
(3)定义一个主类,用来描述job并提交job
public class WordCountRunner {
//把业务逻辑相关的信息(哪个是mapper,哪个是reducer,要处理的数据在哪里,输出的结果放哪里……)描述成一个job对象
//把这个描述好的job提交给集群去运行
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job wcjob = Job.getInstance(conf);
//指定我这个job所在的jar包
// wcjob.setJar("/home/hadoop/wordcount.jar");
wcjob.setJarByClass(WordCountRunner.class);
wcjob.setMapperClass(WordCountMapper.class);
wcjob.setReducerClass(WordCountReducer.class);
//设置我们的业务逻辑Mapper类的输出key和value的数据类型
wcjob.setMapOutputKeyClass(Text.class);
wcjob.setMapOutputValueClass(IntWritable.class);
//设置我们的业务逻辑Reducer类的输出key和value的数据类型
wcjob.setOutputKeyClass(Text.class);
wcjob.setOutputValueClass(IntWritable.class);
//指定要处理的数据所在的位置
FileInputFormat.setInputPaths(wcjob, "hdfs://hdp-server01:9000/wordcount/data/big.txt");
//指定处理完成之后的结果所保存的位置
FileOutputFormat.setOutputPath(wcjob, new Path("hdfs://hdp-server01:9000/wordcount/output/"));
//向yarn集群提交这个job
boolean res = wcjob.waitForCompletion(true);
System.exit(res?0:1);
}
6.MapReduce程序运行模式
6.1 本地运行模式
(1)Mapreduce程序是被提交给LocalJobRunner在本地以单进程的形式运行
(2)而处理的数据及输出结果可以在本地文件系统,也可以在hdfs上
(3)怎样实现本地运行?写一个程序,不要带集群的配置文件(本质是你的mr程序的conf中是否有mapreduce.framework.name=local以及yarn.resourcemanager.hostname参数)
(4)本地模式非常便于进行业务逻辑的debug,只要在Eclipse/Idea中打断点即可
如果在windows下想运行本地模式来测试程序逻辑,需要在windows中配置环境变量:
%HADOOP_HOME% = d:/hadoop-2.6.1
%PATH% = %HADOOP_HOME%\bin
并且要将d:/hadoop-2.6.1的lib和bin目录替换成windows平台编译的版本
6.2 集群运行模式
(1)将Mapreduce程序提交给yarn集群ResourceManager,分发到很多的节点上并发执行
(2)处理的数据和输出结果应该位于hdfs文件系统
(3)提交集群的实现步骤:
-
1、将程序打成JAR包,然后在集群的任意一个节点上用hadoop命令启动
$ hadoop jar wordcount.jar cn.itcast.bigdata.mrsimple.WordCountDriver inputpath outputpath -
2、直接在linux的eclipse中运行main方法
(项目中要带参数:mapreduce.framework.name=yarn以及yarn的两个基本配置) -
3、如果要在windows的Eclipse/Idea中提交job给集群,则要修改YarnRunner类
7.MapReduce核心原理
7.1 MapReduce的Shuffle机制
概述:
MapReduce中,map阶段处理的数据如何传递给reduce阶段,是MapReduce框架中最关键的一个流程,这个流程就叫shuffle;
Shuffle: 洗牌、发牌——(核心机制:数据分区,排序,缓存);
具体来说:就是将maptask输出的处理结果数据,分发给reducetask,并在分发的过程中,对数据按key进行了分区和排序;
7.1.1 主要流程
Shuffle缓存流程:
Shuffle是MR处理流程中的一个过程,它的每一个处理步骤是分散在各个Maptask和Reducetask节点上完成的,整体来看,分为3个操作:
1、分区partition
2、Sort根据key排序
3、Combiner进行局部value的合并
7.1.2 详细流程
一、Map端的Shuffle
Map端会处理输入数据并产生中间结果,这个中间结果会写到本地磁盘,而不是HDFS。每个Map的输出会先写到内存缓冲区中,当写入的数据达到设定的阈值时,系统将会启动一个线程将缓冲区的数据写到磁盘,这个过程叫做spill。
在spill写入之前,会先进行二次排序,首先根据数据所属的partition进行排序,然后每个partition中的数据再按key来排序。partition的目是将记录划分到不同的Reducer上去,以期望能够达到负载均衡,以后的Reducer就会根据partition来读取自己对应的数据。接着运行combiner(如果设置了的话),combiner的本质也是一个Reducer,其目的是对将要写入到磁盘上的文件先进行一次处理,这样,写入到磁盘的数据量就会减少。最后将数据写到本地磁盘产生spill文件(spill文件保存在{mapred.local.dir}指定的目录中,Map任务结束后就会被删除)。
最后,每个Map任务可能产生多个spill文件,在每个Map任务完成前,会通过多路归并算法将这些spill文件归并成一个文件。至此,Map的Shuffle过程就结束了。
二、Reduce端的Shuffle
Reduce端的Shuffle主要包括三个阶段,copy、sort(merge)和reduce。
首先要将Map端产生的输出文件拷贝到Reduce端,但每个Reducer如何知道自己应该处理哪些数据呢?因为Map端进行partition的时候,实际上就相当于指定了每个Reducer要处理的数据(partition就对应了Reducer),所以Reducer在拷贝数据的时候只需拷贝与自己对应的partition中的数据即可。每个Reducer会处理一个或者多个partition,但需要先将自己对应的partition中的数据从每个Map的输出结果中拷贝过来。
接下来就是sort阶段,也成为merge阶段,因为这个阶段的主要工作是执行了归并排序。从Map端拷贝到Reduce端的数据都是有序的,所以很适合归并排序。最终在Reduce端生成一个较大的文件作为Reduce的输入。最后就是Reduce过程了,在这个过程中产生了最终的输出结果,并将其写到HDFS上。
7.2 Job提交流程
1.客户端提交作业给ResourceManager
2.ResourceManager返回给客户端jobid,存储路径(HDFS上的路径)信息
3.客户端将job.jar、job.xml、job.split、job.splitinfo等信息上传到存储路径(HDFS上的路径)
4.客户端通知ResourceManager可以启动job
5.ResourceManager将job加入到job队列当中,等待NodeManager领取job,然后NodeManager启动container,job.jar、job.xml、job.split、job.splitinfo等信息下载到container内,将客户端发出命令启动AppMaster
6.AppMaster计算资源,向ResourceManager请求Maptask的资源
7.ResourceManager分配资源(container),下载job.jar到container内,AppMaster启动Maptask(yarnchild)
8.Maptask执行完成,通知AppMaster,然后释放Maptask资源,AppMaster向ResourceManager申请Reducetask的资源,
9.ResourceManager分配资源(container),下载job.jar到container内,AppMaster启动Reducetask(yarnchild)
10.Reducetask执行完成,通知AppMaster,然后释放Reducetask资源。AppMaster通知ResourceManager。AppMaster释放资源。
7.3 MapReduce中的序列化
7.3.1 概述
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,header,继承体系。。。。),不便于在网络中高效传输;
所以,hadoop自己开发了一套序列化机制(Writable),精简,高效
7.3.2 自定义对象实现MR中的序列化接口
如果需要将自定义的bean放在key中传输,则还需要实现comparable接口,因为mapreduce框中的shuffle过程一定会对key进行排序,此时,自定义的bean实现的接口应该是:
public class FlowBean implements WritableComparable
需要自己实现的方法是:
/**
* 反序列化的方法,反序列化时,从流中读取到的各个字段的顺序应该与序列化时写出去的顺序保持一致
*/
@Override
public void readFields(DataInput in) throws IOException {
upflow = in.readLong();
dflow = in.readLong();
sumflow = in.readLong();
}
/**
* 序列化的方法
*/
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upflow);
out.writeLong(dflow);
//可以考虑不序列化总流量,因为总流量是可以通过上行流量和下行流量计算出来的
out.writeLong(sumflow);
}
@Override
public int compareTo(FlowBean o) {
//实现按照sumflow的大小倒序排序
return sumflow>o.getSumflow()?-1:1;
}
7.4 MapReduce过程中可干预的组件
自定义分区:
1、该类需要继承Partitioner
2、Partitioner<Text, IntWritable>的key-value类型需要map的输出key-value相同
3、重写getPartition()方法,该方法只能返回int类型
4、numPartitions的值需要和reduce的task数量相等
5、默认使用HashPartitioner
自定义的输入输出格式 InputFormat
InputFormat
Hadoop内置的输入文件格式类有:
1、FileInputFormat<K,V>这个是基本的父类,我们自定义就直接使用它作为父类;
2、TextInputFormat<LongWritable,Text>这个是默认的数据格式类,我们一般编程,如果没有特别指定的话,一般都使用的是这个;key代表当前行数据距离文件开始的距离,value代码当前行字符串;
3、SequenceFileInputFormat<K,V>这个是序列文件输入格式,使用序列文件可以提高效率,但是不利于查看结果,建议在过程中使用序列文件,最后展示可以使用可视化输出;
4、KeyValueTextInputFormat<Text,Text>这个是读取以Tab(也即是\t)分隔的数据,每行数据如果以\t分隔,那么使用这个读入,就可以自动把\t前面的当做key,后面的当做value;
5、CombineFileInputFormat<K,V>合并大量小数据是使用;
6、MultipleInputs,多种输入,可以为每个输入指定逻辑处理的Mapper
自定义combiner:
1、需要继承reducer,Reducer<Text, IntWritable, Text, IntWritable>
需要和map端的输出的key-value类型一致,然后还需要和reduce的key-value类型一致
2、它本身是一种mr的优化,但是并不一定都是优化。(key相同较多效率较高)
3、有没有combiner不能影响最终的输出结果。
4、如果使用自定义的Reducer来作为Combiner类,需要将map端的输出需要和reduce端的输出类型保持一致
自定义数据类型:
1、需要实现Writable 或者 writableComparable()
2、需要实现write(DataOutput out) 和 readFields(DataInput in)
3、readFields()和wirte()方法中的属性类型和顺序都要一一对应
4、toString() 、 hashCode() 、 equals() 随便
自定义分组比较器:
job.setGroupingComparatorClass(MyGroupComparator.class);
1、需要实现RawComparator
2、需要重写两个比较方法对象比较 和 字节比较
3、重写字节比较中的方法时,需要注意属性的类型的字节数要与之相等。
4、默认使用对象中的第一个属性的值进行分组
7.6 MapReduce中常见的join
MapReduce中常见的join有三种:map端的join、reduce端的join、semi join
reduce端的join:
核心思想:
在map端将来源于不同的数据或者有不同用处的数据打标记输出,以便在reduce端能识别并进行关联查找。
适用场景:所有表都是大表时(几乎所有的业务逻辑都满足)
优点:解决业务的范围比较广
缺点:从map端输出到reduce端的数据量比较大,且无效数据比较多,大大的增加数据传输的时间,增加了shuffle的耗时,增加了关联查找的时间。
map端的join:
核心思想:将小表进行分布式缓存,然后在map端取出缓存中的数据来进行关联查询。
适用场景:大表和小表同时存在(必须要有一个小表)
优点:从缓存中读取数据,然后在map端直接关联查询,减少map端到reduce端的数据传输
缺点:只适合有小表的业务需求。
semi join:
核心思想:将大表的有效数据提取出来,然后进行缓存,然后再使用map端的join
适用场景:都是大表(且最少有一个大表中的有效数据较小可以缓存)
优点:可以将reduce-side join 变成map-side 。
缺点:需要提前过滤大表中的有效数据并进行缓存。