关于java:记录一次ElasticSearch的查询性能优化

3次阅读

共计 4050 个字符,预计需要花费 11 分钟才能阅读完成。

问题: 慢查问

搜寻平台的公共集群,因为业务泛滥,对业务的 es 查问语法短少束缚,导致问题频发。业务可能写了一个微小的查问间接把集群打挂掉,然而咱们平台人力投入无限,也不可能一条条去审核业务的 es 查问语法,只能通过后置的伎俩去保障整个集群的稳定性,通过 slowlog 剖析等,下图中 cpu 曾经 100% 了。

昨天刚好手头有一点点工夫,就想着能不能针对这些状况,把影响最坏的业务抓进去,进行一些改善,于是昨天花了 2 小时剖析了一下,找到了一些共性的问题,能够通过平台来很好的改善这些状况。

首先通过 slowlog 抓到一些耗时比拟长的查问,例如上面这个索引的查问耗时根本都在 300ms 以上:

{
  "from": 0,
  "size": 200,
  "timeout": "60s",
  "query": {
    "bool": {
      "must": \[
        {
          "match": {
            "source": {
              "query": "5",
              "operator": "OR",
              "prefix\_length": 0,
              "fuzzy\_transpositions": true,
              "lenient": false,
              "zero\_terms\_query": "NONE",
              "auto\_generate\_synonyms\_phrase\_query": "false",
              "boost": 1
            }
          }
        },
        {
          "terms": {
            "type": \[
              "21"
            \],
            "boost": 1
          }
        },
        {
          "match": {
            "creator": {
              "query": "0d754a8af3104e978c95eb955f6331be",
              "operator": "OR",
              "prefix\_length": 0,
              "fuzzy\_transpositions": "true",
              "lenient": false,
              "zero\_terms\_query": "NONE",
              "auto\_generate\_synonyms\_phrase\_query": "false",
              "boost": 1
            }
          }
        },
        {
          "terms": {
            "status": \[
              "0",
              "3"
            \],
            "boost": 1
          }
        },
        {
          "match": {
            "isDeleted": {
              "query": "0",
              "operator": "OR",
              "prefix\_length": 0,
              "fuzzy\_transpositions": "true",
              "lenient": false,
              "zero\_terms\_query": "NONE",
              "auto\_generate\_synonyms\_phrase\_query": "false",
              "boost": 1
            }
          }
        }
      \],
      "adjust\_pure\_negative": true,
      "boost": 1
    }
  },
  "\_source": {
    "includes": \[\],
    "excludes": \[\]
  }
}

这个查问比较简单,翻译一下就是:

SELECT guid FROM xxx WHERE source=5 AND type=21 AND creator='0d754a8af3104e978c95eb955f6331be' AND status in (0,3) AND isDeleted=0;

慢查问剖析

这个查问问题还挺多的,不过不是明天的重点。比方这外面不好的一点是还用了含糊查问 fuzzy_transpositions, 也就是查问 ab 的时候,ba 也会被命中,其中的语法不是明天的重点,能够自行查问,我预计这个是业务用了 SDK 主动生成的,外面很多都是默认值。

第一反馈是当然是用 filter 来代替 match 查问,一来 filter 能够缓存,另外防止这种无意义的含糊匹配查问,然而这个优化是无限的,并不是明天解说的关键点,先疏忽。

错用的数据类型

咱们通过 kibana 的 profile 来进行剖析,耗时到底在什么中央?es 有一点就是开源社区很沉闷,文档齐全,配套的工具也十分的不便和齐全。

能够看到大部分的工夫都花在了 PointRangQuery 外面去了,这个是什么查问呢?为什么这么耗时呢?这里就波及到一个 es 的知识点,那就是对于 integer 这种数字类型的解决。在 es2.x 的时代,所有的数字都是按 keyword 解决的,每个数字都会建一个倒排索引,这样查问尽管快了,然而一旦做范畴查问的时候。比方 type>1 and type<5 就须要转成 type in (1,2,3,4,5) 来进行,大大的减少了范畴查问的难度和耗时。

