【Elasticsearch源碼】查詢源碼分析(一)

1 前言

前面分析過ES的寫入流程源碼,詳情見【Elasticsearch源碼】寫入源碼分析

Elasticsearch(ES)的查詢接口具有分佈式的數據檢索、聚合分析能力,數據檢索能力用於支持全文檢索、日誌分析等場景,如Github平臺上的代碼搜索、基於ES的各類日誌分析服務等;聚合分析能力用於支持指標分析、APM等場景,如監控場景、應用的日活/留存分析等。

本文基於6.7.1版本,主要分析ES的分佈式執行框架及查詢主體流程,探究ES如何實現分佈式查詢、數據檢索、聚合分析等能力。

2 查詢基本流程

圖片來自官網,源代碼取自6.7.1版本:

在這裏插入圖片描述

  1. 客戶端可以將查詢發送到任意節點,接收到查詢的節點會作爲該查詢的協調節點;
  2. 協調節點解析查詢語句,向對應數據分片分發查詢子任務;
  3. 各個分片將本地的查詢結果返回給協調節點,進過協調節點匯聚後返回給客戶端。

如圖所示:客戶端將請求發送到Node3節點,Node3節點進行查詢解析之後,將請求分發到0號和1號分片所在的Node2和Node1,兩者再講各地分片的查詢數據返回至Node3,最終Node3進行匯聚,然後返回客戶端。

從實際的實現來看,協調節點的處理邏輯遠比上述流程複雜,不同類型的查詢對應的協調節點的處理邏輯有一定的差別。

下面先來介紹下常見的2類查詢,在之前的版本有3類查詢:DFS_QUERY_THEN_FETCH、QUERY_AND_FETCH和QUERY_THEN_FETCH,5.3版本之後,QUERY_AND_FETCH已經被移除。

2.1 DFS_QUERY_THEN_FETCH

搜索裏面有一種算分邏輯是根據TF(Term Frequency)和DF(Document Frequency)計算基礎分,但是Elasticsearch中查詢的時候,是在每個Shard中獨立查詢的,每個Shard中的TF和DF也是獨立的,雖然在寫入的時候通過_routing保證Doc分佈均勻,但是沒法保證TF和DF均勻,那麼就有會導致局部的TF和DF不準的情況出現,這個時候基於TF、DF的算分就不準。

爲了解決這個問題,Elasticsearch中引入了DFS查詢,比如DFS_query_then_fetch,會先收集所有Shard中的TF和DF值,然後將這些值帶入請求中,再次執行query_then_fetch,這樣算分的時候TF和DF就是準確的。

2.2 QUERY_THEN_FETCH

ES默認的查詢方式,在查詢的過程中,分爲query和fetch兩個階段:

Query Phase: 進行分片粒度的數據檢索和聚合,注意此輪調度僅返回文檔id集合,並不返回實際數據。

協調節點:解析查詢後,向目標數據分片發送查詢命令。
數據節點:在每個分片內,按照過濾、排序等條件進行分片粒度的文檔id檢索和數據聚合,返回結果。

Fetch Phase: 生成最終的檢索、聚合結果。

協調節點:歸併Query Phase的結果,得到最終的文檔id集合和聚合結果,並向目標數據分片發送數據抓取命令。
數據節點:按需抓取實際需要的數據內容。

3 查詢源碼流程分析

這裏以默認的QUERY_THEN_FETCH查詢爲例:

3.1 查詢請求入口

這一塊邏輯和所有的ES請求的處理是類似的,可以參考bulk請求的過程。以Rest請求爲例:

