欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ES写操作理解

程序员文章站 2022-04-09 23:43:46
一般来说,使用ES都是将其作为分布式搜索系统或者是分布式NoSQL数据库。从这两个角度分别来说一下 ES 的写操作。写操作在分析一个分布式系统的写操作时,一般要考虑以下几个点:可靠性:也就是持久性,数据成功写入系统后,数据不会丢失。一致性:数据写入成功后,再次查询确保读到的是新数据,不能读到旧数据。原子性:一个写入操作或者更新操作要么全部成功,要么完全失败, 没有中间状态。隔离性:多个写入操作不影响。实时性:写入后是否能够理解被查询到。性能:写入性能,吞吐量等。LuceneES 内...

一般来说,使用ES都是将其作为分布式搜索系统或者是分布式NoSQL数据库。

从这两个角度分别来说一下 ES 的写操作。

写操作

在分析一个分布式系统的写操作时,一般要考虑以下几个点:

  • 可靠性:也就是持久性,数据成功写入系统后,数据不会丢失。
  • 一致性:数据写入成功后,再次查询确保读到的是新数据,不能读到旧数据。
  • 原子性:一个写入操作或者更新操作要么全部成功,要么完全失败, 没有中间状态。
  • 隔离性:多个写入操作不影响。
  • 实时性:写入后是否能够理解被查询到。
  • 性能:写入性能,吞吐量等。
Lucene

ES 内部使用了 Lucene 来完成索引的创建以及搜索功能,Lucene 的写操作是通过一个叫做 IndexWriter 类来实现的,IndexWriter 提供三个接口:

public long addDocuments();
public long updateDocuments();
public long deleteDocuments();

通过这三个接口来完成单个文档的写入,更新,删除。只要 doc 通过 IndexWriter 类写入后,就可以通过 IndexSearcher 进行搜索查询了。

这里简单的功能已经实现了,但是没有解决一些问题:

  1. 以上操作是单机操作,并不是分布式。
  2. doc 写入 lucene 后并不是立即可以查询的,需要生成完整的 segement 后才可以被搜索,如何保证实时性?
  3. 生成的 segement 是在内存当中的,如果机器宕机如何保证可靠性?
  4. lucene 无法支持部分更新,如何支持?

上述问题在 lucene 里并没有解决,而在 ES 中引入了多重机制来解决这些问题。

ES 的写操作

ES 采用多 shard 的方式来解决分布式问题。将数据集通过配置的 routing 规则分成多个数据子集,每个数据子集都有子集独立的索引与检索功能。在写入一个新的数据时,根据规则将数据发送到指定的 shard 上创建索引等,这样就实现了分布式功能。

ES 架构也采用了一主多副的架构。每个index由多个Shard组成,在每个Shard中又有一个主节点以及多个副本节点,这个副本个数可以配置。每次写入新数据的时候,写入请求会根据routing规则选择发送给哪个shard,请求里可以配置以什么Field作为路由参数,如果没有配置则使用Mapping里的配置,如果Mapping也没有配置,那么使用_id作为路由参数,通过这个参数计算出哈希值,从而找到对应的shard上的主节点。

请求将会发送给主节点,在主节点执行成功后,从主节点在发送给其他的副本节点。当其他的副本节点执行完成返回给主节点后,写入请求成功,返回结果给客户端。

这种方法的优缺点显而易见,每次执行的延时最小也是两次写入的延时总和,写入效率较低。但是这种方法,能够尽可能的保证数据可靠性。

采用这种多副本的方式,避免了单机或者磁盘故障时,对已经持久化的数据造成损害。

ES写操作理解

ES为了减少磁盘IO保证读写性能,一般是每隔一段时间才会把Lucene里的Segement 进行持久化,对于没有写入内存,还没有Flush到磁盘的Lucene数据,如果发生宕机,如何保证不丢失呢?

TransLog

ES借鉴数据库的处理方式,增加了TransLog模块。

在每一个shard中,写入流程分为两个部分,先写入lucene,再写入TransLog。

