简介:基于Hadoop的好友推荐系统毕业设计资料,采用Java语言开发,面向计算机、通信、人工智能、自动化等专业学生,适用于课程设计、期末大作业和毕业设计参考。项目为个人毕设成果,答辩评审分达98分,核心代码经调试测试可正常运行。源码涉及数据预处理、用户相似度计算、聚类分析和推荐结果生成等MapReduce阶段,逻辑清晰,基础扎实的读者可在此基础上二次开发,扩展推荐策略或调整业务功能。压缩包为ZIP格式,共2000个文件、约79.46MB,以PNG图片、CSS样式、JAR依赖包、Java源码和Class文件为主,辅以JSP页面、XML配置、JS脚本、Properties配置及MD文档,覆盖前端展示、后端逻辑、依赖库与说明文档,目录结构便于检索。目前已有200人浏览学习,适合Hadoop初学者理解分布式推荐系统落地,也适合高年级学生和开发者借鉴完整项目实现。
1. 用 Hadoop 做好友推荐系统:这类毕业设计值不值得做
先说结论:把“好友推荐”和“Hadoop”放在一起,是毕业设计里性价比很高的一种组合。推荐算法本身不复杂,但 Hadoop 这个框架的参与让整个项目有了集群、分布式计算、HDFS 存储、MapReduce 编程模型这些硬核考点,答辩时能讲的东西非常多。比起做一个纯 SSM 增删改查的“管理系统”,这种题目更贴近大数据方向,而且代码量可控、调试路径清晰,一个人完全能搞定。
这个系统要解决的实际问题很直观:在社交网络里,两个人如果有很多共同好友,那么他们大概率也认识,值得互相推荐。基于 Hadoop 实现,就是把“找共同好友”这个动作拆成 Map 和 Reduce 两个阶段,让集群帮忙算。本文按一套完整的毕业设计流程来拆,从数据模型、算法选型、核心代码到伪分布式部署、排错、调参一次讲完,适合正在做 Hadoop 课程设计或选题阶段摇摆不定的同学参考。
2. 数据模型与推荐算法选型:为什么「共同好友」这条路最稳
2.1 好友关系数据模型:两列就够了,不用建模成图
好友推荐系统的数据源头是一张“关系表”,可以理解成社交平台里的好友关系快照。最常见的存储格式是文本文件,每一行一条关系,表示“用户 A 和用户 B 是好友”,字段之间用制表符或空格分隔。例如:
1 2 1 3 1 4 2 3 3 4 4 5这个格式有三个好处:第一,HDFS 里存文本文件最省事,不需要引入 HBase 或者 MySQL,毕业设计阶段数据量也就几十万行,纯文本完全扛得住;第二,一行一条边,后续不管做清洗还是做两两组合,都能按行处理,天然契合 MapReduce 的输入输出模型;第三,字段只有两个,手工构造测试数据非常方便,验证结果时可以直接心算。
有人会问:要不要建一个图数据库,或者把数据设计成邻接矩阵?我的看法是没必要。邻接矩阵适合稠密图,社交关系是典型稀疏图,一万个用户就有上亿个格子的矩阵,99.99% 都是空值;图数据库则需要额外引 Neo4j,把毕业设计的技术栈拖得很重。用两列文本加一个共享好友的统计逻辑,是最贴合 Hadoop 能力的方案。需要强调的一点是,设计输入格式时要在文档里写清楚“用户 ID 用什么类型”,建议统一用字符串。虽然示例数据里是整数,但实际社交平台的用户 ID 往往带前缀,字符串处理起来兼容性更好。
2.2 推荐算法选型:共同好友计数为什么比协同过滤更稳
好友推荐的核心算法有好几个候选:基于用户的协同过滤、基于物品的协同过滤、共同好友计数、社交图谱的 PageRank 变体。毕业设计选哪个,不能光看效果,还要看“能不能讲清楚、能不能用 MapReduce 实现”。
共同好友计数的思路是:如果用户 X 和用户 Y 都同时是用户 Z 的好友,那么 Z 就是 X 和 Y 的一个“共同好友”。统计 X 和 Y 之间所有这样的共同好友数量,数量越大,说明两个人越可能认识。它的计算形式可以写成:
score(X, Y) = | { Z | X-Z 是好友 且 Y-Z 是好友 } |这个算法好在哪里?第一,语义非常直白,答辩时一句话就能讲明白,评委不会质疑“你的推荐逻辑不成立”;第二,它的计算过程能很自然地映射到 MapReduce 的两两组合阶段,不会出现需要全局迭代或复杂矩阵运算的部分;第三,结果好验证,一个小数据集手工就能算出来,程序跑完对一下就知道有没有写错。
协同过滤当然也可以做,但它的相似度计算(比如余弦相似度)需要构造用户对物品的评分向量,对社交关系数据来说“评分”从哪来本身就是一个问题。如果强行把共同好友数量当成评分,那本质上又绕回了共同好友计数,徒增复杂度。还有 PageRank 类算法,需要在多个迭代轮次之间传递状态,MapReduce 要实现迭代需要手动写 chain 或者借助外层循环,调试成本偏高,除非论文想往深度方向写,否则不建议在毕业设计里碰。选择共同好友计数,本质上是把精力留给 Hadoop 本身:数据倾斜怎么处理、分区策略怎么设计、多个 Job 怎么串联,这些才是这个项目真正要展示的东西。
2.3 为什么这个场景非 Hadoop 不可
一个很现实的提问总会出现在答辩现场:这个需求我用 MySQL 也能做啊,一个GROUP BY加JOIN就出来了,为什么非要 Hadoop?
这里需要想清楚答案。单机数据库处理几万用户、几十万条关系确实没问题,但把规模推到千万级用户、上亿条边时,两两组合的候选对会爆炸式增长,单机内存很快不够用。Hadoop 的价值在于把候选对的生成和统计分布到多台机器上并行处理,Map 阶段各算各的,Reduce 阶段按 key 聚合,这是分布式计算的基本功。毕业设计不一定要真的在集群上跑几亿数据,但通过这个项目去展示“我理解分布式计算怎么解决单机瓶颈”,这才是核心。
另外一个层面是技术栈的完整性。一个 Hadoop 项目会涉及 HDFS 文件上传下载、MapReduce 编程模型、YARN 作业调度、集群资源分配、日志查看等内容,这些加在一起比单纯写一个推荐算法的 Java 类要丰富得多。文档说明部分可以顺理成章地从架构设计写到部署运维,论文的每一章都有实际支撑材料,这也是这个选题在毕业设计里经久不衰的原因。至于说数据量小的时候 Hadoop 反而慢,那是分布式框架的固有开销,在论文里作为“不足与展望”提一句就很好。
3. 核心代码实现:好友列表构建与两阶段推荐 Mapper、Reducer 怎么写
3.1 工程结构与 Maven 依赖:一份可以直接改的 pom.xml
项目建议用 Maven 管理,Java 版本选 1.8,这是 Hadoop 兼容性最稳的组合。工程结构分成四个类:清洗阶段的数据预处理 Mapper/Reducer、推荐阶段的候选生成 Mapper/Reducer、一个串联多个 Job 的 Driver 主类。下面是 pom.xml 里最关键的依赖配置。
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <hadoop.version>3.3.4</hadoop.version> </properties> <dependencies> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-client</artifactId> <version>${hadoop.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <executions> <execution> <phase>package</phase> <goals><goal>shade</goal></goals> </execution> </executions> </plugin> </plugins> </build>这里有两个关键点。第一,hadoop-client 是 Hadoop 的 Java 客户端库,打包了 HDFS、MapReduce、YARN 相关的 API,实际上线时版本必须和集群安装的 Hadoop 版本保持一致,否则会出现 RPC 协议不兼容的报错。第二,maven-shade-plugin 一定要加,它的作用是把依赖打成一个 fat jar。很多同学本地编译通过,提交到服务器后报ClassNotFoundException,就是因为没有打 fat jar,Hadoop 集群的 classpath 里找不到你用的第三方类。
3.2 第一步:好友关系清洗与邻接表构建
原始输入数据可能存在重复,比如“1 2”出现了两遍,或者同一对好友被正反写了两行。虽然两两组合的统计阶段会自动处理重复,但数据清洗能让后续 Job 的输入规模降下来,也方便讲清楚整个数据流的层次。这个阶段用一个简单的 Mapper 加 Reducer 完成,输出格式变成“用户 ID + 空格分隔的好友列表”。
public class CleanMapper extends Mapper<Object, Text, Text, Text> { @Override protected void map(Object key, Text value, Context context) throws IOException, InterruptedException { // 去掉行首尾空白,按制表符或空格拆成两个字段 String[] tokens = value.toString().trim().split("\\s+"); if (tokens.length < 2) { return; } String user = tokens[0]; String friend = tokens[1]; // 忽略自关联:用户不能是自己好友 if (user.equals(friend)) { return; } // key 是用户 ID,value 是好友 ID,等待 reduce 聚合 context.write(new Text(user), new Text(friend)); } }public class CleanReducer extends Reducer<Text, Text, Text, Text> { @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { // 把同一个用户的所有好友收集到一个 set,顺便去重 Set<String> friendSet = new HashSet<>(); for (Text val : values) { friendSet.add(val.toString()); } // 输出的 value 是空格分隔的好友列表 context.write(key, new Text(String.join(" ", friendSet))); } }这段逻辑很直白:Map 阶段把“谁的好友是谁”这个事实拆成一对 key-value,Reduce 阶段按用户 ID 分组,把好友 ID 合并成一个字符串列表。值得注意的点有两个:一是自关联过滤不能省,原始数据偶尔会有1 1这种坏数据,不过滤后面推荐时会把自己推荐给自己;二是在 Reducer 里用HashSet去重,比在 Mapper 里判断要可靠,因为同一个好友可能来自多行重复数据,只有到 Reduce 拿到全集才能判断。
3.3 第二步:候选生成 Mapper 的“两两组合”技巧
推荐阶段是整个系统的核心。Mapper 输入是上一步产出的邻接表,每一行是一个用户和它的全部好友。思路是:对用户 U 的好友列表里的任意两个好友 A 和 B,A 和 B 就构成了一个候选对,U 是他们的共同好友。为了避免 A-B 和 B-A 被当成两个不同的 key,生成候选 key 时统一把 ID 小的放在前面。
public class RecommendMapper extends Mapper<Object, Text, Text, Text> { @Override protected void map(Object key, Text value, Context context) throws IOException, InterruptedException { String[] tokens = value.toString().trim().split("\\s+"); if (tokens.length < 2) { return; } String user = tokens[0]; List<String> friends = new ArrayList<>(); for (int i = 1; i < tokens.length; i++) { friends.add(tokens[i]); } // 对好友列表做两两组合,生成候选推荐对 for (int i = 0; i < friends.size(); i++) { String a = friends.get(i); for (int j = i + 1; j < friends.size(); j++) { String b = friends.get(j); // 生成规范化的 key,保证 a-b 和 b-a 落到同一个 reducer String pairKey = buildPairKey(a, b); // value 是共同好友 user context.write(new Text(pairKey), new Text(user)); } } // 标记当前用户与每个好友的直接好友关系,用于过滤已有关系 for (String friend : friends) { String pairKey = buildPairKey(user, friend); context.write(new Text(pairKey), new Text("-1")); } } private String buildPairKey(String x, String y) { // 按字典序把两个 ID 排好序,用逗号连接 if (x.compareTo(y) < 0) { return x + "," + y; } return y + "," + x; } }这里最需要理解的是“两两组合”和“直接好友标记”这两个动作的关系。第一层循环产生的候选对,代表“这两个人可能有共同好友,值得做推荐”;第二层循环产生的标记 key,代表“这两个人已经是好友,不应该再被推荐”。两类 key 都走同一个 pairKey 归一化规则,后续 Reducer 才能把“共同好友数量”和“是否已是好友”收集到同一个分组里做判断。
3.4 第三步:Reducer 统计共同好友并过滤已存在关系
Reducer 负责对每个候选对做最终裁决。它拿到的 value 由两类数据组成:一类是共同好友 ID 字符串,一类是标记 -1。只要 values 里出现 -1,说明这个候选对已经有直接好友关系,直接从输出里排除;否则统计非 -1 的 value 数量,就是共同好友数。
public class RecommendReducer extends Reducer<Text, Text, Text, IntWritable> { @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { int commonFriendCount = 0; boolean alreadyFriend = false; for (Text val : values) { if (val.toString().equals("-1")) { alreadyFriend = true; // 发现已有关系后可以提前退出,不用继续数 break; } commonFriendCount++; } // 已经是好友或者没有共同好友的候选对,不输出 if (!alreadyFriend && commonFriendCount > 0) { context.write(key, new IntWritable(commonFriendCount)); } } }注意一个细节:我在 counting 之后的判断里把commonFriendCount > 0也加上了。如果 A 和 B 只是因为 1 个共同好友被带上,这个推荐其实是可信的;如果为 0,说明候选对来自脏数据,比如用户 U 的好友列表里本身就有重复名字,两两组合时产生了相同的 key,这种结果没有意义。
这个阶段的输出格式是A,B<TAB>共同好友数,例如2,5<TAB>2。如果希望结果按共同好友数从高到低排序,可以在 Driver 里加一个排序 Job,或者把结果导到本地后用脚本处理。毕业设计到这一步已经能讲清楚推荐链路了,排序作为扩展点写进论文即可。
3.5 Driver 类:两个 Job 串联的写法
Driver 是整个程序的入口,负责串联清洗和推荐两个 Job。Hadoop 里多个 Job 串联的标准做法是先提交前一个 Job,等它成功后,再提交下一个 Job,第二个 Job 的输入路径就是第一个的输出路径。
public class RecommendDriver { public static void main(String[] args) throws Exception { if (args.length < 2) { System.err.println("Usage: RecommendDriver <inputPath> <outputPath>"); System.exit(1); } String inputPath = args[0]; String cleanOutputPath = args[1] + "/clean"; String recommendOutputPath = args[1] + "/recommend"; runCleanJob(inputPath, cleanOutputPath); runRecommendJob(cleanOutputPath, recommendOutputPath); } private static void runCleanJob(String input, String output) throws IOException, ClassNotFoundException, InterruptedException { Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "Friend Clean"); job.setJarByClass(RecommendDriver.class); job.setMapperClass(CleanMapper.class); job.setReducerClass(CleanReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); FileInputFormat.addInputPath(job, new Path(input)); FileOutputFormat.setOutputPath(job, new Path(output)); job.waitForCompletion(true); } private static void runRecommendJob(String input, String output) throws IOException, ClassNotFoundException, InterruptedException { Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "Friend Recommend"); job.setJarByClass(RecommendDriver.class); job.setMapperClass(RecommendMapper.class); job.setReducerClass(RecommendReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); FileInputFormat.addInputPath(job, new Path(input)); FileOutputFormat.setOutputPath(job, new Path(output)); job.waitForCompletion(true); } }两个 Job 共用一个输出目录时,记得在提交前把目录删掉,否则 FileAlreadyExistsException 会直接中断程序。有的同学会通过job.getConfiguration().set("mapreduce.job.reduces", "10")单独设置 Reduce 个数,这个建议在推荐 Job 上显式配置,因为候选对的数量远大于用户数量,Reduce 太少了会导致长尾任务。
4. 伪分布式环境跑通全流程:安装配置、数据上传与作业提交
4.1 Hadoop 伪分布式安装与配置:只改三个文件
推荐系统的代码在本地编译通过只是第一步,毕业设计验收时通常要在 Linux 服务器的伪分布式环境上跑通,因为 HDFS 和 YARN 的进程是否正常启动是老师非常在意的点,这对应整个项目的部署能力。伪分布式安装比完全分布式简单很多,核心是配置三个文件:core-site.xml、hdfs-site.xml、yarn-site.xml。
先切到 Hadoop 的配置目录,一般位于$HADOOP_HOME/etc/hadoop。core-site.xml里指定 NameNode 的地址,把 localhost 换成自己的主机名,配置内容如下:
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/home/hadoop/data/tmp</value> </property> </configuration>hadoop.tmp.dir是 Hadoop 存放临时文件的地方,默认值在/tmp下,重启系统时会被清理,导致元数据丢失、NameNode 起不来。强烈建议把它设到一个固定目录,这是很多新手安装完成后第二次开机就格式化之后仍然起不来集群的常见原因。
hdfs-site.xml里设置副本数和 NameNode 的元数据目录。伪分布式只有一台机器,副本数保持默认 1 即可,不要学完全分布式里写 3:
<configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/home/hadoop/data/namenode</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/home/hadoop/data/datanode</value> </property> </configuration>yarn-site.xml要配置 ResourceManager 的地址,同时把 yarn 的调度器和 auxiliary 服务打开,否则 MapReduce 作业能提交但执行时拿不到容器:
<configuration> <property> <name>yarn.resourcemanager.hostname</name> <value>localhost</value> </property> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> </configuration>配置完成后,先格式化 NameNode:执行hdfs namenode -format,然后运行start-dfs.sh和start-yarn.sh。启动后输入jps,应该能看到 NameNode、DataNode、ResourceManager、NodeManager 这四个进程。少任何一个都要先回头看日志目录,不要反复格式化。这一步是整个环境搭建里同学们翻车率最高的地方,具体排错放第五章细说。
4.2 准备数据并上传 HDFS
数据文件按照第 2 章的格式提前准备好,这里给一个稍微大一点的测试数据集,方便后面观察推荐效果:
创建一个friends.txt,内容如下:
1 2 1 3 1 4 2 3 2 5 3 4 4 5 5 6 6 7 7 8 8 9 9 10这个数据手工算一下就能验证:用户 1 和用户 5 有一个共同好友 2,所以系统应该给 1 推荐 5;用户 1 和用户 2 是好友,所以不应该给 1 推荐 2。上传到 HDFS 的命令是:
# 创建输入目录 hdfs dfs -mkdir -p /input # 上传本地数据文件 hdfs dfs -put friends.txt /input/ # 确认上传结果 hdfs dfs -ls /input如果是 Windows 本地开发环境想连接 Linux 上的伪分布式 Hadoop,需要在 Windows 放一份和集群版本一致的winutils.exe并配置HADOOP_HOME环境变量,否则访问 HDFS 时会抛空指针。先在 Linux 上把整条链路跑通,再去折腾 Windows 客户端,这个顺序不要反过来。
4.3 打包提交作业与日志解读
代码在本地用 Maven 打包:
mvn clean package -DskipTests构建成功后进入target目录,会得到一个带依赖的 fat jar。提交作业到 YARN 的命令是:
hadoop jar friend-recommend-1.0.jar RecommendDriver /input /output作业提交后控制台会输出一个 application ID,类似application_1620000000000_0001,同时滚动显示 Map 和 Reduce 的进度百分比。这里要会看两处关键信息:一是 Map 阶段完成后,Reduce 阶段开始前,会展示 shuffle 字节数,如果该数字异常大,往往是候选对生成过多,要考虑数据倾斜;二是作业结束后会输出File System Counters,里面能看到 HDFS 读写字节数,论文里可以作为实验数据使用。查看详细日志的命令是:
yarn logs -applicationId application_1620000000000_0001跑完以后到/output/recommend目录看结果:
hdfs dfs -cat /output/recommend/part-r-00000正常会输出如下形式的推荐结果:
1,5 1 1,6 0 2,4 1 3,5 2 4,2 1 5,1 1 7,9 1里面出现1,6 0这种共同好友数为 0 的记录,说明数据里存在冗余候选,当前版本的 Reducer 保留了 count 大于 0 的过滤,所以这条本不该出现。如果看到了,回头检查 Mapper 里是不是把同一用户的好友列表重复组合了,或者原始数据有自关联。
5. 避坑:Hadoop 好友推荐系统最常见的 5 个运行问题
5.1 推荐结果出现“自己推荐自己”
现象:输出的候选对里出现1,1 3这种格式,也就是给用户自己推荐了自己,共同好友数还是正的。
原因:原始数据里存在自关联边1 1,清洗 Job 里虽然做了过滤,但如果清洗阶段的输出没有被正确读取,或者没有跑清洗 Job 直接拿原始数据提交了推荐 Job,自关联就会被当成一条正常好友关系参与两两组合。另一种可能是在 Mapper 做两两组合时没有排除当前用户 ID 本身。
解决:在清洗 Job 的 Mapper 里和推荐 Job 的buildPairKey调用处都加上一层判断。最保险的方式是在推荐 Job 的 Mapper 中,两两组合循环之前先过滤掉好友列表里与当前用户相同的项,不要只依赖清洗阶段。
5.2 作业卡在 Reduce 阶段 99% 长时间不动
现象:Map 阶段很快跑完,Reduce 进度到 99% 后长时间不结束,日志里能看到某个 reduce task 总是 running。
原因:这是典型的数据倾斜。社交网络数据天然倾斜,明星用户的好友数量是普通用户的成百上千倍,两两组合的候选对数量呈平方级增长,所有候选对都集中到了同一个 key 对应的 reducer 上。另一个常见原因是好友列表里的 ID 很多是相同前缀,Hadoop 默认的分区器按 key 的哈希值取模,同前缀的 key 很容易落在同一个分区。
解决:在 Driver 里给推荐 Job 显式设置更多 Reduce 任务,比如job.setNumReduceTasks(10)或20。如果数据倾斜特别严重,还可以在候选 key 上做二次改造,比如给 key 加盐,把热点 key 先拆散到多个 reduce 计算后再合并。毕业设计阶段设置合理的 reduce 数量通常就够用了,论文里可以把这个过程写成一个实验小节,对比不同 reduce 数量下的作业耗时,这也是一个加分点。
5.3 本地 IDE 能跑通,提交到集群报 ClassNotFoundException
现象:在 Windows 的 IDEA 里执行main方法一切正常,打包后传到 Linux 用hadoop jar提交,作业一启动就抛ClassNotFoundException: org.apache.hadoop.conf.Configuration或找不到自己写的 RecommendMapper。
原因:打包方式不对。IDEA 默认的打包不会把 Hadoop 的依赖打进 jar,而 Hadoop 集群运行时用的 classpath 只包含它自己的库,不包含你项目里的第三方依赖。反过来也有可能:本地 IDEA 能跑是因为 IDE 里配置了 Hadoop 的 classpath,但打包时没有带上自己的target/classes。
解决:按照第 3.1 节的配置,使用maven-shade-plugin打出 fat jar,或者用maven-assembly-plugin指定jar-with-dependencies的 descriptor。打包命令统一用mvn clean package,提交前先检查 jar 包大小,如果只有几十 KB,基本可以确定没有把依赖打进去,正常的 fat jar 至少是几 MB 到几十 MB。另外确认提交时类名写的是RecommendDriver而不是RecommendMapper,Driver 是主类入口,这个写错也会报类找不到。
5.4 提交作业时提示 Output directory already exists
现象:第一次提交作业正常,第二次跑同一份代码、同一个输出路径时,直接报org.apache.hadoop.mapred.FileAlreadyExistsException: Output directory hdfs://localhost:9000/output already exists。
原因:Hadoop 的设计原则是输出目录不能预先存在,防止上一次的脏结果和本次结果混在一起。这是保护机制,不是 bug。很多同学为了省事,手动删除时不写完整路径,或者忘了删干净。
解决:在 Driver 代码里加一个工具方法,提交每个 Job 之前检查输出路径是否存在,存在就递归删除。更实践的习惯是把删除操作写在运行脚本里,每次提交作业前自动执行hdfs dfs -rm -r /output。注意删除命令的路径要是-outputPath而不是-outputPath/recommend,因为两个阶段的输出目录是嵌套关系,只删子目录会导致父目录残留,下次仍然可能会报错。
5.5 伪分布式集群进程启动不全,NameNode 反复退出
现象:执行start-dfs.sh后jps只看到 DataNode 和 SecondaryNameNode,看不到 NameNode,查看日志文件发现NameNode is not formatted或格式化之后依然退出。
原因:两个点最常见。一是hadoop.tmp.dir没有单独配置,默认落在系统临时目录,重启后被操作系统清理了,NameNode 找不到持久化元数据。二是格式化操作重复执行了两次,第二次格式化的状态与 DataNode 的记录不一致,导致 DataNode 无法向 NameNode 上报数据块信息,NameNode 自动退出。
解决:把core-site.xml里的hadoop.tmp.dir和hdfs-site.xml里的dfs.namenode.name.dir、dfs.datanode.data.dir都配置成固定路径,第一次格式化后不要随意再次格式化。如果真的需要重新初始化,把data/tmp和data/namenode、data/datanode目录全部删除后,再重新格式化,保证元数据和数据块存储目录是干净的起点。这一步没做好,后面所有 HDFS 操作都会失败,而且报错信息不完全指向根因,非常折磨人。
6. 调参与验证:把 TopN 推荐结果和效果评估装进论文
跑通基础流程之后,还可以再往前一步:让输出结果是每个用户按共同好友数排好序的 TopN 推荐列表。这也是毕业设计论文里最好写“深入工作”的一部分。
要在 MapReduce 里完成 TopN,常见做法是给推荐 Job 的输出加一个排序阶段。实现思路是自定义一个WritableComparable类型作为 key,组合“共同好友数量”和“用户 ID 对”两个字段,按共同好友数降序、其次按用户对升序排序;再设置一个只含一个 Reduce 任务或利用分区器的排序 Job,把每个用户的 TopN 抽出来。不过,只用一个 Reduce 任务处理全量候选对会出现新的性能瓶颈。
更轻量的替代方案是:不用自定义 Writable,而是把推荐 Job 输出的结果hdfs dfs -get到本地,再用一个简单的 Java 程序或 shell 脚本按“共同好友数大小”分组排序,截取每个用户的前 N 条。这个方案在数据量不超过百万行时完全够用,代码量少,写进论文作为“离线分析阶段”也站得住脚。如果要强调和 Hadoop 的结合,可以在推荐 Job 的 Reducer 内部用一个TreeMap<IntWritable, Text>维护当前 key 对应候选对的推荐计数,Reducer 结束时只输出 top N 条,客户端逻辑几乎不用改。
验证环节我习惯用手工的小样例对照:构造一个三方两两都认识的三角形,比如用户 1、2、3 两两互为好友,这时候 1 和 2 应该互为共同好友?实际算一遍会发现它们之间没有间接候选,因为三个人的边已经全部直连,Reducer 会因 alreadyFriend 标记而把候选对全部过滤掉。只有加入第 4 个用户并让 2 和 4、3 和 4 成为好友,期望结果是2,3有一个共同好友 4(这里要注意如果 2 和 3 本身有直接关系则不会输出)。用这个三角形用例能同时验证“共同好友计数”和“过滤已有关系”两个逻辑,答辩现场跑给你看非常有说服力。
最后说一个我自己的习惯:这个项目里最容易翻车的从来不是算法推演,而是环境变量、目录残留和打包方式这类工程细节。拿到别人的源码,不要急着读算法,先把他给的脚本命令按顺序跑一遍,跑完再改代码,能省一整天的排查时间;自己从头写也是同理,环境通了再写 Mapper 和 Reducer,否则代码越写越慌。希望这篇笔记能帮你把基于 Hadoop 的好友推荐系统一次跑通,也希望帮你在答辩时多一份从容。
本文还有配套的精品资源,点击获取