SolrCloud分布式搜索源码分析

2022-03-25 16:13:14 浏览数 (1)

SolrCloud搜索流程图:

本文主要想讲两个主题:

  1. 目前solr(8.6.0)源码的分布式搜索实现方式, 这部分主要是基于对solr的源码分析.
  2. 尝试对solr的分布式方式做自己的分析, 为什么这么设计? 目前的设计有什么漏洞? 这部分主要是基于阅读solr wiki和jira里面作者记录的设计和实现思路, 相关资料附在了文章最后.

Warm up: 什么是SolrCloud

SolrCloud是solr对分布式搜索的实现, 分布式搜索主要涉及到两个概念, shard和replica.

从作用上, replica主要是做负载均衡/容灾, 本质就是把一个服务器复制N份, 然后将请求均匀分发到N个服务器上.

shard是将索引拆分, 比如一共要索引1000w文档, 如果都存在一个服务器上, 那么可能在不考虑高QPS的情况下, 单一请求的响应时间都已经是不能接受的了, 因此可以将1000w文档存在5个服务器上, 每个服务器保存一份子索引, 也就是分片(shard), 比如服务器A保存文档集[0,200w), 服务器B保存[200w,400w) ... 这样当查询的时候, 多个shard可以并发查询, 然后再将所有shard返回的结果做合并. 值得一提的是, 每一个shard的对应的是一份完整的lucene索引, 是可以自己直接写lucene代码读取的.

在SolrCloud中, shard和replica是配合使用的, 比如一个collection可以分3个shard, 然后每个shard可以分2个replica, 每个replica对应的就是一份lucene索引. 因此实际上就有3*2=6个lucene索引保存在服务器上(比方说可以保存在6个服务器上). 要执行一个查询的时候, 必须要合并3个shard的数据, 每个shard用哪个replica是随机选择的.

确定了分布式集群的逻辑结构之后, 剩下的就是具体处理分布式请求的代码了. 主要可以看成两大块, 索引和查询.

索引的话, 主要是为每一个文档生成一个hash值, 然后通过hash值确定要索引到哪个shard, 然后每一个shard的所有replica里有一个leader, 索引请求先发到leader, 再由leader同步到其他的replica. (这个是solr官方文档的描述, 分布式索引这块的源代码我还没有读)

本文主要是讲分布式查询的过程, 思路来源于我对于solr源码的阅读与理解.

分布式查询过程

当我们请求SolrCloud集群的时候, 一般是通过一个http请求的, 这个http请求可以发送给集群中的任意一台机器, 这台机器我们暂时叫它ClientNode, 然后ClientNode为了执行查询, 会发送请求给所有涉及到的shard分片所在的服务器(实际是每个shard的所有replica中的任意一个), 我们暂时叫它们ShardNode. 要注意的是, 最初接受用户请求并分发给各分片的ClientNode自己本身也是一个ShardNode, 因此它作为ClientNode给各分片发送请求的时候, 也是有可能发送给自己的.

ClientNode处理过程

  1. 用户对ClientNode发起http请求.
  2. ClientNode通过解析request, 由rb.isDistrib属性知道接到的是用户的直接请求, 因此是个分布式请求, 所以触发分布式请求的处理逻辑:
获取TopN ids阶段

这个阶段的目的是要拿到最终返回结果列表的文档ID(unique keys)列表.

怎么搞呢? 比如现在有三个shard, 用户请求返回得分最高的20篇文档, 那么ClientNode就需要向3个ShardNode异步发送3个请求, 每个请求的rows(返回文档数)都是20, fl(返回字段)只要ID和score(或其他排序条件), 然后3个ShardNode会并发查询自己分片的子索引, 得到自己的子索引内得分前20的文档返回给ClientNode.

因此ClientNode最终会收到20*3=60个文档ID, 这60个文档ID是在各自shard中排名前20的文档, 然后ClientNode会根据score在这60个文档中找出得分最高的20个文档, 这样就得到了最终要返回给用户的20个文档的ID和score了.

注: ClientNode给ShardNode发送请求的时候, 通过req.params里的shards.purpose参数注明此次请求的目的, shards.purpose是一个int值, 可以按位同时存储多个请求目的, 如获取TopN ids阶段时候会标记 shards.purpose|=ShardRequest.PURPOSE_GET_TOP_IDS, 代表目的(之一)是获取TopN ids. 后面在补全字段阶段, shards.purpose的值就会有所不同, 会标记shards.purpose|=ShardRequest.PURPOSE_GET_FIELDS, 代表目的(之一)是获取字段.

