新一代搜索引擎项目 ZeroSearch 设计探索

本文作者:kaelhua,腾讯 WXG 后台开发工程师

背景

写这篇文章很大的原因在于不论是内网还是外网,分享内存检索引擎设计的资料都非常稀少,且存量的资料大多侧重于功能性的介绍。

另一方面,在磁盘检索引擎方面,由于开源搜索引擎 ES 的盛行,对于其使用的索引库 lucence 的分析资料反而较为丰富。

本文意在通过分享对于内存检索引擎的认识,核心的解决方案,和一些优化方向的思考等等,略微填补一下关于内存检索引擎设计的资料空缺。

需要说明的是本人进入搜索领域的时间并不长,尽管之前搭建过一些垂类搜索系统,但只是站在应用层面进行使用,真正从事引擎设计的工作也是通过今年 4 月份左右组内重新设计新一代搜索引擎的项目 ZeroSearch 开始,恰巧承担了在线检索的设计与开发。因此这并不是一份多么标准的答案,而是我们对于引擎设计的探索,其质量还需时间检验和调整。

本文属于 ZeroSearch 系列分享中的在线检索设计分享。在本文中假定读者已经对搜索引擎有了基本的了解,至少对倒排求交,打分排序有基本的概念。

系统认知

对于系统的认知深度,会决定我们怎么去看待内存检索这样一个问题,以及由此而产生的的设计方案。尽管本文要讲的是内存检索引擎设计,然而我们还是得从对磁盘搜索引擎的认识开始。

由于 ES 的盛行,以及网页搜索(搜索领域的大 boss)体验的存在,大多数人对检索引擎的认识可能都是基于磁盘检索引擎来理解的,即系统的倒排,正排数据都位于磁盘中,只有在执行检索时,才会将相关的数据 load 到内存中。

其整体的流程大概如图所示:

磁盘搜索引擎在设计的过程中面临的主要问题为:

  • 同时兼具计算密集型与IO密集型任务

  • 磁盘与内存及CPU存在数量级差距的性能GAP,磁盘资源属于瓶颈,而计算量富余。

因此其在设计过程中考虑的核心要素为两点:

  • 任务调度的设计,即管理 IO 任务与计算任务

  • IO 优化 如异步 IO 设计,IOCache 优化,索引压缩等等

尽管 IO 优化也是非常重要的一环,但我们认为磁盘搜索引擎的核心,本质上是一个任务调度的问题。

现在回到内存搜索引擎的讨论上来。很明显,内存检索引擎在去除磁盘 IO 后,其要解决的核心问题是计算量的分配问题,即如何合理的分配计算量,能尽可能的让优质结果展现给用户。

下面是我们给内存检索引擎制定的核心流程:

可以看到我们对于计算量的分配,抽象出了求交,L1 打分,L2 打分等 3 个逻辑阶段。

求交
即根据查询串取出对应的倒排链进行求交,得到结果文档

L1打分
求交出来的文档均会送入L1打分

L2打分
L1得分Top的文档才能进入L2打分

这里为何要将打分分为两个阶段呢?

1 满足高求交数的需要

由于倒排数据处在内存中,因此单篇文档的求交消耗较少,限制引擎召回量的瓶颈往往不在求交,而在打分。轻量级的打分配合高求交数,可以避免求交截断导致的文档无法召回问题的出现

2 满足轻量级业务的打分需求

对于一些排序较简单的业务,不需要单独的精排服务,可以在引擎的 L2 打分过程中满足它的需求。

需要注意的是对于一些高消耗的模型,我们会放在更高层次的排序中,并对其进行抽离,放在独立的 tf 服务上执行,并不会放在引擎的 L1、L2 阶段来执行。

L0打分在离线索引过程中我们会提供接口用于计算文档的质量分,因此全量文档计算都会进行质量分的计算,建立倒排索引过程中,质量分越高的文档,排序越靠前,以保证被优先查找到

核心设计

设计背景

在讲述核心设计之前,需要先了解以下几点背景

1 索引分片分库

索引会先进行分片,多个分片再合并为一个索引库。分片数一旦指定后便不可更改,但是索引库的库数是可以灵活调整的,可以满足业务数据增长,索引数据多集群划分的需求。在检索过程中,索引库是检索的基本单位。这一点与 ES 可做一个简单的对比,ES 为库->分片(多个库)->实例(多个分片)的设计,而我们的设计为分片->库(多个分片)->实例(多个库),即我们将数据分片放到了更底层,打开了它的数量限制,同时对库的数量进行了收敛,原因在于库数越多,引擎性能将越差。关于索引分片分库的详细背景和设计后续组内会另有同学来进行介绍。

2 无 RPC 框架设计

引擎自身不携带 RPC 框架,我们以组件化的思想来进行设计。通俗来说,就是封装成了一个库,提供了初始化函数和唯一的检索入口函数来给到外部进行使用。这种方式有优有劣,优势为无须考虑上层的协议头,可灵活适配于各种 RPC 框架中,并复用已有的运维体系。劣势为对线程的控制能力较弱,理想情况下引擎自身的工作线程与 RPC 工作线程应当资源隔离,通过亲缘性各自分配和独占 CPU,这一点在组件化里难以实现。

