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

搜索引擎分布式系统思维实践

时间:2023-03-21 16:21:11 科技观察

1.引言搜索引擎数据量逐渐膨胀后,分布式搜索是必由之路。除了数据分片,搜索引擎的分布还需要考虑数据的状态和各个组件的状态流。在此分享一下基于ZK设计分布式搜索引擎的一些经验和思考,包括从单机版到分布式版的演进。2.分布式系统分布式系统是硬件或软件组件分布在不同网络计算机上,彼此之间仅通过消息传递进行通信和协调的系统。当单机系统无法处理请求量或数据量时,需要考虑对系统进行合理的分布式改造和部署。CAP(ConsistencyAvailabilityPartitiontolerance)定理是一个众所周知的概念。这三个指标不可能同时达到。所以在实际应用中,我们总是需要针对当前的业务进行取舍,比如在核心数据库领域。为了数据的强一致性,我们可能会牺牲部分可用性,在大流量的服务上我们可能会优先考虑可用性。在Search的搜索和推荐应用场景中,我们应该优先考虑可用性来优先保证性能。在强一致性上做性能上的妥协,只需要保证最终的一致性。3.分布式系统面临的挑战构建一个完整的分布式系统需要解决以下重要问题:可靠的节点状态感知在分布式系统中,异常来自多种情况,包括服务器硬件不可用导致的崩溃、系统故障、严重等各种异常状态异常崩溃退出、网络不稳定导致的链接异常不稳定、服务负载过高导致假死等数据不仅是一直写入,还需要保证每天或每小时的全量数据更新。对于一个在线服务来说,还需要保证检索的稳定性。将形象比作高速换轮也不为过。4.搜索分布式整体结构搜索分布式整体包括几大组件:shard(核心检索逻辑和索引分片)searcher(检索和请求分发)indexbuild(离线索引构建)search-client(服务发现客户端)搜索分布式框架:5.分片模块搜索的分片模块是整个搜索引擎的核心部分,其主要功能包括各个独立的检索单元。主要框架模块包括以下几个部分:5.1IndexSearch的索引包括多种类型每种类型的数据结构都不同。目前已有的内部索引包括正向索引、倒排索引、term索引、Tf索引、向量索引等多种索引形式。正向索引Search的正向索引存储了引擎中每个主键ID到每个doc的完整数据的映射。索引的结构是一个Hashmap结构。每个Key都是主键ID的Hash值,这个值指向每个指向fulldoc的指针。引擎内部使用了两种Hashmaps,一种是主键ID到唯一docid的映射,一种是docid到完整doc的指针映射。倒排索引倒排索引本质上是记录Key到各个doc的映射。在检索时,需要保证倒排链具有高效的读写能力。阅读能力有利于高效复杂的检索语法操作,如AND、OR、NOT等复杂操作。同时,发帖链的数据结构也需要高效的写入能力。需要在引擎搜索的同时将实时数据写入引擎。发帖链不可避免的需要修改,所以高效的写作能力也是关键。数组使用数组作为索引结构。优点是读快,逻辑操作也快,缓存友好,但是写操作不好,只能用于离线固定数据,不会增量写入。跳表(SkipList)跳表的数据结构是链表的一种折衷。读写性能中规中矩。CPU的缓存性能比较差,记录单个docid占用的空间比较大,需要两个指针加一个整数。.BitmapBitmap类型使用位来表示二进制信息,Bitmap中的位数作为Key值。搜索引擎的倒排索引结构更适合Bitmap这种数据结构。同时Bitmap的结构对CPU缓存友好,读写操作都非常快,但由于Bitmap记录了所有Keys的状态,包括Bitmap0,空间可能被严重浪费。RoaringBitmapRoaringBitmap是一种具有一定压缩功能的Bitmap结构。除了保留Bitmap的随机读写性能外,合理处理Bitmap中1和0的密度,减少存储空间,整体性能更好。倒排索引的每一种数据结构都有自己适用的场景和数据。总的来说,RoaringBitmap的综合性能更好。ES搜索引擎(Elasticsearch)对这几类倒排索引有详细的测试。有兴趣的同学可以看看每项测试各自的测试结果。TermIndexTerm索引主要用于存储每个字段被切分后的每个Term。因为Term的数量非常多,如果按照正常的方式存储,会造成很大的空间浪费。同时搜索引擎需要前缀搜索,所以Term词的存储需要Satisfy前缀查询。Search的Term词存储使用的数据结构是FST(Finite-StateTransducer)数据结构,对应详细论文地址,FST的数据结构比前缀查询树Trie树更节省空间,查询效率为基本一样。.向量索引内部向量索引是一种特殊的倒排索引。根据不同的近似向量查询算法,产生不同的索引。对于矢量量化算法,首先将训练后的矢量索引聚类成一定数量的倒排索引。行索引,每一个聚类结果形成一个codeID,倒排的行就是这个cluster对应的vector。所以向量索引是一种特殊的倒排索引。5.2查询排序查询模块是Search的核心功能模块,包含了检索的诸多核心业务逻辑,包括自主研发的分词器MusicWs、分析词性分析模块、语法分析及逻辑搜索模块、Search排序框架和缓存模块等一些模块。6.searcher模块searcher模块是Search的核心部分,是shard模块的上游。它的主要功能包括请求的分片、数据的合并和重新排序。searcher的整体结构如下:6.1Query路由Route模块Route模块的主要功能是对请求的原始Query进行水平拆分。Route会根据存储在ZK路径中的分片信息对请求进行分片。比如请求中会包含最大召回截断fulllimit,Route会同时根据fulllimit的值和分片数量进行分配,然后分发到各个分片节点。Merge模块Merge模块对分片返回的数据进行处理和聚合,对每个分片模块返回的数据进行处理和聚合。6.2排序框架搜索器中的排序框架主要是对最终的全局结果进行重新排序。比如最终的歌曲检索会在歌曲中进行统一打分,每个分片都会将歌曲对应的归一化打分上传到searcher模块,最后对打分进行统一排序。同时,排序框架支持自定义开发的评分器和排序插件。7.搜索客户端和服务发现机制Search的服务发现机制是各种服务之间进行通信的核心模块。除了保证正常的RPC数据调用外,还保证业务异常时正常的流量切换调度。搜索服务发现功能模块:搜索服务发现由服务端和客户端两部分组成,通过ZK进行交互。每个集群的机器IP和端口都存储在ZK上,客户端监听路径的变化。当任意一个列表中的IP被删除后,ZK回调客户端感知,客户端切断与本机的流量。同时客户端和服务端之间存在心跳,用于服务端服务卡死等异常情况下的流量切换。8.Search分布式节点的设计有状态的分布式系统最复杂的是异常的处理,包括数据更新和节点异常的处理。对于搜索,数据更新会导致节点上线和下线。包括状态的变化,集群的扩缩容都会引起各个节点的剧烈变化,导致异常。同时,如果某个节点出现问题,也需要集群智能进行处理和路由,所以前期必须设计可靠的处理机制。8.1各节点的设计shard和searcher节点是整个Search系统中最重要的,首先需要设计合理的层次结构来组成整个分布式系统。上图展示了ZK中分片节点的路径分布。按照集群名和应用名逐层分布。路径末端的节点存储了每个分片的分片信息。第一位是总分片,第二位是第一个分片的ID,所有分片的集群IP和端口列表都注册在这个路径下。Searcher服务监听这条路径,获取当前分发的分片的具体个数和对应的分片ID。当需要扩容时,新的节点服务会在更新数据后将自己对应的IP和端口注册到新的节点上。随着旧的分片机逐渐将数据更新到新的分片上,对应的旧节点中分片集群中的IP越来越少,最后都逐渐迁移到新的节点上。这是为了完成扩展。同理,在缩容时,分片节点进行逆向操作,完成缩容。8.2分片节点和搜索器节点的请求设计在分片节点的设计中,不区分主副本,每个副本之前都有请求流量。这样做的考虑是为了提高机器利用率,但是简单的副本价值不大,所以所有的Replicaweight平衡了所有传入的流量。在部署的时候,每一行都是一个完整的数据集合和一个整体的最小请求行。并且每一列都是同一个数据集,没有主从之分,任意一个节点上都有流量。当其中一个节点出现问题,如节点崩溃、进程退出时,分片内部机制会在崩溃前主动下线,搜索器会自动将流量分配给剩余的分片节点。9.搜索分布式数据流的设计搜索是一种有状态的检索服务。会有一直写入的实时数据和每天或每小时更新的离线数据到引擎。数据的可靠更新非常重要。对于分布式来说,各个分片的输出更新和实时数据的写入是非常重要的环节。该引擎分为实时和离线。在引擎构建系统中,原始数据会按照中台设置的分片总数进行平均分片。分片逻辑是根据每条数据的主键ID取Hash,然后全等,然后为构建系统构建索引,将最终构建的索引放在Search的HDFS路径下。实时数据经过Kafka聚合后,每个shard会统一消费Kafka中的数据,然后根据数据中的主键ID进行Hash判断是否是自己所属的shard,最后判断是否属于被写入它所属的索引。对于一致性处理,由于同一个分片中多个副本的消费速度不同,理论上只能保证同一个分片中多个副本的最终一致性,即某个时刻有一条数据。先到达一个分片的那一刻,会先被检索,同一个搜索词在其他分片中可能检索不到,但这种情况几乎察觉不到,因为多副本的消费速度在每秒处理几十个几千到几十万的数据,也就是说Search的增量写入能力小于1ms,除非其中一个节点的网络有问题或者磁盘异常,才会有写入的问题,最终会出现一些节点数据检索异常,但是这些异常会通过告警及时报警,节点会进行处理。10.小结本文主要总结了搜索引擎的分布式设计与实现。主要的重要部分是如何设计有状态的分布式系统。最重要的核心部分是如何实现对每个节点的状态变化进行处理,对数据进行合理的分片和处理。其中ZK的路径节点设计、自动扩缩容的实现、客户端的服务发现、状态感知功能都是核心部分。