在這裏插入圖片描述
Rest分發由RestController模塊完成。在ES節點啓動時,會加載所有內置請求的Rest Action,並把對應請求的Http路徑和Rest Action作爲<Path, RestXXXAction>二元組註冊到RestController中。這樣對於任意的Rest請求,RestController模塊只需根據Http路徑,即可輕鬆找到對應的Rest Action進行請求分發。RestSearchAction的註冊樣例如下:

    public RestSearchAction(Settings settings, RestController controller) {
        super(settings);
        controller.registerHandler(GET, "/_search", this);
        controller.registerHandler(POST, "/_search", this);
        controller.registerHandler(GET, "/{index}/_search", this);
        controller.registerHandler(POST, "/{index}/_search", this);
        controller.registerHandler(GET, "/{index}/{type}/_search", this);
        controller.registerHandler(POST, "/{index}/{type}/_search", this);
    }

Rest層用於解析Http請求參數,RestRequest解析並轉化爲SearchRequest,然後再對SearchRequest做處理,這塊的邏輯在prepareRequest方法中,部分代碼如下:

    public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
    	//根據RestRequest構建SearchRequest
        SearchRequest searchRequest = new SearchRequest();
        IntConsumer setSize = size -> searchRequest.source().size(size);
        request.withContentOrSourceParamParserOrNull(parser ->
            parseSearchRequest(searchRequest, request, parser, setSize));
		//處理SearchRequest
        return channel -> client.search(searchRequest, new RestStatusToXContentListener<>(channel));
    }

NodeClient在處理SearchRequest請求時,會將請求的action轉化爲對應Transport層的action,然後再由Transport層的action來處理SearchRequest,action轉化的代碼如下:

    public <    Request extends ActionRequest,
                Response extends ActionResponse
            > Task executeLocally(GenericAction<Request, Response> action, Request request, TaskListener<Response> listener) {
        return transportAction(action).execute(request, listener);
    }

    private <    Request extends ActionRequest,
                Response extends ActionResponse
            > TransportAction<Request, Response> transportAction(GenericAction<Request, Response> action) {
        .....
        //actions是個action到transportAction的Map,這個映射關係是在節點啓動時初始化的
        TransportAction<Request, Response> transportAction = actions.get(action);
        ......
        return transportAction;
    }

然後進入TransportAction,TransportAction#execute(Request request, ActionListener listener) -> TransportAction#execute(Task task, Request request, ActionListener listener) -> TransportAction#proceed(Task task, String actionName, Request request, ActionListener listener)。TransportAction會調用一個請求過濾鏈來處理請求,如果相關的插件定義了對該action的過濾處理,則先會執行插件的處理邏輯,然後再進入TransportAction的處理邏輯,過濾鏈的處理邏輯如下:

        public void proceed(Task task, String actionName, Request request, ActionListener<Response> listener) {
            int i = index.getAndIncrement();
            try {
                if (i < this.action.filters.length) {
                	//應用插件的邏輯
                    this.action.filters[i].apply(task, actionName, request, listener, this);
                } else if (i == this.action.filters.length) {
                	//執行TransportAction的邏輯
                    this.action.doExecute(task, request, listener);
                } else {
                   ......
                }
            } catch(Exception e) {
               .....
            }
        }

對於Search請求,這裏的TransportAction對應的具體對象是TransportSearchAction的實例,到此,Rest層轉化爲Transport層的流程完成,下節將詳細介紹TransportSearchAction的處理邏輯。

3.1 查詢請求分發

代碼入口:TransportSearchAction#doExecute。

首先解析獲取了查詢涉及的具體索引列表,包括遠程集羣和本地集羣(遠程集羣用於跨集羣訪問):

            final ClusterState clusterState = clusterService.state();
            //獲取遠程集羣indices列表
            final Map<String, OriginalIndices> remoteClusterIndices = remoteClusterService.groupIndices(searchRequest.indicesOptions(),
                searchRequest.indices(), idx -> indexNameExpressionResolver.hasIndexOrAlias(idx, clusterState));
            //獲取本地集羣indices列表
            OriginalIndices localIndices = remoteClusterIndices.remove(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY);
            if (remoteClusterIndices.isEmpty()) {
                executeSearch((SearchTask)task, timeProvider, searchRequest, localIndices, Collections.emptyList(),
                    (clusterName, nodeId) -> null, clusterState, Collections.emptyMap(), listener,
                    clusterState.getNodes().getDataNodes().size(), SearchResponse.Clusters.EMPTY);
            } else {
            	//遠程集羣的處理邏輯
				.....
            }

