ES写操作理解
一般来说,使用ES都是将其作为分布式搜索系统或者是分布式NoSQL数据库。
从这两个角度分别来说一下 ES 的写操作。
写操作
在分析一个分布式系统的写操作时,一般要考虑以下几个点:
- 可靠性:也就是持久性,数据成功写入系统后,数据不会丢失。
- 一致性:数据写入成功后,再次查询确保读到的是新数据,不能读到旧数据。
- 原子性:一个写入操作或者更新操作要么全部成功,要么完全失败, 没有中间状态。
- 隔离性:多个写入操作不影响。
- 实时性:写入后是否能够理解被查询到。
- 性能:写入性能,吞吐量等。
Lucene
ES 内部使用了 Lucene 来完成索引的创建以及搜索功能,Lucene 的写操作是通过一个叫做 IndexWriter 类来实现的,IndexWriter 提供三个接口:
public long addDocuments();
public long updateDocuments();
public long deleteDocuments();
通过这三个接口来完成单个文档的写入,更新,删除。只要 doc 通过 IndexWriter 类写入后,就可以通过 IndexSearcher 进行搜索查询了。
这里简单的功能已经实现了,但是没有解决一些问题:
- 以上操作是单机操作,并不是分布式。
- doc 写入 lucene 后并不是立即可以查询的,需要生成完整的 segement 后才可以被搜索,如何保证实时性?
- 生成的 segement 是在内存当中的,如果机器宕机如何保证可靠性?
- lucene 无法支持部分更新,如何支持?
上述问题在 lucene 里并没有解决,而在 ES 中引入了多重机制来解决这些问题。
ES 的写操作
ES 采用多 shard 的方式来解决分布式问题。将数据集通过配置的 routing 规则分成多个数据子集,每个数据子集都有子集独立的索引与检索功能。在写入一个新的数据时,根据规则将数据发送到指定的 shard 上创建索引等,这样就实现了分布式功能。
ES 架构也采用了一主多副的架构。每个index由多个Shard组成,在每个Shard中又有一个主节点以及多个副本节点,这个副本个数可以配置。每次写入新数据的时候,写入请求会根据routing规则选择发送给哪个shard,请求里可以配置以什么Field作为路由参数,如果没有配置则使用Mapping里的配置,如果Mapping也没有配置,那么使用_id作为路由参数,通过这个参数计算出哈希值,从而找到对应的shard上的主节点。
请求将会发送给主节点,在主节点执行成功后,从主节点在发送给其他的副本节点。当其他的副本节点执行完成返回给主节点后,写入请求成功,返回结果给客户端。
这种方法的优缺点显而易见,每次执行的延时最小也是两次写入的延时总和,写入效率较低。但是这种方法,能够尽可能的保证数据可靠性。
采用这种多副本的方式,避免了单机或者磁盘故障时,对已经持久化的数据造成损害。
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。
这里是一个写入的逻辑图,在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自己实现了一套更新的逻辑,具体流程如下:
- 收到update请求后,从TransLog中或者是Segment中读取同id 的完整doc数据,记录此时版本号为 v1。
- 将版本号为 v1 的数据与请求中要更新的字段进行合并,得到一个新的完整doc,同时更新内存中的一个 versionMap,获取到完整doc后,将这个update请求转化为index请求。
- 加锁。
- 再次从versionMap中读取该id对应的最大版本号v2,如果没有读到,则在segment或者TransLog里读取。
- 核对两个版本是否冲突(v1 == v2),如果冲突,则回退到最开始的 update 请求去,重新执行。如果不冲突,则执行最新的 add 请求。
- 在 Index doc 阶段,首先将 version + 1 得到 v3,将 doc 写入 lucene 中去(这里lucene会先删除同样id 的doc,接着再新增)。成功后,更新版本号v3到versionMap上。
- 释放锁。
小结
- 可靠性: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