MapReduce基础编程实践(Hadoop 3.5.0和TRAE)

大数据学习路线图

【版权声明】版权所有,请勿转载!

【相关文章推荐】《大数据软件安装和基础编程实践指南》,详细指导VMWare、Ubuntu、Hadoop、HDFS、HBase、Hive、MapReduce、Spark、Flink的安装和基础编程

作者:厦门大学计算机系2025级硕士研究生郑楷阳
指导老师:厦门大学计算机系林子雨副教授

本博客内容与林子雨编著《大数据技术原理与应用(第5版)》(访问教材官网)教材配套。

MapReduce是一种分布式计算编程模型,Hadoop提供了它的开源实现。它把数据处理过程抽象为Map和Reduce两个主要阶段,使编程人员能够通过编写映射和归并逻辑完成并行计算。

本教程以词频统计任务为主线,介绍创建Java项目、添加依赖、编写程序、导出JAR以及读取HDFS上的输入数据进行计算的方法。本次截图所示环境为Ubuntu虚拟机、TRAE CN、Hadoop 3.5.0和JDK 17;JDK运行路径为/usr/lib/jvm/jdk-17.0.20.1+1。

Hadoop需要提前完成安装和HDFS配置。Hadoop 3.5.0服务端要求Java 17,本教程的项目与Hadoop运行环境都使用JDK 17。

本次实际作业日志显示LocalJobRunner,即MapReduce任务在本机执行,输入和输出使用HDFS。第6节将明确采用这一运行方式。

1. 词频统计任务要求

首先在Ubuntu本地文件系统中创建两个文件wordfile1.txt和wordfile2.txt。本实例为了便于观察结果,每个文件只包含两行英文文本。

wordfile1.txt的内容如下:

I love Spark
I love Hadoop

wordfile2.txt的内容如下:

Hadoop is good
Spark is fast

本教程将这两个文件保存到/usr/local/hadoop目录中。可以在Ubuntu终端执行下面的命令创建文件:

cd /usr/local/hadoop
cat > wordfile1.txt <<'EOF'
I love Spark
I love Hadoop
EOF
cat > wordfile2.txt <<'EOF'
Hadoop is good
Spark is fast
EOF

任务要求是:把两个本地文件上传到HDFS的/user/hadoop/input目录,统计该目录下所有输入文件中的单词出现次数,并把结果写入HDFS的/user/hadoop/output目录。

本程序按空白字符分词,区分大小写,不额外去除标点。例如Spark和spark会被视为不同的单词。对于上述输入,预期结果为:

Hadoop  2
I   2
Spark   2
fast    1
good    1
is  2
love    2

单词与次数之间使用制表符分隔。本实例设置一个Reduce任务,输出会按单词的键排序;结果顺序与英文单词出现的先后顺序不同。

开始操作前,先区分下面几类路径:

用途 本教程使用的路径 所属文件系统
原始文本 /usr/local/hadoop/wordfile1.txt和wordfile2.txt Ubuntu本地文件系统
Java项目 /home/hadoop/Documents/trae_projects/WordCount Ubuntu本地文件系统,项目位置可自行选择
JAR部署目录 /usr/local/hadoop/myapp Ubuntu本地文件系统
作业输入 /user/hadoop/input HDFS
作业输出 /user/hadoop/output HDFS

2. 在TRAE中创建项目

启动安装在Ubuntu虚拟机中的TRAE CN。如果打开的是图7-1所示页面,点击右上角的“IDE”进入开发界面。

图7-1 进入TRAE的IDE界面

在IDE界面中,可以点击“打开文件夹”,选择准备存放Java项目的父目录。本次截图使用/home/hadoop/Documents/trae_projects;读者也可以选择自己的项目存放目录。

图7-2 打开项目父目录

如果尚未安装Java扩展,请在左侧扩展面板中搜索并安装“Extension Pack for Java”。已经完成前面HDFS或HBase实践、并已安装该扩展包的读者,可以直接继续操作。

在左侧资源管理器的空白处点击鼠标右键,选择“创建Java项目...”。也可以按Ctrl+Shift+P打开命令面板,执行“Java: Create Java Project...”。

图7-3 创建Java项目

在项目类型选择界面中选择“No build tools”,创建一个不使用Maven或Gradle的普通Java项目。

图7-4 选择No build tools项目类型

随后在文件夹选择窗口中选中项目的父目录,点击右下角的“Select the project location”。这里选择的是父目录,Java扩展会在其中创建项目文件夹。

图7-5 选择项目存放目录

在项目名称输入框中输入“WordCount”,按Enter确认。

图7-6 输入项目名称WordCount

创建完成后,打开新建的WordCount项目文件夹。本次项目目录为/home/hadoop/Documents/trae_projects/WordCount。项目中包含src、bin、lib和.vscode等目录,并在src中生成App.java。

点击打开App.java,等待Java扩展完成项目初始化。如下图所示,底部的状态为“Java: Ready”,左侧下方也已出现“Java Projects”视图。

图7-7 打开App.java并等待Java项目就绪

