资讯动态

大数据推荐系统实战:Hadoop+Spark+Kafka民宿推荐架构解析

发布时间:2026/10/21 2:50:24 来源:尧图企业网站定制
1. 项目背景与整体技术选型做民宿推荐系统这个项目最开始并不是为了赶时髦硬凑大数据组件而是真有一个很现实的痛点民宿数据量一旦上去单机跑的推荐脚本根本扛不住而且业务方既要看“猜你喜欢”的推荐结果又要看“周边热度榜”这种全局统计还要在后台实时看订单流转和用户行为。如果只用一个普通的Web后端加MySQL流量稍微上来接口响应就开始飘更别提离线训练协同过滤模型时对CPU和内存的摧残。所以这套系统的技术选型逻辑很直接数据分层处理离线和实时分开走。Hadoop负责海量历史日志的离线清洗和存储Hive负责把清洗后的数据做成结构化数仓Spark负责在上面跑协同过滤训练和离线预测Kafka则专门承接在线产生的用户行为事件让实时推荐链路能转起来。整体架构可以概括为“离线批处理实时流处理”的双通道两个推荐算法分别在两条链路上输出结果再通过一个统一的服务层对外提供接口。这套方案的直接收益是离线任务可以凌晨定时跑白天不影响在线业务实时链路延迟控制在秒级用户刚点完一个民宿几分钟后刷新页面推荐列表就能出现相似房源。对于民宿这种“短决策周期、强地域属性、高个性化需求”的业务场景这套架构是够用的而且每个组件之间职责清晰出了问题也比较好排查。这里顺带说一句为什么中间层一定要选Hive而不是直接拿Spark SQL替代。Hive在纯离线数仓场景下依然是性价比最高的选择它的元数据管理、分区修剪、与Hadoop生态的契合度都经过大量生产环境验证。而且后续要做可视化报表Hive表可以直接被BI工具连上写SQL就能出图比Spark临时算完再导出的方式要省事得多。2. 双推荐算法的设计动机与核心逻辑2.1 为什么是“双推荐算法”而不是一个通用模型首先明确一下“双推荐算法”在这套系统里的定义它不是同一个模型换参数跑两遍而是两条完全不同思路的推荐链路——基于协同过滤的离线推荐和基于物品关联热度与用户实时行为的流式推荐。协同过滤解决的是“个性化”问题。用户A和用户B历史上浏览、收藏、预订过相似的民宿集合那么A喜欢的新房源B大概率也有兴趣。这是典型的UserCF思路。反过来民宿X和民宿Y经常被同一个人收藏那么当用户看了X之后系统就应该推Y这是ItemCF。离线推荐用的是ItemCF因为民宿数量相对用户数量少得多物品间相似度矩阵的计算量可控而且民宿这个品类的用户兴趣变化快物品的关联关系比用户的聚类关系稳定得多。实时链路则解决“新鲜感”和“时效性”问题。比如台风刚过某海滨城市的民宿价格下调用户正好在刷那个城市系统就要在几分钟内把降价房源推到前面。协同过滤模型不可能这么快反应因为它依赖的是历史统计冷启动阶段甚至连用户行为都没有所以需要另一套基于事件驱动的实时推荐逻辑。这两条链路最终在推荐服务层做融合。融合策略不是简单加权而是分桶实时推荐结果有较高置信度的场景直接覆盖比如用户刚搜索过某个商圈实时结果不足时回退到离线协同过滤的结果两边都有结果则按照业务规则做重排比如过滤掉用户已经预订过的房源、过滤差评率过高的房源、提价过滤等。2.2 协同过滤的工程化细节离线链路里最关键的是物品相似度矩阵的计算。这里选的ItemCF相似度公式用的余弦相似度但实现上做了两个优化。第一个优化是用户行为加权。不能把浏览、收藏、预订当成同样的权重否则数据会被大量无意义浏览污染。我用的权重策略是浏览记1分收藏记4分下单记8分支付成功记10分。这个权重不是拍脑袋定的是根据业务目标反推的——系统最终要提升的是支付转化率所以支付行为必须占主导权重。第二个优化是相似度矩阵的稀疏处理。民宿总数如果到几十万量级物品间两两计算相似度就是几十亿对直接算肯定不行。所以先做了一个热门度截断只保留90天内有过交互行为的民宿再对每个民宿只取与之共现次数最高的Top 200个邻居计算相似度。这样矩阵规模直接缩小几个数量级而且去掉了长尾噪声。相似度计算在Spark里跑的时候核心代码就几百行。数据源是Hive里的一张用户行为事实表按用户ID做分组把每个用户交互过的民宿列表拉出来两两组合生成“物品对”。然后对物品对做聚合统计共现次数和加权得分。最后算余弦相似度把结果写成Hive表供离线推荐查询。这里有个很容易踩的坑如果直接用用户ID做groupByKey某个头部用户交互过上万套房源单条记录就有上万个物品对直接导致数据倾斜。解决方式是对物品对做前缀分区也就是按照物品对中的第一个物品ID加盐分桶让大用户的数据分散到多个分区里处理而不是挤在一个task里。// Spark 离线 ItemCF 相似度计算的核心流程简化示例 val behaviorDF spark.table(ods.user_behavior) .filter($dt bizDate) .select(user_id, item_id, action_weight) val itemPairDF behaviorDF .join(behaviorDF.toDF(user_id, item_id2, weight2), Seq(user_id)) .filter($item_id $item_id2) .select($item_id.as(item_a), $item_id2.as(item_b), ($action_weight $weight2).as(pair_score)) val simDF itemPairDF .groupBy(item_a, item_b) .agg(sum(pair_score).as(co_score)) // 此处再接余弦归一化2.3 实时推荐链路的Kafka接入设计实时链路的数据源是前端埋点的行为事件。用户浏览了哪个房源、收藏了哪个房源、搜索了什么关键词、下单了哪个房源全部封装成JSON经过Nginx打到Kafka的topic里。Topic按事件类型分开比如user_view、user_favorite、user_order分区数按流量预估设置成12个保证Producer写入和Consumer消费的并行度。实时推荐服务用Spark Streaming读取Kafka窗口设在5分钟。在这个窗口内统计每个用户的行为序列并结合当前城市、入住日期等上下文信息实时计算一个“热度候选集”。候选集的构成包括该城市当前最热房源、与用户最近查看房源同商圈的房源、以及被相似用户高频收藏的房源。这套逻辑说起来简单但实时计算有个天然难点窗口内的数据量不够会导致候选集太窄。所以我在实时链路里加了兜底策略——每一个用户都保留一份最近30天的行为画像在Redis里当5分钟窗口内产生的实时信号不足时就取画像中的历史偏好来扩展候选集。Redis的key可以采用user:profile:{userId}value是一个包含偏好商圈、偏好价格带、最近交互房源ID列表的JSON实时任务和推荐服务都能读写这份画像。Kafka消费者的提交方式也需要特别注意。如果用自动提交偏移量任务重启时会丢数据如果用处理完再手动提交又会降低吞吐。我的做法是先关掉自动提交在foreachRDD里处理完业务逻辑后再提交偏移量并且在提交前把偏移量同时写入Kafka的__consumer_offsets和一个外部存储比如HBase这样即使任务异常重启也能从外部存储找回上次处理的位置。2.4 Hive可视化从数仓表到前端图表Hive在系统里不只是给Spark提供训练数据还承担了所有业务报表的底层支撑。民宿运营方最关心的几个面板每日订单量趋势、热门城市民宿价格分布、用户来源渠道占比、收藏转化率漏斗。这些指标全部通过Hive SQL从明细表汇总到汇总表再用可视化工具连接HiveServer2发布成图表。这里有一个很重要的实践建议不要用可视化工具直接查原始明细表一定要先建好汇总表不然一次查询扫全表Hive的MapReduce任务可能要跑几分钟前端图表早就超时了。我建了三层表结构ODS层原始行为日志、订单流水按天分区不轻易改动DWD层清洗后的明细事实表字段规范枚举值统一ADS层面向应用的主题汇总表每个指标一张表行数控制在几千行以内。以“城市热度排行”为例ADS层表结构大概是CREATE TABLE ads_city_heat ( dt STRING COMMENT 日期分区, city STRING COMMENT 城市, pv_cnt BIGINT COMMENT 浏览人次, fav_cnt BIGINT COMMENT 收藏次数, order_cnt BIGINT COMMENT 下单数, gmv_amt DECIMAL(12,2) COMMENT 成交金额, heat_score DOUBLE COMMENT 热度分, update_time STRING ) PARTITIONED BY (dt STRING) STORED AS PARQUET;热度分计算公式为heat_score pv_cnt * 0.3 fav_cnt * 0.5 order_cnt * 1.2 gmv_amt * 0.02。这是业务侧给出的经验公式虽然不复杂但能直接反映民宿运营的核心目标——既要有人看也要有人下单。可视化环节除了出图还要关注一个容易被忽略的问题HiveServer2的并发连接数。BI工具每刷新一次图表就要连接一次如果团队里几十个人同时看板HS2很容易被打挂。解决思路是加一个查询代理层比如用某个开源SQL网关再接一层连接池或者把BI工具的报表结果定时同步到MySQL/Redis里前端只查缓存只有缓存过期才去查Hive。3. 系统架构分层与各组件分工3.1 数据流向全景整套系统的数据流向可以总结为一条主链路和一条旁路。主链路是离线批处理链路Nginx和业务数据库中产生的日志与订单数据通过Sqoop和Flume定期同步到Hadoop的HDFS上。原始日志落地之后再加载到Hive的ODS层表中然后按业务逻辑清洗到DWD层。Spark每天凌晨定时任务开始跑先读取DWD层的行为数据训练或更新协同过滤模型再对全量用户生成离线推荐结果结果写回Hive和Redis。推荐服务启动时从Redis加载自己的推荐列表用户请求进来直接命中。旁路是实时流处理链路前端埋点事件直接进KafkaSpark Streaming从Kafka消费经过窗口聚合和画像更新把候选集写入Redis实时推荐服务从Redis读取候选集与离线结果融合后返回给前端。两条链路的数据最终都会汇总到ADS层供可视化大屏和运营看板使用。Hive在这里既是数仓存储也是离线推荐和可视化统一的“数据底座”。3.2 关键组件配置要点组件的部署细节直接决定系统稳定性这里列几个我在实际项目中调过的参数供参考。HDFS方面NameNode的堆内存要按文件数量规模来配。如果文件数超过5000万默认的4G堆内存肯定不够建议至少8G以上。DataNode的存储目录不要用系统盘优先挂载独立数据盘。数据块副本数保持默认的3但如果你只是测试环境可以改2省一半空间。Spark方面离线训练任务的Executor内存和核心数要匹配。我这里集群是每台机器16核64GExecutor内存给了8G核数给了4个这样一台机器能起4个Executor资源利用比较均衡。Shuffle分区数默认200处理几千万级物品对数据时建议改成2000不然每个分区数据量过大shuffle会OOM。Kafka方面最重要的参数是log.retention.hours。实时链路的原始行为日志如果全部保留磁盘很容易被打爆。我个人设的是保留24小时因为下游实时任务当天就会消费完更久的历史可以从Hive里找回来。另外Kafka的message.max.bytes要调到10MB因为民宿的详情JSON里包含多张图片URL、经纬度、设施列表一条事件可能超过默认的1MB限制。Hive方面最常见的坑是小文件过多。如果每天同步日志时不提前做合并HDFS上会堆积上百万个小文件查询时NameNode压力巨大。我的解决方案是在Hive表定义时就设置TBLPROPERTIES (orc.compressSNAPPY)存储格式用ORC并且在每次ETL任务后用一条INSERT OVERWRITE把当天分区重新写一遍自动合并小文件。3.3 Hive与Hadoop的存储规划表的分区策略直接决定查询效率。民宿项目里所有表都按照dt天分区部分大表增加city二级分区。比如订单事实表一级分区是dt二级分区是city这样运营方查“某天某城市的订单情况”时Hive能够通过分区裁剪直接跳过无关数据。另外表的文件格式我全部用了ORC而不是Parquet原因在于ORC在Hive生态里的原生支持更好尤其在做聚合查询时ORC自带的轻量索引能减少大量IO。如果你用ParquetSpark读写可能更顺手但说到Hive查询性能ORC还是稳一点。4. 项目实操过程中的真实踩坑记录4.1 数据倾斜那个半夜跑不完的Spark任务这个坑几乎每个做大数据的都遇到过。离线协同过滤第一次上线时凌晨的定时任务跑了四个小时还没结束后来定位到是有几个头部用户一个人一个月交互了几万套房源导致物品对聚合的某个key数据量爆炸单个Executor直接算到OOM任务卡死。解决方式前面提到过加盐分桶加前缀分区。我对物品ID做了取模加盐把大key拆成多个子key分别聚合然后再汇总。这样处理后任务从四个小时降到四十分钟。这是整个项目里性价比最高的一次调整没有之一。4.2 Kafka重复消费推荐列表为什么突然全变了有次测试发现Kafka消费者在重启之后部分用户收到的实时推荐结果出现了重复项排查下来是偏移量提交时机不对。当时代码是先写Redis再提交偏移量但Redis写入成功的ACK和Kafka偏移量提交之间没有做事务关联一旦进程在两者之间挂掉重启后就会重复消费之前的窗口数据把同样的房源再次写进Redis。修正方式是把Redis写入做成幂等操作重复写入只保留最新值同时把偏移量提交改成先处理完所有写入再统一提交。以后凡是遇到Kafka相关的实时任务我都会在代码里加上一句注释偏移量提交之前下游所有操作必须幂等。4.3 Hive查询慢到怀疑人生开了谓词下推也没用我一度以为Hive查询慢是集群资源问题后来发现是表结构设计有缺陷。ADS层的热度汇总表最开始没有按城市分区每次查一个城市的热度都要扫描全表。后来把分区策略改成PARTITIONED BY (dt, city)查询从三分钟降到了十秒以内。这个案例让我明白了Hive的查询优化分区设计远远比SQL写法更重要。4.4 可视化图表刷新卡死HS2连接数被打满某运营大促活动期间BI报表一刷新就报错后台日志显示HiveServer2的会话数达到上限。因为十几个运营同时开着看板每个图表每5分钟自动刷新一次HS2默认只能同时处理最多几百个请求直接被拖死。临时处理方式是提高HS2的hive.server2.thrift.max.worker.threads参数但治本的办法还是把高频图表的结果主动推送到Redis设置5分钟过期BI只查Redis不到5分钟的刷新直接返回缓存数据HS2压力瞬间消失。问题现象根因解决方式Spark任务凌晨跑不完数据倾斜头部用户key堆积加盐分桶、前缀分区拆解大keyKafka重启后重复推荐偏移量提交与下游写入非原子幂等写入处理完成后统一提交偏移量Hive查询几分钟无响应分区字段没有下推到查询ADS层表按dtcity双分区可视化刷新连接失败HS2连接数被打满高频图表结果缓存到Redis过期才查HS24.5 双推荐系统评估到底怎么算效果推荐系统上线后不能只看“能跑”要做效果评估。离线模型用F1和NDCG做离线评估比较容易但业务方更关心的是线上转化率。我这里做的是A/B测试实验组走双推荐融合链路对照组只走离线协同过滤。跑了两周的数据实验组的民宿详情页点击率提升18%收藏转化率提升12%下单转化率提升8%。单独看好像不明显但放在一个月几十万订单量级下这个提升带来的GMV增长很可观。5. 项目部署上线与运维要点5.1 从单机到集群的迁移教训一开始为了快速出Demo所有组件装在一台开发机上能跑通流程就万事大吉。但真要准备上生产单机模式连配置都改不下去因为Hadoop默认的副本数是3单机只有一份数据每次启动都会报警。所以建议如果决定做完整的民宿推荐系统至少准备三台机器一台做NameNode和ResourceManager节点另外两台做DataNode和NodeManager节点Kafka和Hive可以部署在资源相对空闲的节点上。小规模集群可以用Ambari或者手动装如果机器数量超过五台建议直接用现成的HDP/CDH发行版虽然开源版停止更新了但稳定性比社区版要好装很多。5.2 定时调度与监控告警离线任务用Azkaban或Airflow调度都行。我的项目里用的是Airflow因为DAG可以清楚表达每个任务的依赖关系。每天凌晨两点开始同步日志三点跑ODS到DWD的清洗任务三点半跑协同过滤训练四点半生成离线推荐结果五点半刷ADS层汇总表。每个任务结束之后检查日志中是否有Exception关键字如果有就触发短信告警。实时链路侧的监控也不可大意。Kafka的消费堆积情况必须纳入监控消费者组lag超过阈值就告警否则用户行为事件一直积压实时推荐会沦为摆设。我用的是PrometheusGrafanaKafka的JMX exporter暴露指标Spark Streaming任务自己上报Lag信息看板上一目了然。5.3 资源预估与成本权衡如果你准备照着这个方案做生产级系统资源预估参考如下民宿业务日活10万、日行为日志约三千万条每条约0.5KB一天原始数据约15GB保存90天的话Hadoop存储需要约1.4TB三副本后约4TB。集群四台机器就够了每台32GB内存、8核CPU、2TB磁盘基本能扛住。如果日活到了百万级批量计算建议引入Spark On Yarn的资源队列隔离实时任务和离线任务分开调度不然离线任务一跑实时推荐就卡顿。6. 这个项目还能往哪个方向扩展这套架构搭完之后后续扩展方向其实很多。第一个方向是引入更多样的用户特征。目前协同过滤用到的还是行为数据如果能加入用户画像标签比如出行目的商务出差还是亲子游、常住城市、消费水平就可以把冷启动问题再往前推一步新用户进入系统后不用等行为积累直接用画像匹配相似人群。第二个方向是引入深度学习模型。双推荐算法里协同过滤负责召回实时热度负责兜底但排序层面目前还是规则重排。后续可以用一个深度排序模型比如Wide Deep或DeepFM把几百个排序特征接进来替代人工规则推荐转化率应该还能再上一个台阶。第三个方向是可视化层增强。目前Hive可视化主要是运营看板后续可以加入用户行为轨迹的实时地图展示用户从浏览到下单的完整路径。这个需要把Kafka里的地理信息拿出来结合某个地理索引引擎做实时渲染技术上是完全可行的。第四个方向是把流式计算框架从Spark Streaming迁移到Flink。Flink在事件时间处理、精确一次语义上比Spark Streaming更顺手如果以后要做分钟级的实时促销定价Flink会更合适。但迁移成本也不小现有链路稳定性如果没问题可以先在旁路试验等验证充分了再全量切换。写在最后做这个民宿推荐系统最大的体会是所谓“大数据项目”真正的难点不在于把组件装起来而在于搞清楚每个组件在这个业务场景里到底该承担什么角色。Hadoop不是用来炫技的它是为了让你几十亿条历史行为数据算得动Kafka也不是非用不可它是因为用户行为每天都在实时产生必须要有一个削峰填谷的管道协同过滤算法本身都写在教科书里但真正让它发挥价值的是权重设计、数据倾斜处理、冷启动兜底这些课外的功夫。如果你也准备做一个类似的推荐系统我的建议是先把数仓模型设计好把自己业务里的行为事件定义清楚再动手搭环境。否则组件堆得再多数据流理不顺一样跑不出靠谱的结果。希望这篇复盘能帮你少走几步弯路祝顺利。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