solr亿万级索引优化实践


    海量数据的索引,第一个要解决的是数据存储的问题,solr提供数据存储平台有两种,第一个是本地磁盘,另一个是HDFS,我们可以通过solrhome的配置来实现。在本次实践中,我们选择的是本地磁盘,因为采用的solrcloud部署模式,本身就是多节点多机器,在存储上不会有问题,还有另一个重要的原因后面会讲到。下面讲讲具体从哪些方面做了实践。

    solr版本:solr6.0.0;主机:red hat; 网络:千兆网

    性能的具体要求是:

    1、在一台机器上普通硬盘的情况下,索引单个文档大小为200字节左右,需要达到的效率为7WTPS。

    2、具有良好的水平扩展性。

    3、对数据备份和数据丢失情况,要求不严格。

    在对索引速度进行优化之前,有必要先对建立索引的整个流程做个大致的了解,如下图所示:

                                                         

   说明:

  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、 返回写索引的结果。

    提交方式

    solr提供了两种提交方式:硬提交和软提交。硬提交(hard commit)指的是在客户端每提交一批数据都通知服务器把数据刷新到硬盘上,检索的时候数据可以立马可见;软提交(soft commit)是让服务器自己控制在一定的时间范围之内刷新数据。很明显,硬提交是非常损耗性能的操作,并且它还会阻塞其他数据的提交,所以我们选择软提交,具体配置方式如下所示:

<autoCommit> 
       <maxTime>${solr.autoCommit.maxTime:30000}</maxTime> 
       <openSearcher>false</openSearcher> 
 </autoCommit>
 
<autoSoftCommit> 
  <maxTime>${solr.autoSoftCommit.maxTime:60000}</maxTime> 
</autoSoftCommit>

    在这份配置文件中,我们指定了服务器每隔60秒对数据做一次软提交,另外推荐opensearch设置为false,否则每次提交都会开启一个opensearch,这个也会损耗性能。

    关闭副本

    solr一个collection会有多个分片(shard),多个shard分别位于不同的节点上,每个节点上可能会有shard的多个复制,其中一个为leader shard,在追求极限速度的情况下,可以将副本数设置为1,这样减去了副本间的数据同步等资源消耗。不过这样做带来的弊端就是数据容灾性降低,和检索性能急剧下降。

    取消分词器

    很显然使用分词器的话会对数据做进一步处理,也是会使得性能大幅度降低的,再不使用全文检索的情况下可以不使用分词器来提高速度。

    增加分片

    在一台物理机上,我们可以部署多个solr实例,在每个实例上可以设置多个shard,这样在索引数据的时候,一个collection会有多个shard在同时入数据,显然速度是可以大幅提升的。不过shard数也不能太多,机器资源是有限的,当太多shard同时写数据,会导致内存和IO压力很大,效果适得其反,应该根据自己的硬件情况进行合理设置。另外,如果是采用的SSD硬盘,设置多个solrhome也会有不错的效果。

    划分collection

    当数据积累的越来越多的时候,哪怕多个shard,每个shard的数量也是巨量的,这个时候,不仅查询性能急剧下降,由于lucene倒排序索引的原理建索引速度也会越来越慢。所以我们尝试控制每个shard数据量不会太大,进行了按业务划分索引库,或者按天按小时建立索引库,这样数据分布到多个collection中,均衡了每个索引库的压力。


       通过上一篇的几个优化方案,我们的索引速度其实已经能得到很大的提升了,从最初的平均每台机器7000TPS/S,大概能到2.5WTPS/S。但是这个速度远远还达不到我们的需求,最关键的时候随着节点数增加速度并不能线性增加,然后又做了许多其他方面的尝试,其中路由方式是比较大的一个方向,本篇将重点介绍这一方案。

       前面介绍了solr在创建索引库的时候可以指定多个shard来同时索引数据,那么问题来了,文档是通过何种规则将数据负载均衡到每个分片之上的呢,solr提供了两种路由方式:哈希路由(composite),指定路由(implict)。其中哈希路由,创建collection的时候需要明确指定shard数量,后期shard可以进行分裂(split)操作,但是不能增加或者删除shard。solr默认的就是采用这种路由模式,利用计算文档ID的哈希值,来判断将此文档索引到哪个分片之上,这样做的好处是可以使得每个shard上数据负载均衡,但在追求极限速度之下,会浪费掉不少时间。第一,计算hash值需要耗时;第二,数据在各个分片之间迁徙分发需要消耗网络以及内存资源。所以我们选择了直接路由模式。工作模式如下图,透过collection,直接指定分片载入数据:

       直接路由模式顾名思义,指定索引具体落在路由到哪个shard,该模式同样在创建collection时需要具体指定shard数量,后期可以动态追加分片以及删除分片,但是如果一个分片过大时不能进行分裂的。需要注意的是:

     1)索引要特殊方式通过以下URL新建

