Elasticsearch详解

Elasticsearch详解

96 
Chandler_珏瑜 22d8d123 271c 4d80 9c59 6990844a9e37
 5.8 2019.05.05 17:19* 字数 10971 阅读 1147评论 5

5.1 Lucene简介

 Lucene是一种高性能、可伸缩的信息搜索(IR)库,在2000年开源,最初由鼎鼎大名的Doug Cutting开发,是基于Java实现的高性能的开源项目。Lucene采用了基于倒排表的设计原理,能够很是高效地实现文本查找,在底层采用了分段的存储模式,使它在读写时几乎彻底避免了锁的出现,大大提高了读写性能。css

Elasticsearch基于lucene,隐藏其复杂性,并提供简单易用的restful API接口、java API接口。因此理解ES的关键在于理解lucene的基本原理。java

5.1.1 核心模块

 Lucene的写流程和读流程如图5-1所示。node


 
图 5-1 Lucene的写流程和读流程

 其中,虚线箭头(a、b、c、d)表示写索引的主要过程,实线箭头(1-9)表示查询的主要过程。算法

 Lucene中的主要模块(见图5-1)及模块说明以下:数据库

  1. analysis:主要负责词法分析及语言处理,也就是咱们常说的分词,经过该模块可最终造成存储或者搜索的最小单元Term。
  2. index模块:主要负责索引的建立工做。
  3. store模块:主要负责索引的读写,主要是对文件的一些操做,其主要目的是抽象出和平台文件系统无关的存储。
  4. queryParser模块:主要负责语法分析,把咱们的查询语句生成Lucene底层能够识别的条件。
  5. search模块:主要负责对索引的搜索工做。
  6. similarity模块:主要负责相关性打分和排序的实现。

5.1.2 核心术语

 下面介绍Lucene中的核心术语。数组

▪️Term:是索引里最小的存储和查询单元,对于英文来讲通常是指一个单词,对于中文来讲通常是指一个分词后的词。缓存

▪️词典(Term Dictionary,也叫做字典):是Term的集合。词典的数据结构能够有不少种,每种都有本身的优缺点,好比:排序数组经过二分查找来检索数据:HashMap(哈希表)比排序数组的检索速度更快,可是会浪费存储空间;fst(finite-state transducer)有更高的数据压缩率和查询效率,由于词典是常驻内存的,而fst有很好的压缩率,因此fst在Lucene的最新版本中有很是多的使用场景,也是默认的词典数据结构。安全

▪️倒排序(Posting List):一篇文章一般由多个词组成,倒排表记录的是某个词在哪些文章中出现过。性能优化

▪️正向信息:原始的文档信息,能够用来作排序、聚合、展现等。bash

▪️段(segment):索引中最小的独立存储单元。一个索引文件由一个或者多个段组成。在Luence中的段有不变性,也就是说段一旦生成,在其上只能有读操做,不能有写操做。

 Lucene的底层存储格式如图5-2所示。图5-2由词典和倒排序两部分组成,其中的词典就是Term的集合。词典中的Term指向的文档链表的集合,叫作倒排表。词典和倒排表是Lucene中很重要的两种数据结构,是实现快速检索的重要基石。词典和倒排表是分两部分存储的,在倒排序中不但存储了文档编号,还存储了词频等信息。


 
图 5-2 Lucene的底层存储格式

 在图5-2所示的词典部分包含三个词条(Term):elasticsearch、lucene和solr。词典数据是查询的入口,因此这部分数据是以fst的形式存储在内存中的。

 在倒排表中,“lucene”指向有序链表3,7,15,30,35,67,表示字符串“lucene”在文档编号为三、七、1五、30、3五、67的文章中出现过,elasticsearch和solr同理。

5.1.3 检索方式

 在Lucene的查询过程当中的主要检索方式有如下四种。

1. 单个词查询

 指对一个Term进行查询。好比,若要查找包含字符串“lucene”的文档,则只需在词典中找到Term“lucene”,再得到在倒排表中对应的文档链表便可。

