目录
1.简介
分片到底是什么,它怎样工作?
- 为什么搜索是近实时的?
- 为什么文档的CRUD操作是实时的?
- ES怎样保证更新持久化,即使断电也不会丢失?
- 为什么删除文档不会立即释放空间?
- 什么是 refresh,flush, optimize API ,以及什么时候你该使用它们?
1.1.使文本可以被搜索
让文本中的每个单词都可以被搜索,数据库需要存多个值。
支持一个字段多个值的最佳数据结构是倒排索引。倒排索引包含了出现在所有文档中唯一的值或词的有序列表,以及每个词所属的文档列表。
注意
当讨论倒排索引时,是指把文档加入索引。因为之前,一个倒排索引是用来索引整个非结构化的文本文档。
ES中的文档是一个结构化的JSON文档。实际上,每一个JSON文档中被索引的字段都有它自己的倒排索引
为了实现倒排索引预期的功能,它必须要知道集合中所有的文档。
在全文检索的早些时候,会为整个文档集合建立一个大索引,并且写入磁盘。只有新的索引准备好了,它就会替代旧的索引,最近的修改才可以被检索。
不可变性
写入磁盘的倒排索引是不可变的,它有如下好处:
- 不需要锁。如果从来不需要更新一个索引,就不必担心多个程序同时尝试修改。
- 一旦索引被读入文件系统的缓存,它就不会改变。只要文件系统缓存有足够的空间,大部分的读会直接访问内存而不是磁盘。有助于性能提升。
- 在索引的声明周期内,所有的其他缓存都可用。它们不需要在每次数据变化了都重建,因为数据不会变。
- 写入单个大的倒排索引,可以压缩数据,较少磁盘IO和需要缓存索引的内存大小。
【缺点】
如果想要搜索一个新文档,必须重见整个索引。
这不仅严重限制了一个索引所能装下的数据,还有一个索引可以被更新的频次。
1.2.动态索引
如何在保持不可变好处的同时更新倒排索引?
答:使用多个索引
不是重写整个倒排索引,而是增加额外的索引反映最近的变化。每个倒排索引都可以按顺序查询,从最老的开始,最后把结果聚合。
Elasticsearch底层依赖的Lucene,引入了 per-segment search 的概念。
一个段(segment)是有完整功能的倒排索引,但Lucene中的索引指的是段的集合,再加上提交点(commit point,包括所有段的文件)
新的文档,在被写入磁盘的段之前,首先写入内存区的索引缓存
索引vs分片
Lucene索引是Elasticsearch中的分片,Elasticsearch中的索引是分片的集合。
当Elasticsearch搜索索引时,发送查询请求给该索引下的所有分片,然后过滤这些结果,聚合成全局的结果。
【 per-segment search 工作】
1. 新的文档首先写入内存区的索引缓存。
2. 这些buffer被提交:
一个新的段——额外的倒排索引——写入磁盘。
新的提交点写入磁盘,包括新段的名称。
磁盘是fsync’ed(文件同步)——所有写操作等待文件系统缓存同步到磁盘,确保它们可以被物理写入。
3. 新段被打开,它包含的文档可以被检索
4. 内存的缓存被清除,等待接受新的文档
当一个请求被接受,所有段依次查询。所有段上的Term统计信息被聚合,确保每个term和文档的相关性被正确计算。通过这种方式,新的文档以较小的代价加入索引。
删除和更新
段是不可变的,所以文档既不能从旧的段中移除,旧的段也不能更新以反映文档最新的版本。
相反,每一个提交点包括一个.del文件,包含了段上已经被删除的文档。
当一个文档被删除,它实际上只是在.del文件中被标记为删除,依然可以匹配查询,但是最终返回之前会被从结果中删除。
文档的更新操作是类似的:当一个文档被更新,旧版本的文档被标记为删除,新版本的文档在新的段中索引。也许该文档的不同版本都会匹配一个查询,但是更老版本会从结果中删除。
1.3.近实时搜索
因为 per-segment search 机制,索引和搜索一个文档之间是有延迟的。
新的文档会在几分钟内可以搜索,但是这依然不够快。
磁盘是瓶颈。
提交一个新的段到磁盘需要 fsync 操作,确保段被物理地写入磁盘,即时电源失效也不会丢失数据。
但是 fsync 是昂贵的,不能在每个文档被索引的时就触发。
所以需要一种更轻量级的方式使新的文档可以被搜索,这意味这移除 fsync 。
位于Elasticsearch和磁盘间的是文件系统缓存。
Lucene允许新段写入打开,好让它们包括的文档可搜索,而不用执行一次全量提交。这是比提交更轻量的过程,可以经常操作,而不会影响性能。
refeash API
在Elesticsearch中,这种写入打开一个新段的轻量级过程,叫做refresh。
默认情况下,每个分片每秒自动刷新一次。
这就是为什么说Elasticsearch是近实时的搜索了:文档的改动不会立即被搜索,但是会在一秒内可见。
【手动刷新】
POST /_refresh // 刷新所有索引
POST /blogs/_refresh // 只refresh 索引 blogs
虽然刷新比提交更轻量,但是它依然有消耗。
人工刷新在测试写的时有用,但是不要在生产环境中每写一次就执行刷新,这会影响性能。
相反,你的应用需要意识到ES近实时搜索的本质,并且容忍它。
不是所有的用户都需要每秒刷新一次。也许你使用ES索引百万日志文件,你更想要优化索引的速度,而不是进实时搜索。你可以通过修改配置项 refresh_interval 减少刷新的频率:
PUT /my_logs
{
"settings": {
"refresh_interval": "30s" // 每30s refresh一次 my_logs;在存在的索引上动态更新
}
}
【关闭自动刷新】
PUT /my_logs/_settings
{ "refresh_interval": -1 }
1.4.持久化变更
没用 fsync 同步文件系统缓存到磁盘,不能确保电源失效,甚至正常退出应用后,数据的安全。为了ES的可靠性,需要确保变更持久化到磁盘。
一次全提交同步段到磁盘,写提交点,这会列出所有的已知的段。在重启,或重新打开索引时,ES使用这次提交点决定哪些段属于当前的分片。
当通过每秒的刷新获得近实时的搜索,依然需要定时地执行全提交确保能从失败中恢复。但是提交之间的文档怎么办?也不想丢失它们。
ES增加了事务日志( translog ),来记录每次操作。有了事务日志,过程现在如下:
1. 当一个文档被索引,它被加入到内存缓存,同时加到事务日志。
2. refresh使得分片的进入如下图描述的状态。每秒分片都进行refeash:
- 内存缓冲区的文档写入到段中,但没有fsync。
- 段被打开,使得新的文档可以搜索。
- 缓存被清除
3. 随着更多的文档加入到缓存区,写入日志,这个过程会继续
4. 不时地,比如日志很大了,新的日志会创建,会进行一次全提交:
- 内存缓存区的所有文档会写入到新段中。
- 清除缓存
- 一个提交点写入硬盘
- 文件系统缓存通过fsync操作flush到硬盘
- 事务日志被清除
事务日志记录了没有flush到硬盘的所有操作。当故障重启后,ES会用最近一次提交点从硬盘恢复所有已知的段,并且从日志里恢复所有的操作。
事务日志还用来提供实时的CRUD操作。当尝试用ID进行CRUD时,它在检索相关段内的文档前会首先检查日志最新的改动。这意味着ES可以实时地获取文档的最新版本。
flush API
在ES中,进行一次提交并删除事务日志的操作叫做 flush 。
分片每30分钟,或事务日志过大会进行一次flush操作。
【手动flush】(很少)
POST /blogs/_flush // flush索引 blogs
POST /_flush?wait_for_ongoing // flush所有索引,等待操作结束再返回
当要重启或关闭一个索引,flush该索引是很有用的。
当ES尝试恢复或者重新打开一个索引时,它必须重放所有事务日志中的操作,所以日志越小,恢复速度越快。
1.5.合并段
【问题】
通过每秒自动刷新创建新的段,用不了多久段的数量就爆炸了。有太多的段是一个问题。每个段消费文件句柄,内存,cpu资源。更重要的是,每次搜索请求都需要依次检查每个段。段越多,查询越慢。
【解决】
小段被合并成大段,再合并成更大的段。
这是旧的文档从文件系统删除的时候。旧的段不会再复制到更大的新段中。
这个过程你不必做什么。当你在索引和搜索时ES会自动处理。
这个过程如图:两个提交的段和一个未提交的段合并为了一个更大的段所示:
1. 索引过程中,refresh会创建新的段,并打开它。
2. 合并过程会在后台选择一些小的段合并成大的段,这个过程不会中断索引和搜索。
3. 下图描述了合并后的操作:
- 新的段flush到了硬盘。
- 新的提交点写入新的段,排除旧的段。
- 新的段打开供搜索。
- 旧的段被删除。
合并大的段会消耗很多IO和CPU,如果不检查会影响到搜素性能。默认情况下,ES会限制合并过程,这样搜索就可以有足够的资源进行。
optimize API
optimize API 最好描述为强制合并段API。
它强制分片合并段以达到指定 max_num_segments 参数。这是为了减少段的数量(通常为1)达到提高搜索性能的目的。
警告
不要在动态的索引(正在活跃更新)上使用 optimize API。后台的合并处理已经做的很好了,优化命令会阻碍它的工作。不要干涉!
在特定的环境下,optimize API 是有用的。
典型的场景是记录日志,这种情况下日志是按照每天,周,月存入索引。旧的索引一般是只可读的,是不可能修改的。
这种情况下,把每个索引的段降至1是有效的。搜索过程就会用到更少的资源,性能更好
【举例】
POST /logstash-2014-10/_optimize?max_num_segments=1 // 把索引中的每个分片都合并成一个段