ElasticSearch学习笔记-第四章 ES分片原理以及读写流程详解

四、ES分片原理以及读写流程详解

在学习ES分片原理以及读写流程之前,需要先学习一些ES的核心概念以及ES集群环境的相关知识

4.1 ES核心概念

4.1.1 索引

索引(Index)相当于MySQL中的数据库,一个索引就是一个拥有几分相似特征的文档的集合。

4.1.2 类型

类型(Type)相当于MySQL中的表,一个类型就是索引的一个逻辑上的分类/分区,其语义完全由用户来定。通常,会为具有一组共同字段的文档定义一个类型。不同的ES版本,对于类型的使用也是不同的。

版本 Type
5.x 支持多种Type
6.x 只能有一种Type
7.x 默认不再支持自定义Type,默认为_doc

4.1.3 文档

文档(Document)相当于MySQL中的行,一个文档就是一个可被索引的基础信息单元,即一条数据。

4.1.4 字段

字段(Field)相当于MySQL中的列,即一个字段就是一个属性字段。

4.1.5 映射

映射(Mapping)用于规定ES如何处理数据,例如,某个字段的数据类型、默认值、分析器、是否能被索引等等。

如何建立合适的映射是使用ES的重点

4.1.6 分片

当一个索引中存储的数据过大时,数据大小就可能超过磁盘空间的容量,或者处理搜索请求的效率大大降低。为了解决这个问题Elasticsearch 提供了将索引划分成多份的能力,每一份就称之为一个分片。创建一个索引的时候,可以指定想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。

分片的主要作用

  • 允许水平分割/扩展内容容量。
  • 允许在分片上进行分布式的、并行的操作,进而提高性能/吞吐量。

至于一个分片怎样分布,它的文档怎样聚合和搜索请求,是完全由 Elasticsearch 管理的,对于用户来说,这些都是透明的,无需过分关心。

注意:前面我们讲到Elasticsearch是基于Lucene进行开发的,在Lucene也有索引的概念。在ES中一个Lucene索引被我们称作一个分片,一个Elasticsearch索引是若干个分片的集合。

当ES在索引中搜索的时候, 它发送查询请求到每一个属于索引的分片(Lucene索引)中,然后合并每个分片的结果到一个全局的结果集。

4.1.7 副本

ES允许用户为分片创建一份或者多份拷贝,这些拷贝被称作复制分片(副本),被拷贝的分片被称为主分片

副本的主要作用:

  • 在分片/节点失败的情况下,提供了高可用性

    为了实现高可用性,因此ES不会把复制分片和主分片放置在同一个节点上

  • 扩展搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。

总之,每个索引可以被分成多个分片。一个索引也可以被复制 0 次(意思是没有复制)或多次。一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。分片和复制的数量可以在索引创建的时候指定。在索引创建之后,用户可以在任何时候动态地改变复制的数量,但是不能改变分片的数量。默认情况下,ES 中的每个索引被分片为 1 个主分片和 1 个复制分片,这意味着,如果ES集群中至少有两个节点,那么索引将会有 1 个主分片和另外 1 个复制分片(1 个完全拷贝),这样的话每个索引总共就有 2 个分片,我们需要根据索引需要确定分片个数。

4.1.8 分配

将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。这个过程是由 master 节点完成的。

集群环境会在下一章节进行讲解。

4.2 集群环境

4.2.1 系统架构

在这里插入图片描述

  • 节点

    每个运行中的ElasticSearch实例被称为一个节点

  • 集群

    一个集群由一个或多个拥有相同cluster.name的节点组成,它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。

  • 主节点

    当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等,并且通常不会让主节点涉及到文档级别的变更和搜索等操作,这样配置时,即使集群只拥有一个主节点,流量的增加也不会使得它成为瓶颈,任何节点都可以成为主节点。

用户可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将用户的请求直接转发到存储着用户所需文档的节点。 无论用户将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回给客户端。Elasticsearch 对这一切的管理都是透明的。

4.2.2 部署集群

本次示例,是在Windows环境下进行单机集群的部署。

具体步骤如下:

  1. 在合适的磁盘空间中,创建ElasticSearch-cluster文件夹

  2. 在ElasticSearch-cluster文件夹中,复制三份es-7.8.0解压版

