分片是 Elasticsearch 分布式存储的基石。在 Elasticsearch 中有 主分片(Primary Shard) 和 副本分片(Replica Shard) 两种。
Primary Shard
Elasticsearch 通过主分片,将数据分布在所有的节点上。主分片可以将一份索引的数据,分散在多个 Data Node 上,实现存储的水平扩展。
主分片(Primary Shard)数在索引创建的时候指定,后续默认不能修改,如果需要修改,需重建索引。
Replica Shard
副本分片(Replica Shard)主要用于提高数据的可用性。否则一旦主分片丢失,如果不设置副本分片,那么就会造成数据丢失。
另外一方面,副本分片还可以一定程度上提升系统的读取性能。Replica Shard 由 Primary Shard 同步,通过增加 Replica 的个数,可以提高读取的吞吐量。
如何规划一个索引的主分片数和副本分片数?
如果主分片数过小,例如创建了 1 个 Primary Shard 的 Index,如果该索引增长很快,那么集群无法通过增加节点实现对这个索引的数据扩展。
如果主分片数设置过大,导致单个 Shard 容量很小,引发一个节点上有过多分片,影响性能。
如果副本分片数设置过多,会降低集群整体的写入性能。
document 到 shard 的路由算法
shard = hash(_routing) % number_of_primary_shards
示例:PUT /posts/_doc/100?routing=bigdata
- Hash 算法确保文档均匀分散到分片中
- 默认的
_routing
值是文档 id - 可以自行指定 routing 数值,例如用相同国家的商品,都分配到指定的 shard
- 设置 Index Settings 后,Primary 数不能随意修改,就是因为上面的路由算法的原因
倒排索引
分片(Shard)是 Elasticsearch 中的最小工作单元,其实是一个 Lucene 的 Index。也就是:
An ES Shard = A Lucene Index
Lucene 中的 Index 是由倒排索引组成的,采用了 Immutable Design,一旦生成,不可更改。
这个不可变性有以下的几点好处:
- 无需考虑并发写文件的问题,避免了锁机制带来的性能问题
- 一旦读入内核的文件系统缓存,便留在那里。只要文件系统有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能
- 缓存容易生成和维护,系统可以充分利用缓存。倒排索引允许数据被压缩
但这个不可变性也同时带来了挑战,如果需要让一个新的 document 可以被搜索,需要重建整个索引才可以。
上面的示意图描述了一个 Lucene 中 Index 的样子。在 Lucene 中,单个倒排索引文件被称为 Segment。
Segment 是自包含的,不可变更的。多个 Segments 组合在一起,称为 Lucene 的 Index,对应 Elasticsearch 中的 Shard。
当有新文档写入时,会生成新 Segment,查询时通过上面说的 hash 算法确认目标 Shard 后,会同时查询这个 Shard 下所有 Segments,并且对结果进行汇总。Lucene 中有一个文件,是用来记录所有 Segments 信息,叫做 Commit Point。
如果一个 document 被删除了呢?这个信息会保存在上图中的 .del
文件中,而不是真的删除。
Refresh
document 的更新操作被提交后,会被写入一个叫做 Index Buffer 的地方缓存起来。之后默认每 1 秒执行一次 Refresh 操作,将 Index Buffer 中所有的内容写入到 Segments 中。注意这个写入过程不会执行 fsync 操作,也就是并不会落盘,仍然在内存中缓存着。
Refresh 的频率可以通过 index.refresh_interval
配置进行调整。Refresh 后,Segment 就有了,当下一次搜索请求进来后,已经可以从这个对应的 Segment 中获取到对应的数据了(虽然还没有落盘)。这也是为什么 Elasticsearch 被称为近实时搜索。
注意如果系统有大量的数据写入,那就会产生很多的 Segments。
Index Buffer 被占满的时候,就会主动触发 Refresh,默认这个阈值是 JVM 的 10%。
Transaction Log
为了保证写入的数据不会丢失,在 Index Buffer 被写入数据的同时,系统也会同时写 Transaction Log。
Transaction Log 是默认落盘的,每个分片(Shard)有一个 Transaction Log。不管哪个 document,只要对应了当前 Shard,都是顺序写入磁盘的,所以落盘的写入性能很高,不涉及随机 IO 操作。
在 Elasticsearch 的 Refresh 操作之后,Index Buffer 被清空,但是注意 Transaction Log 不会清空。因为 Index Buffer 清空写入 Segments,这个写入并没有 fsync,也就是说还没有落盘,Transaction Log 的使命还没有完成。
Flush
系统默认每 30min 调用一次 Flush 操作,每次 Flush 操作执行以下步骤:
- 调用 Refresh 操作,将 Index Buffer 清空并写入 Segments
- 调用 fsync,将缓存中的 Segments 写入磁盘
- 清空(删除)Transaction Log
如果 Transaction Log 满了(默认 512MB),那么也会主动触发 Flush 操作。
Flush 是重操作,性能消耗较高。
Merge
Segments 很多,需要被定期合并。所以 Merge 操作会减少 Segments 数量,并且删除已删除的 document。
Elasticsearch 和 Lucene 会自动进行 Merge 操作,也可以通过手工执行:
POST my_index/_forcemerge
强制执行 Merge 操作。一般在索引从 hot 迁移到 warm 的时候使用。
总结几个问题
为什么 ES 的搜索是近实时的(1s 后被搜到)?
根据 ES 的 document 到 shard 的路由算法,每个提交的 document 在不改变 Primary Shard 数量的情况下,目标 Shard 是不变的。这是前提。
之后因为 document 提交后会进入 Index Buffer,之后默认每 1s 进行一次 Refresh 操作写入到 Segments 中(没有执行 fsync 落盘),那么之后所有的查询请求就可以查询到这个写入的 document 数据了。而上面的路由算法会保证,同一个 document 的请求一定会落在当前的这台机器上,所以即使没有落盘,仍然可以查询到内存中的 Segment 的数据。所以 1s 后写入的数据就可以被搜到。
ES 如何保证在断电时数据也不会丢失?
Index Buffer 被写入的同时也会写入 Transaction Log,这是落盘的。
Transaction Log 是每个 Shard 一个的,所以同一个 Shard 中的所有 document 的 Transaction Log 都是顺序写入的,效率有保证。
当 ES 断电恢复后,会根据当前的 Transaction Log 中记录的数据恢复出来所有的数据。
为什么删除文档不会立刻释放空间?
因为 Lucene Index 是采用倒排索引的,是不可变的。如果删除一个 document,并不会直接删除对应的 Segment,而是添加到 Index 中名叫 .del
的文件中,标记为删除。在查询之后如果发现结果是被删除的,会自动被过滤掉。
所以删除了文档之后不会立刻释放空间。而是需要等上面说的 Merge 操作过了之后,才会真正的删除已删除的文档,并释放空间。