2011年3月网站Lucene调整优化手记

壹.起因

    自网站重构以来,我们加入了Apache Lucene,用来辅助mysql数据库存储查询,以减少对DB的负担,网站的大部分数据共有的特点是不需要即时更新,数据量较大,这正是Lucene擅长解决的问题领域,起始版本是2.4,开始效果不赖,当然也遇到了一些问题,例如判断索引文件合理的大小值问题,分词器的选择问题,对于一个完整的存储查询解决方案来说是不言而喻的,Lucene的学习成本相对而言也较高,理论和内容都比较多,需要花时间和精力来研究。
    09年底,Lucene推出了3.0版,自从2.9版开始,内部结构发生了不小的变化,同时根据官方文档的提示,Lucene在自身性能上有了明显的提高,随后我们做出:升级到3.0的决定。
    2010年,网站各个部分的数据访问基本都是基于Lucene,各类索引的建立访问和管理,成为了新的问题,如何组织索引文件,如何更有效的访问,这类问题不断浮现,2010年底,网站的访问量有了新的提升,如何应对激增的访问,成为了首要的问题。
    2011年开始,访问量最大的www系统显现出了问题,在高访问量下,出现了OutOfMemoryError,当虚拟机可用内存不足2%,且不能正常回收时,会throw出这个Error,一个良好运行的系统,是不应出现这样明显的性能问题的,我们决定:必须要解决这个问题了。

贰.经过

    提到应对访问量,有经验的人员首先想到的是扩展,垂直扩展是我们首先尝试的,最直接的做法是:增大每个tomcat的JVM内存上限,提高tomcat的访问线程上限,研究apache和tomcat之间,如何更有效的转发请求等等,而随后的水平扩展,最直接的做法是,增加tomcat数量,更多的分散负载请求,这些我们都有尝试,效果却不明显,问题何在?最终我们把目光移到了Lucene本身,是不是我们使用它的方式有问题?
    随后展开的查询搜索研究过程暂且不表,我们最终发现了一处地方,很有可能会影响性能:在我们的索引数据查询方法里,都是通过类似这样的方式来获得IndexSearcher对象的

Directory directory = new SimpleFSDirectory(new File(indexDir));
IndexSearcher searcher = new IndexSearcher(directory);
//查询过程
directory.close();
searcher.close();
	

    这看上去好像没有什么问题,类似于JDBC获取Connection对象并最终在finally确保关闭一样,是很标准的做法,但当我们再深入一点,会发现IndexSearcher类还有另外一种构造方法

Directory directory = new SimpleFSDirectory(new File(indexDir));
IndexSearcher searcher = new IndexSearcher(IndexReader.open(directory));
//查询过程
directory.close();
searcher.close();


    这有什么不同?其实第一种构造方法,会在内部调用第二种构造方法,也就是说,IndexSearcher需要一个IndexReader,如果我们用的是第一种,则每次都要重建一个IndexReader对象,并在IndexSearch.close()后一起关闭,而第二种我们手动传入IndexReader对象的方式,则可以保留IndexReader而不关闭,这样就可以重用该对象,上面提到,我们的数据量大部分是不需要即时更新的,也就是说,对于一种类型的索引文件,其实我们只需要打开一个IndexReader对象就可以了。
    在Lucene官方的API说明,提到了IndexSearcher的使用,原文为:For performance reasons it is recommended to open only one IndexSearcher and use it for all of your searches. 说白了,只有一个IndexSearcher,自然也只会有一个IndexReader了,而IndexReader.open()方法的调用次数,正是影响性能的关键所在。
    我们最终选用了保守一点的方法,改变IndexSearcher的构造方式,将IndexReader单例化,并将改变应用到各个主要系统中。

叁.结果
   
    改变之后,性能发生了很大的变化,系统方面,例如原先35机器的cpu使用率在30%-70%之间,1,5,10分钟平均负载都在5.X左右,修改后cpu使用率基本在10%以下,1,5,10分钟平均负载则只有0.X左右,在虚拟机方面,效果也很明显,修改前GC大小回收的两个总时间基本相同,加在一起要占到系统运行时间的近1/6,修改后小GC次数降低了近20倍,而大GC则稳定的在2小时左右才运行一次,每个JVM内存最大上限为1.5G,实际只用到了600M左右,完全消除了OutOfMemoryError出错的可能。

肆.补充
  
    实际在这个过程里,我们也尝试研究了很多种方式,最后简单说明如下:
    1.http://wiki.apache.org/jakarta-lucene/ImproveIndexingSpeed里面说明了建索引时要注意的地方,要优先阅读。
    2.http://wiki.apache.org/jakarta-lucene/ImproveSearchingSpeed里面说明了查询索引时要注意的地方,更要优先阅读。
    3.建立索引时,IndexWriter.MAXBufferedDocs最好不要设置,它默认是关闭的,而IndexWriter.RAMBufferSize属性默认为16M,即内存Document对象达到了16M才会刷新至磁盘,推荐优先应用这个设置。
    4.构造Field对象时,需要传入Field.Index index参数,如不需要boost功能,尽量使用ANALYZED_NO_NORMS而不是ANALYZED,尽量使用NOT_ANALYZED_NO_NORMS而不是NOT_ANALYZED,修改之后,发现并不能有效地节省磁盘空间,但是会影响内存使用。
    5.构造Field对象时,可以设置omitTermFreqAndPositions属性,来不保存词条的位置等信息,API原文说明为:While this option reduces storage space required in the index, it also means any query requiring positional information, such as PhraseQuery or SpanQuery subclasses will silently fail to find results. 修改之后,可以明显地节省10%左右的磁盘空间,但在随后测试发现,虽然程序里没有用到PhraseQuery和SpanQuery查询,但是查询条件也会受到影响而不能分词,初步推断和使用的IK分词器有关,所以这个属性,我们实践过后认为不要随意设置。
    6.通过Eclipse Memory Analyzer软件分析www的heap快照文件,发现Lucene中的FieldCache类比较多,在网上搜索得知,该缓存类和IndexSearcher类的数目有关,但在我们上面的解决方案中,我们的观点更倾向于,该类和IndexReader类的数目有关,此处有待以后验证,现在的系统,并没有像API说明提示的那样,把IndexSearcher也搞成单例,因为现在的内存状况很好,如以后再遇到扩展的性能问题,可以再回到这里,考虑和研究IndexSearcher单例的进一步做法。
    7.再次强调,主要的优化方式和提示,优先查看第1,2条里的官方说明,里面包含了很有价值的信息,因此在本篇不再赘述。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章