es写入和检索优化思路记录

摘要:对es的整体技术架构和优化策略做简单探讨

1.Lucence存储结构概览

es底层存储依赖Lucence框架,这里对Lucence的存储结构做简单介绍。

lucene是java语言编写的全文搜索框架,用于处理纯文本说数据,用空间来换取时间,对需要检索的文件、字符流进行全文索引,在检索的时候对索引进行快速的检索,得到检索位置,这个位置记录检索词出现的文件路径或者某个关键词。

lucence存储结构

lucene 在存储它的全文索引结构时,是有层次结构的,这涉及到5个层次:索引(Index);段(Segment);文档(Document);域(Field);词(Term),他们的关系如下图所示:(lucene 索引存储结构概念图)

es写入和检索优化思路记录_第1张图片

 

文件列表示例:

es写入和检索优化思路记录_第2张图片

 

lucene基本概念

  • index:一个目录一个索引,在Lucene中一个索引是放在一个文件夹中的。

  • segment : lucene内部的数据是由一个个segment组成的,写入lucene的数据并不直接落盘,而是先写在内存中,经过了refresh间隔,lucene才将该时间段写入的全部数据refresh成一个segment;

segment多了之后会进行merge成更大的segment。lucene查询时会遍历每个segment完成。

由于lucene写入的数据是在内存中完成,所以写入效率非常高。

但是也存在丢失数据的风险,所以Elasticsearch基于此现象实现了translog,只有在segment数据落盘后,Elasticsearch才会删除对应的translog。

  • doc : doc表示lucene中的一条记录

  • field :field表示记录中的字段概念,一个doc由若干个field组成。

  • term :term是lucene中索引的最小单位,某个field对应的内容如果是全文检索类型,会将内容进行分词,分词的结果就是由term组成的。如果是不分词的字段,那么该字段的内容就是一个term。

  • 倒排索引(inverted index): lucene索引的通用叫法,即实现了term到doc list的映射。

  • 正排数据:搜索引擎的通用叫法,即原始数据,可以理解为一个doc list。

lucence分段

由于分段是不变的,它们很容易被缓存,使得搜索更快。此外,修改数据集时,如添加一篇文档,无须重建现有分段中的数据索引。这使得新文档的索引也是很快的。

但是更新文档不能修改实际的文档,只是索引一篇新的文档。如此处理还需要删除原有的文档。而且删除也不能从分段中移除文档(这需要重建倒排索引),只是在单独的.del文件中将其标记为“已被删除”。文档只会在分段合并的时候真正地被移除。

因此合并分段的两个目的:第一个是将分段的总数量保持在受控的范围内(这用来保障查询的性能)。第二个是真正地删除文档。
按照已定义的合并策略,分段是在后台进行的。默认的合并策略是分层配置,合并发生在索引、更新或者删除文档的时候,如图所示,该策略将分段划分为多个层次,如果你的分段多于某一层中所设置的最大分段数,该层的合并就会被触发。

es写入和检索优化思路记录_第3张图片

 

段合并本质上有二个阶段:

  • 第一阶段:从众多的段中选择出部分候选段执行段合并。在Lucene中是由MergePolicy来决定。

  • 第二阶段:将选择出的候选段实施合并生成新段的过程。在Lucene中是由MergeScheduler来执行。

HBase 、 Cassandra 等系统都有类似的分段机制,写过程中先在内存缓冲一批数据,不时地将这些数据写入文件作为一个分段,分段具有不变性,再通过一些策略合并分段。

分段合并过程中,新段的产生需要一定的磁盘空间,我们要保证系统有足够的剩余可用空间。

Cassandra 系统在段合并过程中的一个问题就是,当持续地向一个表中写入数据,如果段文件大小没有上限,当巨大的段达到磁盘空间的一半时,剩余空间不足以进行新的段合并过程。

如果段文件设置一定上限不再合并,则对表中部分数据无法实现真正的物理删除。

ES 存在同样的问题。

lucene文件内容

lucene包的文件是由很多segment文件组成的,segments_N文件记录了lucene包下面的segment文件数量。每个segment会包含如下的文件。

Name

Extension

Brief Description

Segment Info

.si

segment的元数据文件

Compound File

.cfs, .cfe

一个segment包含了如下表的各个文件,为减少打开文件的数量,在segment小的时候,segment的所有文件内容都保存在cfs文件中,cfe文件保存了lucene各文件在cfs文件的位置信息

Fields

.fnm

保存了fields的相关信息

Field Index

.fdx

正排存储文件的元数据信息

Field Data

.fdt

存储了正排存储数据,写入的原文存储在这

Term Dictionary

.tim

