☰
MapReduce伪分布式实战:从环境搭建到WordCount作业避坑指南
2026/10/14 21:31:03 网站建设 项目流程

简介:《大数据技术原理与应用》实验报告四是一份聚焦MapReduce初级编程实践的完整实验文档,主要面向计算机科学专业学生、大数据技术初学者以及希望掌握MapReduce开发技能的开发者。文档以VMWare虚拟机、Ubuntu 18.04、JDK1.8、Hadoop-3.1.3和HBase-2.2.2为实验平台,系统演示了通过Eclipse创建MapReduce项目、配置依赖JAR包并实现文件合并去重、整数排序和表格信息挖掘等典型任务,覆盖从环境搭建、代码实现到结果验证的完整流程。压缩包内包含1个docx文档,大小10.48MB,内容涵盖完整的Java代码示例、命令行操作步骤以及实验过程中遇到的数据倾斜、格式不一致、权限不足等问题的解决方案,并附有个人实验心得。已有133人学习下载,适合读者结合代码动手实践,加深对MapReduce编程模型和分布式数据处理流程的理解,同时提升排查与解决实际环境问题的能力。

1. 从实验台到生产雏形:一份能跑通的 MapReduce 编程实践

MapReduce 是很多初学者跨进大数据门槛的第一道坎,经典 WordCount 看十遍不如亲手跑一遍。这份实验报告四的资源打包了 Hadoop 伪分布式环境下从零起步的 MapReduce 初级编程实验:Java 代码怎么写、编译打包怎么做、在 VM 虚拟机里怎么提交作业,以及最容易倒在这一步的端口、内存和权限坑。适合两类人:一是刚装完 Hadoop 还没跑通第一个作业的新手,照着代码把环境、编译、提交整条链路走通;二是准备面试或做课程设计的人,需要一份能解释清楚 Map 和 Reduce 阶段各自做了什么、数据怎么流转的完整案例。它解决的痛点很具体——教程很多,但能把环境配置和代码逻辑连起来讲透的没几个。

2. 预备环境:在 VMWare 虚拟机里搭 Hadoop 伪分布式的关键三步

2.1 为什么选伪分布式而不是真集群

实验报告里给出的场景是单节点伪分布式,也就是用一个 Linux 虚拟机把 HDFS NameNode、DataNode、ResourceManager、NodeManager 全部跑在同一台机器上。很多人一上来就想搞三台机器、五个节点的真集群,结果网络配置不一致,半天都起不来。

伪分布式的价值在于:它把分布式系统的通信机制(RPC、心跳、数据块复制)压缩在一台机器里,让初学者集中精力理解 MapReduce 编程模型本身,而不是被集群运维牵扯。这个实验里 Hadoop 跑了四个守护进程,通过 jps 命令能看到它们的存活状态。配置伪分布式只需要改三个 XML 文件,这是整个环境搭建里最核心的部分。

2.2 三份配置文件的改动点

在 hadoop 安装目录的 etc/hadoop/ 下,需要改 core-site.xml、hdfs-site.xml、yarn-site.xml。下面是实验资源里给的一套能稳定跑通的配置,我按自己复现的经验做了注释:

<!-- core-site.xml: 指定 NameNode 的 RPC 通信地址 --> <configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/home/hadoop/data/tmp</value> </property> </configuration>
<!-- hdfs-site.xml: 副本数设为 1,伪分布式只有一台机器 --> <configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/home/hadoop/data/namenode</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/home/hadoop/data/datanode</value> </property> </configuration>
<!-- yarn-site.xml: 指定 ResourceManager 的地址和附属服务 --> <configuration> <property> <name>yarn.resourcemanager.hostname</name> <value>localhost</value> </property> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <property> <name>yarn.nodemanager.aux-services.mapreduce_shuffle.class</name> <value>org.apache.hadoop.mapred.ShuffleHandler</value> </property> </configuration>

关键点在于:hadoop.tmp.dir 如果不改会默认落在系统临时目录,重启虚拟机后数据容易被清掉,这是一个很容易被忽略的隐患。dfs.replication 设为 1 是必须的,伪分布式环境下如果保持默认的 3,DataNode 会不断报复制不足的警告。yarn-site.xml 里的 mapreduce_shuffle 是 MapReduce 作业在 YARN 上运行时做数据洗牌(Shuffle)的必经通道,漏掉这一项作业会卡在 SUBMITTED 状态。

2.3 格式化与启动的顺序不能错

环境变量和配置文件改完之后,顺序是:先格式化 NameNode,再启动 HDFS 和 YARN。这个顺序错了,比如先启动再格式化,NameNode 的集群 ID 和 DataNode 不一致,DataNode 会一直处于拒绝注册的状态。

