SolrCloud详解
SolrCloud的基本概念
1、Cluster集群:Cluster是一组Solr节点,逻辑上作为一个单元进行管理,整个集群必须使用同一套schema和SolrConfig。
2、Node节点:一个运行Solr的JVM实例。
3、Collection:在SolrCloud集群中逻辑意义上的完整的索引,常常被划分为一个或多个Shard,这些Shard使用相同的Config Set,如果Shard数超过一个,那么索引方案就是分布式索引。SolrCloud允许客户端用户通过Collection名称引用它,这样用户不需要关心分布式检索时需要使用的和Shard相关参数。
4、Core: 也就是Solr Core,一个Solr中包含一个或者多个Solr Core,每个Solr Core可以独立提供索引和查询功能,Solr Core的提出是为了增加管理灵活性和共用资源。SolrCloud中使用的配置是在Zookeeper中的,而传统的Solr Core的配置文件是在磁盘上的配置目录中。
5、Config Set: Solr Core提供服务必须的一组配置文件,每个Config Set有一个名字。最小需要包括solrconfig.xml和schema.xml,除此之外,依据这两个文件的配置内容,可能还需要包含其它文件,如中文索引需要的词库文件。Config Set存储在Zookeeper中,可以重新上传或者使用upconfig命令进行更新,可使用Solr的启动参数bootstrap_confdir进行初始化或更新。
6、Shard分片: Collection的逻辑分片。每个Shard被分成一个或者多个replicas,通过选举确定哪个是Leader。
7、Replica: Shard的一个拷贝。每个Replica存在于Solr的一个Core中。换句话说一个SolrCore对应着一个Replica,如一个命名为“test”的collection以numShards=1创建,并且指定replicationFactor为2,这会产生2个replicas,也就是对应会有2个Core,分别存储在不同的机器或者Solr实例上,其中一个会被命名为test_shard1_replica1,另一个命名为test_shard1_replica2,它们中的一个会被选举为Leader。
8、 Leader: 赢得选举的Shard replicas,每个Shard有多个Replicas,这几个Replicas需要选举来确定一个Leader。选举可以发生在任何时间,但是通常他们仅在某个Solr实例发生故障时才会触发。当进行索引操作时,SolrCloud会将索引操作请求传到此Shard对应的leader,leader再分发它们到全部Shard的replicas。
9、Zookeeper: Zookeeper提供分布式锁功能,这对SolrCloud是必须的,主要负责处理Leader的选举。Solr可以以内嵌的Zookeeper运行,也可以使用独立的Zookeeper,并且Solr官方建议最好有3个以上的主机。
SolrCloud中完整索引(Collection)的逻辑
在SolrCloud模式下Collection是访问Cluster的入口,这个入口有什么用呢?比如说集群里面有好多台机器,那么访问这个集群通过哪个地址呢,必须有一个接口地址,Collection就是这个接口地址。可见Collection是一个逻辑存在的东西,因此是可以跨Node的,在任意节点上都可以访问Collection。Shard其实也是逻辑存在的,因此Shard也是可以跨Node的; 1个Shard下面可以包含0个或者多个Replication,但1个Shard下面能且只能包含一个Leader如果Shard下面的Leader挂掉了,会从Replication里面再选举一个Leader。
SolrCloud索引操作的基本架构图
图中所示为拥有4个Solr节点的集群,索引分布在两个Shard里面,每个Shard包含两个Solr节点,一个是Leader节点,一个是Replica节点,此外集群中有一个负责维护集群状态信息的Overseer节点,它是一个总控制器。集群的所有状态信息都放在Zookeeper集群中统一维护。从图中还可以看到,任何一个节点都可以接收索引创建或者更新的请求,然后再将这个请求转发到索引文档所应该属于的那个Shard的Leader节点,Leader节点更新结束完成后,最后将版本号和文档转发给同属于一个Shard的replicas节点。
SolrCloud的工作模式
SolrCloud中包含有多个Solr Instance,而每个Solr Instance中包含有多个Solr Core,Solr Core对应着一个可访问的Solr索引资源,每个Solr Core对应着一个Replica或者Leader,这样,当Solr Client通过Collection访问Solr集群的时候,便可通过Shard分片找到对应的Replica即SolrCore,从而就可以访问索引文档了。
在SolrCloud模式下,同一个集群里所有Core的配置是统一的,Core有leader和replication两种角色,每个Core一定属于一个Shard,Core在Shard中扮演Leader还是replication由Solr内部Zookeeper自动协调。
访问SolrCloud的过程:Solr Client向Zookeeper咨询Collection的地址,Zookeeper返回存活的节点地址供访问,插入数据的时候由SolrCloud内部协调数据分发(内部使用一致性哈希)。
SolrCloud创建索引和更新索引
索引存储细节:
当Solr客户端发送add/update请求给CloudSolrServer,CloudSolrServer会连接至Zookeeper获取当前SolrCloud的集群状态,并会在/clusterstate.json 和/live_nodes中注册watcher,便于监视Zookeeper和SolrCloud,这样做的好处有以下两点:
1、CloudSolrServer获取到SolrCloud的状态后,它可直接将document发往SolrCloud的leader,从而降低网络转发消耗。
2、注册watcher有利于建索引时候的负载均衡,比如如果有个节点leader下线了,那么CloudSolrServer会立马得知,那它就会停止往已下线的leader发送document。
此外,CloudSolrServer 在发送document时候需要知道发往哪个shard?对于建好的SolrCloud集群,每一个shard都会有一个Hash区间,当Document进行update的时候,SolrCloud就会计算这个Document的Hash值,然后根据该值和shard的hash区间来判断这个document应该发往哪个shard,Solr使用documentroute组件来进行document的分发。目前Solr有两个DocRouter类的子类CompositeIdRouter(Solr默认采用的)类和ImplicitDocRouter类,当然我们也可以通过继承DocRouter来定制化我们的document route组件。
举例来说当Solr Shard建立时候,Solr会给每一个shard分配32bit的hash值的区间,比如SolrCloud有两个shard分别为A,B,那么A的hash值区间就为80000000-ffffffff,B的hash值区间为0-7fffffff。默认的CompositeIdRouter hash策略会根据document ID计算出唯一的Hash值,并判断该值在哪个shard的hash区间内。
SolrCloud对于Hash值的获取提出了以下两个要求:
1、hash计算速度必须快,因为hash计算是分布式建索引的第一步。
2、 hash值必须能均匀的分布于每一个shard,如果有一个shard的document数量大于另一个shard,那么在查询的时候前一个shard所花的时间就会大于后一个,SolrCloud的查询是先分后汇总的过程,也就是说最后每一个shard查询完毕才算完毕,所以SolrCloud的查询速度是由最慢的shard的查询速度决定的。
基于以上两点,SolrCloud采用了MurmurHash 算法以提高hash计算速度和hash值的均匀分布。
更新索引:
1、 Leader接受到update请求后,先将update信息存放到本地的update log,同时Leader还会给document分配新的version,对于已存在的document,如果新的版本高就会抛弃旧版本,最后发送至replica。
2、一旦document经过验证以及加入version后,就会并行的被转发至所有上线的replica。SolrCloud并不会关注那些已经下线的replica,因为当他们上线时候会有recovery进程对他们进行恢复。如果转发的replica处于recovering状态,那么这个replica就会把update放入updatetransaction 日志。
3、当leader接受到所有的replica的反馈成功后,它才会反馈客户端成功。只要shard中有一个replica是active的,Solr就会继续接受update请求。这一策略其实是牺牲了一致性换取了写入的有效性。这其中有一个重要参数:leaderVoteWait参数,它表示当只有一个replica时候,这个replica会进入recovering状态并持续一段时间等待leader的重新上线。如果在这段时间内leader没有上线,那么他就会转成leader,其中可能会有一些document丢失。当然可以使用majority quorum来避免这个情况,这跟Zookeeper的leader选举策略一样,比如当多数的replica下线了,那么客户端的write就会失败。
4、索引的commit有两种,一种是softcommit,即在内存中生成segment,document是可见的(可查询到)但是没写入磁盘,断电后数据会丢失。另一种是hardcommit,直接将数据写入磁盘且数据可见。
SolrCloud索引的检索
1、用户的一个查询,可以发送到含有该Collection的任意Solr的Server,Solr内部处理的逻辑会转到一个Replica。
2、此Replica会基于查询索引的方式,启动分布式查询,基于索引的Shard的个数,把查询转为多个子查询,并把每个子查询定位到对应Shard的任意一个Replica。
3、每个子查询返回查询结果。
4、最初的Replica合并子查询,并把最终结果返回给用户。
SolrShard Splitting的具体过程
一般情况下,增加Shard和Replica的数量能提升SolrCloud的查询性能和容灾能力,但是我们仍然得根据实际的document的数量,document的大小,以及建索引的并发,查询复杂度,以及索引的增长率来统筹考虑Shard和Replica的数量。Solr依赖Zookeeper实现集群的管理,在Zookeeper中有一个Znode 是/clusterstate.json ,它存储了当前时刻下整个集群的状态。同时在一个集群中有且只会存在一个overseer,如果当前的overseer fail了那么SolrCloud就会选出新的一个overseer,就跟shard leader选取类似。
Shard分割的具体过程(old shard split为newShard1和newShard2)可以描述为:
a、在一个Shard的文档到达阈值,或者接收到用户的API命令,Solr将启动Shard的分裂过程。
b、此时,原有的Shard仍然会提供服务,Solr将会提取原有Shard并按路由规则,转到新的Shard做索引。同时,新加入的文档:
1.2.用户可以把文档提交给任意一个Replica,并转交给Leader。
3.Leader把文档路由给原有Shard的每个Replica,各自做索引。
III.V. 同时,会把文档路由给新的Shard的Leader
IV.VI.新Shard的Leader会路由文档到自己的Replica,各自做索引,在原有文档重新索引完成,系统会把分发文档路由切到对应的新的Leader上,原有Shard关闭。Shard只是一个逻辑概念,所以Shard的Splitting只是将原有Shard的Replica均匀的分不到更多的Shard的更多的Solr节点上去。
搭建
搭建solr集群所需要的服务器为:192.168.121.12和192.168.121.14。
1、搭建zookeeper集群步骤:需要三台zookeeper、分别是zk1、zk2、zk3,对应的端口分别为192.168.121.12:2181:192.168.121.12:2182:192.168.121.14:2181。
2、搭建solrcloud集群步骤:
1)搭建四个单机版的solr服务对应的jetty,端口分别是192.168.121.12:8080、192.168.121.12:8081、192.168.121.14:8080、192.168.121.14:8081。
2)设置jetty的启动参数,在每个jetty目录下的bin/jetty.sh,添加以下-DzkHost参数内容内容:
3)将solr配置文件上传到zookeeper中,进行统一管理,进入到/root/soft/solr-4.10.3/example/scripts/cloud-scripts目录中执行zkcli.sh命令:./zkcli.sh -zkhost 192.168.121.12:2181,192.168.121.12:2182,192.168.121.14:2181 -cmd upconfig -confdir /usr/local/solrcloud/solrhome/collection/conf -confname myconf(每个ip地址之间用逗号分隔)
collection为自己创建的solr集合
myconf为上传到zookeeper的集合配置文件的名称
3、启动所有的solr服务、启动所有的zookeeper服务。
4、访问部署的solr集群中任意的端口服务。
5、创建索引集合指令:
http://192.168.121.12:8080/solr/admin/collections?action=CREATE&name=AddressBean&numShards=2&replicationFactor=2&collection.configName=AddressBean
action:表示执行的动作,是创建还是删除。
name:创建的集合名称。
numShards:分片的个数。
replicationFactor:每个分片有多少个副本集。
collection.configName:引用的集合配置文件的名称。
6、删除索引集合指令:
http://192.168.121.12:8080/solr/admin/collections?action=DELETE&name=haixin
实例
package solrtest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.SolrQuery.ORDER;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.impl.CloudSolrClient.Builder;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.UpdateResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.aspectj.lang.annotation.Before;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
public class SolrCloudService {
// zookeeper地址
private static final String zkHostString ="192.168.121.12:2181,192.168.121.12:2182,192.168.121.14:2181";
// collection默认名称,比如我的solr服务器上的collection是collection2_shard1_replica1,就是去掉“_shard1_replica1”的名称
private static String defaultCollection ="AddressBean";
// cloudSolrServer实际
private static CloudSolrClient cloudSolrClient;
final static int zkClientTimeout = 30000;
final static int zkConnectTimeout = 30000;
// 测试方法之前构造 CloudSolrServer
public static void main(String[] args) throws IOException, Exception{
cloudSolrClient = new CloudSolrClient.Builder().withZkHost(zkHostString).build();
cloudSolrClient.setDefaultCollection(defaultCollection);
cloudSolrClient.setZkClientTimeout(zkClientTimeout);
cloudSolrClient.setZkConnectTimeout(zkConnectTimeout);
cloudSolrClient.connect();
JSONObject jsonObject = new JSONObject();
jsonObject.put("id", UUID.randomUUID().toString());
jsonObject.put("userName", "凌晨零点");
//jsonObject.put("realName", "尤忠民");
//jsonObject.put("department", "青岛大学网络中心技术管理二部");
jsonObject.put("createTime", new Date());
//jsonObject.put("jobTitle", "项目经理");
jsonObject.put("gender", "女");
jsonObject.put("address", "青岛市崂山区青岛大学五号楼五楼");
SolrCloudService.addIndexs(jsonObject);
}
// 向solrCloud上创建单个索引
public static void addIndexs(JSONObject jsonObject) throws SolrServerException,
IOException {
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", jsonObject.getString("id"));
doc.addField("userName", jsonObject.getString("userName"));
//doc.addField("realName", jsonObject.getString("realName"));
//doc.addField("department", jsonObject.getString("department"));
doc.addField("createTime", new Date());
//doc.addField("jobTitle", jsonObject.getString("jobTitle"));
doc.addField("gender", jsonObject.getString("gender"));
doc.addField("address", jsonObject.getString("address"));
System.out.println(jsonObject.toString());
cloudSolrClient.add(doc);
cloudSolrClient.commit();
}
//向solrCloud上创建多个个索引
public void addIndexs(JSONArray jsonArray) throws Exception, IOException {
List<SolrInputDocument> docs = new ArrayList<SolrInputDocument>();
for(int i=0;i<jsonArray.size();i++){
JSONObject jsonObject = jsonArray.getJSONObject(i);
SolrInputDocument doc = new SolrInputDocument();
doc.addField("name", jsonObject.getString("name"));
doc.addField("id", UUID.randomUUID().toString());
doc.addField("phoneType", jsonObject.getString("phoneType"));
doc.addField("price", jsonObject.getString("price"));
docs.add(doc);
}
cloudSolrClient.add(docs);
// 提交
cloudSolrClient.commit();
}
//向solrCloud上创建多个对象的索引
public void addIndexs(List<Phone> phoneList) throws Exception, IOException {
cloudSolrClient.addBeans(phoneList);
cloudSolrClient.commit();
}
//向solrCloud上创建一个对象的索引
public void addIndex(Phone phone) throws Exception, IOException {
// 创建一个文档
//添加的Field 必须是在schema.xml 中配置了,不然就报错
// 创建文档2
cloudSolrClient.addBean(phone);
// 提交
cloudSolrClient.commit();
}
//通过指定的id删除solrCloud上的索引
public void delete(List<String> ids) throws Exception {
cloudSolrClient.deleteById(ids);
cloudSolrClient.commit(); //提交
}
//通过指定的域值删除数据
public void deleteByField(String fieldName,String fieldValue) throws Exception {
String query = fieldName + ":" + fieldValue;
cloudSolrClient.deleteByQuery(query);
cloudSolrClient.commit(); //提交
}
//通过指定的域值更新数据
public void updateByField(String id,String fieldName,String fieldValue) throws Exception {
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", id);
doc.addField(fieldName, fieldValue);
cloudSolrClient.add(doc);
cloudSolrClient.commit(); //提交
}
//查询索引
public void query(String fieldName,String fieldValue, Integer page, Integer rows,String fieldSort) throws Exception {
//添加查询
SolrQuery solrQuery = new SolrQuery();
//查询
//代表Solr控制界面Query中的p字段 ,查询字符串,这个是必须的。
//如果查询所有*:* ,根据指定字段查询(Name:张三 AND Address:北京)
//solrQuery.set(fieldName, fieldValue);
solrQuery.setQuery(fieldName+":"+fieldValue);
//代表fq字段 ,(filter query)过虑查询,作用:在q查询符合结果中同时是fq查询符合的,
//例如:q=Name:张三&fq=CreateDate:[20081001 TO 20091031],找关键字mm,并且CreateDate是20081001
// solrQuery.addFilterQuery("itemName:yby");
//指定返回那些字段内容,用逗号或空格分隔多个。
solrQuery.addField("id,phoneType,price");
//按照指定的字段排序
solrQuery.setSort(fieldSort, ORDER.asc);
// 设置分页 start=0就是从0开始,,rows=5当前返回5条记录,第二页就是变化start这个值为5就可以了。
solrQuery.setStart((Math.max(page, 1) - 1) * rows);
solrQuery.setRows(rows);
// 设置高亮
solrQuery.setHighlight(true); // 开启高亮组件
solrQuery.addHighlightField("id");// 高亮字段
solrQuery.setHighlightSimplePre("<em>");// 标记,高亮关键字前缀
solrQuery.setHighlightSimplePost("</em>");// 后缀
//获取查询结果
QueryResponse response = cloudSolrClient.query(solrQuery);
//获取查询到的文档
SolrDocumentList docs = response.getResults();
//查询到的条数
long cnt = docs.getNumFound();
System.out.println("查询到的条数\t"+cnt);
//获取查询结果
for(SolrDocument doc :docs) {
String id = doc.get("id").toString();
System.out.printf("%s\r\n",id);
String phoneType = doc.get("phoneType").toString();
System.out.printf("%s\r\n",phoneType);
}
}
}