一. 什么是分片
分片(shards)是使用多个机器存储数据的方法,MongoDB使用分片以支持巨大的数据存储量和对数据的操作。它将一个很大的集合分割成多个片(shard),每个分片存储于不同的机器或副本集中,每个分片都是一个独立的数据库。
分片为提高系统吞吐量和大数据的存储提供了方法:
- 使用分片减少了每个分片需要处理的请求数
- 使用分片减少了每个分片存储的数据量
二. 分片的基础设施
1. 片键(shard key)
MongoDB通过片键将一个集合分为多个部分,它决定了一个集合的文档在不同的分片上的分布。它有如下特性:
- 片键字段可以是单个字段或者是复合字段
- 片键字段必须被索引(或是索引的前缀)
- 片键字段不能是多键索引
- 集合中的每个文档都必须包含片键字段
- 文档的片键值不可被修改,只能删除文档,并重新插入新片键文档
- 不同文档的片键可以相同
MongoDB的片键有两种:
1.1 基于范围的片键
如有Person集合,其片键age,有两个分片,那么分片1可能负责存储-∞<=age<20
的文档,分片2负责存取20<=age<∞
的文档,这里的∞代表MongoDB所有值的最大值,而不仅限于数字。这是最简单的基于范围的片键模型,我们需要手动指定某个范围的片键位于某个分片上,MongoDB的分片机制要比这个要灵活强大得多。
1.2 基于哈希的片键:
同样,我们可以基于片键哈希值来进行分片之间的分发,集合片键需要有足够大的基数。
在写入的时候,MongoDB(mongos)会将请求分发到负责该片键的分片上,在查询的时候,如果查询涉及了片键,则和写入一样,MongoDB会将请求分发到对应的片键上(针对性查询),否则,MongoDB必须将请求发送到所有的分片上,以获取结果。
更多参考:http://docs.mongoing.com/manual-zh/core/sharding-shard-key.html
2. 块分裂(chunk split)
随着文档的不断写入,各个分片的集合大小会拉开差距,单个分片的集合大小仍然可能是个问题。MongoDB采用单分片多区间的方式来将片键映射到分片,某个区间片键内的文档被称作”块”(chunk),当某个块超过设置的块大小(sharding-chunk-size,默认为64M,会动态调整)时,MongoDB会负责将这个数据块分割为两个,分裂会改变元信息,但效率很高,对集群的性能也没有影响。
3. 块迁移 (chunk migration)
随着块分裂持续下去,会导致不同分片之间的块数量和压力的不均衡,这个时候,MongoDB会开始一次分片间的数据块迁移,平衡各个分片的块数量,并调整各个分片的片键区间。MongoDB允许管理员控制块阀值大小,并且可以通过标记来直接决定集群分布。
块分裂,块迁移示例图:
分片 | 片键 | 块 |
---|---|---|
分片1 | age | (-∞, 20) |
分片2 | age | [20, ∞) |
分片2的文档持续增长,导致块分裂:
分片 | 片键 | 块 |
---|---|---|
分片1 | age | (-∞, 20) |
分片2 | age | [20, 30), [30, 50), [50, ∞) |
当各个分片之间的块数量差距过大时,导致块迁移:
分片 | 片键 | 块 |
---|---|---|
分片1 | age | (-∞, 20), [20, 30) |
分片2 | age | [30, 50), [50, ∞) |
分块过程是由平衡器(balancer)来控制的,整个分裂和迁移过程都是自动的,块的分裂阀值可设置,如果你想要人为控制块分布,只能关闭平衡器,或者重新选择片键。
三. 如何选择片键
1. 范围片键 vs 哈希片键
基于范围的分片方式提供了更高效的范围查询,给定一个片键的范围,分发路由可以很简单地确定哪个数据块存储了请求需要的数据,并将请求转发到相应的分片中。
不过,基于范围的分片会导致数据在不同分片上的不均衡,有时候,带来的消极作用会大于查询性能的积极作用。比如,如果片键所在的字段是线性增长的,一定时间内的所有请求都会落到某个固定的数据块中,最终导致分布在同一个分片中。在这种情况下,一小部分分片承载了集群大部分的数据,系统并不能很好地进行扩展。
与此相比,基于哈希的分片方式以范围查询性能的损失为代价,保证了集群中数据的均衡。哈希值的随机性使数据随机分布在每个数据块中,因此也随机分布在不同分片中。但是也正由于随机性,一个范围查询很难确定应该请求哪些分片,通常为了返回需要的结果,需要请求所有分片。
2. 哈希片键的选择
哈希片键的选择是比较简单的,片键只需要满足基数够大,通常ObjectId,自增ID,时间戳,都是不错的选择。
3. 范围片键选择
范围片键的选择是比较复杂的,但目标是一致的:读写分离和数据局部性,举几个例子:
a. 小基数片键:
小基数片键随着数据增长和块分裂,单个块的片键范围越来越小,最终可能会形成单个块对应单个片键,此时无法再进行块分裂,从而导致单个块过大,吞吐量也会受到影响。
小基数片键满足数据局部性,并且基于片键的查询效率也很高,但MongoDB不能对数据块进行有效地分割,导致读写不能分离。
解决方案:如果是要基于该小基数片键进行大量的查询,可以选择组合片键,确保第二个字段有足够大的基数。
b. 升序片键:
我们可能会选择ObjectId这类自增Key作为片键,这类Key的问题是可能会造成单一且不可分散的性能单点,因为新数据总是写入最新的数据块,没有做到写分离。
c. 随机片键
随机片键的原理有点像哈希片键,比如选择MD5这类Key来做片键,这样能够很好地满足读写分离,文档被随机分布在分片中。但和哈希片键不同的是,哈希片键对片键查询仍然是比较高效的,根据片键算出哈希值,找到指定分片即可。对于随机片键来说,通常我们不会基于随机片键进行查询,而非片键查询需要向所有分片发出请求。因此实际上随机片键相对于哈希片键,在灵活性(哈希函数可以自己设置),查询效率等方面都是有所不如的。
而无论哈希片键还是随机片键,都存在一个问题,数据过于分散,数据局部性不是很高,可能会导致块迁移时,磁盘IO开销很大(冷数据)。
d. 好的片键
由于数据通常满足时间局部性,因此首先我们希望数据大致按照时间排序,但同时,我们希望数据能均匀分布,不要造成性能热点,因此添加一个搜索键作为第二片键。比如{"month":1, "name":1}
,在3月时,当数据量够大时,MongoDB能够根据name键有效合理地分块,随着时间增长,在4月时,3月的数据开始不再被使用,置换出内存,渐渐成为冷数据,并且由于3月的数据不再写入,因此3月的数据也无需被分裂从而进一步造成块迁移,因此不会造成块迁移磁盘IO开销大的问题。
一般情况下,好的片键可以通过该公式推导:{控制局部化:1, 控制读写分离:1}
,控制局部化的键可以是粗粒度时间(考虑一下为什么不是细粒度?),控制读写分离的键通常是查询键。