1. 项目概述1.1 为什么选这个题目每年做计算机毕业设计推荐系统都是热门方向而新闻推荐又是推荐系统里最有嚼头的一个细分领域。我当初选这个题目主要看中三点第一新闻推荐有明确的实时性需求用户早上看的新闻和晚上看的完全不一样这就有理由引入Spark Streaming这类流处理框架第二新闻数据天然带文本属性做TF-IDF、Word2Vec或者LDA主题模型都有现成素材不用像电商推荐那样费劲构造商品特征第三这个题目能同时展示大数据处理能力和机器学习能力答辩时讲起来有东西可讲。说白了毕业设计评委看的是工作量和技术深度基于Spark的新闻推荐系统恰好能覆盖Hadoop生态、Spark核心、机器学习库、Web开发这几个模块工作量足够撑起一篇毕业论文。1.2 系统的整体功能定位先把这个系统到底要做什么说清楚。新闻推荐系统输入是用户的历史行为点击、浏览时长、收藏、搜索关键词输出是一个个性化的新闻列表——用户打开App或网站首页给他推荐哪些新闻顺序怎么排。这里有个容易被毕设新手忽略的点新闻推荐和电商推荐最大的区别在于时效性和冷启动。电商商品几个月不换新闻热点几个小就过时。你昨天推荐用户看“某地发生地震”的新闻今天还推这个用户会觉得这个系统是个傻子。所以系统的核心逻辑必须包含两条线离线计算用户兴趣画像生成候选新闻列表实时跟踪用户最近行为调整推荐结果排序两个结合起来才能算一个真正能用的新闻推荐系统而不是只做个离线的“猜你喜欢”。1.3 系统角色划分从使用者的角度系统分成三类角色普通用户浏览新闻、点击查看详情、收藏、搜索系统根据这些行为给他推荐内容内容管理员发布新闻、编辑新闻分类、下架违规内容系统管理员查看推荐效果统计、管理用户、监控任务运行状态毕设做这个用户端是重头戏管理端可以适当简化但至少要能录入新闻否则系统里没有数据推荐算法跑不起来。1.4 用的技术栈全景模块技术选型选型理由大数据存储HDFS HBaseHDFS存原始日志HBase存用户画像和推荐结果计算框架Spark Core / Spark SQL离线处理用户日志计算用户特征流处理Spark Streaming实时接收用户点击行为更新推荐算法库Spark MLlib提供ALS、TF-IDF、Word2Vec等现成算法消息队列Kafka用户行为日志的缓冲和分发数据库MySQL存储新闻内容、用户注册信息搜索引擎Elasticsearch新闻全文检索也用于召回阶段Web框架Spring Boot后端API开发前端Vue ElementUI管理后台和用户端页面这套组合是当时比较成熟的方案每个组件都经得起答辩追问。最重要的是Spark相关的内容占了很大比重和题目呼应得很好。2. 核心算法选型与设计思路2.1 推荐算法怎么选推荐系统的主流算法无非几种协同过滤、基于内容的推荐、混合推荐。新闻推荐场景下我建议不要单选一种而是用混合策略各取所长。基于用户的协同过滤找和你兴趣相似的用户推荐他们看过的新闻。适合发现新兴趣但在新闻场景有严重问题——热点新闻所有人都看协同过滤会把热点新闻推给所有人个性化程度不够。基于物品的协同过滤计算新闻之间的相似度推荐和你之前看过的新闻相似的新闻。新闻更新快物品相似度矩阵需要频繁更新计算开销大。基于内容的推荐根据你历史看过的新闻的文本主题、关键词推荐同主题的新新闻。这个最贴合新闻场景能解决时效性问题但容易陷入信息茧房。我的设计是基于内容的推荐为主力基于用户的协同过滤做补充最后用实时行为做重排序。打个比方内容推荐管“你平时喜欢看什么类型的新闻”协同过滤管“和你类似的人最近在看什么”实时重排序管“你刚才点了什么马上给你推相关的”。2.2 用户兴趣画像的构建逻辑用户画像本质上就是一组特征向量用来表示用户喜欢什么、不喜欢什么。在新闻推荐里画像是这样构建的第一步行为权重定义用户在系统上的每种行为重要程度不一样。我的设计如下行为类型权重说明点击1.0最基本的兴趣信号浏览时长超过2分钟2.5说明用户真的在看收藏5.0强兴趣信号分享8.0最强兴趣信号搜索关键词匹配3.0主动表达的需求第二步文本特征提取新闻内容经过分词、去停用词、TF-IDF向量化之后每篇新闻变成一个稀疏向量。用户在某个分类下看了3篇财经新闻就把这3篇新闻的向量累加再归一化得到用户在这个分类上的兴趣向量。第三步时间衰减用户的兴趣是会变的三个月前喜欢看体育不代表现在还喜欢。我引入了时间衰减因子兴趣分数 原始分数 * exp(-λ * 天数差)λ取0.05就是大约20天前的兴趣衰减到原值的37%。这个参数可以根据实际数据调整毕设里不用追求最优但要能解释清楚这个公式的意义。2.3 ALS协同过滤的实现细节MLlib里最常用的协同过滤算法是ALS交替最小二乘法用于矩阵分解。在新闻推荐里我们把“用户-新闻-行为分数”看成一个稀疏矩阵ALS把它分解成用户隐因子矩阵和物品隐因子矩阵然后预测缺失位置的分数。import org.apache.spark.ml.recommendation.ALS val als new ALS() .setMaxIter(10) .setRegParam(0.01) .setRank(10) .setUserCol(userId) .setItemCol(newsId) .setRatingCol(score) .setColdStartStrategy(drop) val model als.fit(trainingData)几个参数的解释setRank(10)隐因子数量相当于用10个隐含特征去解释用户的兴趣。太小模型表达力不够太大容易过拟合且训练慢setMaxIter(10)最大迭代次数ALS是迭代算法10次通常已经收敛setRegParam(0.01)正则化参数防止过拟合越大模型越简单setColdStartStrategy(drop)新用户和新新闻没有历史数据直接丢弃预测避免NaN值导致程序崩溃ALS在毕设里最大的坑是数据稀疏性。学生自己做测试用户量就几十个行为记录几百条矩阵稀疏到几乎无法训练。我的建议是如果用户行为数据不够就用程序模拟生成一批合理的测试数据至少上千条否则ALS算法跑出来毫无意义。2.4 基于内容的推荐TF-IDF加Word2Vec内容推荐的流程是这样的新闻文本分词我用的HanLP比IK分词效果好计算TF-IDF特征提取每篇新闻的关键词用Word2Vec把关键词转成词向量把新闻的关键词向量平均得到新闻的语义向量把用户历史阅读的新闻语义向量平均得到用户兴趣向量计算用户兴趣向量与候选新闻向量的余弦相似度排序取Top-NTF-IDF的计算公式TF-IDF TF * IDF TF 词在文档中出现的次数 / 文档总词数 IDF ln(总文档数 / 包含该词的文档数 1)这个公式的含义很直白一个词在当前文档里出现越多越重要但在所有文档里都出现就不重要了。“的”“了”“是”这类停用词IDF接近0自动被过滤掉而“芯片”“降息”“世界杯”这类词IDF很高能准确抓住新闻主题。代码实现from pyspark.ml.feature import HashingTF, IDF, Tokenizer tokenizer Tokenizer(inputColcontent, outputColwords) wordsData tokenizer.transform(newsDF) hashingTF HashingTF(inputColwords, outputColrawFeatures, numFeatures10000) featurizedData hashingTF.transform(wordsData) idf IDF(inputColrawFeatures, outputColfeatures) idfModel idf.fit(featurizedData) rescaledData idfModel.transform(featurizedData)HashingTF有个坑需要注意numFeatures不能设太小否则哈希冲突严重不同词会被映射到同一个特征位上拉低推荐精度。我实测numFeatures10000对新闻数据够用如果语料特别大可以调到50000但训练时间会明显增加。3. 系统架构与数据流设计3.1 整体架构图拆解整个系统的数据流就像一条流水线新闻数据录入 → MySQL → 离线推荐引擎Spark → 候选集 → HBase ↓ 用户点击行为 → Kafka → Spark Streaming → 实时更新 → 推荐结果 ↓ 用户打开App/网站 → Spring Boot后端 → 拉取推荐列表 → Vue前端展示设计这套架构时我考虑了几个关键点为什么中间要加Kafka用户点击行为是高频小数据如果直接打到业务数据库一是压力大二是没法做缓冲。Kafka相当于一个消息管道生产端前端上报行为和消费端Spark Streaming解耦点击100万条也不怕Spark慢慢消费即可。为什么推荐结果存HBaseHBase适合海量数据的随机读写用户画像和推荐结果都是KV结构用HBase很自然。但如果你不熟悉HBase用MySQL也完全能撑住毕设这个量级。我后来考虑到答辩时讲HBase是加分项才决定引入。为什么用Elasticsearch新闻检索是刚需用户搜“AI芯片”要能快速找到相关新闻MySQL的LIKE查询在数据量大时性能差。ES的倒排索引天生适合全文搜索。而且ES也可以作为内容推荐召回的候选来源——把用户兴趣词拿去ES查相关新闻召回效率比全量算相似度快得多。3.2 数据模型设计数据库表的设计直接决定后面代码好不好写我踩过不少坑先把核心表结构列出来。新闻表news字段类型说明idbigint主键titlevarchar(200)新闻标题contenttext正文内容categoryvarchar(50)分类财经/体育/科技等sourcevarchar(100)来源publish_timedatetime发布时间statustinyint0草稿 1已发布 2下架用户行为表user_behavior字段类型说明idbigint主键user_idbigint用户IDnews_idbigint新闻IDbehavior_typetinyint1点击 2收藏 3分享 4浏览超时create_timedatetime行为时间用户画像表user_profile字段类型说明user_idbigint用户IDcategory_preftext分类偏好JSON如{科技:0.8,体育:0.3}keyword_vectortext关键词向量JSONupdate_timedatetime更新时间这个表结构不复杂但能完整支撑推荐系统的数据需求。特别注意user_behavior表是核心中的核心所有推荐算法都靠它训练建表时一定加索引(user_id, create_time)和(news_id, create_time)否则后面查数据慢到怀疑人生。3.3 冷启动问题怎么解决毕设答辩时老师最可能问冷启动问题。新闻推荐系统的冷启动有两个层面新用户冷启动用户刚注册一条行为都没有拿什么推荐我的方案是让用户注册时选择兴趣标签至少选3个我称之为“引导式冷启动”根据兴趣标签直接匹配对应分类的新闻按时间倒序推最近24小时的推热门新闻兜底这是所有系统都用的手段新新闻冷启动一条新发布的新闻没有任何行为数据协同过滤无法给它打分。我的方案是内容推荐天然解决这个问题——新新闻可以立刻做文本分析、提取关键词、计算语义向量只要知道它属于什么主题就能推荐同时给新新闻一个热度加权在推荐列表里适当提升测试用户对它的反应冷启动不是技术问题是策略问题。想清楚“新用户来了先看什么”比用什么算法更重要。4. 集群环境搭建与大数据处理4.1 Spark集群搭建全过程毕设实验环境我建议直接搭在虚拟机里3台机器1主2从。操作系统用Ubuntu 18.04每个节点4G内存2核CPU。如果你计较好可以直接单机跑Spark本地模式但答辩时讲“集群”和讲“单机”的层次感不一样建议有条件还是搭集群。JDK和Scala版本选型这里有个关键版本匹配问题。Spark 2.4.x对应Scala 2.11/2.12Spark 3.x必须用Scala 2.12。我当时用的Spark 2.4.5 Scala 2.11.8 JDK 8这组版本经过了大量生产验证网上资料也最多出了问题好查。别一上来就追新毕设求稳比求新重要。集群配置步骤# 1. 配置hosts文件3台机器都做 192.168.1.101 spark-master 192.168.1.102 spark-worker1 192.168.1.103 spark-worker2 # 2. 配置SSH免密登录 ssh-keygen -t rsa -P cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys # 3. 配置Spark环境变量 export SPARK_HOME/opt/spark export PATH$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbin # 4. 配置spark-env.sh export JAVA_HOME/usr/local/jdk1.8 export SPARK_MASTER_HOSTspark-master export SPARK_WORKER_CORES2 export SPARK_WORKER_MEMORY3g # 5. 配置slaves文件声明worker节点 spark-worker1 spark-worker2启动集群/opt/spark/sbin/start-all.sh然后访问http://spark-master:8080查看Web UI能看到两个Worker节点在线配置成功。4.2 日志数据的清洗与ETL拿到原始用户行为日志后第一步是清洗。日志数据长什么样一般是前端上报的JSON类似{userId:102, newsId:3001, behavior:1, timestamp:2024-05-10 14:23:05, device:mobile}但实际数据里会有大量脏数据比如timestamp字段格式不统一有的带时区有的不带userId或newsId为空字符串behavior字段非法值不是1/2/3/4重复上报的同一条行为清洗流程用Spark SQL做import org.apache.spark.sql.functions._ val rawDF spark.read.json(hdfs://spark-master:9000/data/behavior/*.json) val cleanDF rawDF .filter(col(userId).isNotNull col(newsId).isNotNull) .filter(col(userId) ! col(newsId) ! ) .filter(col(behavior).isin(1, 2, 3, 4)) .withColumn(event_date, to_date(col(timestamp))) .dropDuplicates(userId, newsId, timestamp)这里有几个细节值得说去重不能只按userId和newsId要连同timestamp一起否则用户同一时间点一次新闻只保留一条可能把不同时间点的正常行为误删过滤空值用isNotNull然后再filter空字符串两步都不能少JSON数据里空字符串和null是两种不同的脏数据清洗后的数据要写回HDFS后续的离线推荐直接读清洗后的数据不要让算法和脏数据纠缠4.3 Spark调优的几点经验毕设阶段的Spark调优不需要做到生产级但至少要知道几个基本参数答辩时能说出所以然并行度设置spark.default.parallelism 200 spark.sql.shuffle.partitions 200这两个参数控制Shuffle时的分区数。默认值是200数据量在十万级以下可以保持默认如果数据量大而分区数少会出现部分Executor忙死、部分闲死的数据倾斜问题。内存分配Spark的Executor内存分为执行内存和存储内存。我处理新闻数据时把spark.memory.fraction从默认的0.6调到0.4因为我们多数任务是计算密集型的Shuffle操作需要更多执行内存缓存数据的需求相对少。这个参数不是越高越好要根据任务的实际情况调整。广播变量协同过滤需要计算新闻相似度矩阵这个矩阵不算大几千乘几千但每次任务都要用。我把它做成广播变量val bcNewsFeatures spark.sparkContext.broadcast(newsFeatureMap)广播之后每个Executor只保存一份拷贝避免每次Shuffle都传一遍能省不少时间。我实际测过一组数据优化前跑一次离线推荐任务大约需要12分钟优化后7分多钟。毕设答辩时能拿出这个对比数据说服力很强。5. 推荐引擎的实现全过程5.1 离线推荐流程实现离线推荐是系统的核心每天凌晨定时跑一次生成第二天的推荐候选集。我用Quartz做定时调度每天凌晨2点触发任务。整个流程分为4个阶段// 阶段1加载数据 val behaviorDF spark.read.parquet(hdfs://spark-master:9000/clean/behavior) val newsDF spark.read.jdbc(mysqlUrl, news, props) // 阶段2构建用户-新闻评分矩阵 val ratingDF behaviorDF .join(newsDF.select(id, publish_time), behaviorDF(newsId) newsDF(id)) .select( col(userId), col(newsId), when(col(behavior) 1, 1.0) .when(col(behavior) 2, 5.0) .when(col(behavior) 3, 8.0) .otherwise(2.5) .multiply(exp(-0.05 * datediff(current_date(), col(create_time)))) .as(score) ) // 阶段3ALS训练 val model als.fit(ratingDF) val userRecs model.recommendForAllUsers(20) // 阶段4存入HBase userRecs.foreachPartition { partition val table hbaseConnection.getTable(recommend_result) partition.foreach { row val put new Put(Bytes.toBytes(row.getAs[Int](userId).toString)) put.addColumn(Bytes.toBytes(rec), Bytes.toBytes(news_list), Bytes.toBytes(row.getAs[Seq[Row]](recommendations).mkString(,))) table.put(put) } table.close() }阶段2里的时间衰减因子是容易忽略但很重要的细节。直接按原始行为分数训练三个月前的点击和今天的点击权重一样推荐结果会偏离用户最近的兴趣。加上exp(-0.05 * 天数差)后老行为自然降权。5.2 实时推荐流程实现实时推荐用Spark Streaming消费Kafka里的用户行为数据每30秒处理一批更新的用户最近兴趣。核心逻辑val kafkaParams Map( bootstrap.servers - spark-master:9092, key.deserializer - classOf[StringDeserializer], value.deserializer - classOf[StringDeserializer], group.id - news_recommend, auto.offset.reset - latest ) val stream KafkaUtils.createDirectStream[String, String]( streamingContext, PreferConsistent, Subscribe[String, String](Set(user_behavior), kafkaParams) ) val behaviorStream stream.map(record { val json JSON.parseObject(record.value()) (json.getString(userId), json.getLong(newsId), json.getInteger(behavior)) }) behaviorStream.foreachRDD { rdd rdd.foreachPartition { partition partition.foreach { case (userId, newsId, behavior) // 更新用户在Redis中的实时标签 val key srealtime:user:$userId redisClient.hincrBy(key, newsId.toString, behavior match { case 1 1 case 2 5 case 3 8 case _ 2 }) // 设置过期时间保留最近10天的行为 redisClient.expire(key, 86400 * 10) } } }实时推荐的结果不直接给用户展示而是沉淀到Redis里。当用户刷新首页时推荐服务会合并三路数据离线推荐的候选集HBase、用户兴趣画像HBase、实时行为标签Redis。三部分加权求和得到最终推荐列表。加权公式最终分数 0.5 * 离线推荐分 0.3 * 兴趣画像匹配分 0.2 * 实时行为分这三部分权重不是拍脑袋定的。我做过一组AB测试离线为主、实时为辅的结构在点击率上最优实时权重太高会导致推荐列表波动过大用户以为系统抽风了。5.3 推荐列表的生成与过滤可别以为算出分数就完事了推荐列表生成还有个关键操作——过滤这里有个大坑我踩过。第一次测试时推荐结果里频繁出现已经下架的新闻用户点了404页面体验极差。后来加了三层过滤状态过滤只推荐状态为“已发布”的新闻时间过滤只推荐发布48小时内的新闻新闻和商品不一样用户不爱看三天前的旧闻重复过滤用户已经看过的新闻绝不二次推荐除非他主动搜索val validNews newsDF .filter(col(status) 1) .filter(col(publish_time) date_sub(current_date(), 2)) val finalResult scoredResult .join(validNews, scoredResult(newsId) validNews(id), inner) .filter(!col(newsId).isin(viewedNewsList: _*)) .orderBy(col(score).desc) .limit(50)这层过滤看起来简单但直接决定了用户对系统的第一印象。算法再牛推了个过期新闻用户就会觉得这个推荐系统不靠谱。6. 前后端实现与系统联调6.1 后端API接口设计后端我用Spring Boot写接口不多但每个都要能讲清楚。接口方法功能/api/news/recommendGET获取推荐新闻列表核心接口/api/news/hotGET获取热门新闻榜兜底推荐/api/news/{id}GET获取新闻详情上报浏览行为/api/behaviorPOST上报用户行为点击/收藏/分享/api/news/searchGET新闻搜索接ES/api/user/profileGET获取当前用户兴趣画像调试用推荐接口的逻辑GetMapping(/api/news/recommend) public Result recommend(RequestParam Long userId) { // 1. 从HBase读取离线推荐结果 ListNewsVO offlineRecs recommendService.getOfflineRecs(userId); // 2. 从Redis读取实时行为加分的新闻 ListNewsVO realtimeBoosted recommendService.getRealtimeBoosted(userId); // 3. 合并、去重、加权排序 ListNewsVO result mergeAndSort(offlineRecs, realtimeBoosted); // 4. 不足的部分用热门新闻补齐 if (result.size() 10) { result.addAll(hotNewsService.getHotNews(10 - result.size())); } return Result.success(result); }注意兜底逻辑很重要离线推荐结果可能只算出5条符合条件的新闻加上实时加权也可能不足10条这时候必须用热门新闻补位保证给用户的列表永远是满的。6.2 前端页面实现前端用Vue ElementUI做页面不多首页推荐流、新闻详情页、用户个人中心兴趣标签管理、管理后台新闻录入/用户管理/数据统计。首页推荐流的实现template div classnews-list el-card v-fornews in newsList :keynews.id classnews-item h3 clickclickNews(news){{ news.title }}/h3 p classmeta{{ news.category }} · {{ news.publishTime }} · 来源{{ news.source }}/p p classsummary{{ news.summary }}/p div classtags el-tag v-fortag in news.keywords :keytag sizemini{{ tag }}/el-tag /div /el-card /div /template点击新闻后前端要异步上报行为clickNews(news) { // 上报点击行为 axios.post(/api/behavior, { userId: this.userId, newsId: news.id, behavior: 1 }) // 跳转到详情页 this.$router.push(/news/${news.id}) }上报行为这个动作必须是异步且不能阻塞跳转就算上报失败也不能影响用户看新闻。这里可以做“埋点”的优化——上报失败的请求缓存到LocalStorage下次进入系统时补报保证行为数据的完整性。6.3 系统联调的注意事项前后端联调是我整个项目最痛苦的阶段问题层出不穷跨域问题前端跑在8081端口后端跑在8080端口Vue配置代理// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }HBase连接池后端每次请求都创建HBase连接连接数直接爆炸。后来用了HBaseConnectionPool每次从池子里拿连接用完归还。这个问题在本地测试时根本发现不了到了联调阶段高并发请求一来就暴露。时间格式统一前端传时间戳后端返回时间字符串前端再格式化展示。这个环节如果前后端约定不一致就会出现“2024-05-10”在页面上显示成“2024/05/10”这种问题。统一用yyyy-MM-dd HH:mm:ss格式前端不做二次转换。Redis序列化Spring Boot默认用JDK序列化存进去的Java对象是乱码。我统一改成了JSON序列化器排查看数据时一眼能看明白存了什么。几乎每个问题都是生产环境才会遇到的虽然烦但解决之后对真实项目的理解会深很多。6.4 新闻爬虫与初始数据准备推荐算法要有数据跑没有数据一切都是空谈。当时我爬了一批新闻数据做测试来源是几个新闻网站的公开RSS接口大概爬了5000条新闻。这里有个毕设特有的要求爬的数据要注意版权问题只能用于学习测试不能商用。爬虫用Python写简单抓取RSS源后解析import requests import xml.etree.ElementTree as ET def fetch_news(url): resp requests.get(url, timeout10) root ET.fromstring(resp.text) news_list [] for item in root.iter(item): news_list.append({ title: item.find(title).text, content: item.find(description).text, pub_date: item.find(pubDate).text, category: item.find(category).text if item.find(category) is not None else 综合 }) return news_list爬完之后存MySQL然后写脚本做初步的清洗和分词。这里要说一下爬虫数据质量参差不齐有的description标签里是一堆HTML标签需要正则剥掉有的标题和正文全是乱码编码要统一转成UTF-8。如果你不想爬虫可以用现成的开源新闻数据集比如GitHub上有一些中文新闻分类数据集几万条带分类标注的新闻文本跑推荐算法完全够用。7. 效果评估与性能测试7.1 推荐效果怎么评价毕设不能光做出来就完事得有数据证明系统有效。我用了三个指标离线指标召回率和准确率把用户行为数据按7:3切分70%训练30%验证。用训练集生成推荐列表看推荐结果中有多少出现在用户的真实点击里。准确率 推荐列表中用户真实点击的条数 / 推荐列表总条数 召回率 推荐列表中用户真实点击的条数 / 用户真实点击的总条数这两个指标在推荐系统里通常此消彼长。测试下来我的系统准确率在18.6%召回率在21.3%作为对比热门推荐的准确率约8.2%。虽然绝对值不高但相对提升很明显。如果能证明“推荐比乱推有效”答辩就过关了。在线指标点击率CTR用户看到推荐列表后实际点击的新闻数占推荐新闻总数的比例。我用AB测试的方式一半用户走推荐算法一半用户看热门榜。实测结果推荐组的CTR为12.8%热门组为6.1%推荐效果比热门榜翻了一倍。这个数据拿出来很能说明问题。7.2 系统性能测试性能测试我用JMeter模拟用户请求分别测试了后端服务在不同并发下的响应时间并发数平均响应时间错误率吞吐量50320ms0%156 req/s100680ms0%147 req/s2001120ms0.3%141 req/s5002450ms5.2%118 req/s超过300并发时性能明显下降瓶颈在HBase的RPC开销和MySQL的连接池上限。毕设系统能扛300并发已经足够真正的生产系统还会加Redis缓存层、异步消息队列不是一个量级。7.3 测试数据怎么造如果你的系统没有真实用户也得模拟出合理的测试数据。我用Java写了个数据生成脚本模拟600个用户、5000条新闻、5万条行为记录。关键点在于模拟的行为要符合真实场景用户点击行为服从长尾分布少数热门新闻被大量点击大多数新闻只有零星点击用户兴趣聚类喜欢科技的用户的点击主要集中在科技类而不是均匀分布行为时间有潮汐白天行为多凌晨行为少// 模拟用户兴趣 String[] categories {tech, finance, sports, entertainment}; MapString, Double userPref new HashMap(); userPref.put(categories[random.nextInt(4)], 0.6 random.nextDouble() * 0.3); userPref.put(categories[random.nextInt(4)], 0.2 random.nextDouble() * 0.2);模拟数据虽然不能完全反映真实场景但至少能让算法跑出合理的结果验证系统各个模块是通的。如果你的测试数据让算法跑出“人人推荐完全一样的新闻”那说明数据生成逻辑还不够合理。8. 常见问题与排查技巧8.1 Spark作业频繁失败报OOM内存溢出排查过程先看Spark Web UI上的Executor占用如果全部打满基本是数据倾斜——某个key的数据量远超其他key查看Shuffle读写的记录确认哪些Stage耗时最长数据量分布是否均匀试试加广播变量、增加分区数、改spark.sql.shuffle.partitions我遇到的实际情况是ALS训练时某个热门新闻被点击了2万次这个key在Join时产生了过度膨胀的数据。解决方法是把热门新闻强制过滤掉一部分只保留评分最高的前100条行为记录。这个操作虽然损失了一些数据但换来了稳定性和训练速度。8.2 实时推荐消费Kafka数据延迟越来越大Kafka的消费者组里的Lag持续增长说明消费速度跟不上生产速度。当时的原因是我在Spark Streaming的foreachRDD里直接访问Redis而Redis的单连接在高并发下成为瓶颈每次处理耗时过长。解决方法把Redis操作改成了jedisPool连接池同时把streaming.kafka.maxRatePerPartition设为10000限制每分区每秒消费的消息数避免突发流量压垮下游。8.3 Spark Streaming启动后不消费Kafka数据这个Bug当时排查了很久。Kafka topic是新建的没有数据可以消费Spark Streaming默认的auto.offset.reset是latest表示从最新位置开始消费但新topic的leader epoch记录为空导致消费者拿不到offset。解决方法先把auto.offset.reset改成earliest手动造几条测试数据验证消费链路通了再改回latest。这里有个很典型的案例Spark Streaming测试期经常觉得程序没反应实际上是因为没有数据推过来。可以先写个Kafka生产脚本手动发几条消息验证。8.4 HBase结果查询为空离线推荐跑完HBase里没有数据但Spark作业日志显示成功。最终排查发现HBase连接指向的是集群配置文件里默认的ZooKeeper地址而生产环境的ZooKeeper地址不同Spark作业实际访问了另一套HBase实例数据写到了别处。这个教训是写HBase的代码一定要确认Configuration里的ZooKeeper地址和端口配置正确最好用代码显式指定不要依赖默认配置。8.5 用户点击后推荐列表不变这其实不算Bug是“实时性”没有体现出来。实时推荐的表层逻辑是点击了新闻A30秒内推荐列表里应该出现与A相关的新闻。如果没变原因大概率是前端没有上报行为成功检查Network面板看/api/behavior是否返回200上报成功了但Kafka没消费检查Kafka的LagSpark Streaming把实时标签更新到了Redis但推荐列表的权重公式里实时部分占比过低0.2在分数排序上几乎没有影响第三种情况最容易忽略。我当时为了验证实时推荐效果把实时权重临时调到了0.5效果立刻显现。调回0.3左右但其实后续我手动选择了对热门新闻加权重的方式让用户点击对非热门新闻的排名提升更明显。8.6 排查问题汇总速查表现象可能原因排查顺序Spark作业OOM数据倾斜 / 分区数过少 / 内存配置不够看Web UI → 查Stage耗时 → 调参数Kafka消费延迟Redis访问瓶颈 / 消费速率限制看消费Lag → 检查下游耗时推荐结果为空过滤条件太严 / HBase连接失败 / 数据没清洗查日志 → 检查数据量 → 测试单条SQL推荐列表全是热门新闻冷启动兜底逻辑生效 / 离线推荐没跑检查画像表 → 看任务时间戳协同过滤分数全为NaN冷启动策略没设drop检查ALS参数9. 个人经验总结做个毕设和做一个真实的推荐系统差别很大但通过这个项目能把整套流程走一遍比刷1000道面试题都有用。我实际操作下来最大的体会是推荐系统70%的工作是数据和工程算法只占30%。真正花时间的不是在MLlib里调参而是清洗数据、搭环境、排查集群问题、联调接口。辛辛苦苦把算法写出来结果发现数据脏、环境崩、接口不通算法再好都跑不起来。给正在做类似毕设的同学几个具体建议先搭好数据链路再碰算法。只要确保从MySQL到HDFS到Spark到HBase的这条数据通路是通的后面调试算法就轻松很多。我一开始先折腾算法结果数据通了之后算法重写了一大半因为这个流程反了。做性能对比同一个推荐任务单机模式和集群模式的耗时对比手动给Kafka造数据跑通实时链路。这些数据都是能直接用在工作量展示和答辩里的。把遇到过的坑整理下来写成文档。答辩时老师问“你这系统有什么难点”直接说“我遇到了这些坑这么解决的”比背概念强一百倍。老师一听就知道你是真做了项目不是水出来的。如果你正准备开题强烈建议在这个方向投入时间。Spark和大数据生态是国内产业界最常见的技术栈新闻推荐又是经典的业务场景做这个项目等于一口气把大数据、机器学习、Web开发全串起来了。做完这个项目无论是找大数据开发的工作还是继续深造做推荐方向的研究都有实打实的东西可以拿出手。
企业数字化 ERP 产品动态
相关推荐
MES系统如何实现生产过程质量控制?核心功能与落地实践详解 车间里质量管理的朋友应该都有过这种经历:SPC图表做得再漂亮,不良分析报告写得再深刻,一旦产线上的过程数据靠Excel汇总、靠人工填报、靠下班后再录入,这些分析基本都成了"事后追悼会"。我参与过几个汽车零部件和电子制… · 2026/9/26 12:39:22
腾讯云CodeBuddy下载安装全指南:官方地址与实战解析 聊到"Codebuddy腾讯云代码助手下载地址"这个搜索词,我先说点实在的:我身边不少同事第一次找这东西,都是直接去百度搜"Codebuddy 下载",然后点进某个"软件下载站",装了一堆捆绑软件&… · 2026/9/26 12:39:22
金属加工油雾提取系统全解析:从选型技术到2026市场机遇 我进过不少机加工车间,最让人难忘的不是机床转速有多高,而是推开门那一刻扑过来的油雾味。夏天尤其明显,在CNC旁边站半小时,脸上一层黏糊糊的膜,喉咙开始发涩,衣服上全是金属粉尘和油渍混在一起的灰色印记。… · 2026/9/26 12:39:22
KytyPS5 GPU Tiler核心技术:PS5纹理分块格式如何在Vulkan上高效重建与渲染 KytyPS5 GPU Tiler核心技术:PS5纹理分块格式如何在Vulkan上高效重建与渲染 【免费下载链接】KytyPS5 PlayStation 5 emulator for Windows, Linux and MacOS 项目地址: https://gitcode.com/gh_mirrors/ky/KytyPS5
KytyPS5 是一款开源的 PlayStation 5 模拟器… · 2026/9/26 13:16:04
从大模型到智能体:agent-native架构设计实战解析 聊 agent-native 之前,先抛一个问题:你手里有没有那种“号称接入了大模型,但用户用两次就再也不碰了”的功能?我见过太多团队把聊天窗口塞进 App、把模型接口套在表单后面,就宣称自己在做 AI 应用,结果留存… · 2026/9/26 13:16:04
查重过了AI率挂了?TaoToken 统一 Key 接入 5 款 AI 检测工具实测,毕业之家双降真能救场? /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 13:16:04
Spring Boot 3 + Vue 3 交友平台全栈项目设计与落地实践 一个很典型的全栈项目:后端用 Spring Boot 3,前端用 Vue 3,做成一个交友平台系统。这类项目在各类毕业设计、个人练手作品里出现频率相当高,但大多数写出来都停留在“能跑通”的层面,离“能拿得出手”还有不小距离。我… · 2026/9/26 13:16:04
把Agent当第一公民:agent-native架构的系统设计与实践要点 最近几个月,我在技术评审会上反复听到同一个词:agent-native。创业者BP里写“我们是agent-native平台”,技术方案里写“用agent-native架构重构”,连招聘JD都开始找“agent-native工程师”。但每次我让对方把架构图摊开࿰… · 2026/9/26 13:15:58
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46