ElasticSearch核心概念详解(index/type/doc/node/shard/replica/segment)

前言

ElasticSearch,简称ES,是一个基于Lucene的搜索服务器。要想了解ES,必须先了解Lucene。

数据和搜索

我们知道,生活中我们有两类的数据:

  1. 结构化数据:也称作行数据,是由二维表结构来逻辑表达和实现的数据,严格地遵循数据格式与长度规范,主要通过关系型数据库进行存储和管理。指具有固定格式或有限长度的数据,如数据库,元数据等。
  2. 非结构化数据:又可称为全文数据,不定长或无固定格式,不适于由数据库二维表来表现,包括所有格式的办公文档、XML、HTML、word文档,邮件,各类报表、图片和咅频、视频信息等。

根据两种数据分类,搜索也相应的分为两种:结构化数据搜索非结构化数据搜索。对于结构化数据,因为它们具有特定的结构,所以我们一般都是可以通过关系型数据库(mysql,oracle等)的二维表(table)的方式存储和搜索,也可以建立索引。

对于非结构化数据,也即对全文数据的搜索主要有两种方法:顺序扫描法全文检索

  1. 顺序扫描:通过文字名称也可了解到它的大概搜索方式,即按照顺序扫描的方式查询特定的关键字。例如给你一张报纸,让你找到该报纸中“平安”的文字在哪些地方出现过。你肯定需要从头到尾把报纸阅读扫描一遍然后标记出关键字在哪些版块出现过以及它的出现位置。

这种方式无疑是最耗时的最低效的,如果报纸排版字体小,而且版块较多甚至有多份报纸,等你扫描完你的眼睛也差不多了。

  1. 全文搜索:对非结构化数据顺序扫描很慢,我们是否可以进行优化?把我们的非结构化数据想办法弄得有一定结构不就行了吗?将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。

这种方式就构成了全文检索的基本思路。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引。这种方式的主要工作量在前期索引的创建,但是对于后期搜索却是快速高效的。

Lucene

通过对生活中数据的类型作了一个简短了解之后,我们知道关系型数据库的SQL检索是处理不了这种非结构化数据的。这种非结构化数据的处理需要依赖全文搜索,而目前市场上开放源代码的最好全文检索引擎工具包就属于 apache 的 Lucene了。

但是 Lucene 只是一个工具包,它不是一个完整的全文检索引擎。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。

目前以 Lucene 为基础建立的开源可用全文搜索引擎主要是 Solr 和 Elasticsearch。

Solr 和 Elasticsearch 都是比较成熟的全文搜索引擎,能完成的功能和性能也基本一样。但是 ES 本身就具有分布式的特性和易安装使用的特点,而Solr的分布式需要借助第三方来实现,例如通过使用ZooKeeper来达到分布式协调管理。

不管是 Solr 还是 Elasticsearch 底层都是依赖于 Lucene,而 Lucene 能实现全文搜索主要是因为它实现了倒排索引的查询结构。(稍后展开)

1. ES基本概念详解

ElasticSearch提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。

ElasticSearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。其官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。

不过,Elasticsearch不仅仅是Lucene和全文搜索,我们还能这样去描述它:

  • 分布式的实时文件存储,每个字段都被索引并可被搜索
  • 分布式的实时分析搜索引擎
  • 可以扩展到上百台服务器,处理PB级结构化或非结构化数据

es和lucene,solr一样,都是无模式的基于列式的存储格式,这和大多数的NoSQL数据库是一样的,非常灵活,下面我们通过一张图,来看下关系型数据库映射到es里面,对应的名词关系

1.1 Index

Index,索引,是文档(Document)的容器,是一类文档的集合。

ElasticSearch将它的数据存储在一个或多个索引(index)中。索引就像数据库,可以向索引写入文档或者从索引中读取文档

索引这个词在 ElasticSearch 会有两种意思:

  1. 索引(名词)

    • 类比传统的关系型数据库领域来说,索引相当于SQL中的一个数据库(Database)。索引由其名称(必须为全小写字符)进行标识。
  2. 索引(动词)

    • 保存一个文档到索引(名词)的过程。这非常类似于SQL语句中的 INSERT关键词。如果该文档已存在时那就相当于数据库的UPDATE。

1.2 Type

Type,类型,可以理解成关系数据库中Table(虽然不完全一致)。用于区分同一个集合中的不同细分。

