ElasticSearch文档写入、实时搜索、持久化、段合并机制

1,972 阅读11分钟

索引文档写入

Segments in Lucene

众所周知,Elasticsearch存储的基本单元shard,ES中一个Index可能分为多个shard, 事实上每个shard都是一个Lucence的Index,并且每个Lucence Index由多个Segment组成, 每个Segment事实上是一些倒排索引的集合,每次创建一个新的Document,都会归属于一个新的Segment,不会去修改原来的Segment。且每次的文档删除操作,会仅仅标记Segment中该文档为删除状态,而不会真正的立马物理删除,所以说ES的index可以理解为一个抽象的概念。就像下图所示:

image.png

Commits in Lucene

Commit操作意味着将Segment合并,并写入磁盘。保证内存数据尽量不丢。但刷盘是很重的IO操作,所以为了机器性能和近实时搜索,并不会刷盘那么及时。

Translog

新文档被索引意味着文档会被首先写入内存buffer和translog文件。每个shard都对应一个 translog文件

image.png

Refresh in Elasticsearch

在Elasticsearch中,_refresh操作默认每秒执行一次,意味着将内存buffer的数据写入到一个新的Segment中,这个时候索引变成了可被检索的。写入新Segment后会清空内存buffer。这边是写入file system cache中(内存)

image.png

Flush in Elasticsearch

Flush操作意味着将内存buffer的数据全都写入新的Segments中,并将内存中所有的 Segments全部刷盘,并且清空 translog日志的过程。

image.png

近实时搜索

提交(Commiting)一个新的段到磁盘需要一个fsync(文件同步)来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。但是fsync操作代价很大; 如果每次索引一个文档都去执行一次的话会造成很大的性能问题。
我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着fsync要从整个过程中被移除。
在Elasticsearch和磁盘之间是文件系统缓存。像之前描述的一样,在内存索引缓冲区中的文档会被写入到一个新的段中。但是这里新段会被先写入到文件系统缓存--这一步代价会比较低,稍后再被刷新到磁盘--这一步代价比较高。不过只要文件已经在系统缓存中,就可以像其它文件一样被打开和读取了。

原理

下图表示是es写操作流程,当一个写请求发送到e 后,es将数据写入memory buffer中,并添加事务日志( translog )。如果每次一条数据写入内存后立即写到硬盘文件上,由于写入的数据肯定是离散的,因此写入硬盘的操作也就是随机写入了。硬盘随机写入的效率相当低,会严重降低es的性能。因此es在设计时 memory buffer和硬盘间加入了Linux的高速缓存(File system cache)来提高es的写效率。当写请求发送到es后,es将数据暂时写入 memory buffer中,此时写入的数据还不能被查询到。默认设置下,es每1秒钟将memory buffer中的数据refresh到Linux的File system cache,并清空memory buffer,此时写入的数据就可以被查询到了。 image.png

refresh API 手动刷新

在Elasticsearch中,写入和打开一个新段的轻量的过程叫做refresh。默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说Elasticsearch是近实时搜索:文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。这些行为可能会对新用户造成困惑:他们索引了一个文档然后尝试搜索它,但却没有搜到。这个问题的解决办法是用refresh API执行一次手动刷新:

#刷新所有索引
1. POST /_refresh
#刷新指定索引
2. POST /my_blogs/_refresh
#刷新指定文档
3. PUT /my_blogs/_doc/1?refresh
{"test": "test"}
PUT /test/_doc/2?refresh=true
{"test": "test"}

并不是所有的情况都需要每秒刷新。可能你正在使用 Elasticsearch 索引大量的日志文件, 你可能想优化索引速度而不是近实时搜索,可以通过设置refresh_interval,降低每个索引的刷新频率。refresh_interval可以在既存索引上进行动态更新。在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来:

PUT /my_logs/_settings
{
  "refresh_interval": -1
}
PUT /my_logs/_settings
{
  "refresh_interval": "1s"
}

持久化变更

原理

