MapReduce思想在生活中处处可见。或多或少都成接触过这种思想。MapReduce的思想核心是“分而治之”,适用于大量复杂的任务处理场景(大规模数据处理场景)。即使是发布过论文实现分布式计算的谷歌也只是实现了这种思想,而不是自己原创。
Map负责"分",即把复杂的任务分解为若干个“简单的任务”来并行处理。可以进行拆分的前提是这些小任务可以并行计算,彼此间几乎没有依赖关系。
Reduce负责“合”,即对map阶段的结果进行全局汇总。
这两个阶段合起来正是MapReduce思想的体现。
图:MapReduce思想模型
还有一个比较形象的语言解释MapReduce:
我们要数图书馆中的所有书。你数1号书架,我数2号书架。这就是“Map”。我们人越多,数书就更快。
现在我们到一起,把所有人的统计数加在一起。这就是“Reduce”。
MapReduce是一个分布式运算程序的编程框架,核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在Hadoop集群上。
既然是做计算的框架,那么表现形式就是有个输入(input),MapReduce操作这个输入(input),通常本身定义好的计算模型,得到一个输出(output)。
对许多开发者来说,自己完完全全实现一个并行计算程序难度太大,而MapReduce就是一种简化并行计算的编程模型,降低了开发并行应用的入门门槛、
Hadoop MapReduce构思体现在如下的三个方面:
如何对付大数据处理:分而治之
对相互间不具有计算依赖关系的大数据,实现并行最自然的办法就是采取分而治之的策略。并行计算的第一个重要问题是如何划分计算任务或者计算数据以便对划分的子任务或数据块同时进行计算。不可分拆的计算任务或相互间有依赖关系的数据无法进行并行计算!
构建抽象模型:Map和Reduce
MapReduce借鉴了函数式语言中的思想,用map和Reduce两个函数提供了高层的并行编程抽象模型。
Map:对一组数据元素进行某种重复式的处理;
Reduce:对Map的中间结果进行某种进一步的结果整理。
MapReduce中定义了如下的Map和Reduce
map: (k1; v1) → [(k2; v2)]
reduce: (k2; [v2]) → [(k3; v3)]
Map和Reduce为程序员提供了一个清晰的操作接口抽象描述。通过以上两个编程接口,大家可以看出MapReduce处理的数据类型是
统一构架,隐藏系统层细节
如何提供统一的计算框架,如果没有统一封装底层细节,那么程序员则需要考虑诸如数据存储、划分、分发、结果收集、错误恢复等诸多细节;为此,MapReduce设计并提供了统一的计算框架,为程序员隐藏了绝大多数系统层面的处理细节。
如何提供统一的计算框架,如果没有统一封装底层细节,那么程序员则需要考虑诸如数据存储、划分、分发、结果收集、错误恢复等诸多细节;为此,MapReduce设计并提供了统一的计算框架,为程序员隐藏了绝大多数系统层面的处理细节。
MapReduce 的开发一共有八个步骤, 其中 Map 阶段分为2个步骤,Shuffle 阶段 4 个步骤,Reduce 阶段分为2个步骤
Map阶段2个步骤
1、设置InputFormat类,读取输入文件内容,对输入文件的每一行,解析成key、value对(K1和V1)
2、自定义map方法,对一个键值对调用一次map方法,将第一步的K1和V1结果转换成另外的Key-Value(K2和V2)对,输出结果。
Shuffle阶段4个步骤
3、对map阶段输出的k2和v2对进行分区
4、对不同分区的数据按照相同的key排序
5、(可选)对数据进行局部聚合,降低数据的网络拷贝
6、对数据进行分组,相同key的Value放入一个集合中,得到K2和[V2]
Reduce 阶段 2 个步骤
7、对map任务的输出,按照不同的分区,通过网络copy到不同的reduce节点。
8、对多个map任务的输出进行合并、排序。编写reduce方法,在此方法中将K2和[V2]进行处理,转换成新的key、value(K3和V3)输出,并把reduce的输出保存到文件中。
用户编写的程序分为三个部分:Mapper,reducer,Driver(提交运行mr程序的客户端)
Mapper
(1) 自定义类继承Mapper类
(2) 重写自定义类中的map方法,在该方法中将K1和V1转为K2和V2
(3) 将生成的K2和V2写入上下文中
Reducer
(1) 自定义类继承Reducer类
(2) 重写Reducer中的reduce方法,在该方法中将K2和[V2]转为K3和V3
(3) 将K3和V3写入上下文中
Driver
整个程序需要一个Driver来进行提交,提交的是一个描述了各种必要信息的job对象
(1)定义类,编写main方法
(2)在main方法中指定以下内容:
1、创建一个job任务对象
2、指定job所在的jar包
3、指定源文件的读取方式类和源文件的读取路径
4、指定自定义的Mapper类和K2、V2类型
5、指定自定义分区类(如果有的话)
6、指定自定义分组类(如果有的话)
7、定义自定义的Reduce类和K3、v3的数据类型
8、指定输出方式类和结果输出路径
9、将job提交到yarn集群
2.3wordCount示例编写
需求:在一堆给定的文本文件中统计输出每一个单词出现的总次数
第一步:数据准备
1、创建一个新的文件
cd /export/server
vim wordcount.txt
2、向其中放入以下内容并保
hello,world,hadoop
hive,sqoop,flume,hello
kitty,tom,jerry,world
hadoop
3、上传到HDFS
hdfs dfs -p -mkdir /input/wordcount
hdfs dfs -put wordcount.txt /input/wordcount
第四部:导入相关依赖
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<!-- <verbal>true</verbal>-->
</configuration>
</plugin>
<!--
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
-->
</plugins>
</build>
第三步:代码编写
(1)定义一个mapper类
//首先要定义四个泛型的类型
//keyin: LongWritable valuein: Text
//keyout: Text valueout:IntWritable
public class WordCountMapper exports Mapper<LongWritable,Text,Text,Writable>{
//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 LongWritable (1));
}
}
}
}
(2)定义一个reduce类
public class WordCountReducer extends Reducer<Text,LongWritable,Text,LongWritable> {
//生命周期:框架每传递进来一个kv 组,reduce方法被调用一次
@Override
protected void reduce(Text key, Iterable<LongWritable > values, Context context) throws IOException, InterruptedException {
//定义一个计数器
int count=0;
//遍历这个一组kv的所有v,累加到count中
for(LongWritable value:values){
count += value.get();
}
context.write(key, new LongWritable (count));
}
}
(3)定义一个Driver主类,用来描述job并提交job
public class WordCountRunner{
//该方法用于指定一个job任务
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
//1:创建一个job任务对象,并指定job的名字
Job job = Job.getInstance(conf, "wordcount");
//2:指定job所在的jar包
job.setJarByClass(WordCountRunner.class);
//3:指定源文件的读取方式类和源文件的读取路径
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job, new Path("file:///D:\\input\\wordcount"));
//4:指定自定义的Mapper类和K2、V2类型
job.setMapperClass(WordCountMapper.class);
//设置K2类型
job.setMapOutputKeyClass(Text.class);
//设置V2类型
job.setMapOutputValueClass(LongWritable.class);
//分区和分组采用默认方式
//5:指定自定义的Reducer类和K3、V3的数据类型
job.setReducerClass(WordCountReducer.class);
//设置K3的类型
job.setOutputKeyClass(Text.class);
//设置V3的类型
job.setOutputValueClass(LongWritable.class);
//6:指定输出方式类和结果输出路径
job.setOutputFormatClass(TextOutputFormat.class);
//设置输出的路径
TextOutputFormat.setOutputPath(job, new Path("file:///D:\\output\\wordcount"));
//7:将job提交给yarn集群
boolean bl = job.waitForCompletion(true);
System.exit(bl?0:1);
}
}
(1)mapreduce程序是被提交给LocalJobRunner在本地以单进程的形式运行
(2)而处理的数据及输出结果可以在本地文件系统,也可以在hdfs上
(3)本地模式非常便于进行业务逻辑的调试
(1)将mapreduce程序提交给yarn集群,分发到很多的节点上并发执行
(2)处理的数据和输出结果应该位于hdfs文件系统
(3)提交集群的实现步骤:
1、将Driver主类代码中的输入路径和输出路径修改为HDFS路径
TextInputFormat.addInputPath(job, new Path("hdfs://node1:8020/input/wordcount"));
TextOutputFormat.setOutputPath(job, new Path("hdfs://node1:8020/output/wordcount"));
2、将程序打成JAR包,然后在集群的任意一个节点上用hadoop命令启动
hadoop jar wordcount.jar cn.itcast.WordCountDriver
MapReduce框架运转在**
一个MapReduce作业的输入和输出类型如下图所示:可以看出在整个标准的流程中,会有三组
(TextInputFormat)
第三阶段是调用Mapper类中的map方法。上阶段中每解析出来的一个
第四阶段是按照一定的规则对第三阶段输出的键值对进行分区。默认是只有一个分区。分区的数量是Reducer任务运行的数量。默认只有一个Reducer任务。
第五阶段是对每个分区中的键值对进行排序。首先,按照键进行排序,对于键相同的键值对,按照值进行排序。比如三个键值对<2,2>、<1,3>、<2,1>,键和值分别是整数。那么排序后的结果是<1,3>、<2,1>、<2,2>。如果有第六阶段,那么进入第六阶段;如果没有,直接输出文件。
第六阶段是对数据进行局部聚合处理,也就是conbiner处理,键相等的键值对会调用一次reduce方法。经过这一阶段,数据量会减少。本阶段默认是没有的。
在整个MapReduce程序的开发过程中,我们最大的工作量是覆盖map方法和覆盖reduce方法。
在MapReduce中,通过我们指定分区, 会将同一个分区的数据发送到同一个Reduce当中进行处理。例如:为了数据的统计,可以把一批类似的数据发送到同一个Reduce当中,在用一个Reduce当中统计相同数据的数据,就可以实现类似的数据分区和统计等
其实就是相同类型的数据,有共性的数据,送到一起去处理,在Redcuce过程中,可以根据实际需求(比如按某个维度进行归档,类似于数据库的分组),把Map完的数据Reduce到不同的文件中。分区的设置需要与ReduceTaskNum配合使用。比如想要得到5个分区的数据结果。那么就得设置5个ReduceTask。
需求:将以下数据进行分开处理
详细数据参见partition.csv 这个文本文件,其中第五个字段表示开奖结果数值,现在需求将15以上的结果以及15以下的结果进行分开成两个文件进行保存
这个Mapper程序不做任何逻辑,也不对Key-Value做任何改变,只是接收数据,然后往下发送
public class MyMapper extends Mapper<LongWritable,Text,Text,NullWritable>{
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
context.write(value,NullWritable.get());
}
}
主要的逻辑就在这里,这也是这个案例的意义,通过Partitioner 将数据分发给不同的Reducer
/**
* 这里的输入类型与我们map阶段的输出类型相同
*/
public class MyPartitioner extends Partitioner<Text,NullWritable>{
/**
* 返回值表示我们的数据要去到哪个分区
* 返回值只是一个分区的标记,标记所有相同的数据去到指定的分区
*/
@Override
public int getPartition(Text text, NullWritable nullWritable, int i){
String result=text.toString().split("\t")[5];
if(Integer.parseInt(result)>15){
return 1;
}else{
return 0;
}
}
}
这个Reducer也不做任何处理,将数据原封不动的输出即可
public class MyReducer 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());
}
}
public class PartitionRunner extends Configured implements Tool {
public static void main(String[] args) throws Exception{
//1:创建job任务对象
Job job = Job.getInstance(super.getConf(), "partition_maperduce");
//2:指定job所在的jar包
job.setJarByClass(PartitionRunner.class);
//3:指定源文件的读取方式类和源文件的读取路径
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job, new Path("file:///D:\\input\\partition"));
//4:指定自定义的Mapper类和K2、V2类型
job.setMapperClass(PartitionMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
//5:指定分区类
job.setPartitionerClass(MyPartitioner.class);
//6:指定自定义的Reducer类和K3、V3的数据类型
job.setReducerClass(PartitionerReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//7、设置ReduceTask的个数
job.setNumReduceTasks(2);
//8、指定输出方式类和结果输出路径
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job, new Path("file:///D:\\out\\partition_out6"));
//将job提交给yarn集群
boolean bl = job.waitForCompletion(true);
System.exit(bl?0:1);
}
}