在这里插入图片描述

  1. 更改每一个es的配置,配置文件路径为:你的es安装路径/config/elasticsearch.yml

    node-9201的关键性配置

    #集群名称,同一个集群中的各个节点的这个配置项需要保持一致
    cluster.name: my-es
    #节点名称,同一个集群中的各个节点的这个配置项需要保证唯一
    node.name: node-9201
    #是否是主节点
    node.master: true
    #是否是数据节点,为false则表示不存储数据
    node.data: true
    #ip地址,由于是本机测试,所以指定为localhost
    network.host: localhost
    #http端口号
    http.port: 9201
    #tcp监听端口,同一个集群中的各个节点之间通过tcp协议进行相互通信
    transport.tcp.port: 9301
    #集群内可以发现的其他节点的tcp路径
    discovery.seed_hosts: ["localhost:9302","localhost:9303"]
    discovery.zen.fd.ping_timeout: 1m
    discovery.zen.fd.ping_retries: 5
    #初始化时被指定的主节点
    cluster.initial_master_nodes: ["node-9201"]
    #跨域配置
    http.cors.enabled: true
    http.cors.allow-origin: "*"
    

    node-9202的关键性配置

    #集群名称,同一个集群中的各个节点的这个配置项需要保持一致
    cluster.name: my-es
    #节点名称,同一个集群中的各个节点的这个配置项需要保证唯一
    node.name: node-9202
    #是否是主节点
    node.master: true
    #是否是数据节点,为false则表示不存储数据
    node.data: true
    #ip地址,由于是本机测试,所以指定为localhost
    network.host: localhost
    #http端口号
    http.port: 9202
    #tcp监听端口,同一个集群中的各个节点之间通过tcp协议进行相互通信
    transport.tcp.port: 9302
    #集群内可以发现的其他节点的tcp路径
    discovery.seed_hosts: ["localhost:9301", "localhost:9303"]
    discovery.zen.fd.ping_timeout: 1m
    discovery.zen.fd.ping_retries: 5
    #初始化时被指定的主节点
    cluster.initial_master_nodes: ["node-9201"]
    #跨域配置
    http.cors.enabled: true
    http.cors.allow-origin: "*"
    

    node-9203的关键性配置

    #集群名称,同一个集群中的各个节点的这个配置项需要保持一致
    cluster.name: my-es
    #节点名称,同一个集群中的各个节点的这个配置项需要保证唯一
    node.name: node-9203
    #是否是主节点
    node.master: true
    #是否是数据节点,为false则表示不存储数据
    node.data: true
    #ip地址,由于是本机测试,所以指定为localhost
    network.host: localhost
    #http端口号
    http.port: 9203
    #tcp监听端口,同一个集群中的各个节点之间通过tcp协议进行相互通信
    transport.tcp.port: 9303
    #集群内可以发现的其他节点的tcp路径
    discovery.seed_hosts: ["localhost:9301", "localhost:9302"]
    discovery.zen.fd.ping_timeout: 1m
    discovery.zen.fd.ping_retries: 5
    #初始化时被指定的主节点
    cluster.initial_master_nodes: ["node-9201"]
    #跨域配置
    http.cors.enabled: true
    http.cors.allow-origin: "*"
    

4.2.3 启动集群

  • 启动

    参照2.1小节,依次启动node-9201、node-9202、node-9203即可。

  • 测试(http)

在这里插入图片描述

{
    
    
    "cluster_name": "my-es", // 集群名称
    "status": "green",       // 当前节点的状态
    "timed_out": false,      // 是否超时
    "number_of_nodes": 3,    // 节点总数  
    "number_of_data_nodes": 3, // 数据节点总数
    "active_primary_shards": 0,
    "active_shards": 0,
    "relocating_shards": 0,
    "initializing_shards": 0,
    "unassigned_shards": 0,
    "delayed_unassigned_shards": 0,
    "number_of_pending_tasks": 0,
    "number_of_in_flight_fetch": 0,
    "task_max_waiting_in_queue_millis": 0,
    "active_shards_percent_as_number": 100.0
}

4.2.4 故障转移

  • 在集群中创建如下索引,方便后续的学习

    创建一个users索引,分配三个主分片和一个副本(每个主分片都有一个副本)。

    {
          
          
     "settings" : {
          
          
     "number_of_shards" : 3,
     "number_of_replicas" : 1
     }
    }
    
  • 使用浏览器插件查看集群的整体情况

    我们只启动节点node-9201和节点node-9202

    使用elasticsearch-head插件,直接通过浏览器查看指定集群的情况。

    在这里插入图片描述

    由上图可以看到,此时集群的健康值为green,这表示目前的集群中,三个主分片和三个副本都被正确的分配到了不同的节点上(注意,我们在前面介绍了,如果主分片和其副本在同一个节点上是不安全的)。

    这意味着当集群内任何一个节点出现问题时,我们的数据都完好无损。所有最近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上。这就保证了我们既可以从主分片又可以从副本分片上获得文档。

    即,假如上述的节点node-9201宕机了,并不会影响ES的查询功能,我们依旧可以从副本中获取文档数据。

4.2.5 水平扩容

当应用程序运行了一段时间,并且ES中的数据量逐渐增大后,我们可以通过添加新的节点到ES集群中,实现水平扩容。当启动了第三个节点,集群将会拥有三个节点,它会为了分散负载而对分片进行重新分配。

