5.1 MapReduce概述
-
MapReduce定义
- MapReduce是一个 分布式运算程序的编程框架,是用户开发“基于 Hadoop的数据分析应用”的核心框架
- MapReduce核心功能是将 用户编写的业务逻辑代码 和 自带默认组件 整合成一个完整的分布式运算程序 ,并发运行在一个 Hadoop集群上
-
MapReduce优缺点
-
优点:
-
MapReduce易于编程:
它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的 PC机器上运行
-
良好的扩展性:
当你的计算资源不能得到满足的时候,通过动态增加服务器来扩展它的计算能力
-
高容错性:
比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败 ,而且这个过程不需要人工参与,而完全是由 Hadoop内部完成的
-
适合PB级以上海量数据的离线处理:
可以实现上千台服务器集群并发工作,提供数据处理能力
-
-
缺点:
-
不擅长实时计算:
MapReduce 无法像MySQL 一样,在毫秒或者秒级内返回结果
-
不擅长流式计算:
流式计算的输入数据是动态的,而MapReduce 的输入数据集是静态的,不能动态变化。MapReduce 自身的设计特点决定(sparkstreaming与 flink)
-
不擅长DAG(有向无环图)计算:
- 多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出
- MapReduce 虽然可以做,但是使用后,每个MapReduce 作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下
-
-
5.1.1 MapReduce 核心思想
-
MapReduce核心编程思想
-
分布式的运算程序往往需要分成至少2 个阶段
-
第一个阶段: MapTask 并发实例,完全并行运行,互不相干
- 读数据,并按行处理
- 按空格切分行内单词
- KV键值对<单词,1 >
- 将所有的KV键值对,按照单词的首字母,分成两个分区溢写到磁盘
-
第二个阶段: ReduceTask 并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask 并发实例的输出
-
MapReduce 编程模型只能包含一个Map 阶段和一个Reduce 阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行
-
-
MapReduce 进程
一个完整的MapReduce 程序在分布式运行时有三类实例进程:
- MrAppMaster:负责整个程序的过程调度及状态协调
- MapTask:负责Map 阶段的整个数据处理流程
- ReduceTask:负责Reduce 阶段的整个数据处理流程
-
常用数据序列化类型
- Java中的String在MapReduce 中为Text
- 其余的数据类型,都是直接在后面加Writable
5.1.2 MapReduce 编程规范
-
Mapper阶段:
-
用户自定义的Mapper要继承自己的父类
-
Mapper的输入数据是KV对的形式(KV的类型可自定义)
<偏移量,一行的内容>
-
Mapper中的业务逻辑写在重写的map()方法中
-
Mapper的输出数据是KV对的形式(KV的类型可自定义)
<hadoop,1>,<hadoop,1>,<c,1>
-
map()方法(MapTask进程)对每一个<K,V>调用一次
-
-
Reducer阶段:
-
用户自定义的Reducer要继承自己的父类
-
Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
<hadoop,(1,1)>,<c,(1)>
-
Reducer的业务逻辑写在重写的reduce()方法中
-
Reducer的输出数据是KV对的形式
<hadoop,2>,<c,1>
-
ReduceTask进程对每一组相同k的<k,v>组调用一次reduce()方法
-
-
Driver阶段:
相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是
封装了MapReduce程序相关运行参数的job对象
5.1.3 WordCount案例
-
本地测试:
-
环境准备:
-
创建maven 工程,MapReduceDemo
-
在pom.xml 文件中添加如下依赖
<dependencies> <!--下载Hadoop依赖--> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-client</artifactId> <version>3.1.3</version> </dependency> <!--单元测试--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <!--打印日志--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.30</version> </dependency> </dependencies>
-
在项目的src/main/resources 目录下,新建一个文件,命名为“log4j.properties”
log4j.rootLogger=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n log4j.appender.logfile=org.apache.log4j.FileAppender log4j.appender.logfile.File=target/spring.log log4j.appender.logfile.layout=org.apache.log4j.PatternLayout log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
-
创建包名:wordcount
-
-
编写程序:
-
Mapper类
/* * KEYIN:map阶段输入的key的类型——LongWritable(每行的偏移量) * VALUEIN:map阶段输入的value的类型——Text(每行的文本内容) * KEYOUT:map阶段输出的key的类型——Text(切分出来的每个单词) * VALUEOUT:map阶段输出的value的类型——IntWritable(1) * */ public class WcMapper extends Mapper<LongWritable,Text,Text,IntWritable> { //map()每一行都会调用一次,避免每行都new对象,造成开销 //将用于数据类型转化的实例化写到map()外边 private Text outKey = new Text(); private IntWritable outValue = new IntWritable(); /** * @param key 每行的偏移量 * @param value 每行的文本内容 * @param context 上下文进行map和reduce之间的联络,及和系统之间的联络 * * 写出的形式:< hadoop,1 > < hadoop,1 > < java,1 > < c++,1 >... */ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { //1. 获取一行 // 此时为Text类型,为了使用String丰富的API,对数据处理,需要先转化为String String line = value.toString(); //2. 切分出每个单词 String[] words = line.split("\t"); //2. 循环写出 for (String word:words ) { //再将String和int类型,写回为Text和IntWritable outKey.set(word); outValue.set(1); context.write(outKey,outValue); } } }
-
Reducer类
/* * KEYIN:reduce阶段输入的key的类型——Text * VALUEIN:reduce阶段输入的value的类型——IntWritable * KEYOUT:reduce阶段输出的key的类型——Text * VALUEOUT:reduce阶段输出的value的类型——IntWritable * */ public class WcReducer extends Reducer<Text,IntWritable,Text,IntWritable> { private IntWritable outValue = new IntWritable(); /** * @param key 且分出的单词 * @param values 注意:并不是一个迭代器,集合的顶级父类,可以看作一个集合 * (1,1) (1,1,1) ... * @param context 上下文进行map和reduce之间的联络,及和系统之间的联络 * * 写出的形式:< hadoop,2 > < java,3 > < c++,1 >... */ @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int sum = 0; //统计累加 for (IntWritable value : values) { sum += value.get(); } //数据类型转化 outValue.set(sum); context.write(key,outValue); } }
-
Driver驱动类
public class WcDriver { public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException { //1. 获取job Configuration config = new Configuration(); Job job = Job.getInstance(config); //2. 设置jar包路径 job.setJarByClass(WcDriver.class); //3. 关联mapper和reducer job.setMapperClass(WcMapper.class); job.setReducerClass(WcReducer.class); //4. 设置map输出的kv类型 job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); //5. 设置最终输出的kv类型 job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); //6. 设置输出路径和输出路径 //⭐注意:这里的FileInputFormat的包为org.apache.hadoop.mapreduce.lib.input.FileInputFormat中的 //FileOutputFormat同样 FileInputFormat.setInputPaths(job, new Path("D:/mapreduceTest/input")); FileOutputFormat.setOutputPath(job,new Path("D:/mapreduceTest/output")); //注:输出文件的路径不能存在 //7. 提交作业 boolean result = job.waitForCompletion(true); System.exit(result ? 0 : 1); } }
-
-
输出结果:
c 1 hadoop 1 java 2 javba 1
-
运行报错:
java.lang.UnsatisfiedLinkError: boolean org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(java.lang.String, int)错误
解决方法:把Hadoop的bin目录下的文件复制到windows/system32下
-
DeBug测试:
-
map
-
reduce
-
-
mapper与reducer源码:
public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> { public Mapper() { } protected void setup(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException { } protected void map(KEYIN key, VALUEIN value, Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException { context.write(key, value); } protected void cleanup(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException { } public void run(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException { this.setup(context); //初始化环境 try { while(context.nextKeyValue()) { //执行map方法(需要重写) this.map(context.getCurrentKey(), context.getCurrentValue(), context); } } finally { this.cleanup(context); //清空运行环境 } } public abstract class Context implements MapContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> { public Context() { } } }
-
-
集群测试:
-
Driver驱动类中的输入输出路径是windows本地的,不是集群上的文件目录;同时在集群上执行程序时想要动态地使用输入和输出路径,故需修改代码:
FileInputFormat.setInputPaths(job, new Path(args[0])); FileOutputFormat.setOutputPath(job,new Path(args[1]));
-
添加maven打包需要的插件
<plugins> <!--maven项目打包插件--> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <!--将所有依赖的jar包也一起打包的插件--> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins>
-
使用maven将程序打包:(集群上有程序所依赖的hadoop.jar包,故直接使用不带依赖的jar包即可)
-
-
将jar包拷贝到Hadoop集群的/opt/module/hadoop-3.1.3
-
启动集群
-
执行程序
[cool@hadoop102 hadoop-3.1.3]$ hadoop jar wc.jar WordCountTest1.WcDriver /input /output //使用driver类的全类名