1. 前言
在本节中,我们将探讨 Elasticsearch 如何实现路由注册,并深入分析其 Search 流程的源码。通过了解路由注册的工作原理,我们可以更好地理解 Elasticsearch 的请求处理和数据检索机制。
2. 路由注册
在前面几章的学习中,我们知道 1 次 search 请求,涉及到协调节点接收用户发起的 http 请求,以及内部协调节点将请求转发至数据节点。
2.1 Http 请求路由的注册
请求首先会被 Netty4HttpServerTransport 接收,接着交由 RestController 进行路由分发。
private void tryAllHandlers(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) throws Exception {
// 从 tier 树中,找到该请求路径对应的 RestHandler
Iterator<MethodHandlers> allHandlers = getAllHandlers(request.params(), rawPath);
while (allHandlers.hasNext()) {
final RestHandler handler;
final MethodHandlers handlers = allHandlers.next();
if (handlers == null) {
handler = null;
} else {
handler = handlers.getHandler(requestMethod, restApiVersion);
}
if (handler == null) {
if (handleNoHandlerFound(rawPath, requestMethod, uri, channel)) {
return;
}
} else {
// 找到后,将本次请求转发给该 RestHandler
dispatchRequest(request, channel, handler, threadContext);
return;
}
}
}
RestHandler 与路由的对应关系是在 Node 节点启动的时候注册的。
在 Node 初始化时,会执行 ActionModule#initRestHandlers(...)
public void initRestHandlers(Supplier<DiscoveryNodes> nodesInCluster) {
... 其他路由
...
// 注册 Search 路由
registerHandler.accept(new RestSearchAction());
...
... 其他路由
}
2.2 内部请求路由注册
协调节点内部转发的路由注册是在节点启动时发生的。
负责注册函数为:ActionModule#setupActions(...)
每一个路径,都有一个与之对应的 TransportAction
在本章中,分析的是最常用的 search 流程。该路径,由 TransportSearchAction 负责处理
actions.register(SearchAction.INSTANCE, TransportSearchAction.class);
TransportAction#execute(...) 定义了执行的基本流程
每个子类,自行实现 doExecute(...) 方法,实现自定义逻辑。
这种设计模式叫做 模板方法,日常开发中也相对比较常用,可以学习借鉴别人的封装。
每个 TransportAction 类的构造函数,都被 @Inject 标注。
该注解的作用为:在启动时,会实例化 TransportAction 对象,并自动注入每个 TransportAction 所需要的参数。
ES 实现了自动注入的逻辑,代码入口为 InjectorBuilder#build()
从 ES 整体目录来看,所有的 Action,即路由处理器类,都在 org.elasticsearch.action 包下。
整体包规划,非常清晰。我们可以从 org.elasticsearch.action.search 目录下轻松找到我们想了解的处理类。
再者是:在阅读源码的过程中,你会发现 Elasticsearch 分层思路跟我们平时开发用的 MVC 思路类似。 每个 Action 类似 Application 层。
每个 Action 都会包含非常多的 Service。Action 会组织这些 Service 拼接业务逻辑。
每个 Service 会包含非常多底层文件存储类。也就是我们熟悉的 Repository。 这些底层文件存储类,封装了底层文件的增删改查操作。
是不是和我们的 MVC 分层开发思路很像呢?
3. Search 流程
3.1 协调节点转发 Query 请求
TransportSearchAction#doExecute(...) 方法封装了核心的执行流程,所以我们直接看该函数的实现。
executeRequest 方法的执行逻辑大致如下
- 获取分片在哪些集群上
- 如果不存在跨集群搜索,则走本地集群搜索,否则走跨集群搜索。一般来说,日常开发都是本地集群搜索,所以这里着重讲解本地搜索。
executeLocalSearch 方法会调用 executeSearch 方法。 可以注意下,this:searchAsynchAction 被传入了 executeSearch 方法
该方法的逻辑大致如下:
- 获取需要向哪几个分片发起请求
- 设置当前请求在哪个线程池上运行,准备执行 query 阶段。
private void executeSearch(
SearchTask task,
SearchTimeProvider timeProvider,
SearchRequest searchRequest,
OriginalIndices localIndices,
List<SearchShardIterator> remoteShardIterators,
BiFunction<String, String, DiscoveryNode> remoteConnections,
ClusterState clusterState,
Map<String, AliasFilter> remoteAliasMap,
ActionListener<SearchResponse> listener,
SearchResponse.Clusters clusters,
@Nullable SearchContextId searchContext,
SearchAsyncActionProvider searchAsyncActionProvider
) {
final String[] concreteLocalIndices;
if (searchContext != null) {
...
...
} else {
// 这整个 else 逻辑都是为了获取,应该向哪几个分片发起请求。
// 获取可用的索引,因为传入的 localIndices 只是一个表达式,例如可能传入 test*,因此这里需要转换为真正的索引。
final Index[] indices = resolveLocalIndices(localIndices, clusterState, timeProvider);
// 解析搜索路由,如果索引表达式中存在别名,则会解析出别名对应的实际索引
Map<String, Set<String>> routingMap = indexNameExpressionResolver.resolveSearchRouting(
clusterState,
searchRequest.routing(),
searchRequest.indices()
);
routingMap = routingMap == null ? Collections.emptyMap() : Collections.unmodifiableMap(routingMap);
concreteLocalIndices = new String[indices.length];
for (int i = 0; i < indices.length; i++) {
concreteLocalIndices[i] = indices[i].getName();
}
Map<String, Long> nodeSearchCounts = searchTransportService.getPendingSearchRequests();
// 获取索引在哪些分片上
GroupShardsIterator<ShardIterator> localShardRoutings = clusterService.operationRouting()
.searchShards(
clusterState,
concreteLocalIndices,
routingMap,
searchRequest.preference(),
searchService.getResponseCollectorService(),
nodeSearchCounts
);
// 将当前请求的表达式解析为索引或者别名
final Set<String> indicesAndAliases = indexNameExpressionResolver.resolveExpressions(clusterState, searchRequest.indices());
aliasFilter = buildPerIndexAliasFilter(clusterState, indicesAndAliases, indices, remoteAliasMap);
final Map<String, OriginalIndices> finalIndicesMap = buildPerIndexOriginalIndices(
clusterState,
indicesAndAliases,
indices,
searchRequest.indicesOptions()
);
// 获取要发起请求的目标分片
localShardIterators = StreamSupport.stream(localShardRoutings.spliterator(), false).map(it -> {
OriginalIndices finalIndices = finalIndicesMap.get(it.shardId().getIndex().getUUID());
assert finalIndices != null;
return new SearchShardIterator(searchRequest.getLocalClusterAlias(), it.shardId(), it.getShardRoutings(), finalIndices);
}).collect(Collectors.toList());
}
// 合并跨集群搜索的分片和本地搜索的分片
final GroupShardsIterator<SearchShardIterator> shardIterators = mergeShardsIterators(localShardIterators, remoteShardIterators);
failIfOverShardCountLimit(clusterService, shardIterators.size());
if (searchRequest.getWaitForCheckpoints().isEmpty() == false) {
if (remoteShardIterators.isEmpty() == false) {
throw new IllegalArgumentException("Cannot use wait_for_checkpoints parameter with cross-cluster searches.");
} else {
validateAndResolveWaitForCheckpoint(clusterState, indexNameExpressionResolver, searchRequest, concreteLocalIndices);
}
}
// 解析索引权重
Map<String, Float> concreteIndexBoosts = resolveIndexBoosts(searchRequest, clusterState);
// optimize search type for cases where there is only one shard group to search on
if (shardIterators.size() == 1) {
// if we only have one group, then we always want Q_T_F, no need for DFS, and no need to do THEN since we hit one shard
searchRequest.searchType(QUERY_THEN_FETCH);
}
if (searchRequest.isSuggestOnly()) {
// disable request cache if we have only suggest
searchRequest.requestCache(false);
switch (searchRequest.searchType()) {
case DFS_QUERY_THEN_FETCH ->
// convert to Q_T_F if we have only suggest
searchRequest.searchType(QUERY_THEN_FETCH);
}
}
final DiscoveryNodes nodes = clusterState.nodes();
// 返回 lambda 方法,获取 node 对应的连接,在 SearchQueryThenFetchAsyncAction#executePhaseOnShard 会执行 lambda 内的逻辑
BiFunction<String, String, Transport.Connection> connectionLookup = buildConnectionLookup(
searchRequest.getLocalClusterAlias(),
nodes::get,
remoteConnections,
searchTransportService::getConnection
);
// 获取执行业务的线程池
final Executor asyncSearchExecutor = asyncSearchExecutor(concreteLocalIndices);
final boolean preFilterSearchShards = shouldPreFilterSearchShards(
clusterState,
searchRequest,
concreteLocalIndices,
localShardIterators.size() + remoteShardIterators.size(),
defaultPreFilterShardSize
);
// 异步执行
// searchAsyncActionProvider 是最早传入的 this:searchAsynchAction 方法
searchAsyncActionProvider.asyncSearchAction(
task,
searchRequest,
asyncSearchExecutor,
shardIterators,
timeProvider,
connectionLookup,
clusterState,
Collections.unmodifiableMap(aliasFilter),
concreteIndexBoosts,
listener,
preFilterSearchShards,
threadPool,
clusters
).start();
}
ES 的 search 流程包含 2 部分,1部分是 query,1部分是 fetch。 执行完 searchAsyncActionProvider.asyncSearchAction(...).start() 代码会走到 AbstractSearchAsyncAction#run()
@Override
public final void run() {
for (final SearchShardIterator iterator : toSkipShardsIts) {
assert iterator.skip();
skipShard(iterator);
}
if (shardsIts.size() > 0) {
...
...
// 遍历所有分片,异步发起 query 请求
for (int i = 0; i < shardsIts.size(); i++) {
final SearchShardIterator shardRoutings = shardsIts.get(i);
assert shardRoutings.skip() == false;
assert shardIndexMap.containsKey(shardRoutings);
int shardIndex = shardIndexMap.get(shardRoutings);
// 发起 query 请求
performPhaseOnShard(shardIndex, shardRoutings, shardRoutings.nextOrNull());
}
}
}
接着代码会走到 SearchQueryThenFetchAsyncAction#executePhaseOnShard(...),准备发起 query 查询
protected void executePhaseOnShard(
final SearchShardIterator shardIt,
final SearchShardTarget shard,
final SearchActionListener<SearchPhaseResult> listener
) {
ShardSearchRequest request = rewriteShardSearchRequest(super.buildShardSearchRequest(shardIt, listener.requestIndex));
// 这里的 getConnection() 会执行在 TransportSearchAction#executeSearch() 定义的 lambda => connectionLookup
getSearchTransport().sendExecuteQuery(getConnection(shard.getClusterAlias(), shard.getNodeId()), request, getTask(), listener);
}
需要注意的是:executePhaseOnShard() 方法入参 listener 用于处理执行 query 后的回调。
最后代码会走到 TransportService#sendRequest(final Transport.Connection connection,final String action,final TransportRequest request,final TransportRequestOptions options,final TransportResponseHandler<T> handler)
最后再由该方法向数据节点发起异步请求
3.2 数据节点接收 Query 请求
TransportSearchAction 在初始化时,就会调用 SearchTransportService#registerRequestHandler(...) 注册 RPC 请求处理函数。
数据节点会在 SearchService#executeQueryPhase(ShardSearchRequest request, SearchShardTask task, ActionListener<SearchPhaseResult> listener) 方法中执行 query。
query 结果回调处理,则是由 AbstractSearchAsyncAction#performPhaseOnShard(final int shardIndex, final SearchShardIterator shardIt, final SearchShardTarget shard) 方法的 SearchActionListener 处理
3.3 协调节点转发 Fetch 请求
协调节点收到 query 结果回调后,会进入 fetch 阶段,异步向数据节点发起 fetch 请求。 FetchSearchPhase#innerRun() -> FetchSearchPhase#executeFetch(...)
需要注意的是,FetchSearchPhase#innerRun() 创建了一个 finishPhase Runnable,在执行完 fetch 后,结果会回调至该方法。
3.4 数据节点接收 Fetch 请求
请求会被 SearchService#executeFetchPhase(ShardFetchRequest request, SearchShardTask task, ActionListener<FetchSearchResult> listener) 处理,因为 SearchTransportService#registerRequestHandler(...) 注册了 fetch 请求对应的处理类。
3.5 协调节点返回最终结果
fetch 执行完毕后,结果会返回至协调节点,接着协调节点会执行 finishPhase FetchSearchPhase#innerRun()
接着执行 ExpandSearchPhase#run(...) 方法,并返回 fetch 结果
最后由 Netty4HttpResponseCreator#encode() 消息编码后,再将消息返回至客户端