注: 部分概念介绍来源于网络

一、简介
Elasticsearch索引(elasticsearch index)由一个或者若干分片(shard)组成,分片(shard)通过副本(replica)来实现高可用。一个分片(share)其实就是一个Lucene索引(lucene index),一个Lucene索引(lucene index)又由一个或者若干段(segment)组成。所以,当我们查询一个Elasticsearch索引时,查询会在所有分片上执行,既而到段(segment),然后合并所有结果。

二、倒排索引
Elasticsearch可以对全文进行检索主要归功于倒排索引,倒排索引被写入磁盘后是不可改变的,永远不能被修改。倒排索引的不变性有几个好处:
1) 因为索引不能更新,不需要锁
2) 文件系统缓存亲和性,由于索引不会改变,只要系统内存足够,大部分读请求直接命中内存,可以极大提高性能
3) 其他缓存,如filter缓存,在索引的生命周期内始终有效
4) 写入单个大的倒排索引允许数据被压缩,减少磁盘I/O和需要被缓存到内存的索引的使用量
但倒排索引的不变性,同样意味着当需要新增文档时,需要对整个索引进行重建,当数据更新频繁时,这个问题将会变成灾难。

三、段(segment)
Elasticsearch是基于Lucene来生成索引的,Lucene引入了“按段搜索”的概念。用更多的倒排索引来反映最新的修改,这样就不需要重建整个倒排索引而实现索引的更新,查询时就轮询所有的倒排索引,然后对结果进行合并。
除了上面提到的”段(segment)”的概念,Lucene还增加了一个”提交点(commit point)”的概念,”提交点(commit point)”用来记录所有segment信息。

四、segment文件的合并流程:
elasticsearch允许用户选择段合并政策(merge policy)及储存级节流(store level throttling)

当我们往 ElasticSearch 写入数据时,数据是先写入 memory buffer,然后定时(默认每隔1s)将 memory buffer 中的数据写入一个新的 segment 文件中,并进入 Filesystem cache(同时清空 memory buffer),这个过程就叫做 refresh;每个 Segment 事实上是一些倒排索引的集合, 只有经历了 refresh 操作之后,数据才能变成可检索的。

ElasticSearch 每次 refresh 一次都会生成一个新的 segment 文件,这样下来 segment 文件会越来越多。那这样会导致什么问题呢?因为每一个 segment 都会占用文件句柄、内存、cpu资源,更加重要的是,每个搜索请求都必须访问每一个segment,这就意味着存在的 segment 越多,搜索请求就会变的更慢。

每个 segment 是一个包含正排(空间占比90~95%)+ 倒排(空间占比5~10%)的完整索引文件,每次搜索请求会将所有 segment 中的倒排索引部分加载到内存,进行查询和打分,然后将命中的文档号拿到正排中召回完整数据记录。如果不对segment做配置,就会导致查询性能下降

那么 ElasticSearch 是如何解决这个问题呢? ElasticSearch 有一个后台进程专门负责 segment 的合并,定期执行 merge 操作,将多个小 segment 文件合并成一个 segment,在合并时被标识为 deleted 的 doc(或被更新文档的旧版本)不会被写入到新的 segment 中。合并完成后,然后将新的 segment 文件 flush 写入磁盘;然后创建一个新的 commit point 文件,标识所有新的 segment 文件,并排除掉旧的 segement 和已经被合并的小 segment;然后打开新 segment 文件用于搜索使用,等所有的检索请求都从小的 segment 转到大 segment 上以后,删除旧的 segment 文件,这时候,索引里 segment 数量就下降了。

注:elasticsearch在进行删除时,是不会直接物理删除,而是对要删除的对象进行标记,在进行段合并的时候不复制这些数据到新的索引段中。

所有的过程都不需要我们干涉,es会自动在索引和搜索的过程中完成,合并的segment可以是磁盘上已经commit过的索引,也可以在内存中还未commit的segment:合并的过程中,不会打断当前的索引和搜索功能。

