【ES】写入原理

写索引流程

写索引只能先写主分片然后同步到副本,当一个写请求被发送到某个节点后,这个节点会充当协调节点,根据路由策略找到目标主分片,然后协调节点从自身存储的分片信息中找到目标主分片所在节点,如果目标分片在其他节点上,就转发请求到该节点上。

分片路由策略如下:

1
shard = hash(routing) % number_of_primary_shards

routing 通过 Hash 函数生成一个数字,然后除以主分片数量后余数为目标分片,范围是 0 ~ 主分片数 - 1

routing 默认是文档的 _id,可以设置成一个自定义的值。

通过了解路由策略也就可以理解为什么在创建索引时就要确定好分片数量并且不可变更,因为如果数量变化,那么所有之前路由的值都会失效,数据也就找不到了。

当写请求被路由到所在主分片对应的节点上后,该数据节点会接受请求并写入到磁盘,然后并发将数据复制到所有副本分片(乐观锁),一旦所有副本都响应成功,主分片所在节点会向协调节点返回成功,协调节点向客户端返回成功。

数据存储

ES 内部是通过 Lucene 完成索引的创建写入和搜索,当新增一个文档时,Lucene 首先会进行分词等预处理,然后将文档索引写入到内存缓冲区中,并将本次操作写入事务日志(translog,类似mysql的binlog)用于宕机后内存数据的恢复。translog 写入时会先被写到 os cache,默认情况下每隔 5s 会被 flush 到磁盘中,因此宕机最多会丢失 5s 数据。

默认情况下,Lucene 每隔 1s (refresh_interval)将内存缓冲区中的数据刷到文件系统缓存中,称为一个段(segment),一旦刷入文件系统缓存,segment才可以被用于索引。

refresh_interval决定了 ES 数据的实时性,因此 ES 是一个准实时系统。

segment 在磁盘中是不可修改的,因此避免了磁盘的随机写,所有随机写都在内存中进行。随着时间推移,segment 越来越多,默认情况下 Lucene 每隔 30min 或 segment 空间大于 512M,将缓存中的 segment 持久化到磁盘,称为一个 commit point,此时会删除对应的 translog。

segement 存储文件不可修改,新增、更新和删除处理过程如下:

  • 新增:数据是新的,只需要对当前文档新增一个 segment
  • 删除:不会移除旧 segemnt 中的文档,而是新增一个 .del 文件标记删除,被标记删除的文档仍可以被查询到,但在最终结果返回前会被移除
  • 更新:相当于删除+新增两个动作,旧版本文档会被在 .del 文件中标记删除,同时新版本文档会被索引到新 segment。两个版本的文档都会被查询匹配到,但旧版本文档会在结果集返回前被移除。

segment 不可修改优缺点:

  • 优点
    • 不需要锁,因为没有并发修改数据的问题。
    • 性能提升,由于其不变性,一旦索引被读入内核的文件系统缓存,便会驻留。只要文件系统缓存还有足够空间,大部分读请求会直接读缓存,而不会读磁盘。
    • 其他缓存(如 Filter 缓存)在索引声明周期内始终有效,不需要再每次数据变化时重建,因为数据不会变化。
    • 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和需要被缓存到内存的索引使用量。
  • 缺点
    • 空间浪费,更新和删除时,旧文档数据不会立即被删除而是被标记删除占用磁盘空间。
    • 资源消耗大,每次新增数据都需要新增一个 segment,当 segment 过多时,对服务器资源如文件句柄消耗很大。
    • 增加了查询负担,查询结果需要排除标记删除的文档。

段合并

由于每刷新一次就会新建一个 segment,导致短时间内 segment 数量暴增,而且 segment 数量太多会带来很多麻烦。大量 segment 会影响读性能,每个 segment 都会消耗文件句柄、内存和 CPU 运行周期,更重要的是,每个搜索请求都会轮流检查每个 segment 然后合并查询结果,segment 越多,搜索越慢。

因此 Lucene 会按照一定策略将 segment 合并,合并时会清理被标记删除的文档,合并后数据被拷贝到一个新的大 segment 中。合并过程中不会中断索引和搜索。合并结束后,老的 segment 会被删除,新的 segment 被刷新到磁盘,同时写入一个包含新 segment、排除旧的和较小 segment 的新提交点,新的 segment 被打开用于搜索。

段合并计算量庞大,并且需要进行大量磁盘 I/O,因而会影响写入速率,ES 在默认情况下会对合并流程进行资源限制,以保证搜索性能。