用MapReduce实现二度好友推荐:从环境搭建到集群部署全解析

发布时间:2026/10/22 23:48:28
用MapReduce实现二度好友推荐:从环境搭建到集群部署全解析 社交App里“可能认识的人”这个功能估计大家都刷到过。你刚加了几个新同事系统马上把另外几个同事推给你你给某个老同学点赞第二天列表里就出现了他的大学室友。这种推荐背后有很多种算法但最朴素、也最适合拿来学 MapReduce 的一类就是“你朋友的朋友大概率也是你的朋友”。把这句话落到工程上就有了二度好友推荐。本文要拆解的这个案例就是典型的 Hadoop 2.6.0 Linux CentOS 7 IDEA 环境下用 MapReduce 实现二度好友推荐的全过程。输入是一张好友关系表输出是用户之间的推荐关系以及共同好友数。我会按实际动手的顺序把环境选型、数据设计、算法思路、核心代码、集群提交、问题排查完整讲一遍正在学 Hadoop 或者准备面试手写 MR 的同学可以直接照做。1. 项目拆解二度好友推荐到底在算什么1.1 输入数据与输出目标先明确输入。这个案例的原始数据是一张好友关系列表每一行表示一个用户以及它的直接好友字段之间用制表符分割好友之间用逗号分割。我实际准备的测试数据是下面这样A B,C B A,D,E C A,D,E D B,C E B,C意思是 A 的直接好友是 B 和 CB 的直接好友是 A、D、E以此类推。这里的好友关系我按无向处理A 是 B 的好友那 B 也是 A 的好友。输出目标是什么对任意两个“目前不是直接好友”的用户统计他们有多少个共同好友共同好友数越多说明这两人越有可能认识推荐优先级就越高。用上面的数据跑完最终输出应该是推荐对共同好友数共同好友列表含义A-D2B,CA 和 D 都认识 B、C但互相不认识可以互相推荐A-E2B,CA 和 E 都认识 B、C可以互相推荐B-C3A,D,EB 和 C 交集最大三个共同好友最值得互相推荐这样一看就清楚了。这个案例的输出不是只给某个特定用户推荐而是全局算出所有“二度可达但没有直接关系”的用户对附带推荐权重共同好友数。拿到结果后产品侧按权重排序再结合业务规则过滤就是线上“好友推荐”的候选数据源。1.2 为什么这道题适合用 MapReduce有人说这种需求用 SQL 也能写两张表做 join 就能算共同好友为什么非要用 MapReduce没错数据量小的时候 SQL 完全能搞定但这个案例出现在 Hadoop 生态里目标是一张千万级甚至亿级的好友关系表。这种规模下单机 join 的 shuffle、内存和 IO 都是瓶颈而 MapReduce 天然能把“两两组合”“分组聚合”这类逻辑拆到分布式节点上执行。更深一层看二度好友推荐本质上是在图上做“两步邻域展开”。每个用户是一条边用户和好友是一个点对。要算共同好友就得把所有好友对的两两组合找出来再按照用户对分组。这个“展开-分组-聚合”的模式和 MapReduce 的 Map-Shuffle-Reduce 执行模型刚好一一对应。所以与其说这是一个业务案例不如说它是理解 MapReduce 数据流的最佳练习你想清楚一个键值对该长什么样整个计算模型就通了。1.3 核心难点在哪里我见过的初学者写这个案例大部分不是卡在代码语法上而是卡在一个点上无序对怎么统一。A 的好友列表里有 DD 的好友列表里也有 AA-D 和 D-A 在数据里是两个方向但算法如果想聚合它们的共同好友就必须让这两个方向落到同一个 Reduce 分组里。这是整个案例最关键的设计点。解决办法不复杂自定义一个 pair 类型的 key在构造时就把两个字段按字典序排好。A-D 排成 A-DD-A 也排成 A-DMap 阶段写出这个 key 之后Shuffle 就会自动把它们送到同一个 Reducer。另一个坑是直接关系的过滤。A 和 D 如果是直接好友那即便产生了一些间接候选也不能推荐否则推给用户一个“你早就认识的人”产品体验就是灾难。怎么在不额外引入一张大表的前提下让 Reducer 知道自己正在处理的这个用户对是不是直接好友答案是在 Map 阶段同时输出两类信号一类标记 direct一类标记 indirectReducer 按 key 收到全部信号后先看 direct有就直接丢弃。2. 环境准备Hadoop 2.6.0 与 CentOS 7 版本选择2.1 版本搭配怎么选才不折腾标题里指定的是 Hadoop 2.6.0。这个版本放到今天看已经很老了但作为教学项目它有不可替代的优势社区资料最多、网上踩坑记录最全、对硬件要求低跑通一套伪分布式只需要一台 2 核 4G 的机器。生产环境当然没人再用 2.6.0但学习阶段用它能省掉很多新版本才有的兼容性问题。JDK 版本我建议用 JDK 1.8。Hadoop 2.6.0 官方支持 Java 7 和 Java 8实测 1.8 完全能跑 MapReduce老版本的某些脚本在 Java 9 以上才会出问题。CentOS 7 系统自带的 OpenJDK 版本可能比较杂最稳妥的路径是自己下载一个 JDK 8 的 tar 包解压安装然后配置JAVA_HOME不要偷懒直接用系统自带的java -version能跑就完事后续 Hadoop 脚本会因为找不到JAVA_HOME报错。CentOS 7 本身还分 minimal 和桌面版跑 Hadoop 用 minimal 就够了省内存。如果你只是学习我这里强烈建议先搭伪分布式也就是单节点同时跑 NameNode、DataNode、ResourceManager、NodeManager。整套集群逻辑和三节点完全一致但少了很多网络排查成本。先把流程跑通再考虑加节点。2.2 踩过的配置坑从 hostname 到免密登录伪分布式也要有好习惯。第一步改主机名hostnamectl set-hostname master然后在/etc/hosts里加上一行把本机 IP 映射到 master10.0.0.11 master不配 hosts 会有一个很隐蔽的毛病NameNode 和 DataNode 之间通过主机名互相通信偶尔能启动但状态显示异常日志里全是UnknownHostException。顺手把防火墙关掉避免后面 YARN 容器之间连不上systemctl stop firewalld systemctl disable firewalld接着解压 Hadoop 到/data/hadoop修改etc/hadoop/hadoop-env.sh找到export JAVA_HOME这一行改成你的 JDK 路径。注意 2.6.0 版本有个经典毛病官方下载的安装包是 32 位编译的在 64 位 CentOS 上会报 native 库加载警告Unable to load native-hadoop library for your platform...这个警告不影响纯 Java 的 MapReduce 作业运行不用焦虑。当然如果想消掉可以用源码重新编译 64 位 native 库但教学项目完全没必要别在这上面浪费时间。配置文件的重点我列一下。core-site.xml里指定默认文件系统configuration property namefs.defaultFS/name valuehdfs://master:9000/value /property property namehadoop.tmp.dir/name value/data/hadoop/tmp/value /property /configurationhdfs-site.xml里设置副本数为 1伪分布式不需要三副本property namedfs.replication/name value1/value /propertyyarn-site.xml里启用 shuffle 辅助服务这是 MapReduce 跑在 YARN 上的前提property nameyarn.nodemanager.aux-services/name valuemapreduce_shuffle/value /propertymapred-site.xml需要从模板复制一份再改cp /data/hadoop/etc/hadoop/mapred-site.xml.template /data/hadoop/etc/hadoop/mapred-site.xml核心内容是指定框架为 yarn。配置完先格式化 NameNodehdfs namenode -format格式化只要做一次后面重复启动集群时绝对不能重新格式化否则会把元数据清空之前 HDFS 里的文件全丢。然后start-dfs.sh启动 HDFSstart-yarn.sh启动 YARN最后用jps检查进程。出现 NameNode、DataNode、ResourceManager、NodeManager 四个进程就说明环境就绪了。2.3 IDEA 侧的开发环境集群环境搞定后写代码一般在 IDEA 里做。新建一个 Maven 项目pom.xml里引入一个核心依赖就够了dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version2.6.0/version /dependency这个依赖会把hadoop-common、hdfs、mapreduce-client-core都带进来本地编写、编译、打包都没问题。需要特别说明的是IDEA 直接 Run 默认走的是本地模式也就是 MapReduce 框架不会连接集群而是在本地 JVM 里模拟整个流程。本地模式适合拿小数据快速验证逻辑但它不访问 HDFS路径直接写本地文件路径。真要提交到集群还是得打成 Jar 包丢到 Linux 上执行。这两种运行方式我会在第 5 节完整演示。3. 算法设计一轮 MapReduce 怎么完成推荐3.1 核心思路把好友关系拆成两类信号二度好友推荐的 MapReduce 算法核心就一句话把好友关系拆成两类信号一类标记“这两个人是直接好友”一类标记“这两个人有一个共同的间接好友”。拿测试数据里 A 这一行来说A 的好友是 B 和 C。直接信号A 和 B 是直接好友A 和 C 是直接好友。间接信号B 和 C 共同认识 A。也就是说B 和 C 虽然不一定是好友但他们有一个共同好友 A所以 B、C 这个用户对可以获得一条间接推荐信号信号来源是 A。再看 D 这一行D 的好友是 B 和 C它也会产生一条间接信号B 和 C 共同认识 D。这样 B-C 这个用户对就会收到两条间接信号共同好友是 A 和 D。再加上 E 这一行B-C 收到三条信号共同好友是 A、D、E共同好友数 3。这个逻辑推到每个用户身上就统一了。Map 阶段读一行数据先对每个“user-friend”直接对写一个 direct 标记再对好友列表里任意两两组合写一个 indirect 标记indirect 的值带上当前用户作为“共同好友来源”。Reduce 阶段负责判断和聚合。3.2 自定义 Key让无序对变成有序对前面说了A-D 和 D-A 必须聚合到同一个分组。系统自带的 Text 类型做不到这一点因为字符串 “A-D” 和 “D-A” 在字典序比较时不是同一个 key。所以我们需要自定义一个 Key 类型内部持有两个字段构造时强制按字典序排列。我起名TextPair实现WritableComparable接口。核心逻辑在构造函数里public TextPair(String first, String second) { int cmp first.compareTo(second); if (cmp 0) { this.first.set(first); this.second.set(second); } else { this.first.set(second); this.second.set(first); } }传入 A-D 得到 (A, D)传入 D-A 也会得到 (A, D)。Map 端只要统一用这个 keyShuffle 阶段的排序和分区就会自然把同一个无序对的所有信号送到同一个 Reducer 实例。这一步是整个算法正确性的地基。同时要注意Map 输出的 key 要参与默认的哈希分区。TextPair的hashCode也需要两个字段共同参与计算并且要对称否则可能出现一个问题A-D 和 D-A 虽然 key 内容相同但 hashCode 不一致被分到不同分区。我在实现里用first.hashCode() * 163 second.hashCode()由于 first 和 second 已经排序保证了同一无序对哈希结果一致。3.3 Reduce 阶段先过滤直接好友再聚合共同好友Reducer 拿到一个用户对 key 和它对应的所有信号值。信号值有两类0直连标记说明这两个人已经是直接好友。1:用户ID间接标记冒号后面的用户就是共同好友来源。Reducer 的逻辑并不复杂遍历所有 value只要看到一个0开头的值就说明这是直接好友关系整个用户对直接跳过不再输出。如果没有0就把所有1:xxx里的 xxx 收集起来去重后就是共同好友列表列表长度就是共同好友数。这里有个值得注意的细节为什么不用 Combiner表面上看Map 端应该可以先聚合同一个用户对的间接信号减少网络传输。但在我们的算法里Reducer 必须检查整个分组里是否存在 direct 标记。如果 Combiner 把部分信号提前聚合成条数Reducer 就无法判断这个用户对是否已经是直接好友也可能把不该合并的1:A和1:B合并成计数丢失共同好友的来源信息。所以这个案例不适合加 Combiner保持最简单的一轮 MapReduce 即可。4. 核心代码实现4.1 自定义 TextPair 完整实现TextPair是这次作业的枢纽。它需要具备 Hadoop 序列化的能力才能在网络和磁盘间传输需要实现compareTo才能参与排序import org.apache.hadoop.io.Text; import org.apache.hadoop.io.WritableComparable; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; public class TextPair implements WritableComparableTextPair { private Text first new Text(); private Text second new Text(); public TextPair() { } public TextPair(String first, String second) { int cmp first.compareTo(second); if (cmp 0) { this.first.set(first); this.second.set(second); } else { this.first.set(second); this.second.set(first); } } public Text getFirst() { return first; } public Text getSecond() { return second; } Override public void write(DataOutput out) throws IOException { first.write(out); second.write(out); } Override public void readFields(DataInput in) throws IOException { first.readFields(in); second.readFields(in); } Override public int compareTo(TextPair o) { int cmp first.compareTo(o.first); if (cmp ! 0) { return cmp; } return second.compareTo(o.second); } Override public boolean equals(Object obj) { if (obj instanceof TextPair) { TextPair tp (TextPair) obj; return first.equals(tp.first) second.equals(tp.second); } return false; } Override public int hashCode() { return first.hashCode() * 163 second.hashCode(); } }特别提醒两点。第一必须保留一个无参构造Hadoop 在反序列化 key 时要通过反射调用无参构造创建对象否则会抛java.lang.NoSuchMethodException。第二write和readFields的字段顺序要严格一致先写 first 就必须先读 first顺序错乱会得到完全不可读的 key。4.2 Mapper双重信号输出Mapper 的输入是LongWritable, Text输出 key 是TextPairvalue 是Text。逻辑分两步第一步输出直接关系第二步输出间接关系import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; import java.io.IOException; public class FriendRecommendMapper extends MapperLongWritable, Text, TextPair, Text { Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line value.toString(); String[] fields line.split(\t); if (fields.length ! 2) { return; } String user fields[0].trim(); String[] friends fields[1].trim().split(,); // 第一类信号直接好友标记为 0 for (String friend : friends) { context.write(new TextPair(user, friend), new Text(0)); } // 第二类信号间接候选value 里记录共同好友来源 for (int i 0; i friends.length; i) { for (int j i 1; j friends.length; j) { context.write( new TextPair(friends[i], friends[j]), new Text(1: user)); } } } }为什么直接关系只写一次就够了因为TextPair内部排序后A-B 和 B-A 对应同一个 key。不管这条关系是从 A 那一行发现的还是从 B 那一行发现的最终都归到同一个 Reduce 分组里所以不需要写两遍写多了反而浪费网络带宽。间接信号的 value 设计有讲究。1:B表示“这是一个间接推荐信号B 是该用户对的共同好友”。用冒号做分隔符Reducer 里根据前缀区分信号类型。你也可以只输出 user不加前缀用同一个 value 既表示 direct 又表示 indirect但那样 Reducer 必须扫描完整列才敢下结论而且还要处理重复信号可读性会差很多。加前缀是最直观的做法代价几乎为零。4.3 Reducer过滤与聚合Reducer 输入 key 是TextPairvalue 是Text的可迭代集合输出我直接用Text作为 keyvalue 用一个空Text占位。这样输出文件里每条记录是一行可读文本方便直接查看import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class FriendRecommendReducer extends ReducerTextPair, Text, Text, Text { Override protected void reduce(TextPair key, IterableText values, Context context) throws IOException, InterruptedException { boolean direct false; ListString commonFriends new ArrayListString(); for (Text value : values) { String val value.toString(); if (val.startsWith(0)) { direct true; break; } commonFriends.add(val.substring(2)); } if (direct || commonFriends.isEmpty()) { return; } String joined join(commonFriends); String result key.getFirst() \t key.getSecond() \t commonFriends.size() \t joined; context.write(new Text(result), new Text()); } private String join(ListString list) { StringBuilder sb new StringBuilder(); for (String item : list) { sb.append(item).append(,); } String joined sb.toString(); if (joined.length() 0) { joined joined.substring(0, joined.length() - 1); } return joined; } }这段代码有个效率点一旦发现 direct 标记立刻 break因为已经不需要再处理同组的其他信号了。测试数据量小看不出差别但在真实的大数据场景下这个 break 能省掉大量的无用解析。关于 Reducer 内存还有一个经验要分享。如果在真实业务里想按共同好友数排序输出很多教程会让 Reducer 维护一个 TreeMap收集完所有结果后统一输出。这个思路可以跑通但在大作业里非常危险因为单个 Reducer 会把所有结果都压在内存里数据量大时直接 OOM。我的建议是保持 Reducer 流式输出业务侧要做排序就再加一个排序作业或者直接让下游消费端处理不要把排序逻辑塞进这个 Reducer。4.4 Driver 作业配置Driver 负责组装整个作业。需要注意的配置点是 Map 输出 key 的类型是自定义的TextPair不要漏设置否则框架会按默认的LongWritable反序列化直接报错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 FriendRecommendDriver { public static void main(String[] args) throws Exception { if (args.length ! 2) { System.err.println(Usage: FriendRecommendDriver input path output path); System.exit(-1); } Configuration conf new Configuration(); Job job Job.getInstance(conf, two-degree friend recommendation); job.setJarByClass(FriendRecommendDriver.class); job.setMapperClass(FriendRecommendMapper.class); job.setReducerClass(FriendRecommendReducer.class); job.setMapOutputKeyClass(TextPair.class); job.setMapOutputValueClass(Text.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); } }这里其实有个隐含问题值得展开Reducer 的输出类型和 Mapper 的输出类型不同。Mapper 输出 key 是TextPair而 Reducer 输出 key 是Text所以必须分别设置setMapOutputKeyClass和setOutputKeyClass。很多样例代码只写了其中一个结果作业在运行时出现类型不匹配的异常而且这个异常往往要到 Reduce 阶段才爆发排查起来比较花时间。5. 打包部署从 IDEA 到集群上真实跑一遍5.1 先在 IDEA 里用本地模式验证写完代码不要着急打 Jar先用 IDEA 本地模式跑一遍小数据排除逻辑问题。本地模式不需要连接集群直接 Run Driver 类输入路径写本地文件输出路径写本地目录。注意本地模式的输出目录也遵循“不能已存在”的规则第二次运行前要先删掉旧输出目录。我习惯在项目根目录放一个data/friends.txt内容就是第 1 节那份测试数据。运行完打开输出目录里的part-r-00000应该能看到A D 2 B,C A E 2 B,C B C 3 A,D,E看到这三行算法逻辑基本就验证通过了。这里有个我最初踩过的坑Reducer 输出 value 用了空Text()在本地模式没问题但在集群模式里部分环境会把空 Text 序列化出一个奇怪的字符导致输出文件末尾多个空字段。后来我改成NullWritable更干净不过用Text()不影响主流程判断你可以按自己习惯来。严格一点的话把输出 value 类型改成NullWritable更规范。5.2 IDEA 打 Jar 包的两种方式本地验证通过后开始打包。IDEA 自带 Artifacts 功能菜单路径是 File - Project Structure - Artifacts点加号选 JAR - From modules with dependenciesMain Class 选FriendRecommendDriverBuild 后生成带依赖的 Jar。这种方式最省事但会把所有第三方类都打进去Jar 体积偏大好处是集群环境不需要额外准备依赖。更推荐的是用 Maven Shade 插件在pom.xml里配置好后执行mvn clean package -DskipTests打出来的 Jar 在target目录下。无论是哪种方式最终把 Jar 拿到集群上执行之前先确认里面包含FriendRecommendDriver.class否则提交时会报ClassNotFoundException。5.3 上传数据并提交作业把测试数据传到 HDFS 上hdfs dfs -mkdir -p /input hdfs dfs -put /data/friends.txt /input/friends.txt然后执行提交命令hadoop jar friend-recommend-1.0.jar com.example.FriendRecommendDriver /input/friends.txt /output/recommend如果你的主类不在com.example包下换成实际包名。提交后注意观察终端输出MapReduce 会打印 job 的进度Map 100% Reduce 100%等待完成后再看结果hdfs dfs -cat /output/recommend/part-r-00000正常会得到和本地模式一样的三行结果。提交命令里最关键的错误点我已经遇到过无数次输出目录必须不存在。Hadoop 不会自动覆盖输出目录第二次跑相同作业时必须先执行hdfs dfs -rm -r /output/recommend否则直接报FileAlreadyExistsException。5.4 结果解读与线上衔接输出结果里每一行可以理解成一条“互相推荐”的候选记录。A-D 这行表示 A 和 D 有 2 个共同好友 B、C系统可以把 D 推荐给 A也可以把 A 推荐给 D。B-C 这行共同好友数是 3权重最高在产品里排序应该靠前。线上消费这份结果时常见做法是按共同好友数倒排截断 Top N再过滤掉用户主动屏蔽的对象然后写入推荐服务缓存。MapReduce 作业按天跑一次产出当天的最新推荐池。这也是离线推荐系统的经典链路批量计算候选集在线服务读取排序。如果你后续想学推荐系统二度好友推荐是一个非常顺滑的切入点因为这个小小的作业已经涵盖了“候选生成 - 特征计算 - 结果落地”的完整思路。6. 常见问题与避坑实录6.1 环境与运行时报错速查这个案例我前后帮不同的人调过很多次大部分问题集中在下面这个表里现象常见原因解决办法jps看不到 DataNodeNameNode 格式化后没有成功启动或启动又被重复格式化停止所有 Hadoop 进程清空 hadoop.tmp.dir 下的数据重新格式化只格式化一次提交作业后一直卡在 0%ResourceManager 或 NodeManager 没启动多半是防火墙挡了端口检查防火墙状态确认 8088、9000、8042 等端口可访问Container is running beyond virtual memory limitsYARN 默认虚拟内存比例太小本地跑不爆集群跑必爆在 yarn-site.xml 调大yarn.nodemanager.vmem-pmem-ratio比如改成 4 或 6FileAlreadyExistsException输出目录已存在删除旧输出目录hdfs dfs -rm -rClassNotFoundExceptionJar 里没打主类或类名写错用jar tf检查 Jar 内容确认主类路径Reduce 输出为空数据分隔符不是\tMap 里直接跳过检查原始文件是否用了空格或逗号分隔统一格式自定义 TextPair 报序列化异常write/readFields没实现或字段顺序不一致补全方法保持读写顺序一致6.2 算法实现的经典误区第一个误区是忽略 key 排序。如果TextPair构造器里不做字典序归一A-D 和 D-A 会被当成两个不同 key结果你会发现共同好友数少了一半甚至完全没有结果。排查这个问题的方法是把TextPair的 compareTo 单独写个单元测试输入 (A,D) 和 (D,A)断言两个对象相等且 hashCode 一致。第二个误区是给作业乱加 Combiner。我在前面强调过这个案例的 Reducer 需要看到纯正的 direct 标记和完整的间接来源。加了 Combiner 之后同一 key 的多个1:xxx可能被合并成一条聚合后的 valueReducer 的过滤逻辑就完全失效了轻则结果错误重则输出一堆脏数据。很多人随便写个 Combiner 类上去发现结果对不上百思不得其解其实底层原因就是这个。第三个误区是忘记设置setMapOutputKeyClass。主类里 Mapper 输出TextPairReducer 输出Text因为两者不一致必须分开设置。漏设置时作业能提交但跑到 Shuffle 阶段会报反序列化异常而且异常日志藏在 stdout 里新手经常翻半天找不到。记住一个原则Map 输出类型和 Reduce 输出类型只要有一个不同就要分别显示声明。6.3 我实际跑这个项目时的一些体会这套代码我在伪分布式和两节点集群上都跑过讲两个实际观察。第一数据量在几百行时本地模式和集群模式的耗时差距基本看不出来集群模式反而因为启动 YARN 容器多出十几秒开销。但一旦把数据量放大到几万行本地模式就撑不住了MapReduce 的分布式优势才开始体现。所以做这个案例时建议你准备两个数据集一个小集验证逻辑一个大集感受分布式效果。第二调试的时候别急着上大数据。先把测试数据控制在十行以内每一行都设计好它能产生的信号。比如我的测试数据里特意让 B 和 C 互相不认识但都认识 A、D、E这样能直观看到共同好友数最多的推荐对。小数据验证通过后再扩大数据量否则出问题根本没法肉眼排查。第三Hadoop 2.6.0 的日志系统默认比较啰嗦但 error 级别的关键信息往往藏在最后面。作业失败的时候先找作业 ID再执行yarn logs -applicationId appid查看容器日志比在终端里来回翻输出效率高得多。最后再分享一个小技巧如果你想把最终的推荐结果按共同好友数降序排列不要在这个作业里硬生生加全局排序。MapReduce 里做全局排序通常要控制分区或者再跑一个作业对教学项目来说是过度设计。更务实的做法是直接把结果输出到 HDFS然后用 Hive 或者 Spark 接一层ORDER BY一下就行。这样既不用改流程也能看到清晰的排序结果。这次的二度好友推荐案例重点是把 MapReduce 的数据流理解透自定义 key 怎么设计、信号怎么区分、分组后怎么过滤聚合。这个思维模型打通后面再学 Spark、Flink 的算子都会轻松很多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询