Hadoop好友推荐系统实战:从零搭建到避坑指南 简介这份资源是围绕Hadoop生态构建的好友推荐系统完整项目面向计算机、人工智能、通信工程等专业的在校学生与教师也适合企业员工用于课程设计、毕业设计或项目初期立项演示。项目已通过导师评审答辩评分达95分代码经过实际运行测试功能完整可用。压缩包共约2000个文件整体79.5MB其中png图片与css样式文件占比较大用于前端页面与图表展示java源码、class字节码、jar依赖包构成后端核心逻辑jsp页面、xml配置、properties参数文件与war包则支撑Web部署与集群运行另有md说明文档辅助理解。内容预览可见HUtils、CloudAction、DBService、ClusterDataMapper、CalDistanceMapper等类覆盖工具封装、云操作、数据库服务与距离计算等模块体现推荐算法与Hadoop集群的整合思路。目前已有162人学习下载适合在此基础上二次开发或直接用于毕设、课设与作业场景。1. 从零搭建 Hadoop 好友推荐系统这套方案到底解决什么问题社交产品里最让人头疼的一个需求就是「你可能认识的人」。用户刚注册、好友只有三五个系统却要在一秒钟内从几千万用户里挑出最可能认识的二十个人推给他。这个场景用单机 MySQL 做关联查询数据量一上来基本就废了。基于 Hadoop 的好友推荐系统本质上是把「二度好友 共同好友计数」这类天然适合并行计算的图关系运算拆到 MapReduce 或 Spark 上跑再用一个轻量 Web 层把结果展示出来。它适合两类人一类是正在做 Hadoop 课程设计、需要一套能跑通、能答辩、能写进部署文档的完整项目另一类是刚接触分布式计算想找一个数据规模可控、业务逻辑清晰、又能真正体现 MapReduce 价值的练手场景。整套东西不复杂但坑集中在环境配置和共同好友去重这两块下面按「先立住原理、再动手复现、最后讲坑」的顺序拆开讲。2. 好友推荐的核心逻辑二度好友与共同好友计数2.1 为什么推荐算法要放在 Hadoop 上跑先说清楚业务模型。假设有 A、B、C 三个人A 和 B 是好友B 和 C 是好友那么 A 和 C 之间就存在一条「二度路径」C 就是 A 的潜在好友。如果 A 和 C 之间还有第二个共同好友 D那这条推荐路径就有两条权重更高应该排在前面。所以推荐分数的核心公式就是score(A, C) A 和 C 的共同好友数量。这个计算在单机上做就是一次自连接把好友关系表(user, friend)自己 join 自己找出所有A-B和B-C的组合再按(A, C)分组计数。问题在于一个百万级用户、人均 50 个好友的社交网络好友关系边数就是 5000 万条自连接产生的中间结果会膨胀到几十亿行单机内存和磁盘都扛不住。而 MapReduce 的天然优势就在这里Map 阶段把每条好友关系「翻转」成以中间人为 key 的输出Reduce 阶段对同一个中间人的所有好友做两两组合整个过程可以水平扩展到几十个节点。这就是为什么好友推荐是 Hadoop 的经典入门场景——它的并行度足够高逻辑又足够简单不会一上来就被复杂的图算法劝退。2.2 用 MapReduce 实现共同好友计数的完整代码先定义输入格式。好友关系文件friends.txt每行是用户A,用户B表示两人互为好友双向关系只存一条A,B A,C A,D B,C B,E C,D C,F接下来是核心的 MapReduce 程序。这里用 Java 写因为 Hadoop 原生 API 对课程设计和部署文档最友好Maven 依赖也最稳定// FriendRecommend.java import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.mapreduce.Reducer; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class FriendRecommend { // Mapper把每条好友关系翻转成以「中间人」为 key 的两条记录 // 输入 A,B - 输出 B - A 和 A - B public static class FriendMapper extends MapperObject, Text, Text, Text { private Text outKey new Text(); private Text outValue new Text(); Override protected void map(Object key, Text value, Context context) throws IOException, InterruptedException { String line value.toString().trim(); if (line.isEmpty()) return; String[] pair line.split(,); if (pair.length ! 2) return; // 脏数据直接丢弃 String u1 pair[0].trim(); String u2 pair[1].trim(); // 关键以中间人为 key好友为 value outKey.set(u1); outValue.set(u2); context.write(outKey, outValue); outKey.set(u2); outValue.set(u1); context.write(outKey, outValue); } } // Reducer同一个中间人的所有好友两两组合输出 (A,C) - 1 public static class FriendReducer extends ReducerText, Text, Text, IntWritable { private IntWritable one new IntWritable(1); Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { ListString friends new ArrayList(); for (Text v : values) { friends.add(v.toString()); } // 两两组合注意去重和排除自身 for (int i 0; i friends.size(); i) { for (int j i 1; j friends.size(); j) { String a friends.get(i); String b friends.get(j); if (a.equals(b)) continue; // 统一顺序保证 (A,C) 和 (C,A) 落到同一个 key String pairKey a.compareTo(b) 0 ? a , b : b , a; context.write(new Text(pairKey), one); } } } } public static void main(String[] args) throws Exception { Configuration conf new Configuration(); Job job Job.getInstance(conf, friend-recommend); job.setJarByClass(FriendRecommend.class); job.setMapperClass(FriendMapper.class); job.setReducerClass(FriendReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } }这段代码的逻辑分两层。Mapper 做的是「关系翻转」原始输入A,B表示 A 和 B 是好友Mapper 输出两条记录A - B和B - A这样 Reduce 阶段拿到 key 为 A 的所有 value就是 A 的全部好友列表。Reducer 拿到某个中间人的好友列表后做两两组合输出(A,C) - 1表示 A 和 C 通过这个中间人产生了一次共同好友关系。最后再用一个 Sum 阶段的 Reduce 把相同(A,C)的计数累加就得到了共同好友数量。参数上要注意几个点。pairKey用compareTo统一顺序是为了避免(A,C)和(C,A)被当成两个不同的 key这是新手最容易翻车的地方。if (a.equals(b)) continue排除自身否则会出现自己推荐给自己的玄学结果。另外如果好友列表特别大Reducer 里一次性ArrayList装下所有好友可能 OOM生产环境一般会加一个阈值或者改用 Spark 的groupByKey配合分区。2.3 用 Spark 替代 MapReduce 的写法与选型对比如果你的环境里已经有 Spark用 Scala 或 PySpark 写会短很多而且迭代计算更快。PySpark 版本大概是这样# friend_recommend.py from pyspark import SparkContext sc SparkContext(local, FriendRecommend) # 读取好友关系生成双向边 lines sc.textFile(friends.txt) edges lines.map(lambda line: line.split(,)) \ .flatMap(lambda p: [(p[0], p[1]), (p[1], p[0])]) # 按中间人聚合好友列表 grouped edges.groupByKey() # 两两组合输出 ((A,C), 1) def combine(friends): friends sorted(set(friends)) result [] for i in range(len(friends)): for j in range(i 1, len(friends)): a, b friends[i], friends[j] key (a, b) if a b else (b, a) result.append((key, 1)) return result pairs grouped.flatMap(lambda kv: combine(list(kv[1]))) # 累加共同好友数 scores pairs.reduceByKey(lambda x, y: x y) scores.saveAsTextFile(output)选型上MapReduce 的优势是部署简单、不依赖额外组件、课程设计答辩时老师一看就懂Spark 的优势是代码短、迭代快、适合后续加权重和过滤逻辑。如果只是交作业MapReduce 足够如果想做成能持续迭代的小系统建议直接上 Spark后面加「共同群组」「共同兴趣标签」这类特征时不用重写整个管线。3. Hadoop 伪分布式环境搭建与项目部署3.1 从零开始安装 Hadoop 伪分布式命令与配置热词里「hadoop 伪分布式搭建」「从零开始安装 hadoop」出现频率很高说明大部分人卡在环境这一步。这里给一套在 LinuxUbuntu 20.04 或 CentOS 7 都行上能跑通的流程。前提是装好 JDK 8Hadoop 3.x 对 JDK 11 支持还不稳定别踩这个坑。# 1. 安装 JDK 8 sudo apt update sudo apt install openjdk-8-jdk -y java -version # 确认输出 1.8.x # 2. 下载并解压 Hadoop以 3.3.6 为例官网可查最新稳定版 wget https://downloads.apache.org/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz tar -zxvf hadoop-3.3.6.tar.gz -C /usr/local/ mv /usr/local/hadoop-3.3.6 /usr/local/hadoop # 3. 配置环境变量 echo export HADOOP_HOME/usr/local/hadoop ~/.bashrc echo export PATH$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin ~/.bashrc echo export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 ~/.bashrc source ~/.bashrc接下来改四个核心配置文件都在$HADOOP_HOME/etc/hadoop/下。core-site.xml指定 HDFS 的默认地址configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property /configurationhdfs-site.xml设置副本数为 1伪分布式只有一个节点设 3 会一直报副本不足configuration property namedfs.replication/name value1/value /property /configurationmapred-site.xml指定用 YARN 跑 MapReduceconfiguration property namemapreduce.framework.name/name valueyarn/value /property /configurationyarn-site.xml配置 NodeManager 的辅助服务configuration property nameyarn.nodemanager.aux-services/name valuemapreduce_shuffle/value /property /configuration配置完成后先格式化 HDFS再启动hdfs namenode -format start-dfs.sh start-yarn.sh jps # 应该看到 NameNode、DataNode、ResourceManager、NodeManagerjps输出里如果少了 DataNode九成是hdfs namenode -format执行了多次导致 clusterID 不一致把dfs/data和dfs/name目录清掉重新格式化即可。3.2 把好友推荐程序打包上传并跑通本地用 Maven 打包pom.xml里加 Hadoop 依赖注意provided范围因为集群上已经有这些 jardependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version3.3.6/version scopeprovided/scope /dependency打包命令mvn clean package # 生成 target/friend-recommend-1.0.jar上传数据到 HDFS 并运行hdfs dfs -mkdir -p /user/hadoop/friends/input hdfs dfs -put friends.txt /user/hadoop/friends/input/ hadoop jar target/friend-recommend-1.0.jar FriendRecommend \ /user/hadoop/friends/input /user/hadoop/friends/output hdfs dfs -cat /user/hadoop/friends/output/part-r-00000输出里A,C 2表示 A 和 C 有 2 个共同好友分数越高越应该推荐。到这里一个最小可用的好友推荐管线就跑通了。3.3 用 Docker 快速复现环境避免污染本机如果不想在本机装一堆东西热词里提到的「hadoop 的 docker 镜像」是个好选择。用官方或社区维护的 Hadoop 镜像一条命令起一个伪分布式环境docker run -it -p 9870:9870 -p 8088:8088 \ --name hadoop-dev \ -v $(pwd)/data:/data \ sequenceiq/hadoop-docker:2.7.1 /etc/bootstrap.sh -bash进去之后 HDFS 和 YARN 已经起好了直接hadoop jar跑程序即可。注意镜像里的 Hadoop 版本可能偏老如果代码用了 3.x 的 API需要把pom.xml里的版本对齐否则会报NoSuchMethodError。Docker 方案适合快速验证逻辑但正式部署文档里还是建议写清楚原生安装步骤因为答辩时老师可能会问「不用 Docker 怎么装」。4. 避坑与排查好友推荐系统最常见的 5 个翻车点4.1 推荐结果里出现自己或已经是好友的人现象输出里出现A,A 3或者A,B 5但 A 和 B 明明已经是好友。原因有两个一是 Reducer 里两两组合时没有排除自身二是没有过滤掉已经存在的好友关系。解决方式是在 Reducer 输出前加一层判断或者在最终结果上再跑一个过滤 Job把(A,B)已经在原始好友表里的记录删掉。我一般会在推荐分数计算完之后用一个小 MapReduce 做 anti-join虽然多一个 Job但逻辑清晰不容易出错。4.2 共同好友计数重复累加现象A 和 C 明明只有 2 个共同好友输出却是 4 或 6。原因是(A,C)和(C,A)没有统一顺序被当成了两个不同的 key各自累加了一次。解决方式就是前面代码里的compareTo排序保证无论从哪个方向来key 都是一样的。这个坑在数据量小的时候不容易发现一旦用户量上去结果会整体偏大非常隐蔽。4.3 DataNode 启动后自动退出现象start-dfs.sh执行完jps里没有 DataNode或者过几秒就消失。原因通常是hdfs namenode -format执行了多次导致 NameNode 和 DataNode 的 clusterID 不一致。解决方式是删掉$HADOOP_HOME/data和$HADOOP_HOME/logs下的所有内容重新格式化一次然后只启动一次。记住格式化只能做一次这是血泪经验。4.4 Windows 下用 IDEA 连 Hadoop 报 winutils 缺失现象在 Windows 上跑 MapReduce 单元测试报Could not locate executable null\bin\winutils.exe。原因是 Hadoop 的 Windows 原生库缺失。解决方式是下载对应版本的winutils.exe和hadoop.dll放到HADOOP_HOME/bin下并在 IDEA 的 Run Configuration 里设置HADOOP_HOME环境变量。如果只是写代码不跑本地任务也可以直接打成 jar 丢到 Linux 集群上跑绕开这个问题。4.5 Reducer 内存溢出导致任务卡死现象任务跑到 66% 或 99% 卡住不动日志里出现GC overhead limit exceeded或Container killed by YARN。原因是某个中间人的好友列表特别大Reducer 里一次性加载到ArrayList撑爆了内存。解决方式有两个一是调大mapreduce.reduce.memory.mb和mapreduce.map.memory.mb二是改逻辑不要一次性加载全部好友而是用二次排序或者分片处理。生产环境更推荐后者因为调内存只是拖延问题。5. 进阶技巧用二次排序给推荐结果加权重基础版本只算了共同好友数量但真实场景里共同好友本身也有亲疏之分。比如 A 和 C 的共同好友是 B而 B 和 A 每天聊天、和 C 一年没说过话那这条推荐的权重就应该打折。一个实用的进阶做法是引入「好友亲密度」作为权重用二次排序在 MapReduce 里实现。思路是Mapper 输出的 key 变成(A,C)加上一个权重字段value 是亲密度分数。用CompositeKey把(A,C)和权重组合起来让同一个(A,C)的所有记录落到同一个 Reducer同时在 Shuffle 阶段按权重降序排列。Reducer 里取前 N 条计算加权和。代码上需要自定义WritableComparable和Partitioner比基础版复杂但能显著提升推荐质量。// 自定义组合 key保证同一对用户落到同一分区同时按权重排序 public class PairKey implements WritableComparablePairKey { private String userA; private String userB; private int weight; Override public int compareTo(PairKey o) { int cmp this.userA.compareTo(o.userA); if (cmp ! 0) return cmp; cmp this.userB.compareTo(o.userB); if (cmp ! 0) return cmp; return Integer.compare(o.weight, this.weight); // 权重降序 } Override public void write(DataOutput out) throws IOException { out.writeUTF(userA); out.writeUTF(userB); out.writeInt(weight); } Override public void readFields(DataInput in) throws IOException { userA in.readUTF(); userB in.readUTF(); weight in.readInt(); } }Partitioner 里只按userA和userB做 hash忽略 weight这样同一对用户的所有记录一定会进同一个 Reducer。Reducer 里遍历时第一条就是权重最高的共同好友可以只取前 10 条做加权后面的直接忽略既省内存又符合业务直觉。验证这套逻辑是否生效最直接的办法是拿一个小数据集手工算一遍再和程序输出对比。我一般会构造 10 个用户、20 条好友关系的测试集手工列出所有二度关系和共同好友数跑完程序后逐行核对。如果对不上先检查 key 的顺序是否统一再检查权重排序有没有把顺序搞反。这个习惯帮我省了很多次重新跑全量数据的后悔药。最后说个我自己的习惯每次改完 Reducer 逻辑先用local模式在 IDEA 里跑一遍小数据确认输出正确再打包上集群。直接上集群调逻辑一次跑十几分钟改一行等半天效率太低。希望帮到你。本文还有配套的精品资源点击获取