ElasticSearch——Translog简介

本文是对ElasticsearchTranslog(事务日志)的一个简单整理。

我们知道Elasticsearch是一个near-realtime(近实时)的搜索引擎。这个近实时指的是数据添加到Elasticsearch后并不能马上被搜索到,而是要等待一定时间(默认为1秒)。

这里的原因是ElasticSearch底层依赖lucene来提供搜索能力,一个lucene index由许多独立的Segments组成。

lucene_index

Segment是最小的数据存储单元。其中包含了文档中的词汇字典、词汇字典的倒排索引以及Document的字段数据。因此Segment直接提供了搜索功能。但是Segment能提供搜索的前提是数据必须被提交,即文档经过一系列的处理之后生成倒排索引等一系列数据。可以想见这个过程是比较耗时的。因此Elasticsearch并不会每接收到一条数据就提交到一个Segment中,一方面是因为这样耗时太长,另一方面是这样会生成巨量的Segment,降低了IO性能。

Elasticsearch采取的机制是将数据添加到lucenelucene内部会维护一个数据缓冲区,此时数据都是不可搜索的。每隔一段时间(默认为1秒),Elasticsearch会执行一次refresh操作:lucene中所有的缓存数据都被写入到一个新的Segment,清空缓存数据。此时数据就可以被搜索。当然,每次执行refresh操作都会生成一个新的Segment文件,这样一来Segment文件有大有小,相当碎片化。Elasticsearch内部会开启一个线程将小的Segment合并(Merge)成大的Segment,减少碎片化,降低文件打开数,提升IO性能。

不过这样也带来一个问题。数据写入缓冲区中,没有及时保存到磁盘中,一旦发生程序崩溃或者服务器宕机,数据就会发生丢失。为了保证可靠性,Elasticsearch引入了Translog(事务日志)。每次数据被添加或者删除,都要在Translog中添加一条记录。这样一旦发生崩溃,数据可以从Translog中恢复。

不过,不要以为数据被写到Translog中就已经被保存到磁盘了。一般情况下,对磁盘文件的write操作,更新的只是内存中的页缓存,而脏页面不会立即更新到磁盘中,而是由操作系统统一调度,如由专门的flusher内核线程在满足一定条件时(如一定时间间隔、内存中的脏页面达到一定比例)将脏页面同步到磁盘上。因此如果服务器在write之后、磁盘同步之前宕机,则数据会丢失。这时需要调用操作系统提供的fsync功能来确保文件所有已修改的内容被正确同步到磁盘上。

Elasticsearch提供了几个参数配置来控制Translog的同步:

  • index.translog.durability

    该参数控制如何同步Translog数据。有两个选项:

    • request(默认):每次请求(包括index、delete、update、bulk)都会执行fsync,将Translog的数据同步到磁盘中。
    • async:异步提交Translog数据。和下面的index.translog.sync_interval参数配合使用,每隔sync_interval的时间将Translog数据提交到磁盘,这样一来性能会有所提升,但是在间隔时间内的数据就会有丢失的风险。
  • index.translog.sync_interval:该参数控制Translog的同步时间间隔。默认为5秒。

  • index.translog.flush_threshold_size:该参数控制Translog的大小,默认为512MB。防止过大的Translog影响数据恢复所消耗的时间。一旦达到了这个大小就会触发flush操作,生成一个新的Translog

下面我们来看一看涉及到Translog的操作。

索引操作

对于数据操作最终的目标类都是InternalEngine。这个类关联了Elasticsearchlucene,通过它来操作lucene的接口。

索引操作的方法为InternalEngine.index(),主要步骤如下:

  1. 调用indexIntoLucene方法将数据写入到lucene中。实际的工作是调用IndexWriter将索引数据添加到lucene

    需要注意的是:数据添加到lucene之后,并不能立即被搜索到,必须写入到segment中。这个步骤是由后面的refresh操作完成的。

  2. 调用index.origin().isFromTranslog()方法判断数据是否来自日志

    如果数据来自日志的话就不再重复记录日志,否则记录日志。

    如果index操作成功,则在translog中添加一个INDEX日志。如果index操作失败,则在translog中添加一个NO_OP日志。