# 设置环境变量(写入 ~/.bashrc 并 source 生效) export HADOOP_HOME=/usr/local/hadoop export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin # 格式化 NameNode(只有第一次需要) hdfs namenode -format # 启动 HDFS 和 YARN start-dfs.sh start-yarn.sh # 验证进程是否全部起来 jps

格式化时如果控制台输出 "SHUTDOWN_MSG: Shutting down NameNode at",这是正常的,不是报错。jps 输出里应该看到 NameNode、DataNode、ResourceManager、NodeManager 四个进程,如果少了某一个,去对应日志里查原因。常见的是 DataNode 起不来,这时去 logs 目录看 hadoop-hadoop-datanode.log,多半是 clusterID 不一致,解决方法是把 data 目录下的 namenode 和 datanode 文件夹删掉,重新格式化再启动。

3. 第一个可提交的 MapReduce 作业:WordCount 的完整拆解

3.1 Mapper 阶段:每行文本怎么变成键值对

WordCount 是 MapReduce 的 Hello World,但能把它的三个类写对、编译通过、提交成功,算是迈过了第一道坎。实验资源里给的 WordCount 代码包含了 Map、Reduce、Driver 三个类,主类里用 Job API 把它们串联起来。我在模拟项目X里复现时,把 Mapper 单独抽出来看,逻辑是这样的:

// WordCountMapper.java import java.io.IOException; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> { // map 方法每处理一行调用一次,key 是行偏移量,value 是整行文本 @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString(); // 按空格和制表符切分单词 String[] words = line.split("[ \\t]+"); for (String word : words) { if (word.length() > 0) { context.write(new Text(word), new IntWritable(1)); } } } }

泛型四个参数的含义要区分清楚:LongWritable 和 Text 是输入的键值类型,即行偏移和行内容;Text 和 IntWritable 是输出的键值类型,即单词和计数 1。split 用正则 [ \t]+ 是为了把多个连续空格和制表符都当作分隔符。context.write 的输出会先写到环形缓冲区,等缓冲区达到阈值(默认 80%)再溢写(spill)到本地磁盘,这部分在实验报告里可能一笔带过,但理解它有助于解释为什么小文件多的时候作业很慢。

3.2 Reducer 阶段:为什么你的计数结果总和不对

Reducer 做的事情是从 Mapper 的输出里按 key 分组,把相同 key 的 value 列表汇总。实验报告里的逻辑很简单:遍历 values 累加。但我在自己做数据清洗实验时踩过一个坑——Reducer 的输入 key 一定是排序过的,这个排序是 Shuffle 阶段自动完成的,不需要在代码里做任何事,但如果你自己实现了一个自定义 WritableComparable,排序规则就由你决定,这时候容易写错。

// WordCountReducer.java import java.io.IOException; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> { @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int sum = 0; for (IntWritable val : values) { sum += val.get(); } context.write(key, new IntWritable(sum)); } }

需要注意的是,Iterable<IntWritable> 只能遍历一次。如果你先做了一次遍历去求总和,后面又遍历一次想做平均数,第二次遍历拿到的 values 是空的——因为迭代器背后的数据流已经被消费掉了。这是初学者容易踩的隐蔽问题,单测环境下看不出,用真实数据跑才会发现输出结果缺了一半。

3.3 Driver 类:提交作业时的依赖 jar 陷阱

所有写原生 Hadoop 作业的人后面都会遇到那个经典问题:Exception in thread "main" java.lang.NoClassDefFoundError。实验资源里的 WordCountDriver 写法是这样:

// WordCountDriver.java 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.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; public class WordCountDriver { public static void main(String[] args) throws Exception { if (args.length != 2) { System.err.println("Usage: WordCountDriver <input path> <output path>"); System.exit(-1); } Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "WordCount"); // 关键: 指定主类所在的 jar,否则集群上跑会找不到类 job.setJarByClass(WordCountDriver.class); job.setMapperClass(WordCountMapper.class); job.setReducerClass(WordCountReducer.class); // Mapper 输出的 key/value 类型,Reducer 输出与之相同时可以省略 job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } }

这套代码的核心是 Job 类的装配。setOutputKeyClass 和 setOutputValueClass 指定的是 Reducer 的输出类型,如果 Mapper 和 Reducer 的输出类型不一致,还需要显式调用 setMapOutputKeyClass 和 setMapOutputValueClass。addInputPath 如果传入的是一个目录,MapReduce 会递归读取目录下所有文件——这是它处理大量小文件慢的原因之一。setJarByClass 这一行会在打包的 jar 里标注入口类的完整路径,如果你用 IDE 直接运行而不是打 jar,那 Job 用的是本地模式(local mode),根本不会走 YARN。

把三个类放到同一个 src 目录后,编译打包的命令如下:

