概要
MapReduce是hadoop的核心组件之一,用于大数据并行处理的计算模型、框架和平台,主要解决海量数据的计算。
借鉴了面向对象编程思想
MapReduce概述
核心思想
分而治之,将大问题分解为小问题,解决小问题。
使用MapReduce操作海量数据是,每个MapReduce程序被初始化为一个工作任务,每个工作任务分为Map和Reduce两个阶段:
* Map:负责任务分解,分解为若干个“简单的任务”来并行处理,前提是这些任务没有必然的依赖关系。
* Reduce:负责任务合并,把Map阶段的结果进行全局汇总
MapReduce编程模型
通过两个函数Map()和Reduce()两函数,Map()接收键值对返回键值对,Reduce()接收键值对,返回结果键值对
流程
1. 将原始数据装换为<k1,v1>形式
2. 解析后的<k1,v1>传给Map映射为中间结果形式<k2,v2>
3. <k2,v2>形成<k2,{v2,...}>传给Reduce,把相同k的值通过Reduc()输出形成<k3,v3>
案例–词频统计流程
MapReduce工作原理
工作过程
分片、格式化数据源
- 分片操作:将源文件划分为大小相等的小数据块(hadoop2.x默认128M),hadoop自动为每一个分片运行自定义的Map()操作。
- 格式化操作:将划分好的分片格式化为键值对的数据形式,其中键为偏移量,值为每一行的内容
执行MapTask
每一个Map任务都有内存缓冲区默认100M,当写到80M的时候将数据写入磁盘(溢出),会对数据进行排序,多次溢出多次写入磁盘,最后进行合并
执行Shuffle过程
将MapTask输出的处理结果数据分发给ReduceTask,分发过程中对Key进行分区和排序
执行ReduceTask
输入ReduceTask的数据流形式是<key,{values list}>
形式,可以自定义reduce()方法进行逻辑处理,最终以<key value>
形式输出
写入文件
MapRduce框架自动将ReduceTask生成的减值对传入OutputFormat的write方法实现写入操作
MapTask工作原理
主要经过Read阶段、Map阶段、Collect阶段、Split阶段、Combiner阶段
- Read阶段:通过用户编写的RecordReader,从输入的InputSplit中解析出一个个key/value
- Map阶段:将解析出的key/value交给用户编写的map()函数处理,并产生一系列新的key/value
- Collect阶段:将生成的key/value分片,写入一个环形内存缓冲区
- Split阶段(溢写):当环形缓冲区满后,将数据写入本地磁盘,写入之前在本地进行排序
- Combiner阶段:当所有数据处理完成后,MapTask对所有临时文件进行合并,保证只生成一个文件
ReduceTask工作原理
主要经过Copy阶段、Merge阶段、Sort阶段、Reduce阶段、Write阶段
- Copy阶段:reduce从各个MapTask上进行远程拷贝数据,若数据超过一定大小则写到磁盘上,否则放到内存中
- Merge阶段:在远程拷贝数据的同时,ReduceTask会启动两个后台线程,分别对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘文件过多
- Sort阶段:为了将Key相同的数据聚集在一起,Hadoop采用基于排序的策略,由于MapTask对自己的处理结果局部排序,ReduceTask进行一次归并排序即可
- Reduce阶段:对排序的键值对调用reduce方法,键相等键值对调用一次reduce()方法,每次调用产生多个键值对,最后将这些键值对写入带HDFS系统中
- Write阶段:reduce()函数将计算结果写到HDFS上
Shuffle工作原理
Shuffle是MapReduce的核心,用来确保每个reduce的数据都是按键排序的,他的性能高低取决了整个MapReduce程序的性能高低,map和reduce阶段都涉及shuffle机制。组外合并。combine是组内合并
Map阶段的Shuffle
- 当MapTask处理结果放入缓冲区,当快要溢出时,将在本地磁盘创建一个溢出文件,将缓冲区的数据写入这个文件
- 写入磁盘之前,线程根据ReduceTask的数量将数据分区,一个Reduce任务对应一个分区的数据,为了避免reduce任务分配不均
- 分完数据后,对每个分区的数据进行排序,如果设置了Combiner将排序后的结果进行combiner操作。为了减少执行数据写入磁盘
- map执行结束后由许多溢出文件,这时需要不断进行排序和combine操作为了:减少每次写入磁盘的数据量,减少下一复制阶段网络传输的数据量。最后合并完成了一个已分区且已排序的文件
- 将分区中的数据拷贝给对应的reduce任务
Reduce阶段
- Reduce会接收不同map任务的数据,每个map的数据都是有序的。如果数据量小写入到缓存,如果数据超过一定阈值则将数据合并后写道磁盘中
- 随着溢写文件的增多,后台线程会将他们合并成一个更大的有序文件,为了给后面的合并节约时间
- 合并过程中会产生很多中间文件(写入磁盘了),但MapReduce会让写入磁盘的数据尽可能的少,并且最后一次合并的结果并没有写入磁盘,而是直接输入到reduce函数。
MapReduce编程组件
- InputFormat组件:用于描述输入数据格式,连个功能:数据切分,为Mapper提供输入数据
- Mapper组件:Mapper类是实现Map任务的一个抽象基类,该基类提供了一个map()方法
- Reduce组件:Map输出的键值对,将由Reduce组件进行合并处理,租后的某种形式的输出结果
- Partitioner组件:让Map对Key进行分区,从而可以根据不同的key分发到不同的Reduce中去处理,目的就是将key均匀的分布在ReduceTask上
- Combiner组件:对Map阶段输出的重复数据先祖欧一次合并,然后把新的键值对作为Reduce阶段的输入
- OutputFromat组件:用于描述MapReduce程序输出格式和规范的抽象类
InputFormat组件
用于描述输入数据格式,连个功能:数据切分,为Mapper提供输入数据
- 数据切分
- 为Mapper提供数据输入
public abstract class InputFormat<K, V> {
public InputFormat() {
}
public abstract List<InputSplit> getSplits(JobContext var1) throws IOException, InterruptedException;
public abstract RecordReader<K, V> createRecordReader(InputSplit var1, TaskAttemptContext var2) throws IOException, InterruptedException;
}
Mapper组件
- Mapper类是实现Map任务的一个抽象基类,该基类提供了一个map()方法
- 要自定义map()方法,续继承Mapper方法并重写map()方法
- 以词频统计为例,自定义map()方法
在这里插入代码片
Reducer组件
- map过程输出的减值对,将由Reducer组件进行合并处理
- Reducer类中会调用run()方法,,该方法中定义了setup(),reduce(),cleanup()方法,执行顺序为setup(),reduce(),cleanup()。
- 默认情况下,setup(),cleanup()内部不作任何处理,所以,reduce()方法是处理数据的核心方法
Partitioner组件
- 让Map对Key进行分区,从而可以根据不同的key分发到不同的Reduce中去处理,目的就是将key均匀的分布在ReduceTask上
- 想要自定义Partitioner组件,需要继承Partitioner类并重写getPartition方法。
Combiner组件
- 对Map阶段的输出重复数据做一次合并计算(组内合并),把心的键值对作为reduce阶段的输入
- 自定义Combiner,需要继承Reduce类,重写reduce()方法
- 注意:Combiner组件不予许改变业务逻辑
OutputFromat组件
用于描述MapReduce程序输出格式和规范的抽象类
MapReduce运行模式
本地模式运行
在当前的开发环境模拟MapReduce执行环境,处理的数据及输出结果在本地操作系统
集群运行模式
MapReduce程序打成一个jar包,提交到Yarn集群上去运行任务。Yarn负责资源管理和任务调度,因此处理的数据和输出结果都在HDFS文件系统中。
MapReduce性能优化策略
五个方面:数据输入、Map阶段、Reduce阶段、Sheffle阶段、其他调优属性
-
数据输入:执行前,将小文件合并,使用CombineTextInputFormat作为输入
-
Map阶段
1. 减少溢写:调整io.sort.mb(缓冲区大小,默认100m),调整sort.split.perccent(溢写比例,默认80%) 2. 减少合并:调整io.sort.factor(控制一次合并多少文件) 3. 在map之后,不影响业务逻辑的前提下,先进性combine处理,减少I/O
属性名称 | 类型 | 默认值 | 说明 |
---|---|---|---|
mapreduce.io.sort.mb | int | 100 | 内存缓冲区大小,默认100M |
mapreduce.sort.split.perccent | float | 0.80 | 溢出写入磁盘的百分比 |
mapreduce.task.io.sort.factor | int | 10 | 排序文件时,一次合并的流数,实际开发时可以设置为80 |
mapreduce.task.min.num.spills.for.combine | int | 3 | 运行combiner时,所需的最少溢出文件数 |
-
Reduce阶段
1. 合理设置map和reduce数。 2. 设置map、reduce共存 3. 规避使用reduce 4. 合理设置reduce端的buffer
都在mapred-default.xml中设置,属性如下
属性名称 | 类型 | 默认值 | 说明 |
---|---|---|---|
mapreduce.job.reduce.slowstare.completemaps | float | 0.05 | 当MapTask在执行到5%,就开始为reduce申请资源。开始执行reduce操作,reduce可以开始拷贝map结果数据做reduce shuffle操作 |
mapred.job.reduce.input.buffe | float | 0.0 | 在reduce过程,内存中保存map输出的空间占整个堆空间的比例。可以增加这个值来减小范文磁盘次数 |
- Sheffle阶段:可以给shuffle多一下内存空间,任务节点上的内存大小应尽量大
参数设置位于mapred-site.xml
属性名称 | 类型 | 默认值 | 说明 |
---|---|---|---|
mapred.map.child.java.opts | -Xmx200m | 当用户设置该值会以最大1G jvm heap size 启动MapTask。可能导致内存溢出,建议设置为-Xmx1024m | |
mapred.reduce.child.java.opts | -Xmx200m | 当用户设置该值会以最大1G jvm heap size 启动ReduceTask。可能导致内存溢出,建议设置为-Xmx1024m |
- 其他调优属性:mapred-defult.xml中设置
属性名称 | 类型 | 默认值 | 说明 |
---|---|---|---|
mapreduce.map.memory.mb | int | 1024 | 一个MapTask可用的资源上香,如果超过上限会被杀死 |
mapreduce.reduce.memory.mb | int | 1024 | 一个ReduceTask可用的资源上香,如果超过上限会被 |
mapreduce.map.cpu.vcores | int | 1 | 每个MapTask可食用最多cpu core数目 |
mapreduce.reduce.cpu.vcores | int | 1 | 每个ReduceTask可食用最多cpu core数目 |
mapreduce.reduce.shuffle.parallelcopies | int | 5 | 每个reduce去map中那数据的并行数 |
mapreduce.map.maxattempts | int | 4 | 每个MapTask最大重试次数,超过后则认为该MapTask运行失败 |
案例—倒排索引
- 需求:现假设有三个源文件file1.txt、file2.txt和file3.txt,需要使用倒排索引的方式对这三个源文件内容实现倒排索引,并将最后的倒排索引文件输出。
- 首先,使用默认的TextInputFormat类对每个输入文件进行处理,得到文本中每行的偏移量及其内容。Map过程首先分析输入的<key,value>键值对,经过处理可以得到倒排索引中需要的三个信息:单词、文档名称和词频。
- 经过Map阶段数据转换后,同一个文档中相同的单词会出现多个的情况,而单纯依靠后续Reduce阶段无法同时完成词频统计和生成文档列表,所以必须增加一个Combine阶段,先完成每一个文档的词频统计。
- 经过上两个阶段的处理后,Reduce阶段只需将所有文件中相同key值的value值进行统计,并组合成倒排索引文件所需的格式即可。
实现
- InvertedIndexMapper
public class InvertedIndexMapper extends Mapper<LongWritable, Text, Text, Text> {
private static Text keyInfo = new Text();
private static final Text valueInfo = new Text("1");
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, Text>.Context context) throws IOException, InterruptedException {
// 拆分数据
String line = value.toString();
String[] fields = line.split(" ");
// 获取文本行数据所在文件名
FileSplit fileSplit = (FileSplit) context.getInputSplit();
String fileName = fileSplit.getPath().getName();
// 将K2和V2写入上下文
for(String field: fields){
keyInfo.set(field+":" + fileName);
context.write(keyInfo, valueInfo);
}
}
}
- InvertedIndexCombiner
public class InvertedIndexCombiner extends Reducer<Text, Text,Text,Text> {
private static Text info = new Text();
@Override
protected void reduce(Text key, Iterable<Text> values, Reducer<Text, Text,Text,Text>.Context context) throws IOException, InterruptedException {
int sum = 0;
for (Text value:values){
sum += Integer.parseInt(value.toString());
}
int splitIndex = key.toString().indexOf(":");
info.set(key.toString().substring(splitIndex + 1) + ":"+sum);
// 获取单词
key.set(key.toString().substring(0, splitIndex));
// 将key和value写入上下文
context.write(key,info);
}
}
- InvertedIndexReducer
public class InvertedIndexReducer extends Reducer<Text,Text,Text,Text> {
private static Text result = new Text();
@Override
protected void reduce(Text key, Iterable<Text> values, Reducer<Text,Text,Text,Text>.Context context) throws IOException, InterruptedException {
// 遍历几个,拼接集合内容,生成文档列表
String fileList = new String();
for (Text value: values
) {
fileList += value.toString()+";";
}
// 将k3和v3写入上下文
result.set(fileList);
context.write(key,result);
}
}
- InvertedIndexDriver
public class InvertedIndexDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 获取job任务对象
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 设置job对象
job.setJarByClass(InvertedIndexDriver.class);
job.setMapperClass(InvertedIndexMapper.class);
job.setCombinerClass(InvertedIndexCombiner.class);
job.setReducerClass(InvertedIndexReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
FileInputFormat.setInputPaths(job,new Path("G:\\TwoNextCode\\Hadoop\\input\\"));
FileOutputFormat.setOutputPath(job,new Path("G:\\TwoNextCode\\Hadoop\\output\\"));
// 启动job任务
boolean res = job.waitForCompletion(true);
System.exit(res?0:1);
}
}
案例–数据去重
- 需求
文件file1.txt本身包含重复数据,并且与file2.txt同样出现重复数据,现要求使用Hadoop大数据相关技术对以上两个文件进行去重操作,并最终将结果汇总到一个文件中。
(1) 编写MapReduce程序,在Map阶段采用Hadoop默认作业输入方式后,将key设置为需要去重的数据,而输出的value可以任意设置为空。
(2) 在Reduce阶段,不需要考虑每一个key有多少个value,可以直接将输入的key复制为输出的key,而输出的value可以任意设置为空,这样就会使用MapReduce默认机制对key(也就是文件中的每行内容)自动去重。
- file1.txt
2018-3-1 a
2018-3-2 b
2018-3-3 c
2018-3-4 d
2018-3-5 a
2018-3-6 b
2018-3-7 c
2018-3-3 c
- file2.txt
2018-3-1 b
2018-3-2 a
2018-3-3 b
2018-3-4 d
2018-3-5 a
2018-3-6 c
2018-3-7 d
2018-3-3 c
实现
- DedupMapper.java
public class DedupMapper extends Mapper<LongWritable,Text,Text, NullWritable> {
private static Text field = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
field = value;
context.write(field, NullWritable.get());
}
}
- DedupReducer.java
public class DedupReduce extends Reducer<Text, NullWritable, Text,NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
context.write(key, NullWritable.get());
}
}
- DedupDrive.java
public class DedupDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 获取job任务对象
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 设置job对象
job.setJarByClass(DedupDriver.class);
job.setMapperClass(DedupMapper.class);
job.setReducerClass(DedupReduce.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job,new Path("G:\\TwoNextCode\\Hadoop\\input\\"));
FileOutputFormat.setOutputPath(job,new Path("G:\\TwoNextCode\\Hadoop\\output\\"));
// 启动job任务
boolean res = job.waitForCompletion(true);
System.exit(res?0:1);
}
}
案例–TopN
- TopN分析法是指从研究对象中按照某一个指标进行倒序或正序排列,取其中所需的N个数据,并对这N个数据进行重点分析的方法。
- 现假设有数据文件num.txt,现要求使用MapReduce技术提取上述文本中最大的5个数据,并最终将结果汇总到一个文件中。
- 先设置MapReduce分区为1,即ReduceTask个数一定只有一个。我们需要提取TopN,即全局的前N条数据,不管中间有几个Map、Reduce,最终只能有一个用来汇总数据。
- 在Map阶段,使用TreeMap数据结构保存TopN的数据,TreeMap默认会根据其键的自然顺序进行排序,也可根据创建映射时提供的 Comparator进行排序,其firstKey()方法用于返回当前集合最小值的键。
- 在Reduce阶段,将Map阶段输出数据进行汇总,选出其中的TopN数据,即可满足需求。这里需要注意的是,TreeMap默认采取正序排列,需求是提取5个最大的数据,因此要重写Comparator类的排序方法进行倒序排序。
实现
- 编写TopNMapper.java
public class TopNMapper extends Mapper<LongWritable, Text, NullWritable, IntWritable> {
private static TreeMap<Integer, String> repToRecordMap = new TreeMap<Integer, String>();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 拆分行文本数据,得到每个数字
String line = value.toString();
String[] nums = line.split(" ");
// 将得到数字存到TreeMap集合,获取每一行最大的5个数
for (String num: nums) {
repToRecordMap.put(Integer.parseInt(num), "");
if(repToRecordMap.size() > 5){
repToRecordMap.remove(repToRecordMap.firstKey());
}
}
}
@Override
protected void cleanup(Context context) {
for (Integer i : repToRecordMap.keySet()
) {
try {
context.write(NullWritable.get(), new IntWritable(i));
}catch (Exception e){
e.printStackTrace();
}
}
}
}
- 编写TopNReduce.java
public class TopNReduce extends Reducer<NullWritable, IntWritable,NullWritable,IntWritable> {
private static TreeMap<Integer, String> repToRecordMap = new TreeMap<Integer, String>(new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return b - a;
}
});
@Override
protected void reduce(NullWritable key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 遍历集合,将所有数字存入TreeMap集合,自定义TreeMap集合排序规则
for (IntWritable value: values
) {
repToRecordMap.put(value.get(), "");
// 取出TreeMap最大的5个数
if(repToRecordMap.size()>5){
repToRecordMap.remove(repToRecordMap.firstKey());
}
}
// 将最终结果写入
for ( Integer i :repToRecordMap.keySet()
) {
context.write(NullWritable.get(),new IntWritable(i));
}
}
}
- 编写TopNDriver
public class TopNDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 获取job任务对象
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 设置job对象
job.setJarByClass(TopNDriver.class);
job.setMapperClass(TopNMapper.class);
job.setReducerClass(TopNReduce.class);
job.setOutputKeyClass(NullWritable.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.setInputPaths(job,new Path("G:\\TwoNextCode\\Hadoop\\input1\\"));
FileOutputFormat.setOutputPath(job,new Path("G:\\TwoNextCode\\Hadoop\\output1\\"));
// 启动job任务
boolean res = job.waitForCompletion(true);
System.exit(res?0:1);
}
}
结果