Elasticsearch 5.x 源码分析(12)对类似枚举数据的搜索异常慢的一种猜测

字数 2018阅读 1153

2017-1-9 更新:
https://elasticsearch.cn/article/446
why this happened? 携程的兄弟给出了答案,看来结论是一致的就是number在构造scorer时,实际上是构造一个巨大的bitset并在上面生成一个迭代器。而keyword 在做链表时有跳表查询显然找docid要快好多。
根源就在于Lucene 6.0 对于存储number类型是用k-d tree 造成的。

看来携程的高手看Lucene还是看的很深,需要努力学习!

最后贴出作者给出的结论,如果一定要用number建索引而又不用range操作的,赶紧升级到5.4 吧:

小结:
在ES5.x里,一定要注意数值类型是否需要做范围查询,看似数值,但其实都是做Term或者Terms匹配的,应该定义为keyword字段。
如果RangeQuery的结果集很大,并且还需要和其他更加selective的查询条件做AND的,应该升级到ES5.4+,该版本在底层引入的indexOrDocValuesQuery,可以极大提升该场景下RangeQuery的查询速度。


最近因为一个索引的数据量日益暴增,达到8,9亿,因此相应的慢查询也开始显现,针对其中几种查询更是慢的离谱。
查看了一下这个慢查询的搜索条件其实也很简单,而且,简单到离谱,其中有这么两个条件查询,这两个字段的mapping都是short 类型,在没有这两个条件的查询,延迟是可以接受的,仍然能保持200ms级以内,而 is_deleted 则去到数百毫秒,is_warmup则是暴增到1~3s 不等。


这就有点奇怪了,首先docs 数一样,这两个typemapping 都是short,而且值都是只有0,1,2这样数种枚举值,那为什么这种条件的查询这么慢呢?而且还不一样?
当时自己这个问题看了大半天是无果, 做了一些假设性的猜测也就作罢, 今天好基友“聂风”同学找上门,说这几天也遇到同类的问题,也是百思不得其解,突然吊起胃口,又回过头来思考这个疑难杂症,想一看究竟。

网上有好一些有共同病患的,可惜还没有官方回答,先mark下来说不定我写完我的猜测官方就给解答了 -_-

Elastic对类似枚举数据的搜索性能优化

Why my search slow?


线索一:这两个term其实和Query的order无关

我们首先肯定会觉得这两个term的查询肯定是结果集太大了,所以并没有基于其他更小范围的filter之后做,而是并行来做,并且因为结果集很大,所以捞的时间非常大;是的,我首先就是这个想法,后来当我打开了profile:true看到了结果,表示我的猜测错了!
下面是我打开了profile 的分析结果:



从上面的分析得出,其实耗时并不是因为它并行走了索引去捞了超大量的数据导致,也不是消耗在打分上,更不是消耗在什么合并运算上,偏偏是消耗在一个build_scorer 的过程里。

这里的build_scorer 究竟是什么咚咚呢?
忘了说,我的这个慢查询都是塞在 filter里的,不是不打分的么,没看到Result的score都是0? 那和score 有半毛钱关系?
那就先挖出这个scorer的解释先:

https://www.elastic.co/guide/en/elasticsearch/reference/master/_profiling_queries.html

build_scorer

This parameter shows how long it takes to build a Scorer for the query. A Scorer is the mechanism that iterates over matching documents generates a score per-document (e.g. how well does "foo" match the document?). Note, this records the time required to generate the Scorer object, not actually score the documents. Some queries have faster or slower initialization of the Scorer, depending on optimizations, complexity, etc. This may also showing timing associated with caching, if enabled and/or applicable for the query

留意一下高亮那两句,就是说耗时是耗在了构建 一个叫Scorer Object 的东西上,并且这个东西的耗时程度取决于优化的程度(这里我理解就是建完index之后有没有做一些index的优化,比如force merge, blablabla 。。。)
好了,这个Scorer Object 一看就是Lucene的东西,所以现在暂时先不深入,再挖一下其他有用的东西。


线索二: number 字段的term操作其实都是转换成range查询

不知道上面的截图大家留意到一个细节没有,[0-0] [1-1] ,对,其实这是一个range查询



这个线索只是让我觉得有点点意外,其实本身并无太大的价值,不过也是一个思路吧,就是以后什么样的值采用number来保存。换句话说,就是当对number的操作很慢的时候,首先得想如何提高range的性能。
关于这个课题我最后再讲。


线索三:没有线索了,老老实实看代码吧!

首先看看这个时间是怎么来的:


ProfileWeight.java

从上面的说明结合得出,时间的跨度就是来生成这个scorer上,Weight 类型是一个基类,有多个实现,由于我还未曾系统的去读完整个Lucene的源码,所以这里我也只能猜了,大家都知道,Lucene 把所有的查询会转换成一颗格式化的包含各种类型查询的树,而这个Weight树我理解就是用来做合并和打分用的。
那由于这个是一个term查询,那我们就去看看TermWeightscorer方法


从代码上看,也就是说,当做某个field的检索的时候,其实Lucene会初始化一个这个field 的termEnum 这种东西,并且会调用postings 获取PostingsEnum
那这个PostingsEnum 又是什么鬼,我大概了解就是一种结果docs 的迭代器的抽象,已被到时在Lucene的结果树里可以快速做合并,运算之用

那么再对比一下,PostingsEnum的 普通term的倒排和number的倒排的实现类应该是完全不同的逻辑实现的




好,那我猜测构造慢因该就是做这个postings 慢了,因为它有各自的实现,如果涉及到docs 的指针的迭代,那么大家都知道,Lucene的倒排指向的docs 映射是用BitSet去实现的,那么到现在为止就可以猜测,要初始化一个类似LongBitSet这种东西,跟我这个number的本身的稀疏程度是否有关?


我的一种猜测

至此,我的一个猜测就是如果number类型的条件结果集很大的时候,要构造一个某种BitSet的scorer 其实是挺费劲的(像官方文档说说,还和其他很多因素相关)但是如何解释同样是number,为什么 is_warmup 要比is_deleted还要再慢几倍呢?我猜测是不同docs的这个值的稀疏程度有关,当然要回答这个还要继续深入Lucene的代码,我这里就不再继续drill down了

结论

好了,其实要解决这个问题很简单,如果是枚举类型的数字的话,mapping 用keyword 就好了,其实回顾官方文档的时候,官方文档也清楚列明了的,如果你是不做range操作的可枚举出的数字的话,比如map 的keyset,最好还是用keyword。


后话

恰巧看到一篇blog有提到官方其实一直在努力加快 number的range搜索,具体大家可以读读, 貌似这个优化增加到 ES5.4 以后的版本了

https://www.elastic.co/blog/better-query-planning-for-range-queries-in-elasticsearch

接下来看有机会debug一下Lucene的部分代码,希望大家提出自己的看法,各抒己见,就拍哪里错了误导了大家;好了,洗洗睡了!

参考文献:
https://www.compose.com/articles/how-scoring-works-in-elasticsearch/
https://qbox.io/blog/optimizing-search-results-in-elasticsearch-with-scoring-and-boosting
https://www.elastic.co/guide/cn/elasticsearch/guide/current/scoring-theory.html
https://toutiao.io/posts/4mwfeo/preview
http://www.scienjus.com/elasticsearch-function-score-query/

推荐阅读更多精彩内容