事实上我们是面向 Controller-Proxy-Work 这一类 RPC 框架进行设计的,典型的如 SPP,Svrkit 等,并且在我们的实现过程中,将预处理和回包处理的逻辑均放到了 RPC-Work 线程中进行。

3 以易用性为第一优先级

组内上一代的内存搜索引擎由于基础配置项过多,引擎细节暴露过多,且欠缺配套的 debug 工具/能力,导致它的学习和维护成本都非常高。在新引擎的设计过程中,我们将易用性列为了第一优先级,本质上也是以服务业务为第一优先级,即便是性能方面也需要为易用性让步。易用性方面主要会体现在以下几点:

3.1 引擎的学习成本应具备梯度,满足快速入门使用的需求;

3.2 配置项尽可能少,尽可能避免暴露引擎细节,尽可能以通俗语言表达,如内存大小,线程数量等;

3.3 需要有全面的问题定位能力,根据经验,维护垂搜业务时,最常做的事情就是查文档为什么召不回,如果引擎具备问题一键定位的能力,那么可以有效的减少运维成本。

需要说明的是,尽管这里提到了易用性,但是下面的内容不会涉及到我们为了提升引擎易用性采取的具体做法。这里之所以单独拎出来进行强调,在于根据我过往的业务开发经验,部门内上一代内存搜索引擎的学习和维护成本过高,与业务的快速发展已经不匹配,我认为作为一个基础平台,性能 100 分,还是 80 分,甚至是 70 分,只要可以通过加机器来解决,对于增长型业务来说基本就不太 care 了,而易用性(含可维护性)才是最优先被考量的因素,其对团队的整体效率有很大的影响。

在清楚了大概的设计背景之后,可以开始真正考虑该如何设计我们的检索引擎了。

线程模型设计

下图是我们的检索组件目前使用的线程模型:

每个检索请求到达时,会生成一系列的求交与打分任务,在召回完成之后,会生成一个资源清理任务进行提交,请求完成。

下面对图中的主要元素做下简单的介绍

1 主线程
即RPC框架的Work线程,在Work线程中,会完成请求的预处理和回包处理的逻辑,并且处理求交或者打分任务完成后的回调逻辑。

2 JoinThreadPool
负责处理求交任务的线程池,在上面已经提到过,索引会分片分库,索引库是检索的基本单位,而一个求交任务至少会处理一个索引库(由于数据实时更新,系统中会存在一些小库,多个小库可能会被放到一个求交任务里进行处理),每个求交任务一旦分配到线程,就会将任务完整的执行完(或者超时)。

3 ScoreThreadPool
负责处理打分任务的线程池,打分任务分为L1打分任务和L2打分任务,但是线程池是共用的一个。对于L1打分任务,当一个求交任务完成的求交文档数量达到一定程度时,便会生成一个L1打分任务Push到打分队列中。L2打分任务同理,也是等到L1打分文档达到一定数量才会生产。

4 CleanThreadPool
负责处理资源清理任务的线程池,即资源的清理是异步进行的

5 求交资源池
负责管理求交时需要的一些数据结构,以资源池的形式来完成复用

可以看到整个线程模型是以 Task 为调度粒度的,这种模型有个比较大的缺陷,每个 Task 的消耗其实是不一致的。对于求交任务而言,每个任务会将一个索引库给求交完(达到限制或者超时),而随之产生的 L1 打分任务和 L2 打分任务,每个任务其实都只是求交出来的部分文档,因此求交任务的消耗是非常高的,并且求交任务在入队时是在一个 for 循环里集中式的入队(直到所有的索引库都分配完),为了防止打分任务饿死,这里划分了 3 个线程池以避免这个问题。

然而划分多个线程池本身就是问题所在,至少存在以下 3 个方面的问题:1 增加了配置项,降低了易用性。

2 其实业务并不知道该如何去对各个线程池的线程数量进行配置(尽管引擎会简单的根据 CPU 逻辑核数量进行默认设置),只能不断去调整测试来达到一个合适值。

3 多个线程池的方式不论怎么去配置数量,都不太可能把所有线程都高效利用起来,必然会有计算资源不能充分利用的线程池存在。

尽管存在这么多很容易预见的问题,我们还是先这样做了,一方面是目前开发人力非常少,在线检索这块的开发只有我一个人在兼职,需要弥补完善的东西还有很多,整体确实还比较粗糙,另一方面主要也是目前我们还没有建立一个用于质量标准评测的系统,因此一些优化类的工作优先级都排的比较低。

关于有没有饿死情况的出现,我们的评判标准并不是针对个例的发现,而是通过统计p99,p995,p999等指标来进行评判。因此严格意义上来说,也并不是真正的饿死,毕竟FIFO队列只要入队了迟早会被执行,只是等待时间长和短的问题。
正如标题是对于引擎设计的探索,这里简单分享一下后续计划要尝试的几个线程模型的方向,当然下面所有的方向都是只使用一个线程池。

1 继续维持 FIFO 的模式这个很好理解,也就是所有的 Task 都入同一个先进先出的队列,其实这个改动起来非常简单,只是质量标准评测系统还没搭起来,就暂时没去做对比测试了。

2 逻辑越轻量,优先级越高同样很好理解,即创建一个特殊的优先级队列,对各类 Task 根据逻辑的繁重设定一个优先级,设想的情况是这样的,优先级从高到低为:

清理Task > L1Task > L2Task > 求交Task
  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值