但和Table不同的是,不同表中的字段可以同名,但他们还是独立的,比如表A的a字段可以是VARCHAR类型,表B的a字段可以是INT类型。

而不同类型的同名字段,他们其实是同一个字段,所以无法独立,实际上,es在底层,也是将不同type的字段都映射为扁平的模式。而不是为每种type分配单独的映射空间。

这导致了type不适合于描述完全不同类型的数据 ,如type A有a,b,c三个字段,type B有d,e,f三个字段,那么这种情况建议不要使用type。

A { a,b,c}和B{b,c,d}这种情况才适用,即二者间大部分的数据是相同的,这种情况下,es的扁平化映射,可以复用这部分重合的数据。

之前的版本中,索引和文档中间还有个类型的概念,每个索引下可以建立多个类型,文档存储时需要指定index和type。从6.0.0开始单个索引中只能有一个类型,

7.0.0以后将将不建议使用,8.0.0 以后完全不支持。

1.2.1弃用该概念的原因:

我们虽然可以通俗的去理解Index比作 SQL 的 Database,Type比作SQL的Table。但这并不准确,因为如果在SQL中,Table 之前相互独立,同名的字段在两个表中毫无关系。

但是在ES中,同一个Index 下不同的 Type 如果有同名的字段,他们会被Luecence当作同一个字段 ,并且他们的定义必须相同。所以我觉得Index现在更像一个表,而Type字段则并没有多少意义。

目前Type已经被Deprecated,在7.0开始,一个索引只能建一个Type为_doc

1.3 Document

Document,文档,Index 里面单条的记录称为Document。等同于关系型数据库表中的行

我们来看下一个文档的源数据

  • _index文档所属索引名称。

  • _type文档所属类型名。

  • _idDoc的主键。在写入的时候,可以指定该Doc的ID值,如果不指定,则系统自动生成一个唯一的UUID值。

  • _version文档的版本信息。Elasticsearch通过使用version来保证对文档的变更能以正确的顺序执行,避免乱序造成的数据丢失。

  • _seq_no严格递增的顺序号,每个文档一个,Shard级别严格递增,保证后写入的Doc的_seq_no大于先写入的Doc的_seq_no。

  • primary_termprimary_term也和_seq_no一样是一个整数,每当Primary Shard发生重新分配时,比如重启,Primary选举等,_primary_term会递增1

  • found查询的ID正确那么ture, 如果 Id 不正确,就查不到数据,found字段就是false。

  • _source文档的原始JSON数据。

2. ES分布式概念详解

2.1 集群(cluster)

ElasticSearch集群实际上是一个分布式系统,它需要具备两个特性:

  • 高可用性

    • 服务可用性:允许有节点停止服务;
    • 数据可用性:部分节点丢失,不会丢失数据;
  • 可扩展性

    • 随着请求量的不断提升,数据量的不断增长,系统可以将数据分布到其他节点,实现水平扩展;

一个集群中可以有一个或者多个节点;

我们采用集群健康值来衡量一个集群的状态

  1. green:所有主要分片和复制分片都可用
  2. yellow:所有主要分片可用,但不是所有复制分片都可用
  3. red:不是所有的主要分片都可用

当集群状态为 red,它仍然正常提供服务,它会在现有存活分片中执行请求,我们需要尽快修复故障分片,防止查询数据的丢失;

2.2 节点(Node)

es集群是通过多台服务器来搭建,它们拥有一个共同的clustername比如叫做“escluster”,每台服务器叫做一个节点,用于存储数据并提供集群的搜索和索引功能。

节点拥有自己的唯一名字,默认在节点启动时会生成一个uuid作为节点名,该名字也可以手动指定。

单个集群可以由任意数量的节点组成。如果只启动了一个节点,则会形成一个单节点的集群。其配置文件如下:

1
2
3
4
5
6
7
8
集群名称,用于定义哪些elasticsearch节点属同一个集群。
cluster.name: bigdata
节点名称,用于唯一标识节点,不可重名
node.name: server3
设置索引的分片数,默认为5
index.number_of_shards: 5
设置索引的副本数,默认为1:
index.number_of_replicas: 1

节点是一个ElasticSearch的实例,其本质就是一个Java进程;一台机器上可以运行多个ElasticSearch实例,但是建议在生产环境中一台机器上只运行一个ElasticSearch实例;

