说实话,刚拿到这个题目的时候,我第一反应是"这又是一道标准的大数据毕设题"。但真做下来才发现,"基于Hadoop的健康饮食推荐系统"远比想象中复杂:它既要处理Hadoop生态的搭建和MapReduce离线任务,又要设计合理的推荐算法,还得把前后端串起来交付一个能演示、能写论文、能部署的完整项目。这篇博文我从环境搭建一直聊到论文答辩,把我实际踩过的坑、改过的代码、调试过的数据和验证过的方案都理清楚了,给正在做类似课程设计或毕业设计的同学一个可以照着走的完整参考。
先说结论:这个系统的核心价值,在于把"Hadoop大数据处理"和"个性化推荐"两个命题用一套可解释、可演示、可交付的流程结合起来。HDFS负责存放食材营养数据、菜品数据和用户行为日志,MapReduce负责离线统计用户偏好和菜品特征,推荐阶段再结合健康档案里的规则约束做过滤和排序,最终通过后端API输出到前端页面。整体难度中等偏上,大头其实不在算法,而在环境、数据、联调这些容易被低估的工程细节。
1. 项目背景与核心需求拆解
1.1 健康饮食推荐到底要解决什么问题
健康饮食推荐不是给用户丢一堆菜谱,而是要解决三个层面的问题:第一,用户不知道"按自己的身体状况应该怎么吃",所以系统需要能根据身高、体重、年龄、运动量算出每日热量和三大营养素(碳水、蛋白质、脂肪)的合理区间;第二,即使知道营养目标,用户面对几千道菜也不知道怎么选,所以系统要把满足约束的菜品筛出来,再按照用户的口味偏好排序;第三,用户的偏好数据是动态变化的,吃得越多、点得越多,推荐就应该越准,这一步天然适合用离线批处理来做。
我把用户画像拆成了两部分:静态档案(性别、年龄、身高、体重、目标)和动态行为(收藏、点击、评分、点餐记录)。静态档案用来生成硬性约束,动态行为用来做协同过滤的输入。两者不是替代关系,而是过滤加排序的两层结构。
1.2 Hadoop在毕设型项目里的合理定位
很多同学会纠结一个问题:数据量就几千条,用MySQL不就行了吗,为什么非要用Hadoop?这种质疑在答辩时也经常被老师问。我的理解是,不能把Hadoop硬当成"存储数据库"来用,而是把它定位成"离线数据处理平台":原始日志、食材全量表、菜品特征表落在HDFS上,MapReduce批量计算出用户偏好向量和菜品相似度矩阵,结果再回写到MySQL供线上查询。这样既符合Hadoop擅长离线批处理的特性,又不会让在线推荐请求去读HDFS导致延时爆炸。
这个定位不仅是技术选型,也是论文里"系统架构"章节的叙事主线。我在论文中画架构图时,特意把"数据层-Hadoop处理层-应用层"三层拆开,Hadoop层只做T+1的离线计算,应用层实时推荐走MySQL,这样架构上说得通,实现上也不别扭。
1.3 功能模块划分:哪些必须有,哪些可以不做
根据题目要求和答辩深度,我把系统划分成四个必备模块,其余一律砍掉:
- 用户管理模块:注册、登录、健康档案维护。健康档案是整个推荐规则的数据源,必须有。
- 数据管理模块:食材库、菜品库、营养标签的管理。支持管理员导入数据,这部分直接对接HDFS上传和MapReduce任务触发。
- 推荐引擎模块:规则推荐和协同过滤推荐两大路径,这是核心,也是论文里工作量最集中的地方。
- 展示与反馈模块:推荐结果展示、菜谱详情、用户点击/收藏行为采集,行为要写回日志目录供下一次MapReduce使用。
砍掉的部分包括社区分享、评论互动、社交关系、定时短信提醒等"看起来能加分但实现成本极高"的功能,它们既不能有效支撑"基于Hadoop"这个题目,又会让项目周期失控。
2. 从题目到方案的落地:技术选型与架构设计
2.1 Hadoop版本、JDK和操作系统的三角关系
做Hadoop项目,第一道坎就是版本组合。我一开始直接装了最新的Hadoop 3.3.6配JDK 17,结果NameNode启动报各种类加载错误,后来查文档发现Hadoop 3.3.x虽然支持Java 8和Java 11,但对JDK 17的支持并不完善。最终换成Hadoop 3.3.4加JDK 8,一次通过。操作系统选的CentOS 7.9虚拟机,内存分4G,磁盘40G。为什么不用Ubuntu?不是不能用,而是CentOS在Hadoop生态的传统教程里资料最多,遇到问题搜索解决方案的成功率高得多。
三者的版本组合可以搭配:Hadoop 3.3.x + JDK 8 + CentOS 7/Ubuntu 20.04 都是稳妥选。如果你非要尝试Hadoop 3.4.x,务必先确认它对应的JDK版本再动手,否则环境问题会消耗掉大量做算法的时间。
2.2 为什么不用Spark而坚持用MapReduce
明面上MapReduce比Spark慢、代码啰嗦,但在这个项目里我坚持用MapReduce。原因有三点:一是题目明确写的是Hadoop,MapReduce是Hadoop生态的原生计算模型,和HDFS的集成最直接,不需要额外引入YARN之外的Spark依赖;二是推荐系统的核心计算是"统计用户对菜品的偏好次数"和"计算菜品间相似度",这类任务本质是Count和Join,MapReduce写起来虽然代码量多一些,但逻辑透明,论文里好画流程图,答辩时也容易讲清楚;三是Spark伪分布式部署意味着再多配一套环境,内存占用会更大,虚拟机4G内存下很容易OOM。
2.3 应用层技术栈的选择
应用层我选了Spring Boot 2.7 + MyBatis Plus + MySQL 8.0,前端用Vue 2 + Element UI。这个组合是当前毕设生态里最成熟的:Spring Boot负责提供REST接口,MyBatis Plus操作MySQL,Vue负责渲染推荐卡片。为什么不选前后端不分离的JSP方案?虽然JSP项目部署更简单,但页面写起来太痛苦,而且做演示的时候前后端分离拆开启动明显更专业。
关键设计是:MySQL里存用户、菜品、健康档案、推荐结果表,HDFS里存原始CSV数据、模拟日志、MapReduce中间输出。Hadoop不直接对线上查询提供服务,它只在每天凌晨定时计算一次,把结果写入MySQL的推荐结果表。这样一来,推荐接口的响应时间基本稳定在几十毫秒,完全不会暴露Hadoop的延迟问题。
3. Hadoop伪分布式环境搭建:配置文件与启动验证
3.1 环境准备与SSH免密登录
伪分布式模式下,Hadoop的NameNode、DataNode、ResourceManager、NodeManager都跑在同一台机器上,适合学习也适合毕设演示。前置条件除了JDK之外,还要配SSH免密登录,否则每次启动都要输密码。具体步骤:
# 生成密钥并配置本机免密 ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys另外要注意关闭防火墙,或者放行必要的端口。伪分布式本机访问一般不存在端口拦截问题,但如果你用的是云服务器,核心端口如9870(NameNode Web UI)、8088(YARN Web UI)必须显式放行。
3.2 五个核心配置文件的逐项说明
Hadoop的配置分散在$HADOOP_HOME/etc/hadoop/目录下,核心是下面五个文件:
hadoop-env.sh里必须显式指定JAVA_HOME,很多教程忽略了这一项,结果报错"JAVA_HOME is not set"。
export JAVA_HOME=/usr/local/jdk1.8.0_202core-site.xml配置默认文件系统和临时目录:
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/usr/local/hadoop/tmp</value> </property> </configuration>hadoop.tmp.dir这个路径非常关键,NameNode的元数据、DataNode的数据块都存在这个目录下面。后面踩坑部分我会专门说它。
hdfs-site.xml配置副本数和NameNode Web UI端口:
<configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.http-address</name> <value>localhost:9870</value> </property> </configuration>伪分布式环境下副本数必须设成1,否则DataNode会一直尝试在另外两台机器上复制数据块,日志里满屏警告。
mapred-site.xml指定MapReduce的运行框架:
<configuration> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property> </configuration>yarn-site.xml配置ResourceManager的地址和类加载方式:
<configuration> <property> <name>yarn.resourcemanager.hostname</name> <value>localhost</value> </property> <property> <name>yarn.nodemanager.vmem-check-enabled</name> <value>false</value> </property> </configuration>最后一个属性是重点:虚拟内存检测默认开启,虚拟机内存不足时会杀掉Container,导致MapReduce任务一跑就失败,设置成false是毕设阶段最省心的做法。
3.3 格式化与启动验证
首次启动之前必须格式化NameNode,这个操作会初始化HDFS的元数据目录:
hdfs namenode -format start-dfs.sh start-yarn.sh格式化过程会输出大量日志,看到"successfully formatted"才算成功。启动完成后用jps命令检查进程,正常情况下应该看到五个进程:
- NameNode
- DataNode
- SecondaryNameNode
- ResourceManager
- NodeManager
只有一个办法能确认环境彻底正常:在HDFS上创建目录并上传一个测试文件,然后在Web UI上看到它。如果这一步没问题,再跑一个hadoop自带的wordcount示例任务验证YARN环境。两个验证都通过,才进入数据准备阶段。
4. 数据准备与MapReduce离线任务设计
4.1 食材和菜品数据从哪儿来
数据是推荐系统的食物,没有数据一切算法都是空谈。我的食材数据参考了公开的中国食物营养成分表,包含食物名称、能量(千卡/100g)、蛋白质、脂肪、碳水化合物、膳食纤维、钠、维生素C等重点字段,共整理了150种常见食材,输出成CSV格式。菜品数据则是从美食网站整理的常见家常菜,每条菜品记录包含菜名、主要食材ID列表(用逗号分隔)、烹饪方式(炒、炖、蒸、煮等)、分类(荤菜、素菜、汤类、主食、凉菜)、参考热量。共200道菜,足够支撑推荐效果演示。
这里有个容易被忽略的细节:菜品热量不能直接存一个固定值,因为同一种做法用的食材量不同。我选择在每个菜品记录里存"主要食材及克重"的结构化字段,运行时根据食材营养成分表加权计算菜品热量。这样当用户忌口某种食材时,可以精确地从菜品中剔除对应食材重新计算,推荐逻辑会灵活很多。
4.2 数据清洗与上传HDFS的流程
原始整理出来的数据有很多脏值:食材名称不统一("番茄"和"西红柿"并存)、个别菜品食材列表为空、营养含量出现明显超范围的异常值。写一个Java或Python清洗脚本,统一做三件事:规格化名称,去掉空前缀后缀,建立"番茄"到"西红柿"的同义词映射;丢弃缺失关键字段的记录;对热量超过合理范围的数据做标记处理。清洗后的文件分三个:ingredient.csv、dish.csv、user_log.csv。
上传到HDFS的命令很简单:
hdfs dfs -mkdir -p /food/input hdfs dfs -mkdir -p /food/output hdfs dfs -put /home/hadoop/data/ingredient.csv /food/input/ hdfs dfs -put /home/hadoop/data/dish.csv /food/input/ hdfs dfs -put /home/hadoop/data/user_log.csv /food/input/建议所有数据文件都以-put上传到统一目录,后续MapReduce任务的输入路径用通配符/food/input/*.csv即可一次读入,不用为每个文件写死路径。
4.3 第一个MapReduce任务:统计用户偏好
推荐系统需要知道"每个用户对哪类菜品感兴趣"。我的用户行为日志长这样:
user_id,dish_id,action,timestamp u1001,d023,click,2024-05-12 10:23:11 u1001,d023,collect,2024-05-12 10:25:40 u1002,d087,click,2024-05-12 11:02:05action字段里click记1分,collect记3分,score字段在Mapper阶段直接解析出来。Mapper的输入是每一行日志,输出(user_id + 菜品分类, score),也就是把用户的行为归并到"用户-分类"维度。Reducer对相同key累加,得到每个用户对不同菜品分类的总分。最后再用一个MapReduce任务或者直接在Reduce里并行读dish表,把分类ID映射成可读的分类名称。
伪代码大致如下:
public class PrefMapper extends Mapper<LongWritable, Text, Text, IntWritable> { protected void map(LongWritable key, Text value, Context context) { // 解析user_id, dish_id, action // 查内存缓存dish表得到category String outputKey = userId + ":" + category; int score = action.equals("collect") ? 3 : 1; context.write(new Text(outputKey), new IntWritable(score)); } }跑完之后去hdfs dfs -cat /food/output/preference/part-r-00000看结果,确认数据不是空的,再进入下一阶段。
4.4 第二个MapReduce任务:菜品相似度矩阵
协同过滤需要菜品之间的相似度。我的简化做法是:把"食材集合"作为菜品的特征向量,一道菜包含哪些食材就是它的特征集合,然后计算两两菜品的Jaccard相似度。这个计算用MapReduce来实现时分成两步:第一步以食材为key,输出包含该食材的菜品ID列表;第二步在Reduce端两两组合生成(dish_i,dish_j)对,输出相似度分子(共同食材数)。第三步再用一个简单的作业,根据每个菜品自身的食材数算出分母,得到最终相似度。
这个设计不需要维护复杂的用户-物品评分矩阵,数据规模小,跑起来非常快,而且结果可解释性强:系统可以明确告诉用户"推荐这道菜,是因为和您常吃的西红柿炒蛋共享了食材西红柿和鸡蛋"。答辩时老师问"相似度怎么算的",这一套逻辑三句话就能讲清楚。
5. 推荐算法的两种实现路径:规则推荐与协同过滤
5.1 规则推荐:基于健康档案的营养约束
规则推荐解决的是冷启动问题,也是整个系统"健康"二字的根基。它的逻辑不复杂:根据用户档案的性别、年龄、身高、体重算出BMI,再用Mifflin-St Jeor公式估算基础代谢率(BMR),然后乘以活动系数得到每日维持热量,减掉一定的热量缺口就是减脂目标热量。
以我的模拟数据为例:男性,28岁,身高175cm,体重75kg,活动系数取1.4,则BMR约1675千卡,维持热量约2345千卡,如果目标是减脂就取85%,约1993千卡。把这个每日总热量按早中晚3:4:3分配,每餐大约600千卡、800千卡、600千卡。推荐引擎要做的是从菜品库挑选满足"每餐热量在目标正负15%范围内"且"不含用户忌口食材"的候选菜品,再按用户历史偏好排序。
这套规则我用一个Java工具类实现,输入是用户档案和候选菜品列表,输出是过滤后的推荐列表。它不依赖任何Hadoop组件,但它是MapReduce结果能最终落地到展示层的关键过滤器:协同过滤先召回,规则推荐再精排,两者是流水线关系。
5.2 简化版协同过滤:UserCF还是ItemCF
在项目里我最终用了ItemCF而不是UserCF。原因很实际:用户行为数据是模拟的,用户数量也不多,UserCF在用户维度上很容易出现"所有用户都喜欢同几道菜"的夸张结果;而ItemCF根据菜品相似度推荐,每道菜都能找到几个"看起来真的有关系的替换菜",演示效果好得多。
ItemCF的实现分两个阶段。离线阶段就是前面提到的Jaccard相似度MapReduce任务,产出菜品相似度列表;在线阶段根据用户历史行为中的高评分菜品,找到相似度最高的TopN菜品,再用规则推荐过滤一遍,输出最终结果。
具体Java实现时,我在Redis和自建内存缓存之间犹豫过。最后选了应用启动时加载相似度表到ConcurrentHashMap的方案,项目规模小,内存完全够用,还少维护一个中间件。如果数据量再大一个量级,再引入Redis缓存会更合适。
5.3 冷启动与推荐的混合策略
冷启动问题在毕设答辩中几乎是必问题目,必须提前准备好。我的混合策略是:新注册用户没有历史行为,直接走规则推荐,按健康档案给出营养达标的菜品;老用户有行为记录时,走"ItemCF召回+规则过滤+热度加权"的组合;对于新入库的菜品,因为没有行为数据,相似度矩阵为空,就靠规则推荐兜底,保证新菜有曝光机会。
这里我特别处理了"热度加权":在MapReduce统计用户偏好时同步统计菜品总点击量,作为推荐排序的一个小权重因子。这样即使两道菜都满足用户偏好和营养约束,分值更高的那道菜也能排在前面,效果更贴近真实产品。
6. 后端API、前端页面与推荐结果的整合
6.1 后端API设计
Spring Boot项目的接口按业务划分成五组:用户接口、菜品接口、推荐接口、行为上报接口、数据管理接口。推荐接口是核心,它的调用逻辑是:先从请求头或token里解析用户ID,查用户档案;如果用户有历史行为,调用ItemCF推荐服务生成候选集;调用规则过滤器过滤;返回带营养标签的菜品列表;同时把本次推荐结果写入recommend_log表,供后续评估使用。
主要接口如下表:
| 方法 | 路径 | 功能 |
|---|---|---|
| POST | /api/user/register | 用户注册 |
| POST | /api/user/login | 登录并获取token |
| GET | /api/dish/list | 分页查询菜品 |
| GET | /api/recommend/daily | 获取每日推荐 |
| GET | /api/recommend/alternative/{dishId} | 获取指定菜品的替代推荐 |
| POST | /api/behavior/report | 上报点击/收藏行为 |
设计接口时有个细节:不要把HDFS路径暴露给前端,所有Hadoop相关操作都封装在Service层,前端只认普通JSON数据。这样前端开发根本不需要知道Hadoop的存在,整个系统耦合度很低。
6.2 前端页面与推荐展示逻辑
前端用Vue 2 + Element UI,页面包括登录注册页、健康档案页、菜品浏览页、推荐页、我的收藏页。推荐页核心是一个卡片列表,每张卡片展示菜名、配图(如果没做图片上传就用静态占位图)、热量、蛋白质、脂肪、碳水数值和推荐理由。
推荐理由这一块我特意做了文案模板,比如"这道菜富含优质蛋白,适合您当前增肌期的营养目标""这菜热量约480千卡,符合您午餐控制区间",推荐理由直接从规则引擎的结果中生成。这个细节在系统演示时非常加分,它能让评委一眼看出推荐不是随机的,而是真的有逻辑。
行为上报接口用异步方式调用,用户点击推荐卡片时前端发送/api/behavior/report,后端把行为写入本地日志文件或MySQL日志表,再定时同步到HDFS的/food/input/user_log.csv。毕设阶段不需要做实时埋点系统,用最简单的方式周期追加即可。
6.3 两种推荐结果的整合策略
为了演示Hadoop的效果,我专门在"数据管理"页面加了一个"离线统计报告"模块。当管理员点击"触发MapReduce计算"按钮时,后端通过ProcessBuilder调用hadoop jar命令执行统计作业,完成后页面展示用户偏好分布图和菜品热度Top10。这个小模块说明白了"Hadoop在系统里到底干了什么活",也是论文系统实现章最直观的截图素材。
在线推荐接口的数据来源则从MySQL的推荐结果表读取。这张表由每晚的定时任务生成,定时任务先触发MapReduce算相似度,再调推荐引擎批量计算每个用户的TopN菜品,最后写入MySQL。为了演示方便,我把定时任务改成可手动触发,这样答辩现场可以临时跑一遍,效果很直观。
7. 部署文档、论文写作与答辩准备的实战要点
7.1 一份靠谱部署文档应该怎么写
源码里附带的部署文档如果只写"安装JDK、解压Hadoop、运行项目"这种废话,读者根本部署不起来。我的部署文档按环境版本清单、虚拟机配置要求、Hadoop伪分布式部署、MySQL初始化、后端打包部署、前端打包部署、系统自检、常见问题八个章节来写。每个软件的安装命令都带版本锁定,不能写"安装最新版"。
尤其是Hadoop部分,我把可能遇到的问题按症状、原因、解决方案做了一张表,比如"格式化后NameNode启动失败:先删除hadoop.tmp.dir下所有文件再重试""8088页面打不开:防火墙未放行或未关闭"等。这类内容写进去之后,部署文档才能算真的"可交付"。
7.2 论文结构安排与图表规范
"基于Hadoop的健康饮食推荐系统的设计与实现"这篇论文,我建议按七章组织:绪论、相关技术介绍、需求分析、系统总体设计、系统详细设计与实现、系统测试、总结与展望。其中相关技术介绍要控制篇幅,三四页足够,重点放在HDFS架构、MapReduce编程模型、ItemCF原理。千万不要花大量篇幅抄百度百科,评委翻两页就知道你在注水。
总体设计章节必须画三张图:系统架构图、功能模块图、推荐流程图。架构图体现"客户端-应用层-Hadoop层-数据层"的关系;推荐流程图按"用户请求-规则过滤-ItemCF召回-结果排序"的时序画清楚。另外还要有数据库ER图,表至少七张以上,不然评委觉得数据模型太单薄。
7.3 答辩高频问题的准备思路
准备答辩时,我把老师可能问的问题分了三类。第一类是Hadoop相关:"为什么用Hadoop?"——答案是离线处理用户日志和相似度矩阵,规避在线查询延迟;"MapReduce的shuffle过程是什么?"——这个必须准备,画一个简图配合说明分区、排序、合并的过程。第二类是推荐算法相关:"ItemCF和UserCF的区别?为什么选ItemCF?"——从数据规模和可解释性回答;"冷启动怎么办?"——用规则推荐兜底。第三类是系统相关:"推荐结果怎么验证?"——用离线历史日志回放和人工标注抽样评估,虽然不能做在线A/B测试,但这个思路要能说出来。
8. 常见踩坑问题与调试思路
8.1 NameNode反复格式化后启动失败
这是伪分布式搭建里最经典的坑。HDFS的NameNode和DataNode通过clusterID互相确认身份,如果你格式化过一次NameNode,重新格式化的新clusterID和DataNode当前保存的clusterID不一致,DataNode就不会正常注册。症状表现为:NameNode进程正常,但hdfs dfs -ls /卡住,Web UI上DataNode显示0个存活节点。
解决办法是格式化前把Hadoop临时目录彻底清空:
rm -rf /usr/local/hadoop/tmp hdfs namenode -format这里"彻底清空临时目录"比只执行格式化命令更重要,因为临时目录里保留着旧的数据块信息。这个坑我在首次部署时耗了一个晚上,后来写进部署文档,后面所有复现环境都没再犯。
8.2 MapReduce任务运行失败:虚拟内存检测误杀
伪分布式环境下经常出现Container被NodeManager杀掉的报错,日志里写"Container is running beyond virtual memory limits"。原因前面提过:NodeManager默认开启物理内存和虚拟内存双重检测,而虚拟机里JVM的虚拟内存占用会远超物理内存,检测会把任务误杀。解法是关闭检测:
<property> <name>yarn.nodemanager.vmem-check-enabled</name> <value>false</value> </property>另一个相关坑是堆内存设置。默认的mapreduce任务堆内存可能是512M或更小,而虚拟机的总内存只有4G,跑大一点的数据就OOM。可以把map和reduce阶段的堆内存调到1G:
<property> <name>mapreduce.map.java.opts</name> <value>-Xmx1024m</value> </property> <property> <name>mapreduce.reduce.java.opts</name> <value>-Xmx1024m</value> </property>8.3 结果文件part-r-00000与预期不符
MapReduce任务跑成功过,也生成了part文件,但打开发现全是空数据或者和预期结果对不上。原因一般有两个:一是输入文件的路径写错了,MapReduce读到了空目录,任务虽然启动成功但实际上没读进任何数据,Reducer自然没有输出;二是Mapper里解析字段的分隔符和CSV实际分隔符不一致,我用逗号分隔时某几个字段里本身含逗号(比如菜品描述字段),导致解析错位。解决方法是:先hdfs dfs -cat原始文件确认分隔符,再用hdfs dfs -du -h检查输入路径文件大小,从根上验证输入不是空的。
8.4 定时任务与手动触发的并发问题
由于系统支持手动触发MapReduce计算,答辩演示时会有人手快连点按钮,导致多个MapReduce任务同时跑,抢占YARN资源。我加了一个简单的运行锁:后端用一个AtomicBoolean记录"计算任务是否执行中",为true时直接拒绝再次触发并提示"正在计算中"。这个逻辑很简单,但部署后确实避免了很多启动异常。
8.5 前端页面请求跨域问题
Vue前端占用8080端口,Spring Boot后端占用8081端口,两者开发期必须处理跨域。我写了一个CorsConfig类,放行了指定路径的跨域请求。如果你用的是反向代理方式部署,可以不配跨域,把前端打包后的静态文件和后端放到同一个域名下,通过Nginx转发API请求,生产环境效果更好。
说一下我的整体感受。这套系统做完之后,最大的收获不是"学会了Hadoop命令"或者"写会了ItemCF",而是理解了如何在题目约束下做技术权衡:Hadoop不能为了用而用,推荐不能只追求算法炫技而忽略可解释性,论文也不应该堆砌名词而牺牲逻辑完整性。如果在做这个项目的过程中,你在环境搭建环节就卡住了,先别急着怀疑自己,优先检查版本组合和临时目录清理,这两个排查方向能解决掉九成的基础环境问题。等环境通了,剩余的任务其实就是一个标准的信息管理系统加上一个能讲清楚的推荐引擎,按章节进度走,完全能按期交付。