1、当我们在说 Elasticsearch 检索性能优化的时候,实际在说什么?!

检索响应慢!

并发检索用户多时,响应时间不达标

卡死了!

怎么还没有出结果?

怎么这么慢?

为啥竞品产品的很快就返回结果了?

宕机了

等等......

这些都与可能检索有关,确切的说和检索性能有关。

检索性能的优化涉及知识点比较零散,我以官方文档的检索性能优化部分作为大框架和主线,结合实战经验和咨询经验用通俗易懂的语言做下解读。

2、内存要给到位

Elasticsearch 严重依赖文件系统缓存来加快搜索速度。通常,你应该确保至少有一半的可用内存进入文件系统缓存,以便 Elasticsearch 可以将索引的热点区域保留在物理内存中。

线上环境还见过2核4G配置的,基本上跑不了太多数据量。

3、磁盘必要时换 SSD

对写入速度有超高要求的,SSD就是“祥瑞”。

SSD 成本考虑可能不能一步到位,但至少得是普通机械磁盘。

切记尽量不用:NFS 或 SMB 远程文件系统。

4、CPU 考虑核数和线程数

在并发写入或查询量大之后,就会出现 CPU 打满的情况。

可以优化的空间就是:基于CPU 核数合理调节线程池和队列的大小。

5、数据建模要合理

多表关联非 Elasticsearch 所擅长。换句话说,Elasticsearch 支持多表关联方式有限。

像 Mysql 中的动不动几个表的 join 操作,在 Elasticsearch 要考虑必要性和实现复杂度。

Elasticsearch 多表关联仅限于如下几种:

  • 父子文档 join:适用于子文档频繁更新场景。
  • nested 嵌套类型:适用于子文档相对固定、更新频率低的场景。
  • 大宽表拉伸存储:本质空间换时间。
  • 业务层面自己结合检索后的返回结果,自己实现关联。

且:Nested 可以使查询慢几倍,而父子 Join 类型可以使查询慢数百倍。

大家在建模的时候多考虑,如果不刻意建模全部使用默认字段,看看可能带来的“灾难”性的后果,反过来就能理解建模的重要性。

6、尽可能减少检索字段数目

query_string 或 multi_match 查询目标的字段越多,速度就越慢。

提高多个字段搜索速度的常用技术是在索引时将它们的值借助 copy_to 复制到单个字段中,然后在搜索时使用该字段。

copy_to 实现了 1 带 2 、1 带 3 甚至 1 带 N 的效果。

7、合理设置 size 大小

在检索请求的时候 size 值设置很大,会导致命中数据量大,可能会带来严重的性能问题。

建议:合理设置分页 size 值。如果着实数据量大考虑:scroll 或者 search_after 实现。

8、多使用写入前预处理操作

我之前的文章讲情感分析区间查询的时候,其实本质就三个区间:负面、正面、中性。

如果用 range_query 区间检索势必会慢。

建模的时候,可以考虑数据写入的时候,转成:-1、0、1 的 keyword 类型值。

将 range_query 的范围检索变成了基于倒排索引的精准查找 term query,效率自然会提升。

能借助 ingest 预处理完成的,不要放到后面借助 script + update_by_query。

使用过 script + update_by_query的自然知道有多苦。

看到这里有同感的老铁可以留言说一下感受。

9、能用 keyword 字段类型就不要使用其他

如果一个字段可以设置为:number 数值类型字段也可以设置为 keyword 类型。在建模阶段可以参考如下考虑方式实现建模选型。

得看你的应用场景,如果涉及 range query 推荐 number 类型,具体可以:integer、long 或者其他数值类型。

如果仅是精准匹配 term 级别的检索,那 keyword 就能搞定。

如果还感觉两种都有可能,建议设置:keyword 和 number 双类型,借助 fields 实现。

fields 组合类型实现参考如下:

