“谈我心目中的推荐系统架构”是对架构的详细说明,本文是从另一个视角思考 系统架构的总结。
为了对比重新贴了之前的架构如图1,按逻辑分层的架构如图2。
图1 系统架构图
图2 按逻辑分层的架构图
所谓“三足鼎立”就是图中的离线层,近线层和在线层:
在线层:几乎对应图1大虚线框内的部分,直接处理线上请求。
近线层:图1中处理实时数据的的部分。
离线层:图1中使用spark/tf处理天级或更长周期数据的部分。
互联网就是新名词层出不穷,“近线”就是我最近才听说的,也不知道什么时候由谁创造出来的。但这个词还挺贴切。另外这种分层表达的方式也不是我原创的,但我以前没有太重视,直到最近遇到一些实际情况才醒悟分层有利于整体的思考。后面还将以一些实例说明“越层”带来的问题。
好处至少有一个,就是更有利于思考如何让“三足鼎立”达到平衡,让推荐系统架构更合理,运行得更顺畅。这个问题似乎不值一提,因为几乎没有人蠢到把离线任务搬到在线,反之亦然。但总会有人因为缺乏经验或思考而犯错,当没有人站在全局把控架构时,情况会更严重。把离线和在线之间搞乱的情况毕竟少数,但有了近线层后就不一样的。有的架构没有这一层,这种架构几乎不可能是实时的(除非系统数据量小,能很快更新)。有的架构这层不完善,虽然像模像样也能做些实时任务,但又把本可以放到近线层的任务放到了在线层,白白地浪费了线上资源,接踵而来的是线上耗时严重、超时报警。
忍不住插一句:优化有很多级,从指令级,到多线程,到算法,一直到系统架构。如果系统架构出了问题,还妄想在模块级优化不就是徒劳吗?这时该优化的不是系统而是思维吧?:)
各层功能划分的一个原则
上面的分层图上窄下宽,这是有意这样画的—越往上层对耗时要求越严格,例如离线层耗时一般是小时级、天级;近线层可能是ms级,秒级,也可能分钟级;在线层一般是几十到几百ms。所以一个功能划分原则就是尽量“功能下移”,能放到近线、离线的就不要放到在线,这样就能保证线上逻辑简单运行高效,也有利于算法团队和架构团队的分工,就是说算法团队负责在离线和近线生产数据,架构团队负责在线服务的稳定。这也暗合前文说的算法和架构团队以“数据”为接口的思路。
下面看些实例。
ICF指物品协同过滤,是最基本的推荐算法之一,大致思路如下:假设喜欢物品A的用户集合为Sa,相应物品B的为Sb,通常以皮尔逊距离作为物品A和物品B的相似度距离。计算所有物品两两之间的相似距离就能得到一个相似度矩阵,这样当一个用户喜欢物品W就能从矩阵中找出与W相似的物品推荐给用户。
ICF在架构上怎么实现?个人认为放到近线层是最合理的:
1)通过消息队列实时回传用户U喜欢物品W的信息(U,W)
2)storm/flink/spark订阅并处理该信息,这里可能需要访问线上服务拿到用户画像和物品的详细信息,最后得到ICF算法的推荐结果
3)将计算结果存储起来,比如放到redis/memcache
4)当该用户再次访问时,ICF召回服务直接从存储中拿到候选结果,进行必要处理如曝光过滤,就能返回给用户了。
可以看到耗时的1)~3)都是在近线层处理,4)中只有简单的过滤,不难想像这样既能保证算法效果又能将极大降低线上延时,一举两得。
一种反面教材是将1)~4)统统搬到线上。假设计算要使用用户最近喜欢的X个物品,再假设每个物品平均有Y个相似的物品,这样候选物品就有X*Y个。一方面,为了保证算法效果X,Y不能太小,另一方面X,Y太大又容易增加线上服务延时,进退维谷骑虎难下说的就是这个,然后再在这个基础上搞的什么优化必然是治标不治本。
由此可见放到近线层是合理的,当前能这样做的前提是物品相似关系的相对稳定性。
UCF算法与ICF非常类似,只不过要预先计算用户相似度矩阵。它也应该放到近线层,虽然过程稍有不同:
1)通过消息队列实时回传用户U喜欢物品W的信息(U,W)
2)将W加入U喜欢的物品集合L(U)
3)从相似度矩阵拿到与U相似的用户集合S(U)
4)拿到S(U)中所有用户喜欢的物品集合(这里只是说明原理,算法应该有更精细的筛选),并入集合C
5)C-L(U)就是用户U的推荐候选集
可以想像这同样也是一个相当耗时的过程,如果放到线上层那你就每天专门侍候召回的超时告警吧!
UCF能放到近线的前提是用户相似关系的相对稳定,也就是3)中的S(U)短期内变动不大。
工作的原因,我需要把问题抽象一下。假设有一个服务维护着一批个人信息,包含名称,年龄,住址,身高和工作等,再假设每个用户的信息都串联到一起组成一个大字符串如“Tom,27,Beijing,175,Google",“Goodenough,80,America,180,University”等。另一方面,不同的用户可能只需要完整信息的子集,例如这个子集可能是名称和身高,也可能是名称和工作等。为了高效,服务器只应该返回用户需要的子集信息,而不是完整信息。
一种显然的解法是完全由在线层处理:在线服务遍历用户信息大字符串,根据请求拷贝相应字段并返回。但其实熟悉搜索尤其是索引的人可能早就看出应该放到离线去处理,以空间换时间,具体就是离线对大字符串建立一个数组,数组长度为用户信息维度,数组每个元素记录大字符串中每维信息的起始位置。对于上述例子,需要一个长度为5的数组,分别记录名称,年龄,住址,身高和工作在“Tom,27,Beijing,175,Google”中的起始位置:[0,4,7,15,19]。这样当用户请求名称和身高时,服务就能直接从0和15开始读取相应信息,免去了耗时的遍历。
这个例子直接将线上耗时的操作转移到了线下,是不是有点意思?
这是一个不该存在的例子,因为我真的遇到了将本应放到离线的耗时操作放到了线上,然后就要花很大很大的力气去改造这个架构和服务。不过就像面对离开的人留下的一堆bug一样,我喜欢从“乐观”的角度去思考看待,那就是这毕竟为我们创造了就业机会。这个例子不想说了。
我相信在一个不太合理的架构中,能按这种分层架构思想去优化的点会有很多,尤其是传统的后台开发集中在离线在线,没有太在意“近线层”的存在,也许需要改一改了。
1)一定要有意识地从全局去思考,这大概是为什么“不想当将军的士兵不是好士兵”。
2)不懂点算法可能干不好工程。最怕算法团队提出冠冕堂皇的不合理需求,架构团队却看不穿。
3)没有工程意识也搞不好算法,在各层之间胡乱实现,搞得线上超时报警,这算法效果能有保证吗?
4)“三足鼎立”稍有标题党,但通过实例看到,只有合理地在三者之间权衡规划才能达到相对较优的系统实现。
想获取更多内容,请关注海数据实验室公众号。
本期分享到这里,我们会每天更新内容,咱们下期再见,期待您的再次光临。有什么建议,比如想了解的知识、内容中的问题、想要的资料、下次分享的内容、学习遇到的问题等,请在下方留言。如果喜欢请关注。
社群推荐:
更多有关数据分析的精彩内容欢迎加入海数据在线数据分析交流群,有什么想法或者疑问都可在里面提出,与同行零距离交流,共同成长进步,请识别下面二维码加火星小海马微信,邀你进群。