ElasticSearch第三弹之存储原理

咱们上文中介绍的ES内部索引的写处理流程是在ES的内存中执行的,而数据被分配到特定的主、副分片上以后,最终是存储到磁盘上的,这样在断电的时候就不会丢失数据。具体的存储路径可在配置文件 ../config/elasticsearch.yml 中进行设置,默认存储在安装目录的 Data文件夹下。建议不要使用默认值,由于若 ES 进行了升级,则有可能致使数据所有丢失。文件配置以下:缓存

path.data: /path/to/data  //索引数据
path.logs: /path/to/logs  //日志记录

那么ES是怎么将索引从内存中同步到磁盘上的呢?今天咱们就来讲一下ES的存储原理(搬着小板凳坐好)。安全

咱们先设想一下,ES是不是直接调用 Fsync 物理性地写入磁盘?答案是否认的,若是是直接写入磁盘,磁盘的 I/O 消耗会严重影响性能,那么当写数据量大的时候会形成 ES 停顿卡死,查询也没法作到快速响应, ES 就不会被称为近实时全文搜索引擎了。那么问题来了,ES 是采用什么方式存储的呢?服务器

首先咱们先来讲几个概念,而后再具体介绍下它的整个流程及细节处理,方便你们更好的理解。微信

索引文档被拆分红多个子文档,则每一个子文档叫做段。段提出来的缘由是:在早期全文检索中为整个文档集合创建了一个很大的倒排索引,并将其写入磁盘中。若是索引有更新,就须要从新全量建立一个索引来替换原来的索引。这种方式在数据量很大时效率很低,而且因为建立一次索引的成本很高,因此对数据的更新不能过于频繁,也就不能保证时效性。并发

特色

索引文档是以段的形式存储在磁盘上的,每个段自己都是一个倒排索引,而且段具备不变性,一旦索引的数据被写入硬盘,就不能再修改。异步

那么问题来了,不能修改,如何实现增删改呢?async

  • 新增:新增很好处理,因为数据是新的,因此只须要对当前文档新增一个段就能够了。
  • 删除:段是不可改变的,因此既不能把文档从旧的段中移除,也不能修改旧的段来进行文档的更新。取而代之的是每一个提交点(定义会在下边给出)会包含一个 .del 文件,文件中会列出这些被删除文档的段信息。当一个文档被 “删除” 时,它实际上只是在 .del 文件中被标记删除。一个被标记删除的文档仍然能够被查询匹配到,但它会在最终结果被返回前从结果集中移除。
  • 更新:更新至关因而删除和新增这两个动做组成。当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。

一个Lucene索引会包含一个提交点和多个段,段被写入到磁盘后会生成一个提交点,提交点是一个用来记录全部提交后段信息的文件。一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限。ES在启动或从新打开一个索引的过程当中使用这个提交点来判断哪些段隶属于当前分片。elasticsearch

段的优点
  • 不须要锁。若是你历来不更新索引,你就不须要担忧多进程同时修改数据的问题。
  • 一旦索引被读入内核的文件系统缓存,便会留在哪里,因为其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提高。
  • 其它缓存(像 Filter 缓存),在索引的生命周期内始终有效。它们不须要在每次数据改变时被重建,由于数据不会变化。
  • 写入单个大的倒排索引容许数据被压缩,减小磁盘 I/O 和须要被缓存到内存的索引的使用量。
段的缺点
  • 当对旧数据进行删除时,旧数据不会立刻被删除,而是在 .del 文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样会形成大量的空间浪费。
  • 如有一条数据频繁的更新,每次更新都是新增新的标记旧的,则会有大量的空间浪费。
  • 每次新增数据时都须要新增一个段来存储数据。当段的数量太多时,对服务器的资源例如文件句柄的消耗会很是大。
  • 在查询的结果中包含全部的结果集,须要排除被标记删除的旧数据,这增长了查询的负担。

Refresh(刷新)