之后 es 做了一个优化,在 integer 的时候设计了一种相似于 b -tree 的数据结构,减速范畴的查问,具体能够参考 (https://elasticsearch.cn/arti…)

所以在这之后,所有的 integer 查问都会被转成范畴查问,这就导致了下面看到的 isDeleted 的查问的解释。那么为什么范畴查问在咱们这个场景下,就这么慢呢?能不能优化。

明明咱们这个场景是不须要走范畴查问的,因为如果走倒排索引查问就是 O(1) 的工夫复杂度,将大大晋升查问效率。因为业务在创立索引的时候,isDeleted 这种字段建成了 Integer 类型,导致最初走了范畴查问,那么只须要咱们将 isDeleted 类型改成 keyword 走 term 查问,就能用上倒排索引了。

实际上这里还波及到了 es 的一个查问优化。相似于 isDeleted 这种字段,毫无区分度的倒排索引的时候,在查问的时候,es 是怎么优化的呢?

多个 Term 查问的程序问题

实际上,如果有多个 term 查问并列的时候,他的执行程序,既不是你查问的时候,写进去的程序。

例如下面这个查问,他既不是先执行 source= 5 再执行 type=21 依照你代码的程序执行过滤,也不是同时并发执行所有的过滤条件,而后再取交加。es 很聪慧,他会评估每个 filter 的条件的区分度,把高区分度的 filter 先执行,以此能够减速前面的 filter 循环速度。比方 creator=0d754a8af3104e978c95eb955f6331be 查出来之后 10 条记录,他就会优先执行这一条。

怎么做到的呢?其实也很简略,term 建的时候,每一个 term 在写入的时候都会记录一个词频,也就是这个 term 在全副文档里呈现的次数,这样咱们就能判断以后的这个 term 他的区分度高下了。

为什么 PointRangeQuery 在这个场景下十分慢

下面提到了这种查问的数据结构相似于 b -tree, 他在做范畴查问的时候,十分有劣势,Lucene 将这颗 B -tree 的非叶子结点局部放在内存里,而叶子结点紧紧相邻寄存在磁盘上。当作 range 查问的时候,内存里的 B -tree 能够帮忙疾速定位到满足查问条件的叶子结点块在磁盘上的地位,之后对叶子结点块的读取简直都是程序的。

总结就是这种构造适宜范畴查问,且磁盘的读取是程序读取的。然而在咱们这种场景之下,term 查问可就麻烦了,数值型字段的 TermQuery 被转换为了 PointRangeQuery。这个 Query 利用 Block k-d tree 进行范畴查找速度十分快,然而满足查问条件的 docid 汇合在磁盘上并非向 Postlings list 那样依照 docid 程序寄存,也就无奈实现 postings list 上借助跳表做蛙跳的操作。

要实现对 docid 汇合的疾速 advance 操作,只能将 docid 汇合拿进去,做一些再解决。这个处理过程在 org.apache.lucene.search.PointRangeQuery#createWeight 这个办法里能够读取到。这里就不贴简短的代码了,次要逻辑就是在创立 scorer 对象的时候,顺带先将满足查问条件的 docid 都选出来,而后结构成一个代表 docid 汇合的 bitset,这个过程和结构 Query cache 的过程十分相似。之后 advance 操作,就是在这个 bitset 上实现的。所有的耗时都在构建 bitset 上,因而能够看到耗时次要在 build_scorer 上了。

验证

找到起因之后,就能够开始验证了。将原来的 integer 类型全副改成 keyword 类型,如果业务真的有用到范畴查问,应该会报错。通过搜寻平台的平台间接批改配置,批改实现之后,重建索引就失效了。

索引切换之后的成果也十分的显著,通过 kibana 的 profile 剖析能够看到,之前须要靠近 100ms 的 PointRangQuery 当初走倒排索引,只须要 0.5ms 的工夫。

之前这个索引的均匀 latency 在 100ms+,这个是 es 分片解决的耗时, 从搜寻行为开始,到搜寻行为完结的打点,不蕴含网络传输工夫和连贯建设工夫,单纯的分片内的函数的解决工夫的平均值,失常状况在 10ms 左右。

通过调整之后的耗时降到了 10ms 内。

通过监控查看慢查问的数量,立刻缩小到了 0。

将来

后续将通过搜寻平台侧的能力来保障业务的查问,所有的 integer 咱们会默认你记录的是状态值,不须要进行范畴查问,默认将会批改为 keyword 类型,如果业务的确须要范畴查问,则能够通过后盾再批改回 integer 类型,这样能够保障在业务不理解 es 机制的状况下,也能领有较好的性能,节俭机器计算资源。

目前还遇到了很多问题须要优化。例如重建索引的时候,机器负载太高。公共集群的机器负载散布不平衡的问题,业务的查问和流量不可控等各种各样的问题,要节俭机器资源就肯定会面对这种各种各样的问题,除非土豪式做法,每个业务都领有本人的机器资源,这外面有很多很多颇具技术挑战的事件。

实际上,在这一块还是十分利于积攒教训,对于 es 的理解和成长也十分快,在查问题的过程中,对于搜索引擎的应用和理解会成长的十分快。不仅如此,很多时候,咱们用心的看到生产的问题,继续的跟踪,肯定会有所播种。大家遇到生产问题的时候,务必不要放过任何细节,这个就是你播种的时候,比你写 100 行的 CRUD 更有益处。

(本文作者:任天兵)

本文系哈啰技术团队出品,未经许可,不得进行商业性转载或者应用。非商业目标转载或应用本文内容,敬请注明“内容转载自哈啰技术团队”。

正文完
 0