当我们说的时候 Elasticsearch 当检索性能优化时,你实际上在说什么?
- 慢慢检索响应!
- 并发检索用户多时,响应时间不达标
- 卡死了!
- 为什么还没有结果?
- 怎么这么慢?
- 为什么竞争产品很快就会回到结果?
- 宕机了
等等...
这些都与可能的检索有关,确切地与检索性能有关。
检索性能的优化涉及到分散的知识点。我以官方文档的检索性能优化部分为大框架和主线,结合实践经验和咨询经验,用易于理解的语言进行解释。
2.内存要到位
Elasticsearch 严重依赖文件系统缓存来加快搜索速度。通常,您应该确保至少一半的可用内存进入文件系统缓存,以便 Elasticsearch 索引的热点区域可以保留在物理内存中。
在线环境也见过2核44G配置,基本上不能跑太多数据量。
必要时更换磁盘 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 实现组合类型参考如下:
PUTtest_0001 { "mappings":{ "properties":{ "age":{ "type":"integer", "fields":{ "keyword":{ "type":"keyword" } } } } } }
10.尽量避免使用脚本
如有可能,请避免使用:
- 基于脚本的排序
- 基于脚本的聚合
- 基于script_score 查询
painless 脚本翻译成中文:无痛。
但,用过你就知道有多痛。
- 第一:不太好用,很多例子,官方文档并没有耗尽所有的例子,需要时间去探索。
- 二是性能问题,解决问题一时爽,网上跑悔断肠。
那,不要用,网上真的需要怎么办?
尽量前置写入时结合 ingest script 实现。
如果对写入指标的要求没有那么高,为什么不呢?
11.放弃日期可间接使用缓存
GETindex/_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/