http://xxxx.xxxx.xxx.xxx:port/solr/admin/collections?action=CREATE&name=implicit1&shards=shard1,shard2,shard3&router.name=implicit

     2)在solr4.x版本中通过更改schemal.xml在5.0以上更改managed-schema文件添加以下字段定义:

<field name="_route_" type="string"/>

      3)利用SolrJ添加文档的时候需要加入以下字段:

doc.addField("_route_","shard_x")

       通过直接路由的模式,我们可以每个线程操作一个shard,如果物理机上有多块硬盘,就可以每个硬盘上部署一个solr节点,这样多块硬盘之间的索引性能是互不影响的,我们就可以随着节点数的增加而线性增长。带来的问题就是可能会导致数据分布不均,而使某个分片过载,不过我们系统中数据中间件使用的是apache kafka,一个topic设置了多个partiton,在这一层报文已经做了一次均衡路由,每个partiton消费出来的数据负责写一个solr的shard,所以不用担心这个问题,后面有机会介绍下kafka消息中间件。

       通过具体测试,指定路由速度相比于哈希路由,在单节点下速度并没有提升,但是在多节点下,集群的吞吐量有明显的提升,可以真正做到水平拓展,对于物理机数量充足的情况下,可以满足每天海量索引的更新。下一篇将继续介绍如何利用solrj客户端来提高单节点的速度,这样整个集群的吞吐量又能进一步提高。



      本篇文章主要介绍下如何从客户端solrJ以及服务端参数配置的角度来提升索引速度。

      solrJ6.0提供的Java客户端主要有下面几种接口:HttpSolrClient,ConcurrentUpdateSolrClient,CloudSolrClient。下面分别对这三种接口做一个简单的比较。HttpSolrClient在定义的时候需要明确指定一个solr节点路径,他在提交数据的时候也只能提交到这个节点上;ConcurrentUpdateSolrClient接口在同样是指定具体solr节点路径的,但不一样的事,这是个异步提交的模式,即我们在对客户端添加数据的时候,客户端会将文档缓存到内存队列中,让队列中的数据达到一定数量时,客户端会自动一次性向solr服务器发起一个http请求;CloudSolrClient就比较简单了,这个在定义时只需要指定zookeeper地址就好了,因为solr节点注册时,会将节点信息同步到zookeeper中,在提交数据时,CloudSolrClient会和zookeeper进行通信,发现solr云中的索引库,然后利用LBHttpSolrClient来提交数据。通过对比发现,ConcurrentUpdateSolrClient和CloudSolrClient这两个接口比较优秀,后者比较适用于哈希路由的模式,而前者则比较适合指定路由的模式。

      CloudSolrClient

  public SolrClient CreateCloudSolrClient() {
    CloudSolrClient csClient = new CloudSolrClient(zkUrl);
    csClient.setZkConnectTimeout(zkConnectTimeOut);
    csClient.setZkClientTimeout(zkClientTimeOut);
    csClient.setDefaultCollection(collectionName);
    csClient.setParallelUpdates(true);
    csClient.setRequestWriter(new BinaryRequestWriter());
    csClient.connect();
    return csClient;
  }

       ConcurrentUpdateSolrClient

