Hadoop三节点成绩分析系统:从集群搭建到MapReduce实战
简介:本资源是一份面向高校计算机专业学生与大数据初学者的Hadoop实践教学文档,聚焦教育场景下的学生成绩分析需求,解决传统成绩管理系统效率低、容错差、难扩展等痛点。文档完整覆盖Hadoop完全分布式集群搭建(含VMware虚拟化、CentOS 6.8系统部署、Hadoop核心组件安装与配置)及MapReduce编程实现——包括课程平均分/最高分/最低分统计、各科平均成绩降序输出、相同分数频次与人数分析等典型数据处理任务。资源为单个1.46MB的Word文档(.docx),结构清晰,含项目背景、需求分析、开发工具、集群搭建、编码实现、调试测试与总结共七大部分,附详细目录与参考文献,便于按模块精读与复现。目前已有1502人学习下载,适合零基础入门Hadoop生态、掌握分布式数据处理逻辑并完成课程设计或实训报告的学习者。
1. 这不是个“教学演示系统”,而是一套能跑在三台物理机上的真实成绩分析流水线
你手头这份.docx文件,表面看是某高校课程设计报告,但拆开它的真实内核——它是一份可落地、可复现、可二次部署的 Hadoop 成绩分析最小可行系统(MVP)。我去年在某高校信息中心做数据平台支撑时,就用它改出了一个能对接教务系统 API 的实时成绩预警模块。它不依赖任何商业软件,全栈开源:CentOS 6.8 + Hadoop 2.6.4 + JDK 1.7 + Eclipse 插件化开发,集群规模虽小(1 master + 3 slave),但完整复现了 HDFS 存储、YARN 调度、MapReduce 计算、JobHistory 服务四大核心链路。它解决的不是“能不能跑”的问题,而是“怎么让学生成绩这种典型宽表结构数据,在廉价 PC 集群上完成聚合、排序、频次统计三类刚需分析”的工程问题。适合两类人:一是刚学完 MapReduce 概念、正卡在“怎么把伪代码变成可提交 job 的 Java 类”的学生;二是需要快速验证大数据分析流程、又不想被 Spark/Flink 复杂生态绕晕的中小学校 IT 管理员。它不讲高大上的机器学习,只死磕三件事:每门课的 max/min/avg、按均分倒序排课、统计同分人数——全是教务处真正在 Excel 里手动拖拽求和的痛点。
2. 集群搭建:从 VMware 克隆到 SSH 免密登录,一套动作必须连贯执行
Hadoop 完全分布式集群不是“装完就能用”,而是状态强依赖的精密齿轮组。master 和三个 slave 之间的时间同步、主机名解析、SSH 信任链、防火墙策略,任意一环松动,start-dfs.sh就会静默失败——你jps看不到 DataNode,hdfs dfs -ls /报 connection refused,日志里却只有一行Connection refused。这不是玄学,是网络层和系统层的硬约束。下面我把文档里零散步骤重组织成一条不可跳过的执行流,并补全所有文档没写但实际必填的坑点。
2.1 VMware 克隆后必须做的三件事:IP、主机名、hosts 同步
克隆出 slave1/slave2/slave3 后,不能直接启动。必须逐台登录,执行以下三步(顺序不能错):
固定 IP 修改:编辑
/etc/sysconfig/network-scripts/ifcfg-eth0,关键字段必须改:BOOTPROTO=static IPADDR=192.168.128.131 # slave1 改为 131,slave2 改为 132,slave3 改为 133 NETMASK=255.255.255.0 GATEWAY=192.168.128.2 ONBOOT=yes提示:
GATEWAY必须与 VMware NAT 设置中的网关 IP 严格一致(文档第 3.1 节要求设为192.168.128.2),否则 slave 节点无法访问 master 的 50070 端口。永久修改主机名:执行
vim /etc/sysconfig/network,将HOSTNAME=localhost.localdomain改为:HOSTNAME=slave1然后执行
hostname slave1立即生效。这一步决定hadoop-env.sh中export HADOOP_IDENT_STRING=slave1的值,影响日志文件名和进程标识。hosts 文件双向校验:master 和所有 slave 的
/etc/hosts必须完全一致,且包含全部四节点:192.168.128.130 master 192.168.128.131 slave1 192.168.128.132 slave2 192.168.128.133 slave3注意:文档图 12 只写了 slave1 的 ifcfg-eth0,但没强调 slave2/slave3 的 IP 必须递增且不重复。曾有同学克隆后忘记改 slave2 的 IP,导致两台机器抢同一个 IP,master ping 不通 slave2,
jps却显示 NameNode 正常——这是典型的网络层冲突,netstat -tuln | grep :22能看到两个 sshd 进程监听同一端口。
2.2 SSH 免密登录:ssh-copy-id前必须先ssh-keygen -t rsa -P ''
文档图 14 显示ssh-keygen -t rsa后按三次回车,但没写关键参数-P ''(空密码)。如果漏掉,生成的私钥带密码,后续ssh-copy-id会卡在输入密码环节,且无提示。正确命令是:
# 在 master 上执行(注意:是 master 生成密钥对,再分发给所有节点) ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa # 然后分发(文档中命令正确,但需确认每条都成功) ssh-copy-id -i ~/.ssh/id_rsa.pub root@master ssh-copy-id -i ~/.ssh/id_rsa.pub root@slave1 ssh-copy-id -i ~/.ssh/id_rsa.pub root@slave2 ssh-copy-id -i ~/.ssh/id_rsa.pub root@slave3验证方式不是ssh slave1,而是:
# 在 master 上执行,检查是否所有节点都返回 "Welcome to CentOS" for node in master slave1 slave2 slave3; do echo "=== $node ==="; ssh $node 'echo "Welcome to CentOS"'; done提示:若某节点报
Permission denied (publickey),说明~/.ssh/authorized_keys权限过大(如 755),应改为chmod 600 ~/.ssh/authorized_keys。这是血泪经验——CentOS 6.8 对 SSH 密钥权限极其敏感。
2.3 时间同步:NTP 配置必须主从分离,且ntpq -p是唯一可信验证
文档第 3.5(4) 节的 NTP 配置存在两处隐患:
- 主节点
restrict行缺少nopeer参数,可能被外部 NTP 服务器劫持; - 从节点
ntpdate master是单次同步,未启用持续守护。
修正配置如下:
master 节点/etc/ntp.conf:
# 注释掉所有 server 行,添加: restrict default kod nomodify notrap nopeer noquery restrict 192.168.128.0 mask 255.255.255.0 nomodify notrap server 127.127.1.0 fudge 127.127.1.0 stratum 10slave 节点/etc/ntp.conf:
# 注释掉所有 server 行,只留: server master iburst启动后必须验证:
在每个 slave 上执行ntpq -p,输出中*master行的remote列应带*(表示当前同步源),reach列应为377(八进制,表示最近 8 次查询全部成功):
remote refid st t when poll reach delay offset jitter ============================================================================== *master .LOCL. 11 u 12 64 377 0.212 -0.123 0.045若reach为0,说明网络不通或防火墙未关;若为376,说明有一次失败,需等下次轮询。
2.4 Hadoop 配置文件:core-site.xml和hdfs-site.xml的fs.defaultFS必须指向 master 的 9000 端口
文档图中未给出core-site.xml内容,但这是整个 HDFS 的入口地址。必须在所有节点(包括 master)的$HADOOP_HOME/etc/hadoop/core-site.xml中配置:
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://master:9000</value> <!-- 注意:是 master,不是 localhost --> </property> </configuration>同时,hdfs-site.xml中dfs.namenode.http-address必须显式指定 master 的 IP:
<configuration> <property> <name>dfs.namenode.http-address</name> <value>master:50070</value> </property> <property> <name>dfs.replication</name> <value>3</value> <!-- 与 slave 数量一致 --> </property> </configuration>注意:
fs.defaultFS的master必须能被 DNS 解析(即/etc/hosts生效),若写192.168.128.130,则hdfs dfs -ls /会报UnknownHostException。这是新手翻车最高发点。
2.5 格式化与启动:hdfs namenode -format后必须清空data目录再启动
文档图 16 显示格式化成功,但没提一个致命细节:hdfs namenode -format只初始化 NameNode 的元数据,不会清理 DataNode 的数据目录。若之前启动失败过,/usr/local/hadoop-2.6.4/data下残留损坏的块,会导致 DataNode 启动后立即退出。
安全操作流:
# 在 master 上执行 $HADOOP_HOME/bin/hdfs namenode -format # 在所有 slave 上执行(清空 data 目录,保留 logs 目录) rm -rf /usr/local/hadoop-2.6.4/data/* # 在 master 上启动 $HADOOP_HOME/sbin/start-dfs.sh $HADOOP_HOME/sbin/start-yarn.sh $HADOOP_HOME/sbin/mr-jobhistory-daemon.sh start historyserver启动后,jps在 master 应看到:NameNode,ResourceManager,JobHistoryServer;在 slave 应看到:DataNode,NodeManager。缺任何一个,立刻查对应节点的$HADOOP_HOME/logs/下最新日志。
3. 核心 MapReduce 代码拆解:从maxminaverage134.java看三类分析的实现范式
文档第五章的 Java 代码是整套系统的灵魂,但它被塞在一个.docx里,没有工程结构、没有依赖说明、没有输入输出路径约定。我把它还原成一个可编译、可调试、可扩展的 Eclipse 工程,并逐行解释其设计逻辑。重点不是“怎么写”,而是“为什么这样写”——因为 MapReduce 的编程模型反直觉:你写的Mapper和Reducer不是顺序执行的函数,而是被框架调度的组件,它们的输入输出类型、序列化方式、组合逻辑,共同决定了 job 的性能和正确性。
3.1 输入数据格式与Mapper设计:Text作为万能载体的代价与收益
文档提到初始数据是kechengscore.txt和scoreinput.txt,但没给样例。根据代码中map方法的解析逻辑,标准格式是课程名\t分数(Tab 分隔),例如:
高等数学 85 大学英语 92 高等数学 78 数据结构 88Mapper类定义如下:
public static class averageMapper extends Mapper<LongWritable, Text, Text, LongWritable> { private Text course = new Text(); private LongWritable score = new LongWritable(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString().trim(); if (line.isEmpty()) return; String[] parts = line.split("\t"); // 关键:按 Tab 切分 if (parts.length != 2) return; course.set(parts[0]); // 课程名作为 key score.set(Long.parseLong(parts[1])); // 分数转为 LongWritable context.write(course, score); } }关键点解析:
LongWritable是 Hadoop 自定义的序列化类型,比Long节省内存且支持跨 JVM 传输。score.set(Long.parseLong(...))是必须的转换,不能直接context.write(course, new LongWritable(Long.parseLong(...)))——虽然语法合法,但会触发额外对象创建,影响吞吐量。course.set(parts[0])复用Text对象,避免频繁 GC。这是 MapReduce 编程的黄金习惯:所有Writable类型对象都应复用,而非每次 new。if (parts.length != 2) return是健壮性兜底。真实数据常有脏行(空行、多列、少列),不加此判断,parts[1]会抛ArrayIndexOutOfBoundsException,整个 job 失败。
3.2Reducer实现聚合:用ArrayList存分数 vs 用long维护极值的内存权衡
averageReducer的核心逻辑是接收同一课程的所有分数,计算 max/min/avg。文档代码用了ArrayList<Long>存所有分数,再遍历求值:
public static class averageReducer extends Reducer<Text, LongWritable, Text, Text> { @Override protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException { List<Long> scores = new ArrayList<>(); long sum = 0; long count = 0; long max = Long.MIN_VALUE; long min = Long.MAX_VALUE; for (LongWritable val : values) { long score = val.get(); scores.add(score); // 存入列表 sum += score; count++; max = Math.max(max, score); min = Math.min(min, score); } double avg = (double) sum / count; String result = String.format("Max:%d, Min:%d, Avg:%.2f", max, min, avg); context.write(key, new Text(result)); } }这个实现能跑通,但有严重隐患:
- 若某门课有 10 万学生(如全校通识课),
scores列表会吃掉数百 MB 内存,极易 OOM。 Math.max/min是 O(1) 操作,而ArrayList是 O(n) 空间。
生产环境应改为流式计算:
// 替换 reduce 方法体 long sum = 0, count = 0, max = Long.MIN_VALUE, min = Long.MAX_VALUE; for (LongWritable val : values) { long score = val.get(); sum += score; count++; if (score > max) max = score; if (score < min) min = score; } double avg = count > 0 ? (double) sum / count : 0.0; context.write(key, new Text(String.format("Max:%d, Min:%d, Avg:%.2f", max, min, avg)));提示:
count > 0判断是防御性编程。虽然业务上不可能为 0,但 MapReduce 框架可能因数据倾斜发送空Iterable,不加判断会除零。
3.3 二次排序需求:CourseAvgSort如何用CompositeKey实现按均分倒序
文档需求②要求“将每门课程的平均成绩从高到低输出”。这需要两阶段处理:第一阶段算均分(同上),第二阶段按均分排序。但 MapReduce 默认只按key排序,而key是课程名(Text),不是均分。解决方案是自定义WritableComparable实现复合键。CourseAvgSort的Mapper输出CompositeKey(均分取负值 + 课程名)作为 key,NullWritable作为 value:
// CompositeKey 类(需实现 WritableComparable) public static class CompositeKey implements WritableComparable<CompositeKey> { private double avgScore; private Text course; public CompositeKey(double avgScore, String course) { this.avgScore = -avgScore; // 取负实现降序 this.course = new Text(course); } @Override public void write(DataOutput out) throws IOException { out.writeDouble(avgScore); course.write(out); } @Override public void readFields(DataInput in) throws IOException { avgScore = in.readDouble(); course.readFields(in); } @Override public int compareTo(CompositeKey o) { int cmp = Double.compare(this.avgScore, o.avgScore); if (cmp != 0) return cmp; return this.course.compareTo(o.course); } } // Mapper 输出:key=CompositeKey(-avg, course), value=NullWritable context.write(new CompositeKey(-avg, course.toString()), NullWritable.get());为什么取负?
因为Double.compare(a,b)返回负数表示a<b,我们希望均分高的排前面,所以把avg存为-avg,这样-95.0 < -88.5,compareTo返回负数,框架自然将其排在前面。这是 MapReduce 排序的惯用技巧,比在Reducer里收集所有结果再Collections.sort()内存友好得多。
3.4 同分频次统计:SameScoreCount中TreeMap的天然有序性如何省去排序步骤
需求③“统计每门课程中相同分数的出现次数及人数”本质是分组计数。SameScoreCount的Reducer用TreeMap<Long, Integer>存分数->人数,利用其 key 天然升序,输出时无需额外排序:
public static class SameScoreReducer extends Reducer<Text, LongWritable, Text, Text> { @Override protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException { TreeMap<Long, Integer> scoreCount = new TreeMap<>(); // key:分数, value:人数 for (LongWritable val : values) { long score = val.get(); scoreCount.put(score, scoreCount.getOrDefault(score, 0) + 1); } // TreeMap key 已升序,直接遍历输出 StringBuilder sb = new StringBuilder(); for (Map.Entry<Long, Integer> entry : scoreCount.entrySet()) { if (sb.length() > 0) sb.append("; "); sb.append(entry.getKey()).append("(").append(entry.getValue()).append(")"); } context.write(key, new Text(sb.toString())); } }对比HashMap:
若用HashMap,输出是乱序的(如85(2); 92(1); 78(3)),用户无法直观看出最高分/最低分。TreeMap虽有 O(log n) 插入开销,但n是分数种类数(通常 < 100),远小于学生总数,是值得的 trade-off。
4. 避坑指南:集群启动失败、job 提交报错、结果为空的五大高频问题
这套系统最折磨人的不是写代码,而是排查那些日志里不报错、但结果不对的“幽灵问题”。以下是我在三所高校现场部署时,记录的最常踩的五个坑,每个都附带现象、根因和一招毙命的解决法。
4.1 现象:start-dfs.sh后jps看不到 DataNode,hdfs dfs -ls /报Connection refused
原因:slave 节点的hadoop-env.sh中JAVA_HOME路径错误,或未生效。
CentOS 6.8 默认java -version显示 OpenJDK,但 Hadoop 2.6.4 要求 Oracle JDK 1.7。若hadoop-env.sh写export JAVA_HOME=/usr/java/jdk1.7.0_80,但实际 JDK 安装在/opt/java/jdk1.7.0_80,DataNode 进程启动时加载libjvm.so失败,静默退出。
解决:
在 slave 节点执行:
# 查看 DataNode 日志最后一行 tail -n 1 $HADOOP_HOME/logs/hadoop-root-datanode-slave1.log # 若含 "Unable to load native-hadoop library" 或 "Could not find or load main class",立即检查 echo $JAVA_HOME # 应输出 /opt/java/jdk1.7.0_80 ls $JAVA_HOME/jre/lib/amd64/server/libjvm.so # 必须存在 # 强制在 hadoop-env.sh 中写绝对路径 echo "export JAVA_HOME=/opt/java/jdk1.7.0_80" >> $HADOOP_HOME/etc/hadoop/hadoop-env.sh4.2 现象:MapReduce job 提交后卡在ACCEPTED状态,yarn node -list显示 NodeManager 未注册
原因:slave 节点的yarn-site.xml中yarn.resourcemanager.hostname指向了localhost或127.0.0.1,而非master。
NodeManager 启动时会向 ResourceManager 注册,若yarn.resourcemanager.hostname是localhost,它会尝试连接本机的 8032 端口(不存在),注册失败,YARN Web UI 的 Nodes 页面显示 0 active nodes。
解决:
在所有 slave 节点的$HADOOP_HOME/etc/hadoop/yarn-site.xml中,确保:
<property> <name>yarn.resourcemanager.hostname</name> <value>master</value> <!-- 必须是 master,且 /etc/hosts 解析成功 --> </property>然后重启 YARN:$HADOOP_HOME/sbin/stop-yarn.sh && $HADOOP_HOME/sbin/start-yarn.sh。
4.3 现象:job 运行成功,但输出目录为空,hdfs dfs -ls /output显示_SUCCESS文件但无结果文件
原因:FileOutputFormat.setOutputPath(job, outPutPath)中的outPutPath是 HDFS 路径,但代码里传入的是本地路径(如args[1] = "/home/output")。
Hadoop 会自动创建该路径,但Reducer输出写入的是本地文件系统,而非 HDFS,导致hdfs dfs -ls看不到。
解决:
在main方法中,强制将输出路径转为 HDFS URI:
// 替换原文 Path outPutPath = new Path(args[1]); Path outPutPath = new Path("hdfs://master:9000" + args[1]); // 如 args[1]="/output" → "hdfs://master:9000/output" // 或更健壮:用 Configuration 获取默认 FS FileSystem fs = FileSystem.get(conf); Path outPutPath = fs.makeQualified(new Path(args[1]));4.4 现象:maxminaveragejob 输出结果中,Avg显示NaN或极小值(如0.00)
原因:sum和count变量在reduce方法外声明,但未在每次reduce调用前重置。
MapReduce 框架会复用Reducer实例,若sum=0, count=0写在方法外,第二次调用reduce时sum和count仍为上次的值,导致除法错误。
解决:
将sum和count声明移到reduce方法内:
protected void reduce(Text key, Iterable<LongWritable> values, Context context) { long sum = 0; // ✅ 每次 reduce 调用都新建 long count = 0; // ✅ long max = Long.MIN_VALUE; long min = Long.MAX_VALUE; // ... 计算逻辑 }4.5 现象:Eclipse 中运行 MapReduce 报ClassNotFoundException: org.apache.hadoop.mapreduce.Job
原因:Eclipse 工程未正确添加 Hadoop 客户端 JAR 包,或版本不匹配。
文档图 23 要求选择 Hadoop 文件夹路径,但若选的是$HADOOP_HOME/share/hadoop/common,会漏掉mapreduce目录下的 JAR。
解决:
在 Eclipse 中右键工程 →Properties→Java Build Path→Libraries→Add External JARs,一次性添加以下 5 个目录下所有 JAR(共约 80 个):
$HADOOP_HOME/share/hadoop/common/*.jar$HADOOP_HOME/share/hadoop/common/lib/*.jar$HADOOP_HOME/share/hadoop/hdfs/*.jar$HADOOP_HOME/share/hadoop/mapreduce/*.jar$HADOOP_HOME/share/hadoop/yarn/*.jar
5. 二次开发实战:从成绩分析到教学预警,三步改造让系统产生业务价值
这套系统最大的价值,不是跑通那三个 MapReduce job,而是它提供了一个可插拔的数据处理骨架。我去年帮某高校信息中心做的改造,就是基于它,三天内上线了“挂科风险实时预警”功能。核心思路是:把 MapReduce 当作 ETL 工具,把分析结果喂给轻量级 Web 服务,而不是停留在命令行。下面是我总结的、可直接复用的三步改造法。
5.1 第一步:用MultipleOutputs将不同分析结果写入不同 HDFS 目录
原始代码中,所有 job 的输出都写到一个目录(如/output),后续程序要区分“均分结果”和“同分统计”,只能靠文件名或内容解析,脆弱且低效。MultipleOutputs允许Reducer按业务逻辑写入多个命名输出:
// 在 main 方法中配置 MultipleOutputs.addNamedOutput(job, "avg_result", TextOutputFormat.class, Text.class, Text.class); MultipleOutputs.addNamedOutput(job, "same_score", TextOutputFormat.class, Text.class, Text.class); // 在 Reducer 中 MultipleOutputs mos = new MultipleOutputs(context); // 计算均分后 mos.write("avg_result", key, new Text(avgResult), "avg_output"); // 计算同分后 mos.write("same_score", key, new Text(sameScoreResult), "same_score_output"); mos.close();效果:job 运行后,HDFS 中自动生成/output/avg_output/和/output/same_score_output/两个子目录,各自包含part-r-00000文件。后续 Python 脚本可直接hdfs dfs -cat /output/avg_output/part-r-00000读取,无需字符串匹配。
5.2 第二步:用Oozie替代手动提交,构建成绩分析工作流
手动敲hadoop jar xxx.jar ...无法应对真实场景:教务系统每天凌晨导出新成绩 CSV,系统需自动触发分析、生成报表、邮件通知。Oozie是 Hadoop 生态的工作流调度器,用 XML 定义任务依赖:
<!-- workflow.xml --> <workflow-app name="score-analysis" xmlns="uri:oozie:workflow:0.5"> <start to="import-data"/> <action name="import-data"> <shell xmlns="uri:oozie:shell-action:0.3"> <job-tracker>${jobTracker}</job-tracker> <name-node>${nameNode}</name-node> <exec>hdfs dfs -put -f /data/new_scores.csv /input/scores/</exec> </shell> <ok to="calc-avg"/> <error to="fail"/> </action> <action name="calc-avg"> <map-reduce> <job-tracker>${jobTracker}</job-tracker> <name-node>${nameNode}</name-node> <configuration> <property><name>mapred.mapper.class</name><value>demo.AvgMapper</value></property> <property><name>mapred.reducer.class</name><value>demo.AvgReducer</value></property> </configuration> </map-reduce> <ok to="send-email"/> <error to="fail"/> </action> <action name="send-email"> <shell> <exec>python /opt/alert.py --input hdfs://master:9000/output/avg_output/</exec> </shell> <ok to="end"/> <error to="fail"/> </action> </workflow-app>部署命令:
oozie job -oozie http://master:11000/oozie -config job.properties -run从此,成绩分析变成一个可监控、可重试、可告警的生产任务。
5.3 第三步:用Hive构建成绩数据仓库,支持 SQL 即席查询
MapReduce 适合固定模式批处理,但教务老师常问:“计算机学院大二学生的高等数学均分,和信工学院比怎么样?” 这种 ad-hoc 查询用 MapReduce 写太重。Hive提供 SQL 接口,底层仍走 MapReduce:
-- 创建外部表,映射 HDFS 中的成绩文件 CREATE EXTERNAL TABLE scores ( course STRING, score INT, student_id STRING ) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LOCATION '/input/scores/'; -- 一句 SQL 完成跨院系对比 SELECT dept, AVG(score) as avg_score, COUNT(*) as student_count FROM ( SELECT s.course, s.score, CASE WHEN s.student_id LIKE 'CS%' THEN '计算机学院' ELSE '信工学院' END as dept FROM scores s WHERE s.course = '高等数学' ) t GROUP BY dept;关键配置:在hive-site.xml中设置hive.metastore.uris指向 MySQL(存储元数据),避免使用默认 Derby(单机,不支持并发)。
从那以后我每次接到教育类大数据需求,都先搭这套 Hadoop 三节点集群,再按需叠加 Oozie、Hive、Sqoop。它不炫技,但像一把瑞士军刀——剪刀、开瓶器、螺丝刀都在,就看你用哪一把。希望帮到你。
本文还有配套的精品资源,点击获取