☰
WordCount实验全解析:从伪分布式搭建到MapReduce避坑指南
2026/10/9 4:05:28 网站建设 项目流程

简介:一份面向 Hadoop 初学者的完整实验报告,围绕大数据框架中的 wordcount 单词统计程序展开,从虚拟机安装、环境配置到 Eclipse 与 Hadoop 连接、MapReduce 代码实现均有详细记录。报告以南华大学实验课程为背景,适合正在学习大数据平台编程、需要完成类似课设或实训任务的学生参考。资料仅含 1 个 doc 格式文档,压缩包大小 758KB,内容包含实验目的、运行环境(Window11 + Hadoop 虚拟机 + JDK1.8)、12 步操作说明以及可复用的 Wordcount.java 源码;其中详细记录了 start-all.sh 启动、端口检查、Eclipse 插件加载、MapReduce 连接配置等关键节点。源码自定义 Mapper 与 Reducer 类,清晰展示 split 分词、context 读写和计数归并流程,方便对照理解 MapReduce 的编程框架。目前已有 1727 人浏览学习,对希望快速搭建 Hadoop 单词统计项目、以报告模板完成实验作业的学生有直接参考价值。

1. Hadoop WordCount 实验报告:为什么一个单词统计程序成了大数据的入门第一课

我见过太多人把这份实验报告写成了"环境搭建流水账":贴十几张截图、把 WordCount 源码往正文里一放,最后总结一句"成功运行"。老师要的是你对 MapReduce 计算模型的理解,一份能体现思路的实验报告,比运行截图值钱得多。这篇笔记我按自己带新人时的辅导思路来写,把伪分布式环境准备、源码逐段拆解、打包提交到集群、常见报错排查一次讲透。不管你是本科大数据课程还是自学转行,照着做能少走两三天弯路。

2. Hadoop 实验环境准备:伪分布式搭建与最小验证命令

2.1 为什么本地机器要搭伪分布式,而不是直接跑单机模式

WordCount 是 MapReduce 的 Hello World,它的核心是"分而治之":一个文件被拆成多块,多进程并行统计,最后汇总。如果你只在单机模式跑,程序确实能出结果,但 Map、Reduce 跑在同一个 JVM 里,你根本观察不到分布式调度过程,实验报告也没东西可写。伪分布式模式(Pseudo-Distributed Mode)会用独立进程模拟 DataNode、NameNode 和 ResourceManager,虽然都在一台机器上,但数据流转路径和真实集群一致。

我建议你直接做伪分布式,别贪图省事用单机模式凑合。课程设计或实验报告里,老师通常要求"在 Hadoop 集群上运行",你在一台机器上把伪分布式搭出来,截图里能清楚看到 NameNode、DataNode、NodeManager 进程都活着,这就有了说服力。虚拟机里装 Hadoop 目前仍是主流做法,用 VMware 或 VirtualBox 装 Ubuntu Server 20.04 以上版本,内存分配 2GB 以上,磁盘 20GB 起步。别用 CentOS 6 或 7 的旧镜像,JDK 版本和 Hadoop 3.x 的兼容性会给你埋坑。

2.2 JDK 与 Hadoop 版本选型:先定版本再动手,别两头折腾

版本选型是最容易被忽略、也最容易翻车的环节。Hadoop 3.3.x 要求 JDK 8 或 JDK 11,但很多教材还在讲 Hadoop 2.x 配 JDK 7,照着敲就报UnsupportedClassVersionError。我的固定组合是 JDK 8u202 + Hadoop 3.3.6,这个搭配踩坑最少,网上能查到的报错案例也最全。

安装 JDK 时注意,Ubuntu 的 OpenJDK 默认路径在/usr/lib/jvm/,但java命令可能在/usr/bin/java。配环境变量时尽量写死路径,不要用update-alternatives的软链接,否则后面start-dfs.sh启动脚本在找 JAVA_HOME 时会变得很玄学。下载 Hadoop 安装包后,直接解压到/opt/hadoop,修改hadoop-env.sh里的JAVA_HOME为绝对路径:

# 编辑 /opt/hadoop/etc/hadoop/hadoop-env.sh,找到 JAVA_HOME 行并改为: export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 # 验证 Java 路径有效 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -version

