转自:https://blog.csdn.net/njpjsoftdev/article/details/54133548

系统优化遵从木桶原理:一只木桶能盛多少水,并不取决于最高的木板,而取决于最短的那块木板。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接口,添加布隆过滤器功能,使用布隆过滤器预先过滤查询条件。
  后来我们经过测试,选用了第一种方案,因为布隆过滤器十分消耗内存、加载时间很长,而且我们同一索引库为提高性能,复制到多个硬盘上,所以如果布隆过滤器放在Lucene里,相同过滤器会被加载多次,会浪费相当多的内存,所以我们在Lucene之外做了布隆过滤器,同一索引库共享一个布隆过滤器,节约了内存。
  

3.3屏蔽打分/排序机制

解决问题:
  一次测试发现,同样的条件,精确查询速度还没有模糊查询速度快
这里写图片描述
  研究源代码发现,Lucene会对分词列的精确查询条件进行打分。打分是搜索引擎重要一部分,倒排索引只能回答是不是的问题,打分能够评判查询条件和文档的匹配度,提高检索质量。Lucene打分过程集成了多种经典模型,如TF-IDF、VSM,如图:
  这里写图片描述
  1. coord 一个document满足几个查询,满足多的分值高。
  2. queryNorm,查询归一化,它的意义是让同一文档但不同查询的打分结果有可比较。
  3. tf-idf,tf是term在文档中出现次数,idf逆文档频率是term在多少个文档中出现过除以总文档数。
  4. getBoost,查询时赋的权重。
  5. 归一化,主要三个因素文档权重、field权重、文档长度,这个很重要,因为这个需要单独加载nvm文件,而且在打开库时不会加载,而是在第一次查询时会加载,因此才会造成查询时间的巨大差异。
  这里不详细阐述,只说下它的几个基本原则:
  1. 一个文档符合的查询条件越多分越高。
  2. 一个文档关键词出现次数越多分越高,文档内容越多分越低。
  3. 一个查询词在越多文档中出现权重越低。
  有兴趣的可查看LuceneAPI文档TFIDFSimilarity类说明:
  http://lucene.apache.org/core/4_10_3/core/index.html
  打分会消耗额外IO、需要更多CPU计算、加载整个倒排表,拖累了查询速度,特别实在文档数非常多的情况下。而对模糊查询,Lucene不会进行打分,所以反而更快。在我们的业务场景下,我们不需要TF-IDF这种打分方式,所以我们完全屏蔽了打分这个过程,大大提高了检索速度。
解决方案:
  1. 实现EmptySimilarity,去掉所有计算过程,打分过程完全为空。