PUT test_0001 {   "mappings": {     "properties": {       "age":{         "type":"integer",         "fields": {           "keyword":{             "type":"keyword"           }         }       }     }   } }

10、尽量规避使用脚本

如果可能,请避免使用:

  • 基于脚本的排序
  • 基于脚本的聚合
  • 基于script_score 查询

painless 脚本翻译为中文是:“无痛”。

但,用过你就知道有多痛。

  • 第一:不是很好用,很多示例,官方文档也没有穷尽所有样例,需要花时间摸索。
  • 第二:性能问题,解决问题一时爽,线上一跑“悔断肠”。

那,不让用,线上的确需要咋办?

尽量前置写入的时候结合 ingest + script 实现。

如果对写入指标要求没有那么高,通过稍微增大写入的时间间接提高了检索效率,何乐而不为?

11、日期做舍入处理,能间接利用缓存

GET index/_search {   "query": {     "constant_score": {       "filter": {         "range": {           "my_date": {             "gte": "now-1h/m",             "lte": "now/m"           }         }       }     }   } }

“/m” 的方式能有效利用时间缓存。

12、有效使用 filter 缓存

在 Elasticsearch 查询中有效使用 filter 过滤器可以显着提高搜索性能。

filter 过滤优势体现在:

  • 缓存。
  • 和query 相比,不需要计算评分,所以更快。

13、对历史索引数据使用段合并

前提:基于时间切分索引,对于相对冷的数据,访问密集型没有那么高的数据,推荐使用段合并。

切记:不要对正在写入数据的索引进行段合并。

7.X 版本可以借助 ILM 索引生命周期管理实现。

14、启用 eager global ordinals 提升高基数聚合性能

适用场景:高基数聚合。

高基数聚合场景中的高基数含义:一个字段包含很大比例的唯一值。

global ordinals 的本质是:启用 eager_global_ordinals 时,会在刷新(refresh)分片时构建全局序号。这将构建全局序号的成本从搜索阶段转移到了数据索引化(写入)阶段。

PUT my-index-000001 {   "mappings": {     "properties": {       "tags": {         "type": "keyword",         "eager_global_ordinals": true       }     }   } }

15、预热文件系统缓存

如果重新启动运行 Elasticsearch 的机器,文件系统缓存将是空的,因此操作系统将索引的热点区域加载到内存中需要一些时间,以便快速搜索操作。

可以使用 index.store.preload 设置根据文件扩展名明确告诉操作系统哪些文件应该立即加载到内存中。

PUT /my-index-000001 {   "settings": {     "index.store.preload": ["nvd", "dvd"]   } }

在 lucene 中,

  • nvd 是指:全文检索文件;
  • dvd 是指:用于聚合排序的列存文件。

16、合理使用 index sort 边写入边排序机制

PUT my-index-000001 {   "settings": {     "index": {       "sort.field": "date",        "sort.order": "desc"       }   },   "mappings": {     "properties": {       "date": {         "type": "date"       }     }   } }

这个配置相信你一看就懂,发生在写入前,创建索引的时候设定的排序字段。

本质:通过降低写入速度间接提升检索速度。

17、通过 perference 优化缓存利用率

perference 用在两次检索结果不一致的时候,本质是:主、副本分片数据不一致导致的,有半路由的机制。

合理使用 perference 参数能优化缓存使用率。

18、设置合理的分片数和副本数

主分片的设置需要结合:集群数据节点规模、全部数据量和日增数据量等综合维度给出值,一般建议:设置为数据节点的1-3倍。

分片不宜过小、过碎。有很多小分片可能会导致大量的网络调用和线程开销,这会严重影响搜索性能。

副本数不是越多越好?

在许多情况下,拥有更多副本有助于提高搜索性能。但是不代表副本越多越好。

增加副本的前提是考虑:磁盘存储空间的容量上限和磁盘警戒水位线。本质还是以空间换时间。

一般非高可用场景,基本一个副本足够。

