1. 使文档可被搜索
传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值(这里指单词)的能力。所以es使用倒排索引来创建数据和关键词之前的关系。
倒排索引被写入磁盘后是 不可改变 的:它永远不会修改。不能修改有他的好处:
(1).不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
(2).一旦索引被读入内核的文件系统缓存,由于其不变性。那么大部分读请求会直接命中内存,而不会从磁盘读取。这提供了很大的性能提升。
(3).其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
(4).写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
2. 动态更新索引
如果倒排索引文件是不可变得,那么如何对索引数据进行更新呢?es采用追加索引的方式,当修改或者删除一条数据时,会向倒排索引文件中追加一条记录,检索数据时,会从头开始查询所有结果,在根据检索出来的数据进行合并和过滤。按段搜索的过程如下:
(1) 新增document的过程
es是基于Lucene的,而在lucene中index被分为多个段(segment),每个段都是一个倒排索引文件。lucene中一个索引包括一个提交点和多个段,提交点包含这些段的所有信息。
a. 新文档被收集到内存索引缓存。
b.当内存索引缓存被占满时,则会进行提交。
一个新的段(新的倒排索引文件)被写入磁盘,一个新的提交点(包含这个新段)被写入磁盘,将所有内存索引缓存被刷新到磁盘,以确保他们被写入物理文件。
c.开启新的段,使它可以被搜索。
d.内存索引缓存被清空,等待接收新的文档。
(1) 更新和删除document的过程
段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个 .del 文件,文件中会列出这些被删除文档的段信息。
当一个文档被 “删除” 时,它实际上只是在 .del 文件中被 标记 删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。
文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
3. 近实时搜索
随着按段(per-segment)搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了。新文档在几分钟之内即可被检索,但这里还是不够快,因为提交一个新的段到磁盘需要一个 fsync
来确保段被物理性写入磁盘,这样在断电的时候就不会丢失数据,但是每次新增一个文档,都去fsync
是十分耗时的操作。
像之前描述的一样, 在内存索引缓冲区中的文档会被写入到一个新的段中,但是这里新段会被先写入到文件系统缓存,稍后再被刷新到磁盘—这一步代价比较高。不过只要文件已经在文件系统缓存中, 就可以像其它文件一样被打开和读取了。
Lucene 允许新段被写入和打开—使其包含的文档在未进行一次完整提交时便对搜索可见。意味着在当文档被写入内存索引缓存且没有刷新到磁盘,就对外可搜索了, 这种方式比进行一次提交代价要小得多,并且在不影响性能的前提下可以被频繁地执行。
4. refresh API
在 es中,写入和打开一个新段的轻量的过程叫做 refresh 。 默认情况下每个分片会每秒自动刷新一次。es是近实时搜索的原因: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。
如果你索引了一条数据,但是搜索时没有该记录可以通过以下命令进行手动刷新。
POST /_refresh
POST/index /_refresh
如果你正在使用 es索引大量的日志文件, 你可能想优化索引速度而不是近实时搜索, 可以通过设置 refresh_interval , 降低每个索引的刷新频率:
PUT /my_logs
{
"settings": {
"refresh_interval": "30s"
}
}
refresh_interval 可以在已存索引上进行动态更新。 在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来:
PUT /my_logs/_settings
{ "refresh_interval": -1 } //关闭自动刷新
PUT /my_logs/_settings
{ "refresh_interval": "30s" } //30s刷新一次
5. 持久化变更
如果没有用fsync
把数据从文件系统缓存刷(flush)到硬盘,我们无法确保当出现异常情况时,该数据是否存在。在之前我们说过,当一个document被写入到索引内存缓存时,该文档就能被检索,即使通过每秒refresh,在两次刷新过程中,数据也有可能丢失。
es确保数据的完整新,增加了一个事务日志(translog),每一次操作es都会进行日志记录。通过translog整个流程看起来如下:
(1)索引一个文档,该文档被添加到内存缓冲区,并向事务日志中增加日志。
(2)refresh,将内存缓冲区的内容的文档写入一个新的段中,但是没有fsync
操作,清空缓冲区,但是不清空日志文件。
(3)线程继续工作,更多的文档被添加到缓冲区和日志文件中。
(4)每隔一段时间—例如 translog 变得越来越大—索引被刷新(flush);一个新的 translog 被创建,并且一个全量提交被执行。
所有在内存缓冲区的文档都被写入一个新的段。
缓冲区被清空。
一个提交点被写入硬盘。
文件系统缓存通过 fsync 被刷新(flush)。
老的 translog 被删除。
6. 刷新api
这个执行一个提交并且将操作系统缓存中的段刷新到磁盘的行为在 Elasticsearch 被称作一次 flush 。 分片每30分钟被自动刷新(flush),或者在 translog 太大的时候也会刷新。可以通过以下命令手动刷新:更多设置请看 translog
文档
POST /index/_flush
POST /_flush?wait_for_ongoing
注意:在重启节点或关闭索引之前执行 flush
有益于你的索引。当 Elasticsearch 尝试恢复或重新打开一个索引, 它需要重放 translog 中所有的操作,所以如果日志越短,恢复越快
7.段合并
在每次刷新时,都会将内存缓冲区中的数据写入到一个新的段中,由于自动刷新的存在,所以会导致短时间内会出现多个段,而每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。
es通过在后台进行段合并来解决这个问题。
段合并的过程如下:
1.当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。
2.合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。
3.新的段被刷新(flush)到了磁盘。 es 写入一个包含新段且排除旧的和较小的段的新提交点。新的段被打开用来搜索。老的段被删除。
合并大的段需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行。
8.optimize API
optimize API大可看做是强制合并API。它会将一个分片强制合并到 max_num_segments 参数指定大小的段数目。 这样做的意图是减少段的数量(通常减少到一个),来提升搜索性能。使用命令如下
POST /index/_optimize?max_num_segments=1
特定情况下,使用 optimize API 颇有益处。例如在日志这种用例下,每天、每周、每月的日志被存储在一个索引中。 老的索引实质上是只读的;它们也并不太可能会发生变化。在这种情况下,使用optimize优化老的索引,将每一个分片合并为一个单独的段就很有用了;这样既可以节省资源,也可以使搜索更加快速:
注意:optimize API 触发段合并的操作不会受到任何资源上的限制。这可能会消耗掉你节点上全部的I/O资源, 使其没有其他资源来处理搜索请求,从而有可能使集群失去响应。如果你想要对索引执行 optimize
,你需要先使用分片分配(查看 迁移旧索引)把索引移到一个安全的节点,再执行。