1 前言
前面分析過ES的寫入流程源碼,詳情見【Elasticsearch源碼】寫入源碼分析。
Elasticsearch(ES)的查詢接口具有分佈式的數據檢索、聚合分析能力,數據檢索能力用於支持全文檢索、日誌分析等場景,如Github平臺上的代碼搜索、基於ES的各類日誌分析服務等;聚合分析能力用於支持指標分析、APM等場景,如監控場景、應用的日活/留存分析等。
本文基於6.7.1版本,主要分析ES的分佈式執行框架及查詢主體流程,探究ES如何實現分佈式查詢、數據檢索、聚合分析等能力。
2 查詢基本流程
圖片來自官網,源代碼取自6.7.1版本:
- 客戶端可以將查詢發送到任意節點,接收到查詢的節點會作爲該查詢的協調節點;
- 協調節點解析查詢語句,向對應數據分片分發查詢子任務;
- 各個分片將本地的查詢結果返回給協調節點,進過協調節點匯聚後返回給客戶端。
如圖所示:客戶端將請求發送到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列表,我們可以看到:
- red狀態下可以查詢;
- 默認需要查詢目標索引的所有分片;
- 默認採用QUERY_THEN_FETCH查詢方式;
- 併發查詢分片數最大256;
- 默認是不開啓請求緩存的。
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()從某個分片中主或者所有副本中選一個。
接下一篇:查詢源碼分析(二)。