当前位置: 首页 > solr, 搜索 > 正文

solr SolrIndexSearcher性能问题分析

关键字:
1 星2 星3 星4 星5 星 (2 次投票, 评分: 5.00, 总分: 5)
Loading ... Loading ...
baidu_share
文章目录

问题背景

基于solr的新搜索系统已经使用了有段时间,不过之前上的几个库都很小,也没发现什么性能问题。最近上了thread和post库后,性能问题显现出来。这段时间解决的性能问题有好几个,本文只着重于SolrIndexSearcher的search问题。thread库索引大小有400M多,记录数有400多万,原来的基于lucene的系统的查询处理耗时一般在10-30ms,而同样的请求,新系统耗时在50-100ms。而post库索引大小有6G多,记录数有3000多万,旧系统的查询处理时间一般在30-80ms,而新系统往往需要400-800ms,性能差距还是很明显的。

因为SolrIndexSearcher是基于lucene的IndexSearcher,想必应该是青出于蓝胜于蓝,并且其wiki上给出的性能数据也很不错,所以当我把新系统做完后并没有去细致的测试其性能。但现在系统的性能相比之下确实有些差,并且因为要做多个库的整合搜索,就要避免像thread这样耗时长的库就成为短板。总之,问题需要定位和解决。

原因分析

分析thread应用给搜索server发来的query,query除了包含用户输入的文本,还有两个filter query:1)是fq=fid:1,fid是int型,表示选择某个版块;2)是fq=atm:[int_time1 TO int_time2],atm是int型,表示帖子发表的时间,默认int_time2是请求时的时间,int_time1是比int_time2小6个月的起始时间。实际测试发现,当去掉两个fq,处理耗时很短(即便有sort返回结果数很多),而两个fq同时存在或则其中某一个存在时,耗时都会变长,尤其是匹配的结果总数较多时。

因为query分为filter query和text query,基于lucene的IndexSearcher实现方式可以有两种:1)是将各filter query和text query联合起来构成一个大的Query(就是个BooleanQuery,BooleanClause之间是MUST关系);2)是将filter query转成Filter,和text query区分开。旧系统使用的就是方式2。因为Filter是基于Query构造的,所以两种方式按说性能应该差不多。

翻看SolrIndexSearcher的代码,它除了继承IndexSearcher的一系列search方法,还增加了search(QueryResult qr, QueryCommand cmd),也是我使用的接口。我当然可以认为,solr的这个search方法会对查询过程做了更高效的实现。除去为提升性能(或者降低性能)引入的三种cache代码,以及一些细枝末节的分支代码,SolrIndexSearcher的search方法主要处理过程其实也简单:

1)将一或多个fq构成List<Query>,然后调用getDocSet(final List<Query> queries)得到DocSet,DocSet存放的是匹配的doc id列表,getDocSet方法内部就是遍历queries,对每一个query执行getPositiveDocSet(final Query q)得到该query的DocSet,然后将得到的多个DocSet做合并,得到满足所有fq的DocSet。

2)由DocSet的Filter getTopFilter()方法得到Filter,再联合TopFieldCollector或者TopScoreDocCollector,以及由text query构造的Qeury,调用lucene的void search(Query query, Filter filter, Collector results),再结合一些扫尾工作,完成一次查询过程。

单看这个过程,也很难发现问题在哪。为了跟踪整个查询的执行路径,我索性将SolrIndexSearcher从solr源码中提取出来,这其间也是费了一些周折,因为SolrIndexSearcher使用了一些包级别的代码并且我需要构造的SolrIndexSearcher2不能直接从SolrQueryRequest获得。通过加入一些代码段的耗时统计,发现上述1)耗时占70%以上,再分析发现,问题出在查询fq的DocSet getDocSetNC(final Query query, final DocSet filter)方法中使用的DocSetCollector。DocSetCollector的代码如下:

class DocSetCollector extends Collector {
  int pos=0;
  OpenBitSet bits;
  final int maxDoc;
  final int smallSetSize;
  int base;
  // in case there aren't that many hits, we may not want a very sparse
  // bit array.  Optimistically collect the first few docs in an array
  // in case there are only a few.
  final int[] scratch;
  DocSetCollector(int smallSetSize, int maxDoc) {
    this.smallSetSize = smallSetSize;
    this.maxDoc = maxDoc;
    this.scratch = new int[smallSetSize];
  }
  public void collect(int doc) throws IOException {
    doc += base;
    // optimistically collect the first docs in an array
    // in case the total number will be small enough to represent
    // as a small set like SortedIntDocSet instead...
    // Storing in this array will be quicker to convert
    // than scanning through a potentially huge bit vector.
    // FUTURE: when search methods all start returning docs in order, maybe
    // we could have a ListDocSet() and use the collected array directly.
    if (pos &lt; scratch.length) {
      scratch[pos]=doc;
    } else {
      // this conditional could be removed if BitSet was preallocated, but that
      // would take up more memory, and add more GC time...
      if (bits==null) bits = new OpenBitSet(maxDoc);
      bits.fastSet(doc);
    }
    pos++;
  }
 
  public DocSet getDocSet() {
    if (pos&lt;=scratch.length) {
      // assumes docs were collected in sorted order!
      return new SortedIntDocSet(scratch, pos);
    } else {
      // set the bits for ids that were collected in the array
      for (int i=0; i

可以看到,DocSetCollector使用两个变量来保存collect的doc id,当collect的doc数小于smallSetSize时使用数组scratch,否则使用OpenBitSet bits。getDocSetNC中构造DocSetCollector的代码是:DocSetCollector collector = new DocSetCollector(maxDoc()>>6, maxDoc());。问题就出在OpenBitSet,以thread用例来说,满足fq=fid:1的doc数有200万之多(总doc数有400万),而fq=atm:[int_time1 TO int_time2]有时也有百万之多,这使得DocSetCollector在collect时使用OpenBitSet.fastSet(int index)置位。尽管OpenBitSet要比BitSet高效,但我在本机测试发现,OpenBitSet200万次的fastSet需要50ms,所以不难理解,匹配fq的文档数越多,DocSetCollector就越慢,进而SolrIndexSearcher就越慢。不过,另一方面,当多个fq的DocSet做合并后,实际有效的DocSet大小可能很小,而再和text query做合并后,得到的DocSet就会更小。所以,当索引的文档多时,solr的这种处理效率上就低得多。

我粗略浏览了lucene的相关代码,lucence在通过各种Scorer操作匹配Query的结果时没有使用OpenBitSet,而是主要使用队列、堆等集合来操作匹配结果的收集、合并等操作(各种Scorer的具体实现没有细看,有时间再看吧),而Collector.collect的会是最后真正匹配的结果。从实际测试的效果来看,性能要比solr提升数倍。最后,我也就放弃了search(QueryResult qr, QueryCommand cmd),而是使用lucene的,虽然不能使用上solr的三种cache,但性能还是令人满意的。

本文固定链接: http://www.chepoo.com/solr-index-searcher-performance-analysis.html | IT技术精华网

solr SolrIndexSearcher性能问题分析:等您坐沙发呢!

发表评论