MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
MapReduce是hadoop的核心组件之一,hadoop要实现分布式需要包括两部分,一部分是分布式文件系统 HDFS,一部分是分布式计算框架 MapReduce。MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
MapReduce计算框架用来处理海量数据,而一台计算机不可能计算如此大量的数据,一台不行那就使用多台一起处理数据。“ 分而治之 ”是MapReduce的核心思想。
MapReduce有两个过程:
map阶段
Map负责把一个任务分解成多个任务进行处理。任务分配等会增加程序的复杂性,但人多力量大,效率也会有明显地提高。
reduce阶段
Reduce负责对map阶段的结果进行汇总输出。
下面这个例子就很好的体现了MapReduce的思想
果园里有1000颗果树,现在要计算一共有多少个果子。
- 如果一个人算,就算这个人算的再快也一定要算好久,也有很大的可能 算着算着不想算罢工了 或者 超负荷去医院了;
- 但1000个人算的话,分配到每人身上的任务就只剩下一棵树了,虽然需要额外花点时间安排哪些人算哪些树和资源,但1000人同时开始,效率得到了极大的提高,最后将这1000个人所算的数进行汇总得到总果子数。
注:后面还将具体介绍MapReduce工作过程。
优点
MapReduce易于编程
只需要实现一些简单接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行。也就是说你写一个分布式程序,就跟写一个简单的串行程序是一模一样的。
良好的扩展性(hadoop的特点)
当你的计算资源不能满足的时候,你可以通过简单的增加机器(nodemanager)来扩展它的计算能力
高容错性
MapReduce设计的初衷就是使程序能够部署在廉价的PC机器上,这就要求它具有很高的容错性,比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于整个任务运行失败。
适合PB级以上海量数据的离线处理
可以实现上千台服务器集群并发工作,提供数据处理能力
缺点
不擅长实时计算
MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果
不擅长流式计算
流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化。这是因为MapReduce自身的设计特点决定了数据源必须是静态的
不擅长DAG有向图计算
多个应用程序之间存在依赖关系,后一个应用程序的输入为前一个程序的输出。在这种情况下,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常低下
首先,先介绍MR的常用数据序列化类型,也为了方便后面的理解。
MR中,以 键值对(K-V) 的形式表示每个数据,而 K 和 V 的数据类型需要根据实即情况而定。框架把作业的输入(InPut)看成是一组
Map和Reduce都要设置 输入 和 输出 数据类型;且Map的输出作为Reduce的输入 进行汇总,所以Map输出和Reduce输入必须一样。
程序的InPut就是Map的输入,程序的OutPut就是Reduce的输出。
这里是一个WordCount的例子,结合下面的一些概念和该图一起理解。
一个完整的MapReduce程序在分布式运行时有三类实例进程:
由上图可知,这个程序有3个MapTask 和 2个ReduceTask
MapTask个数(并行度)等于切片数
由上图可知,该程序一共有两个文件。第一个文件是200M,所以被切分成了两个切片;第二个文件是100M(小于128M),单独为一个切片。
ReduceTask个数(并行度)默认等于ReduceTask的进程数
当自己设定分区数时
job.setNumReduceTasks(n):设置分区数
getPartition:自定义所取分区数
由上图可知,该程序自定义分区,将把结果按单词首位(a-p)、(q-z)输出到两个文件中,ReduceTask个数默认为2。
排序概述
排序分类
注:MapTask如何工作、ReduceTask如何工作等若干问题细节下面会继续介绍
过程详解
(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会将数据写到本地磁盘上,生成一个临时文件。在写入之前,先要对数据进行分区划分和一次本地排序。
分区划分。通过调用Partitioner的getPartition()方法就能知道该输出要送往哪个Reducer。默认的Partitioner使用Hash算法来分区,即通过key.hashCode() mode R来计算,R为Reducer的个数。getPartition返回Partition事实上是个整数,例如有10个Reducer,则返回0-9的整数,每个Reducer会对应到一个Partition。map输出的键值对,与partition一起存在缓冲中。
对每部分分区的数据,使用快速排序算法(QuickSort)对key排序。并在必要时对数据进行合并(Combiner)、压缩(Compress
(5)合并Spill文件阶段:当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。
在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
过程详解
(1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
(2)Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。
(3)Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可,再以key进行分组。
(4)Reduce阶段:reduce()函数将计算结果写到HDFS上。
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle
实际上包括了前面所说的 Map阶段的数据溢写、分区、排序、合并;Reduce阶段的Copy、Merge、Sort。都已基本介绍。
这里再来简单介绍下shuffle中的两个可选优化
1、Combiner
本质:提前进行聚合,让MapTask分担ReduceTask的工作。
先讨论下面这个问题
Combiner目的:减少传输到Reduce中的数据量。为了削减Mapper的输出从而减少网络带宽和Reducer之上的负载。
对于Combiner有几点需要说明的是:
combine的输入和reduce的完全一致,输出和map的完全一致
与combine与reducer不同的是,combiner没有默认的实现,需要显式的设置在conf中才有作用。combine是在每一个MapTask所在的节点运行;Reducer是接收全局所有Mapper的输出结果;
并不是所有的job都适用combiner,只有操作满足结合律的才可设置combiner。combine操作类似于:opt(opt(1, 2, 3), opt(4, 5, 6))。如果opt为求和、求最大值的话,可以使用,但是如果是求中值的话,不适用。
2、Compress
在进行网络传输前将数据压缩(如:ReuceCopyu数据前),接收数据后再进行解压,可以大大减少了磁盘IO以及网络IO;将Reduce数据的结果进行压缩,也能够很好的节省空间。
注意:采用压缩技术减少了磁盘IO,但同时增加了CPU运算负担。所以,压缩特性运用得当能提高性能,但运用不当也可能降低性能。
压缩基本原则:
用户编写的程序分成三个部分:Mapper、Reducer和Driver。
Mapper阶段
(1)用户自定义的Mapper要继承自己的父类
(2) Mapper的输入数据是KV对的形式(KV的类型可自定义)
(3) Mapper中的业务逻辑写在map()方法中
(4) Mappen的输出数据是KV对的形式(KV的类型可自定义)
(5) map()方法(MapTask进程)对每一个
Reducer阶段
(1)用户自定义的Reducer要继承自己的父类
(2) Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
(3) Reducer的业务逻辑写在reduce()方法中
(4) ReduceTask进程对每一组相同k的
(5) Reducer的输出数据类型也是KV
Driver阶段
相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象
根据编码规范,要实现自定义Mapper、Reducer、Driver三个类
Mapper类
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
/*
KEYIN 输入数据的key 表示每行数据的偏移量
VALUEIN 输入数据的value 表示每行数据
KEYOUT 输出数据的key 表示每个输出数据的key
VALUEOUT 输出数据的value 表示每个输出数据的key
1、继承Mapper类
2、指定以上四个参数类型
*/
public class WordcountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
//输出的key value
Text k = new Text();
IntWritable v = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//1. 读取每一行
String line = value.toString();
//2. 切割单词
String[] words = line.split(" ");
//3. 循环写出
for (String word : words) {
k.set(word);
context.write(k, v);
}
}
}
Reducer类
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
/*
KEYIN 输入数据的key
VALUEIN 输入数据的value
KEYOUT 输出数据的key
VALUEOUT 输出数据的value
1、继承Reducer类
2、指定以上四个参数类型
*/
public class WordcountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
IntWritable v = new IntWritable();
int sum;
@Override
protected void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
//you,1
//you,1
sum = 0;
//1. 累加求和
for (IntWritable value : values) {
sum += value.get();
}
//2. 写出
v.set(sum);
context.write(key, v);
//you,2
}
}
Driver
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class WordcountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
args = new String[] { "D:\\Z-Test\\input\\InPutWordCount", "D:\\Z-Test\\output\\OutPutWordCount" };
Configuration conf = new Configuration();
// 1 获取配置信息以及封装任务job对象
Job job = Job.getInstance(conf);
// 2 设置jar加载路径
job.setJarByClass(WordcountDriver.class);
// 3 关联map和reduce类
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReducer.class);
// 4 设置map输出kv类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5 设置最终输出kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 提交
boolean result = job.waitForCompletion(true); // 为true 打印运行信息
System.exit(result ? 0 : 1);
}
}
InPutWordCount文件中有两个.txt文件
运行结果
由于没有进行自定义分区,这里默认只有一个分区,也就只有一个part-r-000000文件
统计成功