JAVA_HOME 写软链接路径是伪分布式搭建里最常见的坑之一,因为 Hadoop 的启动脚本通过$JAVA_HOME/bin/java找解释器,软链接有时能解析、有时不能,表现为start-dfs.sh报Error: JAVA_HOME is not set and could not be found。另外,建议在/etc/profile里追加JAVA_HOME、HADOOP_HOME和PATH导出,然后source /etc/profile让配置立即生效。

2.3 免密登录与 NameNode 格式化:两条命令的顺序不能反

伪分布式需要 SSH 本机免密登录,因为 Hadoop 守护进程之间通过 SSH 通信。生成密钥、追加公钥后,测试ssh localhost是否还要输入密码。这里有个细节:如果你之前装过 Hadoop 并格式化过 NameNode,再次格式化前必须先删除/tmp/hadoop-*下的数据目录,否则 NameNode 和 DataNode 的 clusterID 对不上,DataNode 进程会反复启动失败。格式化命令:

# 生成 SSH 密钥(一路回车即可) ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys # 验证免密登录 ssh localhost 'echo ok' # 启动 HDFS 和 YARN /opt/hadoop/sbin/start-dfs.sh /opt/hadoop/sbin/start-yarn.sh # 格式化 NameNode(仅在首次安装或清理数据目录后执行) /opt/hadoop/bin/hdfs namenode -format

顺序上,先改配置文件再格式化,格式化只需要一次。如果以后集群启动不了,删掉数据目录重新格式化是常见恢复手段,但会丢掉已有数据,做实验无所谓,跑生产别这么干。启动后用jps查看 Java 进程,应该能看到 NameNode、DataNode、ResourceManager、NodeManager 四个进程。少了哪一个,就去对应日志目录/opt/hadoop/logs/找*.log文件,看具体报错。

2.4 准备实验数据:用系统文本文件快速生成输入目录

WordCount 需要输入文件,刚搭好的 HDFS 是空的。常见做法是把 Linux 系统自带的文本文件传上去,既能快速开始又不用自己编造数据。拿/etc/profile当输入源,切几个文件放到一个测试目录里:

# 在 HDFS 上创建输入目录,并上传本地文件 /opt/hadoop/bin/hdfs dfs -mkdir -p /wordcount/input /opt/hadoop/bin/hdfs dfs -put /etc/profile /wordcount/input/ /opt/hadoop/bin/hdfs dfs -put /etc/hosts /wordcount/input/ /opt/hadoop/bin/hdfs dfs -ls /wordcount/input/

HDFS 命令的写法和 Linux 命令长得像但实现完全不同,-mkdir -p会自动创建多级目录,-put是上传。如果dfs命令报Operation category READ is not supported in state standby,说明你配了 HA 但没配好,伪分布式别用 HA 配置。数据准备好后,在浏览器打开http://localhost:9870能看到 NameNode 界面,确认 DataNode 已注册、存储容量显示正常。这些截图放进实验报告就是"环境可用"的直观证明。

3. WordCount 源码逐段拆解:Map 阶段到底在算哪一步

3.1 MapReduce 编程模型先立概念:映射、洗牌、归约各管什么

写代码之前必须把 MapReduce 三个阶段的职责分清。Map 阶段读入一行文本,拆成单词,输出<单词, 1>这样的键值对;Shuffle 阶段由框架自动完成,把相同 key 的 value 合并到一起,比如<hello, 1>和<hello, 1>会变成<hello, [1, 1]>;Reduce 阶段接住这些 value 列表,累加出总次数。inputsplit这个概念在实验报告里值得专门写一段:每个输入分片对应一个 Map 任务,分片大小默认等于 HDFS 块大小(128MB),所以一个小文件也会占一个 Map 槽位,大量小文件会拖慢整体效率。

WordCount 的源码网上有一堆版本,但我见过很多同学抄下来跑不通,问题出在泛型类型上。Hadoop 自定义的Text类型等价于 Java 的 UTF-8 字符串,IntWritable等价于int,可序列化是它们最重要的特性。写代码时不要把Text和String混着用,Map 输出的 key 类型必须和 Reducer 输入的 key 类型完全一致,框架要做排序和分组,类型不一致会在运行时直接抛异常。