倒排索引的元数据信息

Term Index

.tip

倒排索引文件,存储了所有的倒排索引数据

Frequencies

.doc

保存了每个term的doc id列表和term在doc中的词频

Positions

.pos

Stores position information about where a term occurs in the index
全文索引的字段,会有该文件,保存了term在doc中的位置

Payloads

.pay

Stores additional per-position metadata information such as character offsets and user payloads
全文索引的字段,使用了一些像payloads的高级特性会有该文件,保存了term在doc中的一些高级特性

Norms

.nvd, .nvm

文件保存索引字段加权数据

Per-Document Values

.dvd, .dvm

lucene的docvalues文件,即数据的列式存储,用作聚合和排序

Term Vector Data

.tvx, .tvd, .tvf

Stores offset into the document data file
保存索引字段的矢量信息,用在对term进行高亮,计算文本相关性中使用

Live Documents

.liv

记录了segment中删除的doc

2.es架构概览

es再lucence的基础之上,进行了分布式的服务管理,以及各种分析功能的封装等。

es基本概念:

  1. 接近实时es是一个接近实时的搜索平台,这就意味着,从索引一个文档直到文档能够被搜索到有一个轻微的延迟

  2. 集群(cluster)一个集群有多个节点(服务器)组成,通过所有的节点一起保存你的全部数据并且通过联合索引和搜索功能的节点的集合,每一个集群有一个唯一的名称标识

  3. 节点(node)一个节点就是一个单一的服务器,是你的集群的一部分,存储数据,并且参与集群和搜索功能,一个节点可以通过配置特定的名称来加入特定的集群,在一个集群中,你想启动多少个节点就可以启动多少个节点。

  4. 索引(index)一个索引就是还有某些共有特性的文档的集合,一个索引被一个名称唯一标识,并且这个名称被用于索引通过文档去执行搜索,更新和删除操作。

  5. 类型(type)type 在6.0.0已经不赞成使用,7版本已经废弃。

  6. 文档(document)一个文档是一个基本的搜索单元

2.1es存储结构

一个ES索引包含很多分片(shard),每个分片对应一个lucence的索引,分片本身就是一个完整的搜索引擎,可以独立的执行建立索引和搜索任务。

索引存储结构:

es写入和检索优化思路记录_第4张图片 

2.2es分布式架构

集群存储结构:

es写入和检索优化思路记录_第5张图片

 

集群是有多个节点组成的,在上图中可以看到集群中有多个不同种类型的节点。

节点是一个Elasticsearch的实例,本质上是一个Java进程。每个节点上面都保存着集群的状态信息,包括所有的节点信息、所有的索引和相关的Mapping于Setting信息和分片的路由信息等。节点按照角色可以划分为主节点、数据节点、协调节点和预处理节点等。

es写入和检索优化思路记录_第6张图片

 Master节点负责管理集群状态信息,包括处理创建、删除索引等请求,决定分片被分配到哪个节点,维护和更新集群状态。值得注意的是,只有Master节点才能修改集群的状态信息,并负责同步给其他节点。可见,Master节点非常重要,在部署上需要考虑单点风险。

协调节点负责接收客户端的请求,将请求路由到到合适的节点,并将结果汇集到一起。

数据节点是保存数据的节点,增加数据节点可以解决水平扩展和解决数据单点的问题。

预处理节点是数据前置处理转换的节点,支持 pipeline管道设置,可以对数据进行过滤、转换等操作。

3.es写入数据流程概览

es写入和检索优化思路记录_第7张图片

 

(1)客户端向 NODE1发送写请求。

(2)NODE1使用文档 ID 来确定文档属于分片 0,通过集群状态中的内容路由表信息获知分片 0 的主分片位于 NODE3 ,因此请求被转发到 NODE3 上。

(3)NODE3 上的主分片执行写操作 。 如果写入成功,则它将请求并行转发到 NODE1和NODE2 的副分片上,等待返回结果 。当所有的副分片都报告成功,NODE3 将向协调节点报告成功,协调节点再向客户端报告成功 。

在客户端收到成功响应时 ,意味着写操作已经在主分片和所有副分片都执行完成。

3.1文档写入流程

 

当有新数据写入的时候,ES首先写入Index Buffer区域,此时是无法检索的。

默认情况下ES每秒钟进行一次Refresh操作,将Index Buffer中的index刷新到文件系统缓存,在文件系统缓存中,是以Segment进行存储的,而且是可以被搜索到的,这就是ES实现近实时搜索:文档的更改无法立即被搜索到,但是在一定时间会变得可见。