然後進入executeSearch方法,構造目的shard列表,我們可以看到:

  1. red狀態下可以查詢;
  2. 默認需要查詢目標索引的所有分片;
  3. 默認採用QUERY_THEN_FETCH查詢方式;
  4. 併發查詢分片數最大256;
  5. 默認是不開啓請求緩存的。
    private void executeSearch(....) {
		// red狀態也可以查詢
        clusterState.blocks().globalBlockedRaiseException(ClusterBlockLevel.READ);
		......
		// 結合routing信息、preference信息,構造目的shard列表
        GroupShardsIterator<ShardIterator> localShardsIterator = clusterService.operationRouting().searchShards(clusterState,
                concreteIndices, routingMap, searchRequest.preference(), searchService.getResponseCollectorService(), nodeSearchCounts);
        GroupShardsIterator<SearchShardIterator> shardIterators = mergeShardsIterators(localShardsIterator, localIndices,
            searchRequest.getLocalClusterAlias(), remoteShardIterators);
        .....
        if (shardIterators.size() == 1) {
        	// 只有一個分片的時候,默認就是QUERY_THEN_FETCH,不存在評分不一致的問題
            searchRequest.searchType(QUERY_THEN_FETCH);
        }
        if (searchRequest.allowPartialSearchResults() == null) {
           // 用戶未定義首選項,採用默認方式
            searchRequest.allowPartialSearchResults(searchService.defaultAllowPartialSearchResults());
        }
        if (searchRequest.isSuggestOnly()) {
            // 默認是沒有開啓請求緩存的
            searchRequest.requestCache(false);
            switch (searchRequest.searchType()) {
                case DFS_QUERY_THEN_FETCH:
                    // 默認情況下DFS_QUERY_THEN_FETCH會轉化成QUERY_THEN_FETCH
                    searchRequest.searchType(QUERY_THEN_FETCH);
                    break;
            }
        }
        .....
        // 最大併發分片數,最大是256:Math.min(256, Math.max(nodeCount, 1)* IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.getDefault(Settings.EMPTY))
        setMaxConcurrentShardRequests(searchRequest, nodeCount);
        boolean preFilterSearchShards = shouldPreFilterSearchShards(searchRequest, shardIterators);
        // 生成查詢請求的調度類searchAsyncAction並啓動調度執行
        searchAsyncAction(task, searchRequest, shardIterators, timeProvider, connectionLookup, clusterState.version(),
            Collections.unmodifiableMap(aliasFilter), concreteIndexBoosts, routingMap, listener, preFilterSearchShards, clusters).start();
    }

然後進入了SearchPhase的實現類InitialSearchPhase的run方法:基於shard進行遍歷,向shard所在節點發送查詢請求,如果列表中有N個shard位於同一個節點,則向其發送N次請求,並不會把請求合併成一個。

    public final void run() throws IOException {
        .....
        if (shardsIts.size() > 0) {
        	// 最大分片請求數可以通過max_concurrent_shard_requests參數配置,6.5之後版本新增參數
            int maxConcurrentShardRequests = Math.min(this.maxConcurrentShardRequests, shardsIts.size());
            ....
            for (int index = 0; index < maxConcurrentShardRequests; index++) {
                final SearchShardIterator shardRoutings = shardsIts.get(index);
                // 執行shard級請求
                performPhaseOnShard(index, shardRoutings, shardRoutings.nextOrNull());
            }
        }
    }

shardsIts是本次查詢涉及的所有分片,shardRoutings.nextOrNull()從某個分片中主或者所有副本中選一個。

接下一篇:查詢源碼分析(二)

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