官方给出了合理副本大小的公式供参考:

如果你的集群有 num_nodes 个节点、num_primaries 主分片,并且你希望最多同时处理 max_failures 个节点故障,那么适合你的副本数为:

 max(max_failures, ceil(num_nodes / num_primaries) - 1)

19、避免使用 wildcard 检索

避免使用 wildcard 通配符检索,尤其是前缀通配符查询。

我自己早些年线上环境实现曾经大量使用:wildcard,导致客户现场演示宕机,我自己因此也写了“检讨书”,血淋淋的教训再次告诉大家。

几年后回头看当时为什么选型 wildcard?复盘原因小结如下:

  • MySql 的使用惯性。

MySql 中 select * from table where title like ‘%长津湖%'。顺理成章的认为 Elasticsearch 中的 wildcard 也能实现类型功能。

  • 对 Elasticsearch 不求甚解。

能简单使用且测试环境小样没有问题,直接更新线上环节。客户现场数据一多,直接崩溃。

  • 为达目的,功能优先,没有考虑性能。

产品经理要求字段中存在的字符都能检索出来。

Elasticsearch 本质是倒排索引提高检索效率,如果分词词典不完备,除非 ngram 逐个字符细粒度分词,否则几乎做不到的。

wildcard 功能方面必然能满足,但是性能问题当时没有做大量测试。

  • 没有对 Elasticseearch 全局认识

全局看,Elasticsearch 就那么几种类型,全文检索类型、精准匹配类型是重头戏。

当时选型的时候,摸着石头过河,拿起石头就用,结果石头有“刺“,把手给扎了。

更好的方式应该是:全局认识,有几种类型石头?哪里有石头?石头应用场景是什么?我的业务需要哪种类型的石头?

后面优化的方案就是:字词混合索引 + match_phrase 短语匹配实现,一方面保证了匹配的精准性,另一方面保证了召回率。

20、谨慎使用 Regex 正则检索

正则检索也会有响应慢及性能问题,要谨慎使用。

21、谨慎使用全量聚合和多重嵌套聚合

聚合的本质是不精准的,原因在于主、副本分片数据的不一致性。

对于实时性业务数据,每分、每秒都有数据写入的,要考虑数据在变化,聚合结果也会随之变化。

我在业务开发中使用全量聚合的目的是规避聚合结果的不精准性,但是带来的则是性能问题。

多重嵌套聚合随之嵌套层数的增多,复杂度也会激增,检索响应速度会变慢甚至带来性能问题。

22、设置合理的 Timeout 时间

超时参数和在参数后终止在执行大量搜索或结果数据庞大时非常有用。

在 python 客户端或者 java 客户端连接的时候都建议设置好 Timeout 值。

23、合理设置删除文档的方式

当数据量非常大了之后怎么办?两种方式做一下对比:

  • 方式一:大索引存储。

数据量大了之后,删除部分索引数据,借助:delete_by_uery 实现。

  • 方式二:冷热集群架构+基于时间切分索引。

必要时候,删除较早日期的索引,借助:delete 实现。

方式一本质是逻辑删除,数据看似删除了,但磁盘空间短期内会暴增。待段合并后,才会物理删除。

方式二本质是物理删除,删除索引会立即释放磁盘。

所以,当磁盘空间吃紧,尤其到了警戒水位线:85%、90%、95%之后。

方式二删除:稳、准、狠。而方式一相对“磨磨唧唧、娘娘们们”,非必要不推荐。

参考

https://www.elastic.co/guide/en/elasticsearch/reference/master/tune-for-search-speed.html

https://opster.com/blogs/improve-elasticsearch-search-performance/

https://opster.com/elasticsearch-glossary/elasticsearch-slow-search-query-guide/

https://medium.com/analytics-vidhya/improving-elasticsearch-query-performance-3b59c6b15a97

https://opster.com/elasticsearch-glossary/elasticsearch-increase-search-speed/