转载

谈我心目中的推荐系统架构

本文记录我对推荐系统架构的一些想法,限于本人水平和技术视野,肯定有不足之处,甚至一些错误,所以恳请大家指出问题,多多交流!

架构图

谈我心目中的推荐系统架构

图1  架构

图1大虚线内是线上服务,请求顺序用数字标识,不带箭头的连线表示通信是双向的,即请求与响应,它周围的各模块分别是:elk日志收集系统,监控系统,A/B实验,处理离线数据的hdfs+spark/tensorflow,处理实时数据的kafka+storm/flink和物品管理。

在线服务内部架构

在线服务的内部如大虚线框内所示,这可能和你遇到的不同,甚至和你想像的也差异很大,但不必奇怪,一来架构因人而异,二来业务规模和团队结构的不同导致服务架构也不一样。 规模 较小时, 整个 线 框完全以 程多线程实现。当规模变大了就需要服务拆分,例如拆分出索引服务和排序模型服务。这时就要考虑如何拆分才能得到高效、易管理、能较好适应团队结构的“微服务”架构。本文记录了个人对此的一点想法。

1. 入口服务与画像

这两个服务功能简单负载小,合并还是拆分对系统性能影响不会太大,具体可以根据实际情况而定,如团队,业务复杂程度等。

2. 排序模型+规则调整

个人倾向于将这两个模块放到一起。随着深度学习在推荐中的应用,排序模型变得庞大,有必要独立为一个服务,而规则调整的处理 一般较简单 ,如同一来源的内容只保留一份,同一分类的内容只保留一份等等,资源消耗也比较小;另一方面,这两个都需要“正排信息”,将二者放到一起后只请求一次正排即可,这可以有效地降低网络开销。因此合并二者能对各自影响有限同时又有效降低网络开销,是非常合理的。

3. 召回服务

实际中大家对召回的处理不太容易达成一致:有的人以微服务的思想,主张把每个召回独立为一个服务;有的人用动态库的方式以方便算法团队频繁更新;也有的将所有召回编译成一个服务,再辅以动态库。

究其原因主要是这几个方面:

a) 服务数量:召回有很多策略如协同过滤,兴趣,热门,兴趣扩展以及各种根据业务特点的策略,所以如果每个召回都是一个单独服务的话,服务数量就比较大,维护成本是比较高的。

b) “流量放大”:召回要访问索引服务及展示去重。如果召回是独立服务,那么一次请求中每个召回都要单独访问索引及去重,这严重地增加了网络开销和索引服务的负担。

c) 开发上线便利性:如果是单独的召回服务,开发上线几乎互不影响,编译成一个服务后就需要一些同步。

我个人主张:把所有召回放到一个服务中,通过配置确定哪几个召回工作;建立完善、便捷的开发、测试和上线环境,保证上线新功能和更新策略的便利性,以此弥补c)中的同步问题:试想如果有了持续集成环境和完善的测试、灰度环境,每个召回在开发、测试通过后就把代码提交到某个分支,然后每天定时自动从该分支拉最新代码上线,而上线只不过是在界面人工点几下按钮。这样做完全能享受到单个服务的所有好处,同时完全不必担心任何开发上线的同步问题,即使上线频繁一些也不会成为负担。只有过多的人为介入才会产生负担。

上述主张还有一些好处:方便统一处理召回结果;强制所有代码入库,保证库中代码和线上完全一致(因为是从固定分支拉代码上线的)。

总之微服务的划分需要恰到好处才能让工作更舒服些。

4. 索引服务与正排信息

召回,排序模型和规则调整都需要索引或正排,所以独立为一个服务集中维护索引信息比较合适。推荐系统很重要的一点是“快”,所以索引还必须有实时更新能力。

召回一般只需要部分正排信息用来简单地计算分数,而模型排序和规则调整会需要正排中较多的信息,所以也可以考虑将正排和索引服务拆分开,前者保存完整正排信息,后者保存“摘要”。

总体来说,这个架构设计基本是一个从上到下的一个类似“树形”的结构,请求从上流动到下,响应从下流动到上。大家也可以考虑下“串行”和“星形”结构,比较它们的优劣。该架构还充分考虑了带宽问题,特别是将需要正排的排序模型和规则调整放到一起,将所有召回放到一个服务中。最后,召回不应该有特别复杂的在线计算,如果有也应该由算法团队迁移到线下,线上大部分过滤及粗排也要放在索引侧,这样召回就不需要拉取正排,就能降低带宽消耗、优化系统响应延时。

