第6章 Map Reduce上手实践
导读
学习一门框架编程技术,在了解框架整体功能特性和工作机制后,快速上手的方式就是利用这个框架来写出属于自己的第一个程序。
本章就以非常典型且能相当好地诠释MAP REDUCE特性的词频统计为例,以详尽的步骤,引导读者成功开发并运行自己的第一个MAP REDUCE分布式数据处理程序。
6.1 Map Reduce入门编程案例
6.1.1 案例需求
假设有大量文本文件,内容样例如下:
hello jack
hello tom
hello jim
tom and jack are good friends
jim and tom are good friends too
需要统计出所有文件中各单词出现的总次数,即词频统计。
6.1.2 思路设计
回顾
MAP REDUCE程序中,整个数据处理的流程分为map和reduce两个阶段,而这两个阶段中真正工作的程序是Map Task和Reduce Task,这两个程序已在框架中提供,而用户在编写自己的MAP REDUCE程序时,则只需要为Map Task和Reduce Task提供进行数据处理时的具体逻辑函数,即map()函数和reduce()函数。
初学阶段开发mapreduce程序时,可以不用过多考虑mapreduce程序运行时的状况,只要抓住mapreduce编程模型中的要点即可,即考虑map阶段读取到原始数据后产生出什么样的key和value,以便能在reduce阶段按key分组聚合运算时能得到需求的结果。
本例逻辑相对简单,说明如下:
- map阶段:
- Map Task会从HDFS的源数据文件中逐行读取数据,然后对每一行调用一次用户自定义的map()方法;
- 用户自定义的map()函数中:
- 将每一行数据切分出单词;
- 为每一个单词构造一个键值对(单词,1);
- 将键值对返回给Map Task。
- reduce阶段:
- Reduce Task会获取map阶段输出的单词键值对;
- Reduce Task会将key相同(即单词相同)的键值对汇聚成组;然后对每一组kv数据调用一次用户自定义的reduce()方法来运算处理;
- 在用户自定义的reduce()函数中:
- 遍历reduce Task所传入的一组kv数据,将value值累加求和,即得到每一个单词的总次数;
- 将计算后得到的(单词,总次数)返回给reduce task。
wordcount逻辑示意图:
![da26f580e8b3582f245a2a6bdda76d88.png](https://i-blog.csdnimg.cn/blog_migrate/26996096d77539bdeac6f810a64bc5ba.jpeg)
6.1.3 关键技术点解析
我们在开发自定义的Mapper类时,需要定义输入数据的泛型类型(KEY IN,VALUE IN)。而这些泛型的确定,依赖于输入数据的类型。
输入数据的类型决定机制描述如下:
Map Task默认情况下是调用TextInputFormat获取一个LineRecordReader来读取文件,其机制为逐行读取,每读到一行数据,就将这一行在文件中的起始偏移量(类型为long)作为key数据,行的文本内容(类型为String)作为value数据;
map()函数处理后产生的数据也需要组织成一个key和一个value,也需要为此声明输出数据的泛型类型(KEY OUT, VALUE OUT);这两个泛型的声明,则取决于我们自定义的map()方法将要生成的结果key-value数据的类型;
而mapreduce中,数据经常需要持久化到磁盘文件,或经网络连接进行传输,而不管持久化到文件还是通过网络传输,key-value数据都需要序列化和反序列化操作。为了降低数据的体积,提高效率,Hadoop开发了一种自己的系列化框架。上述的key-value数据类型也就不能直接采用JDK中提供的原生类型,而是需要使用实现了Hadoop序列化接口的类型,如下:
- KEY IN : 行的起始偏移量,原生类型long,对应的Hadoop序列化类型:LongWritable
- VALUE IN:行的文本内容,原生类型String,对应的Hadoop序列化类型:Text
- KEY OUT: 一个单词,原生类型String,对应的Hadoop序列化类型:Text
- VALUE OUT: 单词次数1,原生类型int,对应的Hadoop序列化类型:IntWritable
提示
所谓的序列化,就是将内存中的java对象结构数据“压扁”成一个线性的二进制数据序列,反序列化则反之。由jdk中的序列化机制所生成的二进制序列中,不仅包含对象中的数据,还包含很多附加信息(如类名、继承关系等),换句话说就是jdk的序列化机制所产生的序列化结果非常臃肿和冗余。
在数据量很大且需要频繁进行磁盘序列化和网络传输的情况下,如果用jdk的原生序列化机制,会导致数据传输和存储的负担太大。所以Hadoop开发了一套自己的序列化框架,对象序列化后包含的信息仅限于对象中的数据内容,从而大大降低磁盘存储和网络传输的负担,提高效率。
由于本书侧重于快速上手和实战,限于篇幅所限,对于细节性原理的阐述不便面面俱到,如有需要更详细更透彻地理解,可扫描二维码来获取更多文档资料和视频讲解进行学习。
6.1.4 工程搭建
在eclipse中新建一个工程,并引入用于开发MapReduce程序的jar包;
方式1:使用maven的方式引入jar包
使用maven来引入依赖,可以在工程的pom.xml中,添加如下依赖配置:
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.8.1</version>
</dependency>
方式2:直接使用本地jar包
从hadoop的安装目录中,拷贝所有jar包到项目工程中,并添加到buildpath即可,如图:
![4c4d70f32258f8bcb726137494e6870c.png](https://i-blog.csdnimg.cn/blog_migrate/718ff4edce7a6dbe69528e326280441c.png)
图6.2 工程结构
6.1.5 Mapper开发
map函数的逻辑:
- 将map task所读取到的一行数据按照单词分隔符切分得到一个字符串数组;
- 遍历字符串数组;
- 对遍历到的每一个单词,通过map task所提供的context来返回一个<单词,1>的key-value数据对;
在工程中新建一个类WordCountMapper并继承Mapper,具体请看示例代码及代码注释:
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
/**
* maptask程序在自己的任务范围数据中(数据切片)
* 每读取一行数据,就调一次本map()方法,并将读到的数据以key-value形式作为参数传给本map()方法
* map()方法按照所需的数据处理逻辑对传入的数据进行处理,然后通过context返回给map task程序
*/
@Override
protected void map(LongWritable key, Text value,Context context) throws IOException, InterruptedException {
// key在默认情况下是maptask所读取到的一行文本的起始偏移量
// value在默认情况下是maptask所读取到的一行文本的字符内容
// 将文本按空格符号切分成单词数组
String[] words = value.toString().split(" ");
// 遍历数组中的每一个单词,将每个单词变成 (单词,1)这样的kv对数据,通过context返回给maptask
for (String word : words) {
context.write(new Text(word), new IntWritable(1));
}
}
}
6.1.6 Reducer类开发
reduce函数的逻辑:
- Reduce Task会将自己所收到的key-value数据按照key进行排序和分组,相同key的数据会连续排放在一起,key“较小”的数据排在前面,然后,将key相同的key-value数据作为一组来进行聚合处理;
- Reduce Task对每一组数据处理时,都是调用用户提供的reduce方法,并传入一个key对象和一个迭代器对象给reduce方法来获取这一组数据中的每一个key-value数据;
- 在reduce函数中,我们只需要使用框架所提供的迭代器来迭代reduce task提供的一组key-value数据(具体来说,就是循环反复调用迭代器的next()方法来逐一获取这组数据中的每一对key-value数据);
- 将每次迭代到的value值进行累加,迭代完成之后,累加值即为某一个key(即某个单词)的总词频;
- 将统计出的这个单词及其总词频,通过reduce task所提供的context进行返回。
在工程中新建一个类WordCountReducer并继承Reducer,具体请看示例代码及注释:
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
/**
* Reduce Task收集到一组相同key的数据,调一次本方法,比如(a,1)(a,1)(a,1)......(a,1)
*/
@Override
protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
int count = 0;
// 每迭代一次,迭代器就会取一对key-value数据:
// 取到的key数据重新设置给方法中的参数key
// 取到的value数据则会设置给以下for循环中的临时变量value
for (IntWritable value : values) {
// 将本次迭代到的value值累加到count变量
count += value.get();
}
// 将这一组数据的聚合结果通过context返回给reduce task
context.write(key, new IntWritable(count));
}
}
6.1.7 job提交客户端开发
自定义Mapper类和自定义Reducer类开发好后,还需要开发一个程序,用于设置关于这个mapreduce job的相关参数,并将这些参数及整个程序jar包提交到Yarn上去启动执行。
在工程中新建一个类WordCountDriver类,并定义一个main方法,具体请看示例代码及注释:
public class WordcountDriver {
public static void main(String[] args) throws Exception {
// 需要将大量的跟jar包程序运行相关的参数进行描述
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//job.setJar("/root/wc.jar");
job.setJarByClass(WordcountDriver.class);
// 指定本次job所要用的mapper类和reducer类
job.setMapperClass(WordCountMapper.class);
job.setCombinerClass(WordCountCombiner.class);
job.setReducerClass(WordCountReducer.class);
// 指定本次job的mapper类和reducer类输出结果的数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
// 指定本次job读取源数据时所需要用的输入组件:源文件在hdfs的文本文件中,用TextInputFormat
job.setInputFormatClass(TextInputFormat.class);
// 指定本次job输出数据时所需要用的输出组件:输出到hdfs文本文件中,用TextOutputFormat
job.setOutputFormatClass(TextOutputFormat.class);
// 指定reduce task运行实例的个数
job.setNumReduceTasks(3);
// 指定源数据文件所在的路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
// 指定输出结果数据文件所在的路径
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 核心代码: 提交jar包给yarn
// job.submit(); // 提交完任务,客户端就退出了
// 提交完任务,客户端打印集群中的运行进度信息,并等待最终运行状态
boolean res = job.waitForCompletion(true);
System.exit(res?0:1);
}
}