Elasticsearch查询解析

2020-05-27 00:00:00 查询 执行 节点 请求 分片

1. 背景

Elasticsearch(ES)可用于全文检索、日志分析、指标分析、APM等众多场景,而且搭建部署容易,后期弹性扩容、故障处理简单。ES在一定程度上实现了一套系统支持多个场景的希望,大幅度降低使用多套专用系统的运维成本(当然ES不是的,不能满足事务等场景)。正是因为其通用性和易用性,ES自2010年发布版本以来得到爆发式的发展,广泛应用于各类互联网公司的不同业务场景。

ES的查询接口具有分布式的数据检索、聚合分析能力,数据检索能力用于支持全文检索、日志分析等场景,如Github平台上的代码搜索、基于ES的各类日志分析服务等;聚合分析能力用于支持指标分析、APM等场景,如监控场景、应用的日活/留存分析等。本文基于ES 5.6.4,主要分析ES的分布式执行框架及查询主体流程,探究ES如何实现分布式查询、数据检索、聚合分析等能力。

2. 分布式查询框架及类型

ES使用开源的Lucene作为存储引擎,它赋予ES高性能的数据检索能力,但Lucene仅仅是一个单机索引库。ES基于Lucene进行分布式封装,以支持集群管理、分布式查询、聚合分析等功能。从使用的直观感受看,ES按照下图方式实现了分布式查询:

  • 查询可发送到任意节点,接收到某查询的节点会作为该查询的协调节点(Coordinating Node)。
  • 协调节点解析查询,向对应数据分片分发查询子任务。
  • 各数据分片检索本地数据并返回协调节点,经汇聚处理后返回用户。

而从实现角度看,协调节点的调度逻辑实际远比上述流程复杂,不同查询对应的协调节点的处理逻辑有一定差别。下面我们先简单介绍ES中常见的3类查询:

2.1 QUERY_THEN_FETCH

这是常用的查询类型,可以完成大多数的分布式查询和聚合分析功能。在这类查询中,协调节点实际需要向其他节点分发两轮任务,也就说前面流程图描述的任务分发阶段(2&3)会有两轮,具体如下:

  • Query Phase:进行分片粒度的数据检索和聚合,注意此轮调度仅返回文档id集合,并不返回实际数据。
    • 协调节点:解析查询后,向目标数据分片发送查询命令。
    • 数据节点:在每个分片内,按照过滤、排序等条件进行分片粒度的文档id检索和数据聚合,返回结果。
  • Fetch Phase:生成终的检索、聚合结果。
    • 协调节点:归并Query Phase的结果,得到终的文档id集合和聚合结果,并向目标数据分片发送数据抓取命令。
    • 数据节点:按需抓取实际需要的数据内容。

2.2 QUERY_AND_FETCH

对于查询仅涉及单个分片的场景,ES会自动对查询流程做优化,在数据节点进行Query Phase的后,直接执行Fetch操作。此类查询为QUERY_AND_FETCH。通过去除一轮任务调度优化查询性能,优化过程由ES自动完成,用户不感知。

2.3 DFS_QUERY_THEN_FETCH

这类查询用于解决ES在多分片、少数据量的场景下计算相关度不准确的问题:以TF/IDF算法为例,ES在计算相关度时仅考虑单个分片内的IDF,可能导致查询结果中,类似的文档因为在不同分片而相关度大为不同的问题。此时可以使用此类查询,在QUERY_THEN_FETCH之前再增加一轮任务调度,用于计算分布式的IDF。但通常情况下,局部和全局IDF的差异会随着索引里文档数的增多渐渐消失,在真实世界的数据量下,这个问题几乎没有影响,没有必要使用此类查询增加一轮任务调度的开销。

关于这类问题的具体描述,可以参考如下文档:

  • 被破坏的相关度
  • How Shards Affect Relevance Scoring in Elasticsearch

3. 查询执行流程

本节我们深入到代码层面,以QUERY_THEN_FETCH类型查询为例,捋着代码主线,从实际执行角度分析ES的查询流程。查询流程的代码逻辑可以整体划分为两个部分:

  • 查询入口:ES接收到用户请求后,根据请求分发框架,进入对应接口的处理逻辑。这部分处理对任何ES请求都是类似的。
  • 查询调度:根据查询请求条件,进行查询的Query Phase、Fetch Phase等执行流程,返回查询结果。

在分析具体的查询处理逻辑之前,我们先介绍查询入口部分,看看用户请求在ES中是如何被分发的。

3.1 查询入口

ES提供用户Transport和Rest两种接口:用户可以通过ES官方提供的Transport Client访问ES集群,这种接口使用的协议与ES集群内部节点间的通讯协议一致;也可以使用简单易用的Rest接口,直接发送Http请求访问ES集群,由ES完成Rest请求到Transport请求的转换。考虑Rest接口的易用性,以及Rest层极低的额外开销,建议用户直接使用Rest接口。

以Rest接口为例,查询入口部分的基本流程如下:

  • Rest分发

Rest分发由RestController模块完成。在ES节点启动时,会加载所有内置请求的Rest Action,并把对应请求的Http路径和Rest Action作为二元组注册到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);
}

相关文章