es调优涉及问题

ES查询策略的选择优化:

 

问题:ES6.8 使用TermQuery查询数值类型字段变慢,改为RangeQuery却变得飞快?

 

profile显示耗时都在build_scorer中。

 


解释真相:
在5.x以前,Lucene版本中无数值类型,本质都是底层转换为字符串,使用倒排索引的方式进行数据查询。
这样的话有一个问题,存储成字符串,对于等值查询问题不大,对于大范围查询仍然比较灾难,底层自动化,转化成了filter的固定OR的查询方式,这样时间开销依旧很大。

 


在Lucene6.0开始取消了数值类型的倒排索引结构,数值类型就使用了 block d-k tree 树的方式存储数值型的范围数据。

 

每个filter/Query都独立执行,拿到各自的结果集以后,再做结果集的合并。会优先从开销比较小的执行单元入手。

 


现在回答问题:6.8的es版本使用了6.0Lucene,数值类型已经使用了bdk-Tree,已经无索引结构。虽然在tree中value是按照顺序进行存储的,但是文档id却无序。这时根据docid去获取具体数据时 ,没法根据句跳跃表这种有序的数据结构去快速查询数据,只能拿出docid集合,去迭代docid集合并获取数据,所以profile显示耗时都在build_scorer中。

 


为什么使用了RangeQuery又变得很快。在bdk-Tree结构中获取docid并获取数据需要迭代docid的列表。如果docid比较大,会耗时较高。但6.0Lucene对RangeQuery做了优化,基于正排索引docValue的方式记录了docId以及实际数据的映射。

 


总结:如果数值类型没有做范围查询的需要则可改为keyword类型;如果一定要使用数值类型,请使用es的5.4以上版本,并使用rangeQuery通过docValue提供性能。

 

 

 

 

搜索时会有两个问题,第一、数量问题,第二,排序问题

1、query and fetch(向所有分片直接发送数据请求,各自算分后直接返回给用户,但用户获取的数据条数将会是原来的n倍,n为分片数量,新版本已经不支持)

 

向索引的所有分片(shard)都发出查询请求,各分片返回的时候把元素文档(document)和计算后的排名信息一起返回。这种搜索方式是最快的。因为相比下面的几种搜索方式,这种查询方法只需要去shard查询一次。但是各个shard返回的结果的数量之和可能是用户要求的size的n倍。

 

2、query then fetch(默认的搜索方式,向所有分片发送匹配并算分的请求,但只返回id和算分并进行重新排序,按照用户的size要求,去除多余数据数据后,从各个shard获取document的完整信息)

 

如果你搜索时,没有指定搜索方式,就是使用的这种搜索方式。这种搜索方式,大概分两个步骤,第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。

 

3、DFS query and fetch

 

这种方式比第一种方式多了一个初始化散发(initial scatter)步骤,有这一步,据说可以更精确控制搜索打分和排名。

 

4、DFS query then fetch(多了一步发散操作。就是算分会在协调节点去做,从各个分片获取匹配数据后,会得到各个分片的总数据量在进行BM25的算分,重新排序) 一般用在跨索引查询

 

比第2种方式多了一个初始化散发(initial scatter)步骤。

 

 

精准度优化:

1 匹配精准度不够参数:minimum_should_match    我们可以通过代码的方式进行再match匹配的字段上添加匹配力度  ,这样如果搜索有4个词汇,那么需要匹配至少3个才行。 

2 匹配精准度不够参数:boots,在匹配某一个指定的字段时,一旦匹配成功让其增加score分值,提升排名。

3 再有就是分值计算不精准到时结果不精准,索引如果分了多个shard那么这些shard上的数据应该更加平均,不应该有数据倾斜,否则es通过TF/IDF,算法中的IDF算法是根据某些词在整体shard中的document去计算的,如果shard中的文档数量相差较大,可能会影响分值。

 

查询速度优化(filter过滤和索引创建的粒度):

4 条件过滤优化,使用query中的filter可以增加速度,因为默认情况下es对filter的结果做了bitset缓存。而且不参与score的计算会减少时间复杂度,然后在小范围内,通过match进行匹配会更提升数据拉取效率。

5 es搜索效率优化,因为es搜索数据量较大情况下,很大程度依赖于操作系统的oscache进行数据缓存的检索,如果数据量过大,需要考虑提升机器本身内存的扩容,或者是增加机器,并增加replicshard数量,用来接受更多的请求提升搜索性能。

例如  8g的数据,3个机器上(指定了1个副本),每个机器当时是1g的内存,发现偶尔搜索会有延时5s左右的情况,增加机器本身8g内存后解决了问题。

 

索引创建经验:使用滑动时间创建新的索引并以时间单位作为后缀,这样可以减少单个索引的分片数量,在有针对的时间点范围的查询能走更少的datemerge过程,如果一类数据仅仅使用一个索引的话,如果数据增长速度惊人,是不利于维护的,并且如果扩展shard也会有效率的损失,不如在创建索引,这个最初阶段直接进行数据分散,做索引的增量,如果真要使用全范围时间维度查询,可以使用多索引映射到同一别名,来直接操作索引别名,或使用模糊后缀的方式如:GET /index_2020*/_search 这种,可支持跨索引查询。

