Elasticsearch的TermsQuery慢查询分析和优化

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 前言 本篇文章主要记录业务上的一个TermsQuery优化和分析的过程和一些思考。 在使用ES的时候,经常会遇到慢查询,这时候可以利用profile进行分析,当利用profile也查看不出什么端倪时候,可以尝试通过阅读代码查看查询为什么这么慢。如下是一个我们内部业务的一个慢查询,经常出现4s左右的延时,一模一样的查询,但是延时不一样,且很难复现。 { "from": 0,

前言

本篇文章主要记录业务上的一个TermsQuery优化和分析的过程和一些思考。

在使用ES的时候,经常会遇到慢查询,这时候可以利用profile进行分析,当利用profile也查看不出什么端倪时候,可以尝试通过阅读代码查看查询为什么这么慢。如下是一个我们内部业务的一个慢查询,经常出现4s左右的延时,一模一样的查询,但是延时不一样,且很难复现。

{
  "from": 0,
  "size": 10,
  "query": {
    "bool": {
      "must": [
        { "term": { "field_1": {"value": "a" }}},
        { "terms": {"field_2": [ "a", "b", "c","f", "e", "f", "g", "h", "i" ]} },
        { "range": { "time": {"from": 1,  "to": 10}}}
      ]
    }
  },
  "sort": [{ "index_sort_field": { "order": "asc"}} ],
  "track_total_hits": false
}

优化和分析

当前索引单shard1亿多条数据,写入较少但是比较稳定,集群写入查询、cpu、io等资源都不紧张。且查询使用了index_sort,没有访问_source文件,没有访问total_hit,表面上已经没有了什么优化空间。

首先利用es的profile分析这个查询,并且请求中禁用了request_cache,但是并没有发现任何问题,查询响应速度很快,也没有任何的慢查询,因此通过profile去复现并分析这个问题,所以直接简单暴力的将shard进行扩容,降低单shard的容量。

1. shard扩容

当前单shard1亿的容量并不是很大,通过扩容将原来的30个shard扩容到了60个shard,效果还是十分明显的,偶尔的慢查询降低到了2s。

原理分析

之前单shard1亿的容量,shard扩容后变成了单shard5000万数据,那么单shard需要查询和计算的总量变少一倍,理论上确实可以降低一倍查询延时。

但是shard扩容也不能解决所有问题,比如当shard扩容到更多时候,效果并不是成倍的下降,特别是在节点不多时候,一个节点上放了更多的shard副本,反而增加了集群的各种负担,比如:

  1. shard变多造成refrensh、bulk、search等相关线程池压力可能更大
  2. 单节点需要串行处理更多的meta信息
  3. shard越多越容易出现search的查询长尾问题:个别shard查询较慢,从而拖累整个查询造成整体查询响应变慢
  4. 当shard超过256个后,查询会分为2个批次进行查询,查询延时会直接升高一倍

因此,这里并没有继续将shard扩容,而是想通过其他方式进行慢查询分析。

2. 复现问题

既然线上很稳定的隔几分钟出现一次慢查询,那么一定不是巧合和一些异常抖动,因此需要尝试其他方式来复现这个问题。

首先怀疑到cache让索引查询变得很快,所以查看了这个索引的queryCache命中率,大约接近80%的命中率,很显然我们需要降低cache命中率来复现这个问题。一般降低cache命中率的方式有:

  1. 提高写入吞吐。
    • 当前写入比较少,几秒钟或者几分钟一条数据,因此造成很多数据都有缓存且没有失效。
  1. 提高查询qps
    • 使用queryCache需要拿到读锁,如果并发很高导致查询线程拿不到锁,那么将利用不到缓存。
      • queryCache是LRU策略的cache,且cache结果很大概率被其它线程改写为新的造成cache失效,因此这里是一个读锁
      • ES认为在高并发情况下,阻塞等待读锁的时间可能已经将查询执行完毕,因此没有必要一直阻塞等待读锁
原理分析

通过这两种策略,可以将慢查询很容易的复现出来,通过查看profile发现:

  1. 没有长尾问题
  2. terms查询十分耗时

问题已经定位到了TermsQuery,那么问题就变成了:

  1. 为什么TermsQuery比较慢?
  2. 为什么有时候慢有时候快?

TermsQuery是ES中的概念,它会转化为Lucene的TermInSetQuery。从下面的Lucene的代码注释中,我们可以看到:

  1. 需要构建DocIdSet的查询会比较慢
  2. term等点查特别快

TermInSetQuery要进行多个term查询,通常term查询是很快的,但是合并多个倒排链去构建DocIdSet会比较慢,最终导致TermIndexSetQuery很慢。TermInSetQuery合并倒排链使用的是DocIdSetBuilder,其流程和RoaringDocIdSet不一致,其中DocIdSetBuilder的流程为:

RoaringDocIdSet.Builder中有很多更多的策略,相比DocIdSetBuilder更优一些,但是Terms查询的倒排链合并用不到这些优化:

在TermInSetQuery中,我们发现lucene还做了一个优化,当terms中term的个数小于16个时候,会将terms的查询转化为bool的should查询,直接合并倒排链可能比上面的构建bitSet会更快,可以用到RoaringDocIdSet的各种优化。这里也给了一些优化的灵感。

综上,整个TermInSetQuery中构建DocIdSet的核心查询流程流程为:

那么,为什么有时候慢有时候快呢,根据profile的第三个特征,基本上可以定位到是缓存问题,因此接下来我们来分析一下缓存问题。

3. 缓存分析

lucene中的具体cache策略管理在UsageTrackingQueryCachingPolicy中,具体的执行流程为:

是否允许缓存是一个比较复杂的问题,相关因素按顺序拆解为如下几个:

  1. Query的内部实现中,isCacheable函数可以决定是否允许缓存
  2. 当前segment是否允许被缓存以及缓存,主要检查当前segment中的文件数量是否达到阈值
  3. 是否可以拿到缓存的读写锁,并发高了拿不到锁则不能使用缓存
  4. IndexReader是否支持缓存,取决于IndexReader#getCoreCacheHelper的实现,如果返回null则不支持缓存
  5. QueryCachingPolicy中的策略

根据上面的一些判断标准,我们发现了2个需要注意的地方:

1. TermInSetQuery内部的isCacheable有什么特殊点吗?

当前我们使用的是TermInSetQuery,其内部的是否允许缓存策略有一些特殊之处,当terms中的term总size过大的时候不允许进行cache,当前阈值为1k,是一个比较小的值。lucene作者认为terms越长则代表着潜在有更多的term,那么其需要cache的内容可能很大,造成cache的浪费和内存溢出的风险。同时,我们分析terms的长度过长可能导致内容越不容易被下次利用到,因此这个角度考虑也有一定的道理。

线上的慢查询中,有大部分都是正好超过1k长度的,因此TermInSetQuery的缓存利用不起来,查询速度也就会变得很慢,这也是线上慢查询的一个原因之一。

2. UsageTrackingQueryCachingPolicy中哪些查询允许缓存?

根据如下shouldCache函数的代码可以得到:

    • 不允许缓存的有:TermQuery,MatchAllDocsQuery,MatchNoDocsQuery,empty的复合查询
    • 耗时的查询需要最近(256次)执行过2次以上才可以缓存
    • 不耗时的查询需要最近执行过5次才可以查询,但是BooleanQuery和DisjunctionMaxQuery最近执行过4次就可以被缓存

4. 转化为shouldQuery

TermInSetQuery的本质是多个term查询,一个term查询很快,多个term查询很慢,是因为需要将需要将多个term查询的倒排链求或集。我们线上的查询结构包含3个部分: 一个term,一个range,一个terms,最终需要将3个结果进行合并求交集。

测试发现,term包含结果200个,range包含100个,terms包含1亿个,并且已知我们线上的terms查询中的单词过多,且命中率很高,整个查询过程需要先求出terms查询的命中DocId集合,而求出这单shard命中1亿的DocId集合,速度肯定很慢很慢。求出这个DocId结合后,再和其它两个查询条件term、range进行结果合并,因此速度会特别慢。

这时候我们的优化方向变成了去掉terms查询,避免terms查询内部提前构造docIdSet,根据terms的语义,和shouldQuery比较相似,因此我们直接转化成为shouldQuery,并且设置minShouldMatch=1。通过这种改造,语义上并没有发生变化,但是可以避免1亿的docIdSet提前构建。

优化后,可以利用到Lucene的DocIdSet合并优化,会优先从最小长度的DocIdSet集合开始遍历,因此最大也就遍历100次,而之前的写法在构造terms的DocIdSet时候,需要先合并内部的倒排链,需要遍历1亿次,性能显然很差,而优化后节省的循环次数降低了100万倍。而之前的写法,只有在缓存命中时候才会有和现在一样的效果,特别依赖缓存,造成查询结果十分不稳定。

当查询中包含terms查询,且shouldQuery没有被利用时候,都可以采用类似的优化策略。当然,我们也可以修改Lucene的合并DocIdSet的策略,将该优化加入到其中。

5. 提高集群缓存

根据官方社区的建议,可以将集群的缓存相关参数调大,提高缓存的命中率。除了cache相关的大小优化外,上文中缓存分析中提高的TermInSetQuery的缓存相关参数也可以在适当的场景进行调账,提高缓存的使用率。

总结

本文主要通过profile查看可能潜在的问题原因,然后分析源码,查找缓存失效原理并提高缓存利用率,以及合理利用现有的Lucene的DocIdSet合并的优化,最终达到了查询延时下降数十倍的效果。

通过上述的分析,我们可以总结TermsQuery查询变慢的原因如下:

  1. cache失效
  2. Terms查询内部合并倒排链太慢

给出的优化建议如下:

  1. 提高缓存利用率
    • 直接修改es层面的cache相关参数,如果有能力或者有特殊场景,还可以修改lucene层面的cache相关参数,包括LRU策略、TermsQuery的是否cache相关参数
    • 并发较大时候缓存会失效,对缓存依赖较高的场景可以通过
  1. 尽量去掉提前的无效倒排链合并
    • 通过改写shouldQuery是一个有效的做法。如果嵌套复杂,可能不能通过该手段进行改写。这时候需要了解一些查询结果集合进行合并的知识,自己在写查询语句的时候,尽量先产生较小的结果集。
  1. 合理进行shard扩容
    • 根据我们的经验,一般情况下一个shard维护5千万到2亿数据即可,过多或者过少都不是一个很好的选择。

参考

相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
目录
相关文章
|
6月前
|
自然语言处理 API 索引
Elasticsearch Analyzer原理分析并实现中文分词
Elasticsearch Analyzer原理分析并实现中文分词
106 0
|
6月前
|
搜索推荐 Java 数据处理
Elasticsearch搜索分析引擎本地部署与远程访问
Elasticsearch搜索分析引擎本地部署与远程访问
|
3天前
|
缓存 监控 安全
Elasticsearch扩展和优化
【11月更文挑战第4天】
17 6
|
1月前
|
存储 自然语言处理 Java
Elasticsearch写入优化
【10月更文挑战第3天】Elasticsearch:从写入原理谈写入优化
75 2
|
10天前
|
存储 SQL 监控
|
10天前
|
运维 监控 安全
|
2月前
|
存储 缓存 自然语言处理
深度解析ElasticSearch:构建高效搜索与分析的基石
【9月更文挑战第8天】在数据爆炸的时代,如何快速、准确地从海量数据中检索出有价值的信息成为了企业面临的重要挑战。ElasticSearch,作为一款基于Lucene的开源分布式搜索和分析引擎,凭借其强大的实时搜索、分析和扩展能力,成为了众多企业的首选。本文将深入解析ElasticSearch的核心原理、架构设计及优化实践,帮助读者全面理解这一强大的工具。
176 7
|
5月前
|
数据库 索引
Elasticsearch索引别名:管理与优化数据访问
Elasticsearch索引别名:管理与优化数据访问
|
4月前
|
运维 监控 Java
在大数据场景下,Elasticsearch作为分布式搜索与分析引擎,因其扩展性和易用性成为全文检索首选。
【7月更文挑战第1天】在大数据场景下,Elasticsearch作为分布式搜索与分析引擎,因其扩展性和易用性成为全文检索首选。本文讲解如何在Java中集成Elasticsearch,包括安装配置、使用RestHighLevelClient连接、创建索引和文档操作,以及全文检索查询。此外,还涉及高级查询、性能优化和故障排查,帮助开发者高效处理非结构化数据。
71 0
|
6月前
|
运维 索引
Elasticsearch 写入优化探索:是什么影响了refresh 耗时?
Elasticsearch 写入优化探索:是什么影响了refresh 耗时?
70 7