Hi,您好,欢迎来到西安盛图软件科技有限公司!

知识分享|超详细:完整的推荐系统架构设计

发布时间:2023-09-15 15:12:00

架构设计概述

架构设计是一个很大的话题,这里只讨论和推荐系统相关的部分。更具体地说,我们主要关注的是算法以及其他相关逻辑在时间和空间上的关系——这样一种逻辑上的架构关系。


在前面的章节中我们讲到了很多种算法,每种算法都是用来解决整个推荐系统流程中的某个问题的。我们的最终目标是将这些算法以合理的方式组合起来,形成一整套系统。在这个过程中,可用的组合方式有很多,每种方式都有舍有得,但每种组合方式都可被看作一种架构。这里要介绍的就是一些经过实践检验的架构层面的最佳实践,以及对这些最佳实践在不同应用场景下的分析。除此之外,还希望能够通过把各种推荐算法放在架构的视角和场景下重新审视,让读者对算法间的关系有更深入的理解,从全局的角度看待推荐系统,而不是只看到一个个孤立的算法。


架构设计的本质之一是平衡和妥协。一个推荐系统在不同的时期、不同的数据环境、不同的应用场景下会选择不同的架构,在选择时本质上是在平衡一些重要的点。下面介绍几个常用的平衡点。

1. 个性化 vs 复杂度

个性化是推荐系统作为一个智能信息过滤系统的安身立命之本,从最早的热榜,到后来的公式规则,再到著名的协同过滤算法,最后到今天的大量使用机器学习算法,其主线之一就是为用户提供个性化程度越来越高的体验,让每个人看到的东西都尽量差异化,并且符合个人的喜好。为了达到这一目的,系统的整体复杂度越来越高,具体表现为使用的算法越来越多、算法使用的数据量和数据维度越来越多、机器学习模型使用的特征越来越多,等等。同时,为了更好地支持这些高复杂度算法的开发、迭代和调试,又衍生出了一系列对应的配套系统,进一步增加了整个系统的复杂度。可以说整个推荐逻辑链条上的每一步都被不断地细化分析和优化,这些不同维度的优化横纵交织,构造出了一个整体复杂度非常高的系统。从机器学习理论的角度来类比,如果把推荐系统整体看作一个巨大的以区分用户为目标的机器学习模型,则可以认为复杂度的增加对应着模型中特征维度的增加,这使得模型的 VC 维不断升高,对应着可分的用户数不断增加,进而提高了整个空间中用户的个性化程度。这条通过不断提高系统复杂度来提升用户个性化体验的路线,也是近年来推荐系统发展的主线之一。

2. 时效性 vs 计算量

推荐系统中的时效性概念体现在实时服务的响应速度、实时数据的处理速度以及离线作业的运行速度等几个方面。这几个速度从时效性角度影响着推荐系统的效果,整体上讲,运行速度越快,耗时越少,得到的效果越好。这是因为响应速度越快,意味着对用户行为、物品信息变化的感知越快,感知后的处理速度越快,处理后结果的反馈就越快,最终体现到用户体验上,就是系统更懂用户,更快地对用户行为做出了反应,从而产生了更好的用户体验。但这些时效性的优化,带来的是更大的计算量,计算量又对应着复杂的实现逻辑和更多的计算资源。在设计得当的前提下,这样的付出通常是值得的。如同前面章节中介绍过的,时效性优化是推荐系统中非常重要的一类优化方法和优化思路,但由此带来的计算压力和系统设计的复杂度也是必须要面对的。

3. 时间 vs 空间