2. AND

 指对多个集合求交集。好比,若要查找既包含字符串“lucene”又包含字符串“solr”的文档,则查找步骤以下。

 (1)在词典中找到Term “lucene”,获得“lucene”对应的文档链表。

 (2)在词典中找到Term “solr”,获得“solr”对应的文档链表。

 (3)合并链表,对两个文档链表作交集运算,合并后的结果既包含“lucene”也包含“solr”。

3. OR

 指多个集合求并集。好比,若要查找包含字符串“luence”或者包含字符串“solr”的文档,则查找步骤以下。

 (1)在词典中找到Term “lucene”,获得“lucene”对应的文档链表。

 (2)在词典中找到Term “solr”,获得“solr”对应的文档链表。

 (3)合并链表,对两个文档链表作并集运算,合并后的结果包含“lucene”或者包含“solr”。

4. NOT

 指对多个集合求差集。好比,若要查找包含字符串“solr”但不包含字符串“lucene”的文档,则查找步骤以下。

 (1)在词典中找到Term “lucene”,获得“lucene”对应的文档链表。

 (2)在词典中找到Term “solr”,获得“solr”对应的文档链表。

 (3)合并链表,对两个文档链表作差集运算,用包含“solr”的文档集减去包含“lucene”的文档集,运算后的结果就是包含“solr”但不包含“lucene”。

 经过上述四种查询方式,咱们不难发现,因为Lucene是以倒排表的形式存储的,因此在Lucene的查找过程当中只需在词典中找到这些Term,根据Term得到文档链表,而后根据具体的查询条件对链表进行交、并、差等操做,就能够准确地查到咱们想要的结果,相对于在关系型数据库中的“like”查找要作全表扫描来讲,这种思路是很是高效的。虽然在索引建立时要作不少工做,但这种一次生成、屡次使用的思路也是很是高明的。

5.1.4 分段存储

 在早期的全文检索中为整个文档集合创建了一个很大的倒排索引,并将其写入磁盘中,若是索引有更新,就须要从新全量建立一个索引来替换原来的索引。这种方式在数据量很大时效率很低,而且因为建立一次索引的成本很高,因此对数据的更新不能过于频繁,也就不能保证明效性。

 如今,在搜索中引入了段的概念(将一个索引文件拆分为多个子文件,则每一个子文件叫作段),每一个段都是一个独立的可被搜索的数据集,而且段具备不变性,一旦索引的数据被写入硬盘,就不可修改。

 在分段的思想下,对数据写操做的过程以下。

  • 新增:当有新的数据须要建立索引时,因为段段不变性,因此选择新建一个段来存储新增的数据。
  • 删除:当须要删除数据时,因为数据所在的段只可读,不可写,因此Lucene在索引文件新增一个.del的文件,用来专门存储被删除的数据id。当查询时,被删除的数据仍是能够被查到的,只是在进行文档链表合并时,才把已经删除的数据过滤掉。被删除的数据在进行段合并时才会被真正被移除。
  • 更新:更新的操做其实就是删除和新增的组合,先在.del文件中记录旧数据,再在新段中添加一条更新后的数据。

 段不可变性的优势以下:

  • 不须要锁:由于数据不会更新,因此不用考虑多线程下的读写不一致状况。
  • 能够常驻内存:段在被加载到内存后,因为具备不变性,因此只要内存的空间足够大,就能够长时间驻存,大部分查询请求会直接访问内存,而不须要访问磁盘,使得查询的性能有很大的提高。
  • 缓存友好:在段的声明周期内始终有效,不须要在每次数据更新时被重建。
  • 增量建立:分段能够作到增量建立索引,能够轻量级地对数据进行更新,因为每次建立的成本很低,因此能够频繁地更新数据,使系统接近实时更新。