3.2 Mapper 类实现:tokenizer 与上下文写入的完整逻辑

下面是完整的 Mapper 内部类,关键代码行有注释:

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> { private final Text word = new Text(); private final IntWritable one = new IntWritable(1); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { // 将当前行转为 String,按空格和制表符切分 String line = value.toString(); String[] words = line.split("[\\s,.;!?]+"); for (String w : words) { if (w.isEmpty()) { continue; // 跳过空串 } word.set(w); context.write(word, one); // 输出 <单词, 1> } } }

这段代码有三个细节要强调。split的正则我刻意加了多个分隔符,只按空格切会在遇到标点时报出奇怪结果,比如 "hello!" 会被统计成 "hello!" 而不是 "hello"。word.set(w)是为了复用 Text 对象,避免每次 new 一个实例导致内存抖动,这是 MapReduce 编程里常见的性能优化点。context.write(word, one)是框架的核心出口,所有输出会先写到环形缓冲区,等缓冲区达到阈值后溢写到本地磁盘,这个机制在实验报告的"Map 端详解"部分可以展开写。

3.3 Mapper 输入 key 的类型:LongWritable 不是随便选的

很多初学者会问:Mapper 输入 key 为什么是LongWritable而不是Text?因为 MapReduce 读文件时,默认的TextInputFormat会把每行的字节偏移量作为 key,第一行偏移量是 0,第二行是上一行的长度加换行符。这个偏移量对业务没有意义,但框架层必须传。真实场景里你几乎不会用这个 key,但面试题常问InputSplit和 key 的关系——每个 Map 任务处理一个 InputSplit,而 InputSplit 里的每条记录按行解析,偏移量递增就是记录的唯一标识。

如果说 Mapper 是 WordCount 的"分词车间",那 Reducer 就是"计数总装线"。Map 输出的<单词, 1>会在框架层自动按单词首字母排序,这不是可有可无的参数,而是 MapReduce 设计的核心机制。理解了这个排序,后面看TotalOrderPartitioner或二次排序题目时就不会乱。

4. Reducer 与 Main 方法:聚合逻辑、作业提交参数与打包细节

4.1 Reducer 类实现:Iterable 为什么是累加的关键

Reducer 接收的 value 是Iterable<IntWritable>,意味着你可以用增强 for 循环遍历这个单词出现的所有1。注意,这个 Iterable 对象在框架里可能被重复使用,所以不要在循环里把 value 保存到 List,只做累加就没问题。实现如下:

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> { private final IntWritable result = new 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(); // 累加当前单词出现的次数 } result.set(sum); context.write(key, result); // 输出 <单词, 总次数> } }

reduce 方法传进来的key已经排好序并分组,相同单词的所有计数会被分到同一个 reduce 调用里。val.get()从 IntWritable 里取出 int 值,累加完后result.set(sum)再写入输出。这里值得在实验报告里写一句:Reducer 的输入输出类型必须和 Mapper 输出类型对齐,如果 Mapper 输出 key 是Text,Reducer 输入 key 也必须写Text,泛型对不上会在编译期报错,这是新人最容易犯的错。

4.2 作业提交入口:setCombinerClass与 job 参数的含义

Main 方法里要做的三件事:创建 Job 实例、设置各类 Class、提交等待完成。这里有一个关键参数setCombinerClass,很多教材把它一笔带过,但它直接影响运行效率。Combiner 是一种"本地 Reducer",在 Map 端先做一次合并,减少 Shuffle 阶段传输到 Reducer 的数据量。以 WordCount 来说,如果把 Reducer 类直接设成 Combiner,语法上完全合法,但语义只在"求和"这类场景下正确。你在实验报告的"性能优化"小节里可以写明:对平均值、最大值这类依赖全局状态的场景,不能直接套 Reducer 做 Combiner。

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 { Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "word count"); job.setJarByClass(WordCountDriver.class); job.setMapperClass(WordCountMapper.class); job.setCombinerClass(WordCountReducer.class); // 本地聚合,优化网络开销 job.setReducerClass(WordCountReducer.class); 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); } }

参数说明:addInputPath接收 HDFS 上的输入目录而不是文件列表,目录下所有文件都会被当作分片;setOutputPath指定的输出目录必须不存在,否则框架会报FileAlreadyExistsException,这是故意设计的安全机制。waitForCompletion(true)的布尔参数表示是否打印任务进度,实验报告截图里那些 Progress 进度条就是它打出来的。

4.3 打包提交到 Hadoop:jar 包的参数顺序与输出目录清理

编译打包有两种常见方式:命令行 javac 加 classpath,或直接用 Maven。课程实验用 Maven 更省事,在pom.xml里声明 hadoop-client 依赖,版本和你集群一致,然后mvn clean package。但很多学校机房的 Maven 仓库是空的,下载依赖会卡半天,所以我一般建议直接用 Hadoop 自带的 classpath 编译:

# 完整编译并打包(依赖 Hadoop 的 lib 目录) mkdir -p build javac -classpath $(/opt/hadoop/bin/hadoop classpath) -d build WordCountMapper.java WordCountReducer.java WordCountDriver.java jar -cvf wordcount.jar -C build . # 提交作业(第一个参数是 HDFS 输入目录,第二个是输出目录) /opt/hadoop/bin/hadoop jar wordcount.jar WordCountDriver /wordcount/input /wordcount/output

hadoop classpath会输出一长串路径,包含了 Hadoop 所有依赖 jar,不用自己一个个找。如果本地机没装 Hadoop,也可以把集群机器上的 classpath 复制出来,但容易出现版本不一致。作业跑完后,用hdfs dfs -cat /wordcount/output/part-r-00000 | head -20查看结果,输出文件是 Hadoop 自己的 SequenceFile 文本格式,多个 Reducer 会产生多个 part-r 文件。这里有个实验报告必写点:part-r-00000的r表示 Reduce 阶段产物,如果是part-m-开头则是 Map 阶段产物,实际作业里这两种文件可能同时存在。

5. WordCount 实验避坑:5 个让程序白跑的经典问题

5.1 输出目录已存在:FileAlreadyExistsException 不是代码 Bug

现象:提交作业后立刻报org.apache.hadoop.mapred.FileAlreadyExistsException: Output directory hdfs://localhost:9000/wordcount/output already exists。原因:上几次运行留下的输出目录没删,MapReduce 强制要求输出目录必须不存在。解决:删掉输出目录再重跑。

/opt/hadoop/bin/hdfs dfs -rm -r /wordcount/output /opt/hadoop/bin/hadoop jar wordcount.jar WordCountDriver /wordcount/input /wordcount/output

这个坑专治"跑第二次就蒙圈"的新手。有人图省事在代码里加FileSystem.delete()自动删输出,我不建议这么干,生产环境里误删数据的风险远大于那点便利。实验报告里可以把这条归到"HDFS 文件系统操作规范"里写,能体现你对分布式文件系统粒度的理解。

5.2 ClassNotFoundException:主类名的包路径对不上

现象:java.lang.ClassNotFoundException: WordCountDriver。原因:hadoop jar后面跟的类名和 jar 包里实际路径不一致。如果源码在默认包下,类名就是WordCountDriver;如果源码带com.example包名,就必须写全限定名com.example.WordCountDriver。解决:

# 查看 jar 包内容确认类的完整路径 jar tf wordcount.jar # 按输出的路径提交 /opt/hadoop/bin/hadoop jar wordcount.jar com.example.WordCountDriver /wordcount/input /wordcount/output

5.3 权限被拒:Permission denied 与 HDFS 根目录所有者是谁

现象:上传文件或创建目录报Permission denied: user=root, access=WRITE, inode="/":hdfs:supergroup:drwxr-xr-x。原因:HDFS 默认权限模型和 Linux 类似,root 用户不一定有写权限,因为 HDFS 的超级用户是启动 NameNode 的用户。解决:一是启动 HDFS 的账户统一用同一个用户,比如都叫 hadoop;二是在 hdfs-site.xml 里临时关权限检查:

<property> <name>dfs.permissions.enabled</name> <value>false</value> </property>

改完配置必须重启 HDFS,stop-dfs.sh再start-dfs.sh。dfs.permissions.enabled只在实验环境用,真实集群不会关。这条坑在伪分布式里极其常见,因为很多人图方便用 root 登录 Linux,而 Hadoop 启动用户写死了hdfs。

5.4 Container 被 kill:内存分配不足导致任务中途失败

现象:作业跑到一半报Container exited with a non-zero exit code 104,或者Container killed by the ApplicationMaster。原因:MapReduce 任务的物理内存超了 YARN 容器的上限,默认的yarn.nodemanager.vmem-pmem-ratio设成了 2.1,而一台 2GB 内存的虚拟机很容易被打爆。解决:调大容器内存参数,让作业跑得松一点。

# mapred-site.xml 中添加: # 每个 Map/Reduce 容器的最大堆内存设为 512MB export HADOOP_OPTS="-Xmx512m"

按我的经验,虚拟机上跑 WordCount 不需要这么大内存,但如果你同时开了 DataNode、NameNode、ResourceManager 和 NodeManager,系统本身占掉 1.5GB,留给容器的不剩多少。用free -h确认物理剩余内存,如果不足 1GB,把JAVA_OPTS调低,或者关掉不用的桌面环境。

5.5 结果全为 0 或乱码:输入数据没传上去或文件编码不对

现象:作业秒成功,打开输出文件全是0或者乱码。原因:输入路径写错但 Hadoop 没报错,比如传了/wordcount/input这个目录,但目录是空的;另一个原因是 Windows 上编辑的文件带了 BOM 头或 UTF-16 编码,TextInputFormat按 UTF-8 读会出现乱码。解决:先用hdfs dfs -ls -R /wordcount/input确认文件大小大于 0,再用file命令检查上传文件的编码。Windows 上写好源码传到 Linux 时,用dos2unix转换行尾符,避免\r被当成单词的一部分。

6. 把 WordCount 实验报告写出亮点:日志解读、InputSplit 考点与 Combiner 优化

这篇实验报告能不能拿高分,关键在于你能不能答出框架背后的细节。我在带毕业生做课程设计时,发现凡是写了下面这些点的,老师给的评语都明显更具体。第一个加分点是把waitForCompletion(true)打印的 Map 进度日志读明白:

map 100% reduce 67%

正常情况下 Map 达到 100% 后 Reduce 才会开始,如果你看到map 0% reduce 100%,说明作业数据量太小,Reduce 在等第一个 Map 的输出,这种情况在集群跑大文件时才会被留意。把这段截图放报告里,标注出 Shuffle 发生在 reduce 进度从 0 到 100% 之间,就展示了你的分布式思维。

第二个加分点是专门写一段 InputSplit 的理解。面试和课程考核常问"在一个运行的 Hadoop 任务中什么是 InputSplit",你至少要说出三层:输入分片是 Map 任务处理的数据单元;分片边界由 FileInputFormat 计算,默认按块大小切分;分片大小不等于数据块大小,分片可以跨多个块但要记录偏移量。结合 WordCount 来说,/wordcount/input里两个小文件会生成两个 InputSplit,所以对应两个 Map 任务,这个现象可以在 YARN 的 ResourceManager 界面截到。按默认配置,128MB 是块大小,实验里的文件远小于这个值,每个文件单独成一个分片,这就是为什么小文件多会导致 Map 数膨胀。

第三个加分点是 Combiner 的效果验证。在 Driver 里先不加setCombinerClass跑一遍,记录 Shuffle 字节数;加上之后再跑一遍,对比 Reduce 阶段的输入记录数。Hadoop 作业完成后,hadoop job -history或 Web UI 的 Counter 区域能看到Reduce shuffle bytes,用表格把两条数据放进实验报告,量化说明"Map 端聚合降低了多少网络传输"。我见过一个学生在 1GB 数据上做对比,Shuffle 字节数下降了约 40%,老师直接在评语里写了"数据翔实"。

我自己的习惯是,写完 WordCount 不急着交,而是再写一个"词频 Top N"的小变体:在 Reducer 里用 TreeMap 保留前 N 个单词,或者用两个 Job 串起来实现全局排序。做完这两个变体,你对 MapReduce 的排序机制、分区机制和作业串联的理解会真正立住。跑通 Hadoop 的 WordCount 只是开始,能把实验报告里的每个现象解释清楚,才是这只"大数据入门拦路虎"带给你的最大收获,希望帮到你。

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

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

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

立即咨询