时间和空间之间的平衡关系可以说是计算机系统中最为本质的关系之一,在推荐系统中也不例外。时间和空间这一对矛盾关系在推荐系统中的典型表现,主要体现在对缓存的使用上。缓存通常用来存储一些计算代价较高以及相对静态变化较少的数据,例如用户的一些画像标签以及离线计算的相关性结果等。但是随着越来越多的实时计算的引入,缓存的使用也越来越广泛,常常在生产者和消费者之间起到缓冲的作用,使得二者可以解耦,各自异步进行。例如实时用户兴趣计算这一逻辑,如果没有将之前计算的兴趣缓存起来,那么在每次需要用户兴趣时都要实时计算一次,并要求在较短的时间内返回结果,这对计算性能提出了较高的要求。但如果中间有一层缓存作为缓冲,则需求方可以直接从缓存中取来结果使用。这在结果的实时性和新鲜度上虽然做了一定的妥协,但却能给性能提升带来极大的帮助。这样就将生产和消费隔离开来,生产者可以根据具体情况选择生产的方式和速度。当然,仍然可以努力提高生产速度,生产速度越快,缓存给时效性带来的损失就越小,消费者不做任何改动就可以享受到这一提升效果。所以说,这种利用缓存来解耦系统,带来性能上的提升以及开发的便利,也是在推荐系统架构设计中需要掌握的一种通用的思路。


上面介绍的一些基本性原则贯穿着推荐系统架构设计的方方面面,是一些具有较高通用性的思路,掌握这些思路,可以产生出很多具体的设计和方法;反过来,每一种设计技巧或方法,也都可以映射到一个或几个这样的高层次抽象原则上来。这种自顶向下的思维学习方法对于推荐系统的架构设计是非常重要的,并且可以推广到很多其他系统的设计中。

系统边界和外部依赖

架构设计的第一步是确定系统的边界。所谓边界,就是区分什么是这个系统要负责的,也就是边界内的部分,以及什么是这个模型要依赖的,也就是边界外的部分。划分清楚边界,意味着确定了功能的边界以及团队的边界,能够让后期的工作都专注于核心功能的设计和实现。反之,如果系统边界没有清晰的定义,可能会在开发过程中无意识地侵入其他系统中,形成冗余甚至矛盾,或者默认某些功能别人会开发而将其忽略掉。无论哪种情况,都会影响系统的开发乃至最终的运转。


系统边界的确定,简单来说,就是在输入方面确定需要别人给我提供什么,而在输出方面确定我要给别人提供什么。在输入方面,就是判断什么输入是需要别人提供给我的,要把握的主要原则包括:



 

依照此原则,图 11-1 展示了推荐系统的主要外部依赖。


图 11-1 推荐系统的主要外部依赖

1. 数据依赖

推荐系统作为一个典型的数据算法系统,数据是其最重要的依赖。这里面主要包括用户行为数据和物品数据两大类,前面介绍的各种算法几乎都是以这两种数据作为输入进行计算的。这些数据除了为推荐系统所用,它们也是搜索、展示等其他重要系统的输入数据,所以作为通用的公共数据和服务,显然不应该在推荐系统的边界内部,而应该是外部依赖。需要特别指出的是,虽然有专门的团队负责行为数据的收集,但是收集到的数据是否符合推荐系统的期望却不是一件可以想当然的事情。例如,对于结果展示的定义,数据收集团队认为前端请求到了结果就是展示,但对于推荐系统来说,只有用户真正看见了才是真实的展示。其中的原因在于数据收集团队并不直接使用数据,那么他们就无法保证数据的正确性,这时就需要具体使用数据的业务方,在这里是推荐团队,来和他们一起确认数据收集的逻辑是正确的。如果数据收集的逻辑不正确,后面的算法逻辑就是在做无用功。花在确保数据正确上的精力和资源,几乎总是有收益的。

2. 平台工具依赖

推荐系统是一个计算密集型的系统,需要对各种形态的数据做各种计算处理,在此过程中,需要一整套计算平台工具的支持,典型的如机器学习平台、实时计算平台、离线计算平台、其他平台工具等。在一个较为理想的环境中,这些平台工具都是由专门的团队来构建和维护的。而在一些场景下,推荐系统可能是整个组织中最早使用这些技术的系统,推荐业务也还没有重要和庞大到需要老板专门配备一个平台团队为之服务的程度,在这种情况下,其中的一些平台工具就需要推荐系统的团队自己负责来构建和维护了。为了简化逻辑,下面我们假设这些平台工具都是独立于推荐系统存在的,属于推荐系统的外部依赖。


