Elasticsearch性能优化之valueCountAgg的8~20倍性能提升

2020-05-22 00:00:00 字符串 类型 性能 转化为 开销

Elasticsearch是一款的开源搜索引擎,其除了可以完成复杂的query请求外,还可以做一些统计聚合的任务,类似sql中的max、sum、count、avg等。事情的缘起在于某一次对Elasticsearch的性能测试中发现,countAgg的性能会比sum等agg性能要低的多,甚至不在一个数量级。

先说结论,在计算double、long等数值类型时候,提高8倍~9左右性能;在计算geo_point类型时候,提高18~20倍性能。优化后的代码已经贡献给alibaba内部团队共建的Elasticsearch分支,并已经通过开源途径贡献给Elasticsearch开源社区,提给ES官方的PR已经被接受,后续将随新版本发布。

测试中发现问题

测试在2亿数据上进行,使用了一个double字段进行测试,如下图所示,avg、sum等字段在进行统计时候,性能十分接近,但是value_count的性能一直表现不佳,甚至不在一个数量级上。基于一些常识性的知识,我们认为count和sum等都是类似的操作,消耗的时间应该都是一样的,因此,我们对value_count的agg进行了探讨。

200Million docs, 1shard,0replica的数据上进行测试如下。

基础理论知识

这里主要介绍lucene和java的String,这两部分决定了我们后续可以进行大幅优化。

Lucene

在lucene中,Collector系列接口可以完成查询结果收集、排序、自定义结果集过滤和收集。Collector和LeafCollector是Lucene结果集收集的核心。Elasticsearch中的统计聚合直接使用了lucene的接口,仅仅是自己实现了Collector接口。核心方法还是lucene中的search接口。

LeafCollector

org.apache.lucene.search.LeafCollector,它有collect()与setScorer()两个方法。

void org.apache.lucene.search.LeafCollector.collect(int doc)

这是一个及其重要的方法。这个docid是segment内的docid,全局的docid可以由LeafReaderContext.docBase(segment文件的编号)+doc得到,这个全局id在Elasticsearch中也大量使用。这里collect方法用来收集每个索引文档,提供的doc参数表示段文件编号,如果你要获取索引文档的编号,请加上当前segment文件Reader的docBase基数,如leafReaderContext.reader().docBase + doc。

void org.apache.lucene.search.LeafCollector.setScorer(Scorer scorer)

设置打分器,如果你的search结果需要打分,可以在这里设置一个打分器,如果不需要打分可以忽略这一步。

TopDocsCollector

org.apache.lucene.search.TopDocsCollector,返回top-N的文档。

TopScoreDocCollector

这是常用的一个结果收集器,默认情况下会根据评分和docId进行排序,因此这个收集器不用显示的指定。

TopFieldCollector

它和上面收集器的区别在于它可以由用户指定按照某一个字段排序然后返回,完成我们常见的Sort函数。

TimeLimitingCollector

它是一个包装器,可以将其他Collector进行wrap,被wrap的Collector必须要指定的时间内返回,超过时间则中断收集过程。

Java String

java开发者都知道jvm中存在着一个叫字符串常量池的东西,字符串被创建出后,即便String类不再使用,也不会被马上回收,因此如果大量的字符串被频繁的创建,而内存又不够使用,那么就会触发字符串常量池的gc,这里的gc是量特别大的字符串的gc,耗时也是十分可观的,例如在es中,随便执行一个上亿规模的查询,如果将所有的字符串放入内存中,内存很容易不够用。

其次,java中还存在这样一个问题,类型转换在强类型转化为泛型时候,开销是几乎可以忽略的,而强制类型转化则会花费很大的开销。如下代码,当Long转String时候,开销会比以往更多,其内部使用了O(n)的时间来拼凑这个字符串,因此long转String在数据量特别大的时候,也会特别的慢。

// java long转string源码,getChars的耗时很高。
public static String toString(long i) {
        int size = stringSize(i);
        if (COMPACT_STRINGS) {
            byte[] buf = new byte[size];
            getChars(i, size, buf);
            return new String(buf, LATIN1);
        } else {
            byte[] buf = new byte[size * 2];
            StringUTF16.getChars(i, size, buf);
            return new String(buf, UTF16);
        }
}

相关文章