摘要: 前言 Elasticsearch是一个很火的分布式搜索系统,提供了很是强大并且易用的查询和分析能力,包括全文索引、模糊查询、多条件组合查询、地理位置查询等等,并且具备必定的分析聚合能力。由于其查询场景很是丰富,因此若是泛泛的分析其查询性能是一个很是复杂的事情,并且除了场景以外,还有不少影响因素,包括机型、参数配置、集群规模等等。数据库
Elasticsearch是一个很火的分布式搜索系统,提供了很是强大并且易用的查询和分析能力,包括全文索引、模糊查询、多条件组合查询、地理位置查询等等,并且具备必定的分析聚合能力。由于其查询场景很是丰富,因此若是泛泛的分析其查询性能是一个很是复杂的事情,并且除了场景以外,还有不少影响因素,包括机型、参数配置、集群规模等等。本文主要是针对几种主要的查询场景,从查询原理的角度分析这个场景下的查询开销,并给出一个大概的性能数字,供你们参考。数组
本节主要是一些Lucene的背景知识,了解这些知识的同窗能够略过。服务器
Elasticsearch的底层是Lucene,能够说Lucene的查询性能就决定了Elasticsearch的查询性能。关于Lucene的查询原理你们能够参考如下这篇文章:数据结构
Lucene查询原理分布式
Lucene中最重要的就是它的几种数据结构,这决定了数据是如何被检索的,本文再简单描述一下几种数据结构:性能
了解了Lucene的数据结构和基本查询原理,咱们知道:测试
如今的问题是,若是给一个组合查询条件,Lucene怎么对各个单条件的结果进行组合,获得最终结果。简化的问题就是如何求两个集合的交集和并集。优化
上面Lucene原理分析的文章中讲过,N个倒排链求交集,能够采用skipList,有效的跳过无效的doc。阿里云
处理方式一:仍然保留多个有序列表,多个有序列表的队首构成一个优先队列(最小堆),这样后续能够对整个并集进行iterator(堆顶的队首出堆,队列里下一个docID入堆),也能够经过skipList的方式向后跳跃(各个子列表分别经过skipList跳)。这种方式适合倒排链数量比较少(N比较小)的场景。spa
处理方式二:倒排链若是比较多(N比较大),采用方式一就不够划算,这时候能够直接把结果合并成一个有序的docID数组。
处理方式三:方式二中,直接保存原始的docID,若是docID很是多,很消耗内存,因此当doc数量超过必定值时(32位docID在BitSet中只须要一个bit,BitSet的大小取决于segments里的doc总数,因此能够根据doc总数和当前doc数估算是否BitSet更加划算),会采用构造BitSet的方式,很是节约内存,并且BitSet能够很是高效的取交/并集。
经过BKD-Tree查找到的docID是无序的,因此要么先转成有序的docID数组,或者构造BitSet,而后再与其余结果合并。
若是采用多个条件进行查询,那么先查询代价比较小的,再从小结果集上进行迭代,会更优一些。Lucene中作了不少这方面的优化,在查询前会先估算每一个查询的代价,再决定查询顺序。
默认状况下,Lucene会按照Score排序,即算分后的分数值,若是指定了其余的Sort字段,就会按照指定的字段排序。那么,排序会很是影响性能吗?首先,排序并不会对全部命中的doc进行排序,而是构造一个堆,保证前(Offset+Size)个数的doc是有序的,因此排序的性能取决于(Size+Offset)和命中的文档数,另外就是读取docValues的开销。由于(Size+Offset)并不会太大,并且docValues的读取性能很高,因此排序并不会很是的影响性能。
上一节讲了一些查询相关的理论知识,那么本节就是理论结合实践,经过具体的一些测试数字来分析一下各个场景的性能。测试采用单机单Shard、64核机器、SSD磁盘,主要分析各个场景的计算开销,不考虑操做系统Cache的影响,测试结果仅供参考。
这个请求耗费了233ms,而且返回了符合条件的数据总数:5184867条。
对于Tag1="a"这个查询条件,咱们知道是查询Tag1="a"的倒排链,这个倒排链的长度是5184867,是很是长的,主要时间就花在扫描这个倒排链上。其实对这个例子来讲,扫描倒排链带来的收益就是拿到了符合条件的记录总数,由于条件中设置了constant_score,因此不须要算分,随便返回一条符合条件的记录便可。对于要算分的场景,Lucene会根据词条在doc中出现的频率来计算分值,并取分值排序返回。
目前咱们获得一个结论,233ms时间至少能够扫描500万的倒排链,另外考虑到单个请求是单线程执行的,能够粗略估算,一个CPU核在一秒内扫描倒排链内doc的速度是千万级的。
咱们再换一个小一点的倒排链,长度为1万,总共耗时3ms。
{"took":3,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":10478,"max_score":1.0,"hits":...}
首先考虑两个Term查询求交集:
这个请求耗时21ms,主要是作两个倒排链的求交操做,所以咱们主要分析skipList的性能。
这个例子中,倒排链长度是1万、500万,合并后仍有5000多个doc符合条件。对于1万的倒排链,基本上不进行skip,由于一半的doc都是符合条件的,对于500万的倒排链,平均每次skip1000个doc。由于倒排链在存储时最小的单位是BLOCK,一个BLOCK通常是128个docID,BLOCK内不会进行skip操做。因此即便可以skip到某个BLOCK,BLOCK内的docID仍是要顺序扫描的。因此这个例子中,实际扫描的docID数粗略估计也有几十万,因此总时间花费了20多ms也符合预期。
对于Term查询求并集呢,将上面的bool查询的must改为should,查询结果为:
{"took":393,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5190079,"max_score":1.0,"hits":...}
花费时间393ms,因此求并集的时间是多于其中单个条件查询的时间。
这个查询咱们主要分析FST的查询性能,从上面的结果中咱们能够看到,FST的查询性能相比扫描倒排链要差许多,一样扫描500万的数据,倒排链扫描只须要不到300ms,而FST上的扫描花费了3秒,基本上是慢十倍的。对于UUID长度的字符串来讲,FST范围扫描的性能大概是每秒百万级。
这个例子中,查询消耗时间的大头仍是在扫描FST的部分,经过FST扫描出符合条件的Term,而后读取每一个Term对应的docID列表,构造一个BitSet,再与两个TermQuery的倒排链求交集。
这个场景咱们主要测试BKD-Tree的性能,能够看到BKD-Tree查询的性能仍是不错的,查找500万个doc花费了500多ms,只比扫描倒排链差一倍,相比FST的性能有了很大的提高。地理位置相关的查询也是经过BKD-Tree实现的,性能很高。
这个结果出乎咱们的意料,居然只须要27ms!由于在上一个例子中,数字Range查询耗时500多ms,而咱们增长两个Term条件后,时间居然变为27ms,这是为什么呢?
实际上,Lucene在这里作了一个优化,底层有一个查询叫作IndexOrDocValuesQuery,会自动判断是查询Index(BKD-Tree)仍是DocValues。在这个例子中,查询顺序是先对两个TermQuery求交集,获得5000多个docID,而后读取这个5000多个docID对应的docValues,从中筛选符合数字Range条件的数据。由于只须要读5000多个doc的docValues,因此花费时间不多。
最后结尾再放一个彩蛋,既然扫描数据越多,性能越差,那么可否获取到足够数据就提早终止呢,下一篇文章我会介绍一种这方面的技术,能够极大的提升不少场景下的查询性能。
阿里云双十一1折拼团活动:已满6人,都是最低折扣了
【满6人】1核2G云服务器99.5元一年298.5元三年 2核4G云服务器545元一年 1227元三年
【满6人】1核1G MySQL数据库 119.5元一年
【满6人】3000条国内短信包 60元每6月
参团地址:http://click.aliyun.com/m/1000020293/