删除操作

删除操作的方法为InternalEngine.delete(),主要步骤如下:

  1. 调用deleteInLucene方法将删除的数据写入到lucene

    Segment文件是不可变更的。当一个Document删除的时候,实际上就是将旧的文档标记为删除。在合并Segment的过程中再将旧的Document删除掉。

  2. 调用index.origin().isFromTranslog()方法判断数据是否来自日志

    如果数据来自日志的话就不再重复记录日志,否则记录日志。

    如果delete操作成功,则在translog中添加一个DELETE日志。

refresh操作:

前面我们提到过,refresh就是将缓冲区中的数据写入到一个新的Segment。这样缓存中的数据就处于可被搜索状态。

默认情况下,Elasticsearch每隔1秒钟执行一次refresh操作。这个配置在IndexSettings中:

1
2
3
public static final TimeValue DEFAULT_REFRESH_INTERVAL = new TimeValue(1, TimeUnit.SECONDS);
public static final Setting<TimeValue> INDEX_REFRESH_INTERVAL_SETTING =
Setting.timeSetting("index.refresh_interval", DEFAULT_REFRESH_INTERVAL, new TimeValue(-1, TimeUnit.MILLISECONDS),

refresh操作在InternalEngine.refresh()中:

1
2
3
4
5
6
7
8
ReferenceManager<IndexSearcher> referenceManager = getReferenceManager(scope);
// it is intentional that we never refresh both internal / external together
if (block) {
referenceManager.maybeRefreshBlocking();
refreshed = true;
} else {
refreshed = referenceManager.maybeRefresh();
}

可以看到refresh操作调用luceneReferenceManager,真正的refresh操作由lucene执行。maybeRefreshBlockingmaybeRefresh的区别是:当有另外一个线程在执行时,maybeRefreshBlocking会阻塞直到另外的线程执行结束,maybeRefresh直接返回。

flush操作

前面我们提到了,flush操作是为了防止translog文件变得太大而影响数据的恢复(当然也可以手动触发flush操作)。

flush操作在InternalEngine.flush()中:

1
2
3
4
5
6
translog.rollGeneration();
commitIndexWriter(indexWriter, translog, null);
refresh("version_table_flush", SearcherScope.INTERNAL, true);
translog.trimUnreferencedReaders();

refreshLastCommittedSegmentInfos();
  1. 调用Translog.rollGeneration方法。生成一个新的Translog
  2. 调用commitIndexWriter方法。向lucene写入commitData信息。将lucene中所有的修改都提交到索引中,然后同步(sync)索引数据到磁盘文件中。这样索引数据就彻底保存到磁盘中了。
  3. 调用refresh方法将缓冲区中数据写入一个新的Segment
  4. 调用Translog.trimUnreferencedReaders删除掉未被引用的日志reader。
  5. 调用refreshLastCommittedSegmentInfos更新最新的Segment信息

recover操作

recover操作的作用是从事务日志中恢复数据,在InternalEngine.recoverFromTranslog()方法中,其主要功能调用recoverFromTranslogInternal来实现。步骤如下:

  1. 首先从lastCommittedSegmentInfos(最后提交的SegmentInfo,记录在lucene中)中获取事务日志的generation
  2. 通过这个generation生成事务日志的snapshot(快照),根据这个snapshot来恢复数据

数据恢复的方法是IndexShard.runTranslogRecovery()。它的功能是从snapshot中遍历Operation(表示记录在事务日志中的数据操作),根据operation重新执行一遍操作,达到恢复数据的目的。

总结

在大部分场景下,性能和可靠性都是需要进行权衡和取舍的,Translog就是这种权衡的产物,它能在性能轻微损失的同时保证数据的可靠性。在许多用于数据存取的软件设计中都能看到事务日志。学习事务日志是非常有意义的事。接下来的文章中我们进一步来分析Translog的原理。

https://www.toutiao.com/i6749080730841645580/
https://leonlibraries.github.io/2017/04/27/ElasticSearch内部机制浅析三/
https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-translog.html
https://www.cnblogs.com/promise6522/archive/2012/05/27/2520028.html