【转】一种基于Lucene的实时搜索方案 - From 淘宝技术部

http://www.tuicool.com/articles/NZ7v6b数据库

 

背景

阿里集团各大业务快速发展过程当中都对搜索服务不少刚性的须要,而这样的搜索需求有着很是明显的特征:快速支持、低成本、实时性和稳定性。缓存

快速支持:性能优化

业务需求急迫、须要一周甚至几天内完成索引服务搭建、测试、上线环节。服务器

低成本:架构

搜索需求方要求接入便捷,低成本的机器和运维成本。框架

实时性:运维

搜索需求方的业务数据发生变化,须要实时在索引中进行更新可见,而这个过程一般须要稳定的保证在100ms内。异步

稳定性:分布式

搜索服务集群不会由于升级、运维操做或者若干台机器的宕机致使搜索服务不稳定。oop

面临的挑战

海量数据

阿里的业务基本动辄就是上10亿上百亿规模,那么如何能行之有效的管理索引和保障搜索性能将是一个很是具备挑战性的工做。

业务复杂

互联网业务负责,那么涉及到须要进行检索的源数据可能来自多个数据库多个表,也就意味着索引数据源可能来源于多库多表,而且表和表之间还有1:N,N:N的关系。而使用过Lucene的同窗们都知道,对于基于Lucene为引擎内核的更新都是完整记录更新,因此对于咱们产品来讲构建索引时候避免不了多表join凑整更新问题。

实时性

在阿里的众多搜索需求场景下,不少时候是把搜索服务当作数据库使用,也就意味着一个页面上产生的更新操做,页面跳转后就能看到最新更新的结果,那么对于咱们搜索服务来讲须要在毫秒级别将更新结果可见。

高可用性

阿里的业务对稳定性的要求是很是高的,因此对于升级、扩容、机器宕机等状况都不能影响正常的搜索和更新服务。

可伸缩性

电商业务一年下来有很是多的大促活动,而一旦有大促活动,搜索的QPS将会比日常翻几倍。因此若是以大促的成本去衡量支持服务,整个服务所须要的机器在平时将很是空闲,从而形成了成本极大浪费。那么针对这种状况,就须要咱们提供一种灵活的能够在线伸缩服务吞吐量的技术:即大促前临时加机器来支持突增的流量,大促后随时下线扩容机器,而整个过程不影响正常的搜索服务。

由于本文篇幅有限,在这里我只会着重介绍:实时性、高可用性在咱们产品中的一些技术实践。

实时解决方案

在介绍咱们产品方案以前,首先介绍下业内常见的实时解决方案,见图1-1实时架构图:

图1-1

图1-1

该方案通常是由:

  • 内存索引(Ram-IndexA)负责数据更新。
  • 内存索引(Ram-IndexA)达到阀值,角色转换成待合并内存索引(Ram-IndexB),同时从新开辟一块新的内存索引(Ram-IndexA)负责新的更新写入,老的内存索引(Ram-IndexB)合而且优化到主索引中。
  • 内存索引(Ram-IndexA)+磁盘索引(Full-IndexA)提供检索服务。

基于该方案能够带来的最大的优点是内存索引能够合并到主索引,避免了索引碎片,从而能够屏蔽全量索引从新构建和保障搜索服务的性能稳定性。可是这种方案仍然会存在如下问题:

  • 生产者(写入量)远远大于消费能力(内存索引构建),影响上游系统(数据生成方)的稳定性。
  • 内存索引合并磁盘主索引并执行优化过程时间较长,若是该过程出现宕机,重启机器后存在丢失数据的可能性。
  • 内存索引和主磁盘索引合并后,主索引是须要从新打开才能让更新可见,而对于大的磁盘索引从新打开一次耗时是比较长,由于须要从新预热数据到内存中。那么对实时性要求很高的须要明显是不合理的。
  • 另外为了保证从新打开主索引视图期间查询是不中断,也就意味着一份大的磁盘索引的资源视图须要被同时打开2份。那么就意味着承载该主索引的机器资源是须要实际承载主索引2倍以上资源才能知足。
  •  大索引的合并并优化的过程对机器IO资源占用较大,而自己搜索服务自己就是IO密集型的应用,因此合并主索引并优化一定对搜索服务稳定性带来影响。
  • 由于要数据更新可见,因此须要频繁的从新打开合并后的磁盘大索引,这样会致使大索引对应的优化Cache出现频繁清空,从新加载的问题。从而使得为性能优化而设置的Cache访问命中率将很是低,这样对于一些复杂的统计查询带来性能上极大不稳定性,同时Cache若是自己所占内存过大,还会带来JVM频繁FullGC的影响。

