solr搭建分佈式搜索引擎

SolrCloud詳解

SolrCloud的基本概念

一、Cluster集羣:Cluster是一組Solr節點,邏輯上做爲一個單元進行管理,整個集羣必須使用同一套schema和SolrConfig。java

二、Node節點:一個運行Solr的JVM實例。node

三、Collection:在SolrCloud集羣中邏輯意義上的完整的索引,經常被劃分爲一個或多個Shard,這些Shard使用相同的Config Set,若是Shard數超過一個,那麼索引方案就是分佈式索引。SolrCloud容許客戶端用戶經過Collection名稱引用它,這樣用戶不須要關心分佈式檢索時須要使用的和Shard相關參數。算法

四、Core: 也就是Solr Core,一個Solr中包含一個或者多個Solr Core,每一個Solr Core能夠獨立提供索引和查詢功能,Solr Core的提出是爲了增長管理靈活性和共用資源。SolrCloud中使用的配置是在Zookeeper中的,而傳統的Solr Core的配置文件是在磁盤上的配置目錄中。apache

五、Config Set: Solr Core提供服務必須的一組配置文件,每一個Config Set有一個名字。最小須要包括solrconfig.xml和schema.xml,除此以外,依據這兩個文件的配置內容,可能還須要包含其它文件,如中文索引須要的詞庫文件。Config Set存儲在Zookeeper中,能夠從新上傳或者使用upconfig命令進行更新,可以使用Solr的啓動參數bootstrap_confdir進行初始化或更新。json

六、Shard分片: Collection的邏輯分片。每一個Shard被分紅一個或者多個replicas,經過選舉肯定哪一個是Leader。bootstrap

七、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。服務器

八、 Leader: 贏得選舉的Shard replicas,每一個Shard有多個Replicas,這幾個Replicas須要選舉來肯定一個Leader。選舉能夠發生在任什麼時候間,可是一般他們僅在某個Solr實例發生故障時纔會觸發。當進行索引操做時,SolrCloud會將索引操做請求傳到此Shard對應的leader,leader再分發它們到所有Shard的replicas。網絡

九、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,這樣作的好處有如下兩點:

一、CloudSolrServer獲取到SolrCloud的狀態後,它可直接將document發往SolrCloud的leader,從而下降網絡轉發消耗。

二、註冊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值的獲取提出瞭如下兩個要求:

一、hash計算速度必須快,由於hash計算是分佈式建索引的第一步。

二、 hash值必須能均勻的分佈於每個shard,若是有一個shard的document數量大於另外一個shard,那麼在查詢的時候前一個shard所花的時間就會大於後一個,SolrCloud的查詢是先分後彙總的過程,也就是說最後每個shard查詢完畢纔算完畢,因此SolrCloud的查詢速度是由最慢的shard的查詢速度決定的。

基於以上兩點,SolrCloud採用了MurmurHash 算法以提升hash計算速度和hash值的均勻分佈。

更新索引:

一、 Leader接受到update請求後,先將update信息存放到本地的update log,同時Leader還會給document分配新的version,對於已存在的document,若是新的版本高就會拋棄舊版本,最後發送至replica。

二、一旦document通過驗證以及加入version後,就會並行的被轉發至全部上線的replica。SolrCloud並不會關注那些已經下線的replica,由於當他們上線時候會有recovery進程對他們進行恢復。若是轉發的replica處於recovering狀態,那麼這個replica就會把update放入updatetransaction 日誌。

三、當leader接受到全部的replica的反饋成功後,它纔會反饋客戶端成功。只要shard中有一個replica是active的,Solr就會繼續接受update請求。這一策略實際上是犧牲了一致性換取了寫入的有效性。這其中有一個重要參數:leaderVoteWait參數,它表示當只有一個replica時候,這個replica會進入recovering狀態並持續一段時間等待leader的從新上線。若是在這段時間內leader沒有上線,那麼他就會轉成leader,其中可能會有一些document丟失。固然可使用majority quorum來避免這個狀況,這跟Zookeeper的leader選舉策略同樣,好比當多數的replica下線了,那麼客戶端的write就會失敗。

四、索引的commit有兩種,一種是softcommit,即在內存中生成segment,document是可見的(可查詢到)可是沒寫入磁盤,斷電後數據會丟失。另外一種是hardcommit,直接將數據寫入磁盤且數據可見。

SolrCloud索引的檢索

一、用戶的一個查詢,能夠發送到含有該Collection的任意Solr的Server,Solr內部處理的邏輯會轉到一個Replica。

二、此Replica會基於查詢索引的方式,啓動分佈式查詢,基於索引的Shard的個數,把查詢轉爲多個子查詢,並把每一個子查詢定位到對應Shard的任意一個Replica。

三、每一個子查詢返回查詢結果。

四、最初的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。

一、搭建zookeeper集羣步驟:須要三臺zookeeper、分別是zk一、zk二、zk3,對應的端口分別爲192.168.121.12:2181:192.168.121.12:2182:192.168.121.14:2181。

二、搭建solrcloud集羣步驟:

  1)搭建四個單機版的solr服務對應的jetty,端口分別是192.168.121.12:8080、192.168.121.12:808一、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的集合配置文件的名稱

三、啓動全部的solr服務、啓動全部的zookeeper服務。

四、訪問部署的solr集羣中任意的端口服務。

  

五、建立索引集合指令:

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:引用的集合配置文件的名稱。

六、刪除索引集合指令:

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);
	        }
	    }
}
相關文章
相關標籤/搜索