2.2.1 四种普通节点

在es节点的yml文件中可以配置节点的类型

1
2
3
conf/elasticsearch.yml:
node.master: true/false
node.data: true/false

其中node.master配置表示节点是否具有成为主节点的资格节点。

此属性的值为true,并不意味着这个节点就是主节点。因为真正的主节点,是由多个具有主节点资格的节点进行选举产生的。所以,这个属性只是代表这个节点是不是具有主节点选举资格。

node.data配置表示节点是否存储数据。

node.master和node.data的取值可以有四种情况,表示四种节点类型。

  • node.master: true并且node.data: true

    • 这种组合表示这个节点即有成为主节点的资格,又存储数据。这个时候如果某个节点被选举成为了真正的主节点,那么他还要存储数据,这样对于这个节点的压力就比较大了。elasticsearch默认每个节点都是这样的配置,在测试环境下这样做没问题。实际工作中建议不要这样设置,这样相当于主节点和数据节点的角色混合到一块了。
  • node.master: false并且node.data: true

    • 这种组合表示这个节点没有成为主节点的资格,也就不参与选举,只会存储数据。这个节点我们称为data(数据)节点。在集群中需要单独设置几个这样的节点负责存储数据,后期提供存储和查询服务
  • node.master: true并且node.data: false

    • 这种组合表示这个节点不会存储数据,有成为主节点的资格,可以参与选举,有可能成为真正的主节点。对于master节点而言,这样的配置是最适合的。
  • node.master: false并且node.data: false

    • 这种组合表示这个节点即不会成为主节点,也不会存储数据,这个节点的意义是作为一个client(客户端)节点,主要是针对海量请求的时候,这些节点负责处理用户请求,实现请求转发,负载均衡等功能。

2.2.2 master节点

拥有选举成为master节点资格的节点经过选举,成为了master节点,

Elasticsearch中的master并不像mysql、hadoop集群的master那样,它既不是集群数据的唯一流入点,也不是所有元数据的存放点。所以,一般情况下Elasticsearch的Master负载是很低的。

master集群的主要工作有:

  1. 同步集群状态:集群状态信息,由master节点进行维护,并且同步到集群中所有节点。也就是说集群中的任何节点都存储着集群状态信息(经过master的同步),但只有Master能够改变信息。我们可以通过接口读取它,如:/_cluster/state
    • 集群状态中包括:
      1. 集群层面的设置
      2. 集群内有哪些节点的信息
      3. 各索引的设置,映射,分析器和别名等设置
      4. 索引内各分片所在的节点位置
  2. 集群状态的修改:集群状态的修改通过Master节点完成,比如索引的创建删除,mapping的修改等等。
    • 我们知道配置项dynamic=true表示对于未mapping的新字段,es会尝试猜测该字段的类型,并mapping它。此时数据节点需要跟Master通信,通知Master修改Mapping。这个时候的index写入是阻塞的。等Master修改了集群状态之后,再同步到所有节点,才可以继续写入。

2.2.3 master选举

详见另一篇文章ElasticSearch Master选举机制浅析

2.3 分片(Shared)

分片是什么?简单来讲就是咱们在ES中所有数据的文件块,也是数据的最小单元块,整个ES集群的核心就是对所有分片进行分布、索引、负载、路由等操作,来达到惊人的速度。

文档存储在分片中,然后分片分配到集群中的节点上。当集群扩容或缩小,Elasticsearch 将会自动在节点间迁移分片,以使集群保持平衡。

假设 IndexA 有2个分片,我们向 IndexA 中插入10条数据 (10个文档),那么这10条数据会尽可能平均的分为5条存储在第一个分片,剩下的5条会存储在另一个分片中。

一个分片(shard)是一个最小级别“工作单元(worker unit)”,大多数情况下,它只是保存了索引中所有数据的一部分

这类似于 MySql 的分库分表。

2.3.1 分片的种类

