简介:一份大数据平台编程实验报告,面向刚开始接触Hadoop与MapReduce编程模型的计算机相关专业学生。报告完整记录了WordCount单词统计程序的实现过程,从Hadoop虚拟机安装、环境配置、Eclipse与Hadoop连接建立,到MapReduce代码编写与运行均有逐步说明。核心的wordcount.java源码可直接参考,涵盖自定义Mapper、Reducer类及Job提交逻辑,便于读者对照学习MapReduce执行流程并在此基础上改写扩展。资源为1个doc格式文档,压缩包大小仅758KB,包含实验目的、环境版本、详细操作步骤与完整源码。已有1726人浏览学习,对正在完成同类课程实验或自学入门Hadoop编程的读者具有较高参考价值。
1. 这份带源码的 Hadoop WordCount 实验报告,到底在让你练什么
大数据实验报告里,Hadoop 编程实现 wordcount 单词统计程序附源码.doc 几乎是所有人交出的第一份 MapReduce 作业:它看起来只是数单词,实际是把 HDFS 上的文件切分、分发、合并的整套流程走了一遍。这篇笔记按实验报告“是什么、怎么做、坑在哪”的顺序展开,先用一个 Mapper 和一个 Reducer 讲清 WordCount 的运行模型,再给出能直接编译打包、提交到伪分布式集群的完整源码,最后把路径、权限、日志这些最容易卡住实验的细节单独拎出来。适合正在做 Hadoop 课程设计、需要写实验报告的同学,也适合用 WordCount 验证新集群能不能用的工程师。
2. 单词统计的运行模型:Map、Shuffle、Reduce 各自做了什么
2.1 单机几行代码就能数完,为什么还要上 Hadoop
先用最直觉的方式统计单词,Python 里就是这么写:
from collections import Counter total = Counter() with open("input.txt", encoding="utf-8") as f: for line in f: total.update(w for w in line.lower().split()) print(total)这段代码在小文本上没问题,但换成真实场景:输入文件总和超过单机内存,或者文件分片存储在 HDFS 的不同节点上,单机程序就只能先把数据全部拉回一台机器,传输和内存都会先崩掉。MapReduce 的核心思路是“数据不动、计算动”:每个分片在它所在节点被本地 Map 处理,中间结果再经 Shuffle 按键分组,最后交给 Reduce 汇总。WordCount 虽然小,却完整演示了这条机制,这也是它被反复用作 Hadoop 入门实验的根本原因。
把 WordCount 选作课程设计还有一层现实理由:这种统计不需要跨节点做关联,输入输出是最基础的文本到文本,跑通它等于验证了 HDFS 上传、YARN 调度、Map 输出写盘、Reduce 拉取数据这条完整链路。所以报告里“会写 WordCount”不是目的,目的是会用这套链路。
2.2 Map 阶段:读一行、切一次词、打一个“出现 1 次”的标签
Mapper 的 map 方法签名是:
void map(LongWritable key, Text value, Context context)key 是当前行在文件里的字节偏移,value 是整行内容。框架每次把一行文本交给 map,不需要自己管理分片在哪个节点、从哪个字节开始读。WordCount 在 map 里只做三件事:把行转小写、按分隔符切词、对每个词输出一次<word, 1>。
新手容易忽略一个点:map 的输出不会直接写 HDFS,而是先写 Map 任务所在节点的本地磁盘,再按键分成多个区。默认分区器按 key 的哈希值决定进哪个区,目的是让同一个 word 的所有<word, 1>最终进入同一个 reduce 任务。你在 map 里执行context.write(word, one)之后,框架会在后台完成分区、排序和合并,这些都不需要写代码。
2.3 Shuffle 和 Sort:框架替你完成的“分组”
Shuffle 在实验报告里容易被一句话带过,但它其实是理解 WordCount 的关键。Shuffle 分两半:map 端把输出分好区落盘,reduce 端再从所有 map 任务拉取属于自己分区的数据,按键排序、合并。等 reduce 方法真正被调用时,同一个 key 的所有 value 已经装进一个 Iterable,这就是reduce(Text key, Iterable<IntWritable> values)里 values 的来源。
排序默认按 key 的字典序,所以 WordCount 的输出文件天然是从 a 到 z 排好序的。如果实验报告里想展示这一步,可以看任务日志里的 Shuffle 统计量,也可以直接观察输出文件是否有序。排序规则还能改写成按频次排,第 6 章会单独讲怎么把它变成 Top N 统计。
2.4 Combiner 和选型理由:为什么 Java 版是实验默认选择
Combiner 是可选的 map 端“预聚合”,WordCount 里可以直接复用 Reducer 类,因为求和满足交换律和结合律。设置 Combiner 后,同一个 Map 任务内部的相同单词会先在本地加一遍,再进入 Shuffle;小文件测试看不出区别,几 GB 输入时能明显减少网络传输。要注意 Combiner 跑在 map 节点上,必须保证它不改变最终语义,像求平均值这种操作就不能简单复用 Reducer。
至于实现选型,实验里交 Java 版是更稳的选择。Hadoop Streaming 用 Python 也能写,但 Java 版直接暴露 Mapper 和 Reducer 接口,出错时堆栈能定位到具体代码行,排错路径最短;同时 Java 版打成 jar 后不依赖集群额外安装的 Python 环境。这个理由写进“方案选型”一节,比“我用的是 Java”更有说服力。
3. 从零写一个可提交的 WordCount:工程骨架、打包命令与两个必调参数
3.1 工程骨架
常见做法是用 Maven 管理 Hadoop 依赖。工程结构如下:
wordcount/ ├── pom.xml └── src/main/java/com/example/WordCount.java只有两个文件。pom.xml里只要一个 Hadoop 客户端依赖,它会连带拉进 hdfs、mapreduce、common 相关类库。完整内容:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>wordcount</artifactId> <version>1.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-client</artifactId> <version>3.3.4</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>com.example.WordCount</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build> </project>两个参数值得说明:maven.compiler.source/target设成 1.8,是为了和大多数 Hadoop 集群的 JDK 版本对齐;mainClass写在 manifest 里,运行时就不用手动补全类名。版本号 3.3.4 可以换成你机器上实际安装的 Hadoop 版本,只要大版本是 2.x 或 3.x,这段代码不用改动。
3.2 完整源码:Mapper、Reducer、Main 入口一次写完
import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.LongWritable; 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; public class WordCount { public static class TokenizerMapper extends Mapper<LongWritable, Text, Text, IntWritable> { private final static IntWritable one = new IntWritable(1); private Text word = new Text(); @Override public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString().toLowerCase(); String[] words = line.split("[^a-zA-Z0-9']+"); for (String w : words) { if (w.isEmpty()) { continue; } word.set(w); context.write(word, one); } } } public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> { private IntWritable result = new IntWritable(); @Override 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); } } public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); 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); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } }逐段过一遍:Mapper<LongWritable, Text, Text, IntWritable>的四个泛型分别是输入 key 类型、输入 value 类型、输出 key 类型、输出 value 类型。LongWritable和Text是 Hadoop 自带的序列化类型,比直接用 Java 的 Long 和 String 更适合在节点间传输。
split("[^a-zA-Z0-9']+")的意思是:按“既不是字母、也不是数字、也不是撇号”的连续字符切分。这样don't会保留成完整单词,但state-of-the-art会被拆成三段。如果实验文本是干净英文,直接改成line.split("\\s+")更简单,按空白切即可。
job.setCombinerClass(IntSumReducer.class)这一行就是前面说的 map 端预聚合,因为求和适合本地先加一遍。waitForCompletion(true)必须传 true,作业提交后会持续打印进度,调试阶段靠它看每个阶段百分比。
3.3 打包、造数据、提交运行
在工程根目录执行:
mvn clean package ls target/wordcount-1.0.jar不习惯 Maven 的话,用 javac 也能编译,需要把 Hadoop 的 classpath 带进去:
javac -cp $(hadoop classpath) -d . WordCount.java jar cf wc.jar WordCount*.class hadoop jar wc.jar com.example.WordCount /input /output提交前先往 HDFS 放一个测试文件。伪分布式环境里输入路径必须是 HDFS 路径,不是 Linux 本地路径:
hdfs dfs -mkdir -p /input echo "hello hadoop hello world" | hdfs dfs -put - /input/a.txt hdfs dfs -cat /input/a.txthdfs dfs -put -的-表示从标准输入读数据,适合快速造实验数据。然后提交:
hadoop jar target/wordcount-1.0.jar /input /output看到日志里出现completed successfully后,检查结果:
hdfs dfs -cat /output/part-r-00000预期输出:
hadoop 1 hello 2 world 1part-r-00000是 reduce 阶段写出的文件,r代表 reduce,后面数字是任务编号。如果设置多个 reduce,会生成part-r-00000、part-r-00001等多个文件。
4. WordCount 跑不通时的排查顺序:路径、目录、权限与日志里的 6 个坑
以下每一条都是从实验报告批改现场和伪分布式翻车经历里攒出来的,按作业提交后事件发生的顺序排:
4.1 提交后一直卡在 Running job,先查 jps
现象:hadoop jar提交后,日志停在Running job不动,或很快报 ApplicationMaster 启动失败。
原因:伪分布式下 NameNode 或 ResourceManager 根本没起来,或者之前格式化过 NameNode 后又改了配置,集群元数据不一致。
解决:先执行jps,伪分布式正常要看到 NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager 五个进程。缺哪个就去logs/目录看对应日志。如果 ResourceManager 和 NodeManager 状态不同步,最稳妥是停掉所有进程重新start-dfs.sh和start-yarn.sh,不要只重启单个进程。
4.2 Input path does not exist:最大头的报错
现象:提交后立刻抛出Input path does not exist: hdfs://localhost:9000/user/root/input。
原因:/input目录在 HDFS 上不存在,或者文件没有真正传上去。初学者最容易犯的错是把本地 Linux 路径当成 HDFS 路径用。
解决:不要凭记忆猜路径,先hdfs dfs -ls /确认目录层级,再hdfs dfs -ls /input确认文件在。hadoop jar接收的路径一律是 HDFS 路径,本地文件必须先put上去。这个坑值得写进实验报告的“实验结果与分析”里。
4.3 FileAlreadyExistsException:输出目录被当成不允许覆盖的保护
现象:提交时报org.apache.hadoop.mapreduce.lib.output.FileAlreadyExistsException: Output directory hdfs://localhost:9000/output already exists。
原因:MapReduce 故意设计成输出目录在提交时必须不存在,防止误覆盖上一次实验的结果。
解决:实验前手动清理:
hdfs dfs -rm -r /output不建议在代码里自动删除输出目录,团队共用集群时很容易删掉别人的结果。把它当成实验流程的一部分,提交前先删一次。
4.4 Permission denied:root 用户在 HDFS 里不是超级用户
现象:日志出现AccessControlException: Permission denied: user=root, access=WRITE, inode="/output":hdfs:supergroup。
原因:HDFS 权限默认开启,Linux 的 root 在 HDFS 里没有写权限,目录 owner 是 hdfs。
解决:实验环境最简单的办法是用环境变量临时切换身份:
HADOOP_USER_NAME=hdfs hadoop jar target/wordcount-1.0.jar /input /output这个环境变量只对当前命令生效,适合单机实验。另一个办法是hdfs dfs -chmod -R 777 /input,但别在生产集群开dfs.permissions.enabled=false,某些教程为了省事这么写,只适合自己虚拟机里的学习环境。
4.5 类找不到或 Java 版本不匹配:编译环境与运行环境不一致
现象:提交后很快报ClassNotFoundException: com.example.WordCount,或者UnsupportedClassVersionError。
原因:前者是 jar 里没有包含主类,或运行时类名写错;后者是本地编译用的 JDK 比集群上的 JRE 新,编译出的 class 文件版本集群不认。
解决:检查 jar 内容:
jar tf target/wordcount-1.0.jar | grep WordCount如果类不在,检查 pom 的mainClass和源码包路径是否对应。版本问题就对比开发机和集群的java -version,把 pom 里的maven.compiler.source/target调到和集群一致。Windows 开发、Linux 集群这种组合最容易踩这个坑。
4.6 结果和预期不一致:正则切词和大小写合并的边界
现象:Hello和hello被算成两个词,或者hello,和hello没合并。
原因:map 里没做toLowerCase(),切词正则把标点一并算进了单词。
解决:WordCount 的“单词”定义完全由切词规则决定。实验输入是干净英文时用line.split("\\s+")最贴题意;想剔除标点就用[^a-zA-Z0-9']+;遇到中文文本就要换用[^\\s]+。这块没有标准答案,按实验要求调正则即可,同时在实验报告里写清楚你选的是哪种规则。
5. 伪分布式与集群模式差在哪:提交命令、参数和实验报告的关键截图
5.1 三种运行模式与提交差异
Hadoop 作业可以在三种形态下跑:本地模式不启动 HDFS 和 YARN,作业跑在单个 JVM 里,适合先验证 Mapper 和 Reducer 逻辑;伪分布式在一台机器上同时跑 NameNode、DataNode、ResourceManager、NodeManager,模拟完整集群,是多数课程实验的标准形态;完全分布式才是多节点真实集群,数据块真正跨机器存储。
提交命令本身没有本质区别,都是hadoop jar。真正的差别在三处:输入输出路径是否在 HDFS 上、作业是否被 YARN 调度、Map 并行度是多少。伪分布式下,一个 128MB 以内的小文件通常只产生 1 个 map,因为 Hadoop 3.x 默认一个分片是 128MB。想看到多个 map 任务,要么故意准备一个超过分片大小的输入,要么调小分片上限参数。
5.2 实验前建议调的四个参数
| 参数 | 默认值 | 伪分布式建议 | 说明 |
|---|---|---|---|
| dfs.replication | 3 | 1 | 只有一台 DataNode,副本 3 没有意义,还会白白占三倍磁盘 |
| mapreduce.job.reduces | 1 | 1 | WordCount 不需要多个 reduce,保持 1 个方便看结果 |
| mapreduce.input.fileinputformat.split.maxsize | 128MB 左右 | 32MB | 想看到多个 map 任务时调小,单位是字节 |
| mapreduce.map.memory.mb | 1024 | 512 | 小实验压小内存,避免小机器容器申请失败 |
dfs.replication配在hdfs-site.xml里,改完重启 HDFS 生效。分片大小可以在提交时用-D临时指定,不需要改配置文件:
hadoop jar target/wordcount-1.0.jar \ -D mapreduce.input.fileinputformat.split.maxsize=33554432 \ /input /output33554432 是 32MB 的字节数。调小分片后,日志里 Map 任务数会从 1 变成更多,能直观感受到 YARN 并行调度的效果,实验报告里对比截图也更好写。
5.3 实验报告里必须有的四样东西
批改这类报告时,能拿分的不只是源码。按这个结构写,基本不会被挑毛病:实验目的两三行、实验环境写清 Hadoop 版本和 Java 版本及部署形态、实验步骤里每步配一条命令、实验结果贴运行日志和输出文件、最后把源码附在附录里。
关键截图四张:jps进程列表、hadoop jar提交后从Running job到completed successfully的日志、hdfs dfs -cat /output/part-r-00000的输出前 20 行、任务结束后 Web UI 上作业的运行状态页(3.x 的 ResourceManager 默认端口 8088)。这四张截图像证据链一样证明作业真跑过,而不是只贴了个代码。
输出文件建议整理成表格:
| 单词 | 频次 |
|---|---|
| hadoop | 1 |
| hello | 2 |
| world | 1 |
把part-r-00000的内容剪贴进报告时,只保留前二三十个词频即可,完整结果作为附录。如果实验结果超过一页,说明输入文件太大,实验报告里截取一段代表性输出就行,没必要全部贴。
6. 基于 WordCount 改一改:三个顺手能做的统计拓展
6.1 单词平均长度:不改 Reducer,只加计数器
MapReduce 自带 Counter 机制,可以在不改变输出文件结构的前提下统计全局信息。在 map 里维护总字符数和总词数,累加进自定义计数器,作业跑完从 Counter 里读数:
context.getCounter("stat", "totalLen").increment(line.length()); context.getCounter("stat", "totalWords").increment(words.length);日志里 Counters 部分会出现stat组下的两个计数,相除就是平均长度。这个改法不需要动 Reducer,是最小的功能扩展。
6.2 输出每个单词首次出现的文件名
map 阶段可以通过context.getInputSplit()拿到当前分片对应的文件路径,拼到 value 里输出。这已经不是标准的 WordCount,而是开始学“如何携带上下文信息”。注意 Reducer 里不能对文件名求和,得改成去重收集,比如存进 TreeSet 后拼接成字符串。功能本身不复杂,但比默认 WordCount 更能体现对 MapReduce 数据流的理解。
6.3 按词频排序的 Top N:把排序键从单词换成频次
默认输出按键排序,WordCount 输出的是单词字典序。要改成“出现次数最多的前 N 个词”,常见做法是自定义一个WritableComparable作为输出 key,比较逻辑里优先比频次。这条路径会牵出二次排序和 GroupingComparator,属于 MapReduce 里偏进阶的内容,面试常考。作为实验报告的“拓展与思考”写三百字解释思路即可,不一定要跑通。
我自己的习惯是:WordCount 这个小工程不只用来交作业,装好一套新 Hadoop 环境后,先把它跑一遍,等于同时验证了 HDFS 读写、YARN 调度和日志定位三条链路。它简单到不会掺入业务干扰,又完整到覆盖提交全过程。以后排查集群问题,全靠第一次跑通时保存的日志和输出做基线对比。希望这份笔记能帮你少走一段弯路。
本文还有配套的精品资源,点击获取