🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# Elasticsearch学习笔记(二)单节点准实时索引的实现 ## 为什么要学习架构? Elasticsearch的一些架构设计,对我们做性能调优、故障处理,具有非常重要的影响。下面将从Elasticsearch 的准实时索引的实现、自动发现、rounting和replica的读写过程,shard的allocate控制 ## 使文本可以被搜索? 在传统的数据库中,一个字段存一个值,但是这对于全文搜索是不足的。想要让文本中的而每个单词都可以 被搜索,这意味着数据库需要多个值。 支持一个字段多个值的最佳数据结构是倒排索引。倒排索引包含了出现在所有文档中唯一的值或或词的有序列 表,以及每个词所属的文档列表。 ![](https://box.kancloud.cn/2016-03-10_56e11d242e8ce.jpg) 倒排索引存储了比包含一个term的文档列表多地多的信息,它可能包含每一个term的文档数量,一个term出现 在制定文档中的频次,每个文档中term的顺序,每个文档的长度,所有文档的平均长度等等。这些统计信息使 Elasticsearch知道哪些term更重要,哪些文档更重要,也就是相关性。 在全文搜索的早些时候,会为整个文档集合建立一个大索引,并且写入磁盘。只有新索引准备好了它就会替代 旧的索引,最近的修改可以被检索。 **不可变性** 写入磁盘的倒排索引是不可变的,它有如下好处: - 不需要锁。如果从来不需要跟新一个索引,就不必担心多个程序见同时尝试修改。 - 一旦索引被读入文件系统的缓存,它就一直在那儿,因为不会改变。只要文件系统缓存有足够的空 间,大部分的读会直接访问内存而不是磁盘。这有助于性能的提升。 - 在索引的声明周期内,所有的其他缓存都可用。他们不需要再每次数据变化了都重建,因此数据不会变。 - 写入单个大的倒排索引,可以压缩数据,较少的磁盘IO和需要缓存索引的大小。 当然,不可变的索引有它的缺点,首先是它不可变。如果想要搜索一个新文档,必须重建整个索引。这不仅限 制了一个索引所能装下的数据,还有一个索引可以被更新的频次。 ## 准实时索引的实现? 本文主要介绍Elasticsearch的准实时索引的实现,至于基于Lucene的倒排索引将不在这里介绍,有兴趣的读者 可以去Lucene的相关文章,或者阅读《Lucene in Action》等书籍。下面将介绍Elasticsearch索引流程中发生的 具体操作,重点在于其中的segment、buffer和translog三部分对性能方面的影响。 ### 1、动态更新的Lucene索引 要做到实时跟新条件下数据的可用和可靠,就需要在倒排索引的基础上,再做一系列更高级的处理。 总结一下Lucene的处理办法:新收到的数据写入新的索引文件里。 Lucene把每次生成的倒排索引,叫做一个段(segment)。然后另外使用一个commit文件,记录索引内的所有 segment。而生成segment的数据来源,则是内存中的buffer,也就是说,动态跟新过后过程如下: 1)当前磁盘上有三个segement可用,同时有一个commit文件记录当前的segment 2)新收到的数据进入内存buffer,索引状态如下所示。 3)buffer刷到磁盘,生成一个新的segment,commit文件同步跟新。 这样可以完成跟新,也产生了几个问题:1、每次一有数据就刷新到磁盘,会增大对磁盘的操作2、刷新到磁盘的 时间占据很大一部分时间3、如果刷新的过程中刷新失败应该怎么控制呢? ![](https://box.kancloud.cn/2016-03-10_56e11d243e39f.jpg) ![](https://box.kancloud.cn/2016-03-10_56e11d2453c90.jpg) ![](https://box.kancloud.cn/2016-03-10_56e11d2467956.jpg) ### 2、删除和更新 segment是不可变的,所以文档即不能从旧的段中删除,旧的段也不能更新以反映文档最新的文本。相反,每 一个提交点包括一个.del文件,包含了段上已经被删除的文档 当一个文档被删除,它是实际上只是在.del文件中被标记删除,亦然可以匹配查询,但最终返回之前会被从结果 中删除。 文档的跟新操作是类似的:当一个文档被更新,旧版本的文档被标记为删除,新版本的文档在新的段中索引。也 许该文档的不同版本都会匹配一个查询,但是老版本会从结果中删除。 ### 3、利用磁盘缓存实现的准实时检索 既然涉及到磁盘,那么一个不可避免的问题就来了:磁盘太慢了!对我们要求的实时性很高的服务来说,这种处 理还不够。所以,在刚刚第3步的处理中,还有一个中间状态: 1)内存buffer生成一个新的segment,刷到文件系统缓存中,Lucene即可检索到这个新的segment,索引状态如 图所示。 2)文件系统缓存真正同步到磁盘上,commit文件跟新。 刷到文件系统缓存中这个步骤,Elasticsearch默认1s的时间间隔,这也就是说相当于是实时搜索的,Elasticsearch 也提供了单独的/_reflush接口,用户如果对1s间隔还是不太满意,可以主动调用接口来保证搜索可见。 reflush API ~~~ POST /_refresh <1> POST /blogs/_refresh <2> ~~~ - <1> refresh所有索引 - <2> 只refresh 索引`blogs` 一般来说我们会通过/_settings接口或者定制template的方式,加大refresh_interval参数: ~~~ PUT /my_logs/_settings { "refresh_interval": -1 } <1> PUT /my_logs/_settings { "refresh_interval": "1s" } <2> ~~~ - <1> 禁用所有自动refresh - <2> 每秒自动refresh ### 4、translog提供的磁盘同步控制 既然refresh只是写到文件系统缓存中,那么最后一步写到实际磁盘又是由什么来控制的呢?如果这期间发生 主机错误、硬盘故障等异常情况,数据会不会丢失? 这里,其实Elasticsearch提供了另一个机制来控制。Elasticsearch也把数据写入到内存buffer的同时, 其实还另外记录了一个treanslog的日志。也就是说,在内存数据进入到buffer这一步骤时,其实还另外记录 了一个translog记录。如图所示 1.当一个文档被索引,它被加入到内存缓存,同时加到事务日志。 **图1:新的文档加入到内存缓存,同时写入事务日志** ![](https://box.kancloud.cn/2016-03-10_56e11d2477b86.jpg) 2.refersh使得分片进入下图描述的状态。每个分片都进行refresh - 内存缓冲区的文档写入到segment中,但是还没有同步到磁盘。 - segment被打开,使得新的文档可以搜索。 - 缓存被清空 - 但是事务日志没变化 图2:经过一次refresh,缓存被清除,但事务日志没有 ![](https://box.kancloud.cn/2016-03-10_56e11d2490b7e.jpg) 3、随着更多的文档加入到缓存区,写入日志 图3:事务日志会记录增长的文档  ![](https://box.kancloud.cn/2016-03-10_56e11d24abef4.jpg) 4、假如在这期间发生异常,Elasticsearch会从commit位置开始,恢复整个translog文件中的记录,保护**数据的一致性**。 等到真正把segement刷到磁盘,且commit文件被清空的时候,translog文件才清空,这一步叫flush,同样地,Elasticsearch提供了 /_flush接口。 - 内存缓存区的所有文档会写入到新段中, - 清楚缓存 - 一个提交点写入到硬盘中 - 文件系统缓存通过gsync操作flush到硬盘 - 事务日志被清除 事务日志记录了没有flush到硬盘的所有操作。当故障重启后,ES会用最近一次提交点从硬盘恢复所有已知的段,并且从日志里恢复所有的操作。 事务日志还用来提供实时的CRUD操作。当年用ID进行CRUD时,它在检索相关段内的文档前会首先检查日志最新的改动。这意味着ES可以实时地获取文档的最新版本。 图4:flush过后,段被全提交,事务日志清除 ![](https://box.kancloud.cn/2016-03-10_56e11d24c6a04.jpg) 在ES中,进行一次提交并删除事务日志的操作叫做 `flush`。分片每30分钟,或事务日志过大会进行一次flush操作。 对于flush操作,Elasticsearch的操作默认设置为:每30分钟主动进行一次flush,或懂translog文件大于512M,主动进行一次flush,这两个行为可以通过 分别设置index.translog.flush_threshold_period和index.translog.flush_threslog_size参数修改 **flush API** 当然也可以手动进行flush ~~~ POST /blogs/_flush <1> POST /_flush?wait_for_ongoing <2> ~~~ - <1> flush索引`blogs` - <2> flush所有索引,等待操作结束再返回 ### 5、translog的一致性 索引数据的一致性通过translog保证。那么translog文件自己呢? 默认情况下,Elasticsearch每5秒就会强制刷新到translog日志到磁盘上,所以,如果数据没有副本,然后又发生故障,确实有可能丢失5秒数据,可以通过设置 index.gateway.local.sync设置,然后重启Elasticsearch 前面一直在讲的是Lucene的索引,Elasticsearch在这个基础上,做了一些改动,Elasticsearch的索引是分片的集合,而分片就相当于Lucene的索引。 ### 6、segement merge的影响 上面提到Lucene思想:新收到的数据写入到新的索引文件里面,每一个索引文件都是不可变的,开新文件就会给服务器带来负载压力因为默认每1秒钟就会有一个新文件产生,每个文件都需要文件句柄、内存、cpu等各种资源,给服务器带来很大的开销 为了解决这个问题,Elasticsearch会不断地在后台运行任务,主动将这些零散地segement做数据归并,尽量让索引中只保有少量的,每个都比较大的segement。 这个过程是由独立的线程来进行的,并不影响segment的产生。归并过程中,删除之间几个小的segment,改成新的大的segment。等检索请求都从小segement转到大的segement上以后,删除没用的小segement。 图1:两个提交的段和一个未提交的段合并为了一个更大的段  ![](https://box.kancloud.cn/2016-03-10_56e11d24e9f77.jpg) 图2:段合并完后,旧的段被删除  ![](https://box.kancloud.cn/2016-03-10_56e11d250820f.jpg) - 新的段flush到了硬盘。 - 新的段写入commit文件,排除旧的段。 - 新的段打开供搜索。 - 旧的段被删除 ### 归并线程设置 segment归并的过程,需要先读取segment,归并计算,再写一遍segment,最后还要保证刷到磁盘,可以说,这是一个非常消耗磁盘I/O 的任务。 默认情况下,归并线程的限速设置indices.store.throttle.max_bytes_per_sec是20MB。对于写入量较大,磁盘转速较高,甚至使用SSD盘的服务器来说,这个限速明显是过低的。对于EIK应用来说,建议可以适当跳大 ~~~ curl -XPUT 'http://localhost:9200/_cluster/settings' -d ' { "persistent" : { "indices.store.throttle.max_bytes_per_sec" : "100mb" } }' ~~~ 归并策略 归并线程是按照一定的归并策略来挑选segment进行归并的。 index.merge.policy.floor_segment默认2MB,小于这个大小的segment,优先被归并 index.merge.policy.max_merge_at_once默认一次最多归并10个segment index.merge.policy.max_merge_at_once_explicit默认optimize时一次最多归并30个segment index.merge.policy.max_merge_segment默认5GB,大于这个大小的segment.不用参与归并,optimize除外 根据这些策略,其我们也可以从另一个角度考虑如何减少segment归并的线程以及提高效应的办法:加大reflush间隔, 尽量让每次新生成的segment本身大小就很大。 **optimize API** optmize API最好描述为强制合并段API。它强制合并段以达到指定max_num_segments参数。这是为了减少段的数量 (通常为1)达到提高搜索性能的目的。 `POST /logstash-2014-10/_optimize?max_num_segments=1 <1>` <1> 把索引中的每个分片都合并成一个段 由于optimize线程对资源的消耗比普通的归并线程大得多,索引,绝对不建议对还在写入数据的热索引执行这个操作,这个问题 对于ELK stack来说很好办,一般索引都是按天分割的。 本文介绍的是单节点准实时索引介绍到这里,考虑到篇幅的问题,将会在下篇介绍Elasticsearch的集群分片解决方案。 [**http://blog.csdn.net/u010994304/article/details/50441419**](http://blog.csdn.net/u010994304/article/details/50441419)