段不可变性的缺点以下:

  • 删除:当对数据进行删除时,旧数据不会被立刻删除,而是在.del文件中被标记为删除。而旧数据只能等到段更新时才能真正地被移除,这样会有大量的空间浪费。
  • 更新:更新数据由删除和新增这两个动做组成。如有一条数据频繁更新,则会有大量的空间浪费。
  • 新增:因为索引具备不变性,因此每次新增数据时,都须要新增一个段来存储数据。当段段数量太多时,对服务器的资源(如文件句柄)的消耗会很是大,查询的性能也会受到影响。
  • 过滤:在查询后须要对已经删除的旧数据进行过滤,这增长了查询的负担。

 为了提高写的性能,Lucene并无每新增一条数据就增长一个段,而是采用延迟写的策略,每当有新增的数据时,就将其先写入内存中,而后批量写入磁盘中。如有一个段被写到硬盘,就会生成一个提交点,提交点就是一个用来记录全部提交后的段信息的文件。一个段一旦拥有了提交点,就说明这个段只有读到权限,失去了写的权限;相反,当段在内存中时,就只有写数据的权限,而不具有读数据的权限,因此也就不能被检索了。从严格意义上来讲,Lucene或者Elasticsearch并不能被称为实时的搜索引擎,只能被称为准实时的搜索引擎。

 写索引的流程以下:

 (1)新数据被写入时,并无被直接写到硬盘中,而是被暂时写到内存中。Lucene默认是一秒钟,或者当内存中数据量达到必定阶段时,再批量提交到磁盘中,固然,默认的时间和数据量的大小是能够经过参数控制的。经过延时写的策略,能够减小数据往磁盘上写的次数,从而提高总体的写入性能。如图5-7所示。

 (2)在达到出触发条件之后,会将内存中缓存的数据一次性写入磁盘中,并生成提交点。

 (3)状况内存,等待新的数据写入。如图5-8所示。

 
图 5-7 图 5-8 Elasticsearch写索引.png

 从上述流程能够看出,数据先被暂时缓存在内存中,在达到必定的条件再被一次性写入硬盘中,这种作法能够大大提高数据写入的书单。可是数据先被暂时存放在内存中,并无真正持久化到磁盘中,因此若是这时出现断电等不可控的状况,就会丢失数据,为此,Elasticsearch添加了事务日志,来保证数据的安全,参见5.2.3节。

5.1.5 段合并策略

 虽然分段比每次都全量建立索引有更高的效率,可是因为在每次新增数据时都会新增一个段,因此通过长时间的的积累,会致使在索引中存在大量的段,当索引中段的数量太多时,不只会严重消耗服务器的资源,还会影响检索的性能。

 由于索引检索的过程是:查询全部段中知足查询条件的数据,而后对每一个段里查询的结果集进行合并,因此为了控制索引里段的数量,咱们必须按期进行段合并操做。可是若是每次合并所有的段,则会形成很大的资源浪费,特别是“大段”的合并。因此Lucene如今的段合并思路是:根据段的大小将段进行分组,再将属于同一组的段进行合并。可是因为对于超级大的段的合并须要消耗更多的资源,因此Lucene会在段的大小达到必定规模,或者段里面的数据量达到必定条数时,不会再进行合并。因此Lucene的段合并主要集中在对中小段的合并上,这样既能够避免对大段进行合并时消耗过多的服务器资源,也能够很好地控制索引中段的数量。

 段合并的主要参数以下:

  • mergeFactor:每次合并时参与合并的最少数量,当同一组的段的数量达到此值时开始合并,若是小于此值则不合并,这样作能够减小段合并的频率,其默认值为10。
  • SegmentSize:指段的实际大小,单位为字节。
  • minMergeSize:小于这个值的段会被分到一组,这样能够加速小片断的合并。
  • maxMergeSize:如有一段的文本数量大于此值,就再也不参与合并,由于大段合并会消耗更多的资源。

 段合并相关的动做主要有如下两个:

  • 对索引中的段进行分组,把大小相近的段分到一组,主要由LogMergePolicy1类来处理。
  • 将属于同一分组的段合并成一个更大的段。

 在段合并前对段的大小进行了标准化处理,经过

  logMergeFactorSegmentSize

 计算得出,其中MergeFactor表示一次合并的段的数量,Lucene默认该数量为10;SegmentSize表示段的实际大小。经过上面的公式计算后,段的大小更加紧凑,对后续的分组更加友好。

 段分组的步骤以下:

 (1)根据段生成的时间对段进行排序,而后根据上述标准化公式计算每一个段的大小而且存放到段信息中,后面用到的描述段大小的值都是标准化后的值。如图5-9所示。


 
