ES 准实时存储机制与文档的分布式存储过程

一. Lucene Index 与 ES Shard

我们知道 ES 底层利用的是 Lucene 的倒排索引实现搜索的,而倒排索引有个特点是: 一旦创建,不可更改。该特点可以带来如下好处:

  • 充分利用缓存
  • 不需要考虑并发写的锁机制带来的影响
  • 可以利用压缩节约空间

但是当新的文档存储进来,需要构建新的倒排索引时,其需要先重新构建倒排索引文件,然后将之后的查询替换到新的倒排索引文件上,在将旧的文件替换掉。当索引文件较大时,整个过程性能消耗非常严重。

为了解决该问题,Lucene 采用了 每次写文件 的机制,即每次倒排索引构建时,都新建一个文件,该文件称为 segment,多个 segment 组成一个 Lucene Index,其对应为 ES 的 shard,也就是说 ES 的 shard 分片是多个倒排索引文件 segment 的集合,而一个或多个 ES shard 分片构成了 ES Index。这个对应关系一定要区分好。

二. ES 准实时查询实现原理

ES 主要通过下面三个过程保证数据存储的可靠性以及搜索的实时性:

  • refresh
  • translog
  • flush

下面进行分别介绍。

1. refresh 操作

ES 在存储文档时,会先将文档写入内存的一个缓冲队列中,然后在生成 segment。

refresh 就是将缓冲队列中的文档刷新到文件系统缓存,生成 segment 并提供查询的过程。

refresh 执行时会清空内存缓冲队列,将其中的文档都写入到 segment 中,为了提高查询的实时性,其利用文件系统缓存,先将 segment 存储到文件系统缓存中,之后就可以对外提供查询服务了,之后才将文件系统缓存中的 segment 真正的写入到磁盘中。

【1】 refresh 执行的时机
  • 1 . 系统默认每秒执行 1 次 refresh,所以新存入的数据最快可以 1 秒钟后就可以被检索到,这也是 ES 能够提供近实时搜索的原因。可以通过 index.setting.refresh_interval 参数设置。

  • 2 . 内存缓冲队列占满时执行 refresh。默认缓冲大小为 jvm heap 大小的 10%,可以通过 indices.memory.index_buffer_size 参数设置,注意这里的缓冲大小是由所有 shard 共享的。具体说明参考文档

  • 3 . 执行 flush 时会触发 refresh 操作

【2】refresh 设置建议

ES 默认每秒执行 refresh 操作,意味着每秒都生成一个 segment 文件。一般来说,很多应用不需要这么高的实时性,对于历史数据迁移这样的操作甚至可以关闭掉 refresh,同时过多的 segment 文件也会降低查询性能,因此如果不需要,可以通过调高 index.setting.refresh_interval 参数来降低 refresh 频率以节省性能。甚至某些操作比如导入历史数据时可以先关掉 refresh 操作,等完成后在开启或者手动 refresh。

PUT test_index/_settings
{
  "index.refresh_interval": "-1"
}

另外 ES 也提供了单独的 refresh 让我们可以手动执行 refresh,以保证搜索的实时性。

# refresh 单个索引
POST /test_index/_refresh
# refresh 多个索引
POST /test_index_01, test_index_02/_refresh
# refresh 索引索引
POST /_refresh

2. translog

上面提到 refresh 只是将倒排索引文件即 segment 写入到了文件系统缓存中,那么如果发了宕机,缓存中的数据没有及时写入磁盘就会造成数据丢失。ES 为了解决该问题提供了 translog 日志。

ES 在将文档写入到内存缓冲时,会同时将文档写入 translog,之后会调用 fsync 将 translog 写入磁盘。可以通过参数 index.translog.sync_interval 控制写入间隔,默认 5s, 最小不能低于 100 ms。另外 ES 在启动时会通过 translog 进行数据恢复,会将 commit point 之后尚未写入磁盘的 translog 数据恢复,有点类似 MySQL 的重做日志。

虽然默认设置 5s 可以避免频繁写入磁盘,提高了 ES 性能,但也会有丢失 5s 钟内的数据的风险,在实际操作中需要考虑谨慎权衡。

3. flush