如果没有用fsync把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。为了保证 Elasticsearch 的可靠性,需要确保数据变化被持久化到磁盘。
在动态更新索引时,我们说一次完整的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点。Elasticsearch 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。
即使通过每秒刷新(refresh)实现了近实时搜索,我们仍然需要经常进行完整提交来确保能从失败中恢复。但在两次提交之间发生变化的文档怎么办?我们也不希望丢失掉这些数据。
Elasticsearch增加了一个translog ,或者叫事务日志,在每一次对Elasticsearch进行操作时均进行了日志记录。通过translog,整个流程看起来是下面这样:

  1. 一个文档被索引之后,就会被添加到内存缓冲区,并且追加到了 translog。
  2. 刷新(refresh)将数据从内存缓冲区存入file system cache,分片每秒被刷新(refresh)一次:
    • 这些在内存缓冲区的文档被写入到一个新的段中,且没有进行fsync操作。
    • 这个段被打开,使其可被搜索。
    • 内存缓冲区被清空。
  3. 这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志。
  4. 每隔一段时间,例如translog变得越来越大,索引被刷新(flush);一个新的translog 被创建,并且一个全量提交被执行。
    • 所有在内存缓冲区的文档都被写入一个新的段。
    • 缓冲区被清空。
    • 一个提交点被写入硬盘。
    • 文件系统缓存通过fsync被刷新(flush)。
    • 老的translog被删除。 translog提供所有还没有被刷到磁盘的操作的一个持久化纪录。当Elasticsearch启动的时候,它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放translog中所有在最后一次提交后发生的变更操作。
      translog也被用来提供实时CRUD。当你试着通过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索之前,首先检查translog任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。

flush API 手动刷磁盘

这个执行一个提交并且截断translog的行为在Elasticsearch被称作一次flush。分片每30 分钟被自动刷新(flush),或者在 translog 太大的时候也会刷新。
flush API可以被用来执行一个手工的刷新(flush):

#刷新(flush)blogs索引。
POST /blogs/_flush 
#刷新(flush)所有的索引并且等待所有刷新在返回前完成。
POST /_flush?wait_for_ongoin

我们很少需要自己手动执行一个的flush操作;通常情况下,自动刷新就足够了。
这就是说,在重启节点或关闭索引之前执行flush有益于你的索引。当Elasticsearch尝试恢复或重新打开一个索引,它需要重放translog中所有的操作,所以如果日志越短,恢复越快。

Translog

translog 的目的是保证操作不会丢失。
在文件被fsync到磁盘前,被写入的文件在重启之后就会丢失。默认translog是每5秒被 fsync刷新到硬盘,或者在每次写请求完成之后执行(index, delete, update, bulk)。这个过程在主分片和复制分片都会发生。最终,这意味着在整个请求被fsync到主分片和复制分片的translog之前,你的客户端不会得到一个200 OK响应。
在每次写请求后都执行一个fsync会带来一些性能损失,尽管实践表明这种损失相对较小(特别 是bulk导入,它在一次请求中平摊了大量文档的开销)。
但是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync 还是比较有益的。比如写入的数据被缓存到内存中,再每5秒执行一次fsync 。
这个行为可以通过设置 durability 参数为 async 来启用:

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

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

段合并

段合并机制(segment merge)

由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。每一个段都会消耗文件句柄、内存和CPU运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。
Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。
启动段合并在进行索引和搜索时会自动进行。这个流程如下图中提到的一样:

  1. 当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。
  2. 合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。 image.png 两个提交了的段和一个未提交的段正在被合并到一个更大的段
  3. 合并完成时的活动:
    • 新的段被刷新(flush)到了磁盘。写入一个包含新段且排除旧的和较小的段的新提交点。
    • 新的段被打开用来搜索。
    • 老的段被删除。 image.png 一旦合并结束,老的段被删除 合并大的段需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。Elasticsearch 在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好地执行。默认情况下,归并线程的限速配置indices.store.throttle.max_bytes_per_sec是20MB。对于写入量较大,磁盘转速较高,甚至使用SSD盘的服务器来说,这个限速是明显过低的。对于ELK Stack应用,建议可以适当调大到100MB或者更高。
PUT /_cluster/settings
{
  "persistent": {
    "indices.store.throttle.max_bytes_per_sec": "100mb"
  }
}

index.merge.scheduler.max_thread_count用于控制归并线程的数目,推荐设置为cpu核心数的一半。如果觉得自己磁盘性能跟不上,可以降低配置,免得IO情况瓶颈。如果非SSD建议设置为1。

归并策略(policy)

归并线程是按照一定的运行策略来挑选 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_merged_segment默认5GB,大于这个大小的segment,不用参与归并。optimize除外。

强制合并(optimize)

optimize API大可看做是强制合API。它会将一个分片强制合并到 max_num_segments 参数指定大小的段数目。这样做的意图是减少段的数量(通常减少到一个),来提升搜索性能。 在特定情况下,使用 optimize API 颇有益处。例如在日志这种用例下,每天、每周、每月的日志被存 储在一个索引中。老的索引实质上是只读的;它们也并不太可能会发生变化。在这种情况下,使用optimize 优化老的索引,将每一个分片合并为一个单独的段就很有用了;这样既可以节省资源,也可以使搜索更加快速:

POST /index/_optimize?max_num_segments=1