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 被清空;clipboard.png
每次 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” 文件中

image.png :::tips 索引与分片的比较
一个 Lucene 索引 我们在 Elasticsearch 称作 分片 。 一个 Elasticsearch 索引 是分片的集合。 当 Elasticsearch 在索引中搜索的时候, 他发送查询到每一个属于索引的分片(Lucene 索引),然后像 执行分布式检索 提到的那样,合并每个分片的结果到一个全局的结果集。 :::

3. 倒排索引不可变性

  • 倒排索引采用 不可变 设计,一旦生成,不可改变;
  • 不可变性,带来的好处:
    • 不需要锁来解决并发写文件的问题,提升并发能力,避免锁带来的性能问题;
    • 倒排索引文件数据不变,一旦读入内核的文件系统缓存,便留在那里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能;
    • 数据可以被压缩,节省 cpu 和 IO 开销;
  • 带来的坏处:
    • 如果需要让一个新的文档可以被搜索,需要重建整个索引;

4. ES搜索如何实现近实时(filesystem,refresh)

image.png :::warning 简化版流程中存在的问题:
每次都必须等待 fsync 将 segment 刷入磁盘,才能将 segment 打开供 search 使用,这样的话,从一个 document 写入,到它可以被搜索,可能会超过1分钟!!!这就不是近实时的搜索了!!!
主要瓶颈在于 fsync 实际发生磁盘 IO 写数据进磁盘,是很耗时的。 :::

所以为了优化写入流程实现 NRT 近实时,引入了文件系统缓存,refresh 操作;
image.png

  • 将 Index buffer 写入 Segment 的过程称为,Refresh。

    • Refresh 不执行 fsync 操作;
  • Refresh 频率,默认 1 秒发生一次。

    • 可通过 index.refresh_interval 配置;
    • Refresh 后,数据就可以被搜索到了,这就是为什么 es 被称为近实时搜索(1 秒);
      1. PUT /my_index
      2. {
      3. "settings": {
      4. "refresh_interval": "30s"
      5. }
      6. }
  • 如果系统又大量的数据写入,那就会产生很多的 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

durability.png
:::tips 基于 translog 和 commit point,如何进行数据恢复 ::: translog.png**

  • 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 删除;

merge.png
POST /my_index/_forcemerge,尽量不要手动执行,让它自动默认执行就可以了;