ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Hadoop四节点集群实战:成绩分析系统与MapReduce避坑指南

Hadoop四节点集群实战:成绩分析系统与MapReduce避坑指南 简介这份资源是面向高校计算机相关专业学生与大数据入门学习者的课程设计文档围绕基于Hadoop的成绩分析系统展开帮助读者理解如何用分布式计算解决学生成绩数据量大、管理效率低的问题。压缩包内共1个docx文件约1.46MB内容为完整的课程设计报告涵盖项目背景、需求分析、开发工具、集群搭建、编码实现、调试测试与总结等章节。文档详细记录了VMware与CentOS 6.8环境准备、Hadoop完全分布式集群安装配置以及用MapReduce统计每门课程平均分、最高分、最低分按平均分降序输出并分析相同分数出现次数与人数等具体实现过程。目前已有1500人学习适合需要完成大数据课程设计、掌握HDFS与MapReduce基础应用、参考集群搭建与成绩分析编码思路的读者可将其作为项目报告模板与实验流程参考。1. 从一份成绩单到四节点集群这套 Hadoop 成绩分析系统到底能跑出什么带过课程设计的人大概都有体会学生成绩表本身不复杂无非学号、姓名、课程、分数几列可一旦要求“按课程分组算最高最低平均”“把平均分从高到低排”“统计同分人数”用 Excel 拉透视表也能做但数据量一上来、课程一多、还要演示分布式计算过程单机脚本就有点撑不住场面了。这套基于 Hadoop 的成绩分析系统核心就是用 HDFS 存成绩数据、用 MapReduce 做分组聚合把“每门课的最高分、最低分、平均分”“课程平均分排序”“同分次数与人数”这三类统计跑在四节点完全分布式集群上。它适合正在做大数据课程设计、需要一套能讲清 HDFS 与 MapReduce 协作流程的从业者或学生也适合想拿一个真实可复现的小集群练手的人。整份资源围绕 Hadoop 2.6.4、CentOS 6.8、JDK 1.7、Eclipse 与 Xshell 展开从虚拟机安装一路写到编码调试是一条完整的落地链路。2. 集群搭建四台 CentOS 6.8 虚拟机的网络、SSH 与时间同步2.1 为什么选完全分布式而不是伪分布式伪分布式把 NameNode、DataNode、ResourceManager 全塞在一台机器上跑通没问题但课程设计里如果只交伪分布式答辩时很容易被追问“分布式体现在哪”。完全分布式至少需要一台 master 加若干 slave资源里给的是 master、slave1、slave2、slave3 四节点。选这个规模的理由很实际三副本机制下DataNode 数量少于 3 时副本放置策略退化成单机或双机看不出 HDFS 的容错效果四节点刚好能让每个 DataNode 都参与存储同时 master 只做 NameNode 和 ResourceManager负载分离清晰。CentOS 6.8 是这套资源锁定的系统版本搭配 Hadoop 2.6.4 和 JDK 1.7属于比较老但稳定的组合。常见做法是先在 VMware Workstation 16 Pro 里装好 master配置完 Hadoop 后再克隆出三个 slave这样能省掉重复装系统和配基础环境的时间。克隆前记得把 master 关机否则克隆出来的虚拟机网卡 MAC 会冲突。2.2 固定 IP 与 NAT 网络配置四台机器要互相通信IP 必须固定。资源里把网段定在 192.168.128.0/24master 用 192.168.128.130slave1 到 slave3 依次往后排。VMware 的 NAT 设置里子网 IP 填 192.168.128.0网关填 192.168.128.2DHCP 起止地址设为 192.168.128.128 到 192.168.128.254。这样做的目的是让虚拟机既能访问外网装软件又能通过固定 IP 互相 SSH。在每台机器上编辑网卡配置文件vi /etc/sysconfig/network-scripts/ifcfg-eth0需要改动的关键项如下BOOTPROTOstatic IPADDR192.168.128.130 NETMASK255.255.255.0 GATEWAY192.168.128.2 ONBOOTyes改完后执行service network restart重启网络服务再用ifconfig确认 IP 是否生效。这里有个容易翻车的点克隆出来的 slave 网卡 MAC 地址会变如果 ifcfg-eth0 里还写着旧的 HWADDR网络起不来。解决办法是把 HWADDR 那一行删掉或者用uuidgen重新生成 UUID 后写回配置文件。2.3 SSH 免密登录与 hosts 映射Hadoop 集群启动时master 需要能免密 SSH 到所有 slave否则 start-dfs.sh 会反复提示输密码。先在 master 上生成密钥对ssh-keygen -t rsa连按三次回车默认在/root/.ssh/下生成 id_rsa 和 id_rsa.pub。然后把公钥复制到四台机器ssh-copy-id -i /root/.ssh/id_rsa.pub master ssh-copy-id -i /root/.ssh/id_rsa.pub slave1 ssh-copy-id -i /root/.ssh/id_rsa.pub slave2 ssh-copy-id -i /root/.ssh/id_rsa.pub slave3执行时会提示输入 yes 和 root 密码输完即可。接着编辑/etc/hosts把四台机器的 IP 和主机名映射写进去192.168.128.130 master 192.168.128.131 slave1 192.168.128.132 slave2 192.168.128.133 slave3验证方式是直接ssh slave1如果不需要密码就能登录说明配置成功。这一步的坑在于如果之前用密码登录过known_hosts 里可能残留旧指纹导致 SSH 拒绝连接。删掉/root/.ssh/known_hosts再重试即可。2.4 NTP 时间同步与防火墙关闭Hadoop 集群对时间敏感各节点时间差太大会导致心跳超时、任务失败。资源里的做法是在 master 上装 NTP 服务端slave 作为客户端同步。master 的/etc/ntp.conf里注释掉所有 server 开头的行加上restrict 192.168.0.0 mask 255.255.255.0 nomodify notrap server 127.127.1.0 fudge 127.127.1.0 stratum 10slave 上的配置更简单注释掉 server 行后加一行server master。然后所有节点关闭防火墙service iptables stop chkconfig iptables offmaster 上启动 NTP 服务并设为开机自启service ntpd start chkconfig ntpd onslave 上先执行ntpdate master同步一次时间再启动 ntpd 并设为开机自启。这里有个血泪经验如果 slave 的 ntpd 先于 ntpdate 启动时间同步可能不生效顺序不能反。3. MapReduce 编码实现三类成绩统计的 Mapper 与 Reducer 设计3.1 初始数据格式与输入路径资源里的初始数据是 kechengscore.txt 和 scoreinput.txt格式是每行一条记录包含学号、姓名、课程、分数。上传到 HDFS 时用hdfs dfs -mkdir -p /score/input hdfs dfs -put kechengscore.txt /score/input/ hdfs dfs -put scoreinput.txt /score/input/MapReduce 默认按行读取TextInputFormat 会把每行的偏移量作为 key、行内容作为 value 传给 Mapper。所以 Mapper 的输入 key 是 LongWritablevalue 是 Text。3.2 计算每门课程的最高分、最低分、平均分这个需求的思路是Mapper 把课程名作为输出 key分数作为输出 valueReducer 收到某门课的所有分数后遍历一次算出 max、min、avg。代码结构如下public class maxminaverage134 { public static class averageMapper extends MapperLongWritable, Text, Text, Text { Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { // 按逗号或制表符切分具体分隔符看数据文件 String[] fields value.toString().split(,); // fields[2] 是课程名fields[3] 是分数 String course fields[2].trim(); String score fields[3].trim(); context.write(new Text(course), new Text(score)); } } public static class averageReducer extends ReducerText, Text, Text, Text { Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { double sum 0; int count 0; double max Double.MIN_VALUE; double min Double.MAX_VALUE; for (Text val : values) { double score Double.parseDouble(val.toString()); sum score; count; if (score max) max score; if (score min) min score; } double avg sum / count; String result max max , min min , avg String.format(%.2f, avg); context.write(key, new Text(result)); } } public static void main(String[] args) throws Exception { if (args.length 2) { System.out.printf(Usage: input output\n); return; } Configuration conf new Configuration(); Job job Job.getInstance(conf, course maxminaverage); job.setJarByClass(maxminaverage134.class); job.setMapperClass(averageMapper.class); job.setReducerClass(averageReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } }逻辑说明Mapper 阶段不做聚合只做“打标签”把课程名提出来当 key这样 Shuffle 阶段会自动把同一门课的所有分数送到同一个 Reducer。Reducer 里用一次遍历同时算 max、min、sum、count避免多次遍历迭代器。参数方面job.setOutputKeyClass和setOutputValueClass指定的是 Reducer 输出类型如果 Mapper 输出类型和 Reducer 不一致还需要单独设setMapOutputKeyClass和setMapOutputValueClass。这里两者一致所以省略了。运行命令hadoop jar maxminaverage134.jar demo.maxminaverage134 /score/input /score/output1输出目录不能预先存在否则 MapReduce 会直接报错退出。这是新手最容易踩的坑之一。3.3 课程平均分从高到低排序MapReduce 本身不保证全局有序只保证同一个 Reducer 内 key 有序。要实现“按平均分从高到低输出”常见做法是分两个 Job第一个 Job 算出每门课的平均分第二个 Job 把平均分作为 key 进行排序。但这样写两个 Job 比较繁琐另一种做法是在第一个 Job 的 Reducer 里把结果写入 HDFS 后再用一个单独的排序 Job 读取。资源里的做法更直接在 Reducer 输出时把平均分拼在 value 里后续用hdfs dfs -cat配合sort命令做本地排序。如果一定要在 MapReduce 里完成可以自定义 WritableComparable把平均分作为排序字段。这里给一个简化版的自定义 keypublic class ScoreWritable implements WritableComparableScoreWritable { private String course; private double avg; Override public void write(DataOutput out) throws IOException { out.writeUTF(course); out.writeDouble(avg); } Override public void readFields(DataInput in) throws IOException { course in.readUTF(); avg in.readDouble(); } Override public int compareTo(ScoreWritable o) { // 按平均分降序 return Double.compare(o.avg, this.avg); } }参数说明compareTo里用o.avg减this.avg实现降序如果写成this.avg - o.avg就是升序。自定义 Writable 必须实现无参构造函数否则 Hadoop 反射创建实例时会报错。这个点很多人第一次写都会漏。3.4 统计同分次数与同分人数第三个需求是“每门课中出现了相同分数的分数、出现次数、以及该分数的人数”。这个本质上是一个二次聚合先按“课程分数”分组计数再按课程分组把同分记录汇总。Mapper 输出 key 为“课程分数”的组合value 为 1Reducer 累加得到每个分数的出现次数。如果还要统计人数需要把学号也带上用 Set 去重。public static class SameScoreMapper extends MapperLongWritable, Text, Text, Text { Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] fields value.toString().split(,); String course fields[2].trim(); String score fields[3].trim(); String studentId fields[0].trim(); // 组合 key课程 分数 context.write(new Text(course _ score), new Text(studentId)); } } public static class SameScoreReducer extends ReducerText, Text, Text, Text { Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { SetString students new HashSet(); int count 0; for (Text val : values) { students.add(val.toString()); count; } String result 出现次数 count , 人数 students.size(); context.write(key, new Text(result)); } }逻辑说明用 Set 去重是因为同一个学生同一门课可能有多条记录比如平时成绩和期末成绩分开录如果直接 count 会把重复记录也算进去。参数方面如果数据里一个学生一门课只有一条记录Set 和 count 结果一样但加上去重逻辑更稳妥。4. 避坑与排查集群起不来、任务跑不动时先看这几处4.1 NameNode 格式化后多次执行导致集群 ID 不一致现象执行hdfs namenode -format后启动集群DataNode 起不来日志里报“Cluster ID 不一致”。原因是每次 format 都会生成新的 clusterID而 DataNode 的 VERSION 文件里还存着旧的。解决办法是删掉所有节点上dfs/data和dfs/name目录重新格式化一次然后重新启动。注意格式化只能执行一次除非你确定要清空所有数据。4.2 SSH 免密登录配了但 start-dfs.sh 仍提示输密码现象手动ssh slave1不需要密码但sbin/start-dfs.sh执行时还是反复提示输入密码。原因是脚本里可能用了不同的用户或不同的密钥路径。检查/etc/hosts里主机名映射是否正确确认ssh-copy-id复制到了目标机器的/root/.ssh/authorized_keys并且该文件的权限是 600、.ssh目录权限是 700。权限不对 SSH 会直接忽略公钥。4.3 MapReduce 任务卡在 map 0% reduce 0%现象任务提交后一直卡在 0%既不报错也不推进。常见原因是 YARN 的 ResourceManager 没起来或者 NodeManager 与 ResourceManager 的通信端口被防火墙拦了。先jps看 master 上有没有 ResourceManagerslave 上有没有 NodeManager。如果进程都在检查yarn-site.xml里yarn.resourcemanager.hostname是否写成了 master以及各节点时间是否同步。时间差超过默认阈值会导致心跳被拒。4.4 输出目录已存在导致 Job 直接失败现象第二次运行同一个 MapReduce 任务时报“Output directory already exists”。这是 Hadoop 的保护机制防止误覆盖结果。解决办法是每次运行前删掉输出目录hdfs dfs -rm -r /score/output1或者在代码里加一行判断如果输出路径存在就自动删除。但生产环境不建议自动删手动确认更安全。4.5 中文乱码或分隔符不匹配导致数组越界现象Mapper 里split(,)后取fields[3]报 ArrayIndexOutOfBoundsException。原因是数据文件里有的行用制表符分隔有的用逗号或者末尾有空行。解决办法是先用hdfs dfs -cat /score/input/kechengscore.txt | head -20看实际分隔符然后在代码里用正则split([,\\t])兼容多种分隔符并对 fields 长度做判断。空行可以在 Mapper 里直接if (value.toString().trim().isEmpty()) return;跳过。5. 进阶技巧用 Counter 做数据质量校验与本地模式快速调试集群跑通之后真正让人头疼的往往不是“能不能跑”而是“跑出来的数对不对”。我一般会在 Mapper 里加几个 Counter用来统计总行数、空行数、分数解析失败数。这样任务跑完在控制台就能看到数据质量概况不用去翻日志。public enum ScoreCounter { TOTAL_ROWS, EMPTY_ROWS, PARSE_ERROR } // 在 map 方法开头 context.getCounter(ScoreCounter.TOTAL_ROWS).increment(1); if (value.toString().trim().isEmpty()) { context.getCounter(ScoreCounter.EMPTY_ROWS).increment(1); return; } try { Double.parseDouble(fields[3].trim()); } catch (NumberFormatException e) { context.getCounter(ScoreCounter.PARSE_ERROR).increment(1); return; }Counter 的好处是不影响主流程跑完在Job.waitForCompletion的输出里能看到每个计数器的值。如果 PARSE_ERROR 大于 0说明数据里有非数字的分数需要回去检查源文件。另一个实用技巧是本地模式调试。每次改完代码都打包上传到集群跑一轮下来少说几分钟。可以在 Eclipse 里把mapreduce.framework.name设成localfs.defaultFS设成file:///这样 MapReduce 会在本地文件系统上模拟执行断点也能直接打。等逻辑确认无误再打包上集群。配置方式是在代码里加conf.set(mapreduce.framework.name, local); conf.set(fs.defaultFS, file:///);注意本地模式下输入输出路径要改成 Windows 本地路径比如D:/score/input不能用 HDFS 路径。这个模式只适合验证逻辑不能用来测分布式性能。还有一个习惯每次提交任务前先用hdfs dfs -cat看一眼输入数据的前几行确认分隔符和字段顺序没变。有次我直接拿上一轮的数据跑结果课程名那一列被调换了位置跑出来的平均分全串了排查了半天才发现是数据源的问题。从那以后我每次跑 MapReduce 之前都强制走一遍“看数据、对字段、清输出目录”这三步省下来的时间远比这三步多。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进