mkdir -p wordcount_classes # 编译三个 java 文件 javac -classpath $(hadoop classpath) -d wordcount_classes WordCountMapper.java WordCountReducer.java WordCountDriver.java # 打成 jar 包 jar -cvf wordcount.jar -C wordcount_classes/ . # 本地模式跑(输入输出都在本地文件系统) hadoop jar wordcount.jar WordCountDriver /home/hadoop/input /home/hadoop/output

在伪分布式环境里,hadoop classpath 输出很长一串路径,包含 Hadoop 自身所有依赖 jar。javac 编译时如果缺少这个 classpath,会报 org.apache.hadoop 包找不到。jar -cvf 的 -C 参数用来切换目录,防止把绝对路径打进包里。提交作业后,YARN 会在同一个虚拟机上启动一个 ApplicationMaster 和若干 Container,日志输出到 stdout 和 stderr,查看作业状态用 yarn application -list。

4. 避坑记录:MapReduce 初学阶段的高频翻车现场

4.1 作业一直处于 ACCEPTED 状态不跑

现象:用 hadoop jar 提交 WordCount 后,控制台只显示 submitted,过几分钟再看还是 ACCEPTED,yarn application -list 能看到作业但 resource 始终为 0。

原因:NodeManager 和 ResourceManager 之间的心跳正常,但 Container 始终分配不到。查 NodeManager 日志发现是本地目录权限或磁盘空间问题。伪分布式环境下,默认的本地目录是 /tmp/hadoop-yarn,如果磁盘剩余空间不足或者目录属主不是 hadoop 用户,Container 的本地化过程就会反复失败。

解决:把 yarn-site.xml 里 yarn.nodemanager.local-dirs 改到 /home/hadoop/yarn-local,并确认属主。同时用 df -h 看磁盘空间,低于 2GB 先清理 /tmp 下的中间文件。从那以后我每次提交作业前都先查一遍磁盘和目录权限,不再盯着 ACCEPTED 干等。

4.2 输出目录已存在导致整个作业直接失败

现象:第一次跑通了 WordCount,修改代码后想再跑一次,直接报错:Output directory hdfs://localhost:9000/user/hadoop/output already exists。

原因:MapReduce 的输出目录如果存在,框架会直接拒绝执行,这是防止覆盖数据的保护机制。很多初学者把本地 PC 的习惯带到 HDFS 上,以为会像本地文件系统那样覆盖。

解决:跑实验前先判断输出目录是否存在,或者在 shell 里显式删除:

hdfs dfs -rm -r /user/hadoop/output # 或者如果输出在本地文件系统 rm -rf /home/hadoop/output

这是最频繁的操作,可以把它固化成一个脚本模板,每次跑实验前先在脚本里执行环境清理,而不是等到报错后再处理。

4.3 Reducer 阶段卡在 33% 不动

现象:作业进度条走到 map 100% reduce 33% 就停住了,持续时间很久,最后报错 OOM 或超时。

原因:reduce 33% 意味着 Shuffle 阶段正在把 Map 输出从各个 Map Task 节点拉取到 Reducer 节点。伪分布式环境下没有网络瓶颈,最常见原因是 Mapper 输出的数据量巨大,而每个 Reducer 只拿到一个分区,导致单点压力过大。初学阶段往往没设置 Reducer 数量,默认是 1 个。

解决:用 job.setNumReduceTasks(2) 或提交时加参数 -D mapreduce.job.reduces=2 来增加 Reducer 数量。如果业务逻辑里根本没有聚合需求,比如只是简单的数据转换,可以在 Driver 里调用 setNumReduceTasks(0)。这是很多人到后面才学到的:Mapper only 的作业性能会好很多,因为省掉了 Shuffle 的排序和分组。

4.4 Mapper 输出类型和 Reducer 输入类型不匹配

现象:作业在 map 阶段报错 ClassCastException,日志关键词为 Mapper 的输出 value 类无法转换为 Reducer 期望的输入类型。

原因:没有显式设置 Mapper 输出类型时,框架会默认使用 Job 的输出类型。如果 Mapper 输出的是 Text 和 LongWritable,而 Reducer 的输入泛型声明是 IntWritable,提交作业时不会立刻报错,但运行时把 key/value 序列化后反序列化就会类型冲突。

解决:在 Driver 里显式添加两行:

job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(LongWritable.class);

这比依赖默认值更稳妥。代码里保持 Mapper 和 Reducer 的泛型类型声明一致,并让日志里的类型信息作为排查依据。

4.5 HBase 关联场景下作业找不到 HBase 类

现象:实验扩展到 HBase 读写场景时,作业一提交就在初始化阶段报 ClassNotFoundException,提示 org.apache.hadoop.hbase.mapreduce.TableInputFormat 不存在。

