简介基于Hadoop 2.2.0的完整词频统计MapReduce示例主要面向正在学习分布式计算或准备入门Hadoop的开发者解决从零搭建首个MapReduce作业时的常见困扰。压缩包内含完整可运行的Java工程既有全部源码也提供编译后的class文件并配有约十万单词的英文测试文本可直接在集群或本地模式下执行词频统计流程清晰展示Map、Shuffle、Reduce各阶段的实现与衔接帮助读者对照代码理解分布式计算的运转机制。压缩包共17个文件以java源码和class字节码为主体另含txt测试数据以及project、classpath工程配置目录结构清晰方便导入集成开发环境查看和调试整体大小仅154KB非常轻量。目前已有5865人学习使用适合用于课程作业、实验参考或自学实践通过运行测试与阅读源码可完整掌握Hadoop词频统计的标准实现、执行细节与工程组织方法。1. Hadoop 词频统计完整版入门 Hadoop 的第一个真实作业第一次在 Hadoop 上跑通词频统计是很多人的第一个 Mapper 和第一个 Reducer。你可能会发现真正的门槛不在那几十行代码上而在环境JAVA_HOME 配没配好、SSH 通不通、HDFS 里有没有数据、yarn-site.xml 是不是少了哪一项。这份「完整版」把从零搭建伪分布式 Hadoop、写词频统计、准备数据、上传 HDFS、提交作业到验证结果的全过程串成一条完整链路适合正在做课程设计、准备 Hadoop 面试或者第一次想在自己机器上完整跑一遍 MapReduce 的人。跟完一遍你收获的不只是「能跑」而是一条可以复制到其他作业的完整流程。2. 环境准备伪分布式 Hadoop 从零搭建与启动前三件小事2.1 为什么选伪分布式而不是完全分布式Hadoop 跑 MapReduce 有三种常见拓扑单机模式、伪分布式、完全分布式。单机模式不需要启动任何守护进程作业直接跑在本地文件系统上适合验证代码逻辑但看不到 HDFS 和 YARN 的调度过程完全分布式需要至少三台机器对课程设计和入门阶段来说太重伪分布式把 NameNode、DataNode、ResourceManager、NodeManager 全部跑在同一台机器上但 HDFS 和 YARN 的完整路径都在最容易暴露真实问题。我推荐这份资源里的伪分布式方案原因有两个。第一伪分布式能完整走一遍「HDFS 上传文件 → ResourceManager 分配容器 → NodeManager 启动任务 → 结果写回 HDFS」的数据流面试里被问到的作业提交流程在这里都能实际观察到第二它能让你在 Log 里看到真实的 Container 调度记录后面排错的经验可以直接迁移到完全分布式集群上。多数搜索引擎里「hadoop 伪分布式搭建」相关的高频问题本质都是这一套环境没调通。2.2 从零开始安装 Hadoop 3.x解压、环境变量与四个核心配置以 Ubuntu JDK 8 Hadoop 3.x 为例JDK 安装这里不展开但注意 Hadoop 3 要求 JDK 8 或 11装 JDK 17 跑老版本会有兼容问题。解压后先做环境变量然后改四个 XML 配置。# 解压 Hadoop 到 /opt版本号按你的实际包名调整 tar -xzf hadoop-3.x.x.tar.gz -C /opt # 配置环境变量写入 ~/.bashrc export HADOOP_HOME/opt/hadoop-3.x.x export PATH$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 # 让配置立即生效 source ~/.bashrc环境变量是后续所有命令能跑起来的前提。JAVA_HOME必须指向 JDK 的实际安装路径不要直接写$(readlink -f /usr/bin/java)这类动态值因为 Hadoop 的启动脚本在 sudo 场景下经常读不到 shell 里继承的变量。HADOOP_HOME要写到解压目录sbin里才是start-dfs.sh和start-yarn.sh很多新人在 bin 目录里找不到启动脚本就是这个原因。接下来改四个配置。core-site.xml 指定默认文件系统hdfs-site.xml 指定副本数mapred-site.xml 指定跑在 YARN 上yarn-site.xml 配置调度与容器。!-- core-site.xml -- configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property /configurationfs.defaultFS 是全局入口所有 HDFS 命令和作业都会打到 localhost:9000。伪分布式的 NameNode 默认监听这个端口如果改成别的路径后面hdfs dfs -ls /会一直报连接拒绝。!-- hdfs-site.xml -- configuration property namedfs.replication/name value1/value /property /configuration伪分布式只有一台机器副本数必须设为 1否则 DataNode 只有一份副本但 NameNode 等两份上传文件会一直处于 under-replicated 状态。这个参数在完全分布式集群里按节点数调整一般默认 3。!-- mapred-site.xml -- configuration property namemapreduce.framework.name/name valueyarn/value /property /configuration这行决定 MapReduce 作业是跑在本地还是交给 YARN。漏掉它的话作业会退回本地模式日志里看不到 Container 调度结果虽然也能出但你已经脱离了分布式流程。!-- yarn-site.xml只贴关键两个属性 -- configuration property nameyarn.nodemanager.aux-service/name valuemapreduce_shuffle/value /property property nameyarn.application.classpath/name value/opt/hadoop-3.x.x/etc/hadoop:/opt/hadoop-3.x.x/share/hadoop/common/*:/opt/hadoop-3.x.x/share/hadoop/common/lib/*:/opt/hadoop-3.x.x/share/hadoop/hdfs/*:/opt/hadoop-3.x.x/share/hadoop/hdfs/lib/*:/opt/hadoop-3.x.x/share/hadoop/mapreduce/*:/opt/hadoop-3.x.x/share/hadoop/mapreduce/lib/*:/opt/hadoop-3.x.x/share/hadoop/yarn/*:/opt/hadoop-3.x.x/share/hadoop/yarn/lib/*/value /property /configurationaux-service 必须配成mapreduce_shuffle这是 Map Task 和 Reduce Task 之间传输中间数据的通道漏配会导致 Reduce 阶段一直等数据、作业卡住直到超时。yarn.application.classpath 是 Hadoop 3.x 特有的坑2.x 时代 YARN 会自己拼 classpath3.x 必须显式声明否则 ApplicationMaster 启动时报 ClassNotFound后面避坑章节会详细讲。2.3 启动前必查JAVA_HOME、SSH 与目录权限配置写完后不要急着start-dfs.sh先做三个检查能省掉后面大半排错时间。这三个问题是我在「hadoop 连接 xhell 不成功」「虚拟机安装 hadoop 步骤」这类高频搜索里看到最多的同类症状只是很多人没意识到根因在前面。# 检查 SSH 免密是否可用伪分布式也要求本机互信 ssh localhost echo ssh ok # 如果报 Permission denied执行下面两条后重试 ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keysHadoop 的 DataNode 和 NodeManager 通过 SSH 启动即使在同一台机器上也要走一遍ssh localhost。很多教程把这一步藏在「伪分布式搭建」最后但实际执行时 80% 的启动失败都栽在这。chmod 600是 SSH 的硬性要求权限太宽松会直接拒绝连接。# 检查 Java 版本与 Hadoop 自带脚本是否一致 java -version # 检查 HADOOP_HOME 是否在 sudo 环境下也可见 echo $HADOOP_HOME如果java -version显示的是 OpenJDK 11 或更高而你的 Hadoop 是 3.1 系列建议直接换成 JDK 8避免后面遇到 Socket 超时这类玄学问题。$HADOOP_HOME 为空的话检查是不是写在 /etc/profile 但这个 shell 没重新登录。检查通过后格式化 NameNode 再启动。注意格式化一定要在首次启动前做且只能做一次重复格式化会让集群里的 DataNode 命名空间 ID 对不上启动时 DataNode 会一直报Incompatible namespaceIDs最后只能清空 data 目录重来。hdfs namenode -format start-dfs.sh start-yarn.sh jpsjps输出里应该看到 NameNode、DataNode、ResourceManager、NodeManager 四个进程。少任何一个先回去看对应日志NameNode 看$HADOOP_HOME/logs/hadoop-hadoop-namenode-*.logDataNode 看hadoop-hadoop-datanode-*.log。这一步通过了环境才算真正立住。3. 词频统计的 MapReduce 实现原理、Python 代码与三处参数陷阱3.1 MapReduce 处理词频的原理为什么拆成 Map 和 Reduce词频统计是 MapReduce 的 Hello World但很多人跑通了却说不清数据流。一次完整的词频作业分四个阶段Map 阶段把每行文本拆成「单词 → 1」的键值对Shuffle 阶段由框架按照 key 排序并分组把相同单词的键值对聚到同一个 Reduce 输入里Reduce 阶段汇总每个单词的计数最后写到 HDFS。Shuffle 是整个机制的核心也是最容易被当成黑匣子的部分。Map 输出的键值对会先写到本地磁盘框架按 key 进行分区和排序再通过网络拉取到 Reduce 端。对于词频统计来说同一个单词的所有计数一定落在同一个 Reducer 上这是结果正确性的前提。面试里常问的「Map 端 Combiner 有什么用」就藏在这里可以在 Map 端先做一次局部汇总减少网络传输量代价是 Combiner 必须满足结合律加法是典型可用的操作。3.2 mapper.py 与 reducer.py完整可跑的 Python 实现Hadoop Streaming 允许用任意可执行脚本实现 Map 和 Reduce。词频统计的 mapper 和 reducer 加起来不到四十行但每一行都值得看清楚。#!/usr/bin/env python3 # mapper.py - 读取标准输入逐行拆词并输出计数 import sys for line in sys.stdin: # 去掉首尾空白统一转小写避免 The 和 the 被当成两个词 line line.strip().lower() # 按任意空白字符切分连续空格不会产生空词 for word in line.split(): # 输出格式必须是 单词\t1中间是制表符 print(f{word}\t1)这段代码的逻辑是逐行处理标准输入strip()去掉换行和首尾空格lower()做大小写归一。split()默认按空白字符切分所以hello world和hello world切出来的词一致不用手动处理多个空格。最后输出的print(f{word}\t1)里必须用制表符\t而不是空格Hadoop Streaming 默认把行内第一个\t左边当 key、右边当 value用空格分隔会在 Reduce 端拿到解析不了的输入。#!/usr/bin/env python3 # reducer.py - 读取排好序的键值对按单词累计计数 import sys current_word None current_count 0 word None for line in sys.stdin: line line.strip() # 按制表符拆出单词和计数只拆第一个 tab word, count line.split(\t, 1) try: count int(count) except ValueError: # 单条脏数据不影响整体作业跳过即可 continue # 因为相同单词已经被 Shuffle 排到一起所以只需判断当前单词是否变化 if current_word word: current_count count else: if current_word is not None: print(f{current_word}\t{current_count}) current_word word current_count count # 处理最后一个单词 if current_word is not None: print(f{current_word}\t{current_count})reducer 的核心假设是「相同 key 相邻」。Hadoop Streaming 在执行 reducer 之前已经完成了排序和分组所以这里不需要用字典维护全局计数只需要记住上一个单词遇到变化时把累积结果输出。line.split(\t, 1)里的1表示只切第一刀防止单词本身含制表符导致解包报错。try-except包裹int(count)是防御性写法Map 端如果有脏数据单个键值对转换失败不会拖垮整个作业。3.3 Hadoop Streaming 的关键参数与编码陷阱脚本逻辑没错不代表作业能跑对。Streaming 作业最容易被忽视的三个参数是-files、-mapper和-reducer的写法。-files会把本地脚本分发到所有 NodeManager 上否则 Task 在执行时找不到脚本-mapper里建议写完整的解释器命令python3 mapper.py不要只写mapper.py因为脚本的可执行位在分发到容器里后可能丢失。字符编码是另一个高频坑。Windows 上编辑的脚本默认是 GBK 或带 BOM 的 UTF-8Hadoop 容器里默认按 UTF-8 解析文本。BOM 会导致第一个单词前面多一个不可见字符\ufeff你会在结果里看到一个类似\ufeffthe的诡异 key。处理办法很简单所有脚本和文本文件统一用 UTF-8 无 BOM 保存如果已经踩了坑用sed -i 1s/^\xef\xbb\xbf// mapper.py去掉开头 BOM。4. 数据准备与 HDFS 操作文件怎么进、多大才值得用 Hadoop4.1 测试数据准备格式、编码与大小词频统计的输入可以是任意文本文件。课程设计里常见的选择是英文小说、新闻语料或者爬虫抓取的网页正文。我这边建议准备一份几 MB 到几十 MB 的纯文本 UTF-8 文件原因后面会讲。先看数据怎么造# 生成一份带重复词的测试文本大约几万行 for i in $(seq 1 5000); do echo hadoop mapreduce spark flink hadoop hdfs yarn mapreduce done words.txt # 检查行数和文件大小 wc -l words.txt du -h words.txt用循环生成的好处是你能提前知道正确答案hadoop出现 10000 次、mapreduce出现 10000 次、其他词 5000 次。等作业跑完直接对比输出文件里的统计值就能确认计算是否正确这是验证阶段的关键。别一开始就拿几百 GB 的语料跑伪分布式单节点没有性能优势跑一次半小时起步调起错来心态会崩。4.2 HDFS 常用命令上传、查看、删除的完整姿势环境启动后第一步是在 HDFS 上建目录、传数据。HDFS 的目录要显式创建不像本地文件系统会自动建父目录。# 创建输入目录-p 表示父目录不存在时自动创建 hdfs dfs -mkdir -p /input # 上传本地文件到 HDFS hdfs dfs -put words.txt /input/ # 查看上传结果确认文件大小和数据块分布 hdfs dfs -ls -h /input # 查看文件块信息确认副本数生效 hdfs fsck /input/words.txt -files -blocks-put是上传命令等价写法是-copyFromLocal。上传完成后-ls -h能直观看到文件大小fsck用来检查文件在 HDFS 上的物理分布能看到 block id 和副本状态。如果fsck输出显示副本数不是 1说明 hdfs-site.xml 的dfs.replication没生效回去检查配置后需要重新格式化。HDFS 上删除文件和本地不一样默认不进回收站删了就真没了。作业输出目录存在是最常见的失败原因所以每次重跑前先删掉旧的输出目录# 删除输出目录-r 递归删除 hdfs dfs -rm -r /output/wc # 确认删除成功 hdfs dfs -ls /output 21 || echo output 目录已清理-rm -r在 HDFS 里是永久删除没有-trash开启的话不存在后悔药。我这边的习惯是每次提交作业前在同一个脚本里先删/output/wc再提交宁可多删一次也不要作业提交到一半报目录已存在。如果你的课程环境里 HDFS 有 Trash 机制hdfs dfs -rm -r会先丢进/user/name/.Trash但伪分布式默认不开按上面这个习惯走最稳。4.3 数据量评估多大文件才值得上 Hadoop这是很多人没想清楚的问题几十 KB 的文本文件用cat加sort一分钟出结果为什么非要上 Hadoop答案是数据规模决定工具选型。HDFS 默认块大小是 128 MB一个不足 128 MB 的文件在 HDFS 上仍然占用一个块的元数据开销伪分布式的单 DataNode 也没法并行读多个块。我一般用这个标准判断文件小于 100 MB直接在本地用 shell 管道做词频统计更快没必要走分布式文件超过几百 MB 到 GB 级别或者你要演示的是「多节点并行」这个场景才值得把数据放到 HDFS 上用 MapReduce 跑。课程设计的报告里如果能写出这段话说明你不是在照抄命令而是理解了分布式计算的基本盘。这份资源配套的测试数据控制在几十 MB就是为了让你在成本可控的前提下把整个链路跑通而不是在等作业上浪费时间。5. 作业提交与避坑排查一次完整词频统计的现场记录5.1 Streaming 提交命令与参数拆解脚本写好、数据上传完接下来是提交作业。下面这条命令是把整个作业交到 YARN 上的标准姿势hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \ -D mapreduce.job.reduces1 \ -D mapreduce.job.namewordcount-demo \ -files mapper.py,reducer.py \ -input /input/words.txt \ -output /output/wc \ -mapper python3 mapper.py \ -reducer python3 reducer.py参数逐项拆开解释-D mapreduce.job.reduces1指定只用 1 个 Reducer词频统计场景下结果要汇总Reduce 并行度大于 1 会出现多个输出文件每个文件是部分单词的计数后续合并反而麻烦-files mapper.py,reducer.py把两个脚本分发到所有容器的当前工作目录脚本里的相对路径才能被找到-input和-output都是 HDFS 路径输出目录必须不存在这是硬性约束-mapper和-reducer后面的字符串会在容器里通过 shell 执行所以python3 mapper.py这种带参数的命令也支持。提交后终端会先显示 Job ID然后进入轮询状态每隔几十秒打印一次进度Info: mapreduce.job: map 0% reduce 0% Info: mapreduce.job: map 100% reduce 0% Info: mapreduce.job: map 100% reduce 100%看到map 100% reduce 100%之后再等一两分钟通常会出现Job completed successfully的日志。这里有个容易误判的点map 到 100% 不代表作业结束reduce 可能还在拉取数据。别看到 map 满就急着 CtrlC按下 CtrlC 作业会被 kill日志里只会留下Job killed。5.2 结果验证与日志定位作业成功不代表结果正确。先看 HDFS 上生成了什么# 列出输出目录part-00000 是 Reducer 写出的结果文件 hdfs dfs -ls /output/wc # 查看内容前 20 行 hdfs dfs -cat /output/wc/part-00000 | head -20 # 统计结果行数并和输入词数对账 hdfs dfs -cat /output/wc/part-00000 | wc -l预期输出应该是hadoop 10000这种「单词加制表符加计数」的格式。_SUCCESS文件存在只代表作业执行成功不代表业务结果正确这是 Hadoop 新手最容易忽略的点。验证的具体动作是拿结果里每个词的计数和输入数据里实际出现的次数做对比一致才算通过。如果作业失败或者结果不对日志是唯一的现场。Streaming 作业的日志分三段看第一段是任务运行时的 stdout/stderr用 ResourceManager 的 Web 界面按 Job ID 查第二段是 Container 日志在 NodeManager 节点上的/opt/hadoop-3.x.x/logs/userlogs目录第三段是 reducer 的报错通常在这段里能看到 Python 的 traceback。查日志的顺序别反先看 userlogs 再看 stderr最后才去看系统日志。5.3 高频问题排查四个真实翻车现场第一条提交作业立即失败报FileAlreadyExistsException。现象是作业还没开始跑日志第一行就是输出路径已存在。原因是 HDFS 的输出目录在之前某次运行中已经生成而且 HDFS 不会自动覆盖旧输出。解决方法是重跑前先执行hdfs dfs -rm -r /output/wc或者把每次的输出路径改成带时间戳的新目录比如/output/wc_$(date %s)我更推荐前者简单且可预期。第二条ApplicationMaster 反复启动失败日志里出现ClassNotFoundException: org.apache.hadoop.mapreduce.v2.app.MRAppMaster。整个作业卡在ACCEPTED状态然后 Container 反复重启直到超时。原因是 Hadoop 3.x 的 YARN 不知道去哪里加载 Hadoop 自身依赖也就是 2.2 节说的yarn.application.classpath没配。解决方法是把 2.2 里那一长串 classpath 补进 yarn-site.xml然后stop-yarn.sh和start-yarn.sh重启。这个坑的隐蔽性在于它会准时出现在你「终于跑起来」的喜悦之后属于必踩项目。第三条作业跑完了但输出文件里有一行类似\ufeffhadoop 123的诡异 key正常单词也对得上但多了一个带乱码前缀的数据。原因是文件是从 Windows 上传的 UTF-8 with BOM 格式Hadoop 逐行读取时把 BOM 留在了第一个单词前面。解决方法是所有文本和脚本统一用 UTF-8 无 BOM 重新保存已经出现的脏数据用sed -i 1s/^\xef\xbb\xbf// words.txt或dos2unix处理。第四条Reducer 完全没有输出日志里 Task 退出码是 1stdout 为空。原因多半是 mapper.py 和 reducer.py 之间有格式断层比如 mapper 里用空格print(f{word} 1)输出而 reducer 里用split(\t)解析两边对不上Reducer 拿到整行当单词去转 int 就抛异常。解决方法是严格约定 mapper 输出key\tvaluereducer 解析\t并先在本地管道验证一次再交到集群具体做法见下一章。6. 调试技巧本地管道先行与通用词频计数器的改造6.1 本地管道验证提交前先把逻辑跑通Streaming 作业的容器环境排查成本远高于本地所以我的习惯是任何脚本上集群之前先用本地管道模拟一遍 MapReduce 的完整数据流cat words.txt | python3 mapper.py | sort -t $\t -k1,1 | python3 reducer.py | head -20这条命令里cat words.txt模拟 HDFS 文件输入mapper.py自己跑一遍输出键值对管道中间的sort模拟 Hadoop 的 Shuffle 排序reducer.py负责汇总。只有管道输出的结果和预期一致才把脚本交到集群上。实际执行时先看这个管道的输出如果这里都是乱序或者数字对不上那问题一定出在脚本本身而不是 Hadoop 环境。本地管道的价值在于把「环境问题」和「代码问题」切分开。管道通了但集群挂了那是配置问题管道就挂了连集群都不需要启动。从那以后我每次写 Streaming 作业都强制先跑这四段管道再考虑提交的事这套习惯帮我省掉了大量排队等集群的时间。6.2 从词频到通用计数器把脚本改造成可复用工具词频统计本身不难但它的框架可以扩展成通用工具。比如统计结果里把所有低频噪音词过滤掉或者按出现次数倒序输出。下面是在 reducer 输出前加一个最低频次过滤的改造思路# 先看结果格式单词\t次数 hdfs dfs -cat /output/wc/part-00000 | awk -F \t $2 100 {print} | sort -t $\t -k2,2 -nr | head -20awk按制表符拆列$2是次数过滤掉小于 100 的词后面的sort -t $\t -k2,2 -nr按次数倒序排序。这一步不用改 Java 或 Python在结果文件上直接操作即可适合课程设计答辩时快速展示「数据洞察」。希望这个完整链路能帮到你。本文还有配套的精品资源点击获取