写入请求到达shard后,先写lucene文件,创建好索引后,此时索引还在内存里,接着去写TransLog,写完TransLog后,刷新TransLog数据到磁盘上,写磁盘成功后,请求返回给客户端。与数据库不同,ES先写内存再写TransLog,数据库则相反,原因可能是ES的写入Lucene操作过于复杂,很容易失败,为了避免TransLog中有大量无效的记录,减少回滚的复杂度以及提高速度,所以这样。

在这里,写入Lucene后,是无法被搜索到的。需要通过refresh把内存的数据转成完整的segment后,才可以被搜索到。这个refresh 的时间可以进行配置,默认是 1s,最快100ms。如果这时候使用GetById的方式来查询,可以直接在TransLog里查询,这时候是可以搜索到的。每隔一段时间,Lucene会把内存的中生成的新segment刷新到磁盘上,刷新后此时数据以及持久化了,则清空旧的TransLog。
ES写操作理解

这里是一个写入的逻辑图,在lucene调用write系统调用的时候,会将数据写入操作系统的内存缓冲区,调用refresh时,是将内存缓冲区内的数据,刷到文件系统的页cache里,此时在查询的时候,在lucene查询时,调用系统调用的read时,就会读到这张页,进而可以进行搜索查询。所以说在refresh之前,数据还在内存缓冲区内,此刻无法搜索读取。接着在指定的时间内(默认为30分钟),lucene会调用flush将文件系统内的页cache上的数据刷到硬盘上,此刻持久化完成,接着清理 TransLog 内的旧数据。

Lucene在写 TransLog 时,从TransLog里,可以进行flush到硬盘上,这个flush的时间间隔可以配置。

为什么不直接从Lucene 里 flush 到硬盘上还要调用refresh以及写入 TransLog呢?是因为 flush 会调用系统调用 fsync,这个 fsync 操作是将文件系统的脏页刷新到磁盘上,这个操作非常的耗时,频繁的调用不靠谱。所以最终采用refresh + TransLog 的方式来做到提升性能 + 保证可靠性。

update

Lucene里不支持部分字段的更新,于是ES自己实现了一套更新的逻辑,具体流程如下:

  1. 收到update请求后,从TransLog中或者是Segment中读取同id 的完整doc数据,记录此时版本号为 v1。
  2. 将版本号为 v1 的数据与请求中要更新的字段进行合并,得到一个新的完整doc,同时更新内存中的一个 versionMap,获取到完整doc后,将这个update请求转化为index请求。
  3. 加锁。
  4. 再次从versionMap中读取该id对应的最大版本号v2,如果没有读到,则在segment或者TransLog里读取。
  5. 核对两个版本是否冲突(v1 == v2),如果冲突,则回退到最开始的 update 请求去,重新执行。如果不冲突,则执行最新的 add 请求。
  6. 在 Index doc 阶段,首先将 version + 1 得到 v3,将 doc 写入 lucene 中去(这里lucene会先删除同样id 的doc,接着再新增)。成功后,更新版本号v3到versionMap上。
  7. 释放锁。
    ES写操作理解
小结
  • 可靠性:Lucene设计中不考虑可靠性,ES通过 TransLog 以及多副片的方式来保证可靠性。
  • 一致性:Lucene的Flush锁只能保证在update时的 delete 和 add 的中间不被 flush,但是 add 之后仍有可能被 flush,这种情况下就可能导致shard内,主节点与其它副节点同时flush,影响查询,只能保证最终一致性。
  • 原子性:add 以及 delete 都是调用的 Lucene 的接口,是原子的。在进行部分更新时,采用version以及局部锁的方式保证。
  • 隔离性:采用version以及局部锁保证。
  • 实时性:使用定期Refresh来刷新,可以在较短的时间内检索到。未刷新的数据,可以通过id的方式,从TransLog内读取到。
  • 性能:在写入时,不用等到所有副片返回才返回给客户端,只需要特定数目的副片返回即可返回给客户端。生成的segment不立刻刷新到磁盘,刷新前的时间内的可靠性由TransLog保证。TransLog中的flush可以配置时间间隔,但是是在牺牲可靠性的基础上。系统写入流程对版本依赖较高,读取频率很高,增加versionMap,减少直接与磁盘的IO开销。

本文地址:https://blog.csdn.net/liuchenxia8/article/details/107213929