ElasticSearch 准实时索引的底层实现原理
1、首先明确几个概念:
- ElasticSearch 节点与分片副本图:
-
segment最终是在磁盘中的。
- segment本质上就是倒排索引集合,每个 segment 是一个包含正排(空间占比90~ 95%)+ 倒排(空间占比5~ 10%)的完整索引文件,
- (关注)每次搜索请求会将所有 segment 中的倒排索引部分加载到内存,进行查询和打分,然后将命中的文档号拿到正排中召回完整数据记录。
-
ES删除数据导致磁盘容量上升原因:
- ES采用的标记删除,首先会将要合并的数据拷贝出来,重新写入到新的segment中,然后删除旧的数据,所以会导致消耗额外的磁盘和IO
- 删除旧数据的方式:
- 每个commit point都会维护一个.del文件,文件内记录了在某个segment内某个文档已经被删除。在segment中,被删除的文档依旧是能够被搜索到的,不过在返回搜索结果前,会根据.del把那些已经删除的文档从搜索结果中过滤掉。
- segment合并时,才真正意义上的物理删除数据;前面讲到删除文档的时候,并没有真正从segment中将文档删除,而是维护了一个.del文件,但是当segment合并的过程中,就会自动将.del中的文档丢掉,从而实现真正意义上的删除操作
-
ReFresh:
- 在ES中,将缓存中的文档写入segment,并打开segment使之可以被搜索的过程叫做refresh。
- 在文档被写入segment之后,segment首先被写入了文件系统的缓存中,这个过程仅使用很少的资源。之后segment会从文件系统的缓存中逐渐flush到磁盘,这个过程时间消耗较大。但是实际上存放在文件缓存中的文件同样可以被打开读取。
- 默认情况下,分片的refresh频率是每秒1次。这就解释了为什么es声称提供实时搜索功能,新增加的文档会在1s内就可以进行搜索了。
-
shard(分片)是一个lucene实例,由多个segment组成,segment中包含了原始数据和倒排索引等一系列数据和元数据信息
-
commit point:文件commit point,用来记录当前所有可用的segment,对所有segment的一个抽象管理
2、倒排索引
- 与传统的数据库不同,在es中,每个字段里面的每个单词都是可以被搜索的。
如hobby:"dance,sing,swim,run",我们在搜索关键字swim时,所有包含swim的文档都会被匹配到,es的这个特性也叫做全文搜索。
为了支持这个特性,es中会维护一个叫做“invertedindex”(也叫逆向索引)的表,表内包含了所有文档中出现的所有单词,同时记录了这个单词在哪个文档中出现过。
在Elasticsearch中, 需要搞清楚几个名词,
如segment/doc/term/token/shard/index等,其实segment/doc/term/token都是lucene中的概念。这样有助于更深入的了解和使用ES。
-
segment :
- lucene内部的数据是由一个个segment组成的,写入lucene的数据并不直接落盘,而是先写在内存中,经过了refresh间隔,lucene才将该时间段写入的全部数据refresh成一个segment,segment多了之后会进行merge成更大的segment。
- lucene查询时会遍历每个segment完成。由于lucene 写入的数据是在内存中完成,所以写入效率非常高。但是也存在丢失数据的风险,所以Elasticsearch基于此现象实现了translog,只有在segment数据落盘后,Elasticsearch才会删除对应的translog。
-
doc:doc表示ES中的一条记录
-
field:field表示记录中的字段概念,一个doc由若干个field组成。
-
term :**term是lucene中索引的最小单位,某个field对应的内容如果是全文检索类型,会将内容进行分词,分词的结果就是由term组成的。**如果是不分词的字段,那么该字段的内容就是一个term。
-
倒排索引(inverted index):我们还是对数据源进行切词,只不过建立索引的方式改变了。
-
正排索引:比如MySQL。索引就类似于目录,文档ID到文档内容、单词的关联关系。平时我们使用的都是索引,都是通过主键定位到某条数据。
-
倒排索引(Inverted index)恰好相反:是单词(分词)到文档ID的关联。
-
这样的好处是每个单词只会出现一次,后面跟 该单词的索引信息,从而避免了重复的问题,减少数据量
-
示例
经过分词和建立索引之后得到的倒排索引如下:
-
此时,当用户搜索“搜索引擎”时,根据倒排索引我们可以很容易的知道“搜索引擎”对应的文档是1和3,然后我们根据正排索引可以查询到文档1和文档2的内容,并返回给用户最终结果。
- 倒排索引里面不止记录了单词与文档的对应关系,它还维护了很多其他有用的数据。
- 如:每个文档一共包含了多少个单词,单词在不同文档中的出现频率,每个文档的长度,所有文档的总长度等等。这些数据用来给搜索结果进行打分,如搜索单词apple时,那么出现apple这个单词次数最多的文档会被优先返回,因为它匹配的次数最多,和我们的搜索条件关联性最大,因此得分也最多。
- 倒排索引是不可更改的,一旦它被建立了,里面的数据就不会再进行更改。这样做就带来了以下几个好处:
- 没有必要给逆向索引加锁,因为不允许被更改,只有读操作,所以就不用考虑多线程导致互斥等问题。
- 索引一旦被加载到了缓存中,大部分访问操作都是对内存的读操作,省去了访问磁盘带来的io开销。
- 因为逆向索引的不可变性,所有基于该索引而产生的缓存也不需要更改,因为没有数据变更。
- 使用逆向索引可以压缩数据,减少磁盘io及对内存的消耗。
3、Segment
segment本质上就是倒排索引集合。
既然倒排索引是不可更改的,那么如何添加新的数据,删除数据以及更新数据?
- 为了解决这个问题,lucene将一个大的逆向索引拆分成了多个小的段segment。
- 每个segment本质上就是一个逆向索引。
- 在lucene中,同时还会维护一个文件commit point,用来记录当前所有可用的segment,当我们在这个commit point上进行搜索时,就相当于在它下面的segment中进行搜索,每个segment返回自己的搜索结果,然后进行汇总返回给用户。
引入了segment和commit point的概念之后,数据的更新流程如下图:
-
新增的文档首先会被存放在内存的缓存中
-
当文档数足够多或者到达一定时间点时,就会对缓存进行commit
a. 生成一个新的segment,并写入磁盘
b. 生成一个新的commit point,记录当前所有可用的segment
c. 等待所有数据都已写入磁盘
-
打开新增的segment,这样我们就可以对新增的文档进行搜索了
-
清空缓存,准备接收新的文档
4、文档的更新与删除:
segment是不能更改的,那么如何删除或者更新文档?
删除:
每个commit point都会维护一个.del文件,文件内记录了在某个segment内某个文档已经被删除。在segment中,被删除的文档依旧是能够被搜索到的,不过在返回搜索结果前,会根据.del把那些已经删除的文档从搜索结果中过滤掉。
更新:
对于文档的更新,采用和删除文档类似的实现方式。当一个文档发生更新时,首先会在.del中声明这个文档已经被删除,同时新的文档会被存放到一个新的segment中。这样在搜索时,虽然新的文档和老的文档都会被匹配到,但是.del会把老的文档过滤掉,返回的结果中只包含更新后的文档。
5、Refresh (实现实时搜索)
在ES中,将缓存中的文档写入segment,并打开segment使之可以被搜索的过程叫做refresh
ES的一个特性就是提供实时搜索,新增加的文档可以在很短的时间内就被搜索到。
-
在创建一个commit point时,为了确保所有的数据都已经成功写入磁盘,避免因为断电等原因导致缓存中的数据丢失,在创建segment时需要一个fsync(同步)的操作来确保磁盘写入成功。
-
但是如果每次新增一个文档都要执行一次fsync就会产生很大的性能影响。
-
在文档被写入segment之后,segment首先被写入了文件系统的缓存中,这个过程仅使用很少的资源。之后segment会从文件系统的缓存中逐渐flush到磁盘,这个过程时间消耗较大。
-
但是实际上存放在文件缓存中的文件同样可以被打开读取。ES利用这个特性,在segment被commit到磁盘之前,就打开对应的segment,这样存放在这个segment中的文档就可以立即被搜索到了。
上图中灰色部分即存放在文件系统缓存中,还没有被commit到磁盘的segment。此时这个segment已经可以进行搜索。
- 在ES中,将文件系统缓存中的文档写入segment,并打开segment使之可以被搜索的过程叫做refresh。
- 默认情况下,分片的refresh频率是每秒1次。这就解释了为什么es声称提供实时搜索功能,新增加的文档会在1s内就可以进行搜索了。
Refresh的频率通过index.refresh_interval:100s参数控制,一条新写入es的日志,在进行refresh之前,是在es中不能立即搜索不到的。
通过执行curl -XPOST127.0.0.1:9200/_refresh,可以手动触发refresh行为。
6、flush与translog
- 前面讲到,refresh行为会立即把缓存中的文档写入segment中,但是此时新创建的segment是写在文件系统的缓存中的。
如果出现断电等异常,那么这部分数据就丢失了。所以es会定期执行flush操作,将缓存中的segment全部写入磁盘并确保写入成功,同时创建一个commit point,整个过程就是一个完整的commit过程。
flush到磁盘:
- ES默认每隔30分钟会将文件系统缓存的segment数据刷入到磁盘
- es会定期执行flush操作,将文件缓存中的segment全部写入磁盘并确保写入成功,同时创建一个commit point,整个过程就是一个完整的commit过程。
但是如果断电的时候,缓存中的segment还没有来得及被commit到磁盘,那么数据依旧会产生丢失。为了防止这个问题,es中又引入了translog文件。
translog保障容错:
- 在写入到内存中的同时,也会记录translog日志,在refresh期间出现异常,会根据translog来进行数据恢复
- 等到文件系统缓存中的segment数据都刷到磁盘中(commit),清空translog文件
文档写入步骤:
-
每当es接收一个文档时,在把文档放在buffer的同时,都会把文档记录在translog中。
-
执行refresh操作时,会将缓存中的文档写入segment中,但是此时segment是放在缓存中的,并没有落入磁盘,此时新创建的segment是可以进行搜索的。
- 按照如上的流程,新的segment继续被创建,同时这期间新增的文档会一直被写到translog中。
- 当达到一定的时间间隔,或者translog足够大时,就会执行commit行为,将所有缓存中的segment写入磁盘(flush操作)。确保写入成功后,translog就会被清空。
执行commit并清空translog的行为,在es中可以通过_flush api进行手动触发。
如:
curl -XPOST127.0.0.1:9200/tcpflow-2015.06.17/_flush?v
通常这个flush行为不需要人工干预,交给es自动执行就好了。同时,在重启es或者关闭索引之间,建议先执行flush行为,确保所有数据都被写入磁盘,避免照成数据丢失。通过调用sh service.sh start/restart,会自动完成flush操作。
7、Segment的合并
Segment太多时,ES定期会将多个segment合并成为大的segment(在磁盘中操作),减少索引查询时IO开销,此阶段ES会真正的物理删除(之前执行过的delete的数据)
-
前面讲到es会定期的将收到的文档写入新的segment中,这样经过一段时间之后,就会出现很多segment。
但是每个segment都会占用独立的文件句柄/内存/消耗cpu资源,而且,在查询的时候,需要在每个segment上都执行一次查询,这样是很消耗性能的。
-
为了解决这个问题,es会自动定期的将多个小segment合并为一个大的segment。
前面讲到删除文档的时候,并没有真正从segment中将文档删除,而是维护了一个.del文件,但是当segment合并的过程中,就会自动将.del中的文档丢掉,从而实现真正意义上的删除操作。
-
当新合并后的segment完全写入磁盘之后,es就会自动删除掉那些零碎的segment,之后的查询都在新合并的segment上执行。Segment的合并会消耗大量的IO、cpu、磁盘容量资源,这会影响查询性能。