refresh API
从内存索引缓冲区把数据写入新段(segment)中,并打开,可供检索,但这部分数据仍在缓存中,未写入磁盘。默认间隔是1s,这个时间会影响段的大小,对段的合并策略有影响。可以进行手动刷新:

# 刷新所有索引
POST /_refresh
# 指定刷新索引
POST /index_name/_refresh

flush API
执行一个提交并且截断translog的行为在Elasticsearch被称作一次flush。每30分钟或者translog太大时会进行flush,所以可以通过translog的设置来调节flush的行为。完成一次flush会有以下过程:
1) 所有在内存缓冲区的文档都被写入一个新的段。
2) 缓冲区被清空。
3) 一个提交点被写入硬盘。
4) 文件系统缓存通过fsync被刷新(flush)。
5) 老的translog被删除。

五、segment 的 merge 对性能的影响:
每次refresh都产生一个新段(segment),频繁的refresh会导致段数量的暴增。段数量过多会导致过多的消耗文件句柄、内存和CPU时间,影响查询速度。基于这个原因,Lucene会通过合并段来解决这个问题。
segment 合并的过程,需要先读取小的 segment,归并计算,再写一遍 segment,最后还要保证刷到磁盘。可以说,合并大的 segment 需要消耗大量的 I/O 和 CPU 资源,同时也会对搜索性能造成影响。所以在Elasticsearch 6.0版本之前对段合并都有“限流(throttling)”功能,主要是为了防止“段爆炸”问题带来的负面影响,这种影响会拖累Elasticsearch的写入速率。当出现”限流(throttling)”时,Elasticsearch日志里会出现类似如下日志:

now throttling indexing: numMergesInFlight=7, maxNumMerges=6
stop throttling indexing: numMergesInFlight=5, maxNumMerges=6

默认情况下,归并线程的限速配置 indices.store.throttle.max_bytes_per_sec 是20MB。对于写入量较大,磁盘转速较高,甚至使用 SSD 盘的服务器来说,这个限速是明显过低的。对于 ELK Stack 应用,建议可以适当调大到 100MB或者更高。设置方式如下:

PUT /_cluster/settings
{
    "persistent" : {
        "indices.store.throttle.max_bytes_per_sec" : "100mb"
    }
}
或者不限制:
PUT /_cluster/settings
{
    "transient" : {
        "indices.store.throttle.type" : "none" 
    }
}

需要注意的是,这里的”限流(throttling)”是对流量(注意单位是Byte)进行限流,而不是限制进程(index.merge.scheduler.max_thread_count)。
indices.store.throttle.type 和indices.store.throttle.max_bytes_per_sec 在版本6.x已被移除,在使用中经常会发现”限速(throttling)”是并发数(index.merge.scheduler.max_thread_count),这两个参数感觉很鸡肋。
但即使上面的限流关掉(none),我们在Elasticsearch日志里仍然能看到”throttling”日志,这主要是因为**merge**的线程数达到了最大,这个最大值通过参数index.merge.scheduler.max_thread_count 来设置,这个配置不能动态更新,需要设置在配置文件elasticsearch.yml里:

index.merge.scheduler.max_thread_count: 3

这个设置允许 max_thread_count + 2 个线程同时进行磁盘操作,也就是设置为 3 允许5个线程。默认值是 Math.min(3, Runtime.getRuntime().availableProcessors() / 2),即服务器 CPU 核数的一半大于 3 时,启动 3 个归并线程;否则启动跟 CPU 核数的一半相等的线程数。

六、手动强制合并 segment:
ES 的 API 也提供了命令来支持强制合并 segment,即 optimize 命令,它可以强制一个分片 shard 合并成 max_num_segments 参数指定的段数量,一个索引它的segment数量越少,它的搜索性能就越高,通常会optimize 成一个 segment。

