Elasticsearch

参考网站:

Elasticsearch Guide [6.5] | Elastic

Elasticsearch: 权威指南 | Elastic

es 中的 segment、translog、refresh、flush、fsync、commit poit 等概念介绍_es commit point_王大丫丫的博客-CSDN博客

【Elasticsearch 技术分享】—— 十张图看懂ES原理 !明白为什么说:ES 是准实时的! - 知乎 (zhihu.com)

一、Elasticsearch 概念介绍

1.1 索引(Index)

索引是相似结构的文档的集合,索引中的数据分散在分片上,类似于关系型数据库中的表(Table)。

1.2 类型(Type)

在 ES6.0 版本中已经废除

1.3 文档(Document)

文档是所有可搜索数据的最小单位,类似于关系型数据库中的记录(Row)

1.4 集群(Cluster)

一个集群可以有一个或多个节点

集群的状态

  • Green:主分片与副本都正常分配
  • Yellow:主分片全部正常分配,有副本分配未能正常分配
  • Red:有主分片未能分配

1.5 节点(Node)

节点其实就是一个 ES 实例,本质上是一个 Java 进程

节点类型

  • 主节点(Master Node)
  • 候选主节点(Master-eligible Node)
  • 数据节点(Data Node)
  • 协调节点(Coordinating Node)
  • 冷热节点(Hot & Warm Node)
  • 机器学习节点(Machine Learning Node)
  • 部落节点(Tribe Node)
  • 预处理节点(Ingest Node)

1.6 映射(Mapping)

类似于关系型数据库中的表定义(Schema)

1.7 字段(Field)

类似于关系型数据库中的字段(Column)

1.8 分片(Shard)

ES 将一个索引中的数据切分为多个分片,分片(Shard)是 Elasticsearch 的基本存储单元。

  • Elasticsearch 索引(Elasticsearch Index)由 n 个分片(Shard)组成
  • 一个分片(Shard)其实就是一个 Lucene 索引(Lucene Index)
  • 一个 Lucene 索引(Lucene Index)由 n 个段(Segment)组成

所以当我们查询一个 Elasticsearch 索引时,查询会在所有分片上执行,继而到段,最后合并所有查询结果返回。

分片类型

  • 主分片(Primary Shard)
  • 副本分配(Replica Shard)

1.9 分段(Segment)

Elasticsearch 可以对全文进行检索主要归功于【倒排索引(Inverted Index)】。倒排索引可以由关键词直接定位到对应的文档,其被写入磁盘后是不可改变的。倒排索引的不变性有几个好处:

  • 索引不能更新,所以不需要锁
  • 由于索引不变,只要系统内存足够大,大部分读请求可以直接命中内存而不用请求磁盘,极大提高性能
  • 如果使用了其他缓存工具,在缓存的有效周期内,索引始终有效
  • 写入单个大的倒排索引允许数据被压缩,以减少磁盘 I/O 和需要被缓存到内存的索引的使用量

同时由于倒排索引的不可变性,这意味着当需要新增文档的时候,需要对整个索引进行重建。而当数据更新频繁的时候,这问题更是会变成灾难。因此 Elasticsearc 使用了一种【近实时搜索(Real-time Search)】的策略,来处理这个问题。

而在 Lucene 中,单个倒排索引文件被称为 Segment,多个 Segment 汇总在一起称为 Lucene 的 Index,对应的就是 ES 的 Shard:

1 × Shard = 1 × Lucene Index = n × Segment = n × n × Document

分段合并(Segment Merge)

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

在 ES 的后台运行一个任务,来检测当前磁盘中的 Segment ,对符合条件的 Segment 进行合并操作。小的段被合并到大的段,然后这些大的段再被合并到更大的段。段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。

ES Merge 操作的对象,是位于 FileSystem Cache 尚未 flush 到磁盘的新段和已经 flush 到磁盘中的老段。

  1. 每秒的刷新(refresh)操作会创建新的段并将段打开以供搜索使用
  2. 合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中(这并不会中断索引和搜索)

Two commited segments and one uncommited segment in the process of being merged into a bigger segment

  1. 新段被 flush 到磁盘,写入一个包含新段且排除了旧的和较小的段的新的提交点(commit point)
  2. 新段被打开搜索,老段删除(物理删除),合并结束

一旦合并结束,老的段被删除

要注意的是,一个大的 Segment 的 Merge 操作是很消耗 CPU、IO 资源的,如果使用不当会影响到本身的 search 查询性能。ES 默认会控制 Merge 进程的资源占用以保证 Merge 期间 search 具有足够资源。