在对外输出方面,系统边界的划定会根据公司组织的不同有所差异。例如,在一些公司中,推荐团队负责的是与推荐相关的整个系统,在输出方面的体现就是从算法逻辑到结果展示,这时候系统的边界就要延伸到最终的结果展示。而在另外一些公司中,前端展示是由一个大团队统一负责的,这时候推荐系统只需要给出要展示的物品 ID 和相关展示信息即可,前端团队会负责统一展示这些物品信息。这两种模式没有绝对的好坏之分,重要的是要与整个技术团队的规划和架构相统一。在本书中,为了叙述简便,我们不讨论前端展示涉及的内容,只专注于推荐结果的生产逻辑。


推荐系统的效果和性能在一定程度上取决于这些依赖系统,所以在寻求推荐系统的优化目标时,目光不能只看到推荐系统本身,很多时候这些依赖系统也是重要的效果提升来源。例如,物品信息的变更如果能被更快地通知到推荐系统,那么推荐系统的时效性就会更好,给到用户的结果也就会更好;再如,用户行为数据收集的准确性能有所提高的话,对应的相关性算法的准确性也会随之提高。在有些情况下,外部系统升级会比优化算法有更大的效果提升。当然,推荐系统的问题也可能来自这些外部的依赖系统。例如,前端渲染展示速度的延迟会导致用户点击率的显著下降,因为这会让用户失去耐心。所以,当推荐系统指标出现下降时,不光要从内部找问题,也要把思路拓展到系统外部,从全局的角度去找问题。综合来讲,外部依赖的存在启发我们要从全链条、全系统的角度来看问题,找问题,以及设计优化方法。

离线层、在线层和近线层架构

架构设计有很多不同的切入方式,最简单也是最常用的一种方式就是先决定某个模块或逻辑是运行在离线层、在线层还是近线层。这三层的对比如表 11-1 所示。


任何使用非实时数据、提供非实时服务的逻辑模块,都可以被定义为离线模块。其典型代表是离线的协同过滤算法,以及一些离线的标签挖掘类算法。离线层通常用来进行大数据量的计算,由于计算是离线进行的,因此用到的数据也都是非实时数据,最终会产出一份非实时的离线数据,供下游进一步处理使用。与离线层相对的是在线层,也常被称为服务层,这一层的核心功能是对外提供服务,实时处理调用方的请求。这一层的典型代表是推荐系统的对外服务接口,接受实时调用并返回结果。在线层提供的服务是实时的,但用到的数据却不一定局限于实时数据,也可以使用离线计算好的各种数据,例如相关性数据或标签数据等,但前提是这些数据已经以对实时友好的形态被存储起来。


近线层则处于离线层和在线层的中间位置,是一个比较奇妙的层。这一层的典型特点就是:使用实时数据(也会使用非实时数据),但不提供实时服务,而是提供一种近实时的服务。所谓近实时指的是越快越好,但并不强求像在线层一样在几十毫秒内给出结果,因为通常在近线层计算的结果会写入缓存系统,供在线层读取,做了一层隔离,因此对时效性无强要求。其典型代表是我们前面讲过的实时协同过滤算法,该算法通过用户的实时行为计算最新的相关性结果,但这些计算结果并不是实时提供给用户的,而是要等到用户发起请求时才会把最新的结果提供给他使用。


下面详细介绍每一层的特点、案例和具体分析。

离线层架构

离线层是推荐系统中承担最大计算量的一个部分,很大一部分的相关性计算、标签挖掘以及用户画像挖掘工作都是在这一层进行的。这一层的任务具有的普遍特点是使用大量数据以及较为复杂的算法进行计算和挖掘。所谓大量数据,通常指的是可以使用较长时间段的用户行为数据和全量的物品数据;而在算法方面,可以使用较为复杂的模型或算法,对性能的压力相对较小。对应地,离线层的任务也有缺点,就是在时间上存在滞后性。由于离线任务通常是按天级别运行的,用户行为或物品信息的变更也要等一天甚至更久才能够被反映到计算结果中。在离线层虽然进行的是离线作业,但其生产出来的数据通常是被实时使用的,因此离线数据在生产出来之后还需要同步到方便在线层读取的地方,例如数据库、在线缓存等。