需要说明的是,Refresh 触发的情况有3种:

  1. 按照时间频率触发,默认情况是每 1 秒触发 1 次 Refresh,可通过index.refresh_interval 设置;

  2. 当Index Buffer 被占满的时候,会触发 Refresh,Index Buffer 的大小默认值是 JVM 所占内存容量的 10%;

  3. 手动调用调用Refresh API。

由于Refresh操作默认间隔为1s,因此会产生大量的小Segment,ES查询时会同时查询所有的Segment,并对结果进行汇总,大量小Segment会使性能变差。因此ES会对小Segment进行段合并(Merge),合并操作会丢弃掉重复的键,并只保留每个键最近的更新。段合并之后搜索请求可以直接访问合并之后的Segment,从而提高搜索性能。

Merge触发的情况有2种:

  1. ES自动启动后台任务进行Merge;

  2. 手动调用_forcemerge API主动触发。

在段合并完成之后,ES会将Segment文件Flush到磁盘中,并创建一个Commit Point文件,用来标识被Flush到磁盘的Segment。Commit Point其实是记录所有的Segment信息,关于移除的Segment的信息会记录在“.del”文件中,查询结果后会从该文件中进行过滤。

Flush操作是将Segment从文件系统缓存写入到磁盘进行持久化,在执行 Flush 的时候会依次执行下面操作:

  1. 清空Index Buffer

  2. 记录 Commit Point

  3. 刷新Segment到磁盘

  4. 删除translog

translog

为了保障数据安全,ES增加了Translog, 在数据写入Index Buffer的同时,写入一份到Translog。

默认每个写入请求,Translog会追加写入磁盘的,这样就可以防止服务器宕机后数据丢失。

如果对可靠性要求不是很高,也可以设置异步落盘,由配置参数 index.translog.durabilityindex.translog.sync_interval控制。

index.translog.durability:默认是request,每个请求都落盘;设置成async,可异步写入。
index.translog.sync_interval:默认5s,不能小于100ms

Translog落盘有2种情况:

  1. 每个请求同步或者异步落盘

  2. Flush的时候,内存中的Segment和Translog同时落盘

3.2写入优化思路参考

(1)translog flush间隔调整

从 es 2.x 开始, 默认设置下,translog 的持久化策略为:每个请求都flush.对应配置项为:

 
  

index.translog.durability: request

这是影响 es 写入速度的最大因素.但是只有这样,写操作才有可能是可靠的,原因参考写入流程.
如果系统可以接受一定几率的数据丢失,调整 translog 持久化策略为周期性和一定大小的时候 flush:

 
  

# async表示translog的刷盘策略按sync_interval配置指定的时间周期进行

index.translog.durability: async

# 加大trans_log刷盘间隔时间。默认是5s,不能低于100ms

index.translog.sync_interval: 120s

# 增大触发refresh操作的trans_log大小,避免频繁产生新的lucence分段。默认是512MB

index.translog.flush_threshold_size: 1024mb

(2)索引刷新间隔调整

默认的refresh间隔是1s,用index.refresh_interval参数可以设置,这样会其强迫es每秒中都将内存中的数据写入磁盘中,创建一个新的segment file。

正是这个间隔,让我们每次写入数据后,1s以后才能看到。

但是如果我们将这个间隔调大,比如30s,可以接受写入的数据30s后才看到,那么我们就可以获取更大的写入吞吐量,因为30s内都是写内存的,每隔30s才会创建一个segment file。

(3)段合并优化

segment merge 操作对系统 CPU 和 IO 占用都比较高,配置参数如下:

 
  

index.merge.scheduler.max_thread_count

index.merge.policy.*

最大线程数max_thread_count的默认值为:

 
  

Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2))

是一个比较理想的值,如果你只有一块硬盘并且非 SSD, 应该把他设置为1,因为在旋转存储介质上并发写,由于寻址的原因,不会提升,只会降低写入速度.

merge 策略有三种:

  • tiered

  • log_byete_size

  • log_doc

默认情况下是tiered:

 
  

index.merge.polcy.type: tiered

索引创建时合并策略就已确定,不能更改,但是可以动态更新策略参数,一般情况下,不需要调整.如果堆栈经常有很多 merge, 可以尝试调整以下配置:

 
  

index.merge.policy.floor_segment

该属性用于阻止segment 的频繁flush, 小于此值将考虑优先合并,默认为2M,可考虑适当降低此值

 
  

index.merge.policy.segments_per_tier

该属性指定了每层分段的数量,取值越小最终segment 越少,因此需要 merge 的操作更多,可以考虑适当增加此值.默认为10,他应该大于等于 index.merge.policy.max_merge_at_once

 
  

index.merge.policy.max_merged_segment