因此为了单纯追求系统某个指标值(如永远不须要作全量),而牺牲掉系统稳定性是得不偿失的。固然上述某些缘由可能在一些好的硬件配置机器下并不会暴露的特别明显,可是从技术架构的机器成本上考虑的话,上述设计方案就不是一种特别合适的方案。那么咱们产品平台便采起了一种更低成本更稳定的实时架构方案来解决上述问题,其主要思路:

  • 采用WAL机制保证上游系统写入磁盘的源数据不丢失,机器宕机重启保证让机器数据快速恢复到宕机前一致。
  • 全部的更新操做只会发生在内存索引,但内存索引不会无限扩大,知足系统设置阀值后就会刷入磁盘,一旦刷入磁盘的索引将不会发生从新打开(只存在标记删除操做),经过这种屏蔽从新打开磁盘索引的操做,也就解决了前面提到因为主索引频繁从新打开致使的查询实时性、资源占用峰值、Cache的命中率、FullGC的问题。
  • 内存索引直接刷磁盘生成子索引,再也不去合并主索引,避免大的主索引须要从新打开,同时子索引的大小可控,基本在100MB以内,避免从新打开子索引很是慢。
  • 子索引数目增多,也就意味着冗余数据变多、查询遍历更多的文件,那么性能一定会形成影响,因此子索引必须经过一种合并策略进行合并优化。咱们产品采起的策略由合并因子和阀值影响,例如:当相同大小的100MB子索引达到10个的时候,触发合并。虽然触发合并后,仍然会带来IO争用,但由于子索引体积小,因此合并优化时间较快,那么对搜索服务的影响基本不存在。另外当子索引合并生成的新子索引达到必定大小时候,合并策略将不会将其归入下次待合并列表中,即永远不会再参与合并。

基于上述的思路,我将着重在实时更新处理、实时索引体系两个方面来跟你们介绍下咱们产品。

实时更新处理

WAL日志

大型分布式系统中故障很常见,设想一下,若是内存索引没有刷写,服务器就宕机了。内存中没有写于硬盘的数据就会丢失。因此咱们的分布式实时搜索产品应对的办法是在写内存索引以前先写入WAL(Write-Ahead Logging,预写式日志)。其写入流程以下:

  • 将WAL日志以追加写的方式写入磁盘日志文件中
  • 将WAL日志的修改操做做用到内存索引中
  •  返回操做成功或者失败

如上所示,在修改内存索引元素以前,要确保与这一个修改相关的操做日志必需要刷入磁盘中。若是检索服务器宕机,没有从RamIndex刷写入Disk的数据将能够经过回放WAL来恢复。而这个过程并不须要人为参与,检索节点内部机制中有恢复流程来处理。

批提交

通常而言搜索系统是须要将WAL日志刷入磁盘才能够构建内存索引的,可是若是每一个事务都要求将日志当即刷入磁盘,系统的吞吐量将会不好。所以,对一致性要求很高的应用,须要当即刷入;相应地,对一致性要求不高的应用,能够考虑不要求当即刷入,首先将WAL日志缓存到内存缓存区中,按期刷入磁盘。可是这种作法有一个问题,若是搜索应用系统意外故障,可能丢失最后一部分更新操做。

批提交(Group Commit,如图1-2批处理流程图)技术是一种有效的优化手段。WAL日志首先写入到系统内容缓存区中:

  •  日志缓存区的数据量超过必定大小,好比128KB;
  • 距离上次刷入磁盘超过必定时间,好比10ms。

当知足以上两个条件中的某一个时,将日志缓存区中多个事务的操做一次性刷入磁盘,接着一次性将多个事务的修改操做逐个返回客户端操做结果。批提交技术保证了WAL日志成功刷入磁盘后,才返回操做结果保障数据的不丢失,虽然牺牲了写事务延时,但大大提升了系统吞吐量。

图1-2

图1-2

CheckPoint检查点

考虑数据写入须要实时可查,那么更新的数据都是在内存索引中,那么可能出现一些问题:

故障恢复时须要回放全部WAL,效率较低。若是WAL超过100GB,那么,故障恢复时间根本没法接受。另外内存有限,内存索引须要达到阀值后转储到磁盘。因此,咱们须要在内存索引转储到磁盘的时候,记录checkpoint时刻的日志回放点,之后故障恢复只须要回放checkpoint时刻日志以后的WAL日志,如图1-3检查点方案流程图所示:

图1-3

图1-3

当机器发送重启,只须要从新加载subindexA、subindexB、subindexC的索引,并重放checkpointC以后的WAL日志,变可以让数据恢复到宕机前一致。

实时索引体系

图1-4

图1-4

根据图1-4实时方案架构图咱们详细说明下实时模式实现流程:

  • 更新操做都会在服务端以WAL落地磁盘
  •  服务端异步线程顺序消费WAL构建成内存索引(Ram-IndexA)
  •  内存索引(Ram-IndexA)大小达到内存阀值将转换角色为Ram-IndexB
  •  从新开辟新的内存索引(Ram-IndexA)负责当前WAL的消费
  •   Ram-IndexB内存索引直接刷入磁盘生成以Index前缀的子索引,如:Index_0,Index_1,Index_2….。
  •  防止索引碎片(Index_0,Index_1…)会愈来愈多从而影响性能,咱们采起一种合并策略能够经过合并因子和索引大小来选出能够合并的小索引进行合并。
  •   达到阀值的子索引将不会在参与合并,那么系统运行一段较长时间后,索引碎片(子索引)也将会愈来愈多,那么系统能够经过从新作一次全量的方式来消除索引碎片带来的影响。

子索引合并策略

前面咱们说到内存索引一旦达到阀值,将被刷入到磁盘,那么磁盘将会存在不少相似index_0、index_一、index2,index_3的索引碎片。若是不对这些索引碎片进行合并,那么随着这些索引碎片的增长,会致使搜索服务性能下降。因此咱们的产品对索引碎片采起了一种合并策略对其进行按期合并。

图1-5-1

图1-5

如图1-5子索引合并流程图所示,内存索引刷入磁盘,将会依次递增的生成index_0,index_1,index_2的磁盘索引碎片。假设当前的合并因子是2,当合并管理器发现存在2个大小一致索引index_0,index_1的时候,变会触发合并操做:index_0和index_1合并成index_3,合并过程当中index_0、index_1依然提供正常服务,当合并操做成功完成,即index_3生成完毕,并对外提供服务。接下来将index_0和index_1的资源引用计数减1,即当基于index_0、index1的查询访问线程结束时,index_0,index_1的资源引用计数为0、索引将正常关闭,这样一个索引碎片合并操做正常结束。可是若是合并过程出现宕机或者异常状况,即当前合并事务未正常结束,那么整个合并过程将会回滚,即index_3被清理,index_0,index_1正常提供服务。固然若是内存索引继续刷到磁盘生成了index_四、index_5,经过合并策略生成了index_6,这个时候发现index_3和index_6又知足了合并条件,那么index_3和index_6又会合并生成index_7。因此经过这种合并策略,小索引碎片逐步会被合并成大索引碎片,可是若是索引碎片越大,那么带来的合并代价也越大,咱们须要设置一个合并阀值,凡是索引碎片达到指定文件大小阀值后,将不会进一步再参与合并,这样就很好的屏蔽了大索引碎片合并代价过大的问题。

全量索引构建

由于前面说到咱们产生索引碎片,而这些索引碎片即便进行了碎片合并而减小碎片数,可是一旦当碎片达到必定大小后就不适合继续进行合并,不然合并代价很大,因此咱们没法避免的会由于碎片问题而致使更新实时性和查询QPS性能损耗问题。因此咱们的解决的办法就是经过一段时间对具体业务所有源数据进行一次构建全量索引DUMP工做,用构建好的新的全量主索引去替换原来老的主索引和磁盘索引,从而让实时更新、搜索服务性能恢复到最佳。

阿里的业务数据规模都很庞大,动辄就上10亿到百亿,那么咱们若是使用Solr原生的基于检索服务节点的索引构建模式会带来2个很大问题:

  • 构建索引就是一个IO密集型的任务,而搜索服务也是IO密集型,那么两个任务若是在一台机器上并存,将会致使双方服务变得都不稳定。
  • 搜索业务数据规模大,致使传统原生构建索引的方式在几十亿数据量规模下所须要的时间特别长,即便深夜访问低峰期开始全量任务,也须要延续到白天甚至是访问高峰期还未结束,从而使得搜索服务出现频繁超时现象。

因此基于上述缘由咱们的搜索平台实现一个分布式全量索引任务调度框架来解决搜索业务全量索引构建的问题。

图1-6

图1-6