补全字段阶段

现在有了返回文档的ID和score, 还需要补全fl中指定的其他要返回的字段.

为啥这一步要单提出来呢? 很显然如果ClientNode在获取TopN ids阶段给各ShardNode发送请求的时候, 直接将fl设成真实要返回的所有字段, 那么后面合并后的结果直接就有所有需要返回的字段了. 但是在solr中, 每次要获得一个文档的stored/docValue字段的时候, 都要调用SolrDocumentFetcher.doc(int i, Set<String> fields) 方法, 如果在获取TopN ids阶段同时获取字段, 那么累计要调用SolrDocumentFetcher.doc()方法20*3=60次, 而这60个文档最终只有20个是要真实返回的, 为其余40个获取其他返回字段是没有任何意义的. 因此要把获取字段阶段独立出来放在获取TopN ids阶段后面, 如果已经找出了最终要返回的20个文档的ID, 那么只需要为这20个文档补全其他字段就够了.

补全字段阶段的想法是非常直观的, 因为要返回的20个文档分散在3个分片中, 因此先把20个文档ID按所在的shard分3组, 然后分别向3个ShardNode异步发送3个请求, 这次每个请求直接指定了IDS参数, 传的是20个文档IDS中在当前分片的子集IDS, FL参数直接指定为真实要获取的字段. 最后ClientNode收到3个ShardNode返回的补全了字段的文档集后, 再按照原来的顺序重新组织成长度为20的文档集列表, 就可以返回给用户了. 这里要注意的是最终返回的score字段的得分使用的是在获取TopN ids阶段计算出的得分, 补全字段阶段要补全的是除了ID, score外的其他字段.

ShardNode处理过程

  1. ClientNode对ShardNode发起http请求, 在正常搜索流程中主要就是两类请求, 一类是在ClientNode的TopN ids阶段发过来的请求, 另一类是在ClientNode补全字段阶段发送过来的请求, 这两种请求对于ShardNode来说没有太本质的区别, 只是request参数不同而已.
  2. ShardNode通过解析request, 由rb.isDistrib属性知道接到的是ClientNode的请求, 因此是个非分布式请求, 所以触发非分布式请求的处理逻辑:
    1. 执行所有components的prepare()方法
      1. QueryComponent在这个阶段完成了query字符串->query对象的parse过程.
    2. 执行所有components的process()方法
      1. QueryComponent在这个阶段完成了真实的搜索lucene索引的操作.
    3. 返回NamedList格式的response给ClientNode

分布式设计讨论

还有哪些设计思路

  1. 不分阶段召回, 对各分片都只做一次请求, 这次请求直接完成获取TopN ids以及获取要返回字段的操作, 然后直接合并所有分片结果就能得到最终结果. 这种思路只是理论可行, 在实际场景中, 如果有深度翻页, 比如start=10000, rows=20, 这样每个分片都要获取10000 20个结果, 这种情况下, 还要把全部文档的返回字段都获取到, 然后还在网络传输给ClientNode, 相比于分阶段召回, 第一次只需要传输ids和排序条件的方案, 是不可接受的.

当前设计的缺陷

  1. 分阶段获取过程中的索引一致性问题: 目前的分布式查询分了两个阶段, 阶段1发起第一次请求从各分片获取TopN ids, 阶段2合并所有分片ids后再发起第二次请求去各分片获取要返回的字段. 然而这两次请求中间是有一个时间窗口的, 在这个时间窗口里, 各分片的索引可能会发生改变, 比如在获取ids阶段根据termA召回了一个文档1, 然后在获取文档1的返回字段的时候, 可能文档1已经被更新了, 已经不包含termA了,这样的话最后就会错误返回一个不包含termA的文档1. 类似的情况还有可能在获取ids阶段召回了文档1, 但是在获取字段阶段, 文档1已经被删除了. 类似的问题其实是需要在两次请求的时候维护每个分片索引的一致性的, 目前solr没有做.

总结

目前的SolrCloud分布式搜索方案并不是完美的, solr的开发者最初在设计时提出了很多要满足的点, 有一些在当初实现的时候(2008年)没能解决的问题, 至今(2020)依然没有解决,相信在很大程度上也是因为有些在工程看似不完美的设计, 在生产环境中其实不是非要解决的. 通过这次学习solr分布式搜索的相关源码以及阅读solr开发者当时的设计文档, 深深感受到了在工程上: Done is better than perfect.

ref

https://cwiki.apache.org/confluence/display/solr/DistributedSearchDesign

https://issues.apache.org/jira/browse/SOLR-303

0 人点赞