ES 中,写入和打开一个新段的轻量的过程叫作 Refresh (即ES内存刷新到文件缓存系统)。ES首先会将文档加载到ES的内存缓冲区(当段在内存中时,就只有写的权限,而不具有读数据的权限,意味着不能被检索),当达到默认的时间(1 秒钟)或者内存的数据达到必定量时,会触发一次刷新(Refresh),这时数据就会被加载到文件缓存系统(操做系统的内存),建立新的段并将段打开以供搜索使用。这就是为何咱们说 ES 是近实时搜索,由于文档的变化并非当即对搜索可见,但会在一秒以内变为可见。这就会存在一个问题:当你索引了一个文档而后尝试搜索它,但却没有搜到。这个问题的解决办法是用 refresh API 执行一次手动刷新。配置以下:性能

POST /_refresh         //刷新(Refresh)全部的索引。
POST /blogs/_refresh   //只刷新(Refresh) blogs 索引。

注: 当写测试的时候,手动刷新颇有用,可是不要在生产环境下每次索引一个文档都去手动刷新。学习

尽管刷新是比提交轻量不少的操做,它仍是会有性能开销,并非全部的状况都须要每秒刷新:当你使用 ES 索引大量的日志文件时,你可能想优化索引速度而不是近实时搜索,这时能够在建立索引时在 Settings 中经过调大 refresh_interval = "30s" 的值,下降每一个索引的刷新频率,设值时须要注意后面带上时间单位,不然默认是毫秒,若是是1毫秒无疑会使你的集群陷入瘫痪。当 refresh_interval=-1 时表示关闭索引的自动刷新。配置以下:

PUT /my_logs
{
  "settings": {
    "refresh_interval": "1s"   //每秒刷新 my_logs 索引
  }
}

refresh_interval 能够在既存索引上进行动态更新。 在生产环境中,当你正在创建一个大的新索引时,能够先关闭自动刷新,待开始使用该索引时,再把它们调回来。

段合并

因为自动刷新流程每秒会建立一个新的段,这样会致使短期内的段数量暴增。而段数目太多会带来较大的麻烦。每个段都会消耗文件句柄、内存和 CPU 运行周期。更重要的是,每一个搜索请求都必须轮流检查每一个段而后合并查询结果,因此段越多,搜索也就越慢。ES 经过在后台按期进行段合并来解决这个问题。小的段被合并到大的段,而后这些大的段再被合并到更大的段(这些段既能够是未提交的也能够是已提交的)。

启动段合并不须要你作任何事,进行索引和搜索时会自动进行:

一、 当索引的时候,刷新(refresh)操做会建立新的段并将段打开以供搜索使用;

二、 合并进程选择一小部分大小类似的段,而且在后台将它们合并到更大的段中,这并不会中断索引和搜索;

三、 “一旦合并结束,老的段被删除” 说明合并完成时的活动:新的段被刷新(flush)到了磁盘,写入一个包含新段且排除旧的和较小的段的新提交点,那些旧的已删除文档从文件系统中清除,被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。

段合并的计算量庞大,须要消耗大量的I/O和CPU资源,并会拖累写入速率,若是任其发展会影响搜索性能。ES 在默认状况下会对合并流程进行资源限制,因此搜索仍然有足够的资源很好地执行。限流阈值默认是20MB/s,若是是SSD,能够考虑100-200MB/s;若是是机械磁盘而非SSD,须要增长设置 index.merge.scheduler.max_thread_count: 1。由于机械磁盘在并发 I/O 支持方面比较差,因此咱们须要下降每一个索引并发访问磁盘的线程数。这个设置容许 max_thread_count + 2 个线程同时进行磁盘操做,也就是设置为 1 容许三个线程,SSD默认是 Math.min(3, Runtime.getRuntime().availableProcessors() / 2),支持很好;若是在作批量导入,不在乎搜索,能够设置为none。配置以下:

PUT /_cluster/settings
{
    "persistent" : {
        "indices.store.throttle.max_bytes_per_sec" : "100mb"
    }
 }
optimize API

optimize API大可看作是强制合并 API。它会将一个分片强制合并到 max_num_segments 参数指定大小的段数目。这样作的意图是减小段的数量(一般减小到一个)来提高搜索性能。

