shard是什么?它是如何工作的?这一章节我们将回答以下问题:
为什么search是准实时的?
为什么文档的CURD操作是实时的?
ES如何确保changes是持久话的的,即使断电也不会丢失?
为什么删除文档并不立刻释放存储空间?
refresh,flush,optimize api是做什么的?什么时候应该使用?
1:making text searchable
传统的database在一个field中存储单独的value(可能是多个word),这对全文索引显然是不够的。field中的每一个word都必须是searchable的,这意味着database需要在一个field中索引多个word。
满足multiple-values-per-field这种需求的数据结构是inverted index。它存储了一个在document中出现的term的有序列表,每一个term对应一个list,存储了含有这个term的document。
Term | Doc 1 | Doc 2 | Doc 3 | ... ------------------------------------ brown | X | | X | ... fox | X | X | X | ... quick | X | X | | ... the | X | | X | ...inverted index也会包含另外一些有效的信息。比如:term在多少个文档中出现过,term在某一个文档中出现的次数,每一个文档中term的顺序,文档的长度,所有文档的平均长度等等。es用这些统计信息来表征term的重要性,这些将在what is relevance章节讨论。
在全文索引的前期,整个文档集合用一个大的inverted index来构建并写入磁盘。只要新的index准备就绪,就取代旧的index,最近的变化数据就变成searchable了。
写入磁盘的inverted index是不可改变的(immutable)。这个特性有很多重要的优势:
不再需要lock操作,如果我们从来都不更新索引,也就没有必要担心并发的访问。
一旦index数据被加载到kernal的文件系统cache中,数据将放在哪里,因为从来不会被改变。只要文件系统cache有足够内存,大多数的读操作将会从内存读取,而非从磁盘,会大幅度提升性能。
在index的生命周期中,其他的cache(比如filter cache)保持有效。无需在数据改变的时候重建,因为数据不会改变。
写一个大的inverted index允许数据被压缩,减少昂贵的磁盘io和内存需要缓存的索引量。
当然,这种特性也有其缺陷。因为数据不可改变,所以如果想要新的文档变成searchable的,不得不重建整个索引。这就在index可以包含的数据量或者索引更新的频率方便有有了限制。
2:dynamically updateable indices
通过1中的论述,我们急需解决的问题是:在不改变immutable特性的前提下如何使得index变成updatable。答案是:use more than one index。
不用重建整个索引,增加一个补充索引来反映最近发生的改变。每一个inverted index可以轮流查询--从最老的索引开始,然后将结果合并。
Lucene中介绍了一个概念per-segment search。一个segment就是一个inverted index,但是现在“index”这个概念逐渐演化成一个segment集合+一个commit point(包含了所有已知的segment)。
一个lucene index在es中我们称为一个shard,而es中的index则是一系列shard。当es执行search操作,会将请求发送到这个index包含的所有shard上去,然后将没一个shard上的执行结果搜集起来作为最终的结果。
文档会是首先添加到内存的buffer中,新的记录先进入内存的buffer中,准备提交。
per-segment search按照以下方式工作:
新文档在内存的index buffer中聚集起来
时常,这个buffer提交:一个新的segment(作为补充性的inverted index),写入磁盘;一个新的commit point写入磁盘,包含了新产生的segment的名称;执行fsync,所有在文件系统cache中内容都写入磁盘确保真正的写入磁盘。
新产生的segment打开,它所包含的文档searchable
内存中的buffer清空,准备接收新的文档。
完毕。
当一个query请求执行的时候,会在所有已知的segment中去执行查询。Term statistics在所有segment中会聚合,以确保相关性的计算的准确性。按照以上的方式,新文档添加到索引中的代价比较低廉。
关于delete和update:
由于index是immutable的,所有文档不能从旧的segment中移除,同样也不能更新。Instead,没一个commit point都包括一个.del文件,这个文件包含了这个segment中所有被删除的文档。当某一个document被deleted的时候,仅仅是在.del中记录了这个文档。当然这个文档仍然可以被匹配到,只是在最终的返回结果中会被过滤掉。更新机制类似,一个比较老的version被记录在.del中,新的version会记录在新的segment中。当新老version都match的时候,老版本会从结果中排除掉。
在Segment merging这一章节中,会介绍是如何真正删除文档的。
3:near real-time search
2中介绍的per-segment机制中,在index和search之间的时间间隔已经得到了很大的改善,新的文档可以在分钟级别变为searchable,但是仍然不够快速。
瓶颈在于磁盘。提交一个新的segment到磁盘需要执行fsync来确保真正的写入磁盘,这样即使断电数据也不会丢失。但是fsync代价也是大的。需要一个更为轻量级的操作来是得新的文档可以searchable。
在es和磁盘之间是文件系统的cache。之前介绍的内存中的index buffer将会写入磁盘形成一个新的segment。但是新的segment会首先写到文件系统的cache中,这个过程是轻量级的,然后会写入磁盘,这个过程是重量级的。但是当一个文件已经存在与cache中,就可以被打开并且searchable。
lucene允许新的segment被打开并且searchable,而无需执行一个full commit操作。这个过程变的非常轻量级而且可以较为频繁的执行。
在es中,这个轻量级的操作成为refresh,写入并且打开一个segment。默认情况下,每一个shard每隔1s会执行一次refresh操作。这也就是我们说es是near real-time search的原因:doc会在1s之内变成searchable的。
注意:refresh虽然比commit要轻,但仍然有一定的性能损耗,在test过程中可以手动执行refresh,但是在生产环境中,不能每索引一个doc就执行一次refresh,会损耗性能。
并不是所有的应用场景都需要每秒都执行refresh操作。也许你用es对大量的log file进行索引,这样你会更倾向于提升索引的速度而不是变得near real-time search,所以,你可以增大refresh的间隔:
PUT /my_logs
{
"settings": {
"refresh_interval": "30s"
}
}
这个设置可以动态调整,甚至可以设置为-1,禁止掉,在完成索引之后重新设置为1s即可。
4:making changes persistent
如果没有fsync操作把数据从文件系统cacheflush到disk中,无法保证在断电之后数据不丢失,哪怕是进程正常退出。es需要确保所有的更改都要持久化到磁盘中。
在3中介绍了full commit会把segment数据flush到磁盘并且生成一个commit point包含了所有可见的segment。es在启动或者reopen index的过程中来确定哪些segment属于这个shard。es会每秒执行一次refresh来确保near real-time search,同样也需要定期执行full commit来确保可以灾难恢复。但是在full commit间隔之间如果发生了数据的变化,而且好这个时间间隔又出现了意外,怎么办呢?我们不想丢失这部分的修改。
es用translog来解决这个问题,translog记录了每一次操作。携带translog,执行流程是这样的:
索引一个文档的时候,加入in-memory buffer,并且写入translog;每一秒执行一次refresh操作,in-memory buffer中的doc写成新的segment,并不执行fsync,segment open并且searchable,in-memory buffer清空,但是translog并不清空;持续的新数据添加到in-memory buffer中,并且写入translog;当translog逐渐变大到一个阈值,执行flush操作,产生一个新的translog,并且执行了full commit(in-memory buffer中的docs写入一个新的segment,buffer清空,commit point写入disk,文件系统cache执行fsync,translog删除,生成新的translog)。
对于没有flush到磁盘的所有操作,translog提供了一种持久化的存储方式。当启动的时候,系统会利用最后一个commit point来执行recovery,并且使用translog来回放在最后一个commit之后的操作。
translog同样需要支持实时的real-time CURD操作。当通过id执行retrieve,update或者delete一个doc,首先会在translog中查看最近的改变数据,然后才会在相关的segment中检索。
es提供了flush api去执行flush操作,但是你几乎不用手动执行flush操作,默认的设置足够了。
在你关闭一个node时候执行一次flush操作会从中受益。当es执行recovery的时候,会从translog中回放操作,因此translog越小,recovery的时间就会越少。
下面问题来了:translog安全么?
translog的目的就是确保操作不会丢失。但是translog也是文件,也会有同样的关于fsync的问题。默认情况下,translog每5s执行一次flush。因此,如果translog是唯一的机制的话,我们可能会丢失5s的数据。幸运的是,translog只是es这个庞大系统的一部分。请注意:一个index操作只有在primary shard和replica shard上都执行成功才算最终的成功。即使primary shard遇到灾难性的损坏,不大可能影响到replica所在节点。虽然我们可以让translog执行fsync更加频繁(牺牲了性能),但是这样做也不大可能提供更好的可靠性。
5:segment merging
自动的refresh进程每一秒钟就会产生一个segment,因此过不了多长时间segment的数量就会膨胀。太多segment也是一个严重的问题。每一个segment都要消耗file handle,内存和cpu周期。更重要的是,每一个search请求都会去询问每一个segment。因此segment越多,search速度越慢。
es通过在后台merge的方式来解决这个问题,小的segment合并成大的segment,大的segment合并成更大的segment。
你并不需要启用merge操作,这个过程在你index和search的时候是自动发生的。就像这样工作:
当index过程中,refresh产生新的segment并open它们使得searchable;merge进程会在后台选择小的segments然后合并成较大的segment,这个过程并不会打断index和search。当merge结束,老的segment就会删除,就像这样工作:
merge生成的新的segment写入磁盘;生成新的commit point,包含新的segment同时排除旧的segment;新的segment open for search;老的segment被删除。
对较大的segment的merge操作会占用大量的cpu和io,会影响到search的性能。默认情况下,es对merge进程做了限制,以保证search进程会有足够的资源来顺利执行。
optimize api提供了强制执行merge的接口。它会让一个shard合并到max_num_segments个segment。这样做的原因就是要减少segment的数目(通常减少到1个)来提升search的性能。注意:optimize api不应该在动态索引(索引还在更新中)中使用,后台的merge进程默认情况下工作状况是非常良好的,而optimize api会妨碍这个进程的执行,所以不要干涉!
在一些特定的应用场景下,optimize api是益处良多的。一个典型的应用场景是就是logging,log数据按天,按周,按月建立索引。老的索引是read-only的,它们几乎不会被改变.这种情况下,老索引可以调用optimize api来合并成一个segment,这样它们会使用较少的资源,search性能也会提升。
POST /logstash-2014-10/_optimize?max_num_segments=1
注意:用optimize api引发的merge是没有做任何限制的。会消耗掉所有的io资源,不会给search留下资源,因此集群会变成unresponsive。如果机会对一个index进行optimize,应该使用shard allocation,首先将index移动到一个safe的node上执行。