Elasticsearch之存储原理
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Elasticsearch之存储原理相关的知识,希望对你有一定的参考价值。
参考技术A倒排索引被写入磁盘后是不可变的,ES解决不变性和更新索引的方式是使用多个索引,利用新增的索引来反映修改,在查询时从旧的到新的依次查询,最后来一个结果合并。
ES底层是基于Lucene,最核心的概念就是 Segment(段) ,每个段本身就是一个倒排索引。
ES中的Index由多个段的集合和 commit point(提交点) 文件组成。
提交点文件中有一个列表存放着所有已知的段,下面是一个带有1个提交点和3个段的Index示意图:
Doc会先被搜集到内存中的Buffer内,这个时候还无法被搜索到,如下图所示:
每隔一段时间,会将buffer提交,在flush磁盘后打开新段使得搜索可见,详细过程如下:
下面展示了这个过程完成后的段和提交点的状态:
通过这种方式,可以使得新文档从被索引到可被搜索间的时间间隔在数分钟,但是还不够快。因为磁盘需要 fsync ,这个就成为性能瓶颈。我们前面提到过Doc会先被从buffer刷入段写入文件系统缓存(很快),那么就自然想到在这个阶段就让文档对搜索可见,随后再被刷入磁盘(较慢)。
Lucene支持对新段写入和打开,可以使文档在没有完全刷入硬盘的状态下就能对搜索可见,而且是一个开销较小的操作,可以频繁进行。
下面是一个已经将Docs刷入段,但还没有完全提交的示意图:
我们可以看到,新段虽然还没有被完全提交,但是已经对搜索可见了。
引入refresh操作的目的是提高ES的实时性,使添加文档尽可能快的被搜索到,同时又避免频繁fsync带来性能开销,依靠的就是文件系统缓存OS cache里缓存的文件可以被打开(open/reopen)和读取,而这个os cache实际是一块内存区域,而非磁盘,所以操作是很快的,这就是ES被称为近实时搜索的原因。
refresh默认执行的间隔是1秒,可以使用 refreshAPI 进行手动操作,但一般不建议这么做。还可以通过合理设置 refresh_interval 在近实时搜索和索引速度间做权衡。
index segment刷入到os cache后就可以打开供查询,这个操作是有潜在风险的,因为os cache中的数据有可能在意外的故障中丢失,而此时数据必备并未刷入到os disk,此时数据丢失将是不可逆的,这个时候就需要一种机制,可以将对es的操作记录下来,来确保当出现故障的时候,已经落地到磁盘的数据不会丢失,并在重启的时候可以从操作记录中将数据恢复过来。elasticsearch提供了translog来记录这些操作,结合os cached segments数据定时落盘来实现数据可靠性保证(flush)。
文档被添加到buffer同时追加到translog:
进行 refresh 操作,清空buffer,文档可被搜索但尚未 flush 到磁盘。translog不会清空:
每隔一段时间(例如translog变得太大),index会被flush到磁盘,新的translog文件被创建,commit执行结束后,会发生以下事件:
下面示意图展示了这个状态:
translog记录的是已经 在内存生成(segments)并存储到os cache但是还没写到磁盘的那些索引操作 (注意,有一种解释说,添加到buffer中但是没有被存入segment中的数据没有被记录到translog中,这依赖于写translog的时机,不同版本可能有变化,不影响理解),此时这些新写入的数据可以被搜索到,但是当节点挂掉后这些未来得及落入磁盘的数据就会丢失,可以通过trangslog恢复。
当然translog本身也是磁盘文件,频繁的写入磁盘会带来巨大的IO开销,因此对translog的追加写入操作的同样操作的是os cache,因此也需要定时落盘(fsync)。translog落盘的时间间隔直接决定了ES的可靠性,因为宕机可能导致这个时间间隔内所有的ES操作既没有生成segment磁盘文件,又没有记录到Translog磁盘文件中,导致这期间的所有操作都丢失且无法恢复。
translog的fsync是ES在后台自动执行的,默认是每5秒钟主动进行一次translog fsync,或者当translog文件大小大于512MB主动进行一次fsync,对应的配置是 index.translog.flush_threshold_period 和 index.translog.flush_threshold_size 。
当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。
translog 也被用来提供实时 CRUD 。当你试着通过ID来RUD一个Doc,它会在从相关的段检索之前先检查 translog 中最新的变更。
默认 translog 是每5秒或是每次请求完成后被 fsync 到磁盘(在主分片和副本分片都会)。也就是说,如果你发起一个index, delete, update, bulk请求写入translog并被fsync到主分片和副本分片的磁盘前不会反回200状态。
这样会带来一些性能损失,可以通过设为异步fsync,但是必须接受由此带来的丢失少量数据的风险:
flush 就是执行commit清空、干掉老translog的过程。默认每个分片30分钟或者是translog过于大的时候自动flush一次。可以通过flush API手动触发,但是只会在重启节点或关闭某个索引的时候这样做,因为这可以让未来ES恢复的速度更快(translog文件更小)。
满足下列条件之一就会触发冲刷操作:
整体流程:
删除一个ES文档不会立即从磁盘上移除,它只是被标记成已删除。因为段是不可变的,所以文档既不能从旧的段中移除,旧的段也不能更新以反映文档最新的版本。
ES的做法是,每一个提交点包括一个 .del 文件(还包括新段),包含了段上已经被标记为删除状态的文档。所以,当一个文档被做删除操作,实际上只是在 .del 文件中将该文档标记为删除,依然会在查询时被匹配到,只不过在最终返回结果之前会被从结果中删除。ES将会在用户之后添加更多索引的时候,在后台进行要删除内容的清理。
文档的更新操作和删除是类似的:当一个文档被更新,旧版本的文档被标记为删除,新版本的文档在新的段中索引。
该文档的不同版本都会匹配一个查询,但是较旧的版本会从结果中删除。
通过每秒自动刷新创建新的段,用不了多久段的数量就爆炸了,每个段消费大量文件句柄,内存,cpu资源。更重要的是,每次搜索请求都需要依次检查每个段。段越多,查询越慢。
ES通过后台合并段解决这个问题。ES利用段合并的时机来真正从文件系统删除那些version较老或者是被标记为删除的文档。被删除的文档(或者是version较老的)不会再被合并到新的更大的段中。
可见,段合并主要有两个目的:
ES对一个不断有数据写入的索引处理流程如下:
合并过程如图:
从上图可以看到,段合并之前,旧有的Commit和没Commit的小段皆可被搜索。
段合并后的操作:
合并完成后新的段可被搜索,旧的段被删除,如下图所示:
注意 :段合并过程虽然看起来很爽,但是大段的合并可能会占用大量的IO和CPU,如果不加以控制,可能会大大降低搜索性能。段合并的optimize API 不是非常特殊的情况下千万不要使用,默认策略已经足够好了。不恰当的使用可能会将你机器的资源全部耗尽在段合并上,导致无法搜索、无法响应。
#yyds干货盘点# 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
是采用什么方式存储的呢?
首先我们先来说几个概念,然后再具体介绍下它的整个流程及细节处理,方便大家更好的理解。
段
索引文档被拆分成多个子文档,则每个子文档叫作段。段提出来的原因是:在早期全文检索中为整个文档集合建立了一个很大的倒排索引,并将其写入磁盘中。如果索引有更新,就需要重新全量创建一个索引来替换原来的索引。这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证时效性。
特点
索引文档是以段的形式存储在磁盘上的,每一个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不能再修改。
那么问题来了,不能修改,如何实现增删改呢?
- 新增:新增很好处理,由于数据是新的,所以只需要对当前文档新增一个段就可以了。
- 删除:段是不可改变的,所以既不能把文档从旧的段中移除,也不能修改旧的段来进行文档的更新。取而代之的是每个提交点(定义会在下边给出)会包含一个 .del 文件,文件中会列出这些被删除文档的段信息。当一个文档被 “删除” 时,它实际上只是在 .del 文件中被标记删除。一个被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前从结果集中移除。
- 更新:更新相当于是删除和新增这两个动作组成。当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
段的优势
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像
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 索引
}
}
段合并
由于自动刷新流程每秒会创建一个新的段,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。每一个段都会消耗文件句柄、内存和 CPU
运行周期。更重要的是,每个搜索请求都必须轮流检查每个段然后合并查询结果,所以段越多,搜索也就越慢。ES
通过在后台定期进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段(这些段既可以是未提交的也可以是已提交的)。
启动段合并不需要你做任何事,进行索引和搜索时会自动进行:
1、 当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用;
2、 合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中,这并不会中断索引和搜索;
3、 “一旦合并结束,老的段被删除” 说明合并完成时的活动:新的段被刷新(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 //合并索引中的每个分片为一个单独的段
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的相关知识,如果你感兴趣的话,可以关注“阿Q说代码”!你也可以后台留言说出你的疑惑,阿Q将会在后期的文章中为你解答。
以上是关于Elasticsearch之存储原理的主要内容,如果未能解决你的问题,请参考以下文章
Elasticsearch核心原理系列:10张图理解Elasticsearch核心概念
Elasticsearch全文搜索技术之二kibana的简介和使用
Elasticsearch全文搜索技术之二kibana的简介和使用