elasticsearch搜索速度优化

预留足够的offheap内存

在一般情况下,应用程序的读写都会被操作系统“cache”(除了direct方式),cache保存在系统物理内存中(线上应该禁用swap),命中cache可以降低对磁盘的直接访问频率。搜索很依赖对系统 cache 的命中,如果某个请求需要从磁盘读取数据,则一定会产生相对较高的延迟。

在Elasticsearch中,使用的内存分为onheap以及offheap部分,onheap部分暂时不说了,回头我这边单独使用一篇文章介绍Elasticsearch的内存模型,这里仅说一下Elasticsearch的offheap部分,由于底层的segment由Lucene直接负责管理,所以查询命中的segment会被放到os的cache中,采用LRU的策略进行管理,且这部分缓存是offheap的。

为了增加查询效率应该尽量增加segment的缓存大小,所以至少为系统cache预留一半的可用物理内存,更大的内存有更高的cache命中率。

使用更高效的硬件

简单粗暴,能用钱解决的都不是事,这是Elasticsearch的用户必须要有的觉悟,但是这样的效果往往也是最好的。

写入性能对CPU的性能更敏感,而搜索性能在一般情况下更多的是在于I/O能力,使用SSD会比旋转类存储介质好得多。尽量避免使用NFS 等远程文件系统,如果 NFS 比本地存储慢3倍,则在搜索场景下响应速度可能会慢10倍左右。这可能是因为搜索请求有更多的随机访问。

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

合理的文档模型及使用方式

如果可能的话,尽量避免join操作

为了让搜索时的成本更低,文档应该合理建模。嵌套(nested)会使查询慢几倍,父子(parent-child)关系可能使查询慢数百倍,因此,如果可以通过扁平化处理(denormalizing)文档来回答相同的问题,则可以在一定程度上减少join的操作,显著地提高搜索速度。

使用routing

对于数据量较大的index,一般会配置多个shard来分摊压力。这种场景下,一个查询会同时搜索所有的shard,然后再将各个shard的结果合并后,返回给用户。

对于高并发的小查询场景,每个分片通常仅抓取极少量数据,此时查询过程中的调度开销远大于实际读取数据的开销,且查询速度取决于最慢的一个分片。

开启routing功能后,ES会将routing相同的数据写入到同一个分片中(也可以是多个,由index.routingpartitionsize参数控制)。如果查询时指定routing,那么ES只会查询routing指向的那个分片,可显著降低调度开销,提升查询效率。

routing的使用方式如下:

# 写入

PUT my_index/my_type/1?routing=user1

{

  "title": "This is a document"

}

# 查询

GET my_index/_search?routing=user1,user2

{

  "query": {

       "match": {

              "title": "document"

              }

       }

}

预索引数据

针对某些查询的模式来优化数据的索引方式。例如,如果所有文档都有一个 price字段,并且大多数查询在一个固定的范围上运行range聚合,那么可以通过将范围“pre-indexing”到索引中并使用terms聚合来加快聚合速度。

例如,文档起初是这样的:

PUT index/type/1 {

"name": "apple",

"price": 13

}

采用如下的搜索方式:

GET index/_search
{
	"aggs": {
		"price_ranges": {
			"ranges": {
				"field": "price",
				"ranges": [
					{
						"to": 10
					},
					{
						"from": 10,
						"to": 100
					},
					{
						"from": 100
					}
				]
			}
		}
	}
}

那么我们考虑在建立索引时对文档进行预索引,增加price_range 字段,mapping 为keyword类型:

PUT index 
{
	"mappings": {
		"type": {
			"properties": {
				"price_range": {
					"type": "keyword"
				}
			}
		}
	}
}

PUT index/type/1
{
	"name": "apple",
	"price": 13,
	"price_range": "10-100"
}

接下来,搜索请求可以聚合这个新字段,而不是在price字段上运行range聚合:

GET index/_search
{
	"aggs ": {
		"price ranges ": {
			"terms ": {
				"field": "price_range"
			}
		}
	}
}

字段mapping设置优化

有些字段的内容是数值,但并不意味着其总是应该被映射为数值类型,例如,一些标识符,将它们映射为keyword可能会比integer或long更好。

另外在6.x 版本,数值类型使用的是BKD-Tree 索引数据结构,适合对数值类型进行范围查询;如果数值类型只会进行精确查询或者是有限个数的integer,设置成keyword 使用倒排索引进行查询的效率要高。

避免使用脚本

   一般来说,应该避免使用脚本。如果一定要用,则应该优先考虑Elasticsearch支持的painless和expressions。

优化date range搜索

在使用日期范围检索时,使用now的查询通常不能缓存,因为匹配到的范围一直在变化。但是使用rounded date则可以利用上query cache。例如:

  • now+1h/d,表示当前时间加上一个小时,并向一天取整。
  • 2015-01-01||+1M/d2015-01-01加上一个月,并向一天取整

 

支持的单位也很多,比如:

 

  • y,代表一年
  • M,代表一个月
  • w,代表一周
  • d,代表一天
  • h,代表一个小时
  • m,代表一分钟
  • s,代表一秒钟
  • ms,代表毫秒

为只读索引执行force-merge

为不再更新的只读索引执行force merge,将Lucene索引合并为单个分段,可以提升查询速度。当一个Lucene索引存在多个分段时,每个分段会单独执行搜索再将结果合并;另外执行force_merge后也会释放无法被GC的segmentCache(另一种是close掉索引)。所以将只读索引强制合并为一个Lucene分段不仅可以优化搜索过程,减少内存占用,对索引恢复速度也有好处。

