solrCloud中的分佈式請求響應超時解決方案

    之前一篇博客中寫道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來記錄日誌,而不影響前臺的展示。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章