问题:慢查询搜索平台公有集群,由于业务量大,缺乏对业务的es查询语法的约束,导致问题频发。业务可能会写一个巨大的query直接杀掉集群,但是我们平台人力投入有限,不可能一一复习业务的esquery语法。我们只能通过安装后的方式来保证整个集群的稳定性。通过slowlog分析等,下图中的CPU已经是100%了。昨天刚好有一点时间,就想着能不能针对这些情况,把受影响最严重的业务抓出来,做一些改进,所以昨天花了2个小时分析了一下,发现了一些共性问题,可以通过平台发现可以改善这些情况。首先,使用slowlog来捕获一些耗时的查询。比如下面这个索引的查询时间基本都在300ms以上:{"from":0,"size":200,"timeout":"60s","query":{"bool":{"must":\[{“匹配”:{“来源”:{“查询”:“5”,“运算符”:“OR”,“前缀\_length”:0,“模糊\_transpositions”:真,“宽松”:false,"zero\_terms\_query":"NONE","auto\_generate\_synonyms\_phrase\_query":"false","boost":1}}},{"terms":{"type":\[“21”\],“提升”:1}},{“匹配”:{“创造or":{"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":\[\]}}这个查询比较简单,翻译过来就是:SELECTguidFROMxxxWHEREsource=5ANDtype=21ANDcreator='0d754a8af3104e978c95eb955f6331be'ANDstatusin(0,3)ANDisDeleted=0;慢查询分析查询问题挺多的,不过不是today比如这里不好的一点就是同时使用了模糊查询fuzzy_transpositions,即查询ab的时候也会命中ba,里面的语法不是今天的重点,大家可以自己查询。猜测这是因为业务使用SDK自动生成,很多都是默认值,第一反应当然是用filter而不是matchquery,首先filter可以缓存,另外避免这种无意义的模糊匹配查询,但是这种优化是有限的,不是今天先忽略讲解的重点。我们通过kibana的profile来分析被滥用的数据类型。耗时在哪里?es有一点就是开源社区很活跃,文档很齐全,配套的工具也很方便很齐全。可以看出大部分时间都花在了PointRangQuery上。这是什么类型的查询?为什么这么耗时?这就涉及到es的一个知识点,即整数类型的处理。es2.x时代,所有的数字都是按照关键字来处理的,每个数字都会建立一个倒排索引,这样查询虽然快,但是一旦进行范围查询。比如type>1和type<5需要转换成typein(1,2,3,4,5),这样就大大增加了范围查询的难度和耗时。后来es做了优化,针对整型设计了类似b-tree的数据结构来加速范围查询。具体可以参考(https://elasticsearch.cn/arti...)所以之后所有的整型查询都会转化为范围查询,从而导致上面看到的isDeleted查询的解释。那么为什么范围查询在我们的场景中这么慢呢?能不能优化一下。显然,这种场景下我们不需要使用范围查询,因为如果使用倒排索引查询,时间复??杂度为O(1),会大大提高查询效率。业务创建索引时,将isDeleted字段内置为Integer类型,最后导致范围查询。那么我们只需要将isDeleted类型改为关键字,使用term查询就可以使用倒排索引了。其实这里也涉及到es的一个查询优化。当isDeleted这样的字段没有区分倒排索引的时候,es在查询的时候是怎么优化的呢?多个term查询的顺序其实如果有多个term查询并行的话,它们的执行顺序并不是你查询的时候写的顺序。比如上面的查询,他既没有按照你代码的顺序先执行source=5再执行type=21进行过滤,也没有同时并发执行所有的过滤条件,然后取交集。es很聪明,他会评估每个过滤条件的判别率,先执行高判别率的过滤,从而加快后续的过滤循环速度。比如creator=0d754a8af3104e978c95eb955f6331be找出10条记录后,就先执行这条。你是怎么做到的?其实也很简单。一个term在创建的时候,每个term在写的时候都会记录一个词频,也就是这个term在所有文档中出现的次数,这样我们就可以判断当前term的区分程度。.为什么PointRangeQuery在这种情况下非常慢?如上所述,该查询的数据结构类似于B树。它在进行范围查询时具有优势。Lucene将这个B-tree的非叶子节点放在内存中,而叶子节点则在磁盘上并排存储。当用作范围查询时,内存中的B树可以帮助快速定位到满足查询条件的叶子节点块在磁盘上的位置,后续对叶子节点块的读取几乎都是顺序的。总结就是这种结构适合范围查询,读盘是顺序的。但是在我们的场景中,term查询比较麻烦,将数值字段的TermQuery转为PointRangeQuery。这个Query使用Blockk-dtree进行范围搜索非常快,但是满足查询条件的docid集合并没有像Postlingslist那样按照docids的顺序存储在磁盘上,所以无法使用跳表进行对贴子列表进行越级操作。为了实现对docid集合的快速推进操作,我们只能把docid集合取出来,做一些再处理。这个过程可以在org.apache.lucene.search.PointRangeQuery#createWeight方法中读取。冗长的代码这里就不贴了。主要逻辑是在创建scorer对象时,先选择所有满足查询条件的docid,然后构造一个代表docid集合的bitset。这个过程与构造查询缓存的过程非常相似。相似的。之后,对该bitset进行高级操作。所有的时间都花在了构建bitset上,可见时间主要花在了build_scorer上。验证找到原因后,就可以开始验证了。将原来的整数类型全部改为关键字类型。如果业务真的对范围查询有用,应该报错。直接通过搜索平台的platform修改配置。修改完成后,索引重建才会生效。索引切换后的效果也很明显。根据kibana的profile分析可以看出,以前接近100ms的PointRangQuery,现在倒排索引只需要0.5ms。之前这个索引的平均延迟是100ms+。这是es分片处理的耗时,从搜索行为开始到搜索行为结束,不包括网络传输时间和连接建立时间,纯分片中函数处理时间的平均值正常情况下约为10ms。调整后,耗时降低到10ms以内。通过监控慢查询的数量,立即降为0。未来我们将借助搜索平台的能力来保障业务查询。对于所有的整数,我们会默认你记录状态值,不需要进行范围查询。默认情况下,它将更改为关键字类型。如果业务确实需要范围查询,那么可以通过后台改回整型,即使业务不了解es机制,也能保证更好的性能,节省机器计算资源。还有很多问题需要优化。比如重建索引时,机器负载过高。公共集群机器负载分布不均衡,业务查询和流量不可控等问题。如果要节省机器资源,肯定会面临这样各种各样的问题。除非土豪靠近,每个业务都有自己的机器资源,里面有很多很多技术上有挑战性的东西。其实这方面还是非常有利于积累经验的,对es的理解和成长也很快。在查题的过程中,对搜索引擎的使用和理解会增长非常快。不仅如此,很多时候,我们用心看到生产问题,不断跟进,一定会有收获。当您遇到生产问题时,一定不要错过任何一个细节。这就是你收获的时候,比写100行CRUD更有益。(本文作者:任天兵)本文由哈尔滨技术团队制作。未经许可,不得转载或商用。如非商业目的转载或使用本文内容,请注明“内容转载自你好技术团队”。
