一、背景
我们在使用Elasticsearch进行查询的过程中发现,如果查询时间跨度大,查询数据集比较庞大,即使只是返回少量的结果,查询耗时仍然比较长。我们通过分析profile和debug跟踪整个查询流程,确认耗时的原因,针对业务特性,提出了相关的优化方案,可以对该类查询提升三到五倍的性能。
二、流程分析
1、Elasticsearch的查询流程
Elasticsearch使用Lucene作为内部引擎。Elasticsearch的查询流程分为两个阶段。第一个阶段为QueryPhase,收到请求的协调节点将查询优化拆分成多个Shard级别的请求发送到对应的数据节点上,数据节点收到请求后调用Lucene进行查询,查询完成后把结果返回给协调节点。第二个阶段为FetchPhase,协调节点将查询结果进行汇总,得到一个文档的id的集合,然后据此依次给数据节点发送查询具体的_source,docvalue等数据信息,最终把丰富好的结果返回给用户。
2、Lucene的查询流程
Lucene的查询发生在Elasticsearch的QueryPhase阶段中数据节点内部。我们知道Lucene中,ES index 底层是分Segment块存储的。当一条带有多个条件的查询进入Lucene后,Lucene会先做一次裁剪,然后对涉及到的segments遍历进行查询。查询流程可以简单分为两个阶段。第一个阶段为评估(Approximation),是对每条子语句单独进行权重计算和匹配,计算出每条子语句的结果集id具体的偏移位置和有效范围。第二个阶段为遍历(Iteration),在这个阶段会选出结果集最少的子语句的结果集作为遍历的Leader,在遍历的过程中,从中筛选符合其他查询条件的数据,得到最终的结果集。
3、查询流程中的四级缓存
Elasticsearch的查询过程中总共有四层缓存,第一层缓存是Elasticsearch的RequestCache,缓存的是整个查询的Shard级别的查询结果,如果数据节点收到重复的查询语句的请求,那么这级缓存就能利用上。第二层缓存是Lucene的LRUQueryCache,缓存的是单条子查询语句的查询结果,如果有类似的查询进来,部分子查询重复,那么LRUQueryCache便能够发挥作用。第三层缓存是程序内存中的MMAP文件映射缓存,Lucene通过DirectByteBuffer将整个文件或者文件的一部分映射到堆外内存,由操作系统负责获取页面请求和写入文件,Lucene就只需要读取内存数据,这样可以实现非常快速的IO操作。第四层缓存是文件系统缓存,是由系统控制的。
理解以上两个流程图后,我们对两个流程图做一个汇总,并标明缓存发生的位置。
三、问题分析
我们已经了解了Elasticsearch以及Lucene的查询流程以及其中的缓存过程,那么耗时究竟在哪呢?接下来我们会分析其中存在的问题。
1、QueryPhase阶段生成LRUQueryCache耗时
Lucene会判断每条子语句是否值得做缓存,如果值得做缓存,便会进入到缓存分支,生成LRUQueryCache,那么下一次带有该子查询语句的查询便可以大大缩短查询时间,所以这次缓存并不需要对size限制,因为每次查询都可能会从缓存中得到。而这恰恰成为了查询耗时长的罪魁祸首。通常情况下,Lucene的缓存策略会对时间(索引有时间字段)的范围查询进行缓存,而如果没有其他维度限制,单单长时间范围的缓存查询会过滤出大量数据。假设我们查询一个月的前200条数据,而Lucene总共会对整一个月的结果id进行缓存。这可能是几亿条,几十亿条,这个耗时可能是真实的查询耗时的几百倍。
LRUQueryCache本身存在的意义是非常重要的,它能将大量重复的批量短查询缓存起来,对于降低整个系统的压力,减少重复的小io操作具有非常重要的作用。但通常情况下,长时间跨度的大查询是由用户手动发起的,并不会频繁或者反复进行,所以LRUQueryCache带来的后续查询体验提升微乎其微,而第一次低效的查询体验已经能让用户崩溃。所以对于长时间跨度的QueryCache对于我们而言没有太大的意义。这块耗时经测试占了该类慢查询总耗时的90%,其中真实的查询耗时与其相比是很小的,所以我们考虑尝试去掉对慢查询的LRUQueryCache。
2、QueryPhase阶段Segment查询耗时
由Lucene的查询流程我们知道,Lucene对涉及到的segments进行遍历。segments越多,查询耗时就越长。segments内部存储的数据越无序,越不利于缓存,查询耗时也越长。所以需要合理的配置Segment的Merge策略,减少过多的Segment存在,但Merge对集群和磁盘io的压力比较大,这点需要考虑。Elasticsearch6.3的新特性中index-sorting支持对数据根据配置的字段进行排序,经测试对查询性能有很大提升。
四、解决方案
1、QueryPhase阶段生成LRUQueryCache 缓存策略优化
为了避免缓存占用过多的查询耗时,我们在Lucene查询的评估阶段,先计算所有可能成为Leading Query 的查询子句的结果集数量 ,并将最小的结果传递给接下来的缓存分支。我们根据我们的业务场景,针对性地设置了一些阈值,确保缓存分支的耗时超出查询耗时一个数量级的时候,就不做单条查询子句的缓存。
2、利用index-sorting优化查询
Index-sorting新特性能够在数据写入时,将数据按照指定的字段的值进行排序。如果查询中包含指定的字段,那查询只需要读取相邻的文件块。我们根据业务的查询场景,对结果集数量比较多的字段进行排序。
五、优化结果
1、QueryPhase阶段生成LRUQueryCache优化结果
我们考虑尝试去掉对慢查询的LRUQueryCache,图1是去掉之前的监控,查询毛刺平均耗时在50ms左右,图2是优化后的监控,查询毛刺平均在18ms左右,带来三倍左右的整体查询耗时的优化。
2、QueryPhase阶段IndexSorting优化结果
IndexSorting对于小查询的优化不明显,我们尝试通过构造大查询来反馈,对于未排序和排序的数据都模拟查询7天的数据,未排序的数据以上查询平均耗时为2s,排序的数据查询平均耗时为400ms,查询性能可提升5倍。