云搜索分布式索引一致性设计
概述
目前流行的云搜索服务的主要特点有:
1:接入快捷、运维能力强,提供了可视化运维数据、多维立体化监控和自助工具界面,实现全托管自运维。
2:服务可靠、高可用。索引系统采用多副本数据和服务冗余、不同级别的隔离、集群联邦和资源预留等容灾机制,确保高可用性。宕机时副本自动迁移、恢复服务、同步索引数据,无需手工介入,做到用户无感知。
3:文档属性丰富,搜索方式多样化。在索引服务流程中文档被嵌入了丰富信息,以便支持集团各业务线多种应用场景,如:文档基础召回功能、文本检索领域、地理位置信息、facet查询方式、排序复杂表达式的便捷定制。
云搜索引模块系统架构
图1 云搜索引系统架构
云搜索引模块的功能为将用户发送的数据从接口层传输到内核层,在内核层转换成索引数据并落地,整个过程需要保证数据完整一致和低延时。简而言之,索引系统的设计原则可以总结为两点:一是保证数据正确和数据完整,二是保证数据有较高的实时性。下面通过架构图,来简单介绍索引系统。
如架构图所示,索引系统主要分成五层:
(一)、接入层。接口提供增删改查操作。在功能上,接入层会对用户发送的文档作格式校验,并通过返回码告知服务响应状态。同时所有的数据请求的都会上传到日志服务器上,方便问题定位和故障恢复。
(二)、数据库层。可采用各大公司自研的分布式kv、klist存储系统作为存储数据库,这一层在索引系统中的作用有两点:一是作为搜索的摘要库,二是提供全量数据来源。
(三)、消息队列层。鉴于Kafka具有高吞吐和支持回放等能力,在消息队列上我们选择了Kafka作为云搜的MQ组件。消息队列层的作用也主要有两点:一是解耦上下游系统,二是为索引补发和实时索引提供数据来源。
(四)、业务层。在索引系统中,业务逻辑主要包含特征信息的添加、NLP分词和字段转换。业务层逻辑相对简单,处理速度非常快,不会成为性能瓶颈。
(五)、索引内核层。本模块可自选流行索引内核实现方式,不同内核层对理解本文影响不大。
索引数据流介绍
在索引模块中,数据流会经过两大模块,如图2所示,左边图可以称为数据固化模块(固化流),右边可称为数据索引化模块(索引流)。
图2 云搜索引数据流向图
固化流
固化流的作用是将用户文档存储到DB和Kafka,DB数据最终会用作摘要查询和全量数据集。Kafka作为系统中唯一的消息队列组件,会连接索引流,保证实时数据从固化流流向索引流。
目前我们将固化流的操作都封装在一个进程中,以dubbo提供对外服务。由于dubbo服务节点无主从之分,所有节点都能提供写服务。在这种模式下,完全依靠服务端来保证用户文档顺序一致性非常困难。同时我们还需要考虑,DB和Kafka两者的原子性,以及两者的消息一致性。
索引流
索引流的数据源来自于Kafka,每个应用分配一个topic。图2所示的builder对应着架构图1中业务层,是业务层的逻辑进程,采用pipeline模式设计实现。每个builder可看作一个Kafka consumer,独占一个消费组,拥有全部实时消息的消费权力。而每个builder将会分配一个Esearch实例,builder被分配的消息仅会发送给自己的Esearch进程。同时,从图2我们可以看到builder和Esearch进程,宿主在同一个Kubernetes Pod中,供Kubernetes 同时调度。同时,多pod间的关系既可理解为副本也可理解为分片。
对于索引流模块而言,如何保证索引副本的数据一致性、如何消除副本间数据消费延时带来的影响以及如何保证分布式节点的容灾能力,都是我们需要重点解决的问题。
固化流分布式一致性解决方案
上一节介绍了固化流的相关特点,主要提出了2点挑战:
如何保证用户发送消息的顺序一致性。
如何保证DB和Kafka两组件原子性和一致性。
保证用户文档顺序一致
为保证用户文档顺序一致性,我们对固化流的每个环节都做了约束和设计,如图3所示。
图3 固化流一致性设计
固化流一致性解决方案包含四部分设计:
一、多进程(线程)用户自哈希
上文提到,在固化流模块中,由于我们的接口服务(dubbo)是多节点的,并且所有节点均提供写功能,当用户对同一文档高并发的操作时,服务端请求到达的顺序无法和客户端的顺序保证一致。由于在服务端上无法规避此类问题,云搜对这种情况进行了兜底,告知用户在多客户端的情况下需要消息主键自哈希,即将在客户端将同主键的操作hash到同线程内。避免造成顺序不一致。
二、同步阻塞
在第一点保证的前提下,我们再来谈固化流框架内的一致性设计。当固化流的数据接口是同步阻塞接口时,保证客户端返回成功即数据落地,就可以保证数据在索引中的顺序和客户端返回的顺序一致,这是一种简单可靠的理论,固化流正是根据这种理论进行设计。根据图3第三个环节,只要对保证DB和Kafka同时写成功后再返回,就可以认为此次请求是同步完成了。DB集群节点是区分主从的,只会对master节点进行写,所以对DB写不会有顺序性问题,可以认为DB是一个同步接口。那么只要使用Kafka的同步接口即可解决我们的问题,在Kafka的同步阻塞接口测试中,耗时无法满足云搜的要求,所以我们采用了Kafka的高性能接口。使用此接口,会产生如下顺序问题:当对同一个id进行高并发操作时,Kafka队列的顺序可能会乱序(毫秒内的多次同主键操作可能会触发)。在接下来的两环节,这个乱序问题将会被纠正。
三、Kafka分区哈希
在发送Kafka时,我们根据文档主键id做了partition的分区选择,保证同主键只会落到同partition,这对保证Kafka的消费顺序至关重要。
四、库一致验证
针对同主键高并发的场景,我们做了一次数据最终状态的确认,即在下游消费Kafka数据时会检查DB中数据的状态,保证文档的状态和DB库是一致的。当Kafka消息乱序后,此在这一环节能纠正顺序。
原子性保证
由于进行了库验证,以DB为数据一致性标准,则固化流中两组件的数据不一致问题也不存在了。而对于DB和Kafka操作的原子性解决也比较简单,因为两个操作封装在一个进程中,只需要对两个操作加上事务,即可以保证DB和Kafka具备原子性。
同时我们会对两组件进行失败重试,避免由于网络等原因导致数据发送失败。同时,在搜索场景中,对索引数据操作幂等性问题并不敏感,重复操作并不会带来数据问题。
索引流分布式一致性解决方案
在索引流模块中,如何保证索引数据不丢失以及副本间数据一致性是我们需要重点考虑和解决的问题。
在分布式系统一致性的解决方案中,比较经典的思路有二阶段提交协议(2pc)和三阶段提交协议(3pc)。索引流保证副本数据一致性借鉴了3pc的思路,3pc的协议可简单描述如下:
一、询问阶段。协调者询问参与者是否可以完成指令,协调者只需要回答是还是不是,而不需要做真正的操作,这个阶段超时导致中止。
二、准备阶段。如果在询问阶段所有的参与者都返回可以执行操作,协调者向参与者发送预执行请求,然后参与者写redo和undo日志,执行操作,但是不提交操作;如果在询问阶段任何参与者返回不能执行操作的结果,则协调者向参与者发送中止请求。
三、提交阶段。如果每个参与者在准备阶段返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源。
在索引流中,协调者是Kubernetes Watcher等相关组件,参与者是各个副本节点。索引流的3pc协议实现可以用下图描述。
图4 索引流全量流程3pc设计
图4比较详细的展示了多副本数据同步的过程,这里简单总结索引流的3pc设计思想:多副本数据沟通协调,以第一个副本为数据基准,通过拷贝获得全量数据,保证了副本间的全量数据一致性。注意,如果没有副本,则创建一个副本进行全量重建,重建完成后重新开始上述协议。
除了全量数据,在节点运行期间,大部分时间索引数据更新都来自实时数据,索引流的实时数据源来自固化流的Kafka。为了保证实时分布式数据一致性,我们对builder做了补偿设计。实时系统允许短时间内副本间实时索引不一致,但是最终通过补偿,各个副本之间数据将会最终一致,整个设计如图5所示。
图5 索引流实时流程补偿设计
为了避免短时间内的副本数据不一致影响用户体验,在查询环节,我们对query做了hash选中索引节点的策略,即同一query多次查询只会请求到同一后端索引服务节点,做到用户无感知。
总结
本文重点介绍了云搜索引模块,主要包括对索引模块架构介绍和阐述了索引创建过程中分布式一致性问题的解决方案。由于近年来微服务盛行,各大公司单体架构都逐步扩展成分布式架构,如何解决分布式一致性也成为必须攻克的难题。从目前经典的分布式一致性解决案例来看,所有的设计都必须结合自身业务特点,无法找到通用的解决办法。在此,欢迎对分布式系统设计和搜索感兴趣的同学和我们一起交流。