flush 是将文件缓存中的 segment 写入磁盘,并清空 translog 的过程,其具体操作步骤如下:

  • 将 translog 写入磁盘
  • 执行 1 次 refresh 操作,清空缓冲队列
  • 将内存中的 segment 刷新到磁盘,并更新 commit point
  • 删除旧的 translog

由此可见 flush 的操作还是非常重的,因此不能允许 flush 频繁操作,具体发生时机有:

  • 间隔时间定时指定: 默认 30 min 执行一次,并且不允许修改
  • translog 空间占满时,默认是 512 M,是索引级别的,可以通过 index.translog.flush_threshold_size 参数设置

三. segment merge (倒排索引归并)

现在我们知道,“最坏”情况下每秒会生成一个 segment 文件,随着时间增长,segment 文件的数据也会快速增加,每个文件都需要消耗资源,这会给服务器带来很大的压力,并且会影响 ES 查询性能。

为了解决该问题,ES 引入了 segment merge 机制,主动将零散的 segment 合并为少量、较大的 segment,以保证 ES 的性能。

【1】segment 归并过程

ES 通过独立的线程进行 segment merge 操作,不会影响到正常数据的读写。其简要过程是:

  • 1 . 搜索较小的 segment
  • 2 . 将较小的 segment 合并为较大的 segment
  • 3 . 更改 commit point,删除之前较小的 segment

注意在 merge 过程中也会将 .del 文件删除,关于 .del 文件后面进行讲述。

【2】 segment 归并策略

  • index.merge.floor_segment: 默认 2 MB,小于该大小的优先被 merge
  • 默认一次最多归并 10 个 segment
  • 默认 force merge 时最多归并 30 个 segment
  • 默认大于 5 GB 的 segment 不进行 merge,optimize 除外

归并线程数

默认值为

Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2))

即不会超过 4 个线程进行 segment merge,可以通过参数 index.merge.scheduler.max_thread_count 设置。

四. 文档的分布式存储与查询

聊完了上面 shard 层面的存储操作,接下来看 ES 层面如何进行数据的分布式存储与查询。

1. 主分片的确认

我们知道一个索引是由多个分片组成的,那么当一个请求达到时,ES 如何判断要执行请求的分片在哪个节点呢?其通过如下算法获取:

ES 分片映射算法

shard = hash(routing) % number_of_primary_shards
  • routing: 路由值,默认是文档的 id 值
  • number_of_primary_shards: 主分片数

由此我们可以知道,ES 通过上面路由算法将文档均匀的分散存储到各个节点上,同时也说明了为什么 ES 的主分片数一旦设定不可修改的原因,因为其数据存储分片的确认依赖于主分片数,一旦修改那么之前的索引数据将完全不可读。

2. ES 的分布式读写过程

了解了主分片的确定,现在看下 ES 数据的存储过程。

假设有 node1、node2、node3 节点,数据的创建过程如下:

  • node1 作为 master 或者路由节点,接收文档创建请求请求
  • node1 根据文档 id 和分片映射算法找到数据要存储的主分片
  • node1 将请求转发到对应的主分片所在节点
  • 主分片执行文档创建,并将请求转发到对应的副分片
  • 主分片接收到副分片创建完成通知后,将结果返回 node1
  • node1 将创建结果返回给客户端

查询过程与此类型:

  • node1 接收到查询请求后,根据 routing 算法获取所有的的分片
  • 通过 cluster state 获取分片所在节点列表,然后轮询获取一个 shard
  • 将查询请求转发到选择到的分片,待分片返回数据后返回给客户端

关于 ES 的查询机制后面在整理文章进行讲解,这里就不作深入分析了。

3. ES 文档的改和删

ES 倒排索引一旦创建是不能更改的,因此其修改本质上是先将旧的文档删除,在创建新的文档的过程。因此这里只简要介绍下删除的过程。

我们知道 segment 文件是不可修改,其删除过程如何呢?其实是 ES 内部维护了一个 .del 的文件。当执行删除操作时,将删除文档的 id 记录到 .del 文件中去,然后在查询结果返回前,先查询一篇 .del 文件,将其中被删除文档的数据略去,不作返回。

真正的删除是在 segment merge 的时候,归并是会对文档和 .del 文件进行删除。

发布了46 篇原创文章 · 获赞 21 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/Ahri_J/article/details/84000013