public SolrClient CreateHttpClient() {
    ModifiableSolrParams params = new ModifiableSolrParams();
    params.set(HttpClientUtil.PROP_MAX_CONNECTIONS, 128);
    params.set(HttpClientUtil.PROP_MAX_CONNECTIONS_PER_HOST, 32);
    params.set(HttpClientUtil.PROP_FOLLOW_REDIRECTS, false);
    params.set(HttpClientUtil.PROP_ALLOW_COMPRESSION, true);
    params.set(HttpClientUtil.PROP_USE_RETRY, true);
    HttpClient httpClient = HttpClientUtil.createClient(params);
    httpLists.add(httpClient);
    ConcurrentUpdateSolrClient client = new ConcurrentUpdateSolrClient(solrUrl + "/" + collectionName, 50, 1);
    client.setConnectionTimeout(zkClientTimeOut);
    client.setSoTimeout(zkConnectTimeOut);
    client.setPollQueueTime(500);
    client.setParser(new BinaryResponseParser());
    client.setRequestWriter(new BinaryRequestWriter());
    return client;
  }

        上篇文章介绍过,在多线程配合指定路由的模式下,集群的性能能够水平拓展,这个时候再配合ConcurrentUpdateSolrClient客户端,单个节点速度也会有比较大的提升。需要注意的是,有两个参数需要慎重设置,第一个是队列大小,这个指的是队列中最多存储多少个请求之后向服务器进行提交,另一个是线程数,表示内部开几个线程来提交数据。这两个参数需要根据自己应用程序的JVM来设置,如果设置的过大,会导致内存溢出。

         由于是异步请求的方式,所以如果在建立索引的过程中出现了异常,异常信息是不会抛给应用程序的,后来通过调试源码发现,solrJ自己在内部处理了这个异常(其实什么都没做,预留了一个空方法),我们可以在选择修改源码来捕捉异常数据,或者将此异常抛出,由应用程序来捕捉异常,持久化异常数据。

          在服务器端,我们也可以通过一些优化,来提高建立索引速度。

          1、段合并

        solr的索引是由段组成,更新索引的时候是写入一个段的信息,几个段共同组成一个索引,在solr优化索引的时候或其他的时候,solr的段是会合并的。所以我们可以对相关参数进行控制,对段的大小以及合并的频率进行调整,来提交系统资源利用效率。

        mergeFactor这个参数是合并因子,当内存中有N个文档时则合并成一个段,当存在N个段文件时则合并成一个新的段文件。

        minMergeSize,指定最小的合并段大小,如果段的大小小于这个值,则可以参加合并。

        maxMergeSize,当一个段的大小大于这个值的时候就不参与合并了。

        maxMergeDocs,当文档数据量大于这个的时候,这个段就不参与合并了。

在实际场景中,应该根据物理机的资源,来配置这些参数,适量加大mergeFactor参数,来降低合并频率,频繁的段合并会消耗大量系统资源。

         2、自动生成ID

         在Solr中,每一个索引,都要有一个唯一的ID,类似于关系型数据库表中的主键。我们在创建文档的时候,需要自己生成一串UUID来标示文档,但是由于ID比较长,在索引过程中,会占用一些额外的网速和内存等资源,我们可以控制在服务器端让solr自己生成UUID,具体配置步骤如下:

        1)修改schema.xml文件,将ID字段修改为UUID类型

 <field name="id" type="uuid" indexed="true" stored="true" required="true" multiValued="false" />  <fieldType name="uuid" class="solr.UUIDField" indexed="true" />

        2)配置solrconfig.xml文件,添加更新策略配置,生成UUID

<updateRequestProcessorChain name="uuid">
     <processor class="solr.UUIDUpdateProcessorFactory">
          <str name="fieldName">id</str>
     </processor>
     <processor class="solr.LogUpdateProcessorFactory" />
     <processor class="solr.DistributedUpdateProcessorFactory" />
     <processor class="solr.RunUpdateProcessorFactory" />
</updateRequestProcessorChain>

        3)配置requestHandler

<requestHandler name="/dataimport" class="solr.DataImportHandler">
    <lst name="defaults">
      <str name="config">tika-data-config.xml</str>
      <str name="update.chain">uuid</str>    
   </lst>
</requestHandler>
 
<requestHandler name="/update" class="solr.UpdateRequestHandler">
       <lst name="defaults">
        <str name="update.chain">uuid</str>
       </lst>  