图 5-9 Lucene段排序

 (2)在数组中找到最大的段,而后生成一个由最大段的标准化值做为上线,减去LEVEL_LOG_SPAN(默认值为0.75)后的值做为下限的区间,小于等于上限而且大于下限的段,都被认为是属于同一组的段,能够合并。

 (3)在肯定一个分组的上下限值后,就须要查找属于这个分组的段了,具体过程是:建立两个指针(在这里使用指针的概念是为了更好地理解)start和end,start指向数组的第1个段,end指向第start+MergeFactor个段,而后从end逐个向前查找落在区间的段,当找到第1个知足条件的段时,则中止,并把当前段到start之间的段统一分到一个组,不管段的大小是否知足当前分组的条件。如图5-10所示,第2个段明显小于该分组的下限,但仍是被分到了这一组。


 
图 5-10 Lucene段分组

 这样作的好处以下:

  • 增长段合并的几率,避免因为段的大小良莠不齐致使段难以合并。
  • 简化了查找的逻辑,使代码的运行效率更高。

 (4)在分组找到后,须要排除不参加合并的“超大”段,而后判断剩余的段是否知足合并的条件,如图5-10所示,mergeFactor=5,而找到的知足合并条件的段的个数为4,因此不知足合并的条件,暂时不进行合并,继续找寻下一个组的上下限。

 (5)因为在第4步并无找到知足段合并的段的数量,因此这一分组的段不知足合并的条件,继续进行下一分组段的查找。具体过程是:将start指向end,在剩下的段(从end指向的元素开始到数组的最后一个元素)中中寻找最大的段,在找到最大的值后再减去LEVEL_LOG_SPAN的值,再生成一下分组的区间值;而后把end指向数组的第start+MergeFactor个段,逐个向前查找第1个知足条件的段:重复第3步和第4步。

 (6)若是一直没有找到知足合并条件的段,则一直重复第5步,直到遍历完整个数组。若是如图5-11所示。


 
图 5-11 Lucene段分组二.png

 (7)在找到知足条件的mergeFactor个段时,就须要开始合并了。可是在知足合并条件的段大于mergeFactor时,就须要进行屡次合并,也就是说每次依然选择mergeFactor个段进行合并,直到该分组的全部段合并完成,再进行下一分组的查找合并操做。

 (8)经过上述几步,若是找到了知足合并要求的段,则将会进行段的合并操做。由于索引里面包含了正向信息和反向信息,因此段合并的操做分为两部分:一个是正向信息合并,例如存储域、词向量、标准化因子等;一个是反向信息的合并,例如词典、倒排表等。在段合并时,除了须要对索引数据进行合并,还须要移除段中已经删除的数据。

5.1.6 Lucene类似度打分

 咱们在前面了解到,Lucene的查询过程是:首先在词典中查找每一个Term,根据Term得到每一个Term所在的文档链表;而后根据查询条件对链表作交、并、差等操做,链表合并后的结果集就是咱们要查找的数据。这样作能够彻底避免对关系型数据库进行全表扫描,能够大大提高查询效率。可是,当咱们一次查询出不少数据时,这些数据和咱们的查询条件又有多大关系呢?其文本类似度是多少?本节会回答这个问题,并介绍Lucene最经典的两个文本类似度算法:基于向量空间模型的算法和基于几率的算法(BM25)。

 若是对此算法不太感兴趣,那么只需了解对文本类似度有影响的因子有哪些,哪些是正向的,哪些是逆向的便可,不须要理解每一个算法的推理过程。可是这两个文本类似度算法有很好的借鉴意义。