在具体实践中,经常放在离线层执行的任务主要包括:协同过滤等行为类相关性算法计算、用户标签挖掘、物品标签挖掘、用户长期兴趣挖掘、机器学习模型排序等。仔细分析这些任务,会发现它们都符合上面提到的特点。这些任务的具体流程各不相同,但大体上都遵循一个共同的逻辑流程,如图 11-2 所示。


 图 11-2 离线层逻辑架构图


在这个逻辑架构图中,离线算法的数据来源主要有两大类:一类是 HDFS/Hive 这样的分布式文件系统,通常用来存储收集到的用户行为日志以及其他服务器日志;另一类是 RDBMS 这样的关系数据库,通常用来存储商品等物品信息。离线算法会从输入数据源获取原始数据并进行预处理,例如,协同过滤算法会先把数据处理成两个倒排表,LDA 算法会先对物品文本做分词处理,等等,我们将预处理后的数据统一称为训练数据(虽然有些离线算法并不是机器学习算法)。预处理这一步值得单独拿出来讲,这是因为很多算法用到的预处理是高度类似的,例如,文本标签类算法需要先对原始文本进行分词或词性标注,行为类相关性算法需要先将行为数据按用户聚合,点击率模型需要先将数据按照点击/展示进行聚合整理,等等。所以在设计离线挖掘的整体架构时,有必要有针对性地将数据预处理流程单独提炼出来,以方便后面的流程使用,做到更好的可扩展性和可复用性。下一步是各种推荐算法或机器学习模型基于各自的训练数据进行挖掘计算,得到挖掘结果。离线计算用到的工具通常包括 Hadoop、Spark 等,结果可能是一份协同过滤相关性数据,可能是物品的文本主题特征,也可能是结果排序模型。接下来,为了让挖掘结果能够被后面的流程所使用,需要将挖掘结果同步到不同的存储系统中。一般来说,如果挖掘结果要被用作下游离线流程的输入,是一份中间结果,那么通常它会被再次同步到 Hive 或 HDFS 这样的分布式文件系统中;如果挖掘结果要被最终的推荐服务在线实时使用,那么它就需要被同步到 Redis 或 RDBMS 这样对实时访问更为友好的存储系统中。至此,一个完整的离线挖掘流程就完成了。


上面讲到离线任务通常以天为单位来执行,但是在很多情况下,提高作业的运行频率以及对应的数据同步频率,例如从一天一次提升到一天多次,都会对推荐系统的效果有提升作用,因为这些都可以被理解为在做时效性方面的优化。一种极限的思想是,当我们把作业的运行频率提高到极致时,例如每分钟甚至每几秒钟运行一次作业,离线任务就变成了近线任务。当然,在这种情况下就需要对离线算法做相应的修改以适应近线计算的要求,例如前面介绍过的实时协同过滤算法就是对原始协同过滤算法的修改,以及将机器学习的模型训练过程从离线改为在线。


所以,虽然我们会把某些任务放到离线层来执行,但并不代表这些任务就只能是离线任务。我们要深入理解为什么将这些任务放在离线层来执行,在什么情况下可以提高其运行频率,甚至变为近线任务,以及这样做的好处和代价是什么。只有做到这一点,才能够做到融会贯通,不被当前的表象迷住眼睛。一种典型的情况是,当实时计算或流计算平台资源不足,或者开发人力资源不足时,我们倾向于把更多的任务放到离线层来执行,因为离线计算对时效性要求较低,出错之后影响也较小。综合来说,就是容错度较高,适合在整体资源受限的情况下优先选择。而随着平台的不断完善,以及人力资源的不断补充,就可以把一些对时效敏感的任务放到近线层来执行,以获得更好的收益。

近线层架构