</requestHandler>
 
  <!-- for back compat with clients using /update/json and /update/csv -->  
  <requestHandler name="/update/json" class="solr.JsonUpdateRequestHandler">
        <lst name="defaults">
         <str name="stream.contentType">application/json</str>
         <str name="update.chain">uuid</str>
        </lst>
  </requestHandler>
  <requestHandler name="/update/csv" class="solr.CSVRequestHandler">
        <lst name="defaults">
         <str name="stream.contentType">application/csv</str>
         <str name="update.chain">uuid</str>
        </lst>
  </requestHandler>
 
  <requestHandler name="/update/extract"
                  startup="lazy"
                  class="solr.extraction.ExtractingRequestHandler" >
    <lst name="defaults">
      <str name="xpath">/xhtml:html/xhtml:body/descendant:node()</str>
      <str name="capture">content</str>
      <str name="fmap.meta">attr_meta_</str>
      <str name="uprefix">attr_</str>
      <str name="lowernames">true</str>
      <str name="update.chain">uuid</str>
    </lst>
</requestHandler>

    这样solr即可自动生成UUID,不需要在客户端额外生成。


     本篇是这个系类的最后一篇,但优化方案不仅于此,需要后续的研究与学习,本篇主要从schema设计的角度来做一些实践。

      schema.xml 这个文件的作用是定义索引数据中的域的,包括域名称,域类型,域是否索引,是否分词,是否存储,是否标准化,是否存储项向量等等。在solr6中这个文件是存放在zookeeper的/configs节点之下的,在创建新的collection时,solr会根据此节点下的信息生成相应的索引库,其相关的配置信息会同步到solrhome/core目录下的core.properties文件中。同步schema文件的指令语句样例为:

bin/solr zk -upconfig -z 127.0.01:2181 -n conf -d /solrhome/configsets/sample_techproducts_configs/conf

    为了改进性能,可以从以下几个方面来着手:

    1、对于field元素,我们将所有只用于搜索的,而不需要作为查询结果的field(特别是一些比较大的field)的stored设置为false,这样这个字段的值将不会被存储,但可以被检索,会减少不小的IO开销。我们设计了一个利用solr来做hbase的二级索引架构,可以利用hbase来存储字段信息,充分利用hadoop的大数据特性。

    2、能不用copyfield这个元素就不用,这个属性会对字段做双倍存储,显然非常耗性能,好处就是在查询的时候,想要对多个字段进行检索只需要检索一个字段。

    3、将一些不需要被检索的字段的index属性,设置成false,这样solr就不会对这个字段进行索引。

  4、不使用中文分词器或者使用高亮功能。termPositions termOffsets的值全都设置成false。

    5、在测试中发现solr在处理小报文(1K以下)的情况下吞吐量并不理想,当适当增大报文,发现速度可以得到大幅度提高,可以从之前的每个节点10M/S暴涨到30M/S。

       但是在很多情况下,我们并不能人为控制报文长度,这个时候,可以通过solr的字段多值来达到目的,即将多条消息的每个字段的值放到一起,在schema中配置multValued为true。存储到solr中是这个样子的:

  

       这样速度是可以单台节点达到30M/S甚至更高,但是带来的问题就是查询会变得很复杂,在命中多值中任意一条记录,结果集会带出所有值,在solr中认为这一组数据是一个文档,我们想了很多方案来解决这个问题,比较简单的方法是,在生成文档的时候控制多值字段中,没有重复的值,这样检索结果则会变得精确,缺点就是灵活性太低。另一个方案,是通过对数据做预聚合,管理快照,由于其实现比较复杂,效果也不是很理想,在此就不做过多描述了。

       在不追求检索精确度,或者对数据可控的情况下,对于索引速度真的可以带来很大的惊喜。

       尾言:solr由于是利用lucene为底层,lucene本身是单机的无法分布式,solr的核心就是引入了分片的机制,在数据规模变得特别庞大的时候各种弊端就显示出来了,无论是建立索引还是查询性能都不尽人意。但通过各种方法的优化与舍弃之后,差不多可以做到水平拓展,线性增长,能够满足大多数的业务场景。但是如果是要对历史数据进行检索的时候,这个历史数据规模又是极其巨量时,solr恐怕是无法承受的。现在兴起了很多列式存储结构以及时间序列的数据库以及仓库,比如driud和tsdb,他们在巨量数据检索时可以带来极高的性能体验。

版权声明:本文为CSDN博主「cyony」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/cyony/article/details/62234890

java达人

ID:drjava

(长按或扫码识别)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值