5.2 Elasticsearch简介

 Elasticsearch是使用Java编写的一种开源搜索引擎,它在内部使用Luence作索引与搜索,经过对Lucene的封装,提供了一套简单一致的RESTful API。Elasticsearch也是一种分布式的搜索引擎架构,能够很简单地扩展到上百个服务节点,并支持PB级别的数据查询,使系统具有高可用和高并发性。

5.2.1 核心概念

 Elasticsearch的核心概念以下:

  • Cluster:集群,由一个或多个Elasticsearch节点组成。
  • Node:节点,组成Elasticsearch集群的服务单元,同一个集群内节点的名字不能重复。一般在一个节点上分配一个或者多个分片。
  • Shards:分片,当索引上的数据量太大的时候,咱们一般会将一个索引上的数据进行水平拆分,拆分出来的每一个数据库叫做一个分片。在一个多分片的索引中写入数据时,经过路由来肯定具体写入那一个分片中,因此在建立索引时须要指定分片的数量,而且分片的数量一旦肯定就不能更改。分片后的索引带来了规模上(数据水平切分)和性能上(并行执行)的提高。每一个分片都是Luence中的一个索引文件,每一个分片必须有一个主分片和零到多个副本分片。
  • Replicas:备份也叫做副本,是指对主分片的备份。主分片和备份分片均可以对外提供查询服务,写操做时先在主分片上完成,而后分发到备份上。当主分片不可用时,会在备份的分片中选举出一个做为主分片,因此备份不只能够提高系统的高可用性能,还能够提高搜索时的并发性能。可是若副本太多的话,在写操做时会增长数据同步的负担。
  • Index:索引,由一个和多个分片组成,经过索引的名字在集群内进行惟一标识。
  • Type:类别,指索引内部的逻辑分区,经过Type的名字在索引内进行惟一标识。在查询时若是没有该值,则表示在整个索引中查询。
  • Document:文档,索引中的每一条数据叫做一个文档,相似于关系型数据库中的一条数据经过_id在Type内进行惟一标识。
  • Settings:对集群中索引的定义,好比一个索引默认的分片数、副本数等信息。
  • Mapping:相似于关系型数据库中的表结构信息,用于定义索引中字段(Field)的存储类型、分词方式、是否存储等信息。Elasticsearch中的mapping是能够动态识别的。若是没有特殊需求,则不须要手动建立mapping,由于Elasticsearch会自动根据数据格式识别它的类型,可是当须要对某些字段添加特殊属性(好比:定义使用其余分词器、是否分词、是否存储等)时,就须要手动设置mapping了。一个索引的mapping一旦建立,若已经存储了数据,就不可修改了。
  • Analyzer:字段的分词方式的定义。一个analyzer一般由一个tokenizer、零到多个filter组成。好比默认的标准Analyzer包含一个标准的tokenizer和三个filter:Standard Token Filter、Lower Case Token Filter、Stop Token Filter。
     Elasticsearch的节点的分类以下:
  • 主节点(Master Node):也叫做主节点,主节点负责建立索引、删除索引、分配分片、追踪集群中的节点状态等工做。Elasticsearch中的主节点的工做量相对较轻。用户的请求能够发往任何一个节点,并由该节点负责分发请求、收集结果等操做,而并不须要通过主节点转发。经过在配置文件中设置node.master=true来设置该节点成为候选主节点(但该节点不必定是主节点,主节点是集群在候选节点中选举出来的),在Elasticsearch集群中只有候选节点才有选举权和被选举权。其余节点是不参与选举工做的。
  • 数据节点(Data Node):数据节点,负责数据的存储和相关具体操做,好比索引数据的建立、修改、删除、搜索、聚合。因此,数据节点对机器配置要求比较高,首先须要有足够的磁盘空间来存储数据,其次数据操做对系统CPU、Memory和I/O的性能消耗都很大。一般随着集群的扩大,须要增长更多的数据节点来提升可用性。经过在配置文件中设置node.data=true来设置该节点成为数据节点。
  • 客户端节点(Client Node):就是既不作候选主节点也不作数据节点的节点,只负责请求的分发、汇总等,也就是下面要说到的协调节点的角色。其实任何一个节点均可以完成这样的工做,单独增长这样的节点更多地是为了提升并发性。可在配置文件中设置该节点成为数据节点:
node.master=false node.data=false 
  • 部落节点(Tribe Node):部落节点能够跨越多个集群,它能够接收每一个集群的状态,而后合并成一个全局集群的状态,它能够读写全部集群节点上的数据,在配置文件中经过以下设置使节点成为部落节点:
tribe: one: cluster.name: cluster_one two: cluster.name: cluster_two 

 由于Tribe Node要在Elasticsearch 7.0之后移除,因此不建议使用。

  • 协调节点(Coordinating Node):协调节点,是一种角色,而不是真实的Elasticsearch的节点,咱们没有办法经过配置项来配置哪一个节点为协调节点。集群中的任何节点均可以充当协调节点的角色。当一个节点A收到用户的查询请求后,会把查询语句分发到其余的节点,而后合并各个节点返回的查询结果,最好返回一个完整的数据集给用户。在这个过程当中,节点A扮演的就是协调节点的角色。因而可知,协调节点会对CPU、Memory和I/O要求比较高。
     集群的状态有Green、Yellow和Red三种,以下所述:
  • Green:绿色,健康。全部的主分片和副本分片均可正常工做,集群100%健康。
  • Yellow:黄色,预警。全部的主分片均可以正常工做,但至少有一个副本分片是不能正常工做的。此时集群能够正常工做,可是集群的高可用性在某种程度上被弱化。
  • Red:红色,集群不可正常使用。集群中至少有一个分片的主分片及它的所有副本分片都不可正常工做。这时虽然集群的查询操做还能够进行,可是也只能返回部分数据(其余正常分片的数据能够返回),而分配到这个分片上的写入请求将会报错,最终会致使数据的丢失。

5.2.2 3C和脑裂

1. 共识性(Consensus)

 共识性是分布式系统中最基础也最主要的一个组件,在分布式系统中的全部节点必须对给定的数据或者节点的状态达成共识。虽然如今有很成熟的共识算法如Raft、Paxos等,也有比较成熟的开源软件如Zookeeper。可是Elasticsearch并无使用它们,而是本身实现共识系统zen discovery。Elasticsearch之父Shay Banon解释了其中主要的缘由:“zen discovery是Elasticsearch的一个核心的基础组件,zen discovery不只可以实现共识系统的选择工做,还可以很方便地监控集群的读写状态是否健康。固然,咱们也不保证其后期会使用Zookeeper代替如今的zen discovery”。zen discovery模块以“八卦传播”(Gossip)的形式实现了单播(Unicat):单播不一样于多播(Multicast)和广播(Broadcast)。节点间的通讯方式是一对一的。

2. 并发(Concurrency)

 Elasticsearch是一个分布式系统。写请求在发送到主分片时,同时会以并行的形式发送到备份分片,可是这些请求的送达时间多是无序的。在这种状况下,Elasticsearch用乐观并发控制(Optimistic Concurrency Control)来保证新版本的数据不会被旧版本的数据覆盖。
 乐观并发控制是一种乐观锁,另外一种经常使用的乐观锁即多版本并发控制(Multi-Version Concurrency Control),它们的主要区别以下:

  • 乐观并发控制(OCC):是一种用来解决写-写冲突的无锁并发控制,认为事务间的竞争不激烈时,就先进行修改,在提交事务前检查数据有没有变化,若是没有就提交,若是有就放弃并重试。乐观并发控制相似于自选锁,适用于低数据竞争且写冲突比较少的环境。
  • 多版本并发控制(MVCC):是一种用来解决读-写冲突的无所并发控制,也就是为事务分配单向增加的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操做只读该事务开始前的数据库的快照。这样在读操做不用阻塞操做且写操做不用阻塞读操做的同时,避免了脏读和不可重复读。
