ES 中的概念与名词


# ES 中的概念与名词

下面将自上而下介绍几个 ES 中常用的概念与名词。

# 集群层面的基本概念

# 集群(Cluster)

一台服务器安装一个 ES 称为单机 ES,因为它是支持分布式的,多个协同工作的 ES 实例就组合成了集群。分布式的 ES 集群可以存储海量的数据,也可以从容地面对更高的并发量。

得益于分布式系统的架构设计,使得 ES 集群拥有高可用性和可扩展性。

  • 高可用性,分为服务可用性、数据可用性。
    • 服务可用性,在有部分节点挂掉的情况下系统还可以对外提供服务。
    • 数据可用性,部分节点挂掉,并且这些节点的数据无法恢复的情况下,也能保证数据不丢失。
  • 可扩展性,当并发量提升,或者数据量增多的情况下,可以通过增加节点数来解决问题。

# 节点(Node)

在集群中,每一个 ES 服务实例就是一个节点(Node1、Node2、Node3),本质上就是一个 Java 进程。每个实例都有自己的名字,就是配置里的 'node.name' 设置的内容。为了标识每个节点,每个节点启动后都会分配一个 UID,存储在 data 目录。各个节点受到集群的管理,我们可以通过增加或者减少节点来达到扩容或减容的目的。

集群里的节点是有分类的,就好像一家公司的不同部门,负责不同的业务和工作,主要的节点类型如下。

  • 主节点(Master)。主节点在整个集群是唯一的,Master 从有资格进行选举的节点(Master Eligible)中选举出来。主节点主要负责管理集群变更、元数据的更改。
  • 数据节点(Data Node)。负责保存数据,要扩充存储时候需要扩展这类节点。数据节点还负责执行数据相关的操作,如:搜索、聚合、CURD 等。所以对服务器的 CPU、内存、I/O 要求都比较高。
  • 协调节点(Coordinating Node)。负责接受客户端的请求,将请求路由到对应的节点进行处理,并且把最终结果汇总到一起返回给客户端。因为需要处理结果集和对其进行排序,需要较高的 CPU 和内存资源。
  • 预处理节点(Ingest Node)。预处理操作允许在写入文档前通过定义好的一些 processors(处理器)和管道对数据进行转换。默认情况下节点启动后就是预处理节点。
  • 部落节点(Tribe Node)。部落节点可以连接到不同集群,并且支持将这些集群当成一个单独的集群处理。但它在未来的版本中将会被淘汰。
  • Hot & Warm Node。不同硬件配置的 Data Node,用来实现 Hot & Warm 架构的节点,有利于降低集群部署成本。例如,在硬件资源好的机器中部署 Hot 类型的数据节点,而在硬件资源一般的机器上部署 Warm Node 节点。

在生产环境中建议将每个节点设置为单一角色。如果业务量或者并发量不大的情况下,为了节省成本可以调整为一个节点多种角色的集群。在开发环境中的话,为了节省资源,一个节点可以设置多种角色(伪集群模式)。

ES 版本变化比较快,所以各个节点类型的配置方式也会变动,具体的可以参考 官方文档 (opens new window)

# 分片(Shard)

一般来说,面对海量数据的时候,分布式系统可以通过增加机器数量来进行水平扩展。所以,系统需要将数据分成多个小块数据,并且尽量均匀地分配到各个机器上,然后可以通过某种策略找到对应数据块所在的位置。

分片(Shard)是 ES 底层基本的读写单元,分片是为了分割巨大的索引数据,让读写可以由多台机器来完成,从而提高系统的吞吐量。

Shard 示例

(Shard 示例)

如上图,数据集 Data 按某些规则分为 4 个部分,然后被存储到 4 个节点上面(一个节点一个分片)。

# 副本(Replica)

为了保证数据可靠性,一般分布式系统都会对数据进行冗余备份,这个备份也就是副本了。

ES 将数据副本分为主从两类型:主分片(primary shard)和副分片(replica shard)。

在写入的过程中,先写主分片,成功后并发写副分片,在数据恢复时以主分片为主。多个副本除了可以保证数据可靠性外,还有一个好处是可以承担系统的读负载。

可以在 Kibana 中运行下面指令来设置分片数量和副本数量:

# 创建 mapping 的时候定义好分片和副本数量。
PUT books
{
  "mappings": {
    "properties": {
        "book_id": {
          "type": "keyword"
        },
        "name": {
          "type": "text"
        }
      }
  },
  "settings": {
    "number_of_shards": 2, # 定义了 2 个分片
    "number_of_replicas": 2 # 定义了每个分片 2 个副分片
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

用上述指令创建 books 索引后,其在 Cerebro 中显示如下图,在红色箭头处 shards: 2 * 3 的意思是共有 2 个分片,每个分片一个主分片和 2 个副分片,加起来就是 3 个副本了。图中黄色箭头处的 0 和 1 代表的是两个分片:分片 0 、分片 1,实线代表主分片,虚线代表分片副本。

主分片和副分片示例

(主分片和副分片示例)

# 集群健康状态

通过集群的健康状态,我们可以了解集群是不是出现问题了。集群健康状态有以下 3 种。

  • Green,集群处于健康状态,所有的主分片和副本分片都正常运行。
  • Yellow,所有的主分片都运行正常,但是有部分副本分片不正常,意味着可能存在单点故障的风险(如果部分主分片没有备份了,一旦这个主分片数据丢失,将导致这些数据永久丢失)。如果集群只有 3 个数据节点,但是分配了 4 个副本(主分片 + 副本分片的总数),这个时候有一个副本无法分配的,因为相同的两份数据不应该被分配到同一个节点上。
  • Red,有部分主分片没有正常运行。

需要注意的是,每个索引也有这三种状态,如果索引丢失了一个副本分片,那么这个索引和集群的状态都变为 Yellow 状态,但是其他索引的的状态仍为 Green

# 集群层面的基本概念

# 索引(Index)

可以简单认为索引是类似于 MySQL 数据库的表。

索引是一类相似文档的集合。ES 将数据存储在一个或者多个 Index 中,例如将用户数据存储在 User Index 中,而将订单数据存储在 Order Index 中。一个索引有一个或者多个分片,索引的数据会以某种方式分散到各个分片上去存储。

Index 示例

(Index 示例)

如上图,索引有 3 个分片。主分片分别是 P1、P2、P3,对应的副本分片为 R1、R2、R3。它们分别位于 3 个节点中。

可以发现主分片和其副本分片不会同时分配在同一个节点上,这样是为了保证当一个节点上的主分片下线时,其他节点上的从副本可以升级为主分片,保证数据的可靠性。

# Mapping

可以简单认为 Mapping 是类似于 MySQL 数据库表结构的定义。

Mapping 定义了索引里的文档到底有哪些字段及这些字段的类型,类似于数据库中表结构的定义。Mapping 有两种作用:

  • 定义了索引中各个字段的名称和对应的类型
  • 定义各个字段、倒排索引的相关设置,如使用什么分词器等

需要注意的是,Mapping 一旦定义后,已经定义的字段的类型是不能更改的。

# 文档(Doc)

我们向 ES 中写入的每一条数据都是一个文档(类似数据库中的一条记录),并且我们搜索也是以文档为单位的,所以文档是 ES 中的主要实体。

在 Kibana 中运行下面指令来插入一条书本的记录:

# 我们指定了文档的 id 为1
POST /books/_doc/1
{
  "book_id":"123",
  "name":"linux 从入门到放弃"
}
1
2
3
4
5
6

在 Kibana 中运行以下命令来查询指定 key-value 的数据:

# 搜索
POST books/_search
{
  "query": {
    "match_phrase": {
      "book_id": "123"
    }
  }
}

# 返回的结果
{
  ......
  "hits" : {
    ......
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "books",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.2876821,
        "_source" : {
          "book_id" : "123",
          "name" : "linux 从入门到放弃"
        }
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

从返回结果中可以看到,之前插入的数据包含在 _source 字段里,结果中还带有其他字段,这些额外的字段都是 ES 为文档加上的元数据,下面是这些字段的解析。

  • _index:文档所属的索引名字,上述是 books。
  • _type:文档所属的类型名字,现在 ES 7.x 版本的类型统一为 "_doc" 。
  • _id:文档的唯一 id。如果我们插入时不指定文档 id,ES 会随机分配,这样有利于数据均匀分散到各个分片。
  • _version:文档的版本信息,并发读写时可以解决文档冲突。
  • _score:相关性算分,代表着查询的匹配性,用来排序。
  • _seq_no_primary_term:是 ES 内部用来保证主分片和副本数据一致性的。

总体来说,文档有以下几个特性。

  • ES 是面向文档的并且以文档为单位进行搜索的,如一条书本记录。
  • 文档以 JSON 格式进行序列化存储。
  • 每个文档都有唯一的 ID。插入数据时不手动指定 ID 的性能会好点,因为系统不需要进一步判断这个 ID 是否已经存在。

# 字段(Field)

每个文档都有一个或者多个字段,例如 books 索引指定了书本的记录有 book_idname 两个字段,这些字段都有指定的类型。字段本质上就是 JSON 中的 Key。

# 词项(Term)

将全文本的内容进行分词后得到的词语就是词项了。例如 "Programmers Love Cat" 使用标准分词器分词后得到 [programmer, love, cat] 这 3 个词项。需要注意的是:分词器除了进行分词外还会进行大小写转换等其他操作。

# 倒排索引与正排索引

这是两种数据结构。

正排索引保存了实体 ID 到实体数据的关联关系。MySQL InnoDB 的索引就是正排索引,使用的是 B+ 树来实现。

正排索引示例

(正排索引示例)

正排索引保存了词项到文档实体的关联关系。倒排索引的具体实现先不展开,先简单了解一下这个概念就可以了。

倒排索引示例

(倒排索引示例)

# 系统层面上的基本概念

# 近实时系统

ES 是一个近实时系统,我们写入的数据默认的情况下会在 1 秒后才能被查询到。

ES 每秒都会把缓存中的数据写入到 Segment 文件中(写入到 Segment 中才能被检索),然后根据某些规则进行刷盘,并且合并这些 Segment。所以需要注意的是,不能在写入数据成功后,立刻进行查询,这个时候可能会出现查询不到数据或者获取到旧数据的情况。

# Lucene 与 ES 的关系

Lucene 是一个用于全文检索的开源项目,ES 在搜索的底层实现上用的就是 Lucene。可以简单认为,ES 就是在 Lucene 上增加了分布式特性的系统服务。

Lucene 也有索引,那 Lucene 的索引和 ES 的索引是个怎么样的关系呢?其实 ES 上的分片(Shard)就是一个完整的 Lucene 索引。

(完)