该属性指定了单个 segment 的最大容量,默认为5GB,可以考虑适当降低此值。

(4)indexing buffer

如果我们要进行非常重的高并发写入操作,那么最好将index buffer调大一些,这和可用堆内存、单节点上的shard数量相关。

indices.memory.index_buffer_size,这个可以调节大一些,设置的这个index buffer大小,是所有的shard公用的,除以shard数量以后,算出来平均每个shard可以使用的内存大小。

一般建议,对于每个shard来说,最多给512mb,因为再大性能就没什么提升了。

es会将这个设置作为每个shard共享的index buffer,那些特别活跃的shard会更多的使用这个buffer。

默认这个参数的值是10%,也就是jvm heap的10%,如果我们给jvmheap分配10gb内存,那么这个index buffer就有1gb,对于两个shard共享来说,是足够的了。

(5)使用bulk请求

单线程发送bulk请求是无法最大化es集群写入的吞吐量的。如果要利用集群的所有资源,就需要使用多线程并发将数据bulk写入集群中。

为了更好的利用集群的资源,这样多线程并发写入,可以减少每次底层磁盘fsync的次数和开销。

首先对单个es节点的单个shard做压测,比如说,先是2个线程,然后是4个线程,然后是8个线程,16个,每次线程数量倍增。

一旦发现es返回了TOO_MANY_REQUESTS的错误,JavaClient也就是EsRejectedExecutionException。

此时那么就说明es是说已经到了一个并发写入的最大瓶颈了,此时我们就知道最多只能支撑这么高的并发写入了。

(6)磁盘间的任务均衡

如果你的部署方案是为path.data 配置多个路径来使用多块磁盘, es 在分配 shard 时,落到各磁盘上的 shard 可能并不均匀,这种不均匀可能会导致某些磁盘繁忙,利用率达到100%,这种不均匀达到一定程度可能会对写入性能产生负面影响.

es 在处理多路径时,优先将 shard 分配到可用空间百分比最多的磁盘,因此短时间内创建的 shard 可能被集中分配到这个磁盘,即使可用空间是99%和98%的差别.后来 es 在2.x 版本中开始解决这个问题的方式是:预估一下这个 shard 会使用的空间,从磁盘可用空间中减去这部分,直到现在6.x beta 版也是这种处理方式.但是实现也存在一些问题:

从可用空间减去预估大小

这种机制只存在于一次索引创建的过程中,下一次的索引创建,磁盘可用空间并不是上次做完减法以后的结果,这也可以理解,毕竟预估是不准的,一直减下去很快就减没了.

但是最终的效果是,这种机制并没有从根本上解决问题,即使没有完美的解决方案,这种机制的效果也不够好.

如果单一的机制不能解决所有的场景,至少应该为不同场景准备多种选择.

为此,可以为 es 增加了两种策略
简单轮询:系统初始阶段,简单轮询的效果是最均匀的
基于可用空间的动态加权轮询:以可用空间作为权重,在磁盘之间加权轮询

(7)节点间的任务均衡

为了在节点间任务尽量均衡,数据写入客户端应该把 bulk 请求轮询发送到各个节点.

当使用 java api ,或者 rest api 的 bulk 接口发送数据时,客户端将会轮询的发送的集群节点,节点列表取决于:

  • client.transport.sniff为 true,(默认为 false),列表为所有数据节点

  • 否则,列表为初始化客户端对象时添加进去的节点.

java api 的 TransportClient 和 rest api 的 RestClient 都是线程安全的,当写入程序自己创建线程池控制并发,应该使用同一个 Client 对象.

建议使用 rest api,兼容性好,只有吞吐量非常大才值得考虑序列化的开销,显然搜索并不是高吞吐量的业务.

如果想观察bulk请求在不同节点上的处理情况,可以通过cat 接口观察 bulk 线程池和队列情况,是否存在不均:

 
  

_cat/thread_pool

4.es搜索数据流程

es写入和检索优化思路记录_第8张图片

 

(1)客户端发送 search 请求到 NODE 3。

(2) Node 3将查询请求转发到索引的每个主分片或副分片中。

(3)每个分片在本地执行查询,并使用本地的 Term / Document Frequency 信息进行打分,加结果到大小为 from + size 的本地有序优先队列中。

(4)每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,协调节点合并这些值到自己的优先队列中,产生一个全局排序后的列表。

协调节点广播查询请求到所有相关分片时,可以是主分片或副分片,协调节点将在之后的请求中轮询所有的分片副本来分摊负载。

查询阶段并不会对搜索请求的内容进行解析,无论搜索什么内容,只看本次搜索需要命中哪些 shard ,然后针对每个特定 shard 选择一个副本,转发搜索清求。

