文章目录
一、MapReduce
1、什么是MapReduce
MapReduce是一个分布式运算程序的编程框架,用户开发“基于Hadoop的数据分析应用”的核心框架。
MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
2、核心编程思想
需求:以统计单词出现的次数为例(查询结果:a-p一个文件,q-z一个文件)
1 ) MapReduce运算程序一般需要分成2个阶段 :Map阶段和Reduce阶段
2 ) Map阶段的并发Map Task ,完全并行运行,互不相干
3 ) Reduce阶段的并发Reduce Task ,完全互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出
4 ) MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。
3、MapReduce进程
- MrAppMaster: 负责整个程序的过程调度及状态协调。
- Map Task: 负责Map阶段的整个数据处理流程。
- Reduce Task: Reduce阶段的整个数据流程
4、切片与MapTask
数据块: Block是HDFS物理上把数据分成一块一块。
数据切片: 数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。
- 一个Job的Map阶段并行度游客户端在提交Job时的切片数决定。
- 每一个Split切片分配一个MapTask并行实例处理
- 默认情况下,切片大小=BlockSzie
- 切片是不考虑数据集整体,而是逐个针对每一个文件单独切片
二、Hadoop序列化
1、序列化与反序列化
序列化: 把内存中的对象,转成字节序列(或其他数据传输协议) 以便于存储到磁盘(持久化)和网络传输。
反序列化: 将收到的字节序列(或其他数据传输协议) 或者是磁盘的持久化数据,转换成内存中的对象。
2、为什么要序列化
一般来说,“活的” 对象只生存在内存里,关机断电就没有了。而且“活的”
对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。然而序列化可以存储"活的”对象,可以将“活的”对象发送到远程计算机。
3、为什么不用Java的序列化
Java的序列化是一一个重量级序列化框架(Serializable) ,一一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header, 继承体系等), 不便于在网络中高效传输。所以,Hadoop自 己开发了一套序列化机制 (Writable) 。
4、Hadoop序列化特点:
(1)紧凑:效使用存储空间。.
(2)快速:读写数据的额外开销小。
(3)可扩展:随着通信协议的升级而可升级
(4)互操作:支持多语言的交互
三、MapReduce 原理分析
1、MapTask工作机制
(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会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
2、ReduceTask工作机制
(1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
(2)Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。
(3)Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
(4)Reduce阶段:reduce()函数将计算结果写到HDFS上。
3、Shuffle阶段
数据从Map输出到Reduce输入过程
环形缓冲区==> 环状数组,超出80M溢出写入硬盘
MapReduce整个过程可以概括为
输入阶段==>map阶段==>shuffle阶段==>reduce阶段==>输出阶段
1、输入文件分片,每一片都由一个MapTask来处理
2、Map输出的中间结果会先放在内存缓冲区中,这个缓冲区的大小默认是100M,当缓冲区中的内容达到80%时(80M)会将缓冲区的内容写到磁盘上。也就是说,一个map会输出一个或者多个这样的文件,如果一个map输出的全部内容没有超过限制,那么最终也会发生这个写磁盘的操作,只不过是写几次的问题。
3、从缓冲区写到磁盘的时候,会进行分区并排序,分区指的是某个key应该进入到哪个分区,同一分区中的key会进行排序,如果定义了Combiner的话,也会进行combine操作
4、如果一个map产生的中间结果存放到多个文件,那么这些文件最终会合并成一个文件,这个合并过程不会改变分区数量,只会减少文件数量。例如,假设分了3个区,4个文件,那么最终会合并成1个文件,3个区
5、以上只是一个map的输出,接下来进入reduce阶段
6、每个reducer对应一个ReduceTask,在真正开始reduce之前,先要从分区中抓取数据
7、相同的分区的数据会进入同一个reduce。这一步中会从所有map输出中抓取某一分区的数据,在抓取的过程中伴随着排序、合并。
8、reduce输出
四、核心功能描述
- Mapper
Mapper将输入键值对(key/value pair)映射到一组中间格式的键值对集合。
Map是一类将输入记录集转换为中间格式记录集的独立任务。 这种转换的中间格式记录集不需要与输入记录集的类型一致。一个给定的输入键值对可以映射成0个或多个输出键值对。
Hadoop Map/Reduce框架为每一个InputSplit产生一个map任务,而每个InputSplit是由该作业的InputFormat产生的
需要多少个Map?
Map的数目通常是由输入数据的大小决定的,一般就是所有输入文件的总块(block)数。
Map正常的并行规模大致是每个节点(node)大约10到100个map,对于CPU 消耗较小的map任务可以设到300个左右。由于每个任务初始化需要一定的时间,因此,比较合理的情况是map执行的时间至少超过1分钟。
这样,如果你输入10TB的数据,每个块(block)的大小是128MB,你将需要大约82,000个map来完成任务,除非使用 setNumMapTasks(int)(注意:这里仅仅是对框架进行了一个提示(hint),实际决定因素见这里)将这个数值设置得更高。
- Reducer
Reducer将与一个key关联的一组中间数值集归约(reduce)为一个更小的数值集。
用户可以通过 JobConf.setNumReduceTasks(int)设定一个作业中reduce任务的数目。
概括地说,对Reducer的实现者需要重写 JobConfigurable.configure(JobConf)方法,这个方法需要传递一个JobConf参数,目的是完成Reducer的初始化工作。然后,框架为成组的输入数据中的每个<key, (list of values)>对调用一次 reduce(WritableComparable, Iterator, OutputCollector, Reporter)方法。之后,应用程序可以通过重写Closeable.close()来执行相应的清理工作。
Reducer有3个主要阶段:shuffle、sort和reduce。
Shuffle
Reducer的输入就是Mapper已经排好序的输出。在这个阶段,框架通过HTTP为每个Reducer获得所有Mapper输出中与之相关的分块。
Sort
这个阶段,框架将按照key的值对Reducer的输入进行分组 (因为不同mapper的输出中可能会有相同的key)。
Shuffle和Sort两个阶段是同时进行的;map的输出也是一边被取回一边被合并的。
Reduce
在这个阶段,框架为已分组的输入数据中的每个 <key, (list of values)>对调用一次 reduce(WritableComparable, Iterator, OutputCollector, Reporter)方法。
Reduce任务的输出通常是通过调用 OutputCollector.collect(WritableComparable, Writable)写入 文件系统的。
应用程序可以使用Reporter报告进度,设定应用程序级别的状态消息,更新Counters(计数器),或者仅是表明自己运行正常。
Reducer的输出是没有排序的
- 作业配置Job Conf
五、World Count 代码实现
1、流程
2、配置依赖
pom.xml
<hadoop.version>2.6.0</hadoop.version>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>${hadoop.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-common</artifactId>
<version>${hadoop.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>${hadoop.version}</version>
</dependency>
3、类的实现
Mapper类
/**
* 参数:
* LongWritable 第几行的行号
* Text 每一行读出的数据 因为 string不能改变大小 拼接序列化比较麻烦(比如拼接int) 所以新写入的一个类,底层是StringBuffer
* Text 最后输出的每一个 key
* IntWritable 输出的每一个key的个数
*/
public class WCMapper extends Mapper<LongWritable,Text,Text, IntWritable> {
private Text out = new Text();
//每个单词传输过来的值都为 1
private IntWritable one = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context ctx) throws IOException, InterruptedException {
//分割单词
String[] split = value.toString().split(" ");
for (String word : split) {
out.set(word);//转成 text 输出
ctx.write(out,one);//每个词都是1
}
}
}
OutPutCollector 将收集到的(k,v)写入到环形缓冲区,然后由缓冲区写到磁盘上。默认的缓冲区大小是100M,溢出的百分比是0.8,也就是说当缓冲区中达到80M的时候就会往磁盘上写。如果map计算完成后的中间结果没有达到80M,最终也是要写到磁盘上的,因为它最终还是要形成文件。
在spill溢出前,会对数据进行分区和排序,即在缓冲区对每个(k,v)键值对hash一个partition值,值相同则在同一个区,同一分区根据key来排序。不同分区在缓冲区根据partition和key值进行排序。
多个溢出的文件再通过merge合并,采用归并排序,合并成一个大的分区且区内有序的溢出文件。
reduce task 根据自己的分区号,到各自的map task节点拷贝相同的partition的数据到reduce task磁盘工作目录,再通过merge归并排序成一个有序的大文件。
Reducer类
/**
* 参数:
* Text :传输过来的key
* IntWritable : 传输过来的个数
* Text : 统计的每一个key
* IntWritable : 统计好的个数
*/
public class WCReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
//分割后传来的key,每个key的值(按上一组的组数),传出去
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable num : values) {
sum += num.get();
}
context.write(key, new IntWritable(sum));
}
}
Job类
public class MyDemo {
public static void main(String[] args) throws Exception {
//使用计算框架
Job job = Job.getInstance(new Configuration());
// jar包开始的类
job.setJarByClass(MyDemo.class);
job.setJobName("wc"); //取个名字
//读文件 并用inputsplit的getSplit方法对文件进行分割生成逻辑区 根据逻辑区的大小开启对应数量的Map Task
FileInputFormat.addInputPath(job, new Path("hdfs://192.168.56.171:9000/data1/data.txt"));
//结果写入文件
FileOutputFormat.setOutputPath(job, new Path("hdfs://192.168.56.171:9000/my1"));
//设置对应的Mapper类
job.setMapperClass(WCMapper.class);
//设置Mapper类的输出的key和value的类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//设置对应的Reduce类
job.setReducerClass(WCReduce.class);
//设置Reduce类的输出key和value的类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//启动job
job.waitForCompletion(true);
}
}