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给各分片发送请求的时候, 也是有可能发送给自己的.
这个阶段的目的是要拿到最终返回结果列表的文档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外的其他字段.
目前的SolrCloud分布式搜索方案并不是完美的, solr的开发者最初在设计时提出了很多要满足的点, 有一些在当初实现的时候(2008年)没能解决的问题, 至今(2020)依然没有解决,相信在很大程度上也是因为有些在工程看似不完美的设计, 在生产环境中其实不是非要解决的. 通过这次学习solr分布式搜索的相关源码以及阅读solr开发者当时的设计文档, 深深感受到了在工程上: Done is better than perfect.
https://cwiki.apache.org/confluence/display/solr/DistributedSearchDesign
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。