基于日期使用rollover创建的索引的旧数据一般都不会再更新。此前的章节中说过,应该避免持续地写一个固定的索引,直到它巨大无比,而应该按一定的策略,例如,每天生成一个新的索引,然后用别名关联,或者使用索引通配符。这样,可以每天凌晨对昨天的索引执行force-merge、Shrink等操作。

预热全局序号(global ordinals)

全局序号是一种shard级别的数据结构,用于在keyword字段上运行terms聚合。它用一个数值来代表字段中的字符串值,然后为每一数值分配一个bucket,在内存中形成一张映射表。构建Global Ordinals的目的是为了减少内存使用、加快聚合统计,在大多数情况下其表现出来的性能都非常好(下一章节介绍什么时候不好)

另外需要一个对 global ordinals 和 bucket的构建过程。默认情况下,它们被延迟构建,因为ES不知道哪些字段将用于 terms聚合,哪些字段不会。可以通过配置映射在刷新(refresh)时告诉ES预先加载全局序数:

execution hint设置

terms聚合有两种不同的机制:

  • 通过直接使用字段值来聚合每个桶的数据(map)。
  • 通过使用字段的全局序号并为每个全局序号分配一个bucket(global_ordinals)。

ES 使用 global_ordinals 作为 keyword 字段的默认选项,它使用全局序号动态地分配bucket,因此内存使用与聚合结果中的字段数量是线性关系。在大部分情况下,这种方式的速度很快。

当查询只会匹配少量文档时,即返回的结果集数据量很小时,可以考虑使用 map。另外 map 只在脚本上运行聚合时使用,因为它们没有序号( ordinals )。

两者的平衡点在于,当数据集大到一定程度的时候,map的内存开销带来的代价抵消了global_ordinals构建带来的开销,从而变得更慢,这个严格来说需要进行测试来确定临界点。

预加载数据到文件系统缓存

如果ES主机重启,则文件系统缓存将为空,此时搜索会比较慢。可以使用index.store.preload设置,通过指定文件扩展名,显式地告诉操作系统应该将哪些文件加载到内存中,这对于提高索引的搜索性能非常有用。但是请注意,这可能会减慢索引的打开速度,因为只有在将数据加载到物理内存之后,索引才会可用。所以需要在查询速度与集群恢复速度中进行折中。如果确定需要预加载的话,不推荐把所有的文件都预加载到内存,通常可能更好的选择是设置为["nvd", "dvd", "tim", "doc", "dim"],这样的话将会预加载norms,doc values,terms dictionaries, postings lists 和 points。配置如下:

curl -X PUT "localhost:9200/index" -H 'Content-Type: application/json' -d'

{

  "settings": {

    "index.store.preload": ["nvd", "dvd", "tim", "doc", "dim"]

  }

}

调节搜索请求中的batched_reduce_size

该字段是搜索请求中的一个参数。默认情况下,聚合操作在协调节点需要等所有的分片都取回结果后才执行,使用 batched_reduce_size 参数可以不等待全部分片返回结果,而是在指定数量的分片返回结果之后就可以先处理一部分(reduce)。这样可以避免协调节点在等待全部结果的过程中占用大量内存,避免极端情况下可能导致的OOM。

该字段的默认值为512M,从ES 5.4开始支持。

如果请求中潜在的分片数量很大,则应将此值用作保护机制,以减少每个搜索请求的内存开销。设置如下:

GET user_order/_search?q=user:kimchy&batched_reduce_size=256



POST  /user_order/_search

{

    "query" : {

        "term" : { "user" : "kimchy"}

        "batched_reduce_size":256

    }

}

使用近似聚合

近似聚合以牺牲少量的精确度为代价,大幅提高了执行效率,降低

了内存使用。由于没使用过,大家需要使用的话,使用方式可以参考官方手册:

Percentiles Aggregation

(https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html)。

Cardinality Aggregation

(https://www.elastic.co/guide/en/elasticsearch/reference/current/aggregations-metrics-cardinality-aggregation.html)。

深度优先还是广度优先

ES有两种不同的聚合方式:深度优先和广度优先。

深度优先是默认设置,先构建完整的树,然后修剪无用节点。大多数情况下深度聚合都

能正常工作,

但是有些特殊的场景更适合广度优先,先执行第一层聚合,再继续下一层聚合之前会先做修剪

修改方式如下:

"collect_mode" : "breadth_first"

自适应副本选择(ARS)

为了充分利用计算资源和负载均衡,协调节点将搜索请求轮询转发到分片的每个副本,轮询策略是负载均衡过程中最简单的策略,任何一个负载均衡器都具备这种基础的策略,缺点是不考虑后端实际系统压力和健康水平。

ES希望这个过程足够智能,能够将请求路由到其他数据副本,直到该节点恢复到足以处理更多搜索请求的程度。在ES中,此过程称为“自适应副本选择(ARS)”。

ES的ARS实现基于这样一个公式:对每个搜索请求,将分片的每个副本进行排序,以确定哪个最可能是转发请求的“最佳”副本。与轮询方式向分片的每个副本发送请求不同,ES选择“最佳”副本并将请求路由到那里。

ARS从6.1版本开始支持,但是默认关闭,可以通过下面的命令动态开启:

PUT /_cluster/settings

{

    "transient":{

        "cluster.routing.use_adaptive_replica_selection":true

    }

}

从ES 7.0开始,ARS将默认开启。

终极版优化方案详见:Elasticsearch配置优化方案最终完整版

猜你喜欢

转载自blog.csdn.net/microGP/article/details/106556301