标签:一对一 合成 bad ike sea count 关系 批量提交 image
Lucene是一种高性能、可伸缩的信息搜索(IR)库,在2000年开源,最初由鼎鼎大名的Doug Cutting开发,是基于Java实现的高性能的开源项目。Lucene采用了基于倒排表的设计原理,可以非常高效地实现文本查找,在底层采用了分段的存储模式,使它在读写时几乎完全避免了锁的出现,大大提升了读写性能。
Elasticsearch基于lucene,隐藏其复杂性,并提供简单易用的restful API接口、java API接口。所以理解ES的关键在于理解lucene的基本原理。
Lucene的写流程和读流程如图5-1所示。
其中,虚线箭头(a、b、c、d)表示写索引的主要过程,实线箭头(1-9)表示查询的主要过程。
Lucene中的主要模块(见图5-1)及模块说明如下:
下面介绍Lucene中的核心术语。
??Term:是索引里最小的存储和查询单元,对于英文来说一般是指一个单词,对于中文来说一般是指一个分词后的词。
??词典(Term Dictionary,也叫作字典):是Term的集合。词典的数据结构可以有很多种,每种都有自己的优缺点,比如:排序数组通过二分查找来检索数据:HashMap(哈希表)比排序数组的检索速度更快,但是会浪费存储空间;fst(finite-state transducer)有更高的数据压缩率和查询效率,因为词典是常驻内存的,而fst有很好的压缩率,所以fst在Lucene的最新版本中有非常多的使用场景,也是默认的词典数据结构。
??倒排序(Posting List):一篇文章通常由多个词组成,倒排表记录的是某个词在哪些文章中出现过。
??正向信息:原始的文档信息,可以用来做排序、聚合、展示等。
??段(segment):索引中最小的独立存储单元。一个索引文件由一个或者多个段组成。在Luence中的段有不变性,也就是说段一旦生成,在其上只能有读操作,不能有写操作。
Lucene的底层存储格式如图5-2所示。图5-2由词典和倒排序两部分组成,其中的词典就是Term的集合。词典中的Term指向的文档链表的集合,叫做倒排表。词典和倒排表是Lucene中很重要的两种数据结构,是实现快速检索的重要基石。词典和倒排表是分两部分存储的,在倒排序中不但存储了文档编号,还存储了词频等信息。
在图5-2所示的词典部分包含三个词条(Term):elasticsearch、lucene和solr。词典数据是查询的入口,所以这部分数据是以fst的形式存储在内存中的。
在倒排表中,“lucene”指向有序链表3,7,15,30,35,67,表示字符串“lucene”在文档编号为3、7、15、30、35、67的文章中出现过,elasticsearch和solr同理。
在Lucene的查询过程中的主要检索方式有以下四种。
指对一个Term进行查询。比如,若要查找包含字符串“lucene”的文档,则只需在词典中找到Term“lucene”,再获得在倒排表中对应的文档链表即可。
指对多个集合求交集。比如,若要查找既包含字符串“lucene”又包含字符串“solr”的文档,则查找步骤如下。
(1)在词典中找到Term “lucene”,得到“lucene”对应的文档链表。
(2)在词典中找到Term “solr”,得到“solr”对应的文档链表。
(3)合并链表,对两个文档链表做交集运算,合并后的结果既包含“lucene”也包含“solr”。
指多个集合求并集。比如,若要查找包含字符串“luence”或者包含字符串“solr”的文档,则查找步骤如下。
(1)在词典中找到Term “lucene”,得到“lucene”对应的文档链表。
(2)在词典中找到Term “solr”,得到“solr”对应的文档链表。
(3)合并链表,对两个文档链表做并集运算,合并后的结果包含“lucene”或者包含“solr”。
指对多个集合求差集。比如,若要查找包含字符串“solr”但不包含字符串“lucene”的文档,则查找步骤如下。
(1)在词典中找到Term “lucene”,得到“lucene”对应的文档链表。
(2)在词典中找到Term “solr”,得到“solr”对应的文档链表。
(3)合并链表,对两个文档链表做差集运算,用包含“solr”的文档集减去包含“lucene”的文档集,运算后的结果就是包含“solr”但不包含“lucene”。
通过上述四种查询方式,我们不难发现,由于Lucene是以倒排表的形式存储的,所以在Lucene的查找过程中只需在词典中找到这些Term,根据Term获得文档链表,然后根据具体的查询条件对链表进行交、并、差等操作,就可以准确地查到我们想要的结果,相对于在关系型数据库中的“like”查找要做全表扫描来说,这种思路是非常高效的。虽然在索引创建时要做很多工作,但这种一次生成、多次使用的思路也是非常高明的。
在早期的全文检索中为整个文档集合建立了一个很大的倒排索引,并将其写入磁盘中,如果索引有更新,就需要重新全量创建一个索引来替换原来的索引。这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证实效性。
现在,在搜索中引入了段的概念(将一个索引文件拆分为多个子文件,则每个子文件叫做段),每个段都是一个独立的可被搜索的数据集,并且段具有不变性,一旦索引的数据被写入硬盘,就不可修改。
在分段的思想下,对数据写操作的过程如下。
段不可变性的优点如下:
段不可变性的缺点如下:
为了提升写的性能,Lucene并没有每新增一条数据就增加一个段,而是采用延迟写的策略,每当有新增的数据时,就将其先写入内存中,然后批量写入磁盘中。若有一个段被写到硬盘,就会生成一个提交点,提交点就是一个用来记录所有提交后的段信息的文件。一个段一旦拥有了提交点,就说明这个段只有读到权限,失去了写的权限;相反,当段在内存中时,就只有写数据的权限,而不具备读数据的权限,所以也就不能被检索了。从严格意义上来说,Lucene或者Elasticsearch并不能被称为实时的搜索引擎,只能被称为准实时的搜索引擎。
写索引的流程如下:
(1)新数据被写入时,并没有被直接写到硬盘中,而是被暂时写到内存中。Lucene默认是一秒钟,或者当内存中数据量达到一定阶段时,再批量提交到磁盘中,当然,默认的时间和数据量的大小是可以通过参数控制的。通过延时写的策略,可以减少数据往磁盘上写的次数,从而提升整体的写入性能。如图5-7所示。
(2)在达到出触发条件以后,会将内存中缓存的数据一次性写入磁盘中,并生成提交点。
(3)情况内存,等待新的数据写入。如图5-8所示。
从上述流程可以看出,数据先被暂时缓存在内存中,在达到一定的条件再被一次性写入硬盘中,这种做法可以大大提升数据写入的书单。但是数据先被暂时存放在内存中,并没有真正持久化到磁盘中,所以如果这时出现断电等不可控的情况,就会丢失数据,为此,Elasticsearch添加了事务日志,来保证数据的安全,参见5.2.3节。
虽然分段比每次都全量创建索引有更高的效率,但是由于在每次新增数据时都会新增一个段,所以经过长时间的的积累,会导致在索引中存在大量的段,当索引中段的数量太多时,不仅会严重消耗服务器的资源,还会影响检索的性能。
因为索引检索的过程是:查询所有段中满足查询条件的数据,然后对每个段里查询的结果集进行合并,所以为了控制索引里段的数量,我们必须定期进行段合并操作。但是如果每次合并全部的段,则会造成很大的资源浪费,特别是“大段”的合并。所以Lucene现在的段合并思路是:根据段的大小将段进行分组,再将属于同一组的段进行合并。但是由于对于超级大的段的合并需要消耗更多的资源,所以Lucene会在段的大小达到一定规模,或者段里面的数据量达到一定条数时,不会再进行合并。所以Lucene的段合并主要集中在对中小段的合并上,这样既可以避免对大段进行合并时消耗过多的服务器资源,也可以很好地控制索引中段的数量。
段合并的主要参数如下:
段合并相关的动作主要有以下两个:
在段合并前对段的大小进行了标准化处理,通过
logMergeFactorSegmentSize
计算得出,其中MergeFactor表示一次合并的段的数量,Lucene默认该数量为10;SegmentSize表示段的实际大小。通过上面的公式计算后,段的大小更加紧凑,对后续的分组更加友好。
段分组的步骤如下:
(1)根据段生成的时间对段进行排序,然后根据上述标准化公式计算每个段的大小并且存放到段信息中,后面用到的描述段大小的值都是标准化后的值。如图5-9所示。
(2)在数组中找到最大的段,然后生成一个由最大段的标准化值作为上线,减去LEVEL_LOG_SPAN(默认值为0.75)后的值作为下限的区间,小于等于上限并且大于下限的段,都被认为是属于同一组的段,可以合并。
(3)在确定一个分组的上下限值后,就需要查找属于这个分组的段了,具体过程是:创建两个指针(在这里使用指针的概念是为了更好地理解)start和end,start指向数组的第1个段,end指向第start+MergeFactor个段,然后从end逐个向前查找落在区间的段,当找到第1个满足条件的段时,则停止,并把当前段到start之间的段统一分到一个组,无论段的大小是否满足当前分组的条件。如图5-10所示,第2个段明显小于该分组的下限,但还是被分到了这一组。
这样做的好处如下:
(4)在分组找到后,需要排除不参加合并的“超大”段,然后判断剩余的段是否满足合并的条件,如图5-10所示,mergeFactor=5,而找到的满足合并条件的段的个数为4,所以不满足合并的条件,暂时不进行合并,继续找寻下一个组的上下限。
(5)由于在第4步并没有找到满足段合并的段的数量,所以这一分组的段不满足合并的条件,继续进行下一分组段的查找。具体过程是:将start指向end,在剩下的段(从end指向的元素开始到数组的最后一个元素)中中寻找最大的段,在找到最大的值后再减去LEVEL_LOG_SPAN的值,再生成一下分组的区间值;然后把end指向数组的第start+MergeFactor个段,逐个向前查找第1个满足条件的段:重复第3步和第4步。
(6)如果一直没有找到满足合并条件的段,则一直重复第5步,直到遍历完整个数组。如果如图5-11所示。
(7)在找到满足条件的mergeFactor个段时,就需要开始合并了。但是在满足合并条件的段大于mergeFactor时,就需要进行多次合并,也就是说每次依然选择mergeFactor个段进行合并,直到该分组的所有段合并完成,再进行下一分组的查找合并操作。
(8)通过上述几步,如果找到了满足合并要求的段,则将会进行段的合并操作。因为索引里面包含了正向信息和反向信息,所以段合并的操作分为两部分:一个是正向信息合并,例如存储域、词向量、标准化因子等;一个是反向信息的合并,例如词典、倒排表等。在段合并时,除了需要对索引数据进行合并,还需要移除段中已经删除的数据。
我们在前面了解到,Lucene的查询过程是:首先在词典中查找每个Term,根据Term获得每个Term所在的文档链表;然后根据查询条件对链表做交、并、差等操作,链表合并后的结果集就是我们要查找的数据。这样做可以完全避免对关系型数据库进行全表扫描,可以大大提升查询效率。但是,当我们一次查询出很多数据时,这些数据和我们的查询条件又有多大关系呢?其文本相似度是多少?本节会回答这个问题,并介绍Lucene最经典的两个文本相似度算法:基于向量空间模型的算法和基于概率的算法(BM25)。
如果对此算法不太感兴趣,那么只需了解对文本相似度有影响的因子有哪些,哪些是正向的,哪些是逆向的即可,不需要理解每个算法的推理过程。但是这两个文本相似度算法有很好的借鉴意义。
Elasticsearch是使用Java编写的一种开源搜索引擎,它在内部使用Luence做索引与搜索,通过对Lucene的封装,提供了一套简单一致的RESTful API。Elasticsearch也是一种分布式的搜索引擎架构,可以很简单地扩展到上百个服务节点,并支持PB级别的数据查询,使系统具备高可用和高并发性。
Elasticsearch的核心概念如下:
node.master=false
node.data=false
tribe:
one:
cluster.name: cluster_one
two:
cluster.name: cluster_two
因为Tribe Node要在Elasticsearch 7.0以后移除,所以不建议使用。
共识性是分布式系统中最基础也最主要的一个组件,在分布式系统中的所有节点必须对给定的数据或者节点的状态达成共识。虽然现在有很成熟的共识算法如Raft、Paxos等,也有比较成熟的开源软件如Zookeeper。但是Elasticsearch并没有使用它们,而是自己实现共识系统zen discovery。Elasticsearch之父Shay Banon解释了其中主要的原因:“zen discovery是Elasticsearch的一个核心的基础组件,zen discovery不仅能够实现共识系统的选择工作,还能够很方便地监控集群的读写状态是否健康。当然,我们也不保证其后期会使用Zookeeper代替现在的zen discovery”。zen discovery模块以“八卦传播”(Gossip)的形式实现了单播(Unicat):单播不同于多播(Multicast)和广播(Broadcast)。节点间的通信方式是一对一的。
Elasticsearch是一个分布式系统。写请求在发送到主分片时,同时会以并行的形式发送到备份分片,但是这些请求的送达时间可能是无序的。在这种情况下,Elasticsearch用乐观并发控制(Optimistic Concurrency Control)来保证新版本的数据不会被旧版本的数据覆盖。
乐观并发控制是一种乐观锁,另一种常用的乐观锁即多版本并发控制(Multi-Version Concurrency Control),它们的主要区别如下:
Elasticsearch集群保证写一致性的方式是在写入前先检查有多少个分片可供写入,如果达到写入条件,则进行写操作,否则,Elasticsearch会等待更多的分片出现,默认为一分钟。
有如下三种设置来判断是否允许写操作:
在Elasticsearch集群中主节点通过ping命令来检查集群中的其他节点是否处于可用状态,同时非主节点也会通过ping来检查主节点是否处于可用状态。当集群网络不稳定时,有可能会发生一个节点ping不通Master节点,则会认为Master节点发生了故障,然后重新选出一个Master节点,这就会导致在一个集群内出现多个Master节点。当在一个集群中有多个Master节点时,就有可能会导致数据丢失。我们称这种现象为脑裂。在5.4.7节会介绍如何避免脑裂的发生。
我们在5.1节了解到,Lucene为了加快写索引的速度,采用了延迟写入的策略。虽然这种策略提高了写入的效率,但其最大的弊端是,如果数据在内存中还没有持久化到磁盘上时发生了类似断电等不可控情况,就可能丢失数据。为了避免丢失数据,Elasticsearch添加了事务日志(Translog),事务日志记录了所有还没有被持久化磁盘的数据。
Elasticsearch写索引的具体过程如下。
首先,当有数据写入时,为了提升写入的速度,并没有数据直接写在磁盘上,而是先写入到内存中,但是为了防止数据的丢失,会追加一份数据到事务日志里。因为内存中的数据还会继续写入,所以内存中的数据并不是以段的形式存储的,是检索不到的。总之,Elasticsearch是一个准实时的搜索引擎,而不是一个实时的搜索引擎。此时的状态如图5-14所示。
然后,当达到默认的时间(1秒钟)或者内存的数据达到一定量时,会触发一次刷新(Refresh)。刷新的主要步骤如下。
(1)将内存中的数据刷新到一个新的段中,但是该段并没有持久化到硬盘中,而是缓存在操作系统的文件缓存系统中。虽然数据还在内存中,但是内存里的数据和文件缓存系统里的数据有以下区别。
(2)打开保存在文件缓存系统中的段,使其可被搜索。
(3)清空内存,准备接收新的数据。日志不做清空处理。
此时的状态如图5-15所示。
最后,刷新(Flush)。当日志数据的大小超过512MB或者时间超过30分钟时,需要触发一次刷新。刷新的主要步骤如下。
删除旧的日志,创建一个空的日志。
此时的状态如图5-17所示。
由上面索引创建的过程可知,内存里面的数据并没有直接被刷新(Flush)到硬盘中,而是被刷新(Refresh)到了文件缓存系统中,这主要是因为持久化数据十分耗费资源,频繁地调用会使写入的性能急剧下降,所以Elasticsearch,为了提高写入的效率,利用了文件缓存系统和内存来加速写入时的性能,并使用日志来防止数据的丢失。
在需要重启时,Elasticsearch不仅要根据提交点去加载已经持久化过的段,还需要根据Translog里的记录,把未持久化的数据重新持久化到磁盘上。
根据上面对Elasticsearch,写操作流程的介绍,我们可以整理出一个索引数据所要经历的几个阶段,以及每个阶段的数据的存储方式和作用。如图5-18所示。
假设我们有如图5-19所示(图片来自官网)的一个集群,该集群由三个节点组成(Node 1、Node 2和Node 3),包含一个由两个主分片和每个主分片由两个副本分片组成的索引。其中,标星号的Node 1是Master节点,负责管理整个集群的状态;p1和p2是主分片;r0和r1是副本分片。为了达到高可用,Master节点避免将主分片和副本放在同一个节点。
将数据分片是为了提高可处理数据的容量和易于进行水平扩展,为分片做副本是为了提高集群的稳定性和提高并发量。在主分片挂掉后,会从副本分片中选举出一个升级为主分片,当副本升级为主分片后,由于少了一个副本分片,所以集群状态会从green改变为yellow,但是此时集群仍然可用。在一个集群中有一个分片的主分片和副本分片都挂掉后,集群状态会由yellow改变为red,集群状态为red时集群不可正常使用。
由上面的步骤可知,副本分片越多,集群的可用性就越高,但是由于每个分片都相当于一个Lucene的索引文件,会占用一定的文件句柄、内存及CPU,并且分片间的数据同步也会占用一定的网络带宽,所以,索引的分片数和副本数并不是越多越好。
写索引时只能写在主分片上,然后同步到副本上,那么,一个数据应该被写在哪个分片上呢?如图5-19所示,如何知道一个数据应该被写在p0还是p1上呢答案就是路由(routing),路由公式如下:
shard = hash(routing)%number_of_primary_shards
其中,routing是一个可选择的值,默认是文档的_id(文档的唯一主键,文档在创建时,如果文档的_id已经存在,则进行更新,如果不存在则创建)。后面会介绍如何通过自定义routing参数使查询落在一个分片中,而不用查询所有的分片,从而提升查询的性能。routing通过hash函数生成一个数字,将这个数字除以number_of_primary_shards(分片的数量)后得到余数。这个分布在0到number_of_primary_shards - 1之间的余数,就是我们所寻求的文档所在分片的位置。这也就说明了一旦分片数定下来就不能再改变的原因,因为分片数改变之后,所有之前的路由值都会变得无效,前期创建的文档也就找不到了。
由于在Elasticsearch集群中每个节点都知道集群中的文档的存放位置(通过路由公式定位),所以每个节点都有处理读写请求的能力。在一个写请求被发送到集群中的一个节点后,此时,该节点被称为协调点(Coordinating Node),协调点会根据路由公式计算出需要写到哪个分片上,再将请求转发到该分片的主分片节点上。写操作的流程如下(键图5-20,图片来自官网)。
(1)客户端向Node 1(协调节点)发送写请求。
(2)Node 1通过文档的_id(默认是_id,但不表示一定是_id)确定文档属于哪个分片(在本例中是编号为0的分片)。请求会被转发到主分片所在的节点Node 3上。
(3)Node 3在主分片上执行请求,如果成功,则将请求并行转发到Node 1和Node 2的副本分片上。一旦所有的副本分片都报告成功(默认),则Node 3将向协调节点报告成功,协调节点向客户端报告成功。
根据routing字段进行的单个文档的查询,在Elasticsearch集群中可以在主分片或者副本分片上进行。查询字段刚好是routing的分片字段如“_id”的查询流程如下(见图5-21,图片来自官网)。
(1)客户端向集群发送查询请求,集群再随机选择一个节点作为协调点(Node 1),负责处理这次查询。
(2)Node 1使用文档的routing id来计算要查询的文档在哪个分片上(在本例中落在了0分片上)分片0的副本分片存在所有的三个节点上。在这种情况下,协调节点可以把请求转发到任意节点,本例将请求转发到Node 2上。
(3)Node 2执行查找,并将查找结果返回给协调节点Node 1,Node 1再将文档返回给客户端。
如果需要給我修改意见的发送邮箱:erghjmncq6643981@163.com
资料参考:《可伸缩服务架构》
转发博客,请注明,谢谢。
走过路过不要错过,您的支持是我持续技术输出的动力所在,金额随意,感谢!!
标签:一对一 合成 bad ike sea count 关系 批量提交 image
原文地址:https://www.cnblogs.com/bigben0123/p/11225809.html