有了上面的铺垫,近线层的存在理由和价值就比较明确了,从生产力发展的角度来看,可以认为它是实时计算平台工具发展到一定程度对离线计算的自然改造;而从推荐系统需求的角度来看,它是各种推荐算法追求实时化效果提升的一种自然选择。


近线层和离线层最大的差异在于,它可以获取到实时数据,并有能力对实时数据进行实时或近实时的计算。也正是由于这个特点,近线层适合用来执行对时效比较敏感的计算任务,例如实时的数据统计等,以及实时执行能够获得较大效果提升的任务,例如一些实时的相关性算法计算或标签提取算法计算。近线层在计算时可使用实时数据,也可使用离线生成的数据,在提供服务时,由于无须直接响应用户请求,因此也不用提供实时服务,而是通常会将数据写入对实时服务友好的在线缓存中,方便实时服务读取,同时也会同步到离线端做备份使用。


通常放在近线层执行的任务包括实时指标统计、用户的实时兴趣计算、实时相关性算法计算、物品的实时标签挖掘、推荐结果的去重、机器学习模型统计类特征的实时更新、机器学习模型的在线更新等,这些任务通常会以如下两种方式进行计算。



 

图 11-3 展示了典型的近线层计算架构图。

图 11-3 典型的近线层计算架构图


从数据源接入的角度来看,近线层主要使用实时数据进行计算,这就引出了近线层和离线层的一个主要区别:近线层的计算通常是事件触发的,而离线层的计算通常是时间触发的。事件触发意味着对计算拥有更多的主动权和选择权,但时间触发则无法主动做出选择。事件触发意味着每个事件发生之后都会得到通知,但是否要计算以及计算什么是可以自己选择的。例如,可以选择只捕捉满足某种条件的事件,或者等事件累积到一定程度时再计算,等等。所以,当某个任务的触发条件是某个事件发生之后进行计算,那么这个任务就很适合放在近线层来执行。例如推荐结果的去重,需要在用户浏览过该物品之后将其加入一个去重集合中,这就是一个典型的事件触发的计算任务。此外,近线层的计算是可以使用离线数据的,但前提是需要提前将这些数据同步到对实时计算友好的存储系统中。


在近线层中执行的典型任务包括但不限于:

 

总结起来,凡是可以和实时请求解耦,但需要实时或近实时计算结果的任务,都可以放到近线层中执行。


近线层的实时计算虽然没有响应时间的要求,但却存在数据堆积的压力。具体来说,近线层计算用到的数据大部分是通过 Kafka 这样的消息队列实时发送过来的,在接收到每一个消息或消息窗口之后,如果对消息或消息窗口的计算速度不够快,就会导致后面的消息堆积。这就像大家都在排队办理业务,如果一个业务办理得太慢,那么排的队就会越来越长,长到一定程度就会出问题。所以,近线层的计算逻辑不宜过于复杂,而且近线层读取的外部数据,例如离线同步好的 Redis 中的数据,也不宜过多,还有 I/O 次数不宜过多。这就要求近线层的计算逻辑和用到的数据结构都要经过精心的设计,共同保证近线层的计算效率,以免造成数据堆积。


除了纯数据统计类型的任务,以及结果去重这样的无数据产出的任务,近线层的大多数任务在离线层都有对应的部分,二者有着明显的优势和劣势,因此应该结合起来使用。典型的如实时协同过滤算法,由于引入了实时性,使得它在一些新物品和新用户上的效果比原始的协同过滤算法的效果好;但由于它只使用实时数据,所以在稀疏性和不稳定性方面的问题也是比较大的,要使用离线版本的协同过滤算法作为补充,才能形成更全面的覆盖。再比如在近线层执行的用户实时兴趣预测,能够捕捉到用户最新鲜的兴趣,准确率会比较高;但由于短期兴趣易受展示等各种因素影响发生较大的波动,如果完全根据短期兴趣来进行推荐的话,则很有可能会陷入局部的信息茧房,产生高度同质的结果,影响用户的整体体验。而如果将离线计算的长期兴趣和短期兴趣相结合,就可以有效避免这个问题,既能利用实时数据取得高相关性,又能利用长期数据取得稳定性和多样性。从这些例子可以看出,离线层和近线层之间并没有不可逾越的鸿沟,二者更多的是在效率、效果、稳定性、稀疏性等多个因素之间进行权衡得到的不同选择,一个优秀的工程师应该做到“码中有层,心中无层”,才算是对算法和架构做到了融会贯通。


