MapReduce入门必学:WordCount全流程详解与Linux实战
凡是接触过大数据的人几乎都绕不开MapReduce和WordCount这对组合。WordCount就是统计文本里每个单词出现的次数看上去简单但它把MapReduce的整个执行链路完整走了一遍——从Map切分、Shuffle分组到Reduce聚合再加上打包、上传、提交到Linux服务器上跑通这一套流程弄明白Hadoop分布式计算算是真正入了门。这篇文章就把我实际跑通这个实例的过程完整记录下来适合刚学完Java基础、准备进入大数据方向的同学参考。很多人学MapReduce的时候在IDE里跑个本地单元测试就觉得自己会了但真正的生产环境是Linux服务器是HDFS集群。代码写完只是第一步怎么用Maven打成可执行的jar包怎么传到服务器上怎么把测试数据放到HDFS怎么用hadoop jar提交作业每一步都有坑。这篇内容就是把这些坑一个个填平给出可以直接照着做的完整操作流程。1. 整体设计与思路拆解1.1 为什么WordCount是MapReduce的“Hello World”很多教程把WordCount说成MapReduce的Hello World这个比喻是准确的。一个Hello World至少要做到两件事环境能跑通、程序能输出结果。WordCount严格来说还多了一件让你理解分布式计算的思维转换。MapReduce是一个计算模型核心就两个阶段。Map阶段做“拆”把输入数据拆成一批键值对Reduce阶段做“合”把相同键的值汇总起来。WordCount恰好把这两个阶段的含义展示得特别清楚拆就是把一行文本拆成一个个单词每个单词记一个1合就是把同一个单词的所有1加起来得出最终次数。更重要的是WordCount是一个端到端的最小闭环。从写Java代码、配Maven、编译打包、上传到Linux服务器、启动集群资源、提交作业、观察日志、读取结果整个链路全部走通你才算是真正接触到了大数据开发的完整工作方式。我见过不少同学在本地把代码跑出来了但一到服务器就懵原因就是链路里的某个环节从来没见过。1.2 一次WordCount作业背后的完整执行链路当你执行hadoop jar命令后作业其实会经历这样一条链路输入文件被Hadoop按块切分每个split对应一个Map任务。Map任务逐行读取文本把每一行按分隔符拆成单词输出单词, 1键值对。框架自动完成Shuffle和Sort把相同单词的键值对分到同一个Reduce任务。Reduce任务对每个单词的所有值进行求和。最终结果写到HDFS的输出目录。这里最常见的一个困惑是Shuffle到底是谁做的答案是框架。MapReduce的设计哲学就是“想做分布式计算但你不用管数据怎么跨节点传输”。Shuffle阶段帮你完成了分区、排序、合并这三件事而你只需要写好Map逻辑和Reduce逻辑。具体到WordCountMap阶段的输入键是行号输入值是整行文本。Map输出的键是单词本身输出的值是整数1。框架把这些单词, 1按照单词的哈希值分区同一个单词必然进入同一个Reducer然后交给你的reduce方法去累加。整个过程你不需要写一行网络通信代码这就是框架的威力。1.3 为什么非要“打包上传到Linux服务器跑”不少初学者有一个疑问我在Windows或Mac的IDE里能直接跑为什么还要打包到Linux服务器上答案很简单你现在跑的是“本地模式”它只是为了调试方便真正的大数据集群跑在Linux上你需要的是“YARN集群模式”。打包的本质是把你的class文件、依赖库和配置文件打成一个可执行的jar包。Linux服务器上装好了Hadoop但你不可能把源码放上去现场编译更不可能在服务器上配一套IDE。把jar包丢上去用一行hadoop jar命令提交作业才是工程化的工作方式。Maven在这里最大的价值是管理依赖。你只需要在pom.xml里声明一个hadoop-client依赖它会自动把MapReduce相关的类库全部拉下来打包时也不需要手动去翻lib目录找classpath。这是最省心的一条路也是我建议新人直接选用的方式。2. 环境准备与前置检查2.1 一台可用的Linux服务器和一套匹配的版本动手之前先把环境理清楚。我踩过最大的坑就是版本不匹配本地Maven用的Hadoop是3.2服务器上装的是2.10跑起来直接报NoSuchMethodError。所以版本这件事一定要在写代码之前就确认好。组件版本建议说明JDK1.8 或 11Hadoop 2.x必须用83.x用8或11都行Hadoop3.3.x3.x系列API稳定相对好排坑Maven3.6负责依赖管理与构建打包Linux任意发行版只要能正常跑Hadoop进程即可这里有一个重要原则开发环境的依赖版本和服务器上的Hadoop版本大版本必须保持一致。比如服务器是3.3.xpom里就写3.3.x不要写2.10.x。小版本有差异通常影响不大但大版本跨代通常会导致运行期才暴露的兼容问题编译阶段根本发现不了。2.2 检查Hadoop环境是否真的可用先别急着写代码登录服务器把下面两个命令执行一下hadoop versionjpshadoop version用来确认Hadoop命令本身可用。jps是JDK自带的一个工具用来查看服务器上正在运行的Java进程。如果HDFS和YARN都正常启动你至少能看到NameNode、DataNode、ResourceManager、NodeManager这几个进程。再检查HDFS是否可用hdfs dfs -ls /如果这一步就报错别急着写代码先把集群环境解决。hadoop命令都起不来后面的一切都是空中楼阁。最常见的失败原因是JAVA_HOME没配好或者是HDFS没有完成格式化。这两类问题都可以在Hadoop的启动日志里直接找到答案。2.3 规划好HDFS目录和权限在HDFS里输入目录和输出目录必须提前想清楚。输出目录绝对不能提前创建因为MapReduce要求输出目录不存在否则会直接报错。输入目录需要提前准备好甚至可以在HDFS上创建一个专门目录来存放测试数据。普通用户连到服务器上经常遇到权限问题。最快的解决办法是使用启动Hadoop集群的那个用户来操作或者对目录执行hdfs dfs -chmod -R 777 /wordcount。这种方式在本地实验环境里没有问题生产环境就别这么干了该上Kerberos上Kerberos该配ACL配ACL。3. WordCount代码实现与核心逻辑解析3.1 创建Maven工程与pom配置先用你的常用IDE创建一个Maven工程建议包名统一比如我用com.example.hadoop。src/main/java下放三个类WordCountMapper、WordCountReducer、WordCountDriver。pom.xml的核心配置如下project modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdwordcount-demo/artifactId version1.0/version properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version3.3.4/version /dependency /dependencies build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.2.4/version executions execution phasepackage/phase goals goalshade/goal /goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.example.hadoop.WordCountDriver/mainClass /transformer /transformers /configuration /execution /executions /plugin /plugins /build /project这里引入maven-shade-plugin是关键一步。它的作用是把项目依赖的Hadoop类库全部合并进最终的jar包打出一个“胖包”。没有它的默认打包方式打出来的只是一个“瘦包”只包含你自己的class文件。瘦包在服务器上也能运行但需要手动指定classpath对新手来说很麻烦。所以我的建议很直接第一次跑通直接打胖包。3.2 Mapper类实现理解“拆分”这一步WordCountMapper的完整代码如下package com.example.hadoop; 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 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 line value.toString(); String[] words line.split( ); for (String w : words) { if (w.trim().length() 0) { word.set(w); context.write(word, one); } } } }泛型LongWritable, Text, Text, IntWritable的含义是输入键类型、输入值类型、输出键类型、输出值类型。Map阶段的输入键是行号所以用LongWritable输入值是整行文本用Text。输出键是单词本身还是用Text输出值是计数1用IntWritable。很多第一次接触的同学会问为什么不用Java原生的String和Integer因为Hadoop是一个分布式系统数据要在节点之间序列化和反序列化。Hadoop自带的Writable类型是它自己的序列化接口性能比Java原生序列化好很多。跨节点传输数据时Text和IntWritable是标配。这段代码里还有一个细节line.split( )是按单个空格切分。如果你的测试文本用的是制表符分隔需要换成line.split(\\t)。如果不确定文本里是什么分隔符可以先用line.split(\\s)这个正则会匹配所有空白字符。入门阶段用英文文本最简单中文文本不在这套逻辑的处理范围内。3.3 Reducer类实现理解“聚合”这一步WordCountReducer的完整代码如下package com.example.hadoop; 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 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); } }Reduce阶段的输入是某一个单词, 这个单词的所有计数这里的关键是第二个参数IterableIntWritable values。它看起来像是一个集合但Hadoop并没有把所有数据一次性加载到内存而是用迭代器方式逐个读取这样即使某个单词出现了几百万次也不会把内存打爆。这个设计是MapReduce“面向大规模数据”思想的一个体现。你写循环累加的过程就像在数一堆扑克牌里的某一种花色有哪些张一张一张翻而不是先把整副牌复制到手里再数。context.write(key, result)就是把最终的统计结果写出去输出的键是单词值是总次数。3.4 Driver主类把作业提交起来WordCountDriver的完整代码如下package com.example.hadoop; 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 { if (args.length ! 2) { System.err.println(Usage: WordCountDriver input output); System.exit(-1); } Configuration conf new Configuration(); Job job Job.getInstance(conf, word count); job.setJarByClass(WordCountDriver.class); job.setMapperClass(WordCountMapper.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); } }Driver类做的事情可以概括为四句话创建配置、创建作业、组装Mapper和Reducer、设置输入输出路径。代码里最容易被忽略的是job.setJarByClass(WordCountDriver.class)这一行它的作用是告诉框架去哪个jar包里找当前这个主类。提交作业时框架会把jar分发到各个节点上靠的就是这个配置。输入输出路径不是硬编码在代码里的而是通过args[0]和args[1]获取。这样做的原因很直接同一个jar包可以复用于不同目录的统计任务不需要每次改代码重新编译。waitForCompletion(true)参数为true时会打印出整个作业的进度日志方便你观察运行过程。如果有一天你写的Mapper输出类型和Reducer输出类型不一样还需要额外设置job.setMapOutputKeyClass和job.setMapOutputValueClass。WordCount里Mapper和Reducer的输出类型恰好一样所以只设置输出类型就够了。3.5 一个必须提前避开的坑版本匹配代码层面的实现其实很快真正让你怀疑人生的往往是版本问题。pom文件里的hadoop-client版本必须和服务器上hadoop version显示的大版本保持一致。这个坑的特点是编译能过、打包能过、上传能过但一提交作业就崩而且错误信息五花八门最常见的就是NoSuchMethodError。我曾经帮同事排查一个类似问题他本地用的客户端版本比服务器新了一个大版本作业启动阶段就一直报方法找不到。最后对照双方的hadoop version输出才确认是版本不匹配。所以我的建议是在项目根目录写一个README第一行就注明服务器Hadoop版本和JDK版本避免后面换机器、换环境时翻车。4. 打包上传与Linux服务器运行实操4.1 Maven打包用shade插件打胖包在工程根目录执行mvn clean package这条命令执行完去target目录下看会生成一个wordcount-demo-1.0.jar。你会发现它的体积有几十MB这很正常因为maven-shade-plugin把依赖的Hadoop类库全部塞进去了。这种包含依赖的包就是我们在服务器上想要的“胖包”。有人会问服务器上明明已经装了Hadoop为什么还要把依赖打进去因为hadoop jar命令虽然会把服务器上的Hadoop库加入classpath但不同版本之间很容易出现兼容问题。胖包打进来的是你自己锁定的版本和环境无关运行行为完全可控。对于学习和快速验证这是最省心、最不折腾的路线。4.2 上传jar包命令行一条scp搞定假设jar包已经生成现在需要把它从开发机传到Linux服务器。最直接的方式是用scp命令scp target/wordcount-demo-1.0.jar 用户名服务器IP:/home/用户名/这条命令的含义是把本地target目录下的jar包复制到你服务器上对应用户的home目录。如果你是在Windows命令行里执行需要确保系统支持OpenSSH也可以使用带图形界面的SFTP工具把文件拖拽上去本质上都是一样的。上传完成后登录服务器先确认文件真的在ls -l wordcount-demo-1.0.jar看到文件大小在几十MB说明上传成功。如果只有几KB那八成传错文件了去检查一下是不是打的瘦包。4.3 准备测试数据和HDFS目录先在本地创建一个测试文件命名随意比如word.txthello hadoop hello mapreduce mapreduce is powerful hello world然后把文件放上HDFShdfs dfs -mkdir -p /wordcount/input hdfs dfs -put word.txt /wordcount/input/ hdfs dfs -cat /wordcount/input/word.txt这里有一个新手常犯的错误输入路径是HDFS路径不是本地路径。MapReduce作业跑在集群上每个节点都需要能读到输入数据所以必须先put到HDFS。如果直接让作业读本地文件提交后几乎必然报文件找不到。先执行一次hdfs dfs -cat确认数据已经落在HDFS上再提交作业可以省掉一次无意义的排错。4.4 提交作业一行命令看到结果服务器上执行提交命令hadoop jar wordcount-demo-1.0.jar com.example.hadoop.WordCountDriver /wordcount/input /wordcount/output第一条参数是jar包路径第二条参数是主类全限定名后面按顺序是输入路径和输出路径。运行后控制台会滚动输出进度日志耐心等一两分钟。当看到计数器信息以及Map-Reduce任务完成的状态作业就成功了。查看结果hdfs dfs -cat /wordcount/output/part-r-00000预期输出大致如下hadoop 1 hello 3 ...如果只有一个Reduce任务输出就是一个文件命名是part-r-00000。这里r表示Reduce产出00000是分区编号。如果将来设置了多个Reducer你会看到part-r-00000、part-r-00001等多个文件每个对应一个分区。4.5 第二次运行前必须做的事WordCount这个作业每跑一次你都需要一个“干净的输出目录”。由于MapReduce框架拒绝覆盖已有输出所以第二次提交前必须先删掉上一次的输出hdfs dfs -rm -r /wordcount/output然后再重新提交作业。这个“先删后跑”的习惯一定要养成。输出目录已存在会直接导致作业初始化失败报错信息是Output directory ... already exists原因很直白框架不想在没经过确认的情况下覆盖历史结果。5. 常见问题与排查技巧实录5.1 权限问题最容易遇到的第一道坎测试阶段报得最多的是这个错误org.apache.hadoop.security.AccessControlException: Permission denied原因通常很简单当前用户对HDFS目录没有写权限。伪分布式环境下HDFS默认的超级用户是启动集群的用户而你登录服务器可能用的是另一个普通用户。最快的解决办法hdfs dfs -chmod -R 777 /wordcount如果你有hdfs命令的sudo权限也可以直接把目录owner改成当前用户。实验环境里chmod 777是最快的生产环境不要这么干那是给自己埋雷。5.2 类找不到、方法找不到依赖与版本冲突如果你提交作业后看到java.lang.ClassNotFoundException: com.example.hadoop.WordCountMapper先检查是不是打了瘦包。瘦包里没有Mapper、Reducer的class框架从jar里加载不到对应的类自然就报这个错误。解决办法就是改用带shade插件的方式重新打包。如果你看到的是java.lang.NoSuchMethodError那就是典型的版本冲突。仔细对比pom里的hadoop-client版本和服务器上hadoop version的版本。两个大版本不一致编译阶段看不出问题运行阶段就会暴露。这类问题排查起来比较隐蔽建议从一开始就锁死版本。5.3 作业卡住不执行或直接失败资源问题有些同学会遇到作业一直显示RUNNING进度却卡在0%或者直接报Container killed by the ApplicationMaster这种情况大概率是YARN资源不够。伪分布式环境默认配置了较大的内存需求如果你的服务器内存只有4G经常会发现Container分配不出来或者吃掉全部内存后被杀掉。小实验可以这样调整在提交前设置较小的map和reduce内存比如hadoop jar wordcount-demo-1.0.jar com.example.hadoop.WordCountDriver \ -D mapreduce.map.memory.mb512 \ -D mapreduce.reduce.memory.mb512 \ /wordcount/input /wordcount/output-D参数会覆盖作业配置这种方式不需要改任何配置文件适合快速验证。5.4 中文乱码与特殊字符导致结果怪异如果你直接用中文文本跑WordCount结果会非常“感人”整行中文被当成一个单词输出乱码统计次数完全不对。原因是WordCount默认按空格切分中文文本里没有空格自然拆不出来再加上文件编码可能是GBK和HDFS默认的UTF-8不一致就产生了乱码。入门阶段建议直接用英文文本测试。如果确实需要统计中文WordCount这个基础实例本身不够用你需要引入额外的分词组件那就是另一个话题了。5.5 快速排查表现象可能原因快速解决办法hadoop version都报错Hadoop环境没配好检查JAVA_HOME、PATH提交后Permission denied目录权限不足hdfs dfs -chmod -R 777 /wordcountClassNotFoundException打的瘦包用shade插件重新打包NoSuchMethodError版本不匹配对比pom和服务器版本Directory already exists输出目录未删除hdfs dfs -rm -r /wordcount/output作业卡在0%YARN资源不足调低map/reduce内存配置中文统计结果异常缺少中文分词先换英文文本验证链路我最初跑这个实例时在打包这一关卡了快两小时。那时候不懂shade拿着一个瘦包在服务器上反复试错后来才发现问题出在依赖没打进去。现在再带新人我总会让他们把版本匹配和胖包这两件事放在前面这两个坑提前避开WordCount基本一次就能跑通。等你把这个流程跑顺了再去看MapReduce的源码、去调Combiner和Partitioner会顺畅很多。