分片是 Elasticsearch 最小的工作单元。一个分片其实就是一个lucene索引,众多的分片组合在一起是一个完整的elasticsearch索引。

一、倒排索引

传统数据库的索引方式并不适用于大数据量的全文检索,且数据库的索引随着数据量增加,仍然存在索引效率变低的问题。

在第一章节中我基于lucene的角度分析了倒排索引的原理,关于具体内容可参考https://www.jianshu.com/p/733fc580e696。Elasticsearch 使用一种称为倒排索引的结构,它适用于快速的全文搜索。

当存储数据时,将数据中的关键词拆分并提取建立索引(就像词典的目录一样);
当查询的时候,会根据查询的内容在目录中寻找,直到找到查询内容所在的文档。

优点:
1)准确率高
2)不会因为数据量的增加导致查询速度大幅下降。(无论是中文还是英文词典而言,作为目录的数量是固定的,基本没有变化,即使数据量增大了,目录还是那么多)

缺点:
索引文件占用额外的硬盘空间。

使用场景:
适合海量数据的查询。

二、动态更新索引

早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换。

倒排索引被写入磁盘后是 不可改变 的:它永远不会修改。

不变性具有哪些价值?
1)不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
2)性能提升。一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。
3)写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。

不变性的缺点?
如果你需要让一个新的文档 可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。

如何在保留不变性的前提下实现倒排索引的更新?
答案是: 用更多的索引。通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。

Elasticsearch 基于 Lucene, 这个 java 库引入了 按段(segment)搜索的概念。 每一段本身都是一个倒排索引, 但索引在Lucene中除表示所有段的集合外, 还增加了提交点的概念。

如下所示有当前有三个段,有一个提交点指向这三个段:

es倒序关键字 es的倒排索引原理_es倒序关键字

模拟一个新文档的创建过程:

  1. 新文档被收集到内存索引缓存

es倒序关键字 es的倒排索引原理_数据库_02

  1. 不时地, 缓存被提交
    (1) 生成一个新的段,追加的倒排索引被写入磁盘。
    (2) 一个新的包含新段名字的提交点被写入磁盘。
    (3) 磁盘进行同步,所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件。
  2. 新的段被开启,让它包含的文档可见以被搜索

es倒序关键字 es的倒排索引原理_数据库_03

  1. 内存缓存被清空,等待接收新的文档

es倒序关键字 es的倒排索引原理_java_04

当一个查询触发时,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。 这种方式可以用相对较低的成本将新文档添加到索引。

段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个 .del 文件,文件中会列出这些被删除文档的段信息。

当一个文档被 “删除” 时,它实际上只是在 .del 文件中被 标记 删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。

文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。

三、近实时搜索

随着按段(per-segment)搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了。新文档在几分钟之内即可被检索,但这样还是不够快。磁盘在这里成为了瓶颈。提交(Commiting)一个新的段到磁盘需要一个fsync来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 但是 fsync 操作代价很大; 如果每次索引一个文档都去执行一次的话会造成很大的性能问题

我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着 fsync 要从整个过程中被移除。在 Elasticsearch 和磁盘之间是文件系统缓存。 像之前描述的一样, 在内存索引缓冲区中的文档会被写入到一个新的段中。 但是这里新段会被先写入到文件系统缓存这一步代价会比较低,稍后再被刷新到磁盘这一步代价比较高。不过只要文件已经在缓存中,就可以像其它文件一样被打开和读取了。

Lucene允许新段被写入和打开(内存缓冲区中包含了新文档的lucene索引),使其包含的文档在未进行一次完整提交时便对搜索可见。这种方式比进行一次提交代价要小得多,并且在不影响性能的前提下可以被频繁地执行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BLGWXt30-1618209268482)(https://upload-images.jianshu.io/upload_images/16830368-58c0fb8aaf0fc7ce.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/400)]

在Elasticsearch中,写入和打开一个新段的轻量的过程叫做refresh 。默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说Elasticsearch是近实时搜索: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。

这些行为可能会对新用户造成困惑: 他们索引了一个文档然后尝试搜索它,但却没有搜到。这个问题的解决办法是用 refresh API 执行一次手动刷新: /users/_refresh

注意:尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候, 手动刷新很有用,但是不要在生产环境下每次索引一个文档都去手动刷新。 相反,你的应用需要意识到 Elasticsearch 的近实时的性质,并接受它的不足 。

可以通过配置设置每个索引的刷新时间:

# 关闭自动刷新
PUT /users/_settings
{ "refresh_interval": -1 }
# 每一秒刷新
PUT /users/_settings
{ "refresh_interval": "1s" }

四、持久化

如果没有用 fsync 把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。为了保证 Elasticsearch 的可靠性,需要确保数据变化被持久化到磁盘。在动态更新索引,我们说一次完整的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点。Elasticsearch 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。

但在两次提交之间发生变化的文档怎么办?我们也不希望丢失掉这些数据。Elasticsearch增加了一个translog ,或者叫事务日志,在每一次对Elasticsearch进行操作时均进行了日志记录。、

此时新建索引的过程:

  1. 一个文档被索引之后,就会被添加到内存缓冲区,并且追加到了translog。

es倒序关键字 es的倒排索引原理_es倒序关键字_05

  1. 刷新(refresh)使分片每秒被刷新(refresh)一次:
    1)这些在内存缓冲区的文档被写入到一个新的段中,且没有进行fsync操作。
    2)这个段被打开,使其可被搜索。
    3)内存缓冲区被清空。

es倒序关键字 es的倒排索引原理_分布式_06

  1. 这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志。

es倒序关键字 es的倒排索引原理_数据库_07

  1. 每隔一段时间,translog变得越来越大,索引被刷新(flush);一个新的translog被创建,并且全量提交被执行。
    1)所有在内存缓冲区的文档都被写入一个新的段。
    2)缓冲区被清空。
    3)一个提交点被写入硬盘。
    4)文件系统缓存通过 fsync 被刷新(flush)。
    5)老的translog被删除。

es倒序关键字 es的倒排索引原理_分布式_08

translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。

translog 也被用来提供实时 CRUD 。当你试着通过 ID 查询、更新、删除一个文档,它会在尝试从相应的段中检索之前, 首先检查 translog 任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。

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

translog 的目的是保证操作不会丢失,在文件被 fsync 到磁盘前,被写入的文件在重启之后就会丢失。默认 translog 是每 5 秒被 fsync 刷新到硬盘, 或者在每次写请求完成之后执行(e.g. index, delete, update, bulk)。这个过程在主分片和复制分片都会发生。

但是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync还是比较有益的。你需要 保证 在发生 crash 时,丢失掉 sync_interval 时间段的数据也无所谓。如果你不确定这个行为的后果,最好是使用默认的参数( “index.translog.durability”: “request” )来避免数据丢失。

五、段合并

由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和 cpu 运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。

Elasticsearch 通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。

段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。

启动段合并不需要你做任何事。进行索引和搜索时会自动进行。

  1. 当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。
  2. 合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。

es倒序关键字 es的倒排索引原理_es倒序关键字_09

  1. 一旦合并结束,老的段被删除
    1)新的段被刷新(flush)到了磁盘。 ** 写入一个包含新段且排除旧的和较小的段的新提交点。
    2)新的段被打开用来搜索。
    3)老的段被删除。

合并大的段需要消耗大量的 I/O 和 CPU 资源,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行。