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