ES写入数据,本质是往shard写入数据,过程如下图:
近实时的原理(refresh)
ES 默认每隔 1 秒会从内存 buffer 中的数据写入 filesystem cache,这个过程叫做 refresh。这也是为什么说 ES 是近实时搜索的原因,因为数据写入到 filesystem cache 后才会被搜索到。
Elasticsearch提供了一个refresh操作,会定时地调用lucene的reopen(新版本为openIfChanged)为内存中新写入的数据生成一个新的segment,此时被处理的文档均可以被检索到。refresh操作的时间间隔由refresh_interval参数控制,默认为1s。还可以在写入请求中带上refresh表示写入后立即refresh,另外还可以调用refresh API显式refresh。
分段(segment)
每个 shard 都是一个 Lucene Index,包含多个 segment 文件和一个 commit point 文件。每个 segment 都是一个倒排索引,存储的是一个个 Document。commit point 记录了segment 文件信息。
在查询的时,会把所有的 segment 查询结果汇总归并后转为最终的分片查询结果返回。
提交点(commit point)
commit point记录当前所有可用的 segment,每个 commit point 都会维护一个.del 文件(ES 删除数据本质是不属于物理删除)。
当 ES 做删改操作时首先会在 .del 文件中声明某个 document 已经被删除,.del文件记录了在某个 segment 内某个文档已经被删除。当查询请求过来时在 segment 中被删除的文件是能够查出来的,但是当返回结果时会根据 commit point 维护的那个 .del 文件把已经删除的文档过滤掉。
合并分段(merge)
refresh默认间隔为1s中,因此会产生大量的小segment。为此ES会运行一个任务检测当前磁盘中的segment,对符合条件的segment进行合并操作,减少lucene中的segment个数,提高查询速度,降低负载。
merge也是文档删除和更新操作后,旧的doc真正被删除的时候。
用户还可以手动调用_forcemerge API来主动触发merge,以减少集群的segment个数和清理已删除或更新的文档。
依赖_source实现部分更新
Lucene只支持对文档的整体更新。ES为了支持局部更新,在Lucene的Store索引中存储了一个_source字段,该字段的key值是文档ID, 内容是文档的原文。当进行更新操作时先从_source中获取原文,与更新部分合并后,再调用lucene API进行全量更新, 对于写入了ES但是还没有refresh的文档,可以从translog中获取。
为了防止读取文档过程后执行更新前有其他线程修改了文档,ES增加了版本机制,当执行更新操作时发现当前文档的版本与预期不符,则会重新获取文档再更新。
保证数据可靠性:
translog(记录日志)
ES数据是先写入缓存中,如果在写入数据时ES节点突然宕机可能会造成数据丢失。为保证数据存储的可靠性,当进行文档写操作时会先将文档写入Lucene,然后写入一份到translog,写入translog是落盘的,这样就可以防止服务器宕机后数据的丢失
translog会每隔5秒或者在一个变更请求完成之后执行一次fsync操作,将translog从缓存刷入磁盘。如果对可靠性要求不是很高,也可以设置异步落盘,可以提高性能,由配置index.translog.durability和index.translog.sync_interval控制。
ES 在写入缓存后,再写入 translog 日志文件(因为有可能缓存写入失败,为了减少写入失败回滚的复杂度,因此先写入缓存)。
由于 translog 是追加写入,因此性能要比随机写入要好。
flush(写入磁盘)
每30分钟或当translog达到一定大小(由index.translog.flush_threshold_size控制,默认512mb), ES会触发一次flush操作,此时ES会先执行refresh操作将buffer中的数据生成segment,然后调用lucene的commit方法将所有内存中的segment 写(fsync)到磁盘。此时Lucene中的数据就完成了持久化,会清空translog中的数据(6.x版本为了实现sequenceIDs,不删除translog)