分片简介
1、什么是分片
分片是指跨机器拆分数据的过程,有时也会用术语分区(partitioning)来表示这个概念。通过在每台机器上放置数据的子集,无须功能强大的机器,只使用大量功能稍弱的机器,就可以存储更多的数据并处理更多的负载。分片还可以用于其他目的,包括将经常访问的数据放置在更高性能的硬件上,或基于地理位置(比如,基于在一个特定语言环境下的用户)来拆分集合中的文档以使它们接近最常对其进行访问的应用程序服务器。
大部分数据库软件支持进行手动分片。使用这种方法,应用程序会维护到多个不同数据库服务器端的连接,每个服务器端都是完全独立的。应用程序不仅管理不同服务器上不同数据的存储,还管理在适当的服务器上查询数据。这种方式可以很好地工作,但当从集群中添加或删除节点,或者面对数据分布或负载模式的变化时,就非常难以维护了。
MongoDB 支持自动分片,这种方式试图将数据库架构从应用程序中抽象出来,并简化系统管理。在某种程度上,MongoDB 允许应用程序好像始终在和一台单机的MongoDB 服务器对话一样。在运维方面,MongoDB 可以自动均衡分片上的数据,使容量的添加和删除变得更容易。
无论从开发还是运维的角度来看,分片都是最复杂的MongoDB 配置方式。有许多组件需要配置和监控,数据在集群中会自动转移。在尝试部署或使用分片集群之前,应该首先熟悉单机服务器和副本集。此外,与副本集一样,配置和部署分片集群的推荐方式是通过 MongoDBOps Manager 或 MongoDB Atlas。如果需要保留对计算基础设施的控制,建议使用 Ops Manager。如果可以 将基础设施管理留给 MongoDB(可以选择在 Amazon AWS、Microsoft Azure 或 Google Compute Cloud 中运行),则推荐使用 MongoDB Atlas。
2、理解集群组件
MongoDB 的分片机制允许你创建一个由许多机器(分片)组成的集群,并将集合中的数据分散在集群中,在每个分片上放置数据的一个子集。这允许应用程序超出单机服务器或副本集的资源限制。
许多人对复制和分片之间的区别感到困惑。记住,复制在多台服务器上创建了数据的精确副本,因此每台服务器都是其他服务器的镜像。相反,每个分片包含了不同的数据子集。
分片的目标之一是使由两个、3 个、10 个甚至数百个分片组成的集群对应用程序来说就像是一台单机服务器。为了对应用程序隐藏这些细节,需要在分片前面运行一个或多个称为 mongos 的路由进程。mongos 维护着一个"目录",指明了哪个分片包含哪些数据。

如上图所示,应用程序可以正常连接到此路由服务器并发出请求。路由服务器知道哪些数据在哪个分片上,可以将请求转发到适当的分片。如果有对请求的响应,路由服务器会收集它们,并在必要时进行合并,然后再发送回应用程序。对应用程序来说,它只知道自己连接到了一个单独的mongod,如下图所示。

