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