微服务是个好思想,但其一个劣势就是增加了服务间调用消耗。不合理的服务划分导致的问题甚至比带来的好处多得多,这时再如何优化单体服务也是事倍功半,毕竟局部的优化不能彻底解决架构的问题。因此本节最后有必要分析上述架构的网络流量情况:

a) 图中1,2,3三步的网络负载主要是用户画像和一些系统参数, 网络开销 不大

b) 第4步的请求中也主要是画像和参数,响应是最终召回的物品id列表,一般 在几百的量级(如果不是这样,那工程团队的日子非常难过), 网络开销不大 。不过在这个架构中,即使id列表比较长,主要负担也是在排序模型而不是网络开销,毕竟这个列表只在第4步的响应中出现。

c)第5步的请求也是画像等参数,响应是什么及网络开销大小就要看召回和索引的功能划分。 如果索引负责粗排和过滤等,返回给召回服务的是物品id列表,那么 网络开销不大; 而如果粗排和过滤放在召回侧,那召回就要请求正排信息,这时的 网络开销是非常可观的 。这就是我主张索引服务负责粗排和过滤的原因,另外一个理由是可以简化召回逻辑。

d)第6步,排序模型和规则调整的请求是id列表,响应是完整的正排信息。单条正排数据是比较大的,但不要忘记最终召回的只有数个id,再辅以索引服务提供的缓存,这个 网络开销也不大

由此可见,这个“微服务”架构在享受的各种好处之后,还能巧妙地避开网络开销陷阱。这是降低系统延时、提升推荐系统时效性的关键。

elk日志收集

以elk收集日志监控各项指标可说是开源的标准的日志处理方式,如果没有特殊情况实在没有必要自己搞一套日志流程。只要按一定方式打印日志,elk就可以聚合日志并灵活地建立索引,监控各项指标,整个过程几乎不需要侵入服务。

elk可以很方便地跟踪一次请求的全流程日志,这点对排查问题十分重要。进一步,如果经常需要确认为什么一篇文章没有被推荐出来,通常需要人工分析日志,有了elk后建立简单分析流程可以快速解决这个问题。更进一步,利用elk完全可以对整个推荐流程进行日志分析,如分析推荐理由,过滤原因,统计推荐效果等。当然对于体量大的业务日志量会非常惊人,这里只要抽样部分日志即可。

日志是个宝,深入挖掘一定会有很多可做的事情。

kafka+storm/flink实时数据处理

“天下武功,唯快不破”,推荐系统尤其如此:必须尽快地更新画像、排序模型。幸运的是已经有开源方案,不需要自己搞一套:通过kafka实时回传展示、点击等数据,接收方通过storm/flink订阅、实时处理各种数据。

这里有一个问题,就是展示和点击的发生时间相隔较远,在kafka中消费点击时早已消费了展示,不能同时拿到这两种数据,不方便计算点击率一类的指标。通常的场景都 需要累积一定的数据量,所以 一种做法是应用方自己消费并关联一定时间窗口内的展示和点击 我还想到的一种解法是,增加一个Elast icSearch集群,在向kafka回传展示数据的同时也向 ES中输入数据,拿到点击数据时再去ES中请求展示结果,从而将二者 关联起来。 然最好的办法应该是使用flink,因为它提供的各种“窗口”能有效解决这个问题。 一般我们遇到的问题开源界早有方案。

总之,利用kafka+flink可以快速地更新排序模型和画像信息,让推荐系统对用户行为快速及时地产生反馈。

监控

有服务就需要监控。这方面也有很多开源工具可以用,如prometheus,sentry,opentracing等等。要监控的东西也很多,服务可用性,调用链,数据正确性,cpu,内存,硬盘,网络,流量等等。

监控不是万能的,也不应该是万能的。基础的监控非常必要,但实际中有些人自觉不自觉地过度依赖于监控而忽略了 从根本上 解决问题,比如忽略了上线前的验证、测试和灰度,将本应上线前发现的问题留给监控。总是将问题简单地归结为监控不够而不深入地解决本质问题也算是一种“懒政”了。

A/B实验

“如果你不能量化它,你就不能改进它”。强大的A/B实验工具是做好推荐的关键。A/B实验工具几乎都是按google的分层实验论文设计的。要做好推荐,这方面还是值得研究研究的。