原因:打包 jar 时没有把 HBase 的依赖引入 classpath。Hadoop 自身不内置 HBase 客户端库,提交作业时 YARN 的 NodeManager 也不会自动加载 HBase 的 jar。

解决:做法是把 HBase 安装目录下的 hbase-server-.jar、hbase-common-.jar、hbase-client-*.jar 全部加入打包,或者用 -libjars 参数在提交时动态传入:

hadoop jar wordcount.jar WordCountDriver -libjars /usr/local/hbase/lib/hbase-server-3.x.jar,/usr/local/hbase/lib/hbase-common-3.x.jar /user/hadoop/input /user/hadoop/output

另外要注意 HBase 的配置文件 hbase-site.xml 必须在 classpath 可见,否则客户端不知道连接哪个 ZooKeeper 地址,会抛 Connection refused。

5. 进阶技巧:用 Combiner 和自定义 Counter 把 WordCount 变成生产级实验

5.1 Combiner 能压掉多少 Shuffle 流量

实验报告里的 WordCount 没有加 Combiner。在集群场景下,每个 Mapper 输出十个 (word, 1) 和输出一个 (word, 10),在 Shuffle 阶段传输的数据量差别很大。Combiner 是在 Mapper 端先做一次局部预聚合,它的本质就是 Reducer 的逻辑复用。实操里这样加:

job.setCombinerClass(WordCountReducer.class);

注意 Combiner 的条件约束:Combiner 的输入类型必须和 Mapper 输出类型一致,输出类型必须和 Reducer 输入类型一致。WordCount 场景下 Combiner 和 Reducer 的逻辑完全一样,所以可以直接复用同一个类。如果业务逻辑不是这种情况,不能强行复用 Reducer 类——比如你的 Reducer 是为了去重,Combiner 如果也做同样的去重,分布式环境下不同 Mapper 的重复数据无法在 Mapper 端被见到,结果会出现误差。

在伪分布式下,Combiner 的优化效果用肉眼不太看得出。我一般会用如下步骤验证它是否生效:先跑一遍不带 Combiner 的作业,再跑一遍带 Combiner 的,对比两者的 Counter 输出中 Shuffle Bytes 的大小。这是实验报告之外最有价值的操作,因为它帮你建立起了“数据量”的意识,而不只是关注功能跑通。

5.2 自定义 Counter:作业运行到哪一步了?

如果作业处理的数据量较大,YARN 的进度条只有百分比,排查问题很困难。自定义 Counter 是生产粒度上的工作习惯,实验资源里提供了一个小的计数器示例:

// 在 Mapper 里定义枚举 enum Counter { MALFORMED_LINES, BLANK_LINES } // 在 map 方法里累加 if (line.trim().length() == 0) { context.getCounter(Counter.BLANK_LINES).increment(1); return; }

提交作业完成后,控制台会打印 Counter 的统计结果,你可以直接看到 BLANK_LINES 的数量。这在清洗数据过滤脏数据时非常有用,不需要打开日志文件去翻,直接从作业统计里判断输入数据的质量。它也能定位数据倾斜问题——比如某个 key 的 value 数量异常多,你可以单独给这个 key 的打点计数,一眼看出数据分布。

5.3 复现实验时的完整验收清单

把这三个文件按 Mapper、Reducer、Combiner、Counter 的顺序组合起来之后,我习惯用下面的顺序做完整验收,这份验收逻辑来自复现某高校实验资源时的总结:

# Step 1:检查配置文件是否齐全 hdfs dfs -ls /user/hadoop/input # Step 2:清理旧输出 hdfs dfs -rm -r /user/hadoop/output # Step 3:提交作业并实时观察 hadoop jar wordcount.jar WordCountDriver /user/hadoop/input /user/hadoop/output # Step 4:验证结果 hdfs dfs -cat /user/hadoop/output/part-r-00000 | head -50 # Step 5:对比任务计数 yarn application -status <application_id>

这份实验报告的代码风格是规整的教科书写法,可读性好、依赖清晰,适合作为第一个能编译能跑通的作业模板。但它给的只是入门骨架,真正拿到生产场景里还需要自己补上 Combiner、自定义 Counter 和参数调优的经验沉淀。如果你只是照着跑一遍,能收获到的是完整的 Hadoop 环境感知和 Java API 熟悉感;如果你在它的基础上把 Combiner 和 Counter 加进去,再配合 HBase 的读写扩展,就相当于把初级实验升级到中阶水平了。

我自己后来在模拟项目X里做日志分析时,WordCount 这个骨架改一改就能当通用分组统计工具用,只要把 Text 换成自定义的 Writable,把累加逻辑换成 TopN 排序就行。从那以后我拿到任何新的 Hadoop 实验资源,都会先花十分钟把环境配置和 Driver 类的装配逻辑看一遍,再直接跳到提交和日志验证环节,省下了大量排查环境问题的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询