简介:这份资源是面向高校计算机相关专业学生与大数据入门学习者的课程设计文档,围绕基于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/24,master 用 192.168.128.130,slave1 到 slave3 依次往后排。VMware 的 NAT 设置里,子网 IP 填 192.168.128.0,网关填 192.168.128.2,DHCP 起止地址设为 192.168.128.128 到 192.168.128.254。这样做的目的是让虚拟机既能访问外网装软件,又能通过固定 IP 互相 SSH。
在每台机器上编辑网卡配置文件:
vi /etc/sysconfig/network-scripts/ifcfg-eth0需要改动的关键项如下:
BOOTPROTO=static IPADDR=192.168.128.130 NETMASK=255.255.255.0 GATEWAY=192.168.128.2 ONBOOT=yes改完后执行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 是 LongWritable,value 是 Text。
3.2 计算每门课程的最高分、最低分、平均分
这个需求的思路是:Mapper 把课程名作为输出 key,分数作为输出 value;Reducer 收到某门课的所有分数后,遍历一次算出 max、min、avg。代码结构如下:
public class maxminaverage134 { public static class averageMapper extends Mapper<LongWritable, 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 Reducer<Text, Text, Text, Text> { @Override protected void reduce(Text key, Iterable<Text> 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,把平均分作为排序字段。这里给一个简化版的自定义 key:
public class ScoreWritable implements WritableComparable<ScoreWritable> { 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 为 1;Reducer 累加得到每个分数的出现次数。如果还要统计人数,需要把学号也带上,用 Set 去重。
public static class SameScoreMapper extends Mapper<LongWritable, 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 Reducer<Text, Text, Text, Text> { @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { Set<String> 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 上有没有 ResourceManager,slave 上有没有 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设成local,fs.defaultFS设成file:///,这样 MapReduce 会在本地文件系统上模拟执行,断点也能直接打。等逻辑确认无误再打包上集群。配置方式是在代码里加:
conf.set("mapreduce.framework.name", "local"); conf.set("fs.defaultFS", "file:///");注意本地模式下输入输出路径要改成 Windows 本地路径,比如D:/score/input,不能用 HDFS 路径。这个模式只适合验证逻辑,不能用来测分布式性能。
还有一个习惯:每次提交任务前,先用hdfs dfs -cat看一眼输入数据的前几行,确认分隔符和字段顺序没变。有次我直接拿上一轮的数据跑,结果课程名那一列被调换了位置,跑出来的平均分全串了,排查了半天才发现是数据源的问题。从那以后我每次跑 MapReduce 之前都强制走一遍“看数据、对字段、清输出目录”这三步,省下来的时间远比这三步多。希望帮到你。
本文还有配套的精品资源,点击获取