es写入和检索优化思路记录_第9张图片

 

1.协调节点辨别出哪个document需要取回,并且向相关分片发出GET请求。

2.每个分片加载document并且根据需要丰富(enrich)它们,然后再将document返回协调节点。

3.一旦所有的document都被取回,协调节点会将结果返回给客户端。

协调节点先决定哪些document是实际(actually)需要取回的。例如,我们指定查询{ "from": 90, "size": 10 },那么前90条将会被丢弃,只有之后的10条会需要取回。这些document可能来自与原始查询请求相关的某个、某些或者全部分片。

协调节点为每个持有相关document的分片建立多点get请求然后发送请求到处理查询阶段的分片副本。

分片加载document主体——_source field。如果需要,还会根据元数据丰富结果和高亮搜索片断。一旦协调节点收到所有结果,会将它们汇集到单一的回答响应里,这个响应将会返回给客户端。

4.1搜索优化思路参考

(1)为文件系统cache预留足够的内存

在一般情况下,应用程序的读写都会被操作系统“ cache ”(除了 direct 方式), cache 保存在系统物理内存中(线上应该禁用 swap ),命中 cache 可以降低对磁盘的直接访问频率。

搜索很依赖对系统 cache 的命中,如果某个请求需要从磁盘读取数据,则一定会产生相对较高的延迟。应该至少为系统 cache 预留一半的可用物理内存,更大的内存有更高的 cache 命中率。

(2)使用更快的硬件

写入性能对 CPU 的性能更敏感,而搜索性能在一般情况下更多的是在于 I/O 能力,使用 SSD 会比旋转类存储介质好得多。

尽量避免使用 NFS 等远程文件系统,如果 NFS 比本地存储慢3倍,则在搜索场景下响应速度可能会慢10倍左右。这可能是因为搜索请求有更多的随机访问。

如果搜索类型属于计算比较多,则可以考虑使用更快的 CPU 。

(3)文档模型

为了让搜索时的成本更低,文档应该合理建模。特别是应该避免 join 操作,嵌套( neste )会使查询慢几倍,父子( parent - child )关系可能使査询慢数百倍。

因此,如果可以通过非规范化( denormalizing )文档来回答相同的问题,则可以显著地提高搜索速度。

(4)选择合适的分页模式

Elasticsearch数据是分片存储的,数据分布在多台机器上。有这样一个场景,如何获取前1000个文档?当获取从990-1000的文档时候,会在每个分片上面都先获取1000个文档,然后再由协调节点聚合所有分片的结果在排序选取前1000个文档。

页数越深,处理文档越多,占用内存越多,耗时越长。所以要尽量避免深度分页。当然,ES官方也注意了这个问题,所以通过index.max_result_window限定最多到10000条数据。当然我们也可以根据业务需要修改这个参数。

这也解释了:为什么Google搜索结果只有相关度最高的17页结果,百度只有76页的结果,原因之一是受限于Elasticsearch深度分页的性能问题。

  • 三种分页方式对比:

类型

场景

From/Size

需要实时获取顶部的部分文档,且需要自由翻页

Scroll

需要全部文档,如导出所有数据的功能

Search_After

需要全部文档,不需要自由翻页

(5)相关性分数取舍

评分消耗资源,如果不需要可使用 filter 过滤来达到关闭评分功能,score 则为 0。

(6)字段映射

尽量使用 keyword 替代一些 long 或者 int 之类,term 查询总比 range 查询好

(7)避免使用脚本

一般来说,应该尽量避免使用脚本。如果一定要用,应该优先考虑painless和expressions。

参考:

Elasticsearch 存储原理-lucene底层数据结构-程序员博客中心

Lucene索引存储结构_MayMatrix的博客-CSDN博客_lucene存储结构

Lucene 的存储结构概述_蝈蝈俊的博客-CSDN博客

java - ES分布式架构及底层原理_个人文章 - SegmentFault 思否

Elasticsearch核心技术(四):分布式存储架构与索引原理分析 - James_Shangguan - 博客园

Lucene索引段合并 - 知乎

Elasticsearch和Lucene分段 - codeduck - 博客园

Elasticsearch核心技术(五):搜索API和搜索运行机制 - James_Shangguan - 博客园

查询阶段 · Elasticsearch 权威指南(中文版)

ES的写入速度优化_51CTO博客_es写入优化

Elasticsearch 调优之 写入速度优化到极限 - xibuhaohao - 博客园

《Elasticsearch实战》

《Elasticsearch源码解析与优化实战》

你可能感兴趣的:(elasticsearch,lucene,搜索引擎)