展开“Java Projects”。如果该视图没有显示,可以在资源管理器标题栏的“...”菜单中勾选“Java Projects”。如果Java扩展提示当前处于轻量模式,按状态栏提示切换到标准模式,才能加载依赖、构建和运行项目。

按Ctrl+Shift+P,执行“Java: Configure Java Runtime”,确认WordCount项目使用JDK 17。本次实际运行截图中的JDK路径为/usr/lib/jvm/jdk-17.0.20.1+1;安装路径不同的读者,应选择自己的JDK 17目录。项目JDK与Java语言服务的运行JDK可以分别设置,不能仅凭语言服务已启动就认定项目JDK已经配置正确。

3. 为项目添加需要用到的JAR包

程序使用Hadoop提供的Java API,因此需要把相应的JAR添加到项目依赖中。本教程的Hadoop安装目录为/usr/local/hadoop。

在“Java Projects”视图中展开WordCount,找到“Referenced Libraries(引用的库)”,点击该行右侧的“+”,打开JAR文件选择窗口。

先进入/usr/local/hadoop/share/hadoop目录,可以看到common、hdfs、mapreduce和yarn等子目录。图7-8显示的是进入mapreduce之前的目录选择界面。

图7-8 进入Hadoop依赖目录

本实验通过TRAE执行的是编译和无参数的入口检查;实际作业由第6节的Hadoop命令运行。按下面的顺序添加本次截图中的依赖:

添加顺序 目录 选择内容
1 /usr/local/hadoop/share/hadoop/mapreduce 当前目录中的.jar文件,子目录不选
2 /usr/local/hadoop/share/hadoop/common hadoop-common-3.5.0.jar和hadoop-nfs-3.5.0.jar
3 /usr/local/hadoop/share/hadoop/common/lib 当前目录中的所有.jar文件

先打开mapreduce目录,选中当前目录中的JAR文件,再点击“Select Jar Libraries”。不要把jdiff、lib或sources文件夹作为JAR文件选中

图7-9 选择mapreduce目录中的JAR文件

再次点击“Referenced Libraries”右侧的“+”,进入common目录。按住Ctrl分别选中hadoop-common-3.5.0.jar和hadoop-nfs-3.5.0.jar,再点击“Select Jar Libraries”。

图7-10 选择common目录中的两个JAR文件

然后重复上述操作,进入common/lib目录,选择其中的所有JAR文件,再点击“Select Jar Libraries”。当该目录下显示的都是JAR文件时,可以使用Ctrl+A全选。这里不能漏掉common/lib中的第三方依赖。

图7-11 选择common/lib中的依赖JAR文件

全部添加后,等待Java扩展刷新项目。在“Referenced Libraries”中可以检查已添加的依赖。若后面代码中的org.apache.hadoop导入语句仍显示错误,先核对依赖文件是否选中、项目是否完成加载,以及所选文件是否都来自当前Hadoop 3.5.0安装目录。

4. 编写Java应用程序

在左侧资源管理器中找到WordCount项目的src目录,点击鼠标右键,依次选择“创建Java类”→“类...”。

图7-12 创建Java类

在输入框中输入“WordCount.java”,按Enter确认。本次截图的输入方式包含.java扩展名,创建后应检查生成的文件名确实为WordCount.java。

图7-13 输入WordCount.java

打开WordCount.java,清空自动生成的内容,输入下面的完整程序并按Ctrl+S保存。源文件直接放在src目录下,不声明package;public class名称必须与文件名WordCount.java一致。App.java是自动生成的演示文件,后面导出时应选择WordCount作为入口。

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 class TokenizerMapper
            extends Mapper<Object, Text, Text, IntWritable> {
        private static final IntWritable ONE = new IntWritable(1);
        private final Text word = new Text();

        @Override
        public void map(Object key, Text value, Context context)
                throws IOException, InterruptedException {
            StringTokenizer tokens = new StringTokenizer(value.toString());
            while (tokens.hasMoreTokens()) {
                word.set(tokens.nextToken());
                context.write(word, ONE);
            }
        }
    }

    public static class IntSumReducer
            extends Reducer<Text, IntWritable, Text, IntWritable> {
        private final IntWritable result = new IntWritable();

        @Override
        public void reduce(Text key, Iterable<IntWritable> values, Context context)
                throws IOException, InterruptedException {
            int sum = 0;
            for (IntWritable value : values) {
                sum += value.get();
            }
            result.set(sum);
            context.write(key, result);
        }
    }

    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);
        job.setNumReduceTasks(1);

        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);
    }
}

5. 编译打包程序

5.1 检查编译与程序入口

保存WordCount.java后,等待Java扩展完成构建。确认“问题”面板中没有阻止编译的错误,再点击编辑器右上角的“Run Java”,或main方法上方的“Run”。

图7-14 编写程序并点击Run Java

此时没有传入输入和输出路径,程序会在终端显示:

Usage: wordcount <in> [<in>...] <out>

图7-15 无参数运行时显示Usage提示

这是参数不足时的用法提示,程序按代码退出,退出状态为2。它说明程序已经进入main方法的参数检查部分,并不表示词频统计作业已经完成。完整运行见第6节。

