首页 » MongoDB实战 » MongoDB实战全文在线阅读

《MongoDB实战》9.2 示例分片集群

关灯直达底部

理解分片的最佳途径就是了解它实际是怎么工作的。幸运的是可以在一台机器上配置分片集群,接下来我们就会这么做,1 还会模拟上一节里提到的基于云的电子表格应用程序的行为。在此过程中,我们会仔细查看全局分片配置,通过第一手资料来了解数据是如何基于分片键进行分区的。

1. 为了进行测试,你可以在单台机器上运行各个mongodmongos进程。在本章后续的内容里,我们会看到生产环境下的分片配置,以及一套切实可行的分片部署所需的最小服务器数量。

9.2.1 配置

配置分片集群有两个步骤。第一步,启动所有需要的mongodmongos进程。第二步,也是比较简单的一步,发出一系列命令来初始化集群。你将构建的分片集群由两个分片和三个配置服务器组成,另外还要启动一个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/"}  

你能说出这个块所表示的范围吗?如果只有一个块,那么它的范围是这个分片集合。这是由minmax字段标识的,这些字段通过$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个块了。当然,每个块所表示的是一段连续范围的数据。可以看到第一个块由$minKeyAbdul的文档构成,第二个块由AbdulBuettner的文档构成。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  

由于块的数量变多了,块的平均范围就变小了,但是每个块都会包含更多数据。举例来看,集合里的第一个块的范围只是AbbottBender,但它的大小已经接近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。总的来说,在变更记录里找到的文档可读性都比较好。在深入了解分片并打算打造自己的分片集群之时,配置变更记录是了解拆分和迁移行为的优秀材料,应该经常看看它。