3. 一致性(Consistency)

 Elasticsearch集群保证写一致性的方式是在写入前先检查有多少个分片可供写入,若是达到写入条件,则进行写操做,不然,Elasticsearch会等待更多的分片出现,默认为一分钟。
 有以下三种设置来判断是否容许写操做:

  • One:只要主分片可用,就能够进行写操做。
  • All:只有当主分片和全部副本均可用时,才容许写操做。
  • Quorum(k-wu-wo/reng,法定人数):是Elasticsearch的默认选项。当有大部分的分片可用时才容许写操做。
     其中,对“大部分”的计算公式为int((primary+number_of_replicas)/2)+1。
     Elasticsearch集群保证读写一致性的方式是,为了保证搜索请求的返回结果是最新版本的文档,备份能够被设置为sync(默认值),写操做在主分片和备份分片同时完成后才会返回写请求的结果。这样,不管搜索请求至哪一个分片都会返回最新的文档。可是若是咱们的应用对写要求很高,就能够经过设置replication=async来提高写的效率,若是设置replication=async,则只要主分片的写完成,就会返回写成功。
4. 脑裂

 在Elasticsearch集群中主节点经过ping命令来检查集群中的其余节点是否处于可用状态,同时非主节点也会经过ping来检查主节点是否处于可用状态。当集群网络不稳定时,有可能会发生一个节点ping不通Master节点,则会认为Master节点发生了故障,而后从新选出一个Master节点,这就会致使在一个集群内出现多个Master节点。当在一个集群中有多个Master节点时,就有可能会致使数据丢失。咱们称这种现象为脑裂。在5.4.7节会介绍如何避免脑裂的发生。

5.2.3 事务日志

 咱们在5.1节了解到,Lucene为了加快写索引的速度,采用了延迟写入的策略。虽然这种策略提升了写入的效率,但其最大的弊端是,若是数据在内存中尚未持久化到磁盘上时发生了相似断电等不可控状况,就可能丢失数据。为了不丢失数据,Elasticsearch添加了事务日志(Translog),事务日志记录了全部尚未被持久化磁盘的数据。
 Elasticsearch写索引的具体过程以下。
 首先,当有数据写入时,为了提高写入的速度,并无数据直接写在磁盘上,而是先写入到内存中,可是为了防止数据的丢失,会追加一份数据到事务日志里。由于内存中的数据还会继续写入,因此内存中的数据并非以段的形式存储的,是检索不到的。总之,Elasticsearch是一个准实时的搜索引擎,而不是一个实时的搜索引擎。此时的状态如图5-14所示。


 
图 5-14 Elasticsearch写数据的过程

 而后,当达到默认的时间(1秒钟)或者内存的数据达到必定量时,会触发一次刷新(Refresh)。刷新的主要步骤以下。
(1)将内存中的数据刷新到一个新的段中,可是该段并无持久化到硬盘中,而是缓存在操做系统的文件缓存系统中。虽然数据还在内存中,可是内存里的数据和文件缓存系统里的数据有如下区别。

  • 内存使用的是JVM的内存,而文件缓存系统使用的是操做系统的内存。
  • 内存的数据不是以段的形式存储的,而且能够继续向内存里写数据。文件缓存系统中的数据是以段的形式存储的,因此只能读,不能写。
  • 内存中的数据是搜索不到,文件缓存系统中的数据是能够搜索的。

(2)打开保存在文件缓存系统中的段,使其可被搜索。
(3)清空内存,准备接收新的数据。日志不作清空处理。
 此时的状态如图5-15所示。


 
图 5-16 Elasticsearch写数据的过程

 最后,刷新(Flush)。当日志数据的大小超过512MB或者时间超过30分钟时,须要触发一次刷新。刷新的主要步骤以下。

  1. 在文件缓存系统中建立一个新的段,并把内存中的数据写入,使其可被搜索。
  2. 清空内存,准备接收新的数据。
  3. 将文件系统缓存中的数据经过fsync函数刷新到硬盘中。
  4. 生成提交点。
  5. 删除旧的日志,建立一个空的日志。
     此时的状态如图5-17所示。


     
    图 5-17 Elasticsearch写数据的过程

     由上面索引建立的过程可知,内存里面的数据并无直接被刷新(Flush)到硬盘中,而是被刷新(Refresh)到了文件缓存系统中,这主要是由于持久化数据十分耗费资源,频繁地调用会使写入的性能急剧降低,因此Elasticsearch,为了提升写入的效率,利用了文件缓存系统和内存来加速写入时的性能,并使用日志来防止数据的丢失。
     在须要重启时,Elasticsearch不只要根据提交点去加载已经持久化过的段,还须要根据Translog里的记录,把未持久化的数据从新持久化到磁盘上。
     根据上面对Elasticsearch,写操做流程的介绍,咱们能够整理出一个索引数据所要经历的几个阶段,以及每一个阶段的数据的存储方式和做用。如图5-18所示。


     
    图 5-18 Elasticsearch写操做流程