es索引的数据量是有限制的,而且官方建议不超过50G,合理范围是20~40G,而且单个shard不应该超过2的23次方条(20亿条)这个值倒是很难挑战,而且不建议索引分片过多,如果多的话数据量多起来以后会影响查询效率,实际要通过协调节点分shard取数, 数据通过网络io汇总后,再处理。

以千万数量作为分界点吧,如果日均能产生千万数据,可以使用天为粒度创建索引。

 

transport 升级为rest客户端的优化:

es7.0 以后就废除了transport 9300 客户端的方式,而提倡使用resthighlevelclient,本身rest性能也是比较不错的了,而且transport相当于一个“集群中内部节点”的启动方式,安全性是不够的,而且每个节点间还会默认建立(16根连接)长连接,如果客户端较多性能也是有问题的,而且es本身客户端连接任意一个节点都可以进行通信,如果使用transport相当于打破了这个规则,时协调节点不受控制;如果使用rest就可以容易的控制,可以在最前端专门提供一个协调节点。

 

聚合方式:

聚合查询aggs,如果使用了terms单纯分桶聚合时,可以指定eager_global_ordinals:true来开启缓存,此时文档字段的type必须为keyword,示例如下:

链接:https://www.cnblogs.com/zzq-include/p/14067840.html

PUT /i1
{
  "mappings": {
    "t1":{
      "properties": {
        "huiyuandengji":{
          "type":"keyword",
          "eager_global_ordinals":true 
        },
         "goumai":{
          "type":"keyword",
          "eager_global_ordinals":true 
        },
          "mingandu":{
            "type":"keyword",
            "eager_global_ordinals":true 
        },
        "age":{
            "type":"keyword",
            "eager_global_ordinals":true 
        }
      }
    }
  }
}

 

 

分词字段进行分桶聚合:

POST /person_list/_mapping/info
{
  "properties": {
        "字段名": {
            "type": "text",
            "fielddata": true,
            "fields": {"raw": {"type": "keyword"}}
            }
        }
}

分词字段需要设置"fielddata": true,才能进行聚合运算,而且需要使用内部不分词的方式,否则会出现分词后在聚合的现象,这种方式未必是我们想要的而且会花费掉大量的内存空间。

 

内部原理

analyzed字符串的字段,字段分词后占用空间很大,正排索引不能很有效的表示多值字符串,所以正排索引不支持此类字段。

fielddata结构与正排索引类似,是另外一份数据,构建和管理100%在内存中,并常驻于JVM内存堆,极易引起OOM问题。

加载过程

fielddata加载到内存的过程是lazy加载的,对一个analzyed field执行聚合时,才会加载,而且是针对该索引下所有的文档进行field-level加载的,而不是匹配查询条件的文档,这对JVM是极大的考验。

fielddata是query-time创建,动态填充数据,而不是不是index-time创建,

内存限制

indices.fielddata.cache.size 控制为fielddata分配的堆空间大小。 当你发起一个查询,分析字符串的聚合将会被加载到fielddata,如果这些字符串之前没有被加载过。如果结果中fielddata大小超过了指定大小,其他的值将会被回收从而获得空间(使用LRU算法执行回收)。

默认无限制,限制内存使用,但是会导致频繁evict和reload,大量IO性能损耗,以及内存碎片和gc,这个参数是一个安全卫士,必须要设置:

indices.fielddata.cache.size: 20%

 

 

监控fielddata内存使用

Elasticsearch提供了监控监控fielddata内存使用的命令,我们在上面可以看到内存使用和替换的次数,过高的evictions值(回收替换次数)预示着内存不够用的问题和性能不佳的原因:

# 按索引使用 indices-stats API
GET /_stats/fielddata?fields=*

# 按节点使用 nodes-stats API
GET /_nodes/stats/indices/fielddata?fields=*

# 按索引节点
GET /_nodes/stats/indices/fielddata?level=indices&fields=*

fields=*表示所有的字段,也可以指定具体的字段名称。

熔断器

indices.fielddata.cache.size的作用范围是当前查询完成后,发现内存不够用了才执行回收过程,如果当前查询的数据比内存设置的fielddata 的总量还大,如果没有做控制,可能就直接OOM了。

熔断器的功能就是阻止OOM的现象发生,在执行查询时,会预算内存要求,如果超过限制,直接掐断请求,返回查询失败,这样保护Elasticsearch不出现OOM错误。

常用的配置如下:

  • indices.breaker.fielddata.limit:fielddata的内存限制,默认60%
  • indices.breaker.request.limit:执行聚合的内存限制,默认40%
  • indices.breaker.total.limit:综合上面两个,限制在70%以内

最好为熔断器设置一个相对保守点的值。fielddata需要与request断路器共享堆内存、索引缓冲内存和过滤器缓存,并且熔断器是根据总堆内存大小估算查询大小的,而不是实际堆内存的使用情况,如果堆内有太多等待回收的fielddata,也有可能会导致OOM发生。

 

 

 

字符串分词后排序不准的问题:(6.X中string已经废除,可以额外指定keyword以及fileddate=true来创建正排索引)

 

 

https://cloud.tencent.com/developer/article/1511163

posted @ 2020-07-24 14:07  soft.push("zzq")  Views(706)  Comments(0Edit  收藏  举报