这是我2016年写的文章,发出来,做索引优化的朋友都可以参考下。
名词解释
Lucene:高性能的全文检索开源的Java工具包。
Sorl :高性能的利用java开发的、开源的,基于Lucene的搜索服务器。
非结构化数据:非结构化的没有固定格式的数据,比如一篇文章等。
索引: 利用分词语言处理等手段得到的词与文档ID对应关系的数据结构。
全文搜索:利用索引对非结构化数据进行搜索的方法。
文档: 类似于数据库的一条记录,由多个字段组成,是建立索引的基础,索引中的分词是对文档中特定分词字段或不分词字段组成。
一.背景介绍
近段时间一直在研究solr和Lucene相关东西,主要是由于工作需要,需要利用Solr来进行日志搜索,但是Solr的建索引效率不是很高,难以满足工作中特定日志的性能要求,需要优化其建索引的性能。
性能的具体要求是:
1、在一台机器上普通硬盘的情况下,索引单个文档大小为200字节左右,需要达到的效率为5WTPS。
2、具有良好的水平扩展性。
3、对数据备份和数据丢失情况,要求不严格。
对于以上的性能要求,设计出合理的架构,并建议验证实践。
本文没有多少高深的技术,我也是个solr和Lucene的初学者,只是一个总结,希望起到抛砖引玉的作用。
二.关于Solr建索引优化
2.1 基本说明
Solr是基于Lucene开发的,外层封装成Servlet形式,运行在Java的Web容器之中,提供索引分片、多副本等功能,并提供基于HTTP的API,方便调用。
Solr的部署有单节点、Master-Salve模式、SolrCloud方式。
SolrCloud具有良好的水平扩展性、自动容错性、自动负载均衡等特性,所以这次采用solrColud的方式进行索引的优化。
SolrCloud中完整的索引为Collection,这是一个逻辑概念,一个Collection由多个shard组成,shard同样为逻辑概念,一个shard下面分为多个副本,每份基本相同,
其中一份被选举为Leader,在建索引的时候通过Leader来新建索引。
用图表示如下:
注:
1、红色字体为Leader,只是个示意,实际上Leader是通过选举算法选取出来的。
2、Collection的数据由shard1和shard2组成,每个shard的数据有两份,
Collection_shard1_core1和Collection_shard1_core2,不同的版本名称可能有差异,对应是一个文件夹,里面保存的是索引的数据。
2.2 建索引的过程
为优化SolrColud的创建索引的效率,有必要对整个建索引的流程有所了解,建索引的流程如下,
下面描述的建索引过程主要是最常见的通过SolrJ方式创建索引,不是采用DIH方式创建索引。
solr建索引过程
简单说明:
1、客户端发送HTTP的POST请求到Solr服务器,报文格式一般有xml、json、javabin(只有java才支持,二进制结构)。
2、Web服务器将请求派发到Solr的Web应用程序(Servlet)。
3、Solr根据请求的URI中的Collection名字在solrConfig.xml找到注册的/update消息处理器;这是单个副本的情况下,如果多个副本的情况下,如果需要判断此副本是否为Leader,
如果是非Leader,则需要将此文档发送给此副本的Leader,如果是非直接路由模式,Solr则会根据文档ID进行hash路由,路由到特定的Leader上。
4、按照solrConfig.xml配置的请求处理链来处理索引,比如分词处理器等。
5、写事务日志,当发送提交后正式将数据写入到存储(初始写入是内存,最后通过硬提交写入磁盘)中。
6、返回写索引的结果。
2.3 优化部分
如果从整体考虑,可以考虑JVM调优,通过优化Web服务器性能,通过程序和配置等其他方面来建索引。
目前通过JVM简单的调优和Web服务器优化,没有怎么研究,简单的试了下,性能改进不是很大。
本文主要考虑程序和其他方面配置方面优化。
2.3.1 报文批量大小优化
首先考虑到我们的建索引是通过HTTP请求发送的,所以在网络速度固定的情况下,可以通过优化报文大小来提高性能。
目前SolrJ的接口可以通过XMl、Javabin的方式来建索引,Javabin方式只有Java程序可以用,是一种特殊格式,在默认情况下使用Javabin方式的报文长度会比XML或JSON的格式要短,经过测试有部分性能。
对于HTTP请求,我们可以通过控制批次大小,一次发送的合适报文批量大小来进行优化,通过实践,目前200个字节左右,发送批量为1W左右性能较高。
2.3.2 路由模式优化
我们注意到在Solr建索引的时候,判断文档属于哪个shard,需要通过文档ID经过hash算法,每个文档都要判断一次,这对于性能来说,是不利的,
这种网络上也有朋友提倡通过直接路由的模式来进行建索引。需要注意的是:
- 索引要特殊方式通过以下URL新建:
http://ip:port/solr/admin/collections?action=CREATE&name=implicit1&shards=shard1,shard2,shard3&router.name=implicit
- 在solr4.x版本中通过更改schemal.xml在5.x更改managed-schema文件添加以下字段定义:
<field name="_route_" type="string"/>
- 在solrJ添加文档的时候需要加入以下字段:
doc.addField("_route_","shard_x")
后面的shard_x要替换为具体的shard,比如shard1或shard2等。通过直接路由模式,多线程的方式,在solr6.0版本的情况下,报文200个字节左右情况下,单节点的处理效率可以达到2.5WTPS,比solr4.9的单节点1W左右TPS有较大的提升。
另外在测试中发现,报文大小在200个字节到1k字节之间对TPS的影响比较大,当然吞吐量在报文更大的时候会有更好效果。
注意:
1、通过这种方式建索引一定要采用多线程同时发送到多个shard中去,在SolrJ中采用HttpSolrServer(在5.x以后好像已经更改为HttpSolrClient) 方式进行发送。
2、如果采用默认的路由模式采用的是CloudSolrServer进行发送,CloudSolrServer内部有负载均衡。
3、采用直接路由的模式,如果其中一个shard所归属的节点挂掉的话,可能造成数据的丢失。CloudSolrServer可以通过zookeeper上的clusterstate.json 信息得到具体的shard的leader的具体url,直接将updateRequest提交给它,不用经过replica转发;另外还可以通过随时监听节点的状态变化,来保证可靠性,所以在性能准许的情况下还是不要采用直接路由的模式。
2.3.3 参数调整优化
solrconfig.xml参数优化调整:
<ramBufferSizeMB>100</ramBufferSizeMB>
<maxBufferedDocs>1000</maxBufferedDocs>
以上两个参数为建索引的默认值,表示达到这个值的时候,才会将索引刷新到磁盘中去,可以适当调整测试。
更改建索引线程数:
<maxIndexingThreads>16</maxIndexingThreads>
这个为建索引的线程数,目前在4.x版本中存在,6.0版本未发现此配置,建议设置为2*cpuNum,默认为8个。
更改合并段参数参数:
<mergeFactor>10</mergeFactor>
索引的存储是按照段为单位的,这个参数决定了多少个段的时候,进行段合并。如果设置的大,则建索引的速度快,但是会导致索引文件多,查询速度慢;
相反,如果设置小,则合并段的频率高,加快查询速度,降低了建立索引的速度。
提交方式的优化:
<autoCommit>
<maxTime>${solr.autoCommit.maxTime:150000}</maxTime>
<maxDocs>50000</maxDocs>
<openSearcher>true</openSearcher>
</autoCommit>
自动提交是指在有文档添加的情况下,经过特定的时间间隔或者添加了特定文档数后进行索引的自动提交,设置了自动提交可以在SolrJ的代码中不同提交,由于有更新日志的存在,即使在solr服务器出现问题的时候,仍然可以在重启solr服务器的时候自动恢复索引数据。
另外参数openSearcher标示提交完成后是否重新打开搜索器,打开的会让索引可见。
注意:自动提交和打开搜索器都是很消耗时间的操作,设置自动提交的文档数量过大的情况下,可能造成建索引的速率产生较大的浮动;
自动调整在多并发的时候可以防止打开多个搜索器的问题。
<autoSoftCommit>
<maxTime>${solr.autoSoftCommit.maxTime:60000}</maxTime>
</autoSoftCommit>
自动软提交,软提交不将索引刷新到存储,所以速率相对比较快,软提交可以让索引立刻可见,故此在要求索引近实时的情况下,可以设置软提交。
注意如果在自动提交的情况下打开了搜索器,而且延迟时间可以接受的情况下,可以不是用自动软提交。
三. 结合Lucene创建索引
3.1 基本思路
从底层来说,Solr利用的Lucene来进行创建索引的,所以在高版本的Solr中,只要是相同版本的Solr和Lucene其用的索引文件是兼容的,solrconfig.xml有使用Lucene的版本信息。
所以提出一种方案,是否可以通过Lucene直接在Solr的目录中创建索引那,这样就可以减少网络的开销和HTTP服务的开销,直接写本地文件效率肯定是更高的。
设想如下图所示:
lucene提升solr性能
1、客户端调用Lucene的API直接在本地的Solr的core的目录建索引。(Collection的一个shard的副本其实就是一个core文件)
2、多个客户端,每个客户端对应一个shard副本,注意这里是一个shard只有一个副本的情况下,如果多个副本可能会导致数据不同步等问题。
3.2 思路实践
有了这个思路后,我就进行了实践,在不考虑数据备份的情况下和查询效率的情况下,优先考虑下建索引的效率,发现如下问题:
1、首先Lucene为了防止同一个目录由多个程序在写,加了文件锁,也就是通过了一个叫write.lock的文件来控制防止重复写的问题。
尝试解决:想到的第一个方法是看看有没有办法进行解锁,Lucene的4.9版本里面还真有个解锁的方法:
if (IndexWriter.isLocked(index)) {
try {
IndexWriter.unlock(index);
} catch (Exception e) {
print_exception_trace(e);
}
}
但是这个方法,调用起来看起来是成功的,但是在真正往solr的目录进行写索引的时候仍然报错。遂放弃。
这个思路本来没错的,既然在solr的索引目录无法再创建Lucene的索引,那么可以考虑在其他的目录创建索引,然后再合并到solr的索引中去。
过程说明:
1、 客户端程序调用lucene的API进行写索引。
2、 索引写到特定的目录下。
3、 调用solr的索引合并的HTTP接口,进行索引合并。
4、 调用HTTP的合并结构后会将Lucene新建的索引目录合并到Solr的索引中去。
注意:
1、 这种合并的URL如下:
http://ip:port/solr/admin/cores?action=mergeindexes&core=collection_shard1_replica1&indexDir=/parkfs01/aus/soft/luncenetmp/0
2、 合并后的索引不是可见的,需要重新加载索引,或者重新做提交,提交时候需要打开搜索器使索引可见:
http://ip:port/ solr/collection/update?commit=true&openSearcher=true
这里的collection要改成具体的collection名字。在单节点时候需要重新加载整个core,
这个耗时很大,单节点仅仅提交,仍然搜索不到新添加的文档。
3、如果在一个目录下进行新建Lucene索引,需要删除目录后以备下次重写。
3.3 架构设计
方案一:
对于改造后的思路,设计了一种模式来进行建索引和提交并发进行,设计的结构如下:
建索引架构改造
说明:
1、文档生成线程是根据数据生成所需要建索引的文档集合。
2、文档集合生成后,发送到中间队列中去。
3、有一个线程池,里面有N多线程,负责取文档集合,写入本地的索引文件夹中去,
注意一个线程对应一个IndexWriter,一个IndexWriter对应一个文件夹。
4、将需要合并的URL和提交的URL,发送到合并和提交队列中去。
5、提交合并线程池取到需要合并的URL和提交的URL,先进行合并,然后提交。
6、提交后,为了让文件夹再次循环利用需要删除文件夹内容。
注意:
1、solr的提交操作是很耗时的,在实际中,可以通过定时提交等手段进行进一步优化,不过solr本身设置的定时提交是无法起作用的。
2、在删除目录数据之前对于4.x版本需要进行IndexWriter的删除索引动作,不删除索引直接写会存在段信息错误的问题。
3、对于lucene的6.0版本,在删除文件的时候需要保存加锁文件write.lock;不能通过删除索引的方式进行索引信息的清理,不然数据仍然有问题,直接删除文件即可。
4、以上设计经过测试,效率并没有高多少,也就是2WTPS左右。
方案二:
经过方案一的测试,发现速度并没有达到预期,方案一本想通过各个环节分离来达到并行的最大速率的目的,结果并不如意,经过分析后设计第二套方案,如图:
方案二说明:
1、根据要求生成文档。
2、根据生成的文档列表生成可执行单元,可以执行单元的主要是创建IndexWriter和添加索引动作。
3、将可行对象发送给线程池执行。
4、执行完毕后将需要合并的索引目录和url发送到合并队列,合并队列数量达到一定数量后,执行合并索引动作和提交索引动作。
5、索引线程池存在建立一定数量索引后,会关闭原来的IndexWriter,从新创建的目录生成新的IndexWriter。
注意:
1、因为IndexWriter是线程安全的,所以线程池可以共同操作同一个IndexWriter对象。
2、在合并索引后,不能立刻删除目录,调用SOLR的合并索引的URL返回后,后台也有可能还在合并。
3、经过测试在一个索引文档286个字节条件下,solr6.0版本,速率大概在4.7W和5.1W之间浮动。
方案三:
方案二测试的速率不太稳定,而且需要定时执行删除索引文件,提交和合并索引的速率还是比较慢。
需要合并、提交索引和删除索引文件的根本在于Lucene的索引和solr的索引不在同一个目录,写同一个目录测试有问题的是4.9版本,所以想在solr6.0版本上测试试试。
结果在solr6.0版本,通过直接程序删除write.lock后,可以将索引直接写到solr的索引目录里面去。但是如果让数据可见,就必须重新reload整个索引副本,在reload整个索引副本后,数据可以查询,但是由于我们删除了write.lock,导致了重新reload的solr日志存在部分错误信息。
简单的测试结果如下:
1、测试环境:2台机器,2个solr节点,实际使用一个节点,配置:64G内存、16CPU。
2、线程个数:1个。
3、按照报文大小分别为0.2k、1k、2k测试结果如下:
序号 | 报文大小(字节) | 速率(TPS) | 吞吐量(MB/s) |
---|---|---|---|
1 | 286 | 51088 | 9.9 |
2 | 1228 | 37900 | 44 |
3 | 2406 | 38000 | 84 |
网友评论