MapReduce实验全解析:WordCount入门与Writable分区实战

📅 发布时间:2026/10/12 13:48:08
MapReduce实验全解析:WordCount入门与Writable分区实战
简介围绕《大数据技术原理与应用》实验四“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。这一对一对的数据进 Mappermap 方法处理完输出的中间键值对不会直接进 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 输出的键值对要序列化后写入磁盘、在网络中传给 reducerJava 原生序列化机制 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、sumFlowreadFields 必须严格按同一顺序读错一位不会抛异常数据全乱这是序列化最常见的黑匣子问题。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 设为 2048map 和 reduce 容器内存分别设 512m如果嫌改配置麻烦可以在提交命令里加参数覆盖-D mapreduce.map.memory.mb512 -D mapreduce.reduce.memory.mb512。伪分布式还有一个优势日志都在本机出问题时去 $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 MapperLongWritable, 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 ReducerText, IntWritable, Text, IntWritable { private IntWritable result new IntWritable(); Override protected void reduce(Text key, IterableIntWritable 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 失败重试ctrlC 中断后用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、sumFlowreadFields 就必须按同样顺序读这三个字段否则反序列化后数据全部错位。这个问题不会报错只会算错是最隐蔽的坑我见过有人排查了一下午最后发现是 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 PartitionerText, 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 和 FlowBeanFlowReducer 负责按手机号累加流量。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 的 Stringreduce 端反序列化出来类型对不上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这类环境问题在你排错顺序里应该排到代码前面因为环境是最大的黑匣子。最后说一个带血泪的教训不管代码多简单都要用一个只有三五行的最小输入文件先跑通再换大文件。我在实验课上见过太多人一上来把整本书导进集群跑二十分钟后数据错位回头查才发现是输入里有非法字符。先用小文件验证逻辑再用大文件验证性能这两步分开实验就能稳稳收尾。希望帮到你。本文还有配套的精品资源点击获取