分段合并默认策略

  • index.merge.policy.floor_segment:默认 2MB,小于这个大小的 segment,优先被归并;
  • index.merge.policy.max_merge_at_once:默认一次最多归并 10 个 segment;
  • index.merge.policy.max_merge_at_once_explicit:默认 forcemerge 时一次最多归并 30 个 segment;
  • index.merge.policy.max_merged_segment:默认 5GB,大于这个大小的 segment,不用参与归并。force merge 除外

分段合并策略优化

  1. 对于非实时业务可以加大 refresh 的时间间隔,减少 segment 的生成数量;
  2. 加大 flush 间隔,尽量让每次新生成的 segment 本身大小就比较大;
  3. 提前做好 force merge 减少业务高峰期的 merge 的操作

索引优化技巧

1.10 刷盘(commit)

commit VS flush

在 Elasticsearch 中,flush 和 commit 是两个不同的操作。

  • flush 操作是将内存中的数据刷新到磁盘上的过程。它确保了在节点崩溃或关闭时,数据不会丢失。flush 操作是一个轻量级的操作,它将内存中的数据写入磁盘,但不会等待数据被持久化到磁盘上。
  • commit 操作是将刷新的数据持久化到磁盘上的过程。它确保了数据在节点崩溃或关闭时的持久性。commit 操作是一个相对较重的操作,它会等待数据被完全持久化到磁盘上。

简而言之,flush 操作是将内存中的数据写入磁盘,这个过程是可以异步的,即数据写入磁盘后不需要等待确认。而 commit 操作是将数据持久化到磁盘上,这个过程是同步的,需要等待数据被完全写入磁盘并确认后才返回。

二、Elasticsearch Json 结构示例

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
31
{
"_id": "KJTnBYkB5GK1mQJI5u1p", // 文档唯一ID
"_index": "properties_camera_gn_2023_06", // 文档所属索引名
"_type": "_doc", // 文档所属类型名
"_version": 1, // 文档的版本信息
"_score": 1, // 评分,为相关打分,是这个文档在这次查询中的算分(暂时理解为与查询条件的相关性)
"_source": { // 文档元素json数据,当搜索文档的时候,默认返回的就是_source这个字段
"createTime": 1688021231207,
"deviceId": "7a367c995e8044f8bd4fe3a042be789c",
"intValue": 333,
"messageId": "871806074498596864",
"messageType": "system",
"property": "fdsa",
"propertyType": "int",
"timestamp": 1688021231161,
"type": "built-in"
},
"fields": {
"createTime": ["2023-06-29T06:47:11.207Z"],
"deviceId": ["7a367c995e8044f8bd4fe3a042be789c"],
"intValue": [333],
"messageId": ["871806074498596864"],
"messageType": ["system"],
"property": ["fdsa"],
"property.keyword": ["fdsa"],
"propertyType": ["int"],
"propertyType.keyword": ["int"],
"timestamp": ["2023-06-29T06:47:11.161Z"],
"type": ["built-in"]
}
}

三、Elasticsearch CRUD

3.1 增

Index

1
2
PUT my_index/_doc/1
{"user":"mike", "comment":"You know, for search"}

Create

1
2
3
4
5
PUT my_index/_create/1
{"user":"mike", "comment":"You know, for search"}

POST my_index/_doc(不指定 ID,则会自动生成)
{"user":"mike", "comment":"You know, for search"}

Index VS Create

Index 和 Create 插入时都会检测 _version,即通过 id 得出文档的版本号(只获取版本而不是 doc 的全部内容,能够从一定程度上减少系统的开销)。如果没有指定文档 id 直接 add 固然最好,但如果指定了文档 id,那么就要进行更耗时的 update 操作,不同的是:

  • Index 如果没有指定 version,那对于已有的 doc,_version 会递增,并对文档进行覆盖。如果指定了 _version,但与已有文档的 _version 对不上,则插入失败;如果对得上,则覆盖,_version 递增。
  • Create 通过 version,判断出文档已存在,则直接插入失败,抛出一个已经存在的异常。

用途

在批量请求的时候最好使用 create 方式进行导入。

假如你批量导入一个大小为 500MB 的文件,中途突然网络中断,可能其中有 5 万条数据已经导入,那么第二次尝试导入的时候,如果选用 index 方式,那么前 5 万条数据又会重复导入,增加了很多额外的开销,如果是 create 的话,ES 针对 bulk 操作机制是忽略已经存在的(当然在 bulk 完成后会返回哪些数据是重复的),这样就不会重复被导入了。

3.2 删(Delete)

1
DELETE my_index/_doc/1

由于倒排索引(Inverted Index)的不可变性,即其分段(Segment)是不可变的,所以在进行删除操作的时候,文档 Document 并不会从 Segment 中移除。而是在每次 commit 或者 flush 的时候,在 .del 文件中列出被删除的 Document,然后在查询的时候,在查询结果返回之前会过滤掉这些被删除的 Document。(逻辑删除)

