2011年3月网站Lucene调整优化手记
壹.起因
自网站重构以来,我们加入了ApacheLucene,用来辅助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的使用,原文为:ForperformancereasonsitisrecommendedtoopenonlyoneIndexSearcheranduseitforallofyoursearches.说白了,只有一个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.Indexindex参数,如不需要boost功能,尽量使用ANALYZED_NO_NORMS而不是ANALYZED,尽量使用NOT_ANALYZED_NO_NORMS而不是NOT_ANALYZED,修改之后,发现并不能有效地节省磁盘空间,但是会影响内存使用。
5.构造Field对象时,可以设置omitTermFreqAndPositions属性,来不保存词条的位置等信息,API原文说明为:Whilethisoptionreducesstoragespacerequiredintheindex,italsomeansanyqueryrequiringpositionalinformation,suchasPhraseQueryorSpanQuerysubclasseswillsilentlyfailtofindresults.修改之后,可以明显地节省10%左右的磁盘空间,但在随后测试发现,虽然程序里没有用到PhraseQuery和SpanQuery查询,但是查询条件也会受到影响而不能分词,初步推断和使用的IK分词器有关,所以这个属性,我们实践过后认为不要随意设置。
6.通过EclipseMemoryAnalyzer软件分析www的heap快照文件,发现Lucene中的FieldCache类比较多,在网上搜索得知,该缓存类和IndexSearcher类的数目有关,但在我们上面的解决方案中,我们的观点更倾向于,该类和IndexReader类的数目有关,此处有待以后验证,现在的系统,并没有像API说明提示的那样,把IndexSearcher也搞成单例,因为现在的内存状况很好,如以后再遇到扩展的性能问题,可以再回到这里,考虑和研究IndexSearcher单例的进一步做法。
7.再次强调,主要的优化方式和提示,优先查看第1,2条里的官方说明,里面包含了很有价值的信息,因此在本篇不再赘述。