之前一篇博客中寫道solrCloud對查詢的請求是在服務端進行的組裝,是對所有的shard的所有的replica進行的輪訓的。這兩天看了下在服務端solr是如何進行操作的,這裏涉及到的代碼超級多,我就只貼一部分,用來說明大意即可。
在將查詢請求發往到某個replica之後,先根據path找到某個requestHandler(我們這裏用select舉例),然後再用這個requestHandler中所有的searchComponent進行查詢操作,他的分佈式的操作就是體現在多個searchComponent中,每一個searchComponent不只是要完成它所存在的shard中的工作,還有其他shard中的工作,想當然這裏不會使用同步,也就是不會在當前的shard的任務完成之後纔會將請求轉發到其他的shard,最好是採取異步執行的方式,將某個任務交給線程池,然後繼續執行自己的任務,在執行完成後再處理其他的shard返回的數據。solr正是採取了後者——使用一個shardHandler用來轉發請求到其他的shard,然後異步的等待其他的shard的執行結果。在solrCloud中使用的shardHandler的實現類是HttpShardHandler,顧名思義,他是採用http的協議與其他的shard進行交互的,其他shard的操作結果返回到當前的shard中,然後再組裝最後的結果。在solrhome下我們可以發現有個solr.xml,裏面就有關於HttpShardHandler的配置,
<shardHandlerFactory name="shardHandlerFactory" class="HttpShardHandlerFactory"> <!--用於產生一個HttpShardHandler--> <int name="socketTimeout">${socketTimeout:600000}</int> <!--httpClient的socketTimeout--> <int name="connTimeout">${connTimeout:60000}</int> <!--httpClient的connectionTimeout--> </shardHandlerFactory>
HttpShardHandler發起http請求使用的是apache的httpClient,上面的兩個配置就是配置的httpClient的兩個超時時間(httpCLient有三個超時時間,詳情參看我的另一個博客)。想到這就會有很多的疑問,如果訪問的時候某個shard死掉了呢(zk中的session還沒有過期的情況),又或者他沒有死掉但是他的操作非常慢一直到超過上面配置的socketTimeout呢,這種情況下怎麼操作?但凡遇到這種情況,看源碼是最好的辦法,在httpShardHandler中,有個submit方法,他就是某個searchComponent添加任務到httpShardHandler的線程池中,我們看一下這個方法:
/**
* 第一個參數表示要發起的請求
* 第二個參數表示要發送到的shard的所有的replica的url,用|分隔
* 第三個參數表示請求的參數
*/
@Override
public void submit(final ShardRequest sreq, final String shard, final ModifiableSolrParams params) {
// do this outside of the callable for thread safety reasons
final List<String> urls = getURLs(sreq, shard);//獲得本次訪問的所有的url,一個shard有多個replica
Callable<ShardResponse> task = new Callable<ShardResponse>() {//將請求封裝爲一個可以異步執行的callable,最後返回的是一個ShardResponse
@Override
public ShardResponse call() throws Exception {
ShardResponse srsp = new ShardResponse();
if (sreq.nodeName != null) {
srsp.setNodeName(sreq.nodeName);
}
srsp.setShardRequest(sreq);
srsp.setShard(shard);
SimpleSolrResponse ssr = new SimpleSolrResponse();
srsp.setSolrResponse(ssr);
long startTime = System.nanoTime();
try {
params.remove(CommonParams.WT); // use default (currently javabin)
params.remove(CommonParams.VERSION);
QueryRequest req = makeQueryRequest(sreq, params, shard);
req.setMethod(SolrRequest.METHOD.POST);
// no need to set the response parser as binary is the default
// req.setResponseParser(new BinaryResponseParser());
// if there are no shards available for a slice, urls.size()==0
if (urls.size() == 0) {
// TODO: what's the right error code here? We should use the same thing when
// all of the servers for a shard are down.
throw new SolrException(SolrException.ErrorCode.SERVICE_UNAVAILABLE, "no servers hosting shard: " + shard);
}
if (urls.size() <= 1) {
String url = urls.get(0);
srsp.setShardAddress(url);
try (SolrClient client = new HttpSolrClient(url, httpClient)) {//如果只有一個url,也就是隻有一個replica,則直接用這個url發起http請求
ssr.nl = client.request(req);
}
} else {
LBHttpSolrClient.Rsp rsp = httpShardHandlerFactory.makeLoadBalancedRequest(req, urls);//如果有多個replica,則會進行負載均衡。
ssr.nl = rsp.getResponse();
srsp.setShardAddress(rsp.getServer());
}
} catch (ConnectException cex) {
srsp.setException(cex);
} catch (Exception th) { // 從這兩個catch可以發現,如果在執行的時候發生了任何的異常都會將異常封裝到srsp也就是最後的結果中,而不會拋出異常。
srsp.setException(th);
if (th instanceof SolrException) {
srsp.setResponseCode(((SolrException) th).code());
} else {
srsp.setResponseCode(-1);
}
}
ssr.elapsedTime = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
return transfomResponse(sreq, srsp, shard);
}
};
try {
if (shard != null) {
MDC.put("ShardRequest.shards", shard);
}
if (urls != null && !urls.isEmpty()) {
MDC.put("ShardRequest.urlList", urls.toString());
}
pending.add(completionService.submit(task));//將封裝的任務添提交到completionService,由其他線程執行這個任務,等待執行結果,然後將最後的結果放到一個集合中(pending就是一個泛型是Future的集合)
} finally {
MDC.remove("ShardRequest.shards");
MDC.remove("ShardRequest.urlList");
}
}
看完上面的代碼就知道了原來果然是採用的異步執行,並且在執行過程中不會拋出任何的錯誤,如果有錯誤的也會封裝在結果中。然後我們再看一下取結果的時候的操作,下面的代碼摘抄於org.apache.solr.handler.component.SearchHandler.handleRequestBody(SolrQueryRequest, SolrQueryResponse)這個方法,
boolean tolerant = rb.req.getParams().getBool(ShardParams.SHARDS_TOLERANT, false); //從請求中得到shards.tolerant參數,默認是false
ShardResponse srsp = tolerant ? shardHandler1.takeCompletedIncludingErrors():shardHandler1.takeCompletedOrError();//得到線程池執行的結果
if (srsp == null) break; // no more requests to wait for
// Was there an exception?
if (srsp.getException() != null) { //如果有異常
// If things are not tolerant, abort everything and rethrow
if(!tolerant) {//如果沒有在參數中寫shards.tolerant=true,則報錯
shardHandler1.cancelAll();//取消所有的操作,
if (srsp.getException() instanceof SolrException) {
throw (SolrException)srsp.getException();
} else {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, srsp.getException());
}
} else {
if(rsp.getResponseHeader().get("partialResults") == null) {//如果是容錯的,也就是shards.tolerant=true,則不報錯,允許部分成功,然後再響應頭中添加一個值partialResults=true,表示這詞的請求是部分成功。
rsp.getResponseHeader().add("partialResults", Boolean.TRUE);
}
}
}
現在知道了如果在請求的時候害怕因爲某個shard響應太慢而耽誤太多的時間,則可以將httpShardHandler的兩個timeout配置的小一點,然後再請求中設置shards.tolerant=true,這樣就可以了。我測試的java代碼(我這次使用的是solr5.5.3):
static CloudSolrClient getServer(){
CloudSolrClient server = new CloudSolrClient("10.90.26.115:2181/solr5");
server.setZkConnectTimeout(10000*3);
return server;
}
static void queryTest() throws SolrServerException, IOException{
CloudSolrClient server = getServer();
SolrQuery query = new SolrQuery("id:?6");//我搜一下id是兩位數,並且是以6結尾的。
query.set("shards.tolerant", true);//設置允許出錯
QueryResponse response = server.query("你的集合的名字", query);//
System.out.println(response.getResults().getNumFound());
System.out.println(response.getResponseHeader().get("partialResults"));
}
執行上面的代碼,分三個階段,
第一個階段是將所有的shard都存活,可以發現打印的partialResults是null,
第二個是將某一個shard停掉,設置不容錯,即shards.tolerant=false,結果是報異常,提示某個shard沒有節點處理。
第三個是維持某一個shard停掉,設置shards.tolerant=true 可以發現不報錯了,但是numFound變少了,而且打印的是true。
至此,已經掌握容錯請求的實現。在實際生產中可以根據響應頭的partialResults來記錄日誌,而不影響前臺的展示。