public class EmptySimilarity extends Similarity {

private static long ZERO=0;
@Override
public long computeNorm(FieldInvertState state) {return ZERO;
}@Override
public SimWeight computeWeight(float queryBoost,CollectionStatistics collectionStats, TermStatistics... termStats) {return new EmptySimWeight();
}@Override
public SimScorer simScorer(SimWeight weight, AtomicReaderContext context)throws IOException {return new EmptyScorer();
}public class EmptySimWeight extends SimWeight {@Overridepublic float getValueForNormalization() {return ZERO;}@Overridepublic void normalize(float queryNorm, float topLevelBoost) {}}public static class EmptyScorer extends SimScorer {@Overridepublic float score(int doc, float freq) {return ZERO;}@Overridepublic float computeSlopFactor(int distance) {return ZERO;}@Overridepublic float computePayloadFactor(int doc, int start, int end,BytesRef payload) {return ZERO;}}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
  2. 自定义Collector,结果数满足了抛异常退出,防止读入多余倒排表。

public class SimpleCollector extends Collector implements Iterable {

private final List<Integer> hitList;
private final int numHits;
private int docBase;public SimpleCollector(int numHits) {if(numHits<0)throw new IllegalArgumentException("numHits should > 0");this.numHits = numHits;this.hitList = new ArrayList<Integer>(numHits);
}@Override
public void collect(int doc) throws IOException {if(hitList.size()<numHits){hitList.add(docBase+doc);}else{//若结果满了抛异常退出throw new HitListFullException();}
}
public int size(){return hitList.size();
}
@Override
public void setScorer(Scorer scorer) throws IOException {//ignore scorer
}@Override
public void setNextReader(AtomicReaderContext context) throws IOException {//因为是分段的,所以需要记载每个段起始文档号this.docBase=context.docBase;
}@Override
public boolean acceptsDocsOutOfOrder() {//接受乱序,提高性能,因为最后要自己排序return true;
}@Override
public Iterator<Integer> iterator() {Collections.sort(hitList);return hitList.iterator();
}public static class HitListFullException extends RuntimeException{public HitListFullException(){super("HitList already full");}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
  使用如下:

IndexSearcher indexSearcher = new IndexSearcher(DirectoryReader.open(FSDirectory.open(new File("/index/lucene_test"))));//使用空打分器indexSearcher.setSimilarity(new EmptySimilarity());SimpleCollector simpleCollector=new SimpleCollector(2);try {indexSearcher.search(query, simpleCollector);} catch (HitListFullException e) {//e.printStackTrace();// ignore}System.out.println(simpleCollector.size());//遍历文档号for(int hit:simpleCollector){indexSearcher.doc(hit);}indexSearcher.getIndexReader().close();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
3.4 取结果优化

解决问题:
  上面的测试条件还有一个问题,就是他们取同样数量的文档数,时间却差了很多。
这里写图片描述

  原因就是因为模糊查询不打分,所以文档ID是顺序的,为顺序IO读方式,而打分后文档ID完全乱序,为随机IO读方式。
解决方案:
  1. 自定义Collector,按文档ID升序排序且结果数满足立即退出。
  2. 多任务合并取结果操作,这样相同ID的文档只会取一次。
  

3.5解决Query被转成Filter

解决问题:
  我们有一个组合条件:

select * from indexdb where Time > 20170104 AND Time < 20170105 AND Protocol = ‘TCP’ AND Content =’not exist’
1
  这里需要合并多个查询条件的倒排表,Lucene在合并倒排表时,并不会一次性读出所有倒排表,而是将倒排表抽象成迭代器,延迟获取,而且如果有一个AND条件查询结果为空,它就直接返回,不会读任一倒排表。这里Content查询结果为空,但这个查询还是很久才返回,debug跟踪Lucene源代码发现,Lucene会对Query查询重写来优化性能,这里的Time条件因为匹配到词数太多,而被Lucene改写成Filter,Filter一个特点就是会读出符合查询条件的所有倒排表,并做成BitSet,所以查询时间都消耗在了读倒排表上。
解决方案:
  1. 去掉了CapTime条件,改由应用层去做,按时间预先分库。
  2. 调整子查询顺序,将匹配结果更少的放前面。
  3. 留心Lucene的重写机制,有时候重写过的查询条件不一定符合我们预期。
  

3.6索引库大小性能比较

解决问题:
  Lucene一个索引库多大合适?
解决方案:
  这里涉及到Lucene索引结构设计:Lucene是分段的。分段是指Lucene接收到索引请求后,会先放缓存,缓存满后才会写到磁盘中去,变成一个Segment,Segment创建好了之后就不会再修改,每个Segment相当于一个功能完整的小索引库,它包含之前说的所有索引文件。当然这样会导致索引库中有很多段,所以Lucene后台会有合并线程定期去合并小的段。
  段数越少,检索时随机IO次数请求就越少,段结果合并操作越少。如果只有一个段,那么一个查询条件就需要加载一个后缀词块,但有10个段,就需要分别加载10个段的后缀词块和倒排表,再合并10个段的查询结果。分库本质上跟分段是一样的,调整库大小,减少库数量,就是减少段数来提高性能。
  库大小测试结果:
  总共575G的索引库,我们分为6个100g的库和71个10g的库来分别测试
  打开库测试

库类型 打开时间(s) 库内存占用(g)
大库 11 1.3
小库 18 2.2
  查询测试

库类型 查询条件 查询时间(ms)
大库 content=’trump’ 1100
小库 content=’trump’ 5700
  可以看出大库相比小库不管再打开时间、内存占用、查询效率上都有着很大优势,所以在条件允许下,尽量把库调大。但也需注意两个问题:
  1. 合并大库是有成本的。
  2. 库越大,分发成本越高,容错率越低。

总结

 以上就是我们对Lucene的一些优化经验,回顾起来就是三点:
  1. 认清业务需求。
  2. 分析底层原理,找出性能瓶颈。
  3. 研究代码架构,找到优化切入点。
 这也是我们对其他开源项目的使用方法,知其然更只知其所以然。

Lucene底层原理和优化经验分享(2)-Lucene优化经验总结相关推荐

  1. Lucene底层原理和优化经验分享(1)-Lucene简介和索引原理

    基于Lucene检索引擎我们开发了自己的全文检索系统,承担起后台PB级.万亿条数据记录的检索工作,这里向大家分享下Lucene底层原理研究和一些优化经验. 从两个方面介绍: 1. Lucene简介和索 ...

  2. 计算机一级经验分享,计算机一级考试经验

    计算机一级分为两个部分考试:一是理论题,二是上机题.下面是计算机一级考试经验分享,希望对大家有帮助. 理论题,顾名思义,也就是考书本知识,都是选择题,一共二十道.但是考试范围就有几百道题,不过也不用担 ...

  3. 计算机共享原理,synchronize底层原理 游戏电脑问题解决分享!

    sync 1 package com.paddx.test.concurrent; 2 3 public class SynchronizedDemo { 4 public void method() ...

  4. 5分钟了解搜索引擎Lucene的原理

    场景 假设现在有10W+份word文档,让你做个web页面,给出关键词能快速搜索结果,你会怎么做?那至少有3种方案, 顺序扫描,每次检测文档中是否包含关键词,包含则加入结果列表,不包含继续查找下一个, ...

  5. RabbitMQ实战经验分享

    RabbitMQ实战经验分享 原文:RabbitMQ实战经验分享 前言 最近在忙一个高考项目,看着系统顺利完成了这次高考,终于可以松口气了.看到那些即将参加高考的学生,也想起当年高三的自己. 下面分享 ...

  6. 西安交通大学城市学院计算机二级,西安交通大学城市学院计算机系举行考研经验分享交流会...

    为进一步助力计算机系考研工作,协助学生明确考研目标.掌握考研学习规划,计算机系于4月20日晚在F100举行考研经验分享交流会.本次经验分享交流会特邀到西安交通大学数学学院张改英教授.外国语学院赵亚军教 ...

  7. 计算机专业基础综合408备考经验分享

    计算机专业基础综合408备考经验分享 写这些个经验贴,其实倒是很耗费时间的,不过想来想去,或许真能有点用,也就写了. 序言 关于计算机专业基础综合408这个科目,有些同学不了解,这里做个科普 该科目考 ...

  8. 视频教程-网管转行linux运维并做到运维经理经验分享-Linux

    网管转行linux运维并做到运维经理经验分享 Linux运维经理,高级运维经理,长期从事商城等知名企业的千万级.亿万级PV门户网站维护工作. 专注于Linux服务器架构运维近8年,擅长系统高并发.超多 ...

  9. 【21考研】合肥工业大学计算机经验分享集锦

    o( ̄▽ ̄)ブ 21考研结束了,22考研开始了,学姐收到了很多21上岸的学弟学妹们的经验分享,把这份成功传递给22的你们啦~ 经验分析集锦 经验分享1--初试390+ 经验分享2--跨考 经验分享3- ...

最新文章

  1. 详解Python中的循环的几个类型
  2. python下载安装教程3.8.0-Python3.8下载
  3. 微信公众号开发之网页授权认证获取用户的详细信息,实现自动登陆
  4. Java Socket实战之三:传输对象
  5. 9 QM配置-检验计划配置-维护检验类型
  6. centos php mcrypt_Centos 建议使用epel源
  7. Sentinel一键下载安装运行_分布式系统集群限流_线程数隔离_削峰填谷_流量控制_速率控制_服务熔断_服务降级---微服务升级_SpringCloud Alibaba工作笔记0030
  8. [转载] python不允许使用关键字_Python中关键字global与nonlocal的区别
  9. 线性代数矩阵思维导图_斌叔2021考研数学公式:几种特殊形式的矩阵
  10. vue中的组件重定向
  11. SharePoint 2010 BDC Model项目部署出错:“The default web application could not be determined.”...
  12. 备份数据 宝塔linux_华为云服务器安装宝塔Linux面板及宝塔面板数据库备份导入体验...
  13. python单循环_「单循环赛」单循环赛制 - seo实验室
  14. 【新知实验室】腾讯云TRTC初体验
  15. AutoCAD VBA二次开发地形图多边形裁剪
  16. python构建决策引擎_决策引擎与机器学习模型的集成 | 信数这么干(一)
  17. 计算机操作系统学习笔记 第一章、操作系统概论
  18. openCV绘制简单Sierpinski(分形)图形
  19. 展示类页面测试Excel基础
  20. 【解决】fatal error: X11/XXXX.h: No such file or directory

热门文章

  1. mysql小数类型字段_mysql小数类型字段,float,double
  2. 黑马高级班day10
  3. Bowers Wilkins(宝华韦健)发布全新真无线蓝牙耳机
  4. 立创梁山派GD32F450ZGT6--0.96IIC屏移植u8g2
  5. 网络统考计算机一共多少题,计算机应用基础统考题库 2016年9月网络统考试题1答案...
  6. Linux字符设备重命名,【收藏】Linux常用命令全称及讲解(五)
  7. 关于一篇icsp的论文
  8. centos文件或文件夹有锁问题的解决
  9. 【计算机图形学】绘制图形
  10. 某中大型游戏公司面试题