Shard 分片,是 es 中最小的工作单元,它实际上就是一个 Lucene Index实例(Index:文档的容器)。Lucene 底层的 Index 是分为多个 segment 的,每个 segment(段)都会存放部分数据;
下面以一个简化后的 document 写入流程了解分片内部的实现机制,同时思考一些问题:
- 为什么 es 的搜索时近实时的(1 秒后被搜到)?
- es 如何保证在断电时数据也不会丢失?
- 为什么删除文档,并不会例可释放空间?
1. 简化版的es实现document写入流程
(1)数据写入 buffer;
(2)commit point;(每个一段时间,或内存buffer满了)
(3)buffer中的数据写入新的 index segment;
(4)等待在 os cache 中的 index segment 被 fsync 强制刷到磁盘上;
(5)新的 index sgement 被打开,供 search 使用;
(6)buffer 被清空;
每次 commit point 时,会有一个 .del 文件,标记了哪些 segment 中的哪些 document 被标记为 deleted 了;
搜索的时候,会依次查询所有的 segment,从旧的到新的,比如被修改过的 document,在旧的 segment 中,会标记为 deleted,在新的 segment 中会有其新的数据;
2. Lucene Index
- 在 Lucene 中,单个倒排索引文件被称为 Segment。
- Segment 是不可变更的;
多个 Segment 汇总在一起,称为 Lucene Index
- An ES Shard = A Lucene Index;
当有新 document 写入时,会生成新的 Segment,查询时会同时查询所有 Segments, 并且对结果汇总;
- Lucene 中有一个文件,用来记录所有 Segments 信息,称为 Commit Point;
删除的文档信息,保存在 “.del” 文件中
:::tips
索引与分片的比较
一个 Lucene 索引 我们在 Elasticsearch 称作 分片 。 一个 Elasticsearch 索引 是分片的集合。 当 Elasticsearch 在索引中搜索的时候, 他发送查询到每一个属于索引的分片(Lucene 索引),然后像 执行分布式检索 提到的那样,合并每个分片的结果到一个全局的结果集。
:::
3. 倒排索引不可变性
- 倒排索引采用 不可变 设计,一旦生成,不可改变;
- 不可变性,带来的好处:
- 不需要锁来解决并发写文件的问题,提升并发能力,避免锁带来的性能问题;
- 倒排索引文件数据不变,一旦读入内核的文件系统缓存,便留在那里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能;
- 数据可以被压缩,节省 cpu 和 IO 开销;
- 带来的坏处:
- 如果需要让一个新的文档可以被搜索,需要重建整个索引;
4. ES搜索如何实现近实时(filesystem,refresh)
:::warning
简化版流程中存在的问题:
每次都必须等待 fsync 将 segment 刷入磁盘,才能将 segment 打开供 search 使用,这样的话,从一个 document 写入,到它可以被搜索,可能会超过1分钟!!!这就不是近实时的搜索了!!!
主要瓶颈在于 fsync 实际发生磁盘 IO 写数据进磁盘,是很耗时的。
:::
所以为了优化写入流程实现 NRT 近实时,引入了文件系统缓存,refresh 操作;
将 Index buffer 写入 Segment 的过程称为,Refresh。
- Refresh 不执行 fsync 操作;
Refresh 频率,默认 1 秒发生一次。
- 可通过 index.refresh_interval 配置;
- Refresh 后,数据就可以被搜索到了,这就是为什么 es 被称为近实时搜索(1 秒);
PUT /my_index
{
"settings": {
"refresh_interval": "30s"
}
}
如果系统又大量的数据写入,那就会产生很多的 Segment;
Index Buffer 被占满时,会触发 Refresh,默认值是 JVM 的 10%;
Segment 写入磁盘的过程相对耗时,借助文件系统缓存,Refresh 时,先将 Segment 写入缓存以开放查询;
5. ES如何保证在断电时数据不丢失(translog,flush)
:::warning
Refresh 版流程中存在的问题:
一旦机器宕机,缓存中的数据丢失,那么 es 就会出现大量数据的丢失;
:::
所以为了优化写入流程保证数据存储的可靠性,引入了 translog、flush 操作;
再次优化的写入流程
- 数据写入 buffer 缓冲和 translog 日志文件
- 每隔一秒钟,buffer 中的数据被写入新的 segment file,并进入 os cache,此时 segment 被打开并供 search 使用
- buffer 被清空
- 重复1~3,新的 segment 不断添加,buffer 不断被清空,而 translog 中的数据不断累加
- 当 translog 长度达到一定程度的时候,commit 操作发生
- buffer 中的所有数据写入一个新的 segment,并写入 os cache,打开供使用
- buffer 被清空
- 一个 commit ponit 被写入磁盘,标明了所有的 index segment
- filesystem cache 中的所有 index segment file 缓存数据,被 fsync 强行刷到磁盘上
- 现有的 translog 被清空,创建一个新的 translog
:::tips
基于 translog 和 commit point,如何进行数据恢复
:::
**
fsync+清空translog,就是flush,默认每隔 30 分钟 flush 一次,或者当 translog 满(默认 512 M)的时候,也会flush;
- POST /my_index/_flush,一般来说别手动 flush,让它自动执行就可以了;
translog,也是先写入 os cache,然后每隔5秒被 fsync 一次到磁盘上。
- 高版本开始,Transaction Log 默认落盘;
- 每个分片有一个 Transaction Log;
由于在一次增删改操作之后,只有当 fsync 在 primary shard 和 replica shard 都成功之后,那次增删改操作才会成功;
所以 document 的增删改,会触发 fsync 操作;
但是这种依赖在一次增删改时强行 fsync translog,可能会导致部分操作比较耗时;
也可以通过允许部分数据丢失(间隔时间5秒内的数据丢失),设置异步来 fsync translog;
PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
6. ES如何实现海量磁盘文件合并(segment merge,optimize)
:::warning
Translog 版的流程中还是会存在一些问题:
每秒 refresh一个 segment file,文件过多,而且每次search都要搜索所有的segment,很耗时;
:::
所以为了优化写入流程实现海量磁盘文件合并,引入了 segment merge、optimize;
默认会在后台执行 segment merge 操作,在 merge 的时候,被标记为 deleted 的 document 也会被彻底物理删除;
每次 merge 操作的执行流程:
- 选择一些有相似大小的 segment,merge 成一个大的 segment;
- 将新的 segment flush 到磁盘上去;
- 写一个新的 commit point,包括了新的 segment,并且排除旧的那些 segment;
- 将新的 segment 打开供搜索;
- 将旧的 segment 删除;
POST /my_index/_forcemerge
,尽量不要手动执行,让它自动默认执行就可以了;