MapReduce学习笔记
总感觉自己学一点忘一点,所以想通过博客来加深自己的印象,想对自己所学的知识做一个总结,如果有书写不对的地方,请各位见谅,并指出其中的错误之处,万分感激。
一、MapReduce基本概念
1、mapreduce的思想
MapReduce是Hadoop的三大组件之一,是一个分布式运算程序的编程模型,核心功能是将用户编写的业务逻辑代码和自带默认的组件,整合成一个完整的分布式运算程序,并发的运行在Hadoop的集群上。
MapReduce的核心思想是“分而治之”,使用于大量复杂的任务处理场景
Map负责“分”:把复杂的任务分解为若干个“简单的任务”,来并行的处理。可以进行拆分的前提的是这些小任务是并行计算的,彼此之间几乎没有依赖
Reduce负责“合”:即是对Map端的结果进行全局的汇总
这两个阶段的合并就是对MapReduce思想的具体体现,举个例子:我们要去数图书馆的所有书,你去数1号书架,我去数2号书架,这就是“Map”,然后我们把所有人的统计数累加在一起,这就是“Reduce”。
2、MapReduce的设计构思
1、如何处理大量的数据:分而治之
对互相间不具有依赖计算关系的大数据,实现并行最自然的办法就是采取分而治之的策略。但是并行计算的第一个重要问题就是如何划分计算任务或者计算数据以便对划分的子任务或者数据块同时进行计算。不可拆分的计算任务或者相互间互有依赖关系的数据是无法进行计算的。
2、构建抽象模型:Map和Reduce
MapReduce借助了函数式语言的编程思想,并且提供了Map和Reduce两个清晰的操作接口:
Map:对一组数据进行某种重复性的处理。
Reduce:对Map的中间结果进行结果的整合。
3、统一构架,隐藏系统层细节
如何提供统一的计算框架,如果没有统一封装底层细节,那么程序员则需要考虑诸如数据存储、划分、分发、结果收集、错误恢复等诸多细节;为此,MapReduce设计并提供了统一的计算框架,为程序员隐藏了绝大多数系统层面的处理细节。
MapReduce最大的亮点在于通过抽象模型和计算框架把需要做什么(what need to do)与具体怎么做(how to do)分开了,为程序员提供一个抽象和高层的编程接口和框架。程序员仅需要关心其应用层的具体计算问题,仅需编写少量的处理应用本身计算问题的程序代码。如何具体完成这个并行计算任务所相关的诸多系统层细节被隐藏起来,交给计算框架去处理:从分布代码的执行,到大到数千小到单个节点集群的自动调度使用。
3、mapreduce的框架结构
一个完整的mapreduce程序在分布式运行时有三类实例进程
- MRAppMaster:负责整个程序的过程调度以及状态的协调
- MapTask:负责map阶段的数据处理流程
- ReduceTask:负责reduce阶段的整个数据处理流程
Mapreduce基于yarn上的运行流程
二、MapReduce执行流程
这是我从官网找到的mapreduce的执行流程,但是有点抽象!不太好理解。
下图,为我根据官网的图片绘制的mapreduce的执行流程以及自己的理解,如果有书写不正确的地方,请各位大佬留下宝贵的建议。鉴于图片太大,因此分为了map图解和reduce图解。
Map阶段
1、InputFormat
首先,读取数据组件InputFormat(默认的是TextInputFormat)会通过getSplits的方法对输入目录中的文件信息逻辑切片,有多少个splits,就对应的启动多少个MapTask,split与block的关系是默认的一对一关系,将输入文件切分后,通过Recordreader对象(默认的是LineRedocrdReader)进行读取,将读取的值返回给map
FileInputFormat的切片机制:
A. 简单地按照文件的内容长度进行切片
B. 切片大小,默认等于block大小
C. 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
2、MapTask阶段
map读取split切分后返回的<key,value>的值,执行自己的逻辑代码,读取一行,调用一次RecordReader,处理完毕后的结果通过context.write的方法进行数据收集,把数据集放到一个collect的容器中,进行下一步的处理.
3、分区
在map端收集到的数据,会先对其进行分区处理,默认是对key hash后的值对reduce task数量取模后的值,这样做是为了平衡reduce的处理能力,分区过后,会将分区后的信息,以及数据信息写入到内存中,内存中的这块区域叫做环形缓冲区,通过批量的收集数据来减少磁盘对IO的影响.
环形缓冲区其实就是一个数组,数组中存放着key,value的序列化数据和key,value的元数据信息,包括partition的起始位置,value的起始位置,以及value的长度.
环形缓冲区的默认大小是100M,当map的输出结果很多的时候,很可能会撑爆内存,在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区继续读取数据,这个过程叫做spill(溢写),缓冲区有个默认的比例:当溢写线程启动后,会锁定80M的内存,执行溢写过程,空出来的20M会继续执行给内存中写数据,一直以这个比例循环的执行
4、排序
溢写启动后,会将80M的数据内的key进行排序,通过key的序列化字节默认的进行排序
序列化:把结构化的对象转化为字节流
反序列化:是序列化的逆过程,把字节流对象转化为对象
排序: return thisValue < thatValue ? -1 : (thisValue == thatValue ? 0 : 1); #this代表当前对象o1,that代表传参对象o2
返回正数的话,当前对象(调用compareTo方法的对象o1)要排在比较对象(compareTo传参对象o2)后面,返回负数的话,放在前面。
5、规约(是对mapreduce的优化,不影响任何结构,减少网络传输,默认可以省略)
每次溢写会在磁盘上生成一个临时文件(写之前判断是否有combiner),如果map的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个临时文件存在。当整个数据处理结束之后开始对磁盘中的临时文件进行merge合并,因为最终的文件只有一个,写入磁盘,并且为这个文件提供了一个索引文件,以记录每个reduce对应数据的偏移量。
Reduce阶段
6、分组
在分组之前会先有copy(拉取数据)与sort(全局的排序)两个阶段
copy阶段包含一个eventFetcher来获取已完成的map列表,由Fetcher线程去copy数据,在此过程中会启动两个merge线程,分别为inMemoryMerger和onDiskMerger,分别将内存中的数据merge到磁盘和将磁盘中的数据进行merge。
merge有三种形式:内存到内存;内存到磁盘;磁盘到磁盘。
默认的第一种方式不会启动当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的文件。
把分散的数据合并成一个大的数据后,还会再对合并后的数据排序。
7、ReduceTask阶段
对排序后的键值对调用reduce方法,键相等的键值对调用一次reduce方法,
8、TextOutPutFormat
每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到HDFS文件中。
三、MapReduce的入门案例(word count)
单词统计的数据
数据上传到HDFS上
编写WordCountRunner 代码
Driver代码
package wordcount;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.io.*;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
public class WordCountRunner {
public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(WordCountRunner.class);
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job,new Path("hdfs://hadoop01:9000/wordcount/wordcount.txt"));
job.setMapperClass(WordCountMap.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
job.setReducerClass(WordCountReduce.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
job.setOutputFormatClass(TextOutputFormat.class);
Path path = new Path("hdfs://hadoop01:9000/wordcount/output");
TextOutputFormat.setOutputPath(job,path);
//如果存在输出文件就将其删除
FileSystem fileSystem = FileSystem.get(new URI("hdfs://hadoop01:9000"), conf, "root");
if(fileSystem.exists(path)){
fileSystem.delete(path,true);
}
boolean b = job.waitForCompletion(true);
System.exit(b?0:1);
}
}
Map代码
package wordcount;
import org.apache.hadoop.io.*;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class WordCountMap extends Mapper<LongWritable,Text,Text,LongWritable> {
Text text = new Text();
LongWritable one = new LongWritable();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] split = value.toString().split("\t");
for (String word : split) {
text.set(word);
one.set(1);
context.write(text,one);
}
}
}
reduce代码
package wordcount;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.io.*;
import java.io.IOException;
public class WordCountReduce extends Reducer<Text,LongWritable,Text,LongWritable>{
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
long count = 0;
for (LongWritable value : values) {
count+=value.get();
}
context.write(key,new LongWritable(count));
}
}
将项目打包到hadoop中以jar包的方式运行
控制台输出如下
会在target目录下生成jar包
将jar包丢到$HADOOP_HOME$
/share/hadoop/mapreduce 下面
执行hadoop jar + jar包全名+wordcountRunner的全路径
控制台输出map和reduce的进度说明程序执行成功
我们将结果写出到了hdfs上面的/wordcount/out的输出目录,查看目录内容就会发现下面多了两文件,分别是_SUCCESS和part-r-00000
_SUCCESS 是一个执行成功的标识
part-r-00000存放的是我们执行完毕后的结构
查看part-r-00000,与我们预期的结果一致