此时,启动节点node-9203,再次查看集群的分片分配情况。

在这里插入图片描述

从上图可以看到,当新的节点加入集群后,集群重新将各个分片进行了重新分配。此时每个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每个分片的性能将会得到提升。

分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。 我们这个拥有 6 个分片(3 个主分片和 3 个副本分片)的索引可以最大扩容到 6 个节点,每个节点上存在一个分片,并且每个分片拥有所在节点的全部资源。

那么,如果我们想扩容超过6个节点呢?

主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够存储 的最大数据量。(实际大小取决于你的数据、硬件和使用场景。) 但是,读操作(搜索和返回数据可以同时被主分片或副本分片所理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。

因此ES允许用户在运行中的集群上动态地调整副本分片的数量,用户可以通过调整副本的数量来按需伸缩集群。

  • 将副本数量调整至2(每个主分片对应两个副本分片)

    向ES集群发送PUT请求,http/localhost:9201/索引名/_settings,且在请求体中添加如下内容

    {
          
          
        "number_of_replicas":2
    }
    

    在这里插入图片描述

  • 查看集群状态

    在这里插入图片描述

    users 索引现在拥有 9 个分片:3 个主分片和 6 个副本分片。 这意味着我们可以将集群扩容到 9 个节点,每个节点上一个分片。相比原来 3 个节点时,集群搜索性能可以提升 3 倍。当然,如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。 此时,需要增加更多的硬件资源来提升吞吐量。但是更多的副本分片数提高了数据冗余量:按照上面的节点配置,我们可以在失去 2 个节点的情况下不丢失任何数据。

4.2.6 应对故障

当ES集群中的主节点宕机(关闭node-9201节点)后,ES集群会选举一个新的节点(这些备选节点,就是在配置文件中node.master配置项为true的那些节点,由于我们在前面配置集群环境时,三个节点的该配置项都为true,所以此时ES集群会在node-9202和node-9203中选择一个节点作为新的主节点)。

通过插件可以看到此时的ES集群环境状态如下:

在这里插入图片描述

由上图可以看到,此时主节点为node-9203,原本在node-9201的分片0和1都是主分片,新的主节点会立即将这些分片 node-9202上对应的副本分片提升为主分片, 此时集群的状态将会为yellow。**这个提升主分片的过程是瞬间发生的,如同按下一个开关一般。**虽然我们拥有所有的三个主分片,但是同时设置了每个主分片需要对应 2 份副本分片,而此时只存在一份副本分片,所以集群是yellow的状态,虽然处于yellow状态,但是ES集群的功能都可以照常使用。

如果我们重新启动node-9201,集群可以将缺失的副本分片再次进行分配,那么集群的状态也将恢复成之前的状态。 如果 node-9201 依然拥有着之前的分片,它将尝试去重用它们,同时仅从主分片复制发生了修改的数据文件。和之前的集群相比,只是 Master 节点切换了。

在这里插入图片描述

上图表示再次启动node-9201后的集群状态

注意:故障转移、水平扩容、应对故障这几个章节各位读者尽量自己手动操作一遍,体会ES集群的设计理念。

4.2.7 路由计算

  • 路由算法

    当为ES集群添加一个文档时,文档会被存储到一个主分片中,由于在集群环境里,会有多个主分片,Elasticsearch是如何知道应该把这个文档放入哪个主分片中呢?

    为了解决上述的问题,Elasticsearch使用了一个算法来进行路由计算,得出应该把该文档放入哪个分片中,并且在查询时也通过该算法来获取存储这个文档的分片。

    算法如下:

    shard = hash(routing) % number_of_primary_shards
    

    routing是一个可变值,默认是文档的_id,也可以由用户自己定义。

    这个算法的含义是:对routing取哈希值后除以主分片的数量取余数,得到的值就是0(主分片数量-1)之间的数字(计数从0开始,比如3个主分片,那么范围就是02),就是文档存放的分片位置。

    这就解释了为什么要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。

  • 自定义路由

    所有的文档 API( get 、 index 、 delete 、 bulk 、 update 以及 mget )都接受一个叫做 routing 的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档(例如所有属于同一个用户的文档)都被存储到同一个分片中。

    该部分会在后续章节进行详细讲解。

4.3 分片控制

讲述该小节时,使用如下的集群环境中的cluster-test索引,该索引有3个主分片和1个副本

在这里插入图片描述

在这个集群中,每个节点都有能力处理任意请求。 每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。在后续的示例中,我会将所有的请求都发送给node-9201,此时node-9201就被称为协调节点

注意:在工作中,为了达到负载均衡的目的,一般会轮询集群中的所有节点,使其共同的承载请求,而不是将所有的请求都发送给同一个节点。

4.3.1 写流程(大致流程)

在Elasticsearch中,**新增、删除、修改(全量)**文档的请求都属于写流程,写流程必须先在主分片上完成后再同步给副本分片。

例如请求:

PUT/DELETE http://localhost:9200/cluster-test/_doc/1001

新增、全量更新或者删除某个具体的文档

在新增文档时如果没有指定主键,ES会自动生成主键,并且根据自动生成的主键进行路由计算。

接下来,我们通过一个例子来学习Elasticsearch中的写流程是如何在ES集群中进行的。

假设,我们发送新增、删除、修改(全量)文档的请求到node-9201,那么ES会做如下的处理:

  1. node-9201使用文档的_id(如果指定了routing参数,则使用对应的参数)进行路由计算,得到文档属于的分片,例如,属于分片0,那么node-9201节点会将该请求转发给node-9202(因为主分片0在node-9202上)。
  2. node-9202在主分片0上处理该请求。如果成功了,它会将请求转发到node-9203,让副本分片0做同样的处理,当副本分片0处理成功后,会告知node-9202,随后node-9202会将处理成功的结果告知node-9201。
  3. node-9201返回客户端请求结果。

具体的示意图如下:

在这里插入图片描述

在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。当然Elasticsearch也提供了一些参数供用户干预这个过程(可以以数据安全为代价进一步提升性能),当然,这些参数很少被使用,因为Elasticsearch已经足够快了。

下面表格中罗列了可以干预这个过程的一些参数以及含义。

参数 含义
consistency consistency,即一致性。consistency的参数值可用设置为one(只要主分片的状态正常,就执行写操作),all(必须所有的主分片和副本分片的状态都是正常的才执行写操作)以及quorum(大多数分片状态是正常的就允许写操作),默认值就是quorum,这就表示在默认设置下,即使仅仅是在试图执行一个写操作之前,主分片都会要求必须要有规定数量(quorum)的分片副本处于活跃可用状态,才会去执行写操作。这是为了避免在发生网络故障的时候进行写操作,进而导致数据的不一致。规定数量(quorum)的计算公式为int( (primary + number_of_replicas) / 2 ) + 1。其中,number_of_replicas指的是索引设置中的设定副本数量,而不是当前处于活跃状态的副本数量。
timeout 如果没有足够的副本分片会发生什么? Elasticsearch 会等待,希望更多的分片出现,默认情况下,它最多等待 1 分钟。用户可以使用 timeout 参数调整等待时间。

注意:新建索引时,索引的副本数量默认为1,这意味着为满足规定数量应该需要两个活动的分片副本,这些默认的设置就会导致我们无法在单个节点上做任何写操作。因此ES规定只有当number_of_replicas大于1时,规定数量才会生效。

4.3.2 读流程(大致流程)

读流程,这里所讲的是根据routing或者id读取指定文档的流程。(注意与搜索数据流程进行区分)。

例如,请求为GET http://localhost:9201/cluster-test/_doc/1001

接下来,我们通过一个例子来学习Elasticsearch中的读流程是如何在ES集群中进行的。

假设,我们发送读取数据的请求到node-9201,那么ES会做如下的处理:

  1. node-9201对文档进行路由计算,得到文档属于的分片,例如,属于分片0,并且会使用round-robin随机轮询算法,在主分片0和其所有的副本分片中随机选择一个,让读请求负载均衡,node-9201节点会将该请求转发给具体的节点,例如node-9203。

    使用routing参数或者直接使用id进行路由计算,读取数据。

  2. node-9203在副本分片0上处理该请求,返回查询结果给node-9201。

  3. node-9201返回查询结果给客户端。

具体的示意图如下:

在这里插入图片描述

4.3.3 更新流程(大致流程)

部分更新一个文档结合了之前讲解的读取和写入流程。

接下来,我们通过一个例子来学习Elasticsearch中的更新流程是如何在ES集群中进行的。

假设,我们发送更新数据的请求到node-9201,那么ES会做如下的处理:

  1. node-9201节点对文档进行路由计算,得到文档所属的分片,例如,属于分片0。

  2. node-9201节点将请求转发给node-9202节点(主分片0在该节点上)。

  3. node-9202节点处理更新请求,读取文档,修改_source字段中的JSON数据,并且尝试重新索引该文档(如果此时另外一个进程正在修改该文档,则会重试步骤3,超过retry_on_conflict次数后放弃)。

    上面所说的重新索引该文档其实是指,将旧版本的文档标记为删除(即,在.del文件中标记旧版本文档),然后生成一条新版本的文档,并写入这条新版本的文档。

  4. 如果node-9202节点成功更新文档,它会将新版本的文档转发给node-9203节点,node-9203节点重建对于新版本文档的索引(倒排索引)。

    这一步副节点会做与主节点相同的事情。(标记旧版本文档为删除状态,且写入新版本的文档)

  5. node-9203节点更新成功后,返回响应给node-9202节点。

  6. node-9202节点会将更新成功的结果返回给node-9201节点。

  7. node-9201节点将结果返回给客户端。

具体的示意图如下:

在这里插入图片描述

注意

当主分片把更改转发到副本分片时, 它不会转发更新请求。 相反,它转发完整文档的新版本。请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。 如果 Elasticsearch 仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。

4.3.4 多文档操作流程(大致流程)

这里的多文档操作流程指mget和bulk请求。

mget请求的处理流程:

  1. 客户端向node-9201节点发送mget请求。

  2. node-9201节点为每一个节点都创建多文档获取请求,然后并行转发这些请求给所有节点,例如node-9202和node-9203。

  3. 当node-9202节点以及node-9203节点处理完请求后,会将结果响应给node-9201节点。

  4. node-9201节点将请求结果响应给客户端。

    整个流程相当于是批量的get请求,每个节点对于请求的处理参考前面介绍的读流程。

bulk请求的处理流程:

  1. 客户端向node-9201节点发送bulk请求。

  2. node-9201节点为每个节点都创建批量请求,然后并行转发这些请求给每个包含主分片的节点。

  3. 当所有节点处理完请求后,会将结果响应给node-9201节点。

  4. node-9201节点将请求结果响应给客户端。

    整个流程相当于是批量的新建、删除、更新请求,每个节点对于请求的处理参考前面介绍的写流程。

4.4 分片原理

4.4.1 文档搜索(按段搜索)
  • 不可变的倒排索引

    早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘,一旦需要为新的文档建立倒排索引,就需要替换整个倒排索引,即,倒排索引被写入磁盘后是不可改变的,只能整个替换。

    这样做的优点在于:

    1. 不需要锁。如果从来不更新索引,就不需要担心多进程同时修改数据的问题。

    2. 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够

      的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。

    3. 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。

    这样做的缺点也十分明显:如果需要让一个新的文档可被搜索,就需要重建整个倒排索引,这就对一个倒排索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。

  • 动态更新倒排索引

    为了能够在保留倒排索引的不可变性的前提下,实现倒排索引的更新,Elasticsearch采用了补充倒排索引的方法,通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。在检索时,每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并,这样就可以避免频繁的重建倒排索引而导致的性能损耗了。

  • 按段搜索

    Elasticsaerch是基于Lucene开发的,其拥有着按段(segment)搜索的概念,每一个段(segment)本身就是一个倒排索引。除了段之外,还有着提交点(commit point)的概念,在提交点中记录当前所有可用的segment,上面提到的新增补充倒排索引,其实就是新增一个段。

    段和提交点的关系如下图所示

在这里插入图片描述

  • 分段思想下的写数据

    当一个新的文档被添加到索引时,会经历如下流程(这里只关注段和提交点的使用情况)

    1. 新文档被添加到内存缓存。

      此时的提交点、段以及内存缓存示意图如下

      在这里插入图片描述

    2. 不时地【默认经过一定时间,或者当内存中数据量达到一定阶段时,再批量提交到磁盘中。(具体细节后续的写流程底层原理会进行讲解)】,缓存被提交

      • 一个新的段(补充的倒排索引)被写入磁盘

      • 一个新的包含新段的提交点生成并被写入磁盘

        一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限;相反,当段在内存中时,就只有写数据的权限,而不具备读数据的权限,所以也就不能被检索了。

      • 磁盘进行同步——所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件

    3. 新的段被开启,让它包含的文档可以被检索到

    4. 内存缓存被清空,等待接收新的文档

      此时的提交点、段以及内存缓存示意图如下

      在这里插入图片描述

  • 分段思想下的删除/更新数据

    当需要删除数据时,由于数据所在的段是不可改变的,所以不能从把文档从旧的段中直接移除,此时每个提交点都会包含一个.del文件,文件中存储着被删除的数据id。(逻辑删除)

    当需要更新数据时,会先进行删除操作,再进行新增操作,即,先在.del文件中记录旧数据,再在新段中添加一条更新后的数据。

  • 分段思想下的查询数据

    查询所有段中满足查询条件的数据,然后对每个段里查询的结果集进行合并,得到一个大的结果集,然后将.del文件中记录的被删除的数据剔除后返回给客户端。

4.4.2 近实时搜索

从上一小节介绍的分段思想下的新增数据流程可以看出,当一个新的文档被写入时,数据还在内存缓存中,此时这条数据是不可查询的,因此Elasticsearch的查询是近实时搜索的。

当提交一个新的段到磁盘时,需要使用系统调用fsync来确保数据被物理性地写入磁盘,这样在断电后就不会丢失数据了。然而fsync的代价很大,如果每次新增/修改数据都使用fsync来将其物理性地写入磁盘,就会导致很大的性能损耗。

在Elasticsearch中使用的是更加轻量化的方式来使得一个文档可以被检索,即,要将fsync从文档被写入到其可被检索的过程中移除掉,以此来提升性能。为了达到这个目标,在Elasticsearch和磁盘之间,是操作系统的文件系统缓存(OS Cache)

Elasticsearch的内存缓存(Memory)和硬盘(Disk)之间是操作系统的文件系统缓存(OS Cache)

在这里插入图片描述

像上一小节描述的一样, 在内存索引缓冲区中的文档会被写入到一个新的段中,但是这里新段会被先写入到文件系统缓存**(这一步代价会比较低),稍后再被批量刷新到磁盘(这一步代价比较高)**。只要文件已经在文件系统缓存中,就可以像其它文件一样被打开和读取了,即,可以被检索。

上面介绍的,把内存缓冲区中的数据写入文件系统缓存的过程叫做refresh,该操作,默认情况下每一秒或者内存缓冲区的数据达到一定数据量时就会执行一次(4.4.1小节中讲述的那样),这也就是为什么我们会说Elasticsearch是近实时搜索了(文档的变化,会在一秒后可见,因此并不是实时的,是近实时的)。

当然,Elasticsearch提供了refresh API可供用户手动执行refresh操作,例如发送请求/索引名/_refresh即可。

尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候, 手动刷新很有用,但是不要在生产环境下每次索引一个文档都去手动刷新。 相反,我们的应用需要意识到 Elasticsearch 的近实时的性质,并接受它的不足。

有的场景不需要每秒执行一次refresh(例如添加大量的日志文件到ES中),这该如何满足上述场景的需求呢?

我们可以通过设置索引的refresh_interval来调整执行refresh操作的时间间隔。

{
    
    
 "settings": {
    
    
 "refresh_interval": "30s" 
 }
}

refresh_interval 可以在已存在的索引上进行动态更新。 在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来。

# 关闭自动刷新
PUT /索引名/_settings
{
    
     "refresh_interval": -1 } 
# 每一秒刷新
PUT /索引名/_settings
{
    
     "refresh_interval": "1s" }

此时,Elasticsearch的写流程如下图所示。(该流程图,目的在于循序渐进的为各位呈现Elasticsearch的写流程设计思想,此处的流程图并不完整)

在这里插入图片描述

4.4.3 持久化变更

上一小节,我们介绍了Elasticsearch通过refresh操作将文档数据从内存缓存中写入文件系统缓存达到轻量化的查询机制,在这个过程中,将fsync系统调用移除了,如果没有用 fsync 把数据从文件系统缓存写入到硬盘(我们将把文件系统缓存中的数据写入硬盘的操作称为flush),就无法保证在断电甚至是程序正常退出之后依然存在(即,没有持久化)

Elasticsearch为了保证可靠性,就需要确保数据变更被持久化到磁盘,为了实现这个需求,Elasticsearch增加了translog(事务日志),来作为补偿机制,防止数据的丢失,在translog中记录了所有还未被持久化到磁盘的数据。

关于translog,需要弄明白下面三个问题。

  • 什么时候写入数据到translog中?

    当写入数据到内存缓存中后,就会追加一份数据到translog中。(后面会详细介绍这一部分)

  • 什么时候使用translog中的数据?

    当Elasticsearch启动时,不仅会根据最新的一个提交点加载已持久化的段,还会根据translog中的数据,将未持久化的数据重新持久化到磁盘上。

  • 什么时候清理translog中的数据?

    当文件系统缓存中的数据被flush到磁盘上后,就会删除旧的translog,并且生成一个新的空白的translog。

    **默认每30分钟或者translog太大(默认为512MB)的时候会执行一次flush操作。**通常情况下,自动刷新就足够了。当 Elasticsearch 尝试恢复或重新打开一个索引时, 它需要重放 translog 中所有的操作,所以如果日志越短,恢复越快。

    可以通过index.translog.flush_threshold_size配置参数来指定translog的最大容量。

增加了translog后,Elasticsearch的写流程如下图所示(详细流程会在写入流程底层原理小节进行详细的讲解)

在这里插入图片描述

虽然translog是用来防止数据丢失,但是也有数据丢失的风险

  • 写translog详解

    从上面的写流程图可以看到,translog在内存缓存以及磁盘上都有一份,只有当内存中的translog通过fsync系统调用被flush到磁盘上后,才是可靠的。

    执行translog的flush操作有两种模式——异步和同步,默认为同步模式,这个模式可以通过参数index.translog.durability来进行调整,并且可以通过参数index.translog.sync_interval来控制自动执行flush的时间间隔。

    #异步模式
    index.translog.durability=async
    #同步模式
    index.translog.durability=request
    

    当处于同步模式时,默认会每次写请求之后就会执行一次fsync操作,这个过程在主分片和复制分片都会发生,这就意味着,在整个请求被fsync到主分片和复制分片的磁盘中的translog之前,客户端都不会得到一个200的响应。(即,此模式下,写入请求成功,就表示着已经将本次的数据落盘到磁盘中的translog中了,这就保证了数据的可靠性)。

    当处于异步模式时,默认会5秒执行一次fsync操作,并且这个动作时异步的,这就意味着,即使你的写请求得到了200的响应,也并不代表着本次请求的数据已经落盘到磁盘中的translog中,即,本次操作并不可靠。(在五秒之内断电,这部分数据就会丢失)。

    注意

    Elasticsearch对于translog的flush操作默认是同步模式,虽然每次修改数据都会执行一次translog的flush操作,但是这个代价要远远小于每次修改数据都执行一次段的flush操作,因此使用translog的补偿机制,是权衡了性能以及数据安全的一个方案。除非有特殊需求,否则默认地,使用同步模式即可

4.4.4 段合并
  • 介绍及流程

    在4.4.2小节,我们介绍了,每秒执行的refresh操作都会创建一个新的段,经过长时间的积累,索引中会存在大量的段,当段的数量过大时,不仅会占用过多的服务器资源,并且还会影响检索的性能。

    前文介绍过,每次搜索时,会查询所有段中满足查询条件的数据,然后对每个段里查询的结果集进行合并,检索越慢。

    Elasticsearch采用了段合并的方式来解决段数量过多的问题,在Elasticsearch中有一个后台进程专门负责段的合并,它会定期执行段的合并操作。

    段合并的操作流程如下:

    1. 将多个小的段合并成一个新的大的段,在合并时已删除的文档(.del文件中存储的文档id对应的文档)或被更新文档的旧版本不会被写入到新的段中。
    2. 将新的段文件flush写入磁盘
    3. 在该提交点中标识所有新的段文件,并排除掉旧的和已经被合并的段文件
    4. 打开新的段文件用于搜索使用
    5. 等所有的检索请求都从小的段文件转到大的段文件上以后,删除旧的段文件

    以上流程对于用户而言是透明的,Elasticsearch会在索引文档以及搜索文档时自动执行。被合并的段可以是磁盘上已经提交过的索引,也可以在内存中还未提交的段,在合并的过程中,不会打断当前的索引和搜索功能

  • 段合并的性能影响

    从上面的段合并的流程介绍,我们就可以看出,段合并的流程不仅涉及到段的读取、新的段的生成,还涉及到段的flush操作,因此,如果不对段合并加以控制,将会消耗大量的 I/O 和 CPU 资源,同时也会对搜索性能造成影响。

    在Elasticsearch中,默认地一次性只能合并十个段,并且段的容量大于5GB时不参与段合并,并且归并线程的默认配速为20MB/S。

    我们可以通过下面几个参数来对段合并的规则进行调整。

    #更改配速为100MB/s
    {
          
          
        "persistent" : {
          
          
            "indices.store.throttle.max_bytes_per_sec" : "100mb"
        }
    }
    #设置优先被合并的段的大小,默认为2MB
    index.merge.policy.floor_segment
    #设置一次最多合并的段数量,默认为10个
    index.merge.policy.max_merge_at_once
    #设置可被合并的段的最大容量,默认为5GB
    index.merge.policy.max_merged_segment
    
4.4.5 写流程详解

在4.2.8.1小节我们学习了写流程的大致流程,我们学习了在整体上,Elasticsearch是如何在集群环境下对客户端发起的写请求进行处理的。在这个小节,博主将会结合每个节点接收到写请求后具体的处理,来对写流程进行总结。

写流程总结如下:

  1. 客户端发送写请求到协调节点
  2. 协调节点根据routing参数(如果没有指定,则默认是文档的id)进行路由计算(详见4.2.7小节),计算出该文档所属的主分片位置。
  3. 协调节点转发写请求到主分片所在节点。
  4. 主分片所在节点收到写请求后,就进入了单个节点的写流程。
  5. 主分片所在节点处理完写请求后,将写请求并行转发到其副本分片所在的所有节点,这些节点收到请求后,会做相同的处理。
  6. 所有的副本分片所在节点处理完写请求后,会将处理结果返回给主分片所在节点,主分片所在节点再将处理结果返回给协调节点。
  7. 协调节点返回结果给客户端。

Elasticsearch的单个节点的写流程详解如下图所示。

在这里插入图片描述

文字描述如下:

  1. 将数据写入内存缓存(**index buffer)**中

  2. 将数据追加到事务日志(translog)中

  3. 默认每秒执行一次refresh操作,将内存缓存中的数据refresh到**文件系统缓存(OS Cache)**中,生成段(segement),并打开该段供用户搜索,同时会清空内存缓存(index buffer)中的数据。

  4. 默认每次写入数据后通过fsync系统调用将内存中的translog写入(flush)到磁盘中。

    同步模式是每次写入数据后都会fsync到磁盘

    异步模式是每5秒fsync到磁盘

  5. 默认每30分钟或者translog大小超过512M后,就会执行一次flush将文件系统中的数据写入磁盘。

    1. 生成新的段写入磁盘
    2. 生成一个新的包含新的段的提交点写入磁盘
    3. 删除旧的translog,并生成新的translog
  6. Elasticsearch会开启归并进程,在后台对中小段进行段合并,减少索引中段的数目,该过程在文件系统缓存和磁盘中都会进行。

4.4.6 读流程详解

读流程总结如下:

  1. 客户端发读请求到协调节点
  2. 协调节点根据routing参数(如果没有指定,则默认是文档的id)进行路由计算(详见4.2.7小节),计算出该文档所属的分片位置。
  3. 使用round-robin随机轮询算法在该文档所属分片中任意选择一个(主分片或者副本分片),将请求转发到该分片所在节点。
  4. 该节点收到请求后,就进入单个节点的读流程。
  5. 该节点把查询结果返回给协调节点。
  6. 协调节点把查询结果返回给客户端。

Elasticsearch单个节点的读流程如下图所示

在这里插入图片描述

文字描述如下:

  1. 节点接收到读数据请求
  2. 根据请求中的doc id字段从translog缓存中查询数据,如果查询到数据则直接返回结果。
  3. 在第2步没有查到结果,从磁盘中的translog查询数据,如果查询到数据则直接返回结果。
  4. 在第3步没有查到结果,从磁盘中的各个段中查询结果,如果查到数据则直接返回结果。
  5. 经过前面的步骤,如果都没查到结果,则返回null。

注意,Elasticsearch在读取数据时,会先尝试从translog中获取,再从segement中获取,这是因为,前面我们讲了对所有文档的写入/修改/删除操作都会先被记录在translog中,然后再通过refresh、flush操作写入segament,因此,translog中会记录着最新的文档数据,所以如果从translog查到了目标数据,直接返回即可,如果没有,再去尝试从segament中获取。

4.4.7 搜索流程详解

这里的搜索流程,指的是search,注意要与上面介绍的读流程进行区分。读流程是指拿着doc id去通过正排索引查找数据search流程则与search的流程与searchType相关

searchType的默认值是 Query then Fetch

可以简要的理解为:先通过倒排索引拿到doc id,然后再根据doc id通过正排索引查找数据

searchType有四种,具体如下:

  • Query And Fetch

    向索引的所有分片(shard)都发出查询请求,各分片返回的时候把元素文档(document)和计算后的排名信息一起返回。

    这种搜索方式是最快的。因为相比下面的几种搜索方式,这种查询方法只需要去shard查询一次。但是各个shard返回的结果的数量之和可能是用户要求的size的n倍。

  • Query Then Fetch(默认)

    这种搜索模式分两个步骤。

    1. 向所有的shard发出请求,各分片只返回”足够“(预估是排序、排名以及分值相关)的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。
    2. 去相关的shard取document。这种方式返回的document与用户要求的size是相等的。
  • DFS Query And Fetch

    这种方式比第一种方式多了一个initial scatter phrase步骤,有这一步,可以使评分精确度更高。

  • DFS Query Then Fetch

    这种方式比第二种方式多了一个initial scatter phrase步骤

一般使用默认的模式即可。

下面,我们将学习使用Query Then Fetch模式下的搜索流程。

搜索流程分为两个阶段,Query(查询阶段)Fetch(获取阶段)

  • Query

    1. 协调节点接收search请求后,广播该请求到所有分片上(包括主分片和副本分片)。

    2. 每个分片独立执行搜索,使用倒排索引进行匹配,根据匹配相关性构建出一个大小为from+size(from和size就是分页时的参数)的优先级排序结果队列(包含文档的id和所有参与排序的字段的值,比如_score)。

      在该阶段会查询OS Cache中的segament缓存,此时有些数据可能还在Memory中,因此Elasticsearch是近实时搜索。(可以对照前面的介绍进行理解)

    3. 每个分片将其优先级排序结果队列返回给协调节点。

    4. 协调节点创建一个新的优先级排序结果队列,并对全局结果进行排序,得到一个排序结果列表(包含所有排序的字段值、文档id)。

    5. 进入Fetch阶段。

  • Fetch

    1. 协调节点根据排序结果列表,向相关的分片提交多个 GET 请求。
    2. 每个分片收到GET请求后,执行上面介绍过的读流程,根据文档id获取详细的文档信息,并返回给协调节点。
    3. 协调节点返回结果给客户端。

参考

【尚硅谷】ElasticSearch教程入门到精通(基于ELK技术栈elasticsearch 7.x+8.x新特性)

猜你喜欢

转载自blog.csdn.net/weixin_42584100/article/details/131904555