3.3 改(Update)

1
2
POST my_index/_update/1
{"doc":{"user":"mike", "comment":"You know, Elesticsearch"}}

由于 Lucene 中的 update 其实就是覆盖替换,并不支持针对特定 Field 进行修改,因此 ES 中的 update 为了实现针对特定字段修改,在 Lucene 的基础上做了一些改动:

  1. 每次 update 都会调用 InternalEngine 中的 get 方法,来获取整个文档信息;

  2. 对旧的 Document 标记被删除,写入到 .del 文件中(逻辑标记、逻辑删除);

  3. 把带有新 Document 的 Segment 写入(增量保存)

从而实现针对特定字段进行修改。但这也就导致了每次更新要获取一遍原始文档,性能上会有很大影响。

所以根据使用场景,有时候使用 index 会比 update 好很多。

3.4 查(Read)

1
GET my_index/_doc/1

四、Elasticsearch API

Elasticsearch Bulk API

在一个 REST 请求中,重新建立网络开销十分损耗性能,因此 ES 提供 Bulk API,支持在一次 API 调用中,对不同的索引进行操作,从而减少网络传输开销,提升写入速率。

它支持 Index、Create、Update、Delete 四种类型操作,可以在 URI 中指定索引,也可以在请求的方法体中进行。

同时多条操作中如果其中有一条失败,也不会影响其他的操作,并且返回的结果包括每一条操作执行的结果。

示例:

1
2
3
4
5
6
POST _bulk
{"index":{"_index":"users","_id":"3"}}
{"username":"wumx"}
{"delete":{"_index":"users","_id":"1"}}
{"update":{"_index":"users","_id":"2"}}
{"doc":{"age":"12"}}
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{
"took": 126,
"errors": true,
"items": [{
"index": {
"_index": "users",
"_type": "_doc",
"_id": "3",
"_version": 8,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 28,
"_primary_term": 1,
"status": 200
}
}, {
"delete": {
"_index": "users",
"_type": "_doc",
"_id": "1",
"_version": 3,
"result": "deleted",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 29,
"_primary_term": 1,
"status": 200
}
}, {
"update": {
"_index": "users",
"_type": "_doc",
"_id": "2",
"status": 404,
"error": {
"type": "document_missing_exception",
"reason": "[_doc][2]: document missing",
"index_uuid": "r6N_mV8VRwmxfc4TyoKIqA",
"shard": "0",
"index": "users"
}
}
}]
}

took 表示消耗了 93 毫秒,errors 为 true 表示在这些操作中错误发生,发现是 update 操作发生了错误,id 为 2 的文档不存在,所以报错了。

在使用 Bulk API 的时候,当 errors 为 true 时,需要把错误的操作修改掉,防止存到 ES 的数据有缺失。

Elasticsearch _mget API

批量查询需要指明要查询文档的 id,可以在一个 _mget 操作里查询不同索引的数据,可以减少网络连接所产生的开销,提高性能。

下面我们来实际操作下,输入以下代码执行,就可以得到文档 id 为 1,3 的数据。

1
2
3
4
5
6
7
8
9
10
GET /_mget
{
"docs": [{
"_index": "users",
"_id": "1"
}, {
"_index": "users",
"_id": "3"
}]
}
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
{
"docs": [{
"_index": "users:",
"_type": "_doc",
"_id": "1",
"_version": 2,
"_seq_no": 27,
"_primary_term": 1,
"found": true,
"_source": {
"username": "wupx",
"age": "18",
"job": "coder",
"message": "hello world"
}
}, {
"_index": "users",
"_type": "_doc",
"_id": "3",
"_version": 7,
"_seq_no": 24,
"_primary_term": 1,
"found": true,
"_source": {
"username": "wumx"
}
}]
}

Elasticsearch fresh API

1
2
3
4
5
// 刷新所有索引
POST /_refresh

// 指定刷新索引
POST /index_name/_refresh

Elasticsearch flush API

五、总结与分析

  1. ES 建立在高性能的 Lucene 基础之上,依据 Inverted Index 作为核心数据结构,实现了对海量数据的高效查询
  2. ES 通过引入分片概念,成功地将 Lucene 部署到分布式系统中,实现了分布式环境下的高可用
  3. ES 通过定时 refresh lucene in-momory-buffer 中的数据,实现了**【近实时】的写入和查询能力**;
  4. ES 通过引入 translog,多副本机制(即:一个分片的主副分片不能分片在同一个节点上),以及定期执行 flush、Segment Merge 等操作,进一步保证了数据可靠性和较高的存储性能。





  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 Liangxj
  • 访问人数: | 浏览次数: