一.Es的组成部分
- index 索引: 索引类似于mysql 中的数据库,Elasticesearch 中的索引是存在数据的地方,包含了一堆有相似结构的文档数据。
- type 类型: 类型是用来定义数据结构,可以认为是 mysql 中的一张表,type 是 index 中的一个逻辑数据分类(在7.0版本中去除)。
- document 文档: 类似于 MySQL 中的一行,不同之处在于 ES 中的每个文档可以有不同的字段,但是对于通用字段应该具有相同的数据类型,文档是es中的最小数据单元,可以认为一个文档就是一条记录。
- field 字段: field是Elasticsearch的最小单位,一个document里面有多个field。
- shard 分片: 单台机器无法存储大量数据,es可以将一个索引中的数据切分为多个shard,分布在多台服务器上存储。有了shard就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。
- replica 副本: 任何一个服务器随时可能故障或宕机,此时 shard 可能会丢失,因此可以为每个 shard 创建多个 replica 副本。replica可以在shard故障时提供备用服务,保证数据不丢失,多个replica还可以提升搜索操作的吞吐量和性能。primary shard(建立索引时一次设置,不能修改,默认5个),replica shard(随时修改数量,默认1个),默认每个索引10个 shard,5个primary shard,5个replica shard,最小的高可用配置,是2台服务器。
二、概念介绍
1)segment : 众所周知,Elasticsearch 存储的基本单元是 shard , ES 中一个 Index 可能分为多个 shard , 事实上每个 shard 都是一个 Lucence 的 Index ,并且每个 Lucence Index 由多个 Segment 组成, 每个 Segment 事实上是一些倒排索引的集合, 每次创建一个新的 Document , 都会归属于一个新的 Segment , 而不会去修改原来的 Segment ; 且每次的文档删除操作,会仅仅标记 Segment 中该文档为删除状态, 而不会真正的立马物理删除, 所以说 ES 的 index 可以理解为一个抽象的概念。es 每秒都会生成一个 segment 文件,当文件过多时 es 会自动进行 segment merge(合并文件),合并时会同时将已经标注删除的文档物理删除;
2)translog: translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。为了防止 elasticsearch 宕机造成数据丢失保证可靠存储,es 会将每次的操作同时写到 translog 日志中。新文档被索引意味着文档会被首先写入内存 buffer ,操作会被写入 translog 文件。每个 shard 都对应一个 translog 文件;translog 会每隔 5 秒异步执行或者在每一个请求完成之后执行一次 fsync 操作,将 translog 从缓存刷入磁盘,这个操作比较耗时,如果对数据一致性要求不是跟高时建议将索引改为 async ,如果节点宕机时会有 5 秒数据丢失;
3)refresh:写入和打开一个新 segment 的轻量的过程,es 接收数据请求时先存入内存中,默认每隔一秒会从内存 buffer 中将数据写入 filesystem cache 中的一个 segment,内存 buffer 被清空,这个时候索引变成了可被搜索的,这个过程叫做 refresh;
4) flush:es 默认每隔 30 分钟或者操作数据量达到 512mb ,会将内存 buffer 的数据全都写入新的 segment 中,内存 buffer 被清空,一个 commit point 被写入磁盘,并将 filesystem cache 中的数据通过 fsync 刷入磁盘,同时清空 translog 日志文件,这个过程叫做 flush;
5) fsync:fsync 是一个 Unix 系统调用函数, 用来将内存 buffer 中的数据存储到文件系统. 这里作了优化, 是指将 filesystem cache 中的所有 segment 刷新到磁盘的操作;
6) commit:为了数据安全, 每次的索引变更都最好要立刻刷盘, 所以 Commit 操作意味着将 Segment 合并,并写入磁盘。保证内存数据尽量不丢。刷盘是很重的 IO 操作, 所以为了机器性能和近实时搜索, 并不会刷盘那么及时。
7) commit point: 记录当前所有可用的 segment ,每个 commit point 都会维护一个 .del 文件( es 删除数据本质是不属于物理删除),当 es 做删改操作时首先会在 .del 文件中声明某个 document 已经被删除,文件内记录了在某个 segment 内某个文档已经被删除,当查询请求过来时在 segment 中被删除的文件是能够查出来的,但是当返回结果时会根据 commit point 维护的那个 .del 文件把已经删除的文档过滤掉;
三.倒排索引
倒排索引的特点是根据匹配到的分词映射到文档id,而非传统的由id映射到具体的数据。
在搜索引擎中,每个文档都有一个对应的文档 ID,文档内容被表示为一系列关键词的集合。例如,某个文档经过分词,提取了 20 个关键词,每个关键词都会记录它在文档中出现的次数和出现位置。那么,倒排索引就是 关键词到文档 ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了该关键词。
四.Es写数据的流程
1.数据写入第一阶段写入内存
最开始一个写入请求进来,直接写入内存中的buffer,这样做是为了速度。这时候这份写入数据还不能被查询到。与数据写入buffer并行的是数据写入translog,这是为了提高数据的可靠性。如果这时候buffer里面的数据丢了,translog的数据可以恢复。
- translog的数据写入的是流水账结构简单速度快。它可以每次记录流水都刷盘,保证数据不丢,但是默认配置是5秒刷盘一次,这样可以提高效率。
- translog和主流程的区别类似一个银行有两个记账员,主记账员记的总账,比如记录每个用户账号里面还有多少钱,每次要和之前数据比对汇总,次记账员只记录用户干了什么。所以次记账员比较轻松,可以记更加快。如果主记账员的当月账本丢了,次记账员哪里每天做的流水还能恢复主账本。
2.数据写入第二阶段写入OSCache(refresh)
buffer里面的内容每隔1秒钟就生成一个segment,然后es把segment写入OSCache。OSCache是操作系统管理的,也在内存中,如果计算机正常关闭,操作系统会让、完成OSCache落盘才关机,如果异常关闭OSCache 数据就会丢失。写入OSCache的数据已经可给操作系统了,已经可以读取到了。es每秒生成一个segment,所以es写入到能够读取到数据之间的延时理论最多1秒。
3.数据写入第三阶段写入磁盘(flush)
OSCache里面的数据并是不可靠的,如果数据丢失,可以通过translog来恢复。前面说了,写入buffer的时候同时在translog里面做了记录,所以translog里面有全部的 位于OSCache中的segment的内容。重放translog就能得到这部分数据。
translog并不能让它无限增长,所以当translog达到一定尺寸,或者一定时间(30分钟),就可以清空一下translog。translog的内容=buffer+OSCache segment。
- 第一步,把buffer里面的内容refresh成一个新的segment。
- 第二步,把OSCache里面的segment合并,并且刷盘,一起刷入磁盘的还有几号以后得segment是已经刷盘了(commit point)。
- 第3步清空translog。
五.Es检索数据的流程(Query Then Fetch)
- 客户端发送请求到一个coordinate node。协调节点会创建一个大小为 from + size 的空优先队列。
- 协调节点将搜索请求转发到所有的shard对应的primary shard 或 replica shard ,都可以。每个分片在本地执行查询并添加结果到大小为 from + size 的本地有序优先队列中。
- query phase:每个shard将自己的搜索结果(所有文档的 ID和排序值)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作。
- fetch phase:接着由协调节点根据doc id去各个节点上拉取实际的document数据,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表,最终返回给客户端。
写请求是写入primary shard,然后同步给所有的replica shard
读请求可以从primary shard 或者 replica shard 读取,采用的是随机轮询算法。
六.Es为什么是准实时的
因为Es检索数据是从segment中检索的,Es写入数据的buffer里面的内容每隔1秒钟就生成一个segment,然后es把segment写入OSCache,此时才能被检索到,所以Es从数据写入到可以检索到有1秒左右的延迟。
七.Es和数据库的一致性问题解决方案
1.业务直接双写数据库和Es
优点:
逻辑简单,在原有的逻辑基础上,增加一个写入逻辑即可。
实时性高,几乎是同时写入。
缺点:
增加一次写操作,影响性能,特别是在高并发的时候。
当数据库写入成功后,Es写入失败,会有不一致的风险。
2.数据库写成功后,生产一个MQ消息,由消费者写Es
优点:
MQ的使用使得MySQL和ES之间的依赖性降低,提高了系统的可难搞性和扩展性。
MQ可以提供消息的持久化存储,确保即使系统故障,消息也不会丢失,保障最终一致性。
MQ和数据库互相备份,当一个系统崩溃时可以从另外一个系统恢复。
缺点:
引入了MQ,增加了系统复杂度。
MQ消费积压时,会降低Es和数据库的实时性,导致Es中查不到数据。
3.监听Mysql的binlog
优点:
不用入侵业务代码,原有业务不需要做改造。
缺点:
引入了binlog监听,增加了工作量。
果采用MQ消费解析的Binlog信息,也会像方案二一样存在MQ延时的风险。
八.什么是doc values
在es搜索中使用的是倒排索引的数据结构,而在聚合中使用的是一个叫Doc Values的数据结构,该结构可以使得聚合更快、更高效对内存更友好。
当你需要在索引中检索一个值时,此时倒排索引的结构非常适合索引,但是当你对该值进行排序时,该结构就不太适合了,因为我们需要获取该文档中该字段对应的值,此时我们就需要正排索引。
在es中,Doc Values 就是一种列式存储结构,默认情况下,在索引一个文档时,也会创建对应的Doc Values 。
es中的 Doc Values 常被应用到以下场景:
(1)对一个字段进行排序。
(2)对一个字段进行聚合。
(3)某些过滤,比如地理位置过滤。
(4)某些与字段相关的脚本计算。
九.Es的数据类型有哪些
-
文本类型:
-
数值类型:
- integer、long、float、double:这些数值字段会被建立索引,存储在有序的结构中,便于快速查找和范围查询。
-
日期类型:
- date:日期字段会被转换为毫秒数(自Unix纪元以来的毫秒数),并存储在索引中,支持排序和范围查询。
-
布尔类型:
- boolean:布尔字段被存储为true或false,并建立索引以便快速查找。
-
二进制类型:
- binary:二进制数据字段通常不被建立索引,而是存储在文档中。如果需要搜索二进制数据,可以使用特殊的技术,如倒排索引的扩展或使用外部索引服务。
-
复合类型:
-
地理空间类型:
-
特殊类型:
- ip:用于存储IP地址。
十.Es倒排索引的前缀字典树(Term Index)
Term Index只保存了每个term的前缀,比如一本英文词典,A开头的词从第几页开始,AN开头的词从第几页开始,每一个节点都保存了该前缀在Term Dictionary的偏移量。将所有的term进行字典排序,查找目标词时用二分查找,时间复杂度为logN
倒排索引的工作流程:
目标词=>Term Index(查找大概的位置,得到要查询的起始offset和截止offset)=>Term Dictionary(根据offset进行二分查找)=>Posting List(获取目标文档id)
十一.如何解决Es的深分页问题
深分页问题的原因
Elasticsearch 的分页机制是基于 from
和 size
参数的:
-
from
:指定从第几条文档开始返回。 -
size
:指定返回的文档数量。
当 from
的值很大时(例如 from=10000
),Elasticsearch 需要执行以下操作:
-
从每个分片中获取
from + size
条文档。 -
在协调节点(Coordinating Node)上对所有分片的结果进行排序和合并。
-
返回指定范围的文档。
这种机制会导致:
-
内存消耗高:需要在内存中存储大量文档。
-
性能下降:随着
from
的增大,查询时间显著增加。 -
资源浪费:大量文档被加载但最终被丢弃。
解决方案
方案 1:使用search_after
参数
search_after
是一种基于游标的分页方式,适合深分页场景。它通过指定上一页的最后一个文档的排序值来获取下一页的数据。
步骤:
-
在查询中指定排序字段(必须包含唯一字段,如
_id
)。 -
使用
search_after
参数传递上一页最后一个文档的排序值。
GET /my_index/_search
{
"size": 10,
"sort": [
{"timestamp": "asc"},
{"_id": "asc"}
],
"search_after": [1633036800000, "abc123"]
}
-
优点:性能高,适合深分页。
-
缺点:需要维护排序值和游标。
方案 2:使用 scroll
API
scroll
API 适合需要遍历大量数据的场景(如数据导出)。它会创建一个快照,允许在多次请求中逐步获取数据。
步骤:
-
初始化
scroll
查询,指定快照的存活时间。 -
使用
scroll_id
逐步获取数据。
# 初始化 scroll
POST /my_index/_search?scroll=1m
{
"size": 100,
"query": {
"match_all": {}
}
}
# 使用 scroll_id 获取下一页
POST /_search/scroll
{
"scroll": "1m",
"scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAA..."
}
-
优点:适合大数据量的遍历。
-
缺点:快照会占用资源,不适合实时分页。
方案 3:使用 slice
并行化
slice
可以将一个大查询拆分为多个小查询并行执行,适合大数据量的分页。
GET /my_index/_search
{
"slice": {
"id": 0, # 分片 ID
"max": 5 # 总分片数
},
"query": {
"match_all": {}
}
}
-
优点:提高查询效率。
-
缺点:需要客户端合并结果。
方案 4:限制最大分页深度
在 Elasticsearch 中可以通过 index.max_result_window
参数限制 from + size
的最大值(默认 10000)。如果超过该值,查询会失败。
PUT /my_index/_settings
{
"index": {
"max_result_window": 50000
}
}
-
优点:防止用户查询过深的分页。
-
缺点:无法完全解决深分页问题。
方案对比
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
search_after | 深分页、实时分页 | 性能高,适合深分页 | 需要维护排序值和游标 |
scroll | 大数据量遍历、数据导出 | 适合大数据量 | 快照占用资源,不适合实时分页 |
slice | 大数据量并行查询 | 提高查询效率 | 需要客户端合并结果 |
限制分页深度 | 防止用户查询过深的分页 | 简单易实现 | 无法完全解决深分页问题 |
参考文章: