ElasticSearch进阶之Shard内部揭秘

当我们了解了ElasticSearch的一些基本概念之后,就会自然而然地想去深入了解一下一些ElasticSearch内部的实现,比如说它是如何做到近乎实时的查询的,以及如何处理数据的持久化等等问题。本文就来详细介绍shard内部为这些问题所做的各种优化和实现。

不变性

我们在《ElasticSearch之Analysis介绍》中介绍了为了使得文本可以被搜索,我们需要位置创建inverted Index,关于它的概念我们就不详细介绍了。这里一个很有趣的问题就是不变性。我们说希望写入到磁盘中的Inverted Index不再变化,这样的特性会带来很多好处:

  1. 我们对它的访问不再需要加锁,因为没有人会来修改它,所以可以随意的读。
  2. 可以很方便地cache,只要我们有足够的空间cache,那么它就可以一直在那边。

显然现实不会像我们想象的这样美好,因为我们总是需要有新的文档被搜索到,这就需要更新index。那如何才能做到这一点呢?

动态更新index

现在我们面临的问题就是动态更新inverted index,同时又不丧失我们上面提到的不变性带来的优点。一个常见的方法就是使用多个index,比如我们把需要更新的内容加入到一个新的inverted index中,然后查询的时候遍历所有的index,最后把结果再合并起来,这样我们可以使得之前的不变性带来的优点得以保存,又可以增加新的文本。

这个想法在Lucene中是如何实现的呢?我们可以认为inverted index被分成了各个segment,然后有一个commit point来表示哪些segment当前是可以被查询的。如下图所示:

新的文本增加进来的时候,首先会把它们放到一个in-memory的index buffer里面,如下图所示:

当这个buffer有足够的数据之后,就会把它commit到上面的commit point里面:

  1. 创建一个新的segment,并写入到磁盘中。
  2. 一个新的包含这个segment的commit point会写入到磁盘中。
  3. Disk的写需要flush,用来保证真的被写入了。

在这之后,这个segment就可以把搜索到了,如下图所示:

我想这个时候,聪明的你应该会问,假如我是修改或者删除原来的怎么办,我们之前提到segment是不可以修改的,但是我们可以使用一个别的方法来达到同样的效果:

对于删除来说,那就是加入一个.del的文件,每次有删除我们就加入到这个文件中。这样在搜索的时候,其实被删除的segment还是被搜索的,只是在返回结果之前会去.del文件中搜索一下,看是不是被删除了,如果是的,那就不会结果中返回。

如果是更新又如何处理,一样的,我们需要同时做两件事情,假如一个新的版本的segment,同时把旧版本的segment标志成删除。这样同个版本都会被搜索,只是旧版本因为.del文件的存在不会返回,就如同真的被删除了一样。

近乎实时的查询

有了我们上面这种per-segment的处理方法之后,新加一个文本,并让他被查询的延时就快了很多,可以达到分钟级别,但是显然还是不够快。那么我们有没有什么好的办法来进一步优化呢?

在思考优化之前,让我们先来看看为什么上面这种实现的性能瓶颈在哪里,其实主要问题还在于磁盘写的性能,因为我们在把一个segment加入到commit point之前,希望它是已经被保存到磁盘中的,因为这样即使有power down的风险,也不会有数据的丢失。

既然我们知道了瓶颈在磁盘的写上面,那么有没有什么好的方法解决呢?Lucene使用的一个方法就是先把加入的segment写到到系统的cache中,只要这个写入完成了就可以被查询了,如下图灰色的部分,然后它可以再慢慢写到磁盘中,但这个写入完成的时候再加入到commit point中:

这样一来整个查询的性能就得到了很大的提升。

我们把这个写到系统cache的操作称为refresh,默认来说,这个refresh的频率是每秒一次。这就是为什么我们说可以做到近乎实时的查询了。当然并不是所有的场景都需要这么频繁的更新,你可以通过下面命令来修改更新的频率,设置可以设为-1来关闭这个更新。

改变的持久化

我们前面提到只有在数据真正写到磁盘之后,才会加入到commit point中,这样即使发生意外比如断电,数据也不会丢失。ElasticSearch在每次启动的时候也的确是根据commit point来决定哪些segment是已经commit了的。现在我们为了优化查询的速度引入了refresh的概念,这里就有一个问题需要解决,那就是这些已经在系统内存中但还没有被flush到disk中的数据,如何保证他们不丢失。

这里我们又引入了一个新的概念,就是translog,它保存了所有的Elasticsearch的操作记录。是不是很熟悉,对了,几乎所有的数据库的持久化都会这么做,这里ElasticSearch也不例外。有了这个translog之后,整个流程就变成了下面这样:

  1. 当有一个文本被index之后,它会加入到in-memory的buffer中,同时也会加入到translog之后。
  2. 然后步骤和之前一样,写到系统cache,可以被搜索等。
  3. 随着时间的推移,当translog越来越大之后,就把整个index flush到磁盘中,然后创建新的index,在flush到磁盘完成之后,就可以把旧的translog删除了。

这样一来,在ElasticSearch每次启动的时候,除了要从commit point来恢复,还需要把相应的Translog replay一下。

关于上面提到的flush时机除了发生在translog很大的时候,还有一个固定的interval,一般来说是30分钟,也就是说即使没有很多translog产生,默认每30分钟也会flush一次。

Segment的合并

随着时间的推移,你会发现Segment的数据量会越来越大。维护太多的segment显然不是一个好的主意。这里就会有一个merge的过程,也就是说ElasticSearch其实会有一个后台的thread来merge各个小的segment。这里就可以做一些优化,比如之前删除掉的内容就不需要再次合并进来,只需要保留最终的结果就可以了。那这个merge的过程是如何实现的呢?其实主要有以下几步:

  1. 把已经存在的segment merge到一个大的segment中。但是这个时候其实这个大的segment是不能被搜索的。
  • 把这个大的segment flush到disk,然后修改commit point包含这个新的大的segment并同时去除已经被merge的小的segment。然后那些旧的segment就可以被删除了。

总结

至此本文就把shard中内部的实现细节都解释清楚了,希望对你有所帮助。

You may also like...

Leave a Reply

Your email address will not be published.