3、在单机集群上进行分片
在单台机器上快速建立一个集群。首先,使用 --nodb和 --norc 选项启动 mongo shell:
js
$ mongo --nodb --norc
使用 ShardingTest 类创建集群。在刚启动的 mongoshell 中运行以下代码:
js
st = ShardingTest({
name:"one-min-shards",
chunkSize:1,
shards:2,
rs:{
nodes:3,
oplogSize:10
},
other:{
enableBalancer:true
}
});
chunksize 选项会在后面行介绍。目前只需将其设置为 1。至于其他传递给 ShardingTest 的选项,name仅仅是分片集群的一个标签,shards 指定了集群将由两个分片组成(在本例中,这样做是为了保持较低的资源需求),rs 将每个分片定义为一组 3 个节点的副本集,其oplogSize 为 10MiB(同样,保持较低的资源占用)。虽然可以为每个分片仅运行一个单独的 mongod 进程,但是以副本集创建每个分片可以将典型的分片集群架构描绘得更加清晰。在最后一个选项中,我们指示ShardingTest 在集群启动后启用均衡器。这可以确保数据均匀分布在两个分片上。
ShardingTest 是 MongoDB 工程师为内部使用而设计的一个类,因此没有外部文档。但是,由于附带在了MongoDB 服务器端的程序中,因此它提供了一个使用分 片集群的最直接方法。ShardingTest 最初是为支持服务器端测试套件而设计的,现在仍然用于此目的。默认情况下,它在保持尽可能低的资源占用以及建立体系结构相对复杂的分片集群方面,提供了许多便利。它假设你的机器上存在 /data/db 目录,如果 ShardingTest 运行失败,则需要创建该目录,然后重新运行命令。
当运行这个命令时,ShardingTest 会为你自动做很多事情。它会创建一个包含两个分片的集群,每个分片都是一个副本集。同时会对副本集进行配置,并使用必要的选项启动每个节点以建立复制协议。它会启动一个 mongos来管理跨分片的请求,这样客户端就可以像与一个独立的mongod 通信一样与集群进行交互。最后,它会为用于维护路由表信息的配置服务器启动一个额外的副本集,以确保查询被定向到正确的分片。记住,分片的主要使用场景是拆分数据集以解决硬件和成本的限制,或为应用程序提 供更好的性能(比如地理分区)。MongoDB 分片以一种与应用程序在许多方面无缝对接的方式提供了这些功能。
当 ShardingTest 完成集群设置后,将启动并运行 10 个进程,你可以连接到这些进程:两个副本集(各有 3 个节点)、一个配置服务器副本集(有 3 个节点),以及一个 mongos。默认情况下,这些进程会从 20000 端口开始。mongos 会运行在 20009 端口上。在本地机器上运行的其他进程以及之前对 ShardingTest 的调用可能会影响 ShardingTest 使用的端口,但是确定集群进程运行在哪些端口上应该不会有什么困难。
接下来会连接到 mongos 来使用集群。整个集群会将日志转储到当前 shell 中,因此打开第二个终端窗口,并启动另一个 mongo shell:
js
$ mongo --nodb
使用这个 shell 连接到集群的 mongos。再次说明,你的mongos 应该运行在 20009 端口上:
js
> db = (new Mongo("localhost:20009")).getDB("accounts")
注意,mongo shell 中的提示符应该发生变化,以反映出已经连接到一个 mongos。shell 作为客户端连接到了 mongos。可以开始向mongos 发送请求了,它会将请求路由到相应的分片。你不需要知道任何关于分片的信息,比如有多少个分片或者它们的地址是什么。只要有分片存在,就可以将请求发送给 mongos,并允许其转发到合适的分片上。
首先插入一些数据:
js
> for (var i=0; i<100000; i++) {
db.users.insert({"username" : "user"+i, "created_at" : new Date()});
}
> db.users.count()
100000
可以看到,与 mongos 的交互与使用单机服务器是一样的。
可以运行 sh.status() 来获得集群的总体视图。此命令会提供一个分片、数据库以及集合的摘要。
js
> sh.status()
--- Sharding Status ---
sharding version: {
"_id": 1,
"minCompatibleVersion": 5,
"currentVersion": 6,
"clusterId": ObjectId("5a4f93d6bcde690005986071")
}
shards:
{
"_id" : "one-min-shards-rs0",
"host" :
"one-min-shards-rs0/MBP:20000,MBP:20001,MBP:20002",
"state" : 1 }
{ "_id" : "one-min-shards-rs1",
"host" :
"one-min-shards-rs1/MBP:20003,MBP:20004,MBP:20005",
"state" : 1 }
active mongoses:
"3.6.1" : 1
autosplit:
Currently enabled: no
balancer:
Currently enabled: no
Currently running: no
Failed balancer rounds in last 5 attempts: 0
Migration Results for the last 24 hours:
No recent migrations
databases:
{ "_id" : "accounts", "primary" : "one-min-shards-rs1",
"partitioned" : false }
{ "_id" : "config", "primary" : "config",
"partitioned" : true }
config.system.sessions
shard key: { "_id" : 1 }
unique: false
balancing: true
chunks:
one-min-shards-rs0 1
{ "_id" : { "$minKey" : 1 } } -->> { "_id" : { "$maxKey" : 1 } }
on : one-min-shards-rs0 Timestamp(1, 0)
sh 类似于 rs,但它是用于分片的:这是一个全局变量,定义了许多关于分片的辅助函数,可以通过运行sh.help() 进行查看。正如 sh.status() 的输出所示,当前有两个分片和两个数据库(config 数据库是自动创建的)。
你的 accounts 数据库的主分片(primary shard)可能与这里显示的不同。主分片是为每个数据库随机选择的一个"主基地"。所有的数据都会在这个主分片上。MongoDB 现在还不能自动分发数据,因为它不知道你希望如何或者是否进行分发。你必须明确指定,在每个集合中应该如何分布数据。
主分片与副本集的主节点不同。主分片是指组成某个分片的整个副本集。副本集中的主节点是集合中可以接收写操作的单台服务器。
要对一个特定的集合进行分片,首先需要在集合的数据库上启用分片。如下所示,运行 enableSharding 命令:
js
> sh.enableSharding("accounts")
现在可以对 accounts 数据库中的集合进行分片了。
在对集合进行分片时,需要选择一个片键(shardkey)。片键是 MongoDB 用来拆分数据的一个或几个字段。如果选择在 "username" 字段上分片,MongoDB 就 会根据用户名的范围对数据进行拆分:"a1-steak-sauce" 到 "defcon"、"defcon1" 到 "howie1998",等等。可以将选择一个片键看作为集合中的数据选择一个排列顺序。这与索引的概念类似,也十分合理:随着集合的增大,片键会成为集合中最重要的索引。只有创建了索引的字段才能够作为片键。
因此,在启用分片之前,必须在想要分片的键上创建一个索引:
js
> db.users.createIndex({"username" : 1})
现在可以通过 "username" 来对集合进行分片了:
js
> sh.shardCollection("accounts.users", {"username" : 1})
尽管这里在选择片键时没有做太多考虑,但在实际系统中,这是一个需要仔细斟酌的重要决定。
等待几分钟并再次运行 sh.status(),可以看到比之前显示出了更多的信息:
js
> sh.status()
--- Sharding Status ---
sharding version: {
"_id" : 1,
"minCompatibleVersion" : 5,
"currentVersion" : 6,
"clusterId" : ObjectId("5a4f93d6bcde690005986071")
}
shards:
{ "_id" : "one-min-shards-rs0",
"host" :
"one-min-shards-rs0/MBP:20000,MBP:20001,MBP:20002",
"state" : 1 }
{ "_id" : "one-min-shards-rs1",
"host" :
"one-min-shards-rs1/MBP:20003,MBP:20004,MBP:20005",
"state" : 1 }
active mongoses:
"3.6.1" : 1
autosplit:
Currently enabled: no
balancer:
Currently enabled: yes
Currently running: no
Failed balancer rounds in last 5 attempts: 0
Migration Results for the last 24 hours:
6 : Success
databases:
{ "_id" : "accounts", "primary" : "one-min-shards-rs1",
"partitioned" : true }
accounts.users
shard key: { "username" : 1 }
unique: false
balancing: true
chunks:
one-min-shards-rs0 6
one-min-shards-rs1 7
{ "username" : { "$minKey" : 1 } } -->>
{ "username" : "user17256" } on : one-min-shards-rs0 Timestamp(2, 0)
{ "username" : "user17256" } -->>
{ "username" : "user24515" } on : one-min-shards-rs0 Timestamp(3, 0)
{ "username" : "user24515" } -->>
{ "username" : "user31775" } on : one-min-shards-rs0 Timestamp(4, 0)
{ "username" : "user31775" } -->>
{ "username" : "user39034" } on : one-min-shards-rs0 Timestamp(5, 0)
{ "username" : "user39034" } -->>
{ "username" : "user46294" } on : one-min-shards-rs0 Timestamp(6, 0)
{ "username" : "user46294" } -->>
{ "username" : "user53553" } on : one-min-shards-rs0 Timestamp(7, 0)
{ "username" : "user53553" } -->>
{ "username" : "user60812" } on : one-min-shards-rs1 Timestamp(7, 1)
{ "username" : "user60812" } -->>
{ "username" : "user68072" } on : one-min-shards-rs1 Timestamp(1, 7)
{ "username" : "user68072" } -->>
{ "username" : "user75331" } on : one-min-shards-rs1 Timestamp(1, 8)
{ "username" : "user75331" } -->>
{ "username" : "user82591" } on : one-min-shards-rs1 Timestamp(1, 9)
{ "username" : "user82591" } -->>
{ "username" : "user89851" } on : one-min-shards-rs1 Timestamp(1, 10)
{ "username" : "user89851" } -->>
{ "username" : "user9711" } on : one-min-shards-rs1 Timestamp(1, 11)
{ "username" : "user9711" } -->>
{ "username" : { "$maxKey" : 1 } } on : one-min-shards-rs1 Timestamp(1, 12)
{ "_id" : "config", "primary" : "config", "partitioned" : true }
config.system.sessions
shard key: { "_id" : 1 }
unique: false
balancing: true
chunks:
one-min-shards-rs0 1
{ "_id" : { "$minKey" : 1 } } -->>
{ "_id" : { "$maxKey" : 1 } } on : one-min-shards-rs0 Timestamp(1, 0)
这个集合被分成了 13 个块,每个块是数据的一个子集。这些是按照片键范围排列的({"username" : minValue}-->> {"username" : maxValue} 表示每个数据块的范围)。查看输出信息的 "on" : shard 部分,可以看到这些块均匀地分布在各个分片之间。
将集合拆分成数据块的过程如图所示。在分片之前,集合实际上是一个单独的块。分片根据片键将其拆分成更小的块。之后这些数据块可能会分布到集群中。



