
【作者主页】Francek Chen
【专栏介绍】⌈ ⌈ ⌈大数据技术原理与应用 ⌋ ⌋ ⌋专栏系统介绍大数据的相关知识,分为大数据基础篇、大数据存储与管理篇、大数据处理与分析篇、大数据应用篇。内容包含大数据概述、大数据处理架构Hadoop、分布式文件系统HDFS、分布式数据库HBase、NoSQL数据库、云数据库、MapReduce、Hadoop再探讨、数据仓库Hive、Spark、流计算、Flink、图计算、数据可视化,以及大数据在互联网领域、生物医学领域的应用和大数据的其他应用。
【GitCode】专栏资源保存在我的GitCode仓库:https://gitcode.com/Morse_Chen/BigData_principle_application。
文章目录
-
- 一、任务要求
- [二、编写 Map 处理逻辑](#二、编写 Map 处理逻辑)
- [三、编写 Reduce 处理逻辑](#三、编写 Reduce 处理逻辑)
- [四、编写 main 方法](#四、编写 main 方法)
- 五、编译打包代码以及运行程序
- 小结
在大数据处理架构 Hadoop 中,我们已经介绍了如何在单台机器上搭建伪分布式 Hadoop 环境,并介绍了如何利用 Hadoop 自带的实例程序来分析数据。现在我们来介绍如何编写基本的 MapReduce 程序帮助自己实现数据分析。下面首先给出基本任务要求,然后阐述如何编写 MapReduce 程序来实现任务要求。这里采用的 Hadoop 版本为 3.1.3。
一、任务要求
在大数据处理架构 Hadoop 中,我们运行 Hadoop 自带的实例程序统计了"input"文件夹下所有文件中每个单词出现的次数。在 MapReduce:04 实例分析:WordCount 中我们介绍了用 MapReduce 程序实现单词出现次数统计的基本思路和具体执行过程。下面我们介绍如何编写具体实现代码以及如何运行程序。
首先,我们在本地创建两个文件,即文件 A 和 B。
文件 A 的内容如下:
text
China is my motherland
I love China
文件 B 的内容如下:
text
I am from China
假设 HDFS 中已经创建好了一个 input 文件夹,现在把文件 A 和 B 上传到 HDFS 中的 input 文件夹下(注意,上传之前,请清空 input 文件夹中原有的文件)。现在的目标是统计 input 文件夹下所有文件中每个单词的出现次数,也就是说,程序应该输出如下形式的结果:
text
I 2
is 1
China 3
my 1
love 1
am 1
from 1
motherland 1
接下来,我们编写 MapReduce 程序来实现这个功能,主要包括以下几个步骤。
(1)编写 Map 处理逻辑。
(2)编写 Reduce 处理逻辑。
(3)编写 main 方法。
(4)编译打包代码以及运行程序。
二、编写 Map 处理逻辑
为了把文档处理成我们希望的效果,首先需要对文档进行切分。通过前面的内容我们可以知道,数据处理的第一个阶段是 Map 阶段,在这个阶段中文本数据被读入并进行基本的分析,然后以特定的键值对的形式进行输出,这个输出将作为中间结果,继续提供给 Reduce 阶段作为输入数据。
在本例中,我们通过继承类 Mapper 来实现 Map 处理逻辑。首先,为类 Mapper 设定好输入类型以及输出类型。这里,Map 的输入是 <key,value> 形式,其中,key 是文本文件中一行的行号,value 是该行号对应的文件中的一行内容。实际上,在代码逻辑中,key 值并不需要用到。对于输出的类型,我们希望在 Map 部分完成文本分割工作,因此输出应该为 <单词,出现次数> 的形式。于是,最终确定的输入类型为 <Object,Text>,输出类型为 <Text,IntWritable>。其中,除了 Object 以外,都是 Hadoop 提供的内置类型。为实现具体的分析操作,我们需要重写 Mapper 中的 map 函数。以下为 Mapper 类的具体代码:
java
public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
private static final IntWritable one = new IntWritable(1);
private Text word = new Text();
public TokenizerMapper() {
}
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()) {
this.word.set(itr.nextToken());
context.write(this.word, one);
}
}
}
在上述代码中,实现 Map 逻辑的类名称为 TokenizerMapper。在 TokenizerMapper 类中,首先将需要输出的两个变量 one 和 word 初始化。对于变量 one,可以将其直接初始化为 1,表示某个单词在文档中出现过。在 Map 函数中,前两个参数是函数的输入,value 为 text 类型,是指每次读入文本的一行,Object 类型的 key 则是指该行数据在文本中的行号。在我们这个简单的示例中,key 其实并没有被明显地用到。然后,通过 StringTokenizer 这个类以及其自带的方法,对 value 变量(即文本中的一行)进行拆分,拆分后的单词存储在 word 中,one 作为单词计数。实际上,在函数的整个执行过程中,one 的值一直为 1。Context 是 Map 函数的一种输出方式,通过写该变量,可以直接将中间结果存储在其中。按照这样的处理逻辑,第二个文件在 Map 后输出的中间结果如下:
text
<"I",1>
<"am",1>
<"from",1>
<"China",1>
三、编写 Reduce 处理逻辑
在 Map 部分得到中间结果后,接下来进入 Shuffle 阶段。在这个阶段中 Hadoop 自动将 Map 的输出结果进行分区、排序、合并,然后分发给对应的 Reduce 任务来处理。下面给出 Shuffle 过程后的结果,这也是 Reduce 任务的输入数据:
text
<"I",<1,1>>
<"is",1>
......
<"from",1>
<"China",<1,1,1>>
Reduce 阶段需要对上述数据进行处理并得到我们最终期望的结果。其实,在这里已经可以很清楚地看到 Reduce 需要做的事情,就是对输入结果中的数字序列进行求和。下面给出 Reduce 处理逻辑的具体代码:
java
public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
public IntSumReducer() {
}
public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
this.result.set(sum);
context.write(key, this.result);
}
}
类似于 Map 的实现,这里仍然需要继承 Hadoop 提供的类并实现其接口(重写其方法)。这里编写的类的名字为 IntSumReducer,它继承自类 Reducer。至于 Reduce 过程的输入/输出类型,从上面代码中可以发现,它们与 Map 过程的输出类型本质上是相同的。在代码的开始部分,我们设置变量 result 来记录每个单词的出现次数。为了具体实现 Reduce 部分的处理逻辑,我们需要重写 Reducer 类所提供的 Reduce 函数。在 Reduce 函数中我们可以看到,其输入类型较 Map 过程的输出类型发生了一点小小的变化,即 IntWritable 变量经过 Shuffle 阶段处理后,变为 Iterable 容器。在 Reduce 函数中,我们会遍历这个容器,并对其中的数字进行累加,最终可以得到每次单词总的出现次数。同样,在输出时,我们仍然使用 Context 类型的变量存储信息。当 Reduce 过程结束时,就可以得到最终需要的数据了。
四、编写 main 方法
为了让 TokenizerMapper 和 IntSumReducer 类能够协同工作,我们需要在主函数中通过 Job 类设置 Hadoop 程序运行时的环境变量,以下是具体代码:
java
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] otherArgs = (new GenericOptionsParser(conf, args)).getRemainingArgs();
if (otherArgs.length < 2) {
System.err.println("Usage: wordcount <in> [<in>...] <out>");
System.exit(2);
}
Job job = Job.getInstance(conf, "word count"); // 设置环境参数
job.setJarByClass(WordCount.class); // 设置整个程序的类名
job.setMapperClass(WordCount.TokenizerMapper.class); // 添加 Mapper 类
job.setReducerClass(WordCount.IntSumReducer.class); // 添加 Reducer 类
job.setOutputKeyClass(Text.class); // 设置输出类型
job.setOutputValueClass(IntWritable.class); // 设置输出类型
for (int i = 0; i < otherArgs.length - 1; ++i) {
FileInputFormat.addInputPath(job, new Path(otherArgs[i])); // 设置输入文件
}
FileOutputFormat.setOutputPath(job, new Path(otherArgs[otherArgs.length - 1])); // 设置输出文件
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
在代码的开始部分,我们通过类 Configuration 获得程序运行时的参数情况,并将它们存储在 String\[\] otherArgs 中。随后,我们通过类 Job 设置环境参数。首先,设置整个程序的类名为 WordCount.class(这个类包含了单词统计的全部实现代码)。然后,添加已经写好的 TokenizerMapper 类和 IntSumReducer 类。接下来,还需要设置整个 Hadoop 程序的输出类型,即 Reduce 输出结果 <key, value> 中 key 和 value 各自的类型。最后,根据之前已经获得的程序运行时的参数,设置输入/输出文件路径。
五、编译打包代码以及运行程序
下面给出 WordCount 类的完整代码:
java
import java.io.IOException;
import java.util.StringTokenizer;
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.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
public class WordCount {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] otherArgs = (new GenericOptionsParser(conf, args)).getRemainingArgs();
if (otherArgs.length < 2) {
System.err.println("Usage: wordcount <in> [<in>...] <out>");
System.exit(2);
}
Job job = Job.getInstance(conf, "word count");
job.setJarByClass(WordCount.class);
job.setMapperClass(TokenizerMapper.class);
job.setCombinerClass(IntSumReducer.class);
job.setReducerClass(IntSumReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
for (int i = 0; i < otherArgs.length - 1; ++i) {
FileInputFormat.addInputPath(job, new Path(otherArgs[i]));
}
FileOutputFormat.setOutputPath(job, new Path(otherArgs[otherArgs.length - 1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
private static final IntWritable one = new IntWritable(1);
private Text word = new Text();
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()) {
word.set(itr.nextToken());
context.write(word, one);
}
}
}
public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
}
大家可能对程序最初引用的许多外部包有些疑惑,其实它们大部分是 Hadoop 自己的组件,也被称为 Hadoop 的 API,这些包的基本功能见表1。
表1 WordCount类中引用的包的基本功能
| 包 | 功能 |
|---|---|
| org.apache.hadoop.conf | 定义了系统参数的配置文件处理方法 |
| org.apache.hadoop.fs | 定义了抽象的文件系统 API |
| org.apache.hadoop.mapreduce | Hadoop 分布式计算框架 MapReduce 的实现,包括任务的分发调度等 |
| org.apache.hadoop.io | 定义了通用的 I/O API,用于针对网络、数据库、文件等数据对象进行读写操作等 |
由于在安装 Hadoop 之前,我们已经安装了 Java 程序(JDK),所以这里可以直接用 JDK 包中的工具对代码进行编译。在执行下面操作之前,请把当前工作目录设置为 Hadoop 的安装目录,即"/usr/local/hadoop"目录:
bash
cd /usr/local/hadoop
export CLASSPATH="/usr/local/hadoop/share/hadoop/common/hadoop-common-3.1.3.jar: /usr/local/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-client-core-3.1.3.jar:/usr/local/hadoop/share/hadoop/common/lib/commons-cli-1.2.jar:$CLASSPATH"
javac WordCount.java
如果系统环境找不到 javac 程序的位置,那么请使用 JDK 中的绝对路径。
编译之后,在文件夹下可以发现有 3 个".class"文件,这是 Java 的可执行文件。此时,我们需要将它们打包并命名为 WordCount.jar,命令如下:
bash
jar -cvf WordCount.jar *.class
到这里,我们就得到像 Hadoop 自带实例一样的 JAR 包了,可以运行得到结果。在运行程序之前,需要启动 Hadoop,包括 HDFS 和 MapReduce。启动 Hadoop 之后,我们可以运行程序,命令如下:
bash
./bin/hadoop jar WordCount.jar WordCount input output
最后,我们可以运行下面命令查看结果:
bash
./bin/hadoop fs -cat output/*
小结
本文介绍了基于 Hadoop 3.1.3 编写 WordCount MapReduce 程序的完整流程。任务目标是统计 HDFS 中 input 文件夹下文件 A 和 B 内每个单词的出现次数。实现分为四步:编写 Mapper 类(TokenizerMapper),将每行文本切分为单词,输出 <单词,1> 键值对;编写 Reducer 类(IntSumReducer),对相同单词的计数进行累加求和;编写 main 方法,通过 Job 类设置运行环境,包括指定 Mapper、Reducer、输入输出路径及类型;最后编译打包成 JAR 文件并提交执行。程序使用了 Hadoop 的核心 API 包,如 org.apache.hadoop.mapreduce 等。通过此实例,大家可以掌握 MapReduce 程序的基本编写方法,理解 Map 阶段的数据切分与 Reduce 阶段的聚合汇总逻辑。
欢迎 点赞👍 | 收藏⭐ | 评论✍ | 关注🤗