如图1-6 DUMP中心架构图所示简单描述下一个业务全量索引构建的流程:

  •  将一个具体业务相关的上下文信息以全量索引构建任务的形式提交给JobNode
  • JobNode根据TaskNode空闲程度,选择好若干个TaskNode并将全量任务下发到具体的TaskNode。JobNode根据了这些上下文信息把一个全量任务分解成若干个TaskNode进行,这样有效的用到分布式并行任务的优点来加速索引构建。
  • 被选择出来的若干个TaskNode根据授予任务的上下文信息,获取业务数据的来源类型和存储地址,(如数据库、Hadoop云梯),而后经过流的方式消费源数据内容并构建成索引。
  • 一个索引任务执行完毕后,将完成的全量索引根据指定的目标存储源进行回流(通常仍是HDFS)。

那么经过这种离线的分布式索引构建中心具体为搜索业务带来什么呢,主要在如下几个方面体现:

  • 完全隔离全量构建索引和搜索服务的耦合性,杜绝资源抢用状况。
  • 实现与业务细节无关的全量构建任务集群,能为接入搜索平台的全部业务进行全量索引构建服务,意味着极大提升机器综合利用率,不用为具体业务搭建具体的全量DUMP集群。
  • 索引构建和检索服务隔离后,DUMP任务节点(TaskNode)能够在最大化利用机器资源,即对底层索引构建细节深刻优化,因此极大提高了海量数据索引构建速度。
  • DUMP中心快速构建海量数据索引并回流索引文件到存储中心的过程,为在线搜索服务无缝扩容和线上故障快速恢复提供了数据来源基础。

其余优化

咱们产品在solr和Lucene上作了不少优化来适应一些业务的需求,本文篇幅有限,因此在这里我主要挑出一个比较有表明性的优化实践:Cache改造。

Cache改造

用过solr的同窗们都知道全部的Cache都是由SolrIndexSearcher来管理的,如图1-7 Searcher结构图所示:

图1-7

图1-7

而在咱们的实时模式下须要让更新的数据实时可见,那么必须近实时的用新的SolrIndexSearcher-new去替换SolrIndexSearcher-old。(如图1-7)而这样实时的替换 也就引起以下问题:

  • solrIndexSearcher的替换,意味着基于solrIndexSearcher层的Cache(如图1-7所示的4种Cache)所有失效,那么意味着毫秒级别会频繁有大空间的内存须要被垃圾回收,最终会触发频繁的FullGC。
  • 若是Cache配置还打开了预热功能(warm),那么新的SolrIndexSearcher在替换以前须要将其管理的新Cache进行预热。数据量若是较大,那么预热时间会较长,从而引起数据实时性可见问题。

因此基于如上的问题,终搜产品从新设计了一些Cache,将Cache的管理由SolrIndexSearcher迁移到IndexReader层中来,如图1-8  Cache结构图所示:

图1-8

图1-8

首先,先阐述下咱们这种优化思路的前置条件,前文中提到咱们内存索引会直接刷磁盘而不用合并到主索引中,这样在磁盘存在的主索引、子索引对应的内存视图对象IndexReader在任什么时候候都不须要从新打开,而以IndexReader管理的Cache一旦建立后将不会被失效,而须要涉及到预加载Cache的过程只是在刷入磁盘或者系统从新启动过程当中一次将配置涉及到的Cache都预加载到内存中,那么以前存在的频繁失效致使GC、预加载慢引发实时性的若干问题都将不复存在。

因此经过将Cache从Searcher层迁移到IndexReader层的设计使得实时模式下的引擎在复杂的统计查询下性能也能获得很好的保证。

总结与展望

本文中咱们深刻的探讨了一种高稳定性实时搜索引擎系统实践,这些实践内容也依托于咱们的产品服务于阿里众多业务线。而目前咱们的产品搜索服务集群已经将近700台,接入业务范围也涵盖整个阿里集团。而这些业务特别是在数据量和访问量的成倍增加的状况下,咱们产品更加须要关注

  • 再也不须要为数据规模和访问规模增加而提心吊胆。
  • 更加合理利用机器资源,搜索服务集群吞吐量能够根据业务实际状况来动态调整。

归根结底其实这些要求是对搜索服务系统的易扩展提出了更高的要求,即如何提供一种无缝的在线扩容方案达到搜索服务吞吐量无上限的目标,而这个目标也正是咱们产品目前正在重点关注的方向,而关于这块的内容但愿有机会在新的文章中跟你们作深刻探讨。

做者简介

柳明(花名:洪震),阿里技术专家,阿里一站式搜索服务平台TSearcher的负责人。目前关注于分布式、高性能、高稳定性的搜索服务领域。

相关文章
相关标签/搜索