optimize API不该该被用在一个活跃的索引--一个正积极更新的索引:后台合并流程已经能够很好地完成工做,optimizing 会阻碍这个进程,不要干扰它!在特定状况下,使用 optimize API 很有益处。例如在日志这种用例下,天天、每周、每个月的日志被存储在一个索引中,老的索引实质上是只读的;它们也并不太可能会发生变化。在这种状况下,使用optimize优化老的索引,将每个分片合并为一个单独的段就颇有用了,这样既能够节省资源,也可使搜索更加快速。

POST /logstash-2014-10/_optimize?max_num_segments=1 //合并索引中的每一个分片为一个单独的段

请注意,使用 optimize API 触发段合并的操做不会受到任何资源上的限制。这可能会消耗掉你节点上所有的I/O资源,使其没有余力来处理搜索请求,从而有可能使集群失去响应。 若是你想要对索引执行 optimize,你须要先使用分片分配把索引移到一个安全的节点,再执行。

Translog

为了提高写的性能,ES 并无每新增一条数据就增长一个段到磁盘上,而是采用延迟写的策略。等文件系统中有新段生成以后,在稍后的时间里再被刷新到磁盘中并生成提交点。虽然经过延时写的策略能够减小数据往磁盘上写的次数提高了总体的写入能力,可是咱们知道文件缓存系统也是内存空间,属于操做系统的内存,只要是内存都存在断电或异常状况下丢失数据的危险。为了不丢失数据,ES 添加了事务日志(Translog),事务日志记录了全部尚未持久化到磁盘的数据。

translog 默认是每5秒被 fsync 刷新到硬盘,或者在每次写请求完成以后执行(index, delete, update, bulk)操做也能够刷新到磁盘。在每次请求后都执行一个 fsync 会带来一些性能损失,尽管实践代表这种损失相对较小(特别是bulk导入,它在一次请求中平摊了大量文档的开销)。对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync 仍是比较有益的。咱们能够经过设置 durability 参数为 async 来启用:

PUT /my_index/_settings
{
    "index.translog.durability": "async",
    "index.translog.sync_interval": "5s"
}

这个选项能够针对索引单独设置,而且能够动态进行修改。若是你决定使用异步 translog 的话,你须要保证在发生crash时,丢失掉 sync_interval 时间段的数据也无所谓。若是你不肯定这个行为的后果,最好是使用默认的参数( "index.translog.durability": "request" )来避免数据丢失。

Flush

执行一个提交而且截断 translog 的行为在ES中被称做一次flush。分片每30分钟被自动刷新(flush)或者在 translog 太大的时候也会刷新。能够经过设置translog 文档来控制这些阈值,flush API 能够被用来执行一个手工的刷新(flush):

POST /blogs/_flush                //刷新(flush) blogs 索引。
POST /_flush?wait_for_ongoing     //刷新(flush)全部的索引而且而且等待全部刷新在返回前完成。

总结

最后咱们来讲一下添加了事务日志后的整个存储的流程吧:

  • 一个新文档被索引以后,先被写入到内存中,可是为了防止数据的丢失,会追加一份数据到事务日志中。不断有新的文档被写入到内存,同时也都会记录到事务日志中(日志默认存储到文件缓存系统,每五秒刷新一下到本地磁盘,可是会致使数据丢失,也能够设置参数每一个请求都同步,可是性能降低)。这时新数据还不能被检索和查询。
  • 当达到默认的刷新时间或内存中的数据达到必定量后,会触发一次 Refresh,将内存中的数据以一个新段形式刷新到文件缓存系统中并清空内存。这时虽然新段未被提交到磁盘,可是能够提供文档的检索功能且不能被修改。
  • 随着新文档索引不断被写入,当日志数据大小超过 512M 或者时间超过 30 分钟时,会触发一次 Flush。内存中的数据被写入到一个新段同时被写入到文件缓存系统,文件系统缓存中数据经过 Fsync 刷新到磁盘中,生成提交点,日志文件被删除,建立一个空的新日志。
  • 经过这种方式当断电或须要重启时,ES 不只要根据提交点去加载已经持久化过的段,还须要读取 Translog 里的记录,把未持久化的数据从新持久化到磁盘上,避免了数据丢失的可能。

阿Q正在将ES的知识作一个系统的学习与讲解,后续还会持续输出ES的相关知识,若是你感兴趣的话,能够关注gzh“阿Q说代码”!也能够加我微信qingqing-4132,期待你的到来!