5.2 导出WordCount.jar

按Ctrl+Shift+P打开命令面板,输入“Java: Export Jar”,选择“Java: 导出到Jar文件...”或对应的“Java: Export Jar...”命令。

图7-16 执行Java导出JAR命令

在“Export Jar: Determine main class”界面中,选择“WordCount”,按Enter确认。不要选择App,也不要选择“without main class”。

图7-17 选择WordCount主类

随后进入“Export Jar: Determine elements”界面。取消所有依赖JAR的勾选,只保留bin编译输出,再点击右上角的“确定”。

图7-18 查看导出内容的默认选择状态

等待导出结束,确认日志显示导出成功,默认导出到项目根目录下。

在Ubuntu终端中检查JAR内容:

cd ~/Documents/trae_projects/WordCount
jar tf WordCount.jar

输出中应包含下面三个编译类;JAR中还可能包含App.class和清单等其他条目:

WordCount.class
WordCount$TokenizerMapper.class
WordCount$IntSumReducer.class

如果缺少WordCount的主类或内部类,返回项目检查编译错误,完成构建后重新导出。

5.3 复制到Hadoop部署目录

在Ubuntu终端中执行:

mkdir -p /usr/local/hadoop/myapp
cd ~/Documents/trae_projects/WordCount
cp WordCount.jar /usr/local/hadoop/myapp/
cd /usr/local/hadoop/myapp
ls

其中项目目录应与第2节创建项目时的实际选择一致。下图是执行结果。

图7-19 复制JAR到myapp并查看文件

此时WordCount.jar已经部署到/usr/local/hadoop/myapp,下一步使用Hadoop命令运行它。

6. 运行程序

6.1 启动HDFS并检查输入目录

如果HDFS尚未启动,在Ubuntu终端执行:

cd /usr/local/hadoop
./sbin/start-dfs.sh

服务已启动时无需重复启动。本实验沿用已有Hadoop安装与HDFS配置。可以查看默认文件系统设置:

cd /usr/local/hadoop
./bin/hdfs getconf -confKey fs.defaultFS

结果应指向已配置的HDFS服务,即以hdfs://开头。如果显示file:///,请先完成HDFS配置再继续,这时使用/user/hadoop/input并不会自动把数据放到HDFS中。

创建输入目录并查看其中的内容:

cd /usr/local/hadoop
./bin/hdfs dfs -mkdir -p /user/hadoop/input
./bin/hdfs dfs -ls /user/hadoop/input

第一次完成本实验时,input应为空。如果该目录已用于其他练习,先核对已有文件;可以改用另一个空的HDFS目录,例如/user/hadoop/wordcount/input,并同步修改后面的创建、上传和运行命令。程序会统计输入目录中的所有有效输入文件,额外文本会改变词频结果。

6.2 上传两个输入文件

将第1节创建的两个本地文件上传到HDFS:

cd /usr/local/hadoop
./bin/hdfs dfs -put ./wordfile1.txt ./wordfile2.txt /user/hadoop/input/
./bin/hdfs dfs -ls /user/hadoop/input

检查HDFS中是否有wordfile1.txt和wordfile2.txt,也可以直接核对它们的内容:

cd /usr/local/hadoop
./bin/hdfs dfs -cat /user/hadoop/input/wordfile1.txt
./bin/hdfs dfs -cat /user/hadoop/input/wordfile2.txt

6.3 确认输出目录不存在

MapReduce提交前,/user/hadoop/output必须不存在,不要事先创建这个目录。

第一次运行且尚无output目录时,可以直接进行下一步。如果上次实验已经生成了output,且旧结果无需保留,可以删除本次实验的旧输出:

cd /usr/local/hadoop
./bin/hdfs dfs -rm -r /user/hadoop/output

如果需要保留旧结果,应把下一步命令的输出路径改成一个尚不存在的新目录,例如/user/hadoop/output2。重复运行时无需删除和重建输入目录。

6.4 提交词频统计作业

在Ubuntu终端执行:

cd /usr/local/hadoop
./bin/hadoop jar ./myapp/WordCount.jar input output

WordCount.jar是本地JAR文件,最后两个路径分别为HDFS输入目录和输出目录。

运行过程中会显示任务进度和计数器。下图就是本次的实际运行记录,可以看到map与reduce均达到100%,并显示作业成功完成。

图7-20 本地MapReduce作业成功完成

6.5 查看词频统计结果

作业成功后,结果写入HDFS的/user/hadoop/output目录。可以执行下面的命令,只读取Reduce输出文件:

cd /usr/local/hadoop
./bin/hdfs dfs -ls /user/hadoop/output
./bin/hdfs dfs -cat '/user/hadoop/output/*'

预期结果为:

Hadoop  2
I   2
Spark   2
fast    1
good    1
is  2
love    2

实际运行结果如下图所示:
图7-21 查看HDFS中的词频统计结果

至此,完成了创建Java项目、编写MapReduce词频统计程序、打包部署和查看结果的完整练习。再次运行时,保持输入文件不变,并删除不再需要的旧实验输出,或指定新的输出目录。