理解分片的最佳途径就是了解它实际是怎么工作的。幸运的是可以在一台机器上配置分片集群,接下来我们就会这么做,1 还会模拟上一节里提到的基于云的电子表格应用程序的行为。在此过程中,我们会仔细查看全局分片配置,通过第一手资料来了解数据是如何基于分片键进行分区的。
1. 为了进行测试,你可以在单台机器上运行各个mongod
和mongos
进程。在本章后续的内容里,我们会看到生产环境下的分片配置,以及一套切实可行的分片部署所需的最小服务器数量。
9.2.1 配置
配置分片集群有两个步骤。第一步,启动所有需要的mongod
和mongos
进程。第二步,也是比较简单的一步,发出一系列命令来初始化集群。你将构建的分片集群由两个分片和三个配置服务器组成,另外还要启动一个mongos
与集群通信。你要启动的全部进程如图9-2所示,括号里是它们的端口号。
图9-2 由示例分片集群构成的进程全貌
你要运行一堆命令来启动集群,因此如果觉得自己一叶障目,不见泰山,不妨回头看看这张图。
1. 启动分片组件
让我们开始为两个副本集创建数据目录吧,它们将成为分片的一部分。
$ mkdir /data/rs-a-1$ mkdir /data/rs-a-2$ mkdir /data/rs-a-3$ mkdir /data/rs-b-1$ mkdir /data/rs-b-2$ mkdir /data/rs-b-3
接下来,启动每个mongod
进程。因为要运行很多进程,所以可以使用--fork
选项,让它们运行在后台。2以下是启动第一个副本集的命令。
2. 注意,如果运行在Windows上,fork
是没用的。因为必须打开新终端窗口来运行每个进程,最好把logpath
选项也忽略了。
$ mongod --shardsvr --replSet shard-a --dbpath /data/rs-a-1 --port 30000 --logpath /data/rs-a-1.log --fork --nojournal$ mongod --shardsvr --replSet shard-a --dbpath /data/rs-a-2 --port 30001 --logpath /data/rs-a-2.log --fork --nojournal$ mongod --shardsvr --replSet shard-a --dbpath /data/rs-a-3 --port 30002 --logpath /data/rs-a-3.log --fork --nojournal
以下是启动第二个副本集的命令:
$ mongod --shardsvr --replSet shard-b --dbpath /data/rs-b-1 --port 30100 --logpath /data/rs-b-1.log --fork --nojournal$ mongod --shardsvr --replSet shard-b --dbpath /data/rs-b-2 --port 30101 --logpath /data/rs-b-2.log --fork --nojournal$ mongod --shardsvr --replSet shard-b --dbpath /data/rs-b-3 --port 30102 --logpath /data/rs-b-3.log --fork --nojournal
如往常一样,现在要初始化这些副本集了。单独连上每个副本集,运行rs.initiate
,随后添加剩余的节点。第一个副本集上的命令是这样的:3
3. arete
是本地主机的名字。
$ mongo arete:30000> rs.initiate
大概一分钟之后,初始节点就变成主节点了,随后就能添加剩余的节点了:
> rs.add(/"arete:30000/")> rs.add(/"arete:30001/", {arbiterOnly: true})
初始化第二个副本集的方法与之类似。在运行rs.initiate
后等待一分钟:
$ mongo arete:30100> rs.initiate> rs.add(/"arete:30100/")> rs.add(/"arete:30101/", {arbiterOnly: true})
最后,在每个副本集上通过Shell运行rs.status
命令,验证一下两个副本集是否正常运行。如果一切顺利,就可以准备启动配置服务器了。4现在,创建每个配置服务器的数据目录,通过configsvr
选项启动各个配置服务器的mongod
进程。
4. 同样的,如果是运行在Windows上,忽略--fork
和-logpath
选项,在新窗口里启动各个mongod
。
$ mkdir /data/config-1$ mongod --configsvr --dbpath /data/config-1 --port 27019 --logpath /data/config-1.log --fork --nojournal$ mkdir /data/config-2$ mongod --configsvr --dbpath /data/config-2 --port 27020 --logpath /data/config-2.log --fork --nojournal$ mkdir /data/config-3$ mongod --configsvr --dbpath /data/config-3 --port 27021 --logpath /data/config-3.log --fork --nojournal
用Shell连接或者查看日志文件,确保每台配置服务器都已启动并已正常运行,并验证每个进程都在监听配置的端口。查看每台配置服务器的日志,应该能看到这样的内容:
Wed Mar 2 15:43:28 [initandlisten] waiting for connections on port 27020Wed Mar 2 15:43:28 [websvr] web admin interface listening on port 28020
如果每个配置服务器都在运行了,那么就能继续下一步,启动mongos
。必须用configdb
选项来启动 mongos
,它接受一个用逗号分隔的配置服务器地址列表:5
5. 在配置列表时要小心,不要在配置服务器地址间加入空格。
$ mongos --configdb arete:27019,arete:27020,arete:27021 --logpath /data/mongos.log --fork --port 40000
2. 配置集群
现在已经准备好了所有的组件,是时候来配置集群了。先从连接mongos
开始。为了简化任务,可以使用分片辅助方法,它们是全局sh
对象上的方法。要查看可用辅助方法的列表,请运行sh.help
。
你将键入一系列配置命令,先从addshard
命令。该命令的辅助方法是sh.addShard
,它接受一个字符串,其中包含副本集名称,随后是两个或多个要连接的种子节点地址。这里你指定了两个先前创建的副本集,用的是每个副本集中非仲裁节点的地址:
$ mongo arete:40000> sh.addShard(/"shard-a/arete:30000,arete:30001/") { /"shardAdded/" : /"shard-a/", /"ok /":1}> sh.addShard(/"shard-b/arete:30100,arete:30101/") { /"shardAdded/" : /"shard-b/", /"ok /":1}
如果命令执行成功,命令的响应中会包含刚添加的分片的名称。可以检查config
数据库的shards
集合,看看命令的执行效果。你使用了getSiblingDB
方法来切换数据库,而非use
命令:
> db.getSiblingDB(/"config/").shards.find{ /"_id/" : /"shard-a/", /"host/" : /"shard-a/arete:30000,arete:30001/" }{ /"_id/" : /"shard-b/", /"host/" : /"shard-b/arete:30100,arete:30101/" }
listshards
命令会返回相同的信息,这是一个快捷方式:
> use admin> db.runCommand({listshards: 1})
在报告分片配置时,Shell的sh.status
方法能很好地总结集群的情况。现在就来试试。
下一步配置是开启一个数据库上的分片,这是对任何集合进行分片的先决条件。应用程序的数据库名为cloud-docs
,可以像下面这样开启分片:
> sh.enableSharding(/"cloud-docs/")
和以前一样,可以检查config
里的数据查看刚才所做的变更。config
数据库里有一个名为databases
的集合,其中包含了一个数据库的列表。每个文档都标明了数据库主分片的位置,以及它是否分区(是否开启了分片):
>db.getSiblingDB(/"config/").databases.find{ /"_id/" : /"admin/", /"partitioned/" : false, /"primary/" : /"config/" }{ /"_id/" : /"cloud-docs/", /"partitioned/" : true, /"primary/" : /"shard-a/" }
现在你要做的就是分片spreadsheets
集合。在对集合进行分片时,要定义一个分片键。这里将使用组合分片键{username: 1, _id: 1}
,因为它能很好地分布数据,还能方便查看和理解块的范围:
sh.shardCollection(/"cloud-docs.spreadsheets/", {username: 1, _id: 1})>
同样,可以通过检查config
数据库来验证分片集合的配置:
> db.getSiblingDB(/"config/").collections.findOne{ /"_id/" : /"cloud-docs.spreadsheets/", /"lastmod/" : ISODate(/"1970-01-16T00:50:07.268Z/"), /"dropped/" : false, /"key/" : { /"username/" : 1, /"_id/" : 1 }, /"unique/" : false}
分片集合的定义可能会提醒你几点;它看起来和索引定义有几分相似之处,尤其是有那个unique
键。在对一个空集合进行分片时,MongoDB会在每个分片上创建一个与分片键对应的索引。6可以直接连上分片,运行getIndexes
方法进行验证。此处,你可以连接到第一个分片,方法的输出包含分片键索引,正如预料的那样:
6. 如果是在对现有集合进行分片,必须在运行shardcollection
命令前创建一个与分片键对应的索引。
$ mongo arete:30000> use cloud-docs> db.spreadsheets.getIndexes[ { /"name/" : /"_id_/", /"ns/" : /"cloud-docs.spreadsheets/", /"key/" : { /"_id/" : 1 }, /"v/" : 0 }, { /"ns/" : /"cloud-docs.spreadsheets/", /"key/" : { /"username/" : 1, /"_id/" : 1 }, /"name/" : /"username_1__id_1/", /"v/" : 0 }]
一旦完成了集合的分片,分片集群就准备就绪了。现在可以向集群写入数据,数据将分布到各分片上。下一节里我们会了解到它是如何工作的。
9.2.2 写入分片集群
我们将向分片集合写入数据,这样你才能观察块的排列与移动。块是MongoDB分片的要素。每个示例文档都表示了一个电子表格,看起来是这样的:
{ _id: ObjectId(/"4d6f29c0e4ef0123afdacaeb/"), filename: /"sheet-1/", updated_at: new Date, username: /"banks/", data: /"RAW DATA/"}
请注意,data
字段会包含一个5 KB的字符串以模拟原始数据。
本书的源代码中包含一个Ruby脚本,你可以用它向集群写入文档数据。该脚本接受一个循环次数作为参数,每个循环里都会为200个用户各插入5 KB的文档。脚本的源码如下:
require /'rubygems/'require /'mongo/'require /'names/'@con = Mongo::Connection.new(/"localhost/", 40000)@col = @con[/'cloud/'][/'spreadsheets/']@data = /"abcde/" * 1000def write_user_docs(iterations=0, name_count=200) iterations.times do |n| name_count.times do |n| doc = { :filename => /"sheet-#{n}/", :updated_at => Time.now.utc, :username => Names::LIST[n], :data => @data } @col.insert(doc) end endendif ARGV.empty? || !(ARGV[0] =~ /^d+$/) puts /"Usage: load.rb [iterations] [name_count]/"else iterations = ARGV[0].to_i if ARGV[1] && ARGV[1] =~ /^d+$/ name_count = ARGV[1].to_i else name_count = 200 end write_user_docs(iterations, name_count)end
如果手头有脚本,可以在命令行里不带参数运行脚本,它会循环一次,插入200个值:
$ ruby load.rb
现在,通过Shell连接mongos
。如果查询spreadsheets
集合,你会发现其中包含200个文档,总大小在1 MB左右。还可以查询一个文档,但要排除data
字段(你不想在屏幕上输出5 KB文本吧)。
$ mongo arete:40000> use cloud-docs> db.spreadsheets.count200> db.spreadsheets.stats.size1019496> db.spreadsheets.findOne({}, {data: 0}){ /"_id/" : ObjectId(/"4d6d6b191d41c8547d0024c2/"), /"username/" : /"Cerny/", /"updated_at/" : ISODate(/"2011-03-01T21:54:33.813Z/"), /"filename/" : /"sheet-0/"}
现在,可以检查一下整个分片范围里发生了什么,切换到config
数据库,看看块的个数:
> use config> db.chunks.count1
目前只有一个块,让我们看看它什么样:
> db.chunks.findOne{ /"_id/" : /"cloud-docs.spreadsheets-username_MinKey_id_MinKey/", /"lastmod/" : { /"t/" : 1000, /"i/" : 0 }, /"ns/" : /"cloud-docs.spreadsheets/", /"min/" : { /"username/" : { $minKey:1}, /"_id/" : { $minKey:1} }, /"max/" : { /"username/" : { $maxKey:1}, /"_id/" : { $maxKey:1} }, /"shard/" : /"shard-a/"}
你能说出这个块所表示的范围吗?如果只有一个块,那么它的范围是这个分片集合。这是由min
和max
字段标识的,这些字段通过$minKey
和$maxKey
限定了块的范围。
MINKEY与MAXKEY
作为BSON类型的边界,
$minKey
与$maxKey
常用于比较操作之中。$minKey
总是小于所有BSON类型,而$maxKey
总是大于所有BSON类型。因为给定的字段值能包含各种BSON类型,所以在分片集合的两端,MongoDB使用这两个类型来标记块的端点。
通过向spreadsheets
集合添加更多数据,你能看到更有趣的块范围。还是使用之前的Ruby脚本,但这次要循环100次,向集合中插入20 000个文档,总计100 MB:
$ ruby load.rb 100
可以像下面这样验证插入是否成功:
> db.spreadsheets.count20200> db.spreadsheets.stats.size103171828
样本插入速度
注意,向分片集群插入数据需要花好几分钟时间。速度如此之慢有三个原因。首先,每次插入都要与服务器交互一次,而在生产环境中可以使用批量插入。其次,你是在使用Ruby进行插入,Ruby的BSON序列器要比其他某些驱动的慢。最后,也是最重要的,你是在一台机器上运行所有分片节点的,这为磁盘带来了巨大的负担,因为四个节点正在同时向磁盘写入数据(两个副本集的主节点,以及两个副本集的从节点)。有理由相信,在适当的生产环境部署中,插入的速度会快许多。
插入了这么多数据之后,现在肯定有不止一个块了。可以统计chunks集合的文档数快速检查块的状态:
> use config> db.chunks.count10
运行sh.status
能看到更详细的信息,该方法会输出所有的块以及它们的范围。简单起见,我只列出头两个块的信息:
> sh.statussharding version: { /"_id/" : 1, /"version/":3} shards: { /"_id/": /"shard-a/", /"host/": /"shard-a/arete:30000,arete:30001/" } { /"_id/": /"shard-b/", /"host/": /"shard-b/arete:30100,arete:30101/" } databases: { /"_id/": /"admin/", /"partitioned/": false, /"primary/": /"config/" } { /"_id/": /"test/", /"partitioned/": false, /"primary/": /"shard-a/" } { /"_id/": /"cloud-docs/", /"partitioned/": true, /"primary/": /"shard-b/" } shard-a 5 shard-b 5 { /"username/": { $minKey:1}, /"_id/" : { $minKey:1}}-- >> { /"username/": /"Abdul/", /"_id/": ObjectId(/"4e89ffe7238d3be9f0000012/") } on: shard-a { /"t/" : 2000, /"i/" : 0 } { /"username/" : /"Abdul/", /"_id/" : ObjectId(/"4e89ffe7238d3be9f0000012/") } -->> { /"username/" : /"Buettner/", /"_id/" : ObjectId(/"4e8a00a0238d3be9f0002e98/") } on : shard-a { /"t/" : 3000, /"i/" : 0 }
情况明显不同了,现在有10个块了。当然,每个块所表示的是一段连续范围的数据。可以看到第一个块由$minKey
到Abdul
的文档构成,第二个块由Abdul
到Buettner
的文档构成。7不仅块变多了,块还迁移到了第二个分片上。通过sh.status
的输出能看到这个变化,但还有更简单的方法:
7. 如果你是跟着示例一路做下来的,会发现自己的块分布与示例稍有不同。
> db.chunks.count({/"shard/": /"shard-a/"})5> db.chunks.count({/"shard/": /"shard-b/"})5
集群的数据量还不大。拆分算法表明会经常发生数据拆分,正如你所见,这能在早期就实现数据和块的均匀分布。从现在开始,只要写操作在现有块范围内保持均匀分布,就不太会发生迁移。
早期块拆分
分片集群会在早期积极进行块拆分,以便加快数据在分片中的迁移。具体说来,当块的数量小于10时,会按最大块尺寸(16 MB)的四分之一进行拆分;当块的数量在10到20之间时,会按最大块尺寸的一半(32 MB)进行拆分。
这种做法有两个好处。首先,这会预先创建很多块,触发一次迁移。其次,这次迁移几乎是无痛的,因为块的尺寸越小,其迁移的数据就越少。
现在,拆分的阈值会增大。通过大量插入数据,你会看到拆分是怎么缓缓减慢的,以及块是怎么增长到最大尺寸的。试着再向集群里插入800 MB数据:
$ ruby load.rb 800
这条命令会执行很长时间,因此你可能会想在启动加载进程后暂时离开吃些点心。执行完毕之后,总数据量比以前增加了8倍。但是如果查看分块状态,你会发现块的数量差不多只是原来的两倍:
> use config> db.chunks.count21
由于块的数量变多了,块的平均范围就变小了,但是每个块都会包含更多数据。举例来看,集合里的第一个块的范围只是Abbott
到Bender
,但它的大小已经接近60 MB了。因为目前块的最大尺寸是64 MB,所以如果继续插入数据,很快就能看到块的拆分了。
另一件值得注意的事情是块的分布还是很均匀的,就和之前一样:
> db.chunks.count({/"shard/": /"shard-a/"})11> db.chunks.count({/"shard/": /"shard-b/"})10
尽管刚才在插入800 MB数据时块的数量增加了,但你还是可以猜到没有发生迁移;一个可能的情况是每个原始块被一拆为二,期间还有一次额外的拆分。可以查询config
数据库的changelog
集合加以验证:
> db.changelog.count({what: /"split/"})20> db.changelog.find({what: /"moveChunk.commit/"}).count6
这符合我们的猜测。一共发生了20次拆分,产生了20个块,但只发生了6次迁移。要再深入了解一下究竟发生了什么,可以查看变更记录的具体条目。举例来说,以下条目记录了第一次的块移动:
> db.changelog.findOne({what: /"moveChunk.commit/"}){ /"_id/" : /"arete-2011-09-01T20:40:59-2/", /"server/" : /"arete/", /"clientAddr/" : /"127.0.0.1:55749/", /"time/" : ISODate(/"2011-03-01T20:40:59.035Z/"), /"what/" : /"moveChunk.commit/", /"ns/" : /"cloud-docs.spreadsheets/", /"details/" : { /"min/" : { /"username/" : { $minKey : 1 }, /"_id/" : { $minKe y:1} }, /"max/" : { /"username/" : /"Abbott/", /"_id/" : ObjectId(/"4d6d57f61d41c851ee000092/") }, /"from/" : /"shard-a/", /"to/" : /"shard-b/" }}
这里可以看到块从shard-a
移到了shard-b
。总的来说,在变更记录里找到的文档可读性都比较好。在深入了解分片并打算打造自己的分片集群之时,配置变更记录是了解拆分和迁移行为的优秀材料,应该经常看看它。