一个分片就是一个运行的 lucene 实例,一个节点可以包含多个分片,这些分片可以是:

  • 主分片(primary shard

    • 用于解决数据水平扩展的问题,一个索引的所有数据是分布在所有主分片之上的(每个主分片承担一部分数据,主分片又分布在不同的节点上)
    • 一个索引的主分片数量只能在创建时指定,es默认情况下数量为5,主分片数量一经指定后期无法修改,除非对数据进行重新构建索引(reindex操作)。
  • 副分片(replica shard

    • 用于解决数据高可用的问题,一个副本分片即一个主分片的拷贝,其数量可以动态调整,通过增加副本分片也可以实现提升系统读性能的作用。
    • 副本分片还可以实现es的故障转移,如果持有主分片的节点挂掉了,一个副本分片就会晋升为主分片的角色。
      • 为了达到故障转移的作用,主分片和其对应的副本分片是不会在同一个节点上的。
    • 对文档的新建、索引和删除请求都是写操作,必须在主分片上面完成之后才能被复制到相关的副本分片。
      • 为了提高写入的能力,ES这个过程是并发写的,同时为了解决并发写的过程中数据冲突的问题,ES 通过乐观锁的方式控制,每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。一旦所有的副本分片都报告写成功才会向协调节点报告成功,协调节点向客户端报告成功。
    • es默认情况下为每个主分片创造一个副本

2.3.2 分片的优势

  1. 突破单节点容量上限,例如我们有10TB大小的总文档,分成20个分片分布于10台节点上,那么每个节点只需要1T的容量即可。
  2. 服务高可用,由于有副本分片的存在,只要不是存储某个文档的node全挂了,那么这个文档数据就不会丢。副本分片提供了灾备的能力。
  3. 故障转移,当主分片节点故障后,可升级一个副分片为新的主分片来应对节点故障。
  4. 扩展性能,通过在所有replicas上并行搜索来提高读性能.由于replicas上的数据是近实时的(near realtime),因此所有replicas都能提供搜索功能,通过设置合理的replicas数量可以极高的提高搜索吞吐量

2.3.3 分片的配置

创建 IndexName 索引时候,在 Mapping 中可以如下设置分片 (curl)

1
2
3
4
5
6
7
8
9
PUT indexName
{
"settings": {
...
"number_of_shards": 5,
"number_of_replicas": 1
...
}
}

或者

1
2
3
4
5
6
7
8
9
curl -H "Content-Type: application/json" -XPUT localhost:9200/indexName -d '
{
"settings": {
...
"number_of_shards": 5,
"number_of_replicas": 1
...
}
}'

当索引创建完成的时候,主分片的数量就固定了,但是复制分片的数量可以随时调整,根据需求扩大或者缩小规模。如把复制分片的数量从原来的 1 增加到 2 :

1
2
3
4
curl -H "Content-Type: application/json" -XPUT localhost:9200/indexName/_settings -d '
{
"number_of_replicas": 2
}'

2.3.4 分片数量

对于生产环境中分片的设定,需要提前做好容量规划,因为主分片数是在索引创建时预先设定的,后续无法修改。

那么分片的数量是否越大越好呢?答案当然是否定的。

  • 分片数设置过小
    • 导致后续无法通过增加节点进行水平扩展。
    • 导致分片的数据量太大,数据在重新分配时耗时;
  • 分片数设置过大
    • 每个分片都是一个小的lucene索引,会消耗相应的资源;
    • 影响搜索结果的相关性打分,影响统计结果的准确性;
    • 单个节点上过多的分片,会导致资源浪费,同时也会影响性能(每个搜索请求会调度到索引的每个分片中.但当分片位于同一个节点,就会开始竞争相同的硬件资源时, 性能便会逐步下降);

默认情况下,ES会为每个索引创建5个分片,即使是在单机环境下。这种冗余被称作过度分配(Over Allocation),目前看来这么做完全没有必要,仅在散布文档到分片和处理查询的过程中就增加了更多的复杂性,好在ES的优秀性能掩盖了这一点。但我们要知道在单机环境下配置5个分片是没有必要的。

分片的数量和大小没有定例,可以参考官方的文档我在 Elasticsearch 集群内应该设置多少个分片?,提取核心要素就是:

  1. “我应该有多少个分片?”
    • 答: 每个节点的分片数量应该保持在保持在低于每1GB堆内存对应集群的分片在20-25之间。
    • 也就是shared number/node GBs <20 或shared number/node GBs <25,即shared number<20 * node GBs 或 shared number<25 * node GBs
  2. “我的分片应该有多大”?
    • 答:分片大小为50GB通常被界定为适用于各种用例的极限,即不应该超过50GB。但实际上,根据经验,小于30GB更为合理

2.3.5 分片和副本的分布

配置一套高可用的集群,我们必须要了解es集群的数据分布和负载原理,我们先来看下es如何分布分片。

2.3.5.1 主分片分布

假设我们只有三个主分片:

  • 单机分片分布:
  • 2个节点分片分布:
  • 3个节点分片分布:
  • 9个节点分片分布:

可以看到,es尽量根据我们指定的分片数来平均分配到各个节点上

2.3.5.2 副本分布

假设我们有3个节点,3个主分片,和若干个副本(下图边框有粗有细,粗的是主分片,细的是副本分片)

  • 1个副本
  • 2个副本
  • 3个副本

可以看到,es依旧尽量根据我们指定的主副分片数来平均分配到各个节点上,但是不会把存着相同数据的主副分片放在同一个节点上

如果分片数量太多(如3个副本的情况),由于此时每台机器都已经占满自己的3个分片了,所以此时需要增加新的机器来存放每个主分片的第三个副本,如果没有新的机器。es不会允许同一个节点有多余的分片,所以提示了Unassigned,表示这些分片未指定。

2.3.5.3 多个索引的分片分布

2.3.6 分片分配策略和原理

详见ELASTICSEARCH ALLOCATION 分析

2.3.7 读写数据时的分片路由

加入我们有一个拥有3个节点的集群,共拥有12个分片,其中有4个主分片(S0、S1、S2、S3)和8个副本分片(R0、R1、R2、R3),每个主分片对应两个副本分片,节点1是主节点(Master节点)负责整个集群的状态。

2.3.7.1 对特定doc的读写操作

写数据是只能写在主分片上,然后同步到副本分片。这里有四个主分片,数据是根据什么规则写到特定分片上的呢?这条索引数据为什么被写到S0上而不写到S1或S2上?那条数据为什么又被写到S3上而不写到S0上了?

首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:

shard = hash(routing) % number_of_primary_shards

routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到余数 。这个在 0 到 numberofprimary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。

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

由于在ES集群中每个节点通过上面的计算公式都知道集群中的文档的存放位置,所以每个节点都有处理读写请求的能力。

在一个写请求被发送到某个节点后,该节点即为前面说过的协调节点,协调节点会根据路由公式计算出需要写到哪个分片上,再将请求转发到该分片的主分片节点上。

假如此时数据通过路由计算公式取余后得到的值是 shard = hash(routing) % 4 = 0,则具体流程如下:

  1. 客户端向ES1节点(协调节点)发送写请求,通过路由计算公式得到值为0,则当前数据应被写到主分片S0上。

  2. ES1节点将请求转发到S0主分片所在的节点ES3,ES3接受请求并写入到磁盘。

  3. 并发将数据复制到两个副本分片R0上,其中通过乐观并发控制数据的冲突。一旦所有的副本分片都报告成功,则节点ES3将向协调节点报告成功,协调节点向客户端报告成功。

2.3.7.2 搜索时的读操作

es最强大的是做全文检索

  1. 客户端发送请求到一个coordinate node。

  2. 协调节点将搜索请求转发到所有的shard对应的primary shard 或 replica shard ,都可以。

  3. query phase:每个shard将自己的搜索结果(其实就是一些doc id)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。

  4. fetch phase:接着由协调节点根据doc id去各个节点上拉取实际的document数据,最终返回给客户端。

写请求是写入primary shard,然后同步给所有的replica shard

读请求可以从primary shard 或者 replica shard 读取,采用的是随机轮询算法。

2.4 段(segment)

数据被分配到特定的分片和副本上之后,最终是存储到磁盘上的,这样在断电的时候就不会丢失数据。

具体的存储路径可在配置文件 ../config/elasticsearch.yml中进行设置,默认存储在安装目录的data文件夹下。建议不要使用默认值,因为若ES进行了升级,则有可能导致数据全部丢失。

1
2
path.data: /path/to/data  //索引数据
path.logs: /path/to/logs //日志记录

segment是实现ES近实时搜索的关键,是数据索引(动词)过程中的重要载体。在说segment前,我们先要了解ES数据的存储和检索原理。

2.4.1 倒排索引

倒排索引从逻辑结构和基本思路上来讲非常简单。下面我们通过具体实例来进行说明,使得读者能够对倒排索引有一个宏观而直接的感受。

假设文档集合包含五个文档,每个文档内容下图所示,在图中最左端一栏是每个文档对应的文档编号。我们的任务就是对这个文档集合建立倒排索引。

中文和英文等语言不同,单词之间没有明确分隔符号,所以首先要用分词系统将文档自动切分成单词序列。这样每个文档就转换为由单词序列构成的数据流,为了系统后续处理方便,需要对每个不同的单词赋予唯一的单词编号,同时记录下哪些文档包含这个单词,在如此处理结束后,我们可以得到最简单的倒排索引:

“单词ID”一栏记录了每个单词的单词编号,第二栏是对应的单词,第三栏即每个单词对应的倒排列表。比如单词“谷歌”,其单词编号为1,倒排列表为{1,2,3,4,5},说明文档集合中每个文档都包含了这个单词。

之所以说上图所示倒排索引是最简单的,是因为这个索引系统只记载了哪些文档包含某个单词,而事实上,索引系统还可以记录除此之外的更多信息:

  • 单词ID:记录每个单词的单词编号;
  • 单词:对应的单词;
  • 文档频率:代表文档集合中有多少个文档包含某个单词
  • 倒排列表:包含单词ID及其他必要信息
  • DocId:单词出现的文档id
  • TF:单词在某个文档中出现的次数
  • POS:单词在文档中出现的位置

以单词“加盟”为例,其单词编号为6,文档频率为3,代表整个文档集合中有三个文档包含这个单词,对应的倒排列表为{(2;1;<4>),(3;1;<7>),(5;1;<5>)},含义是在文档2,3,5出现过这个单词,在每个文档的出现过1次,单词“加盟”在第一个文档的POS是4,即文档的第四个单词是“加盟”,其他的类似。

2.4.2 倒排索引的不变性

写到磁盘的倒序索引是不变的:写到磁盘后,倒排索引就再也不变

这会有很多好处:

  1. 不需要添加锁。如果你从来不用更新索引,那么你就不用担心多个进程在同一时间改变索引。
  2. 因为不变,所以可以很好的做缓存。只要内核有足够的缓存空间,绝大多数的读操作会直接从内存而不需要经过磁盘。这大大提升了性能。
  3. 写一个单一的大的倒序索引可以让数据压缩,减少了磁盘I/O的消耗以及缓存索引所需的RAM。

然而,索引的不变性也有缺点。如果你想让新修改过的文档可以被搜索到,你必须重新构建整个索引

我们来试想一下这样一个场景:对于一个索引内的所有文档,我们将其分词,建立了一个很大的倒排索引,并将其写入磁盘中。

如果索引有更新,那就需要重新全量创建一个索引来替换原来的索引。这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,更无法保证时效性。

2.4.3 分段存储

如何在不丢失不变形的好处下让倒序索引可以更改?答案是:使用不只一个的索引。 新添额外的索引来反映新的更改来替代重写所有倒序索引的方案。

为了解决这个问题,Lucene引入了段(segment)的概念,简单来说,一个段segment存储着若干个文档,以及这些文档的索引信息(如词频,词向量,域(field)索引等),也就是说,一个segment是一个完整的倒序索引的子集。

segment文件中存储的内容,可见【Lucene】Lucene 学习之索引文件结构

所以现在index在Lucene中的含义就是多个segments的集合。文档被成功存储的整个过程如下:

  1. 延迟写策略

    • 如果每次将数据写入磁盘,磁盘的I/O消耗会严重影响性能。故而Lucene采用延迟写策略,新的文档建立时首先在内存中建立索引buffer:
  2. Refresh

    • 当达到默认的时间(1秒钟)或者内存的数据达到一定量时,会触发一次刷新(Refresh),将内存中的数据整合,生成到一个新的段。
    • 此时,按理来说,应该将新生成的段刷进磁盘当中,但是将新的segment提交到磁盘需要fsync来保障物理写入。fsync是很耗时的,它不能在每次文档更新时就被调用,否则性能会很低。
    • 在内存和磁盘之间是文件系统缓存,于是,ES先将新生成的段先写入到内核的文件系统缓存中,这个过程很轻量。
    • 默认情况下每个分片会每秒自动refresh一次。可以通过参数index.refresh_interval来修改这个刷新间隔
    • 我们也可以手动触发 refresh,
      • POST/_refresh 刷新所有索引。
      • POST/nba/_refresh刷新指定的索引。
    • 在这个阶段,新生成的segment(下图灰色)虽然还未写入磁盘,但已经能够被搜索了,这也是es近实时搜索的原理。
  3. Flush

    • 新增的段被刷新到磁盘中
    • 段被写入到磁盘后会生成一个提交点,提交点是一个用来记录所有提交后段信息的文件。
    • 一般Flush的时间间隔会比较久,默认30分钟,或者当translog(后文介绍)达到了一定的大小(超过512M),也会触发flush操作。

这里的内存使用的是ES的JVM内存,而文件缓存系统使用的是操作系统的内存

新的数据会继续的被写入内存,但内存中的数据并不是以段的形式存储的,因此不能提供检索功能。由内存刷新到文件缓存系统的时候会生成了新的段,并将段打开以供搜索使用,而不需要等到被刷新到磁盘。

注意,在内存中的新文档不一定能够被索引,只有生成段后,新文档才可以被索引。

以上是新增文档操作,删除和更新操作过程有些类似:

  • 删除,由于不可修改,所以对于删除操作,不会把文档从旧的段中移除,而是通过新增一个 .del文件,文件中会列出这些被删除文档的段信息。
    • 这个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。
  • 更新,不能修改旧的段来进行反映文档的更新,其实更新相当于是删除和新增这两个动作组成。
    • 会将旧的文档在 .del文件中标记删除,然后文档的新版本被索引到一个新的段中。
    • 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就会被移除。

2.4.4 事务日志(Translog)

虽然通过延时写的策略可以减少数据往磁盘上写的次数,提升了整体的写入能力,但是我们知道文件缓存系统也是内存空间,属于操作系统的内存,只要是内存都存在断电或异常情况下丢失数据的危险。

为了避免丢失数据,Elasticsearch添加了事务日志(Translog),事务日志记录了所有还没有持久化到磁盘的数据。添加了事务日志后整个写索引的流程如下图所示。

  • 一个新文档被索引(动词)之后,先被写入到内存中,但是为了防止数据的丢失,会追加一份数据到事务日志中。不断有新的文档被写入到内存,同时也会不断被记录到事务日志中。这时新数据还不能被检索和查询。
  • 当达到默认的刷新时间或内存中的数据达到一定量后,会触发一次refresh,将内存中的数据以一个新段形式刷新到文件缓存系统中并清空内存。这时虽然新段未被提交到磁盘,但是可以提供文档的检索功能且不能被修改。
  • 随着新文档索引不断被写入,当日志数据大小超过512M或者时间超过30分钟时,会触发一次flush。内存中的数据被写入到一个新段同时被写入到文件缓存系统,文件系统缓存中数据通过 fsync 刷新到磁盘中,生成提交点,日志文件被删除,创建一个空的新日志。
    • 2.4.5 es写操作总结

  1. 先写入内存buffer,在buffer里的时候数据是搜索不到的;同时将数据写入translog日志文件。
  2. 如果buffer快满了,或者到一定时间,就会将内存buffer数据refresh 到一个新的segment file中,但是此时数据不是直接进入segment file磁盘文件,而是先进入os cache。这个过程就是 refresh。
  3. 每隔1秒钟,es将buffer中的数据写入一个新的segment file,每秒钟会写入一个新的segment file,这个segment file中就存储最近1秒内 buffer中写入的数据。

2.4.6 段合并

由于自动刷新流程(refresh)每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦,比如每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段然后合并查询结果,所以段越多,搜索也就越慢。

Elasticsearch通过在后台定期进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档不会被拷贝到新的大段中。合并的过程中不会中断索引和搜索。

段合并在进行索引和搜索时会自动进行,合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中,这些段既可以是未提交的也可以是已提交的。合并结束后老的段会被删除,新的段被 flush 到磁盘,同时写入一个包含新段且排除旧的和较小的段的新提交点,新的段被打开可以用来搜索。

段合并的计算量庞大, 而且还要吃掉大量磁盘 I/O,段合并会拖累写入速率,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好地执行。


参考资料

  1. Elasticsearch权威指南

  2. elasticsearch5.x集群HA### 2.3.3 分片和副本的分布

  3. Elasticsearch(4)— 基本概念(Index、Type、Document、集群、节点、分片及副本、倒排索引)

  4. 【ES】ElasticSearch 深入分片

  5. Elasticsearch分片

  6. elasticsearch节点(角色)类型解释:node.master和node.data

  7. 全文搜索引擎Elasticsearch,这篇文章给讲透了

0%