但需要注意的是,optimize 命令是没有限制资源的,也就是你系统有多少IO资源就会使用多少IO资源,这样可能导致一段时间内搜索没有任何响应,所以,optimize命令不要用在一个频繁更新的索引上面,针对频繁更新的索引es默认的合并进程就是最优的策略。如果你计划要 optimize 一个超大的索引,你应该使用 shard allocation(分片分配)功能将这份索引给移动到一个指定的 node 机器上,以确保合并操作不会影响其他的业务或者es本身的性能。

但是在特定场景下,optimize 也颇有益处,比如在一个静态索引上(即索引没有写入操作只有查询操作)是非常适合用optimize来优化的。比如日志的场景下,日志基本都是按天,周,或者月来索引的,旧索引实质上是只读的,只要过了今天、这周或这个月就基本没有写入操作了,这个时候我们就可以通过 optimize 命令,来强制合并每个shard上索引只有一个segment,这样既可以节省资源,也可以大大提升查询性能。

optimize 的 API 如下:
POST /index-name/_optimize?max_num_segments=1
GET _cat/segment/index-name?v
POST /index-name/_forcemerge?max_num_segments=1

段合并会不会吃光所有的机器资源,造成服务暂时不可用(optimize?max_num_segments=1就会吃光所有资源),但是我没有从官方文档找到_forcemerger 这种方式的资源消耗。

七、segment 性能相关设置

7.1、查看某个索引中所有 segment 的驻留内存情况:

curl -XGET 'http://127.0.0.1:9200/_cat/segments/索引节点名称?v&h=shard,segment,size,size.memory'

7.2、性能优化
7.2.1、选择正确的段合并策略
尽管段合并是Lucene的责任,ElasticSearch也允许用户配置想用的段合并策略。到目前为止,有三种可用的合并策略:
tiered(默认)
它能合并大小相似的索引段,并考虑每层允许的索引段的最大个数
log_byte_size
该策略不断地以字节数的对数为计算单位,选择多个索引来合并创建新索引
log_doc
与log_byte_size类似,不同的是前者基于索引的字节数计算,后者基于索引段文档数计算
为了告知ElasticSearch我们想使用的段合并策略,可以将配置文件的index.merge.policy.type字段配置成我们期望的段合并策略类型。index.merge.policy.type: tiered
一旦使用特定的段合并策略创建了索引,它就不能被改变。但是,可以使用索引更新API来改变该段合并策略的参数值。
tiered合并策略
这是ElasticSearch的默认选项。它能合并大小相似的索引段,并考虑每层允许的索引段的最大个数。读者需要清楚单次可合并的索引段的个数与每层允许的索引段数的区别。在索引期,该合并策略会计算索引中允许出现的索引段个数,该数值称为阈值(budget)。如果正在构建的索引中的段数超过了阈值,该策略将先对索引段按容量降序排序(这里考虑了被标记为已删除的文档),然后再选择一个成本最低的合并。合并成本的计算方法倾向于回收更多删除文档和产生更小的索引段。
如果某次合并产生的索引段的大小大于index.merge.policy.max_merged_segment参数值,则该合并策略会选择更少的索引段参与合并,使得生成的索引段的大小小于阈值。这意味着,对于有多个分片的索引,默认的index.merge.policy.max_merged_segment则显得过小,会导致大量索引段的创建,从而降低查询速度。用户应该根据自己具体的数据量,观察索引段的状况,不断调整合并策略以满足应用需求。
log byte size合并策略
该策略会不断地以字节数的对数为计算单位,选择多个索引来合并创建新索引。合并过程中,时不时会出现一些较大的索引段,然后又产生出一些小于合并因子(merge factor)的索引段,如此循环往复。你可以想象,时而有一些相同数量级的索引段,其个数会变得比合并因子还少。当碰到一个特别大的索引段时,所有小于该级别的索引段都会被合并。索引中的索引段个数与下次用于计算的字节数的对数成正比。因此,该合并策略能够保持较少的索引段数量并且极小化段索引合并的代价。
log doc合并策略
该策略与log_byte_size合并策略类似,不同的是前者基于索引的字节数计算,而后者基于索引段的文档数计算。以下两种情况中该合并策略表现良好:文档集中的文档大小类似或者你期望参与合并的索引段在文档数方面相当。
合并策略配置

