当前位置: 首页 > 科技观察

抛弃Python,知乎为何选择Go重构推荐系统?

时间:2023-03-17 15:32:03 科技观察

知乎从问答起家,逐渐成长为一个大型的综合性知识内容平台。截至目前,用户数已超过2.2亿,提问超过3000万次,回复超过1.3亿次。同时,知乎积累了大量优质文章、电子书等付费内容。因此,知乎在连接人与知识的路径上有大量的推荐场景。据粗略统计,除了首页推荐,我们已经有20多个推荐场景;并且在业务的快速发展中,不断增加新的推荐业务需求。在这样的背景下,构建一个更通用、方便业务接入的推荐系统就变得势在必行。重构推荐系统需要考虑哪些因素?如何进行技术选型?重构过程中会遇到哪些陷阱?希望知乎踩坑的经历能给大家带来一些思考。背景在谈总体架构设计之前,我们先来回顾一下推荐系统的整体流程和架构。通常,由于模型要求的特性和排序的性能考虑,我们通常将简单的推荐系统分为召回层和排序层。召回层主要负责从候选集中选择待排序的集合,然后获取排序特征,排序后模型选择推荐结果给用户。简单推荐模型适用于一些需要单一推荐结果,只负责单一目标的推荐场景,比如新闻详情页推荐、文章推荐等。但在现实中,作为一个通用的推荐系统,需要考虑用户的多维度需求,如用户多样性需求、时效性需求、结果满意度需求等。因此,需要在推荐过程中使用多个不同的队列,根据不同的需求进行排序,然后使用多队列融合策略来满足用户的不同需求。在我们看来,大体是这样的发展路线。比如今年7月份,由于业务发展较快,架构相对独立的历史原因,我们的推荐系统有多套,架构比较简单。以其中一个设计比较完善的推荐架构的系统为例,其整体架构如下。可以看到这个架构已经包含了recall层和ranking层,还考虑了secondaryranking。那么问题是什么?首先,对多通道召回支持不友好。现有架构的召回是耦合在一起的,因此开发和研究的成本很高,并且访问多个召回相对困难。然后,召回阶段只是使用Redis作为召回基础。Redis具有查询效率高、服务稳定等诸多优点。然而,将其用作所有召回层的基础会放大其缺点。首先,它不支持稍微复杂的召回逻辑。其次,它无法对大量结果进行召回计算。第三,不支持嵌入召回。第三点是在整体架构实现的时候,架构逻辑的剥离不够干净,导致架构采样逻辑薄弱,各种通用特征和通用监控的构建难度大。第四点,我们知道在推荐系统中,featurelog的构建是一个非常重要的环节,是推荐质量的重要基础之一。然而,在现有的推荐系统框架中,特征日志的构建缺乏统一的验证和实施方案,各业务“各显神通”。第五,当前系统不支持多队列融合,严重限制了通用架构的可扩展性和易用性。因此,我们打算重构知乎的通用推荐服务框架。重构之路上重构前的思考***,语言的选择。早期有大量的知乎服务是基于Python开发的。但是在实践过程中,发现了Python资源消耗过大、无法使用多人协同开发等各种问题。随后,公司进行了大规模改造。现在知乎在语言层面的技术选型比较开放。目前公司拥有Python、Scala、Java、Golang等开发语言项目。那么对于推荐系统服务,由于其计算量大,多并发的特点,语言的选择还是需要考虑的。其次,出于架构上的考虑,需要解决支持多队列洗牌和支持多路召回的问题,其设计是绝对可插拔的。第三,在recall层,除了传统的Rediskvrecall(partialcfrecall、popularrecall等应用场景)之外,我们还需要考虑一些其他的索引数据库来支持更多的索引类型。首先我们先来看语言的选择,大体比较各种语言的特点。我们从以下几个方面进行比较。在性能方面,根据公开的基准测试,Golang与Java和Scala大约是同一个数量级,大约是Python的30倍。其次,Golang的编译速度更快,与Java、Scala相比优势明显。第三,它的语言特性决定了Golang的高开发效率。另外,由于缺少trycatch机制,在用Golang开发时很难考虑异常处理。很多,所以上线后维护成本比较低。但是Golang有一个明显的缺陷,就是目前第三方库比较少,尤其是跟AI相关的。那么,基于以上优缺点,我们为什么选择Golang进行重构呢?1、Golang有天然优势,支持高并发,占用资源相对少。这个优势正是推荐系统所需要的。推荐系统中存在大量的高并发场景,比如多通道召回、特征计算等。2、知乎内部基础组件的Golang版本生态比较完善。目前我们知道Golang内部使用越来越活跃,大量的基础组件已经Golang化,包括基础监控组件等,这也是我们选择Golang的一个重要原因。但需要强调的是,语言的选择并不是唯一的答案,它是结合公司技术和业务场景的选择。说完语言的选择,我们在架构上如何解决,才能在重构时支持多队列shuffling和多路召回?这个在设计模式中比较常见,就是“抽象工厂模式”:首先我们构建一个队列注册管理器,在一个map中注册回调,将当前服务的所有队列做成json配置的可自由插拔的模式,比如下面的配置,指定一个服务需要的所有队列,存放在queues字段中。使用名称从注册管理器的映射中调用相应的队列服务。之后,我们就可以并发处理多个队列了。对于多渠道召回,以及整个推荐具体流程的可插拔性,与上面的处理方式类似,比如下面的队列:我们可以指定需要召回的来源,指定合并策略等,当某一个process不需要处理,会自动按照默认的步骤进行处理,这样在具体Queue的实现中,可以通过下面的简单操作自由配置。讲完了关于架构的一些思考点和具体的架构实现方案。下面介绍召回层的具体技术选择。我们先回顾一下。常用的推荐召回源中,有topic(tag)-basedrecall、entityrecall、regionalrecall、CF(collaborativefiltering)recall、embeddingrecall等NN产生的recall。所以为了能够支持这些recall,我们应该如何在技术上实现它?我们先从使用的角度来看一下常用的NoSQL数据库产品。Redis是典型的k-v存储。它的简单性和高性能使其在行业中得到广泛应用。但是不支持复杂查询的问题也使得Redis在复杂召回场景中并不占优,所以一般适用于kv类型的召回,比如热门召回、区域召回、少量CF召回。HBase作为海量数据的列式存储数据库,有一个明显的缺点就是复杂查询性能差,所以一般适用于数据查询量大但查询简单的场景,比如推荐系统中的读取和推送场景。其实ES已经不是数据库,而是一个通用的搜索引擎范畴。它最大的优点是支持复杂的聚合查询。因此,从总体上搜索其用户是一个比较合适的选择。上面我们介绍了generalrecall的技术选型,那么如何处理embeddingrecall,我们的解决方案是基于Facebook开源的faiss包构建一个generalANN(approximatenearestneighbor)检索服务。faiss是一个为密集向量提供高效相似性搜索和聚类的框架。它具有以下特点:1、提供多种嵌入召回方式;2、检索速度快,我们基于python接口进行封装,影响时间在几ms-20ms之间;3、C++实现,提供python包调用;4.大部分算法支持GPU实现。从上面的介绍我们可以看出,在一般的推荐场景下,我们的召回层一般都是基于ES+Redis+ANN的模型构建的。ES主要支持比较复杂的召回逻辑,比如基于多个主题的混合召回;Redis主要用于支持流行的召回,以及比较小规模的CF召回;ANN主要支持embeddingrecalls,包括embedding和由nn产生的CF训练输出的embedding等。引入了以上的思考点之后,我们的整体架构就基本成型了,如下图所示。该框架可以支持多队列融合,每个队列也支持多路召回,这样可以更好的支持不同的推荐场景。另外我们召回ES+Redis+ANN的技术栈方案,可以更好的支持多种不同类型的召回,达到服务线的最终目的。重构遇到的一些问题及解决方案1.离线任务和模型的管理问题。我们都有做线上服务的经历。我们往往更倾向于关注线上的业务逻辑代码,而往往忽视了线下代码任务的管理和维护。但离线编码任务和模型在推荐场景中至关重要。因此,如何有效维护离线代码和任务是我们面临的首要问题。2.功能日志问题。在推荐系统中,我们经常会遇到特征拼接和特征“时间旅行”的问题。所谓特征时间遍历,是指在模型训练时,利用预测时无法获得的“未来信息”。这主要是由于训练标签和特征拼接的时间不够严谨造成的。如何构建方便通用的特征日志,减少特征拼接错误和特征交叉是我们面临的第二个问题。3、服务监控问题。通用的推荐系统在监控的基础上尽可能做到通用和可复用,减少针对具体业务的监控开发量,方便业务定位问题。4.离线任务和模型的管理。在包括推荐系统在内的算法方向,需要构建大量的离线任务来支持各种数据计算服务,以及模型的常规训练。然而在实际工作中,我们往往忽略了代码管理对于离线任务的重要性。随着时间的推移,各种数据和特征的质量无法得到保证。为了尽可能解决此类问题,我们从三个方面来做。第一,将通用推荐系统所依赖的离线任务的代码统一到一处进行管理;第二,将所有任务以包的形式统一管理,保证所有任务都依赖于***包;三是构建任务成果监控体系,全面监控线下任务产出情况。5.功能日志问题。AndrewNg之前说过:“挖掘特征是困难的、耗时的并且需要专业知识。应用机器学习基本上就是在做特征工程。』我们理想的推荐系统模型应该有干净的原始数据,可以很容易地处理成可学习的数据集,并且可以通过一定的算法对模型进行学习,达到不断优化预测效果的目的。但是在现实中,我们需要处理多种数据源,包括数据库,日志,离线,在线。有这么多的来源原始数据的处理,难免会遇到各种问题,比如特征拼接错误,特征“时间旅行”等等,这里体现的本质问题之一就是特征处理过程的规范性问题。那么我们如何解决这个问题呢?问题?首先,我们使用在线而不是离线,通过在线记录特征日志而不是原始数据,并统一特征日志Proto,使得特征解析脚本可以统一领域。6.服务监控问题。关于监控问题,知乎搭建了基于StatsD+Grafana+InfluxDB的监控系统,支持各种监控日志的采集、存储和展示。基于这个系统,我们可以很方便的搭建自己微服务的各种监控。这里不介绍通用的监控系统,主要介绍基于推荐系统的监控建设实践。首先,让我们回顾一下推荐系统的总体设计。我们采用了“可插拔”的多队列和多回调设计,所以我们可以在通用架构设计中获取到各种信息,比如业务线名称、业务名称、队列名称、进程名称等。这样,我们可以通过下面的方式实现监控,这样监控就可以通用化的设计,而不需要为每一个推荐的业务都设计太多的监控和相关的告警。实现了以上之后,我们推荐系统的监控系统是什么样子的呢?首先,每个业务都可以通过grafana展示页面进行设置。我们可以看到每个flow的各种数据,以及recall来源的比例关系,还有特征分布,rankingscore分布等等。未来的挑战说完遇到的一些问题,我们再来看看未来的挑战。随着业务的快速发展,数据和规模还在不断扩大,架构需要不断迭代;第二点是随着推荐业务越来越多,如何协调策略的通用性和业务间的隔离性;三点,资源调度和性能开销也需要不断优化;***,如何在多个机房之间保持数据同步也是一个需要考虑的问题。总结***,简单总结一下:***点,重构语言的选择,关键是要结合公司的技术背景和业务场景;第二点,架构要尽可能灵活,可迭代;,监测应尽早进行,并尽可能低级化和通用化。