上篇讲到请求通过遍历每个shard发送请求,执行executePhaseOnShard方法,转发请求的同时定义了一个Listener,用于监听处理结果。
private void performPhaseOnShard(final int shardIndex, final SearchShardIterator shardIt, final ShardRouting shard) {
final Thread thread = Thread.currentThread();
if (shard == null) {
.....
} else {
try {
executePhaseOnShard(shardIt, shard, new SearchActionListener<FirstResult>(
shardIt.newSearchShardTarget(shard.currentNodeId()), shardIndex) {
@Override
public void innerOnResponse(FirstResult result) {
// 执行收到成功的回复
maybeFork(thread, () -> onShardResult(result, shardIt));
}
@Override
public void onFailure(Exception t) {
// // 执行收到失败的回复
maybeFork(thread, () -> onShardFailure(shardIndex, shard, shard.currentNodeId(), shardIt, t));
}
});
} catch (final Exception e) {
.....
}
}
}
然后进入SearchQueryThenFetchAsyncAction#executePhaseOnShard,通过SearchTransportService的sendChildRequest方法向具体的分片发送Query阶段的子任务进行异步处理。
transportService.sendChildRequest(connection, QUERY_ACTION_NAME, request, task,
new ConnectionCountingHandler<>(handler, reader, clientConnections, connection.getNode().getId()));
3.2 收集查询结果
每个分片在执行完毕Query子任务后,通过节点间通信,回调祖父类InitialSearchPhase的onShardSuccess方法,把查询结果记录在协调节点保存的数组结构results中,并增加计数:
private void onShardResult(FirstResult result, SearchShardIterator shardIt) {
// 对收集到的结果进行合并
onShardSuccess(result);
// 检查是否所有请求都已经收到回复了
successfulShardExecution(shardIt);
}
public final void onShardSuccess(Result result) {
successfulOps.incrementAndGet();
results.consumeResult(result);
AtomicArray<ShardSearchFailure> shardFailures = this.shardFailures.get();
if (shardFailures != null) {
shardFailures.set(result.getShardIndex(), null);
}
}
private void successfulShardExecution(SearchShardIterator shardsIt) {
// 计数器累加
final int xTotalOps = totalOps.addAndGet(remainingOpsOnIterator);
// 检查是否收到全部回复
if (xTotalOps == expectedTotalOps) {
onPhaseDone();
} else if (xTotalOps > expectedTotalOps) {
throw new AssertionError(....);
} else if (shardsIt.skip() == false) {
maybeExecuteNext();
}
}
当返回结果的分片数等于预期的总分片数时,协调节点会进入当前Phase的结束处理,启动下一个阶段Fetch Phase的执行。注意,这里有个有意思的地方,ES中只需要一个分片执行成功,即会进行后续Phase处理得到部分结果,当然它会在结果中提示用户实际有多少分片执行成功。
onPhaseDone会调用executeNextPhase方法进入下一个阶段,从而开始进入Fetch 阶段。
3.3 Fetch阶段
代码入口:SearchQueryThenFetchAsyncAction#getNextPhase->FetchSearchPhase#innerRun
从查询阶段的shard列表中遍历,跳过查询结果为空的shard,对特定目标shard执行executeFetch方法来获取数据,其中包括分页信息。
private void executeFetch(...) {
// 发送请求
context.getSearchTransport().sendExecuteFetch(connection, fetchSearchRequest, context.getTask(),
new SearchActionListener<FetchSearchResult>(shardTarget, shardIndex) {
@Override
public void innerOnResponse(FetchSearchResult result) {
// 处理返回成功的消息
counter.onResult(result);
}
@Override
public void onFailure(Exception e) {
try {
// 处理返回失败的消息
counter.onFailure(shardIndex, shardTarget, e);
} finally {
releaseIrrelevantSearchContext(querySearchResult);
}
}
});
}
executeFetch的参数querySearchResult包含分页信息,同时定义了Listener,每成功获取一个shard数据之后就执行counter.onResult,其中调用对结果的处理回调,把结果保存到数组中,然后执行countDown。
void onResult(R result) {
try {
resultConsumer.accept(result);
} finally {
countDown();
}
}
3.3.1 收集结果
收集器定义在innerRun内,包括收到的shard数据存放在哪里,收集完成后谁来处理:
final CountedCollector<FetchSearchResult> counter = new CountedCollector<>(r -> fetchResults.set(r.getShardIndex(), r),docIdsToLoad.length, finishPhase, context);
fetchResults用于存储从某个shard收集的结果,每收到一个shard数据就执行一次counter.countDown()。当所有shard收集完成之后,countDown会触发执行finishPhase:
final Runnable finishPhase = ()
-> moveToNextPhase(searchPhaseController, scrollId, reducedQueryPhase, queryAndFetchOptimization ?
queryResults : fetchResults);
moveToNextPhase就是要执行下一个阶段,下一个阶段要执行的任务定义在FetchSearchPhase的构造函数里面,主要是触发
ExpandSearchPhase:
context.executeNextPhase(this, nextPhaseFactory.apply(internalResponse, scrollId));
FetchSearchPhase(InitialSearchPhase.SearchPhaseResults<SearchPhaseResult> resultConsumer,
SearchPhaseController searchPhaseController,
SearchPhaseContext context) {
this(resultConsumer, searchPhaseController, context,
(response, scrollId) -> new ExpandSearchPhase(context, response, // collapse only happens if the request has inner hits
(finalResponse) -> sendResponsePhase(finalResponse, scrollId, context)));
}
取回结果之后,进入ExpandSearchPhase#run,主要判断是否启用字段折叠(field collapsing),根据需要实现字段折叠,如果没有实现,直接返回给客户端。
Field Collapsing属于一类特殊的查询场景,这里不详细介绍。
ExpandSearchPhase的下一个阶段就是回复客户端了,在sendResponsePhase方法里面实现:
private static SearchPhase sendResponsePhase(InternalSearchResponse response, String scrollId, SearchPhaseContext context) {
return new SearchPhase("response") {
@Override
public void run() throws IOException {
context.onResponse(context.buildSearchResponse(response, scrollId));
}
};
3.4 数据节点的处理过程
前面讲了在协调节点,Query和Fetch阶段的处理流程,再来看看在数据节点的处理流程:
3.4.1 Query阶段
协调节点通过SearchTransportService的sendExecuteQuery()函数向目标数据节点发送
QUERY_ACTION_NAME类型的查询子任务,通过请求路径QUERY_ACTION_NAME可以在SearchTransportService中找到对应的处理函数SearchService.executeQueryPhase():
transportService.registerRequestHandler(QUERY_ACTION_NAME, ShardSearchTransportRequest::new,
ThreadPool.Names.SAME, false, true,
new TaskAwareTransportRequestHandler<ShardSearchTransportRequest>() {
@Override
//收到Query请求
public void messageReceived(ShardSearchTransportRequest request, TransportChannel channel, Task task) {
// 执行查询
searchService.executeQueryPhase(request, (SearchTask) task,
new ChannelActionListener<>(channel, QUERY_ACTION_NAME, request));
}
});
然后执行executeQueryPhase进入loadOrExecuteQueryPhase方法,先判断是否允许cache,默认是true,会把查询结果放到cache中,查询时优先从cache中取。这个cache由节点的所有分片共享,基于LRU算法实现:空间满的时候删除最近最少使用的数据。cache并不缓存全部检索结果。由index.requests.cahce.enable参数控制。
private void loadOrExecuteQueryPhase(final ShardSearchRequest request, final SearchContext context) throws Exception {
final boolean canCache = indicesService.canCache(request, context);
context.getQueryShardContext().freezeContext();
if (canCache) {
indicesService.loadIntoContext(request, context, queryPhase);
} else {
queryPhase.execute(context);
}
}
核心的查询封装在queryPhase.execute(context)中,其中调用了lucene实现搜索,同时实现聚合:
public void execute(SearchContext searchContext) throws QueryPhaseExecutionException {
.....
aggregationPhase.preProcess(searchContext);
final ContextIndexSearcher searcher = searchContext.searcher();
boolean rescore = execute(searchContext, searchContext.searcher(), searcher::setCheckCancelled);
if (rescore) { // only if we do a regular search
// 全文检索且需要评分
rescorePhase.execute(searchContext);
}
// 自动补全和纠错查询
suggestPhase.execute(searchContext);
// 实现聚合查询
aggregationPhase.execute(searchContext);
.....
}
execute(searchContext)方法首先从searchContext中获取查询参数和查询对象query,然后生产处理查询结果的collector,最终调用Lucene的IndexSearcher.search(…)函数进行查询,具体参考下面关键代码:QueryPhase#execute(SearchContext searchContext, final IndexSearcher searcher,Consumer checkCancellationSetter):
.....
// 获取参数和查询对象
queryResult.from(searchContext.from());
queryResult.size(searchContext.size());
Query query = searchContext.query();
.....
// 生成处理查询结果的queryCollector,在QueryCollectorContext类TopDocsCollectorContext类里面实现。
queryCollector = QueryCollectorContext.createQueryCollector(collectors);
//调用Lucene接口进行查询
searcher.search(query, queryCollector);
Query:查询对象用于指定查询条件,在分片内进行数据检索。
Collector:用于对查询结果进行处理,如shard级别的Top N处理,聚合计算等。
3.4.2 Fetch阶段
前面的Query阶段已经拿到了查询结果的文档ID,接下来根据ID在数据节点拿到最后的结果:
协调节点通过SearchTransportService的sendExecuteFetch(…)函数向目标数据节点发送Transport路径为FETCH_ID_ACTION_NAME的查询子任务,通过FETCH_ID_ACTION_NAME可以在SearchTransportService中找到对应的处理函数SearchService.executeFetchPhase(…):
transportService.registerRequestHandler(FETCH_ID_ACTION_NAME, ShardFetchSearchRequest::new,
ThreadPool.Names.SAME, true, true,
new TaskAwareTransportRequestHandler<ShardFetchSearchRequest>() {
@Override
// 收到Fetch请求
public void messageReceived(ShardFetchSearchRequest request, TransportChannel channel, Task task) {
// 执行Fetch请求
searchService.executeFetchPhase(request, (SearchTask)task,
new ChannelActionListener<>(channel, FETCH_ID_ACTION_NAME, request));
}
});
对Fetch响应的实现在SearchService#executeFetchPhase方法,核心是调用 fetchPhase.execute(context)方法,按照命中的doc取得相关的数据(source、store fields、highlight、docvalue fields等信息),填充到SearchHits中,最终封装到FetchSearchResult中。
for (FetchSubPhase fetchSubPhase : fetchSubPhases) {
fetchSubPhase.hitExecute(context, hitContext);
}
4 思考
本文主要分析了ES的分布式查询流程,对于查询的分词,计算等操作都是在Lucene底层实现。但是聚合是在ES中实现(基于Lucene的查询结果)。
后续再补充相关的查询细节。