配置tiered合并策略

当使用tiered合并策略时,可配置以下这些选项:
index.merge.policy.expunge_deletes_allowed:默认值为10,该值用于确定被删除文档的百分比,当执行expungeDeletes时,该参数值用于确定索引段是否被合并。
index.merge.policy.floor_segment:该参数用于阻止频繁刷新微小索引段。小于该参数值的索引段由索引合并机制处理,并将这些索引段的大小作为该参数值。默认值为2MB。
index.merge.policy.max_merge_at_once:该参数确定了索引期单次合并涉及的索引段数量的上限,默认为10。该参数值较大时,也就能允许更多的索引段参与单次合并,只是会消耗更多的I/O资源。
index.merge.policy.max_merge_at_once_explicit:该参数确定了索引优化(optimize)操作和expungeDeletes操作能参与的索引段数量的上限,默认值为30。但该值对索引期参与合并的索引段数量的上限没有影响。
index.merge.policy.max_merged_segment:该参数默认值为5GB,它确定了索引期单次合并中产生的索引段大小的上限。这是一个近似值,因为合并后产生的索引段的大小是通过累加参与合并的索引段的大小并减去被删除文档的大小而得来的。
index.merge.policy.segments_per_tier:该参数确定了每层允许出现的索引段数量的上限。越小的参数值会导致更少的索引段数量,这也意味着更多的合并操作以及更低的索引性能。默认值为10,建议设置为大于等于index.merge.policy.max_merge_at_once,否则你将遇到很多与索引合并以及性能相关的问题。
index.reclaim_deletes_weight:该参数值默认为2.0,它确定了索引合并操作中清除被删除文档这个因素的权重。如果该参数设置为0.0,则清除被删除文档对索引合并没有影响。该值越高,则清除较多被删除文档的合并会更受合并策略青睐。
index.compund_format:该参数类型为布尔型,它确定了索引是否存储为复合文件格式(compound format),默认值为false。如果设置为true,则Lucene会将所有文件存储在一个文件中。这样设置有时能解决操作系统打开文件处理器过多的问题,但是也会降低索引和搜索的性能。
index.merge.async:该参数类型为布尔型,用来确定索引合并是否异步进行。默认为true。
index.merge.async_interval:当index.merge.async设置为true(因此合并是异步进行的),该参数值确定了两次合并的时间间隔,默认值为1s。请记住,为了触发真正的索引合并以及索引段数量缩减操作,该参数值应该保持为一个较小值。

配置log byte size合并策略

当采用log_byte_size合并策略时,可配置以下选项:
merge_factor:该参数确定了索引期间索引段以多大的频率进行合并。该值越小,搜索的速度越快,消耗的内存也越少,而代价则是更慢的索引速度。如果该值越大,情形则正好相反,即更快的索引速度(因为索引合并更少),搜索速度更慢,消耗的内存更多。该参数默认为10,对于批量索引构建,可以设置较大的值,对于日常索引维护则可采用默认值。
min_merge_size:该参数定义了索引段可能的最小容量(段中所有文件的字节数)。如果索引段大小小于该参数值,且merge_factor参数值允许,则进行索引段合并。该参数默认值为1.6MB,它对于避免产生大量小索引段是非常有用的。然而,用户应该记住,该参数值设置为较大值时,将会导致较高的合并成本。
max_merge_size:该参数定义了允许参与合并的索引段的最大容量(以字节为单位)。默认情况下,参数不做设置,因而在索引合并时对索引段大小没有限制。
maxMergeDocs:该参数定义了参与合并的索引段的最大文档数。默认情况下,参数没有设置,因此当索引合并时,对索引段没有最大文档数的限制。
calibrate_size_by_deletes:该参数为布尔值,如果设置为true,则段中被删除文档的大小会用于索引段大小的计算。
index.compund_format:该参数为布尔值,它确定了索引文件是否存储为复合文件格式,默认为false。可参考tiered合并策略配置中该选项的解释。

配置log doc合并策略

