Elasticsearch-03-浅谈分片
3.1 分片
3.1.1 文档路由到分片
ES为了实现分布式的搜索,会将索引划分为数个主分片和副本。那么,当我们查询一个文档时,ES怎么知道这个文档存储在哪个分片上呢?当我们存储一个分片时,ES又该如何安排它存储在那个分片上呢?
实际上,这个过程是根据这个公式来决定的:
shard = hash(routing) % number_of_primary_shards
routing
是一个变量,可以由用户给定,也可以由系统决定,它默认是文档的id。routing
通过哈希函数经过计算之后,再用除以主分片数得到的余数来确定该文档所属的分片位置
这也就解释了为什么要在创建索引的时候为什么就要明确主分片的数量且不能再改变。因为如果主分片数量改变之后,就有可能导致数据丢失。
3.1.2 分片数量
既然主分片数量在确定之后,那么是不是应该在创建的时候设置越多的主分片数越好?
显然不是!这里我们要先建立一个概念:每个分片都是一个完整的Lucene实例
这意味着创建和使用分片是有代价的,每个分片都或多或少的占用系统资源。同时,每次搜索请求都会要求所有的分片返回搜索结果,如果这些分片都集中在同一台服务器上,这可能引起资源竞争从而使得无法正常响应。
分片的数量和大小控制的一些经验之谈
- 可以手动合并较小的分片来减轻系统负担。通常情况下,分片合理的大小在几GB~几十GB之间,但最大不要超过50GB
- ES节点所持有的分片数应当和其堆空间大小成正比,这个比例一般要低于20~25个分片每GB
- 虽然ES可以并行的在多个分片内执行查询操作,但查询大量小分片不一定比查询几个大分片快
3.1.3 快速搜索
分片能进行搜索的原理是使用了倒排索引,那么它是怎么实现快速搜索的呢
1)倒排索引的不可变性
倒排索引在被写入磁盘后,是永远不可变的,也就是它永远不能被修改。这种特性具有如下价值:
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
- 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和需要被缓存到内存的索引的使用量。
但这种不变性也会带来一些麻烦:如果你需要让一个新的文档 可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
2)并行搜索
并行搜索指的是ES会在主分片及其所有副本分片上进行并行搜索,以加快搜索速度。这里不再赘述
3.1.4 分片的细节
既然倒排索引都是不可变的,那么当有更多的文档被创建之后该怎么办呢?难道说每创建一个文档都要重新创建一次倒排索引吗?
答案是:创建更多的倒排索引,用额外的倒排索引来记录新增加的文档记录。在搜索的时候,每个倒排索引都会根据时间顺序来进行检索,最后再将结果合并。
ES是基于Lucene来实现搜索功能,Lucene中提出了按段搜索的概念,每一段其实就是一个倒排索引。多个段和提交点就构成了Lucene中可进行搜索的索引,也就是ES中的分片。
1)索引的更新
-
新文档被收集到内存索引缓存中包含新文档的 Lucene 索引
-
不时地, 缓存被 提交 :
- 一个新的段,一个追加的倒排索引—被写入磁盘。
- 一个新的包含新段名字的 提交点 被写入磁盘。
- 磁盘进行 同步,所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件。
-
新的段被开启,让它包含的文档可见以被搜索。由于物理写入磁盘开销大,所以在实际运行的过程中,只要数据在缓存中,就可以被外部所读取访问到了。因为Lucene 允许新段被写入和打开—使其包含的文档在未进行一次完整提交时便对搜索可见。 这种方式比进行一次提交代价要小得多,并且在不影响性能的前提下可以被频繁地执行。
-
内存缓存被清空,等待接收新的文档。
-
当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。 这种方式可以用相对较低的成本将新文档添加到索引。
2)持久化问题
由于这些数据在文件系统的缓存中,如果没有用 fsync
把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。为了保证 Elasticsearch 的可靠性,需要确保数据变化被持久化到磁盘。
Elasticsearch为了保证数据的完整性,增加了一个叫 translog
(事务日志)的东西来协助保存数据。所以总的工作流程如下
-
一个文档被索引之后,就会被添加到内存缓冲区,同时追加到了 translog
-
刷新(refresh)使分片处于提交的状态,分片每秒被刷新(refresh)一次:
- 这些在内存缓冲区的文档被写入到一个新的段中,且没有进行
fsync
操作。 - 这个段被打开,使其可被搜索。
- 内存缓冲区被清空。
- 这些在内存缓冲区的文档被写入到一个新的段中,且没有进行
-
这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志
-
每隔一段时间,translog 变得越来越大,索引被刷新(flush);一个新的 translog 被创建,并且一个全量提交被执行:
- 所有在内存缓冲区的文档都被写入一个新的段。
- 缓冲区被清空。
- 一个提交点被写入硬盘。
- 文件系统缓存通过
fsync
被刷新(flush)。 - 老的 translog 被删除。
translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。
translog 也被用来提供实时 CRUD 。当你试着通过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索之前, 首先检查 translog 任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。
translog的安全性
在文件被
fsync
到磁盘前,被写入的文件在重启之后就会丢失。默认 translog 是每 5 秒被fsync
刷新到硬盘, 或者在每次写请求完成之后执行(e.g. index, delete, update, bulk)。这个过程在主分片和复制分片都会发生。最终, 基本上,这意味着在整个请求被fsync
到主分片和复制分片的translog之前,你的客户端不会得到一个 200 OK 响应。在每次请求后都执行一个 fsync 会带来一些性能损失,尽管实践表明这种损失相对较小(特别是bulk导入,它在一次请求中平摊了大量文档的开销)。
但是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync 还是比较有益的。比如,写入的数据被缓存到内存中,再每5秒执行一次
fsync
。这个行为可以通过设置
durability
参数为async
来启用:PUT /my_index/_settings { "index.translog.durability": "async", "index.translog.sync_interval": "5s" }
这个选项可以针对索引单独设置,并且可以动态进行修改。如果你决定使用异步 translog 的话,你需要 保证 在发生crash时,丢失掉
sync_interval
时间段的数据也无所谓。请在决定前知晓这个特性。如果你不确定这个行为的后果,最好是使用默认的参数(
"index.translog.durability": "request"
)来避免数据丢失。
3)段合并
由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。
Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。在合并的时候,Elasticsearch会将哪些被标记为删除的文档从文件系统中删除,更新前的旧文档也不会被拷贝到新的段中
段合并是在索引和搜索时自动进行的操作,这意味着小段的合并可能不会对ES运行带来较大的影响,但大段的合并需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。所以ES在默认情况下会限制段合并流程的资源,用于保证搜索服务不受太大影响。
optimize
,手动段合并APIPOST /<index>/_optimize?max_num_segments=1
- 注意:通过
optimize
进行段合并时,ES不会对其资源进行限制,这可能会消耗掉你节点上全部的I/O资源, 使其没有余裕来处理搜索请求,从而有可能使集群失去响应。- 注意:
optimize
API 不应该 被用在一个活跃的索引,一个正积极更新的索引。后台合并流程已经可以很好地完成工作。