多说一句,与些类似,后台开发必须掌握性能检测工具,只有有能力快速定位性能瓶颈,才能解决它。

hdfs+spark/tf

实时数据通过流式计算处理,离线的大批量数据就要靠hdfs,spark/tensorflow这类东西了,如天级更新用户画像和排序模型等等。

物品管理

没有好的内容,架构和算法再先进也没有用,所以必须重视内容管理。内容来源超出了本文范围了。物品/内容经过处理后就被推到索引服务中,然后被召回、被分发给用户。内容管理与索引服务的衔接需要注意以下两点:

1. 时效性。索引服务是实时的,所以内容管理也应该是实时的,这样整个系统才有实时性。

2.数据多样性。内容有多种,常见的有视频和图文,也可能有话题或其它各种形式,对它们的处理既有共性又有差异,所以在实现上最好是配置化。拷贝粘贴只会产生垃圾代码。

评估

已经有了A/B实验环境,为什么还要评估?A/B是线上行为,评估是线下行为。个人认为不能简单地只看线上指标而忽略线下评估,比如新加的内容源质量如何,新的策略效果如何,用户行为日志的分析等等,都需要线下人工介入分析。只有评估过的内容和策略,我们心里才能对它们有底,线上指标好不一定代表达到期望的效果。同样,用户行为分析很难,但“重放”用户对系统的使用过程,才能更了解用户和我们的系统性能。

写在最后:是什么决定着架构?

架构是各方面妥协下的功能划分 。影响架构的因素很多, 如业务场景,技术选型,研发运维环境,开发人员素质和基础设施 等等, 这里想从研发环境和团队结构这两个角度谈一点个人看法 。

下面开始我的总( 感( 发) 慨( 泄) )。

1. 研发环境

一套完善的研发环境至少包括开发,测试,预发布(灰度、准入)和线上四类环境。重要的一点是,这四套环境要完全 隔离: 在开发环境写代码,打标签放到测试环境测试,通过后放到预发布环境用真实流量跑足够时间,确认cpu/内存/通信等等各方面没有异常后才可上线。遇到不通过的就返回上一阶段。此外还要有配套设施如持续集成和自动上线流程,从而最大程度地减少人工参与。

初看之下这似乎只是提高了 研发效率 ,和架构没有太大关联,其实不然。如果一个模块上线流程繁杂或者改动频繁且线上经常出问题,那么在没有完善的研发环境时,一般选择把该模块独立成单独服务,即使这样将大大增加整个系统的负担。但如果有了“开发、测试、灰度、上线”四大步骤和相应配套,我们就能步步为营、信心满满、高效地维护这个模块,这时,原来的模块划分一定就有更合理的选择!

以大家熟悉的召回为例。如果研发环境不完善,那么稳定性可靠性是没有保证的,这时合理的做法是把每个召回搞成独立服务,但问题也很明显:服务数量庞大;召回内容重叠,增加系统负担;“放大”流量,白白地浪费公司资源。面对这种情况,将所有召回融合成一个服务是比较合理的,尤其是能解决召回数量庞大问题。具体开发流量是:召回在各自分支开发,测试通过后将代码提交到预发布分支;定时(或由专人)拉预发布分支最新代码编译发布到预发布环境,通过验证后就可以推到线上分支;定时自动拉取线上分支代码编译、上线。

由此可见研发环境对架构的影响是多么的明显和巨大!

2. 团队结构

曾经遇到的一些问题让我深感架构极大地受团队结构的影响,后来知道早有一个叫“康威定律”的东西。团队经常分成算法和工程两部分,理想的情况应该是算法和工程 以“数据”为接口 ,即线上完全地由工程团队负责,算法团队在线下生成策略数据,这样互相影响最小。虽然可能会有些出入,但我想大的原则应该如此。一旦这个接口模糊了,算法团队也负责很多线上服务,工程团队在设计架构时就必须考虑算法团队的感受,然后悲剧就开始了:产生一个接一个的怪胎,非主流,不伦不类。

不管架构如何受各种环境的影响,最主要的还是主观能动的人,只有扩展了自己的视野和水平,才能知道什么是美好的架构,才能突破现有环境,向美好的方向努力。

关于架构可谓“前人之述备矣”,以上仅仅是个人一点想法,不一定对,欢迎交流。

原文  https://mp.weixin.qq.com/s/BBpKkbnRo8u4rZIkRNgZTA
正文到此结束
Loading...