当使用log_doc(文档数对数)合并策略时,可配置以下这些选项:
merge_factor:与log_byte_size合并策略中该参数的作用相同,请参考前面的解释。
min_merge_docs:该参数定义了最小索引段允许的最小文档数。如果某索引段的文档数低于该参数值,且merge_factor参数允许,就会执行索引合并。该参数默认值为1000,它对于避免产生大量小索引段是非常有用的。但是用户需要记住,将该参数值设置过大会增大索引合并的代价。
max_merge_docs:该参数定义了可参与索引合并的索引段的最大文档数。默认情况下,该参数没有设置,因而对参与索引合并的索引段的最大文档数没有限制。
calibrate_size_by_deletes:该参数为布尔值,如果设置为true,则段中被删除文档的大小会在计算索引段大小时考虑进去。
index.compund_format:该参数为布尔值,它确定了索引文件是否存储为复合文件格式,默认为false。可参考tiered合并策略配置中该选项的解释。
与前面介绍的合并策略类似,上面提及的属性需要以index.merge.policy为前缀。例如,要设置min_merge_docs属性,则应该设置index.merge.policy.min_merge_docs属性。除此之外,log_doc合并策略支持index.merge.async和index.merge.async_interval属性,就像tiered合并策略那样。

7.3、合并策略:
合并线程是按照一定的运行策略来挑选 segment 进行归并的。主要有以下几条:

1) index.merge.policy.floor_segment:默认 2MB,小于该值的 segment 会优先被归并。
2) index.merge.policy.max_merge_at_once:默认一次最多归并 10 个 segment
3) index.merge.policy.max_merge_at_once_explicit:默认 forcemerge 时一次最多归并 30 个 segment
4) index.merge.policy.max_merged_segment:默认 5 GB,大于该值的 segment,不用参与归并,forcemerge 除外
5) index.merge.policy.segments_per_tier:每层允许的段数量大小,默认值是10。一般 >= max_merge_at_once。

7.4、设置延迟提交
根据上面的策略,我们也可以从另一个角度考虑如何减少 segment 归并的消耗以及提高响应的办法:加大 refresh 间隔,尽量让每次新生成的 segment 本身大小就比较大。这种方式主要通过延迟提交实现,延迟提交意味着数据从提交到搜索可见有延迟,具体需要结合业务配置,默认值1s;
针对索引节点粒度的配置如下:

curl -XPUT http://127.0.0.1:9200/索引节点名称/_settings -d '{"index.refresh_interval":"10s"}'

7.5、对特定字段field禁用 norms 和 doc_values 和 stored:
norms、doc_values 和 stored 字段的存储机制类似,每个 field 有一个全量的存储,对存储浪费很大。如果一个 field 不需要考虑其相关度分数,那么可以禁用 norms,减少倒排索引内存占用量,字段粒度配置 omit_norms=true;如果不需要对 field 进行排序或者聚合,那么可以禁用 doc_values 字段;如果 field 只需要提供搜索,不需要返回则将 stored 设为 false;

八、调度
ElasticSearch还允许我们定制合并策略的执行方式。索引合并调度器(scheduler)分为两种,默认的是并发合并调度器ConcurrentMerge-Scheduler。

8.1、并发合并调度器
该调度器使用多线程执行索引合并操作,其具体过程是:每次开启一个新线程直到线程数达到上限,当达到线程数上限时,必须开启新线程(因为需要进行新的段合并),那么所有索引操作将被挂起,直到至少一个索引合并操作完成。
为了控制最大线程数,可以通过修改index.merge.scheduler.max_thread_count属性来实现。
如果我们的系统是8核的,那么调度器允许的最大线程数可以设置为4。

8.2、顺序合并调度器
它使用同一个线程执行所有的索引合并操作。在执行合并时,该线程的其他文档处理都会被挂起,从而索引操作会延迟进行。

8.3、设置合并调度
为了设置特定的索引合并调度器,用户可将index.merge.scheduler.type的属性值设置为concurrent或serial。
 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