分片(sharding)是指将数据库拆分,将其拆分到不同机器的过程,有时也用分区(partitioning)来表示,它是 MongoDB 为应对数据增长需求而采取的办法。
为什么要分片
- 增加单台服务器可用的磁盘空间
- 减轻单台服务器的负载
- 处理单个mongod无法承受的吞吐量
分片原理
在搭建mongodb分片集群之前,我们需要先了解一下其架构和原理,下图是mongodb分片集群的架构图:
mongodb shard.png
mongodb分片集群由三大组件组成:mongos路由、配置服务器config Server和分片shard,下面一一介绍它们的作用。
mongos路由
mongos是一个前置路由,我们的应用客户端并不是直接与分片连接,而是与mongos路由连接,mongos接收到客户端请求后根据查询信息将请求任务分发到对应的分片,在正式生产环境中,为确保高可用性,一般会配置两台以上的mongos路由,以确保当其中一台宕机后集群还能保持高可用。
配置服务器
配置服务器相当于集群的大脑,它存储了集群元信息:集群中有哪些分片、分片的是哪些集合以及数据块的分布集群启动后,当接收到请求时,如果mongos路由没有缓存配置服务器的元信息,会先从配置服务器获取分片集群对于的映射信息。同样的,为了保持集群的高可用,一般会配置多台配置服务器。
shard分片
分片是存储了一个集合部分数据的MongoDB实例,每个分片 可以是一台服务器运行单独一个Mongod实例,但是为了提高系统的可靠性实现自动故障恢复,一个分片应该是一个复制集。
通过分片,我们将一个集合拆分为多个数据块,这些数据块分别部署在不同的机器上,这样可以做到增加单台机器的磁盘可用空间,同时将查询分配到不同的机器上,减轻单台机器的负载。
取值基数
即是片键字段的备选值。这里有2个必须了解的知识点,理解它们才能理解为什么取值基数这么重要:
- chunk定义的是一个连续的片键值范围,文档中的片键字段取值在这个范围内时,文档就属于这个chunk;
- 在不同shard间均衡时是以chunk为单位,而不是文档;
但是chunk其实只是一个虚拟的概念,它仅存在于元数据中,存放文档的shard并不知道chunk的存在。在一个集群中,文档的分布大致是这样的:
也许有人要问,而为什么要有chunk的概念?为什么不是以文档为单位进行均衡?那我们看一下如果以文档为单位进行均衡会带来什么后果:
- 首先我们必须知道每个文档分布在哪个shard上(元数据)
- 如果这样的分布情况放在config上,那么元数据数量将和文档数量一样多,代表着config的数据量将会跟shard达到同样的水平
这在实现上是不现实的,因为我们既然选择分片,大部分时候是因为数据容量或处理能力已经超出了单台机器所能承受的范围。所以如果元数据也达到了这样的数量,即代表元数据很有可能也必须分片,那么又会有元数据的元数据,以及元数据的元数据的元数据……现实当中元数据只是一个复制集而已,可见它的数据量要远小于分片中的数据量(最小可达1/250000),而造成这个结果的原因正是因为有chunk的存在。举个容易理解的例子:
如果文档是学生,那么chunk就是班级,运动会的时候老师会以班级为单位指挥大家行动而不是指挥每一个人。同样,均衡的时候是以chunk为单位指挥每个chunk的文档到哪里去,而不是每个文档。
上面说了很多关于chunk存在的必要性的题外话,那么为什么取值基数对于chunk如此重要?取值基数直接决定了理论上最多能有多少chunk。而最多有多少chunk又会影响到什么?再来看一个例子:
假设要存储一个学校的所有的师生情况,选择年龄为片键。人的年龄具有非常固定的范围,假设为[0,99]。可见当学校人数较多的时候,chunk可能被拆得非常细(请暂时忽略什么时候会拆chunk的问题,后文再详细描述),比如(0, 10], (11, 15], (16, 30] …。但是无论怎么拆,因为年龄是整数,最细的情况下也就是一个数字一个chunk,所以我们最多只可以拆出100个chunk。随着数据增长这100个chunk将会越来越大,并且每个chunk的读写压力往往是不均匀的,所以哪个shard存储了压力较大的chunk比较多,相应地这个shard的压力也会比较大。但是此时系统并不会有任何动作,因为对于系统而言,每个片上的chunk数量是均衡的。
综上所述,取值基数直接决定了一共有多少个chunk,从而间接影响到分片的数据量/压力分布。选择时应该尽可能选择基数较大(即可选值较多)的字段作为片键。
片键取值分布
前一条原则讲的是片键的可能取值范围,这一条原则讲的则是片键在可能取值范围内的分布情况。分布情况会对集群造成什么影响?前面已经提到MongoDB的均衡是以chunk为单位进行的,只要chunk数量均衡了,对系统而言就是均衡的。所以我们不难发现,即使chunk数量均衡,文档数量可能并不均衡。如图所示,shard 1上虽然有2个chunk但实际文档数量可能还不如shard 2上的一个chunk多。此时无论是文档数量还是空间大小都是不均衡的,而我们却无能为力。
仍然以上面的例子继续说明:
对于学校而言,人数最多的是7~20岁的学生,可见7,8,9……19,20这几个chunk会非常繁忙,数据量会非常大,导致拥有这些chunk的shard也会非常繁忙并且面临存储压力。如果不巧这些chunk中的大部分正好都分布在同一个片上,那么这个片可能就忙不过来了。
这就体现出了片键取值分布情况的重要性:片键取值分布直接影响到每个片的压力情况,应该尽可能选择取值分布均匀的字段做片键。
- 测试批量插入数据验证
mongos> for ( var i=1;i<10000000;i++){db.call.insert({"name":"user"+i,age:i})};
- 查看当前是否已经分片到两个shard中去了
mongos> sh.status()
--- Sharding Status ---
sharding version: {
"_id" : 1,
"minCompatibleVersion" : 5,
"currentVersion" : 6,
"clusterId" : ObjectId("589b0cff36b0915841e2a0a2")
}
shards:
{ "_id" : "sha", "host" : "sha/sha1:60000,sha2:60001,sha3:60002", "state" : 1 }
{ "_id" : "shard", "host" : "shard/shard1:50000,shard2:50001,shard3:50002", "state" : 1 }
active mongoses:
"3.4.1" : 1
autosplit:
Currently enabled: yes
balancer:
Currently enabled: yes
Currently running: no
Balancer lock taken at Wed Feb 08 2017 20:20:16 GMT+0800 (CST) by ConfigServer:Balancer
Failed balancer rounds in last 5 attempts: 5
Last reported error: Cannot accept sharding commands if not started with --shardsvr
Time of Reported error: Thu Feb 09 2017 17:56:02 GMT+0800 (CST)
Migration Results for the last 24 hours:
4 : Success
databases:
{ "_id" : "zhao", "primary" : "shard", "partitioned" : true }
zhao.call
shard key: { "name" : 1, "age" : 1 }
unique: false
balancing: true
chunks: #数据已经分片到9个chunks里面了
sha 4
shard 5
{ "name" : { "$minKey" : 1 }, "age" : { "$minKey" : 1 } } -->> { "name" : "user1", "age" : 1 } on : sha Timestamp(4, 1)
{ "name" : "user1", "age" : 1 } -->> { "name" : "user1", "age" : 21 } on : shard Timestamp(5, 1)
{ "name" : "user1", "age" : 21 } -->> { "name" : "user1", "age" : 164503 } on : shard Timestamp(2, 2)
{ "name" : "user1", "age" : 164503 } -->> { "name" : "user1", "age" : 355309 } on : shard Timestamp(2, 3)
{ "name" : "user1", "age" : 355309 } -->> { "name" : "user1", "age" : 523081 } on : sha Timestamp(3, 2)
{ "name" : "user1", "age" : 523081 } -->> { "name" : "user1", "age" : 710594 } on : sha Timestamp(3, 3)
{ "name" : "user1", "age" : 710594 } -->> { "name" : "user1", "age" : 875076 } on : shard Timestamp(4, 2)
{ "name" : "user1", "age" : 875076 } -->> { "name" : "user1", "age" : 1056645 } on : shard Timestamp(4, 3)
{ "name" : "user1", "age" : 1056645 } -->> { "name" : { "$maxKey" : 1 }, "age" : { "$maxKey" : 1 } } on : sha Timestamp(5, 0)