简介:围绕《大数据技术原理与应用》实验四“MapReduce初级编程实践”,这是一份覆盖面较全的课程实验参考文档,适合计算机专业学生、大数据初学者及MapReduce开发者用作入门练习。文档基于VMWare虚拟机、Ubuntu 18.04与Eclipse环境,结合JDK1.8、Hadoop-3.1.3、HBase-2.2.2,完整呈现从项目创建、jar包导入、Java类编写到HDFS提交运行的全流程,重点解决数据去重、数据排序与数据挖掘三类典型问题,包括文件合并去重、输入文件排序及表格信息挖掘等实验任务。压缩包内含1个docx说明文档,大小10.48MB,内容既有核心代码实现与运行结果,也整理了Hadoop配置错误、权限不足、数据倾斜、格式不一致等常见问题的解决方案和实验心得。目前已有133人学习,适合作为课程作业参考、实验报告模板或考前复习材料,能够帮助读者快速掌握MapReduce编程方法和排查问题的思路。
1. 实验报告四在讲什么:MapReduce 首次上手的三个难点
MapReduce 初级编程实践,是《大数据技术原理与应用》里第一次让你真正写出分布式程序的实验。前面几课还在用 hdfs shell 敲指令,本质上是单机思维;到这一课,要求你写一个能跑在 Hadoop 上的 Java 程序,打成 jar 包提交到集群分析文本数据。我带新人的经验是三道坎:编程模型从循环遍历变成 Mapper 和 Reducer 分工;调试方式从控制台输出变成看日志和计数器;最难受的是,分布式跑完结果是文件不是打印,没法一眼确定对错。下面按实验报告的路线,从数据流到 WordCount 到调试方法,再把我踩过的坑一次讲清楚,适合正在写实验报告四、或者刚装完 Hadoop 准备提交第一个 MapReduce 程序的人。
2. MapReduce 编程模型先立住:Mapper、Reducer 与 Driver 的分工边界
2.1 先搞懂数据流:分片、Map、Shuffle、Reduce 四个阶段里谁在干活
很多初学者拿到实验要求直接抄 WordCount,跑通了也不知道为什么输入目录下放两个文件就输出两份 part 结果。我一般让学生先画一条数据流:输入文件先被 Hadoop 切成 InputSplit,默认一个 HDFS 块对应一个分片,块大小在伪分布式里默认 128MB,但注意单个小文件即使只有 1KB 也会独占一个分片,所以分片数量由文件个数和块大小共同决定。每个分片被 RecordReader 逐行读成键值对,键是行偏移量 LongWritable,值是整行内容 Text。这一对一对的数据进 Mapper,map 方法处理完输出的中间键值对不会直接进 reducer,而是先进 shuffle 阶段。
shuffle 里做了三件事,顺序不能乱:分区决定某个键去哪一台 reduce,默认 HashPartitioner 按 key 的哈希值对 reduce 个数取模;排序按 key 做字典序,同一个分区内所有键是有序的;合并是可选项,在 map 端本地把同 key 的 value 先聚合一遍,减少网络传输。之后每个 reducer 拉取属于自己分区的数据,框架再按 key 分组,把同一个 key 的所有 value 放进同一个 Iterable,调用一次 reduce 方法。最后 reducer 把结果写入 HDFS 的 part-r-xxxxx 文件,reducer 有几个,part 文件就有几个。
这块只看代码非常难理解,因为 Mapper 和 Reducer 里没有一句显式的 shuffle 代码,它是框架在 map 输出之后、reduce 输入之前悄悄做的。实验四要求你写的三个部分是经典套路:继承 Mapper 重写 map、继承 Reducer 重写 reduce、在 main 里用 Job 类组装。段与段之间所有传输格式被 Hadoop 约定死,所以你必须知道 shuffle 的存在,否则后面遇到"reduce 收不到数据""分组不符合预期"这类问题,你会以为是代码 bug,实际是没理解分区和分组的默认行为。有个很常见的误用是:以为相同的 key 一定进同一个 reducer,这在默认分区下成立,但如果你在驱动里改了分区器或按字段子串设置了 key,进了不同分区,相同的逻辑 key 就会被拆开处理。
还有一个关键参数叫 mapreduce.input.fileinputformat.split.minsize 和 maxsize,默认情况下不调它。实验里最直接的现象是:你丢 3 个小文件进输入目录,控制台会出现 3 个 map,而不是按数据量来。这不算错,但如果你以为 map 数由数据量决定,方向就反了。map 数量是分片决定的,reduce 数量才是你代码里 setNumReduceTasks 决定的。入门实验不需要调 split 参数,但要能在报告里写下"map 任务数与输入分片数一致"这句观察,说明你真看过作业监控页面。
2.2 Mapper 与 Reducer 的接口约束:为什么键值对必须是 Writable
看泛型<LongWritable, Text, Text, IntWritable>,很多学生问为什么不用 Java 自带的 Long 和 String。答案在分布式传输。Mapper 输出的键值对要序列化后写入磁盘、在网络中传给 reducer,Java 原生序列化机制 Serializable 会把类名、继承结构、版本号都写进字节流,体积大而且只能 Java 自己读;Hadoop 自定义的 Writable 接口只写自身字段,紧凑而且字段顺序可控。所以凡是参与 map 输入、map 输出、reduce 输出三处的数据类型,都必须实现 Writable。Text、IntWritable、LongWritable、NullWritable 是框架已经实现的,够用;一旦要传自定义对象,就必须自己实现 Writable,重写 write 和 readFields,这是实验四后半段的标准考点。
要区分 Writable 和 WritableComparable 两个接口:Writable 只管序列化,WritableComparable 在序列化之外还要求能 compareTo 比较,所以专门被用来做 key。因为在 shuffle 的排序和分组环节,框架需要比较 key 的大小;Text 和 IntWritable 都实现了 WritableComparable,可以直接当 key 用。而你自定义的 FlowBean 如果只实现 Writable,它只能当 value,不能当 key,除非再实现 compareTo。这个区别在实验报错里非常常见:把只实现了 Writable 的类写到 setMapOutputKeyClass 里,运行时报错 Comparable 缺失。
另一个容易被忽略的点:Text 和 String 不是一回事。String 里的 length() 是 UTF-16 的 char 个数,Text 里的 getLength() 是字节数,Text 还支持在已有对象上 set 新值复用。在 Mapper 里频繁 new Text 会造成明显 GC 压力,所以我的模板里用 word.set(w) 而不是 new Text(w)。至于字段顺序错位的危害,我在第 4 章举个具体例子:write 写的是 upFlow、downFlow、sumFlow,readFields 必须严格按同一顺序读,错一位不会抛异常,数据全乱,这是序列化最常见的"黑匣子"问题。
2.3 本地模式、伪分布式、真集群怎么选:实验报告该在哪种模式下跑
《大数据技术原理与应用》的实验环境一般是单机装 Hadoop,跑伪分布式。所谓伪分布式,就是一台机器上同时启动 NameNode、DataNode、ResourceManager、NodeManager 等全部进程,对外它表现得像一个小集群,完整走完"提交 jar → 拆分 → 调度 → map → shuffle → reduce → 写回 HDFS"全链路。它的边界也清楚:所有进程共享同一台机器的 CPU 和内存,没有真正的跨节点网络传输,shuffle 里的"拉取"变成本地读文件。
另一种是本地模式,MapReduce 跑在提交作业的那个 JVM 里,不启动 YARN,不产生 part 文件,结果直接在控制台打印。本地模式适合做单元测试,跑得快,但它不写 HDFS,也就验证不了 hdfs 命令下的输出,我一般只在验证 Mapper/Reducer 逻辑时用,不让它代替伪分布式交实验。完全分布式是三台以上机器各司其职,实验报告四用不着,但你要知道三者的框架差异,否则会出现在本地模式跑通、换伪分布式就报错的情况,本质是配置路径和权限不同。
伪分布式下最需要调的是内存参数。如果虚拟机只分给 Hadoop 2GB 甚至 1GB 内存,YARN 默认的容器申请会让任务 OOM。我常用的做法是在 yarn-site.xml 里把 yarn.nodemanager.resource.memory-mb 设为 2048,map 和 reduce 容器内存分别设 512m;如果嫌改配置麻烦,可以在提交命令里加参数覆盖:-D mapreduce.map.memory.mb=512 -D mapreduce.reduce.memory.mb=512。伪分布式还有一个优势:日志都在本机,出问题时去 $HADOOP_HOME/logs/userlogs 翻 task 的 stderr,比在远程集群上方便得多,这对新手排查是巨大的救场。
3. 用 Eclipse 跑通 WordCount:环境检查、代码与提交命令
3.1 伪分布式启动前的检查清单:别上来就 start-all.sh
实验报告最常见的翻车起点不是代码,是环境和 jar 包。我每次带实验都让学员先按下面顺序检查,哪一步报错就在哪一步修,不要跳:
jps看进程:NameNode、DataNode、ResourceManager、NodeManager、SecondaryNameNode 是否齐全;hdfs dfsadmin -report看 DataNode 是否存活、存储容量是否正常;- 启动命令:伪分布式一般先
hdfs namenode -format初始化(只做一次,重复格式化会丢数据),再start-dfs.sh和start-yarn.sh; - 确认输入目录:
hdfs dfs -ls /input能看到目录,且目录下有文本文件,没有就先建目录传文件。
# 检查 HDFS 状态,三个命令按顺序执行 jps hdfs dfsadmin -report hdfs dfs -ls /input解释一下:jps 是 JDK 自带的 JVM 进程查看工具,比 ps 过滤 java 更直观;dfsadmin -report输出里的Live datanodes (1)表示数据节点注册成功;-ls /input确认实验输入文件在位。如果你刚格式化完 NameNode,第一次启动 DataNode 上报可能需要十几秒,ssh 免密没配好时 DataNode 会反复退出,这个在避坑章里讲。
检查完进程之后,确认 HDFS 上没有残留的上一次输出目录。MapReduce 的输出目录必须不存在,否则会直接报 FileAlreadyExistsException。我习惯在提交前先跑一条hdfs dfs -rm -r /output,实验时可以任性删,但如果你这是生产数据,绝对不要养这个习惯,删除前先-ls看清楚里面是什么。
3.2 WordCount 完整代码:带注释版的 Mapper、Reducer、Driver
下面这份是我在实验课里给的 WordCount 模板,和 Hadoop 官方示例逻辑一致,但注释写得更细。注意 Mapper、Reducer、Driver 三个类要分三个文件放,因为一个 Java 文件只能有一个 public 类。
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; // Mapper:输入键是行偏移量,输入值是整行文本 public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> { private final static IntWritable one = new IntWritable(1); private Text word = new Text(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { // 按空格和制表符切分,得到该行里的单词 String[] words = value.toString().split("\\s+"); for (String w : words) { if (w.length() == 0) continue; word.set(w); context.write(word, one); // 每出现一次输出 1 } } } // Reducer:相同单词的所有 1 会在这里累加 public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> { private 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); } } // Driver:配置作业的入口 public class WordCount { public static void main(String[] args) throws Exception { if (args.length != 3) { System.err.println("Usage: WordCount <in> <out> <reducerNum>"); System.exit(2); } Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "word count"); job.setJarByClass(WordCount.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])); job.setNumReduceTasks(Integer.parseInt(args[2])); // 实验可设 1 或 2 System.exit(job.waitForCompletion(true) ? 0 : 1); } }代码后的逻辑说明:Mapper 里我复用了 word 和 one 两个对象,没有每次循环 new,这是 MapReduce 的常见优化,因为 map 方法会被调用成千上万次,new 对象会给 GC 造成压力。context.write(word, one) 里 one 是同一个对象引用,序列化时框架会把它当值拷贝一份,所以复用不会导致数据错乱。Reducer 里必须 new 一个 result 再 set,因为同一个 result 对象要多次写出,set 会替换内部值。
参数说明:args[2] 是 reduce 任务数,这个参数是我额外加的。实验默认不传时 reduce task 是 1,但传 2 可以观察"输出目录里出现两个 part-r-xxxxx 文件"的效果。job.setCombinerClass(WordCountReducer.class) 对求和场景等效于本地预聚合,能明显减少 map 到 reduce 之间的网络传输量,但不是什么 reduce 都能当 combiner,比如求平均值就不行。
提示:Combiner 只有在操作满足交换律和结合律时才能和 Reducer 用同一个类。求和可以,求平均不行。如果你在实验里把 Combiner 设成求平均的逻辑,结果一定错,而且错得没有规律。
3.3 打出 jar 包并提交:hadoop jar 命令和四个必查参数
Eclipse 里导出 jar 包:右键项目 → Export → JAR file,选 WordCount.java 所在包,不需要把 Hadoop 依赖打进去,运行时集群会提供。如果用 Maven,常见做法是 maven-jar-plugin 配合 manifest 指定 Main-Class,或者直接mvn package后配合hadoop jar target/xxx.jar提交。新手最容易把依赖也打进 fat jar,导致 jar 体积几十 MB,提交慢且容易出包冲突。
# 提交作业:<输入目录> <输出目录> <reduce数> hadoop jar wordcount.jar WordCount /input /output 1 # 查看结果 hdfs dfs -cat /output/part-r-00000 | head -n 20 # 删除输出目录以便重跑 hdfs dfs -rm -r /output命令说明:hadoop jar 第一个参数是 jar 路径,第二个是主类全限定名,如果你的代码有 package,这里必须写包名加类名,比如 com.example.WordCount;如果 manifest 里指了 Main-Class,第二个参数可以不写。后面依次是程序 args。/output 必须是 HDFS 上不存在的路径,所以每次重跑前先 -rm -r。hdfs dfs -cat 把结果按顺序打到终端,配合 head 截取前 20 行,这是实验阶段最有效的检查方式。
提交后你会看到控制台输出 map 0% reduce 0%、map 100% reduce 100%,然后出现 "Job complete"。如果停在某个百分比不动,多数不是慢,而是数据倾斜或 task 失败重试,ctrl+C 中断后用yarn application -status <appId>查,或者去 ResourceManager 的 Web UI,默认 8088 端口看任务日志。伪分布式阶段日志通常直接打到终端,但一旦 yarn 回收了 task,就只有去 logs/userlogs 目录翻,这个路径在生产上是排查利器。
4. 把实验从 WordCount 升一级:自定义 Writable 与分区的完整骨架
4.1 为什么 WordCount 不够:遇到"一条记录多字段"时 Writable 登场
实验报告四在 WordCount 之外,常配一个统计题:比如统计手机号的上行流量、下行流量和总和。这种题每条记录是一个对象,不再是"单词 → 计数",如果你用四个 String 当中间值传,mapper 和 reducer 的泛型会变得很长,而且字符串拼接在传输和解析上都容易踩坑。正确做法是实现一个 Writable 类,把一条记录封装成一个对象,让整个对象能被序列化后在网络上传输。传对象而不是传多个字符串,代码结构清晰,业务字段增减时只改一个类。
我一般给实验用的模板是一个 FlowBean,包含上行流量、下行流量、总流量三个字段,手机号做 key 不放 bean 里。核心代码如下。
import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import org.apache.hadoop.io.Writable; public class FlowBean implements Writable { private long upFlow; // 上行流量 private long downFlow; // 下行流量 private long sumFlow; // 总流量 public FlowBean() {} // 反序列化时必须有无参构造器 public void set(long up, long down) { this.upFlow = up; this.downFlow = down; this.sumFlow = up + down; } @Override public void write(DataOutput out) throws IOException { out.writeLong(upFlow); out.writeLong(downFlow); out.writeLong(sumFlow); } @Override public void readFields(DataInput in) throws IOException { this.upFlow = in.readLong(); this.downFlow = in.readLong(); this.sumFlow = in.readLong(); } @Override public String toString() { return upFlow + "\t" + downFlow + "\t" + sumFlow; } }写完后逻辑说明:write 和 readFields 的顺序必须严格一致,write 写了 upFlow、downFlow、sumFlow,readFields 就必须按同样顺序读这三个字段,否则反序列化后数据全部错位。这个问题不会报错,只会算错,是最隐蔽的坑,我见过有人排查了一下午,最后发现是 write 和 readFields 顺序写反了。还要注意必须保留无参构造器,因为 Hadoop 反序列化时会通过反射调用 newInstance(),你没写无参构造器,运行时报的是 NoSuchMethodException,但堆栈信息说得很含糊。
4.2 自定义分区器:按手机号前缀把数据分到不同 reduce 输出
Partitioner 是实验四第二个常见扩展点:默认 HashPartitioner 按 key 的 hashCode 对 reduce 数取模,结果分区均匀但业务无意义。自定义 Partitioner 让你决定某个 key 走向哪个 reduce。以运营商流量统计为例,想把 139 号段和 137 号段分开,或者按省份写不同输出文件,就重写 Partitioner。
import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Partitioner; public class ProvincePartitioner extends Partitioner<Text, FlowBean> { @Override public int getPartition(Text key, FlowBean value, int numPartitions) { String phone = key.toString(); if (phone.startsWith("139")) return 0; // 分区 0 if (phone.startsWith("137")) return 1; // 分区 1 return 2; // 其他放分区 2 } }在 Driver 里挂上分区器并设置 reduce task 数量,代码就三行:
job.setPartitionerClass(ProvincePartitioner.class); job.setNumReduceTasks(3); // reduce 数量必须大于分区器返回的最大值 + 1逻辑说明:分区器返回的 int 是 [0, numPartitions) 之间的分区号,reducer 个数必须大于等于最大分区号加 1,你返回 2 却只设 2 个 reduce,会直接抛异常;设 4 个但只有 3 个分区数据,会有一个 reduce 处理空数据,最后输出一个 0 字节的 part-r-00003 文件,这是正常现象,不会报错。实验报告写分析时,可以把"reduce 数大于分区数会产生空输出文件"写进去,老师会觉得你是真跑过而不是抄的。
4.3 把这些组件装进一个作业:区分 map 输出和 reduce 输出的类型
以上零散组件怎么拼,我给一个缩小版骨架,实验时把 WordCount 的 main 函数替换成下面骨架即可。FlowMapper 负责从日志行里截取手机号、上行流量和下行流量,产出 Text 和 FlowBean;FlowReducer 负责按手机号累加流量。
Job job = Job.getInstance(new Configuration(), "flow-stat"); job.setJarByClass(FlowDriver.class); job.setMapperClass(FlowMapper.class); job.setReducerClass(FlowReducer.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(FlowBean.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(FlowBean.class); job.setPartitionerClass(ProvincePartitioner.class); job.setNumReduceTasks(4); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1);参数说明:setMapOutputKeyClass 和 setOutputKeyClass 是两回事,map 输出键类型不一定等于 reduce 输出键类型。上面例子 map 输出 key 是 Text(手机号),reduce 输出 key 还是 Text,所以两个都设;如果 map 输出类型和 reduce 输出类型一致,不设置 setMapOutput* 也能跑,一旦不一致,不设置就会报类型转换错误。这是实验里高频报错点,我每次都强调:Mapper 端泛型和 Reducer 端泛型要分开看待,Job 配置里分别指定。另一个坑是 setNumReduceTasks(4) 时,分区器最大返回分区号 2,那么分区 3 没有数据,输出目录会出现一个空 part,很多学生以为程序出 bug 了,其实没有,数一下 reducer 个数就对上了。
5. MapReduce 初学避坑:5 个我浪费过一晚上的问题
5.1 现象:任务一直卡在 map 100% reduce 0%,跑步动的程序
我第一次跑 WordCount 就是这个状态,map 很快就 100% 了,reduce 卡死在 0% 不动。原因是我的 map 输出 key 用了 Java 的 String,reduce 端反序列化出来类型对不上,reduce 拉取数据后没法处理,一直在重试。解决方法是把 map 输出类型改成 Text、IntWritable 等 Writable 类型。所以遇到卡住先查泛型声明对不对,不要先怀疑集群配置。把 setMapOutputKeyClass 和 setMapOutputValueClass 写明确,别依赖默认值,这是最快定位手段。
5.2 现象:输出目录里只有 _SUCCESS,没有 part-r-00000
输出目录下如果只有 _SUCCESS 空文件,说明 job 实际没有产生任何 reduce 输出。常见原因有两个:一是 reduce 函数里写出的 key/value 的泛型与配置的 setOutputKeyClass 不一致,框架默默丢掉了无法转换的记录;二是输入目录下文件本身是空文件,或文本编码不是 UTF-8 导致切出的行全是空。解决方法:先用 hdfs dfs -cat /input/xxx 确认文件有内容;再看 reduce 输出的泛型声明;最后看日志有没有 WARN。记住,_SUCCESS 只是标记文件,它代表作业结束,不代结果存在。
5.3 现象:本地能跑通,集群上报 ClassNotFoundException
在 Eclipse 里直接运行 main 方法用的是 classpath 依赖,但打成 jar 提交到 YARN 后,如果自定义 Writable 类没被打进 jar 包,或者依赖的第三方库没带上,NodeManager 反序列化时就会报找不到类。解决方法是确认 jar 包内容:用jar tf wordcount.jar看自定义类在不在;第三方库可以用-libjars参数或者打成 fat jar。我踩过最疼的一次是改代码后重新导出 jar 时勾选错了模块,跑的还是老代码,所以现在导出后我都先看 jar 包时间戳,再提交。
5.4 现象:Reduce 结果和单机算的不一样,数据对不上
这个坑最折磨人。有一次学员统计单词数,结果比单机 grep 出来的多一截,最后发现是输入的文本文件里有多余的空行和制表符,split("\s+") 能过滤普通空格,但某些行是连续制表符加回车,正则匹配后产生了空串,又把它写进输出。更隐蔽的是如果 map 端写了 Combiner,且 Combiner 和 Reducer 逻辑不等价,比如求平均,结果必然错。所以要保证 Combiner 是幂等可结合的操作,求和、求最大都可以,求平均绝对不行。实验报告如果写了 Combiner,请顺手写清楚为什么求和场景 Combiner 等价于 Reducer。
5.5 现象:重复运行同一作业报 FileAlreadyExistsException
输出目录已存在时,MapReduce 为了防止覆盖直接抛异常。你以为是 bug,实际上是框架的安全机制。我一直用的解决办法:每次重跑前先 hdfs dfs -rm -r /output,或者给输出目录加时间戳。但在生产上千万不要随意删目录,至少先 -ls 确认路径和内容。还有一种类似情况是两个人同时提交写同一个输出目录,第二个会直接失败,这也能解释为什么实验课多人共用集群时容易出现"作业被拒"。
6. 验证你的实验结果:用计数器判断作业是真的健康,还是碰巧出了结果
跑通实验不等于实验成功。我看一个人写的 WordCount 靠不靠谱,不盯着屏幕上 "Job complete" 那行字,先看三样东西:part 文件个数和大小、reduce 输出记录数、日志里有没有 WARN 级别的任务重试。
第一招是看计数器。作业结束后控制台会打印一组 Counters,我用得最多的是 Map-Reduce Framework 下的三个值:Map input records、Reduce output records、Spilled Records。Map input records 应该等于输入文件总行数,Reduce output records 应该等于你想要的结果条目数,两个能对上,作业逻辑基本没问题。如果 Spilled Records 很大而内存参数没调,说明 map 输出溢写磁盘了,性能不健康,但功能上没错。
第二招是强制显式打印。我在 main 方法最后加一段,直接把计数器值打出来,实验阶段比去翻 part 文件数行数省事:
if (job.waitForCompletion(true)) { System.out.println("reduce output records=" + job.getCounters().findCounter("Map-Reduce Framework", "Reduce output records").getValue()); System.exit(0); } else { System.exit(1); }逻辑说明:waitForCompletion(true) 返回后计数器还在,直接取 Reduce output records 的值。这个值比你去 cat 多个 part 文件数行数可靠,因为多 reducer 时输出文件不止一个。注意计数器名称里的空格和大小写都不能写错,写错就抛 IllegalArgumentException,建议从日志里拷贝字符串而不是手打。
第三招是看日志验证。我把日志级别调成 INFO,提交时看到 "Running job: job_xxxx",多看一眼 map 100% reduce 100% 之间有没有 Task failed 字样。伪分布式下最典型的隐蔽失败是 Map output records 有值,但 Reduce input records 少于它,这通常意味着部分 map task 被 kill 后重新执行。发现这类情况,先去看 yarn 的 userlogs,路径在 $HADOOP_HOME/logs/userlogs/ ,能翻到每个 task 的 stderr,里边的 WARN 才是真正的病根。
我自己的习惯是,每次跑完实验固定记一条 echo $? 加时间戳的命令,再顺手存一份输出文件的 md5,重跑时对比。这不是课程要求,但对以后调生产 job 有好处。碰到玄学问题,先确认你看到的日志是哪一次提交的,再谈代码哪里错了。DataNode 时断时续、YARN 容器被 kill,这类环境问题在你排错顺序里应该排到代码前面,因为环境是最大的黑匣子。
最后说一个带血泪的教训:不管代码多简单,都要用一个只有三五行的最小输入文件先跑通,再换大文件。我在实验课上见过太多人一上来把整本书导进集群,跑二十分钟后数据错位,回头查才发现是输入里有非法字符。先用小文件验证逻辑,再用大文件验证性能,这两步分开,实验就能稳稳收尾。希望帮到你。
本文还有配套的精品资源,点击获取