首页 / 资讯中心 / 文章详情

基于Hadoop的好友推荐系统:从共同好友到TopN完整实战

基于Hadoop的好友推荐系统:从共同好友到TopN完整实战 ★ FEATURED ARTICLE
简介基于 Hadoop 实现的好友推荐系统是一套完整的毕业设计资源采用 Java 开发包含源码和文档说明面向计算机、大数据、通信、人工智能等专业学生既可用于课程设计、期末大作业或毕设参考也适合 Hadoop 初学者进阶学习。压缩包共 2000 个文件大小约 79.46MB主要包含 Java 源码、JSP 页面、XML 配置、Jar 依赖包等此外还有大量 PNG 图片和 CSS 文件用于系统页面展示与前端样式配置Java 类中涉及数据访问、Mapper 计算、聚类与距离计算等模块整体代码结构较清晰。该项目是作者个人毕设答辩评审 98 分代码已完成调试测试可正常运行。配套文档说明有助于理解好友推荐场景下的数据处理流程与协同过滤思路便于二次开发与功能扩展。目前已有 200 人浏览学习适合不同基础的学习者参考实践。1. 基于Hadoop的好友推荐系统一套能交差又能讲清楚的毕业设计如果你正在为毕业设计选题发愁又不想选一个“听起来很AI、实际跑不出数”的题目基于Hadoop的好友推荐系统是目前最稳妥的方向之一。它不算新颖但胜在链路完整Hadoop环境搭建、HDFS存储、MapReduce计算、前端展示、文档说明每一环都能在论文里写出实打实的内容答辩时也能把一个完整流程讲明白。这套系统做的事情很直接——输入一份“用户-好友列表”输出一个“你可能认识的人”的TopN候选列表而核心算法其实只是共同好友计数。本文会按我实际做过的方案把环境搭建、MapReduce源码、参数配置和踩坑记录全部拆开讲新手照着操作就能跑通。2. 推荐原理与数据模型为什么“找共同好友”两轮MapReduce就能出结果2.1 共同好友计数的数学本质好友矩阵的转置相乘很多教程一上来就抛“基于物品的协同过滤”“基于用户的协同过滤”听起来高大上但落到好友推荐这个场景最经典的算法反而是最简单的“共同好友数”。它的逻辑用一句话说就是如果两个陌生人都认识张三那他们俩大概率也愿意认识彼此。把这句话翻译成数学语言就是一个矩阵运算。假设有一张好友关系矩阵 M行是用户列也是用户M[i][j]1 表示用户 i 是用户 j 的好友。那么 M 乘以 M 的转置得到的结果矩阵里第 (a,b) 个元素的值就是用户 a 和用户 b 的共同好友数量。这就是社交网络里最常见的“Friend-of-Friend”推荐也叫“二度人脉推荐”。但注意这里有个很关键的问题MapReduce 不能直接算矩阵乘法。一个 10 万用户的矩阵光是存储就需要 100 亿个元素伪分布式环境下根本跑不动。所以实际落地时我们不构建矩阵而是对每个用户的好友列表做两两组合生成候选对。用一个小例子说明用户 A 的好友是 B、C、D那么从这一行数据里可以产生三个候选对(B,C)、(B,D)、(C,D)。每一对的值都是 A表示“A 是 B 和 C 的共同好友”。把所有用户的数据都这样处理完再按候选对分组数一数每组有多少个不同的值就是共同好友数。这个做法的巧妙之处在于它把矩阵乘法拆成了“局部两两组合 全局聚合”正好是 MapReduce 最擅长的事情。Map 阶段做组合Reduce 阶段做计数一个作业就能完成。但实际系统里通常要跑两个作业第一个作业生成候选对并统计共同好友数第二个作业做排序取 TopN因为 MapReduce 默认只按键排序不按值排序直接在第一个作业里输出 TopN 需要额外处理。这也是本文源码部分要重点讲清楚的地方。2.2 数据格式与HDFS目录规划一表两job怎么组织开发这套系统前先要把数据格式定下来否则后面写 MapReduce 解析逻辑时会非常痛苦。我常用的输入格式是一行一个用户用制表符分隔用户 ID 和好友 ID 列表好友 ID 之间用英文逗号分隔A B,C,D B A,C,E C A,B,F D A,G E B,G F C G D,E这里有一个容易忽略的细节好友关系是对称的A 的好友列表里有 BB 的好友列表里也必须有 A否则后面过滤“已经是好友”的候选对时会漏掉。写一个校验脚本检查对称性比在代码里调试半天要快得多。HDFS 目录规划方面我一般建三个目录/user/hadoop/input存放原始好友关系数据/user/hadoop/friendpair存放第一个作业的输出也就是候选对和共同好友列表/user/hadoop/recommend存放第二个作业的输出也就是最终推荐 TopN。这样的好处是两个作业解耦第一个作业跑完可以先检查中间结果确认候选对生成正确再提交第二个作业。尤其是排错的时候能直接hdfs dfs -cat看中间结果比对着日志猜省事得多。2.3 选型边界为什么是MapReduce而不是Spark以及什么时候该上Hive你可能要问现在大数据生态里Spark 的流行度早就超过原生 MapReduce 了为什么毕业设计还要用 Hadoop MapReduce我的看法是MapReduce 更适合教学和答辩它的运行机制是“一个 Map 阶段 一个 Reduce 阶段”出错时日志堆栈简单直接适合在论文里画流程图。而 Spark 的 RDD、DAG、宽窄依赖这些概念如果只是照抄别人的代码答辩时很容易被问穿。但是有一个边界必须清楚如果数据量真的到了 TB 级这种“先两两组合再聚合”的写法会产生巨大的中间数据因为组合数是 O(n²) 的。比如一个用户有 1000 个好友一次 map 就要输出约 50 万条记录。这时候原生 MapReduce 的 Shuffle 会成为瓶颈更合理的方案是用 Spark 的groupByKey配合内存计算或者直接用 Hive SQL 让优化器帮我们处理。第 4 章末尾会给出一段 Hive 对照 SQL你可以根据自己的数据量决定用哪种方式。另外补充一句如果你的目标不是毕业设计而是真上线那这套“共同好友计数”逻辑还需要配合用户行为权重、时间衰减因子一起用否则推荐结果会偏向那些好友数量极多的高热度用户。关于这点在第 5 章的数据倾斜部分会展开。3. Hadoop伪分布式搭建从零到能跑通的最小配置3.1 JDK、SSH与Hadoop安装三条命令之外还要做的事第 1 章说过这套系统的第一道坎是环境。Hadoop 的安装本身只有三步下载、解压、配环境变量但很多同学在这一步就卡了三四天所以我把完整流程写出来。我用的是 Hadoop 3.3.4、JDK 1.8操作系统是 CentOS 7你不需要完全一致但版本别差太多。打开终端依次执行# 更新系统并安装必要的工具 sudo yum install -y java-1.8.0-openjdk-devel ssh rsync # 创建Hadoop专用用户如果已用root可以先跳过用户切换这步 sudo useradd -m hadoop sudo passwd hadoop # 配置SSH免密登录到本机 su - hadoop ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys ssh localhost这段命令里ssh localhost如果能直接登录不提示输密码说明免密配置成功。这个步骤很容易被忽略但 HDFS 的 DataNode 启动时需要 SSH 到各个节点伪分布式虽然只有一台机器也必须先搞定免密。然后是安装 Hadoop 本身cd /opt sudo tar -zxvf hadoop-3.3.4.tar.gz -C /usr/local/ sudo mv /usr/local/hadoop-3.3.4 /usr/local/hadoop sudo chown -R hadoop:hadoop /usr/local/hadoop # 配置环境变量写入 ~/.bashrc export HADOOP_HOME/usr/local/hadoop export PATH$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin source ~/.bashrc # 验证 hadoop version最后还要改/etc/hosts给机器起一个固定的主机名否则 NameNode 启动时解析主机名会失败127.0.0.1 localhost 192.168.1.100 node1把HOSTNAME也改成node1执行hostnamectl set-hostname node1后重新登录。这个操作比很多人想的更重要后面配置core-site.xml时会用到主机名。3.2 三个核心配置文件的参数core-site、hdfs-site、yarn-siteHadoop 的配置分散在etc/hadoop/目录下的好几个 XML 文件里最核心的是core-site.xml、hdfs-site.xml、yarn-site.xml还有一个mapred-site.xml注意这个文件默认叫mapred-site.xml.template要手动重命名。先看core-site.xmlconfiguration property namefs.defaultFS/name valuehdfs://node1:9000/value /property property namehadoop.tmp.dir/name value/home/hadoop/hadoop_data/tmp/value /property /configurationfs.defaultFS是 NameNode 的地址这里用node1而不是localhost是为了后面hdfs dfs命令和 Java 代码里都能用同一个地址访问集群。如果你在代码里写hdfs://localhost:9000在 shell 里却用hdfs://node1:9000可能出现“文件系统不同”的诡异问题。hadoop.tmp.dir是 Hadoop 存放临时文件的根目录默认在/tmp下系统重启后会被清空所以一定要改成自己的目录。很多同学早上起来发现 HDFS 进不去了元数据全部丢失就是因为这个参数没改。然后是hdfs-site.xmlconfiguration property namedfs.replication/name value1/value /property property namedfs.namenode.name.dir/name value/home/hadoop/hadoop_data/name/value /property property namedfs.datanode.data.dir/name value/home/hadoop/hadoop_data/data/value /property /configuration伪分布式只有一台机器副本数必须设为 1否则 DataNode 会一直提示“块副本不足”的警告。dfs.namenode.name.dir和dfs.datanode.data.dir分别存 NameNode 的元数据和 DataNode 的块数据也要指向独立目录不能跟hadoop.tmp.dir混在一起。接下来是yarn-site.xmlconfiguration property nameyarn.nodemanager.aux-services/name valuemapreduce_shuffle/value /property property nameyarn.nodemanager.resource.memory-mb/name value4096/value /property /configurationaux-services必须配置成mapreduce_shuffle否则 MapReduce 作业提交后会一直卡在 ACCEPTED 状态。如果你的机器内存只有 4Gyarn.nodemanager.resource.memory-mb可以调到 3072给系统留一点余量。最后是mapred-site.xmlconfiguration property namemapreduce.framework.name/name valueyarn/value /property property namemapreduce.map.memory.mb/name value1024/value /property property namemapreduce.reduce.memory.mb/name value2048/value /property /configuration这几个参数是亲测过的血泪经验默认的 map/reduce 内存上限太低跑稍大一点的数据就会触发GC overhead limit exceeded第 5 章会单独讲这个坑。这里先把参数立好。3.3 启动、jps验证与数据上传确认HDFS真的在工作配置写完之后第一次启动 Hadoop 需要先格式化 NameNodehdfs namenode -format这一步会生成 NameNode 的元数据目录并且把集群 ID 写入目录里的VERSION文件。注意格式化只需一次以后不要随便重复执行。然后启动 HDFS 和 YARNstart-dfs.sh start-yarn.sh启动完成后用jps命令检查进程。伪分布式模式下你应该能看到这五个进程NameNodeDataNodeSecondaryNameNodeResourceManagerNodeManager缺哪个进程就说明对应的服务启动失败了优先看/usr/local/hadoop/logs/下的hadoop-hadoop-namenode-node1.log这类日志文件。接下来的验证分两步先检查 HDFS 的基本状态再上传真正要用的数据文件。可以先创建一个模拟的好友数据文件friends.csv格式就是第 2 章的 7 行数据然后执行上传命令# 创建输入目录 hdfs dfs -mkdir -p /user/hadoop/input # 上传数据 hdfs dfs -put /home/hadoop/friends.csv /user/hadoop/input/ # 查看文件 hdfs dfs -ls /user/hadoop/input/ # 检查文件块分布 hdfs fsck /user/hadoop/input/friends.csv -files -blocks -locationsfsck命令会输出文件在 HDFS 上的块 ID 和所在 DataNode 的地址这是判断“数据真的存进了 HDFS”最直接的证据。如果这里能看到一个blk_开头的块说明数据已经完成了分块和副本复制后面跑 MapReduce 时才会从 HDFS 读取而不是从本地文件系统读取。4. 源码实现两个MapReduce作业完成好友推荐4.1 作业一从好友列表生成“用户对-共同好友”候选集代码层面我建议用 Java 写因为 Hadoop 原生 API 就是 Java 的网上能查到的源码和博客也以 Java 为主。第一个作业的 Mapper 核心逻辑是解析一行用户数据对好友列表做两两组合输出键为“好友对”值为“当前用户ID”。import org.apache.hadoop.io.*; import org.apache.hadoop.mapreduce.Mapper; import java.io.IOException; public class FriendRecommendMapper extends MapperLongWritable, Text, Text, Text { private final Text outKey new Text(); private final Text outValue new Text(); 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); if (parts.length ! 2) return; String user parts[0]; String[] friends parts[1].split(,); // 对好友列表做两两组合生成候选好友对 for (int i 0; i friends.length; i) { String friendA friends[i].trim(); if (friendA.equals(user)) continue; for (int j i 1; j friends.length; j) { String friendB friends[j].trim(); if (friendB.equals(user)) continue; // 使用compareTo保证两个好友的键唯一避免(A,B)和(B,A)被当作两对 String pairKey friendA.compareTo(friendB) 0 ? friendA - friendB : friendB - friendA; outKey.set(pairKey); outValue.set(user); context.write(outKey, outValue); } } } }这里有一个容易被忽视的点为什么用friendA.compareTo(friendB)来决定顺序如果不排序A 的好友列表里有 B 和 C 时可能输出B-C而 B 的好友列表里有 A 和 C 时输出C-A这两个键在 Shuffle 阶段会被分配到不同的 Reduce导致共同好友统计不出来。用compareTo强行规定键的字母序能让同一个好友对无论从哪一行解析都生成完全相同的键。Reducer 阶段做的事情就更简单了把同一好友对的所有 value也就是共同好友 ID收集起来输出键是好友对值是“共同好友数 共同好友 ID 列表”。import org.apache.hadoop.io.*; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class FriendRecommendReducer extends ReducerText, Text, Text, Text { private final Text outValue new Text(); Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { ListString commonFriends new ArrayList(); // 同一个好友对的所有value都是共同好友 for (Text val : values) { String friend val.toString(); if (!commonFriends.contains(friend)) { commonFriends.add(friend); } } // 输出格式好友对 \t 共同好友数 \t 共同好友ID列表 int count commonFriends.size(); String value count \t String.join(,, commonFriends); outValue.set(value); context.write(key, outValue); } }这段代码里我用了一个ArrayList.contains()去重在数据量大时会比较慢换成HashSet会更合适。毕业设计的数据量一般不大用 ArrayList 反而让逻辑更直白回答答辩时也更容易解释。作业一的 Drivermain 方法需要注意输出目录不能存在Hadoop 默认不允许覆盖输出目录import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; public class FriendPairJob { public static void main(String[] args) throws Exception { if (args.length ! 2) { System.err.println(Usage: FriendPairJob input output); System.exit(-1); } Configuration conf new Configuration(); Job job Job.getInstance(conf, friend pair); job.setJarByClass(FriendPairJob.class); job.setMapperClass(FriendRecommendMapper.class); job.setReducerClass(FriendRecommendReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); FileInputFormat.setInputPaths(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } }跑完作业一之后用下面的命令查看中间结果hadoop jar recommend.jar FriendPairJob /user/hadoop/input /user/hadoop/friendpair hdfs dfs -cat /user/hadoop/friendpair/part-r-00000如果数据正常你会看到类似这样的输出A-D 1 A B-C 2 B,C这个结果说明B和C有A这个共同好友而A和D的共同好友还是A自己逻辑上没问题但显然不是我们想要的推荐——因为 A 和 D 本来就是好友。这个问题由作业二来解决。4.2 作业二统计共同好友数并输出推荐TopN作业一输出的候选对里包含了很多“已经是好友”的用户对比如上面的A-D。真正的推荐系统要推荐那些“不是好友但共同好友多”的人。所以作业二需要在读取作业一结果时过滤掉已经存在好友关系的对。过滤的依据是什么最直接的方法是把原始的好友关系表加载到内存做一个HashSet判断某一对用户是否已经是好友。MapReduce 提供了 DistributedCache 机制Hadoop 3 中推荐用job.addCacheFile()可以在作业启动前把一个小文件分发到所有节点上。作业二的 Mapper 代码如下import org.apache.hadoop.io.*; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.fs.Path; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.HashSet; import java.util.Map; import java.util.TreeMap; public class TopNMapper extends MapperObject, Text, Text, Text { private final HashSetString existingFriends new HashSet(); private final TreeMapLong, String topNMap new TreeMap(); private static final int TOP_N 20; Override protected void setup(Context context) throws IOException, InterruptedException { // 从DistributedCache加载已有的好友关系格式为 user\tfriend Path[] cacheFiles context.getLocalCacheFiles(); if (cacheFiles ! null cacheFiles.length 0) { BufferedReader reader new BufferedReader(new FileReader(cacheFiles[0].toString())); String line; while ((line reader.readLine()) ! null) { String[] parts line.split(\t); if (parts.length 2) { String a parts[0].trim(); String b parts[1].trim(); String key a.compareTo(b) 0 ? a - b : b - a; existingFriends.add(key); } } reader.close(); } } Override protected void map(Object key, Text value, Context context) throws IOException, InterruptedException { String line value.toString().trim(); if (line.isEmpty()) return; String[] parts line.split(\t); if (parts.length 2) return; String pairKey parts[0]; long count Long.parseLong(parts[1]); // 过滤掉已经是好友的用户对 if (existingFriends.contains(pairKey)) return; topNMap.put(count, line); if (topNMap.size() TOP_N) { topNMap.remove(topNMap.firstKey()); } } Override protected void cleanup(Context context) throws IOException, InterruptedException { // 从大到小输出TopN注意TreeMap默认按从小到大排序 for (Map.EntryLong, String entry : topNMap.descendingMap().entrySet()) { context.write(new Text(String.valueOf(entry.getKey())), new Text(entry.getValue())); } } }这个实现的核心是TreeMap它天然对键排序topNMap.size() TOP_N时不断删除最小元素map 任务结束时cleanup里剩下的就是最大的 20 个候选对。这里要注意TreeMap的键是共同好友数如果两个候选对的共同好友数相同后写入的会覆盖先写入的需要改成TreeMapLong, ListString或者给键加一个随机后缀否则可能漏掉一部分结果。作业二不需要 Reduce所以在 Driver 里要设置job.setNumReduceTasks(0)输出文件就不是part-r-而是part-m-。hadoop jar recommend.jar TopNJob /user/hadoop/friendpair /user/hadoop/recommend -files hdfs:///user/hadoop/input/edges.txt其中-files参数指定的是从 HDFS 上读取的好友关系边表格式是每行一对好友关系两列用制表符分隔。4.3 Job参数设置Combiner、JVM复用与输出目录清理两个作业的 Driver 代码缺了几个生产环境下必调的参数单独拎出来讲。第一个是 Combiner。作业一 Shuffle 阶段的数据量很大因为同一个好友对会被很多个用户当作共同好友输出。可以在 Driver 里加一行job.setCombinerClass(FriendRecommendReducer.class);Combiner 的逻辑和 Reducer 完全一样但它是在每个 Map 任务本地先做一次合并减少发送到 Reduce 端的数据量。不是所有场景都能把 Reducer 当 Combiner 用但“汇总统计”这种场景没问题因为合并操作满足交换律和结合律。第二个是 JVM 复用。MapReduce 默认每个任务启动一个新的 JVM启动开销在数据量大时非常明显。在mapred-site.xml里设置探查参数property namemapreduce.job.jvm.numtasks/name value5/value /property意思是每个 JVM 最多运行 5 个任务减少 JVM 反复启动的开销。我在第 3 章配置环境时没有写这个参数现在补上。第三个是输出目录清理。作业第二次跑的时候如果输出目录已经存在会直接抛FileAlreadyExistsException。我在 Driver 里会加一段预处理代码Path outputPath new Path(args[1]); outputPath.getFileSystem(conf).delete(outputPath, true);这段代码在提交作业前先删除输出目录省得每次手动hdfs dfs -rm -r也避免作业失败后残留的半成品数据干扰下一次运行。4.4 Hive写法对比一行SQL能替代多少Java代码如果你完成 MapReduce 版本之后还有时间建议把同样的逻辑用 Hive 写一遍论文里可以做对比分析。Hive 的底层虽然还是 MapReduce但 SQL 表达力更强代码量能大幅缩减。WITH expanded AS ( SELECT user, friend FROM friend_relation LATERAL VIEW explode(friends) t AS friend ) SELECT e1.user AS user_a, e2.user AS user_b, COUNT(DISTINCT e1.friend) AS common_cnt FROM expanded e1 JOIN expanded e2 ON e1.friend e2.friend AND e1.user e2.user GROUP BY e1.user, e2.user ORDER BY common_cnt DESC LIMIT 20;这段 SQL 的核心是LATERAL VIEW explode()把每一行的好友列表拆成多行然后自连接连接条件是“两个人的好友相同”即e1.friend e2.friend最后按用户对分组统计共同好友数。e1.user e2.user的条件也起到了去重作用避免把 (A,B) 和 (B,A) 算成两组。对比下来你会发现MapReduce 版本的 4 个类、几百行 Java 代码Hive 版 15 行 SQL 就搞定了。但这也恰恰是毕业设计要写 MapReduce 的原因Hive 帮我们屏蔽了太多细节答辩证道“Hive 底层如何执行 join”时如果只写过 SQL 会很难回答而你亲自写过 Mapper 和 Reducer就能理解 Hive 的 join 实际上也是 Map 端或 Reduce 端的连接操作。5. 避坑指南Hadoop好友推荐最常见的5个翻车现场5.1 Container内存超限GC overhead limit exceeded怎么根治现象作业提交后进度一直停在 60% 左右日志里出现大量GC overhead limit exceededTaskAttempt 被反复 kill 重启整个作业跑了一个多小时还没结束。原因YARN 给 Container 分配的内存不够。默认配置下mapreduce.reduce.memory.mb是 1024MB但 JVM 的堆上限mapreduce.reduce.java.opts也继承了这个值一旦数据量上来GC 频繁触发最终直接 OOM。解决把内存参数调大同时保证java.opts的值不要超过 YARN 的 Container 内存上限否则 Container 启动阶段就会被 NodeManager 杀掉。property namemapreduce.reduce.memory.mb/name value2048/value /property property namemapreduce.reduce.java.opts/name value-Xmx1536m/value /property注意-Xmx要比 Container 内存小 512M 左右剩下留给 JVM 以外的开销。如果机器内存只有 4G建议先把yarn.nodemanager.resource.memory-mb调低到 3072否则多个 Container 同时申请内存会把 NodeManager 拖垮。5.2 反复格式化后集群起不来CLUSTERID不一致现象之前 Hadoop 跑得好好的某天想重新初始化环境执行了hdfs namenode -format然后start-dfs.sh但jps一看 DataNode 没起来日志里报Incompatible clusterIDs。原因namenode -format会生成一个新的集群 ID但 DataNode 的data目录里保存的还是旧的集群 ID。两个 ID 对不上DataNode 拒绝启动。解决格式化之前先清空 NameNode 和 DataNode 的数据目录。stop-dfs.sh rm -rf /home/hadoop/hadoop_data/name rm -rf /home/hadoop/hadoop_data/data hdfs namenode -format start-dfs.sh如果不确定数据目录在哪里可以看hdfs-site.xml里的dfs.namenode.name.dir和dfs.datanode.data.dir配置。以后不要反复格式化同一个集群格式化前先咨询一下为什么需要格式化90% 的情况用别的方式能解决。5.3 第二次跑同一个Job直接报错FileAlreadyExistsException现象修改了代码重新hadoop jar提交作业日志里抛异常org.apache.hadoop.mapred.FileAlreadyExistsException提示输出目录已存在。原因MapReduce 默认不覆盖输出目录这是防止误删数据的设计决策。解决两种方式任选。一是每次手动删除hdfs dfs -rm -r /user/hadoop/friendpair二是在 Driver 代码里加自动清理第 4.3 节已经写过。我自己的习惯是代码里加自动清理因为这个作业的输出是中间结果删掉不心疼但最终结果目录不要用自动删除防止误操作把有价值的输出冲掉。5.4 IDE里能跑hadoop jar却ClassNotFound现象在 IDEA 里点 Run 能正常跑完作业但打包成 jar 用hadoop jar提交后报ClassNotFoundException: org.apache.hadoop.fs.Path或类似错误。原因IDEA 里运行时用的是本地 Hadoop 依赖但hadoop jar提交到集群后需要代码里手动设置job.setJarByClass()让框架知道从哪个 jar 加载类。如果你的 jar 包没有把依赖的第三方类打进去就会在 Reduce 端遇到类找不到。解决第一步在 Driver 里写job.setJarByClass(YourMainClass.class)我写的代码里已经有了。第二步如果用了第三方库打包时用 Maven Shade 插件把依赖打成一个 fat jar。毕业设计项目一般只用 Hadoop 自带的类所以 setJarByClass 就足够了。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.2.4/version executions execution phasepackage/phase goalsgoalshade/goal/goals /execution /executions /plugin5.5 Reduce长尾热门用户把共同好友对打成数据倾斜现象作业过程中大部分 Reduce 任务 30 秒跑完但有一两个 Reduce 任务跑了 20 分钟还在跑整个作业被这两个任务拖到超时。原因数据倾斜。假设用户 “A” 是社交达人有 5000 个好友那 A 参与的组合会产生近 1250 万个候选对这些候选对大概率都包含 A 的某个好友对而默认的 HashPartitioner 让这些键集中在少数几个 Reduce 上。解决第一选择是给键加盐做两阶段聚合第一阶段加随机前缀分散到多个 Reduce第二阶段去掉前缀再聚合一次。好友推荐场景还有一个更简单的手段过滤掉好友数超过阈值的用户比如好友数超过 2000 的用户不参与两两组合因为这些高热度用户容易被推荐给所有人推荐价值反而不高。在 Map 阶段加一个判断if (friends.length 2000) return;这样会牺牲部分高热度用户的推荐覆盖率但显著降低作业运行时间。面试被问到数据倾斜时能说出“加盐两阶段聚合”和“业务层过滤极端值”这两个方案基本就拿住得分点了。6. 进阶验证把推荐结果做成用户敢信的TopN6.1 留出法验证从真实好友里藏掉20%再算准确率跑完推荐很多人不知道如何证明“推荐得准”。最朴素也最有效的做法是留出法验证把原始数据里的好友关系随机删掉 20%当作“用户还没有成为好友的关系”用剩下 80% 的数据做推荐看 TopN 推荐结果里有多少命中被删掉的那 20%。操作时先写一个 Python 脚本把friends.csv里的边随机抽取 20% 单独存成test_edges.csv从原始文件里删掉这些边得到train_edges.csv。用训练数据跑完整推荐流程然后写一段校验脚本# 读取被隐藏的真实好友关系 test_edges set() with open(test_edges.csv, r, encodingutf-8) as f: for line in f: a, b line.strip().split(\t) test_edges.add((a, b) if a b else (b, a)) # 读取推荐结果格式共同好友数 \t 好友对 \t 共同好友列表 hits 0 total 0 with open(recommend_result.txt, r, encodingutf-8) as f: for line in f: parts line.strip().split(\t) if len(parts) 2: continue pair parts[1] a, b pair.split(-) key (a, b) if a b else (b, a) total 1 if key in test_edges: hits 1 precision hits / total if total 0 else 0 print(f推荐总数: {total}, 命中隐藏好友数: {hits}, Precision{total}: {precision:.2%})注意这里算的是 Precision 而非 Recall因为 TopN 推荐天然只看“推荐的结果里有没有正确的”不追求覆盖所有可能的好友。如果 Precision 能到 10%~20%对这个算法来说已经是相当好的结果毕竟 TopN 只有 20 个名额而候选可能有几万。6.2 让推荐结果有解释性输出共同好友ID而不是只有一个计数很多毕业设计在这一步戛然而止输出一张“用户A推荐用户B”的表格就结束了。但你的系统如果拿到用户面前演示一定会被问一个问题为什么推荐这个人我建议作业一的输出保留共同好友 ID 列表代码里已经保留了然后在最终推荐结果的前端展示中把“共同好友数”替换成“你和张三有 5 个共同好友李四、王五、赵六……”。要让前端拿到这些信息需要在作业二里把作业一的完整输出行传递下去而不是只传计数。第 4.2 节的TopNMapper里已经做了这一点topNMap的值存的是line包含共同好友列表后续解析时把parts[2]拆出来就可以给前端用。用户看到推荐理由后点下“加好友”的意愿会明显提升。这个细节在答辩时讲出来导师会认为你考虑了真实产品体验而不仅仅是把数据跑通。6.3 演示与面试的收尾技巧最后说一个操作层面的习惯正式演示前我会预先把recommend输出目录清空并跑完一次完整作业把结果存在 HDFS 上演示时再跑一次展示全过程。这样即使现场集群状态波动也能先给出结果再慢慢看流程。面试或答辩时除了讲代码建议把下面这几个点提前组织好语言共同好友推荐本质是好友矩阵的转置乘法但用 MapReduce 实现时拆成了两两组合和聚合两步为什么需要两个 MapReduce 作业而不是一个作业直接输出 TopN数据倾斜如何影响作业性能你的作业里怎么缓解如果数据量扩大 100 倍瓶颈会在哪里怎么扩展。这些都是 Hadoop 面试题里的高频方向。说实话这个项目做了两轮之后我自己最大的教训是不要一上来就写代码先用小数据把两个作业的数据流跑通再扩展完整数据集。数据流一旦理解偏了后面每跑一次作业都会在某个角落里翻车。希望这篇笔记能帮到你让你的 Hadoop 毕业设计少踩几个坑。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站