上面讲到离线层的任务在一定条件下可以放到近线层来执行,那么类似地,近线层的任务是否可以放到在线层来执行呢?这个问题其实涉及离线层、近线层这两层作为整体和在线层的关系。如果把推荐系统比作一支打仗的军队,那么在线层就是在前方冲锋陷阵的士兵,直接面对敌人的攻击,而离线层和近线层就是提供支持的支援部门,离线层就像是生产粮食和军火的大后方,近线层就像是搭桥修路的前方支援部门,二者的本质都是让前线士兵能够最高效、最猛烈地打击敌人,但其业务本质导致它们无法到前线去杀敌。离线层和近线层是推荐系统的生产者,在线层是推荐系统的消费者(也会承担一定的生产责任),它们有着截然不同的分工和定位,是无法互换的。

在线层架构

在线层与离线层、近线层最大的差异在于,它是直接面对用户的,所有的用户请求都会发送到在线层,而在线层需要快速给出结果。如果抽离掉其他所有细节,这就是在线层最本质的东西。在线层最本质的东西并不是在线计算部分,因为在极端情况下,在接收到用户请求之后,在线层可以直接从缓存或数据库中取出结果,返回给用户,而不做任何额外计算。而事实上,早年还没有引入机器学习等复杂的算法技术时,绝大多数计算都是在离线层进行的,在线层就起到一个数据传递的作用,很多推荐系统基本都是这么做的,甚至时至今日,这种做法仍然是一种极端情况下的降级方案。


推荐系统发展到现在,尤其是各种机器学习算法的引入,使得我们可以使用的信息越来越多,可用的算法也越来越复杂,给用户的推荐结果通常是融合了多种召回策略,并且又加了重排序之后的结果,而融合和重排序现在通常是在在线层做的。那么问题来了:这些复杂计算一定要放到在线层做吗?为了回答这个问题,不妨假设:如果将所有计算都放在离线层做,在线层只负责按照用户 ID 查询返回结果,是否可行?如果将所有计算都放在离线层做,由于不知道明天会有哪些用户来访问系统,所以就需要为每个用户都计算出推荐结果,这要求我们计算出全平台所有用户的推荐结果,而对于那些明天没有来访问系统的用户,今天的计算就浪费掉了。但这仍然不够,因为明天还会有新来的用户,这些用户的信息在当前计算时是拿不到的,所以,即使今天离线计算出了所有当前用户的推荐结果,明天也还会有大量覆盖不到的用户。这就是将上面提到的复杂计算一定要放在在线层做的第一个主要原因:只有按需实时计算才能覆盖到所有用户,并且不会产生计算的浪费。从另一个角度来看,如果今天就把用户的推荐结果完全计算出来,若用户明天的实时行为表达出来的兴趣和今天的不相符,或者机器学习模型中一些关键特征的取值发生了变化,那么推荐结果就会不准确,并且无法及时调整。例如,用户昨天看的是手机,今天打算买衣服,但我们昨天计算出的推荐结果是以手机为主的,那么用户今天的需求是无法满足的。这就是需要在在线层做复杂计算的第二个主要原因:只有在线实时计算,才能够充分利用用户的实时信息,包括实时兴趣、实时特征以及其他近线层计算的结果等。除此以外,还有其他原因,比如实时处理可以快速应对实时发生的业务请求等。以上这些原因共同决定了在线层存在的意义。


上一篇:知识分享|嵌入式IoMT设备的安全设计
下一篇:干货分享|为什么越来越多的人选择 PostgreSQL,放弃了 MySQL