【Elasticsearch源码】查询源码分析(二)

接上一篇:【Elasticsearch源码】查询源码分析(一)

上篇讲到请求通过遍历每个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的查询结果)。

后续再补充相关的查询细节。

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值