简介:面向云计算课程学习者与Hadoop入门者,这份实验报告以93分成绩完整呈现了在Linux环境下运行Hadoop MapReduce程序的实操流程。内容从Java源文件编写开始,逐步展开CLASSPATH环境变量配置、javac编译命令、jar打包细节(含manifest清单文件与目录结构注意事项),最终演示如何用bin/hadoop jar命令提交作业,并补充了Hadoop自带wordcount单词统计程序的完整操作:创建HDFS数据目录、上传输入文件、运行作业、查看统计结果。报告还包含五层三角形打印算法的分析,以及作者对MapReduce工作机制、jar包管理与执行的实践总结,能帮助读者快速建立从代码到集群运行的整体认知。资源为1个PDF文件,大小603KB,内容精炼,适合课前预习或课后复盘。已有382人学习下载,是云计算实验课的实用参考。
1. 云计算技术实验里的 Hadoop MapReduce:跑通一次,才算真正入门
很多人学云计算技术,第一道坎不是理论,而是亲手把一份 Java 代码编译、打包、扔到 Hadoop 上跑出结果。MapReduce 这套编程模型看起来简单——一个 map、一个 reduce、一个 main——但真正在命令行里走一遍,你会发现坑全在细节里:CLASSPATH 没配好 javac 直接报“包不存在”,jar 包里类路径不对运行时抛 ClassNotFoundException,HDFS 输出目录已存在作业秒挂。这份实验报告(实验三:运行 Hadoop MapReduce 程序)的价值在于:它用最朴素的方式展示了一条完整链路——从 vim 写 Java 文件,到 javac 编译、jar 打包,再到 bin/hadoop jar 执行,最后跑通 Hadoop 自带的 wordcount。适合两类人:一是正在上云计算实验课的学生,需要一个可照抄的完整流程;二是刚搭好 Hadoop 想验证环境是否正常的从业者。本次实验基于 Hadoop 2.10.1,跑在 Linux 命令行下,先声明:整个过程不涉及任何集群调优,就是把 MapReduce 作业“能跑、能出结果”这件事做到位。
2. 先过编译关:CLASSPATH 与 javac 的参数细节
2.1 在 /etc/profile 里配置 CLASSPATH 的完整写法
这份实验第一步就是“设置 CLASSPATH”。很多新手不理解为什么要配这玩意儿,直接跳过,结果 javac 编译时抛出一堆“程序包 org.apache.hadoop.io 不存在”的错误。原因是:Java 编译器要找到 Hadoop 相关的类库,而 Hadoop 的 jar 包散落在安装目录的 share/hadoop 下面,默认不在 javac 的搜索路径里。不告诉编译器去哪儿找,它自然找不到。
我当时配的时候踩过一个认知误区:只配了 $HADOOP_HOME/share/hadoop/common 这一个目录,结果编译时 mapper 相关的类还是找不到。正确做法是要把 common、hdfs、mapreduce、yarn 这四块的 jar 都加进去,而且要用通配符 * 把 lib 目录下的依赖 jar 也包进来。在 /etc/profile 末尾追加下面这段:
export HADOOP_HOME=/opt/hadoop-2.10.1 export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin export CLASSPATH=$CLASSPATH:$HADOOP_HOME/etc/hadoop:$HADOOP_HOME/share/hadoop/common/lib/*:$HADOOP_HOME/share/hadoop/common/*:$HADOOP_HOME/share/hadoop/hdfs:$HADOOP_HOME/share/hadoop/hdfs/lib/*:$HADOOP_HOME/share/hadoop/hdfs/*:$HADOOP_HOME/share/hadoop/mapreduce/*:$HADOOP_HOME/share/hadoop/yarn/*:$HADOOP_HOME/share/hadoop/yarn/lib/*然后 source 一下让配置生效:
source /etc/profile echo $CLASSPATH第三行 echo 是为了确认 CLASSPATH 真的写进去了。注意一个常见问题:如果你是在 XShell 或 MobaXterm 里敲的 source,只对当前终端会话生效,重新开一个窗口后需要再 source 一次,或者重新登录系统让 /etc/profile 自动加载。另外,通配符 * 在 CLASSPATH 里会被 shell 展开,但如果路径不存在,这个变量就变成空字符串,所以第一步务必先确认 HADOOP_HOME 指向的目录真实存在。
2.2 javac 编译与常见报错
配好 CLASSPATH 后,就可以写 Java 代码了。实验报告里用的是 vim triangle.java,我在实际复现时习惯把代码写到带包名的目录结构里,因为后面打 jar 包时要保证 class 文件在包路径下。先看完整的 triangle 程序,这个程序用 MapReduce 的方式输出一个五层三角形,和报告里的“嵌套两层循环”对应:
package triangle; import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; public class Triangle { public static class TriangleMapper extends Mapper<Object, Text, Text, Text> { @Override protected void map(Object key, Text value, Context context) throws IOException, InterruptedException { // 外层循环:控制三角形层数,固定5层 for (int i = 1; i <= 5; i++) { StringBuilder line = new StringBuilder(); // 内层第一个循环:输出空格,逐层递减 for (int j = 5 - i; j > 0; j--) { line.append(" "); } // 内层第二个循环:输出星号,奇数递增:1、3、5、7、9 for (int k = 0; k < 2 * i - 1; k++) { line.append("*"); } context.write(new Text("line-" + i), new Text(line.toString())); } } } public static void main(String[] args) throws Exception { if (args.length != 2) { System.err.println("用法: triangle <输入路径> <输出路径>"); System.exit(-1); } Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "triangle"); job.setJarByClass(Triangle.class); job.setMapperClass(TriangleMapper.class); // 纯map作业,不需要reduce job.setNumReduceTasks(0); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } }这个程序故意把 reduce 数量设为 0,因为在纯格式化输出的场景里,map 阶段就可以完成全部计算,设置 reduce 反而多一次 shuffle 开销。注意 context.write 的 key 用了 "line-1" 这种有意义的文本,方便在输出文件里定位每一行;如果只关心星号排列,key 也可以传空字符串。
编译命令如下:
javac -encoding UTF-8 -classpath $CLASSPATH -d . Triangle.java参数说明:-encoding UTF-8 防止因中文注释导致的编码错误;-classpath $CLASSPATH 让 javac 能找到 Hadoop 的类库;-d . 表示按包名自动生成目录结构,编译后会在 ./triangle/ 下生成 Triangle.class 和 Triangle$TriangleMapper.class。别小看 -d 这个参数,如果不加,class 文件会直接生成在根目录下,后面打 jar 包时包路径就乱了。
编译成功的标志是没有输出任何信息,命令行安静地回到提示符。如果报“程序包 org.apache.hadoop.io 不存在”,多半是 CLASSPATH 没配全;如果报“编码 GBK 的不可映射字符”,说明没加 -encoding UTF-8。class 文件生成后,用 ls -R triangle 查看目录结构,确认 Triangle.class 确实在 triangle 子目录下。
3. jar 打包:manifest.mf 和包路径是两处翻车点
3.1 为什么 jar 包里必须有包路径
实验报告里有一句特别重要的备注:“java 和 class 文件在 jar 包中应该位于一个文件夹,否则之后运行时会出错。”这句话我在第一次实验时没当回事,结果踩了个大坑:直接用 jar cvf triangle.jar *.class 打包,把所有 class 文件放在 jar 包根目录,运行时报 ClassNotFoundException。
原因在于 Java 的类加载机制。Hadoop 提交作业时,会根据你在命令行里指定的主类名(比如 triangle.Triangle)到 jar 包里去寻找对应的二进制类文件。如果你的 jar 包根目录下是 Triangle.class,而主类名带包前缀 triangle.Triangle,JVM 会按 “triangle/” + “Triangle.class” 的路径去查找,找不到就抛异常。反过来,如果你命令行里也不带包名,直接写 Triangle,勉强能跑,但一旦程序里有多个类或者引用了外部依赖,这种“展平”的打包方式就会乱套。所以养成良好的习惯:编译时用 -d . 生成包目录,打包时把整个 triangle 目录打进去,运行时写全类名。
3.2 manifest.mf 的写法与 jar 命令参数
打包前先创建一个清单文件 manifest.mf,用来告诉 JVM 主类在哪儿。内容很简单:
Main-Class: triangle.Triangle注意最后一定要回车换行,否则有的 JDK 版本会警告“清单文件最后一行缺少换行符”,主类设置可能失效。然后执行打包命令:
jar cvf triangle.jar triangle/命令参数解释:c 表示创建一个新的压缩文件;v 表示在打包时输出详细进度信息(每个被打进去的文件都会列出来);f 指定生成的 jar 文件名;后面跟要打进去的目录 triangle/。执行完你会看到类似 added manifest、adding: triangle/Triangle.class 的输出,此时 jar 包里的结构是 triangle/Triangle.class 和 triangle/Triangle$TriangleMapper.class,和代码里的包名对上了。
如果你想用实验报告里的完整参数写法 jar cvfm triangle.jar manifest.mf triangle/,注意 -m 参数指定清单文件的顺序:jar cvfm 后面跟的第二个参数是 jar 文件名,第三个参数才是 manifest 文件。很多人在这里把顺序搞反,导致 jar 命令把 manifest 当成要打包的目录,报“manifest.mf 是一个目录”或“没有这样的文件或目录”。
打包完成后验证一下内容:
jar tf triangle.jar输出里应该能看到 META-INF/MANIFEST.MF 和 triangle/Triangle.class、triangle/Triangle$TriangleMapper.class 这三项。如果只看到 META-INF 和零散的 .class 文件,说明包路径不对,重新用 -d . 编译后再打。
4. 提交运行:hadoop jar 的参数顺序与 triangle 完整链路
4.1 准备 HDFS 输入目录并理解 input split 的作用
运行 hadoop jar 之前,必须先准备 HDFS 上的输入文件。就算 triangle 这个纯 map 作业实际上不太需要读取输入内容,MapReduce 框架仍然要求输入路径存在且可读,否则作业会报 Input path does not exist 直接失败。先在 HDFS 上创建好输入输出目录:
hdfs dfs -mkdir -p /user/root/mr/input-mapreduce 作业默认读取 HDFS 而不是本地文件系统,这一点经常有新手搞混。如果你在本地创建一个空文件后直接回车,作业会提示找不到路径。上传一个最简单的输入文件,内容随便写一行文本即可:
echo "hello mapreduce" | hdfs dfs -put - /user/root/mr/input/hello.txt这里的 - 表示从标准输入读数据,hdfs dfs -put 会把管道里的内容上传成 hello.txt 文件。上传后验证一下:
hdfs dfs -ls /user/root/mr/input此时需要理解一个概念:input split。MapReduce 框架会根据输入文件的大小切分成若干个逻辑分片,每个分片由一个 map task 处理。默认情况下,一个文件如果小于 HDFS 块大小(这里没特殊配置,就是 128MB),就只产生一个 split,对应一个 map。这个知识点在 Hadoop 面试题里几乎必考,实验阶段只要知道“输入文件数量和大小影响 map 数”就够了。如果你想看到作业实际起了多少个 map,在执行 jar 时的输出日志里会有一行 map 100% reduce 0%,仪表盘上也会显示 Map 总数。
4.2 执行 hadoop jar 并解读运行日志
现在提交 triangle 作业,命令格式固定为 hadoop jar <jar包> <主类全名> <输入路径> <输出路径>:
hadoop jar triangle.jar triangle.Triangle /user/root/mr/input /user/root/mr/output-triangle执行后如果环境正常,终端会滚动输出任务进度信息,关键看这几处:首先是 Job 名称和 Job Identifier,形如 job_xxx_0001,后续查日志和去 Web UI 看进度都靠它;然后是一串 Running job: job_xxx_0001;之后是 map 阶段进度 map 0% 逐步涨到 map 100%;最后是 Job complete 和 Counters 汇总。整个过程不需要等太久,纯 map 作业如果没有数据量压力,通常十几秒内完成。
作业完成后查看输出目录:
hdfs dfs -ls /user/root/mr/output-triangle hdfs dfs -cat /user/root/mr/output-triangle/part-m-00000输出文件名的规律是 part-m-xxxxx 表示来自 map 阶段(reduce 数为 0 时),part-r-xxxxx 表示来自 reduce 阶段。cat 出来的内容应该是五组 key-value:line-1 对应一个空格加一个星号,line-2 两个空格加三个星号,以此类推。看到这个结果,triangle 程序就算完整跑通了。
4.3 一个最常见的参数错误:输出路径必填且不能已存在
很多人第一次跑 hadoop jar 时只给了三个参数,比如 hadoop jar triangle.jar triangle.Triangle /input,然后报错 ArrayIndexOutOfBoundsException。原因是 MapReduce 作业的 main 方法期待两个参数,程序里明确写了 args[0] 是输入路径、args[1] 是输出路径。如果没有第二个参数,程序会直接 exit(-1),或者因为访问 args[1] 抛异常。
另一个高频报错是 FileAlreadyExistsException,message 里写着 Output directory hdfs://xxx already exists。这是 Hadoop 的安全机制:输出目录已存在时它拒绝覆盖写入,防止误删历史结果。解决方法不是手动删除旧目录然后重跑——那样可能误删,而是换一个新目录名,或者用 hdfs dfs -rm -r 明确删掉要清理的旧路径。我一般会在每次重跑前统一执行 hdfs dfs -rm -r /user/root/mr/output-triangle,确保路径一定可用。
5. 避坑记录:Hadoop MapReduce 实验最常见的五个问题
5.1 现象:运行时报错 “Output directory already exists”,作业直接失败
原因:Hadoop 默认输出目录必须不存在,这是为了防止覆盖历史结果而设计的安全机制。
解决:不要试图改配置绕过它。养成习惯,每次跑作业前先确认输出路径是否已存在,存在就换目录名,或执行 hdfs dfs -rm -r /user/root/mr/output-triangle 清理掉。注意 hdfs dfs -rm 只删除 HDFS 上的文件,不会动本地文件系统,别搞混了。
5.2 现象:上传文件或 MapReduce 作业访问 HDFS 时报 Permission denied
原因:HDFS 的用户权限模型和 Linux 不同。你在当前 shell 里是 root 或其他用户,但 HDFS 超级用户可能是 hdfs,或者目录的 owner 不是你当前用户,默认权限下你不能写入。
解决:先用 hdfs dfs -ls / 看看目录属主,再用 hdfs dfs -chmod -R 777 /user/root 这种命令对工作目录放宽权限。如果是伪分布式环境,有人直接修改 hdfs-site.xml 里的 dfs.permissions.enabled 为 false 来关闭权限检查,我不建议这么干,这会把 HDFS 的所有安全机制废掉,实验结束后容易忘改回来,后续学习 HDFS 权限管理时会产生误解。正确做法是把目录权限控制在实验范围内。
5.3 现象:hadoop jar 执行时抛 ClassNotFoundException,报错信息里出现 triangle.Triangle
原因:jar 包内部没有按包名路径存放 class,或者主类名写错了。我在第 3 章提到的场景就是典型:编译时没加 -d . 参数,class 文件全在根目录;或者打包时打的是 *.class 而不是 triangle/ 目录。
解决:重新编译、重新打包。具体操作是 javac -encoding UTF-8 -classpath $CLASSPATH -d . Triangle.java,然后 jar cvf triangle.jar triangle/,再用 jar tf triangle.jar 确认 manifest 和 class 路径都正确。如果 manifest 没设置 Main-Class,也有可能在提交时提示 “Class triangle.Triangle not found”,这是另一处原因,检查 manifest 最后是否有换行。
5.4 现象:作业一直在跑,终端只显示 map 0% reduce 0%,不知道发生了啥
原因:小实验的输入数据量太小,作业调度和初始化本身需要一点时间;另一种可能是集群里有其他作业在排队,或者 YARN ResourceManager 资源不足,新作业在等待。
解决:不要干等。按 Ctrl+C 不会取消作业,作业提交到 YARN 后会继续跑。正确做法是打开另一个终端窗口,用 yarn application -list 查看正在运行的作业列表,再配合日志查看实际进度。如果确定作业卡死了不想等,用 yarn application -kill <application_id> 杀作业,然后再检查输入路径对不对、代码里的循环有没有死循环。实验场景里最常见的还是输入路径写错导致 job 一直重试,先看日志再动手。
5.5 现象:wordcount 跑完后,输出只有一行或者结果明显不对
原因:wordcount 的输入文件里的单词分隔符默认是空格和制表符,如果你的文本里有中文标点或特殊符号,会被认成一个完整单词,统计结果和“数数”的直觉不一致。另一个可能是输入文件编码问题,UTF-8 文件没指定编码,某些字符被拆成乱码。
解决:查看输出文件 part-r-00000 时,仔细核对每一行格式。格式是“单词 次数”,中间用制表符分隔。如果是编码问题,在执行 wordcount 之前用 iconv 转一下文件编码,或者在创建文件时直接保证 UTF-8。实验室里最简单的方法是避免使用全角字符,老老实实准备英文单词文本。
6. wordcount 实验的验证习惯:从 _SUCCESS 到日志链路
6.1 wordcount 两条核心命令,注意路径别写错
Hadoop 自带 wordcount 示例程序,位于 mapreduce 的 examples jar 里,路径是/opt/hadoop-2.10.1/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.10.1.jar。运行命令比自定义 jar 简单,不需要写主类名,直接写 wordcount 这个别名即可:
hdfs dfs -mkdir -p /data/wordcount echo "hello world hello hadoop hello mapreduce" | hdfs dfs -put - /data/wordcount/input.txt hadoop jar /opt/hadoop-2.10.1/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.10.1.jar wordcount /data/wordcount /output/wordcount这里有个细节:/output/wordcount 是绝对路径,对应 HDFS 根目录下的 output 目录。如果你当前用户不是 hdfs,可能会因为根目录不可写而报错,所以我更推荐把所有实验路径放在 /user/root 下面,比如 /user/root/wordcount-output,避开权限束缚。另外注意,命令里的输出目录 /output/wordcount 在第一次运行时创建成功,第二次再跑同样的命令就会撞上“output directory already exists”,这点和自定义 jar 完全一致。
执行完成后,验证这五个地方才能确定作业真的成功了:
hdfs dfs -ls /user/root/wordcount-output hdfs dfs -cat /user/root/wordcount-output/part-r-00000 hdfs dfs -cat /user/root/wordcount-output/_SUCCESS第一行看目录里有哪些文件,第二行看统计结果,第三行确认 _SUCCESS 标记文件存在。_SUCCESS 是 Hadoop 作业成功结束的空文件,只有正常完成的作业才会生成它——如果中途失败,目录里只会有 part 文件和日志,没有 _SUCCESS。一次优秀的验证习惯是:先看 _SUCCESS,再看 part-r-00000 的内容。这个习惯能帮你快速分清“作业跑完了但结果错”和“作业根本没跑完”两种完全不同的情况。
6.2 日志链路:从 Web UI 到 YARN 日志
命令行跑完后,如果对作业内部的执行过程有疑问,最有力的验证工具是 YARN 的 Web UI。在浏览器里打开 http://<你的服务器IP>:8088/cluster,找到对应的 Application ID(终端里运行 yarn application -list 也能拿到),点进去可以看到作业的 Map 数、Reduce 数、启动时间、结束时间、消耗的容器资源。再往下点某个 task,可以看到具体的 container 日志,包括 map 阶段的输出、reduce 阶段的输出、报错堆栈。
这一环节能验证很多实验报告里“写没写”但“没验没验”的东西:比如 triangle 作业时 map 数量是否为 1(因为只有一个输入文件,一个 split);wordcount 时 reduce 数量是否为 1(默认 reduce 数是 1);每个 reduce 的输出是否均匀。
从那以后,我每次跑完一个 MR 作业,都会强制走一遍这个验证链:先看 _SUCCESS 文件,再看 part 文件内容,然后打开 Web UI 确认任务数和时间,最后再决定要不要去翻日志。这个习惯帮我挡住过好几次“表面成功、实际数据错了”的翻车事故。实验报告可以只写到“运行成功查看统计结果”,但如果你想真正理解 MapReduce 的工作机制,这一步的日志链路值得你多花十分钟。希望帮到你。
本文还有配套的精品资源,点击获取