系统优化遵从木桶原理:一只木桶能盛多少水,并不取决于最高的木板,而取决于最短的那块木板。优化Lucene也同样,找到性能瓶颈,找对解决方法,才能事半功倍,本文将从三方面阐述我们的Lucene优化经验:
1. 找准方向 -> Lucene性能瓶颈分析。
2. 找对方法 -> Lucene代码架构分析。
3. 方法落地 -> 优化经验总结。
1. Lucene性能瓶颈分析
上篇Lucene底层原理分析了Lucene索引结构:内存+磁盘,打开索引库时只有tip和fdx文件会被加载到内存中,tip为FST的前缀索引,fdx为正向文件索引,其他文件tim、doc、fdt都放在硬盘,一次完整的检索过程与索引文件的交互过程如图:
整个流程至少发生三次随机IO:
1. 读后缀词块
2. 读倒排表
3. 取文档(如果文档号跳跃性很大或者因为打分完全乱序,那么会发生更多次随机IO,极端情况就是取多少文档就发生多少次随机IO)
当前机械硬盘随机IO响应时间平均在10ms左右,远大于CPU+内存计算时间,而且这只是针对一个查询条件,若多个查询条件、跨多列、甚至模糊查询,随机IO请求更多,因此Lucene查询性能瓶颈主要集中磁盘IO性能上,尤其随机IO性能。所以我们的优化方向就是:
1. 减少IO请求。
2. 顺序IO代替随机IO。
2. Lucene代码架构
上一节分析了Lucene性能瓶颈,这一节分析Lucene代码架构,找到从哪里下手去实现优化。
Lucene从4.0版本后,代码全面模块化,并开放了很多接口,包括索引格式接口Codec、打分接口Similarity、文档收集接口Collector,开发者想基于Lucene再开发,不再需要侵入式修改源代码,而是基于接口,插件式修改。我们结合业务场景和开放接口自定义了Lucene检索模式。
Lucene检索大致时序图:
1. APP解析用户查询生成查询条件Query。
2. IndexSearcher重写Query并生成Weight。
3. Weight会生成Scorer,Scorer创建相应查询条件的倒排表迭代器。
4. 调用scoreALl(),遍历所有文档ID,依次传给传给Collector。
5. Collector得到文档ID后,调用打分模块Similarity得到文档分值,并根据分值和文档收集器具体实现决定是否返回。Lucene默认的收集器TopScoreDocCollector,会根据用户定义的文档数如100,返回分值前100的文档ID。
我们对Lucene的修改主要在图中标红的文档收集过程,一是屏蔽打分,二是修改文档收集模式,下一节会详细阐述。
3. 优化经验总结
基于底层原理和代码架构,我们知道了需要做什么和怎么做:IO、IO、还是IO,以下我们全文检索系统的主要优化方案:
3.1单盘优化
解决问题:
硬盘随机IO性能低。
解决方案:
1. 将原先的Raid5拆分,改用单盘,因为Raid5随机读写性能 < n*单盘。
2. 将索引文件tim、doc使用固态硬盘SSD存放,正向文件fdt使用机械硬盘,这样综合了SSD随机读写性能高,机械硬盘成本低、存储空间大的优点。
3. 对同一磁盘上索引库进行统一管理,单线程处理对同一硬盘上索引库的检索请求,防止同一硬盘多库之间同时访问降低磁盘性能。这里可以根据实际测试情况调整具体线程数,但线程数不宜过多。
3.2布隆过滤器
解决问题:
有些单词不在索引库里,但还需要进索引库查询,发起不必要的IO请求。
解决方案:
使用布隆过滤器,预先判断单词是不是在该索引库里。布隆过滤器原理很简单,对一单词哈希,并映射到相应bit,设置为1,判断时同样做哈希,并去相应bit位取值,若为1,则可能存在,进库查询,若为0,则肯定不存在,不需进库查询。
对Lucene实现布隆过滤器有两种方式:
1. 在应用层,Lucene之外实现。
2. 改写Lucene的Codec接口,添加布隆过滤器功能,使用布隆过滤器预先过滤查询条件。
后来我们经过测试,选用了第一种方案,因为布隆过滤器十分消耗内存、加载时间很长,而且我们同一索引库为提