5.2.4 在集群中写索引

 假设咱们有如图5-19所示(图片来自官网)的一个集群,该集群由三个节点组成(Node 一、Node 2和Node 3),包含一个由两个主分片和每一个主分片由两个副本分片组成的索引。其中,标星号的Node 1是Master节点,负责管理整个集群的状态;p1和p2是主分片;r0和r1是副本分片。为了达到高可用,Master节点避免将主分片和副本放在同一个节点。


 
图 5-19 写索引

 将数据分片是为了提升可处理数据的容量和易于进行水平扩展,为分片作副本是为了提升集群的稳定性和提升并发量。在主分片挂掉后,会从副本分片中选举出一个升级为主分片,当副本升级为主分片后,因为少了一个副本分片,因此集群状态会从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将向协调节点报告成功,协调节点向客户端报告成功。


 
图 5-20 写索引

5.2.5 集群中的查询流程

 根据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再将文档返回给客户端。

 
图 5-22

 当一个搜索请求被发送到某个节点时,这个节点就变成了协调节点(Node 1)。协调节点的任务是广播查询请求到全部分片(主分片或者副本分片),并将它们的响应结果整合成全局排序后的结果集合,由上面步骤3所示,默认返回给协调节点并非全部的数据,而是只有文档的id和得分score,由于咱们最后只返回给用户size条数据,因此这样作的好处是能够节省不少带宽,特别是from很大时。协调节点对收集回来的数据进行排序后,找到要返回的size条数据的id,再根据id查询要返回的数据,好比title、content等。取回数据等流程以下(见图5-23,图片来自官网)。
(1)Node 3进行二次排序来找出要返回的文档id,并向相关的分片提交多个得到文档详情的请求。
(2)每一个分片加载文档,并将文档返回给Node 3。
(3)一旦全部的文档都取回了,Node 3就返回结果给客户端。
 
图 5-23

协调节点收集各个分片查询出来的数据,再进行二次排序,而后选择须要被取回的文档。例如,若是咱们的查询指定了{"from": 20, "size": 10},那么咱们须要在每一个分片中查询出来得分最高的20+10条数据,协调节点在收集到30✖️n(n为分片数)条数据后再进行排序,排序位置在0-20的结果会被丢弃,只有从第21个开始的10个结果须要被取回。这些文档可能来自多个甚至所有分片。
 由上面的搜索策略能够知道,在查询时深翻(Deep Pagination)并非一种好方法。由于深翻时,from会很大,这时的排序过程可能会变得很是沉重,会占用大量的cpu、内存和带宽。由于这个缘由,因此强烈建议慎重使用深翻。
分片能够减小每一个片上的数据量,加快查询的速度,可是在查询时,协调节点要在收集数(from+size)✖️n条数据后再作一次全局排序,若这个数据量很大,则也会占用大量的CPU、内存、带宽等,而且分片查询的速度取决于最慢的分片查询的速度,因此分片数并非越多越好。
 ES的API应用测试,性能优化,开发使用等内容将在下一篇《Elasticsearch详解-续》中完成。

《Elasticsearch详解-续》

 

若是须要給我修改意见的发送邮箱:erghjmncq6643981@163.com
资料参考:《可伸缩服务架构》
转发博客,请注明,谢谢。

走过路过不要错过,您的支持是我持续技术输出的动力所在,金额随意,感谢!!

相关文章
相关标签/搜索