预留足够的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/d,2015-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
深度优先还是广度优先
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配置优化方案最终完整版