注意块列表开始和结束处的键,即 $minKey 和$maxKey。$minKey 可以被认为是"负无穷"。这个值比MongoDB 中的其他值都要小。类似地,$maxKey 相当于"正无穷"。它比任何其他值都要大。因此,总是会在块范围中看到这两个"极值"。片键的值始终位于 $minKey和 $maxKey 之间。这两个值实际上是 BSON 类型,不应该用在应用程序中,它们主要是供内部使用的。如果希 望在 shell 中引用它们,可以使用 MinKey 和 MaxKey常量。
现在数据已经分布在多个分片上了,让我们尝试执行一些查询。首先,查询一个特定的用户名:
js
> db.users.find({username: "user12345"})
{
"_id" : ObjectId("5a4fb11dbb9ce6070f377880"),
"username" : "user12345",
"created_at" : ISODate("2018-01-05T17:08:45.657Z")
}
可以看到,查询语句工作正常。现在执行一下 explain 来看看 MongoDB 在幕后是如何处理的:
js
> db.users.find({username: "user12345"}}).explain()
{
"queryPlanner" : {
"mongosPlannerVersion" : 1,
"winningPlan" : {
"stage" : "SINGLE_SHARD",
"shards" : [{
"shardName" : "one-min-shards-rs0",
"connectionString" :
"one-min-shards-rs0/MBP:20000,MBP:20001,MBP:20002",
"serverInfo" : {
"host" : "MBP",
"port" : 20000,
"version" : "3.6.1",
"gitVersion" : "025d4f4fe61efd1fb6f0005be20cb45a004093d1"
},
"plannerVersion" : 1,
"namespace" : "accounts.users",
"indexFilterSet" : false,
"parsedQuery" : {
"username" : {
"$eq" : "user12345"
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "SHARDING_FILTER",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"username" : 1
},
"indexName" : "username_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"username" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"username" : [
"[\"user12345\", \"user12345\"]"
]
}
}
}
},
"rejectedPlans" : [ ]
}]
}
},
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1515174248, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1515173700, 201)
}
从 explain 输出的 "winningPlan" 字段中可以看到,集群使用单个分片 (one-min-shards-rs0)完成了这个查询。根据之前展示出来的 sh.status() 的输出,可以看到user12345 确实属于集群中为该分片列出的第一个块的键范围。
由于 "username" 是片键,因此 mongos 能够将查询直接路由到正确的分片上。作为对比,下面来看看查询所有用户的结果:
js
> db.users.find().explain()
{
"queryPlanner":{
"mongosPlannerVersion":1,
"winningPlan":{
"stage":"SHARD_MERGE",
"shards":[
{
"shardName":"one-min-shards-rs0",
"connectionString":
"one-min-shards-rs0/MBP:20000,MBP:20001,MBP:20002",
"serverInfo":{
"host":"MBP.fios-router.home",
"port":20000,
"version":"3.6.1",
"gitVersion":"025d4f4fe61efd1fb6f0005be20cb45a004093d1"
},
"plannerVersion":1,
"namespace":"accounts.users",
"indexFilterSet":false,
"parsedQuery":{
},
"winningPlan":{
"stage":"SHARDING_FILTER",
"inputStage":{
"stage":"COLLSCAN",
"direction":"forward"
}
},
"rejectedPlans":[
]
},
{
"shardName":"one-min-shards-rs1",
"connectionString":
"one-min-shards-rs1/MBP:20003,MBP:20004,MBP:20005",
"serverInfo":{
"host":"MBP.fios-router.home",
"port":20003,
"version":"3.6.1",
"gitVersion":"025d4f4fe61efd1fb6f0005be20cb45a004093d1"
},
"plannerVersion":1,
"namespace":"accounts.users",
"indexFilterSet":false,
"parsedQuery":{
},
"winningPlan":{
"stage":"SHARDING_FILTER",
"inputStage":{
"stage":"COLLSCAN",
"direction":"forward"
}
},
"rejectedPlans":[
]
}
]
}
},
"ok":1,
"$clusterTime":{
"clusterTime":Timestamp(1515174893, 1),
"signature":{
"hash":BinData(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId":NumberLong(0)
}
},
"operationTime":Timestamp(1515173709, 514)
}
从这个 explain 中可以看到,该查询必须访问两个分片才能找到所有数据。通常来说,如果在查询中没有使用片键,mongos 就不得不将查询发送到每个分片上。
包含片键并可以发送到单个分片或分片子集的查询称为定向查询(targeted query)。必须发送到所有分片的查询称为分散--收集查询(scatter-gather query),也称为广播查询:mongos 会将查询分散到所有分片,然后再从各个分片收集结果。
完成这个实验后,就可以关闭副本集了。切换回原来的shell,并按几次 Enter 键返回到命令行,然后运行st.stop() 干净地关闭所有服务器:
js
> st.stop()
如果不确定某个操作的作用,那么使用 ShardingTest 快速创建一个本地集群并尝试一下会很有帮助。