Mongo分片之分片管理
导航: Mongo分片: 1.Mongo分片介绍。 2.Mongo分片之配置分片。 3.Mongo分片之选择片键。 4.Mongo分片之分片管理。 |
对数据库管理员来说,分片集群是最困难的部署类型。本章将学习在集群上执行管理任务的方方面面,内容包括:
-
- 检査集群状态:集群有哪些成员?数据保存在哪里?哪些连接是打开的?
- 如何添加、删除和修改集群的成员;
- 管理数据移动和手动移动数据。
1.检查集群状态
有一些辅助函数可用于找出数据保存位置、所在分片,以及集群正在进行的操作。
1.1 使用sh.status查看集群摘要信息
使用sh.status()可査看分片、数据库和分片集合的摘要信息。如果块的数量较少,则该命令会打印出每个块的保存位置。否则它只会简单地给出集合的片键,以及每个分片的块数:
> sh.status() --- Sharding Status --- sharding version: { "_id" : 1, "version" : 3 } shards: { "_id" : "shard0000", "host" : "localhost:30000", "tags" : [ "USPS" , "Apple" ] } { "_id" : "shard0001", "host" : "localhost:30001" } { "_id" : "shard0002", "host" : "localhost:30002", "tags" : [ "Apple" ] } databases: { "_id" : "admin", "partitioned" : false, "primary" : "config" } { "_id" : "test", "partitioned" : true, "primary" : "shard0001" } test.foo shard key: { "x" : 1, "y" : 1 } chunks: shard0000 4 shard0002 4 shard0001 4 { "x" : { $minKey : 1 }, "y" : { $minKey : 1 } } -->> { "x" : 0, "y" : 10000 } on : shard0000 { "x" : 0, "y" : 10000 } -->> { "x" : 12208, "y" : -2208 } on : shard0002 { "x" : 12208, "y" : -2208 } -->> { "x" : 24123, "y" : -14123 } on : shard0000 { "x" : 24123, "y" : -14123 } -->> { "x" : 39467, "y" : -29467 } on : shard0002 { "x" : 39467, "y" : -29467 } -->> { "x" : 51382, "y" : -41382 } on : shard0000 { "x" : 51382, "y" : -41382 } -->> { "x" : 64897, "y" : -54897 } on : shard0002 { "x" : 64897, "y" : -54897 } -->> { "x" : 76812, "y" : -66812 } on : shard0000 { "x" : 76812, "y" : -66812 } -->> { "x" : 92793, "y" : -82793 } on : shard0002 { "x" : 92793, "y" : -82793 } -->> { "x" : 119599, "y" : -109599 } on : shard0001 { "x" : 119599, "y" : -109599 } -->> { "x" : 147099, "y" : -137099 } on : shard0001 { "x" : 147099, "y" : -137099 } -->> { "x" : 173932, "y" : -163932 } on : shard0001 { "x" : 173932, "y" : -163932 } -->> { "x" : { $maxKey : 1 }, "y" : { $maxKey : 1 } } on : shard0001 test.ips shard key: { "ip" : 1 } chunks: shard0000 2 shard0002 3 shard0001 3 { "ip" : { $minKey : 1 } } -->> { "ip" : "002.075.101.096" } on : shard0000 { "ip" : "002.075.101.096" } -->> { "ip" : "022.089.076.022" } on : shard0002 { "ip" : "022.089.076.022" } -->> { "ip" : "038.041.058.074" } on : shard0002 { "ip" : "038.041.058.074" } -->> { "ip" : "055.081.104.118" } on : shard0002 { "ip" : "055.081.104.118" } -->> { "ip" : "072.034.009.012" } on : shard0000 { "ip" : "072.034.009.012" } -->> { "ip" : "090.118.120.031" } on : shard0001 { "ip" : "090.118.120.031" } -->> { "ip" : "127.126.116.125" } on : shard0001 { "ip" : "127.126.116.125" } -->> { "ip" : { $maxKey : 1 } } on : shard0001 tag: Apple { "ip" : "017.000.000.000" } -->> { "ip" : "018.000.000.000" } tag: USPS { "ip" : "056.000.000.000" } -->> { "ip" : "057.000.000.000" } { "_id" : "test2", "partitioned" : false, "primary" : "shard0002" }
块的数量较多时,sh.status()命令会概述块的状态,而非打印出每个块的相关信息。如需査看所有的块,可使用sh.status(true)命令(true参数要求sh.status()命令打印出尽可能详尽的信息)。
sh.status()显示的所有信息都来自config数据库。
运行sh.status()命令,使MapReduce获取这一数据,因此,如果启动数据库时指定了 --noscripting选项,则无法运行sh.status()命令。
1.2 检查配置信息
集群相关的所有配置信息都保存在配置服务器上config数据库的集合中。可直接访问该数据库,不过shell提供了一些辅助函数,并通过这些函数获取更适于阅读的信息。不过,可始终通过直接査询config数据库的方式,获取集群的元数据。
提示:永远不要直接连接到配置服务器,以防配置服务器数据被不小心修改或删除。应先连接到mongos,然后通过config数据库来査询相关信息,方法与查询其他数据库一样:
mongos> use config
如果通过mongos操作配置数据(而不是直接连接到配置服务器),mongos会保证将修改同步到所有配置服务器,也会防止危险操作的发生,如意外删除config数据库等。
总的来说,不应直接修改config数据库的任何数据(例外情况下面会提到)。如果确实修改了某些数据,通常需要重启所有的mongos服务器,才能看到效果。
config数据库中有一些集合,本节将介绍这些集合的内容和使用方法。
1.config.shards
shards集合跟踪记录集群内所有分片的信息。shards集合中的一个典型文档结构如下:
> db.shards.findOne() { "_id" : "spock", "host" : "spock/server-1:27017,server-2:27017,server-3:27017", "tags" : [ "us-east", "64gb mem", "cpu3" ] }
分片的"_id"来自于副本集的名称,所以集群中的每个副本集名称都必须是唯一的。
更新副本集配置的时候(比如添加或删除成员),host字段会自动更新。
2.config.databases
databases集合跟踪记录集群中所有数据库的信息,不管数据库有没有被分片:
> db.databases.find() { "_id" : "admin", "partitioned" : false, "primary" : "config" } { "_id" : "test1", "partitioned" : true, "primary" : "spock" } { "_id" : "test2", "partitioned" : false, "primary" : "bones" }
如果在数据库上执行过enableSharding,则此处的"partitioned"字段值就是true。"primary"是“主数据库”(home base)。数据库的所有新集合均默认被创建在数据库的主分片上。
3.config.collections
collections集合跟踪记录所有分片集合的信息(非分片集合信息除外)。其中的文档 结构如下:
> db.collections.findOne() { "_id" : "test.foo", "lastmod" : ISODate("1970-01-16T17:53:52.934Z"), "dropped" : false, "key" : { "x" : 1, "y" : 1 }, "unique" : true }
下面是一些重要字段。
- _id
集合的命名空间。
- key
片键。本例中指由x和y组成的复合片键。
- unique
表明片键是一个唯一索引。该字段只有当值为true时才会出现(表明片键唯一的)。片键默认不是唯一的
4.config.chunks
chunks 集合记录有集合中所有块的信息。Chunks集合中的一个典型文档结构如下所示:
{ "_id" : "test.hashy-user_id_-1034308116544453153", "lastmod" : { "t" : 5000, "i" : 50 }, "lastmodEpoch" : ObjectId("50f5c648866900ccb6ed7c88"), "ns" : "test.hashy", "min" : { "user_id" : NumberLong("-1034308116544453153") }, "max" : { "user_id" : NumberLong("-732765964052501510") }, "shard" : "test-rs2" }
下面这些字段最为有用。
- _id
块的唯一标识符。该标识符通常由命名空间、片键和块的下边界值组成。
- ns
块所属的集合名称。
- in
块范围的最小值(包含)。
- max
块范围的最大值(不包含)。
- shard
块所属的分片。
这里的lastmod和lastmodEpoch字段用于记录块的版本。例如,如一个名为foo.bar-_id-1的块被拆分为两个块,原本的foo.bar-_id-1会成为一个较小的新块,我们需要一种方式来区别该块与之前的块。因此,我们用t和i字段表示块的主(major)版本和副(minor)版本:主版本会在块被迁移至新的分片时发生改变,副版本会在块被拆分时发生改变。
sh.status()获取的大部分信息都来自于config.chunks集合。
5.config.changelog
changelog集合可用于跟踪记录集群的操作,因为该集合会记录所有的拆分和迁移操作。
拆分记录的文档结构如下:
{ "_id" : "router1-2013-02-09T18:08:12-5116908cab10a03b0cd748c3", "server" : "spock-01", "clientAddr" : "10.3.1.71:62813", "time" : ISODate("2013-02-09T18:08:12.574Z"), "what" : "split", "ns" : "test.foo", "details" : { "before" : { "min" : { "x" : { $minKey : 1 }, "y" : { $minKey : 1 } }, "max" : { "x" : { $maxKey : 1 }, "y" : { $maxKey : 1 } }, "lastmod" : Timestamp(1000, 0), "lastmodEpoch" : ObjectId("000000000000000000000000") }, "left" : { "min" : { "x" : { $minKey : 1 }, "y" : { $minKey : 1 } }, "max" : { "x" : 0, "y" : 10000 }, "lastmod" : Timestamp(1000, 1), "lastmodEpoch" : ObjectId("000000000000000000000000") }, "right" : { "min" : { "x" : 0, "y" : 10000 }, "max" : { "x" : { $maxKey : 1 }, "y" : { $maxKey : 1 } }, "lastmod" : Timestamp(1000, 2), "lastmodEpoch" : ObjectId("000000000000000000000000") } } }
从details字段中可以看到文档在拆分前和拆分后的内容。
这里显示的是集合第一个块被拆分后的情景。注意,每个新块的副版本都发生了增长:新块的lastmod分别是Timestamp(1000,1)和Timestamp(1000,2).
数据迁移的操作比较复杂,每次迁移实际上会创建4个独立的changelog文档:一条是迁移开始时的状态,一条是from分片的文档,一条是to分片的文档,还有一条是迁移完成时的状态。中间的两个文档比较有参考价值,因为可从中看出每一步操作耗时多久。这样就可得知,造成迁移瓶颈的到底是磁盘、网络还是其他什么原因了。
例如,from分片的文档结构如下:
{ "_id" : "router1-2013-02-09T18:15:14-5116923271b903e42184211c", "server" : "spock-01", "clientAddr" : "10.3.1.71:27017", "time" : ISODate("2013-02-09T18:15:14.388Z"), "what" : "moveChunk.to", "ns" : "test.foo", "details" : { "min" : { "x" : 24123, "y" : -14123 }, "max" : { "x" : 39467, "y" : -29467 }, "step1 of 5" : 0, "step2 of 5" : 0, "step3 of 5" : 900, "step4 of 5" : 0, "step5 of 5" : 142 } };
details字段中的每一步表示的都是时间,stepN of 5信息以毫秒为单位,显示了步骤的耗时长短。当from分片收到mongos发来的moveChunk命令时,它会:
(1) 检査命令的参数;
(2) 向配置服务器申请获得一个分布锁,以便进入迁移过程;
(3) 尝试连接到to分片;
(4) 数据复制,这是整个过程的“临界区”(critical section);
(5) 与to分片和配置服务器一起确认迁移是否成功完成。
注意:step4 of 5中的to和from分片间进行的是直接通信:每个分片都是直接连接到另一个分片和配置服务器上,以进行迁移。如果from分片在迁移过程的最后一步出现短暂的网络连接问题,它可能会处于无法撤销迁移操作也无法继续进行下去的状态。在这种情况下,mongod会关闭。
to分片的changloe文档与from分片类似,但步骤有些许不同:
{ "_id" : "router1-2013-02-09T18:15:14-51169232ab10a03b0cd748e5", "server" : "spock-01", "clientAddr" : "10.3.1.71:62813", "time" : ISODate("2013-02-09T18:15:14.391Z"), "what" : "moveChunk.from", "ns" : "test.foo", "details" : { "min" : { "x" : 24123, "y" : -14123 }, "max" : { "x" : 39467, "y" : -29467 }, "step1 of 6" : 0, "step2 of 6" : 2, "step3 of 6" : 33, "step4 of 6" : 1032, "step5 of 6" : 12, "step6 of 6" : 0 } }
当to分片收到from分片发来的命令时,它会执行如下操作。
(1) 迁移索引。如果该分片不包含任何来自迁移集合的块,则需知道有哪些字段上建立过索引。如果在此之前to分片已有来自于该集合的块,则可忽略此步骤。
(2) 刪除块范围内已存在的任何数据。之前失败的迁移(如果有的话)可能会留有数据残余,或者是正处于恢复过程当中,此时我们不希望残留数据与新数据混杂在一起。
(3) 将块中的所有文档复制到to分片。
(4) 复制期间,在to分片上重新执行曾在这些文档上执行过的操作。
(5) 等待to分片将新迁移过来的数据复制到集群的大多数服务器上。
(6) 修改块的元数据以完成迁移过程,表明数据已被成功迁移到to分片上。
6.config.tags
该集合的创建是在为系统配置分片标签时发生的。每个标签都与一个块范围相关联:
> db.tags.find() { "_id" : { "ns" : "test.ips", "min" : {"ip" : "056.000.000.000"} }, "ns" : "test.ips", "min" : {"ip" : "056.000.000.000"}, "max" : {"ip" : "057.000.000.000"}, "tag" : "USPS" } { "_id" : { "ns" : "test.ips", "min" : {"ip" : "017.000.000.000"} }, "ns" : "test.ips", "min" : {"ip" : "017.000.000.000"}, "max" : {"ip" : "018.000.000.000"}, "tag" : "Apple" }
7.config.settings
该集合含有当前的均衡器设置和块大小的文档信息。通过修改该集合的文档,可开启或关闭均衡器,也可以修改块的大小。注意,应总是连接到mongos修改该集合的值,而不应直接连接到配置服务器进行修改。
2.查看网络连接
集群的各组成部分间存在大量的连接。本节将学习与分片相关的连接信息。
2.1 查看连接统计
可使用connPoolStats命令,査看mongos和mongod之间的连接信息,并可得知服务器上打开的所有连接:
> db.adminCommand({"connPoolStats" : 1}) { "createdByType": { "sync": 857, "set": 4 }, "numDBClientConnection": 35, "numAScopedConnection": 0, "hosts": { "config-01:10005,config-02:10005,config-03:10005": { "created": 857, "available": 2 }, "spock/spock-01:10005,spock-02:10005,spock-03:10005": { "created": 4, "available": 1 } }, "totalAvailable": 3, "totalCreated": 861, "ok": 1 }
形如"host1,host2,host3"的主机名是来自配置服务器的连接,也就是用于“同步”的连接。形如"name/host1,host2,...,hostN"的主机是来自分片的连接。available的值表明当前实例的连接池中有多少可用连接。
注意:只有在分片内的mongos和mongod上运行这个命令才会有效。
在一个分片上执行connPoolStats,输出信息中可看到该分片与其他分片间的连接,包括连接到其他分片做数据迁移的连接。分片的主连接会直接连接到另一分片的主连接上,然后从目标分片吸取数据。
进行迁移时,分片会建立一个ReplicaSetMonitor(该进程用于监控副本集的健康状况),用于追踪记录迁移另一端分片的健康状况。由于mongod不会销毁这个监控器,所以有时会在一个副本集的日志中看到其他副本集成员的信息。这是很正常的,不会对应用程序造成任何影响。
2.2 限制连接数量
当有客户端连接到mongos时,mongos会创建一个连接,该连接应至少连接到一个分片上,以便将客户端请求发送给分片。因此,每个连接到mongos的客户端连接都会至少产生一个从mongos到分片的连接。
如果有多个mongos进程,可能会创建出非常多的连接,甚至超出分片的处理能力:一个mongos最多允许20 000个连接(mongod也是如此)。如果有5个mongos进程,每个mongos有10 000个客户端连接,那么这些mongos可能会试图创建50 000个到分片的连接!
为防止这种情况的发生,可在mongos的命令行配置中使用maxConns选项,这样可以限制mongos能够创建的连接数量。可使用下列公式计算分片能够处理的来自单一mongos的连接数量:
maxConns=20 000 - (mongos进程的数量x3)-(毎个副本集的成员数量x3) -(其他/mongos进程的数量)
以下为公式的相关说明。
- (mongos进程的数量x 3)
每个mongos会为每个mongod创建3个连接:一个用于转发客户端请求,一个用于追踪错误信息,即写回监听器(writeback listener),—个用于监控副本集状态。
- (每个副本集的成员数量x 3)
主节点会与每个备份节点创建一个连接,而每个备份节点会与主节点创建两个连接,因此总共是3个连接。
- (其他/mongos进程的数量)
这里的其他指其他可能连接到mongod的进程数量,这种连接包括MMS代理、shell的直接连接(管理员用),或者是迁移时连接到其他分片的连接。
注意:maxConns只会阻止mongos创建多于maxConns数量的连接,但并不会帮助处理连接耗尽的问题。连接耗尽时,请求会发生阻塞,等待某些连接被释放。因此,必须防止应用程序使用超过maxConns数量的连接,尤其是在mongos进程数量不断增加时。
MongoDB实例在安全退出时,会在终止运行之前关闭所有连接。已经连接到MongoDB的成员会立即收到套接字错误(socket error),并能够重新刷新连接。但是,如果MongoDB实例由于断电、崩溃或者网络问题突然离线,那些已经打开的套接字很可能没有被关闭。在这种情况下,集群内的其他服务器很可能会认为这个MongoDB实例仍在有效运转,但是当试图在该MongoDB实例上执行操作时,就会遇到错误,继而刷新连接(如果此时该MongoDB实例再次上线且运转正常的话)。
连接数量较少时,可快速检测到某台MongoDB实例是否已离线。但是,当有成千上万个连接时,每个连接都需要经历被尝试、检测失败,并重新建立连接的过程,此过程中会得到大量的错误。在出现大量重新连接时,除了重启进程,没有其他特殊有效的方法。
3.服务器管理
随着集群的增长,我们可能需要增加集群容量或者是修改集群配置。本节将讲解向集群添加服务器以及从集群删除服务器的方法。
3.1 添加服务器
可随时向集群中添加新的mongos。只要保证在mongos的--configdb选项中指定了一组正确的配置服务,mongos即可立即与客户端建立连接。
可使用addShard命令,向集群添加新分片。
3.2 修改分片的服务器
使用分片集群时,我们可能会希望修改某单独分片的服务器。要修改分片的成员,需直接连接到分片的主服务器上(而不是通过mongos),然后对副本集进行重新配置。集群配置会自动检测更改,并将其更新到config.shards上。不要手动修改config.shards。
只有在使用单机服务器作为分片,而不是使用副本集作为分片时,才需手动修改config.shards。
将单机服务器分片修改为副本集分片
最简单的方式是添加一个新的空副本集分片,然后移除单机服务器分片
如果希望把单机服务器分片转换为副本集分片,过程会复杂得多,而且需要停机。
(1) 停止向系统发送请求。
(2) 关闭单机服务器(这里称其为server-1)和所有的mongos进程。
(3) 以副本集模式重启server-1 (使用--replSet选项)。
(4) 连接到server-1,将其作为一个单成员副本集进行初始化。
(5) 连接到配置服务器,替换该分片的入口,在config.shards中将分片名称替换为 setName/server-1:27017的形式^确保三个配置服务器都拥有相同的配置信息。手动修改配置服务器是有风险的!
可在每个配置服务器上执行dbhash命令,以确保配置信息相同:
> db.runCommand({"dbhash" : 1})
这样可以得到每个集合的MD5散列值。不同配置服务器上,config数据库的集合可能会有所不同,但config.shards应始终保持一致。
(6) 重启所有mongos进程。它们会在启动时从配置服务器读取分片数据,然后将副本集当作分片对待。
(7) 重启所有分片的主服务器,刷新其配置数据。
(8) 再次向系统发送请求。
(9) 向server-1副本集中添加新成员。
这一过程非常复杂,而且很容易出错,因此不建议使用。应尽可能地将空的副本集作为新的分片添加到集群中,数据迁移的事情交给集群去做就好了。
3.3 删除分片
通常来说,不应从集群中删除分片。如果经常在集群中添加和删除分片,会给系统带来很多不必要的压力。如果向集群中添加了过多的分片,最好是什么也不做,系统早晚会用到这些分片,而不应该将多余的分片删掉,等以后需要的时候再将其重新添加到集群中。不过,在必要的情况下,是可以删除分片的。
首先保证均衡器是开启的。在排出数据(draining)的过程中,均衡器会负责将待删除分片的数据迁移至其他分片。执行removeShard命令,开始排出数据。removeShard将待删除分片的名称作为参数,然后将该分片上的所有块都移至其他分片上:
> db.adminCommand({"removeShard" : "test-rs3"}) { "msg" : "draining started successfully", "state" : "started", "shard" : "test-rs3", "note" : "you need to drop or movePrimary these databases", "dbsToMove" : [ "blog", "music", "prod" ], "ok" : 1 }
如果分片上的块较多,或者有较大的块需要移动,排出数据的过程可能会耗时更长。 如果存在特大块(jumbo chunk),可能需临时提高其他分片的块大小,以便能够将特大块迁移到其他分片。
如需査看哪些块已完成迁移,可再次执行removeShard命令,查看当前状态:
> db.adminCommand({"removeShard" : "test-rs3"}) { "msg" : "draining ongoing", "state" : "ongoing", "remaining" : { "chunks" : NumberLong(5), "dbs" : NumberLong(0) }, "ok" : 1 }
在一个处于排出数据过程的分片上,可执行removeShard任意多次。
块在移动前可能需要被拆分,所以有可能会看到系统中的块数量在排出数据时发生了增长。假设有一个拥有5个分片的集群,块的分布如下:
test-rs0 10 test-rs1 10 test-rs2 10 test-rs3 11 test-rs4 11
该集群共有52个块。如果删除test-rs3分片,最终的结果可能会是:
test-rs0 15 test-rs1 15 test-rs2 15 test-rs4 15
集群现在拥有60个块,其中18个来自test-rs3分片(原本有11个,还有7个是在 排出数据的过程中创建的)。
所有的块都完成迁移后,如果仍有数据库将该分片作为主分片,需在删除分片前将这些数据库移除掉。removeShard命令的输出结果可能如下:
> db.adminCommand({"removeShard" : "test-rs3"}) { "msg" : "draining ongoing", "state" : "ongoing", "remaining" : { "chunks" : NumberLong(0), "dbs" : NumberLong(3) }, "note" : "you need to drop or movePrimary these databases", "dbsToMove" : [ "blog", "music", "prod" ], "ok" : 1 }
为完成分片的删除,需先使用movePrimary命令将这些数据库移走:
> db.adminCommand({"movePrimary" : "blog", "to" : "test-rs4"}) { "primary " : "test-rs4:test-rs4/ubuntu:31500,ubuntu:31501,ubuntu:31502", "ok" : 1 }
然后再次执行removeShard命令:
> db.adminCommand({"removeShard" : "test-rs3"}) { "msg" : "removeshard completed successfully", "state" : "completed", "shard" : "test-rs3", "ok" : 1 }
最后一步不是必需的,但可确保已确实完成了分片的删除。如果不存在将该分片作为主分片的数据库,则块的迁移完成后,即可看到分片删除成功的输出信息。
注意,如果分片开始排出数据,就没有内置办法停止这一过程了。
3.4 修改配置服务器
修改配置服务器是非常困难的,而且有风险,通常还需要停机。注意,修改配置服务器前,应做好备份。
在运行期间,所有mongos进程的--configdb选项值都必须相同。因此,要修改配置服务器,首先必须关闭所有的mongos进程(mongos进程在使用旧的--configdb参数时,无法继续保持运行状态),然后使用新的--configdb参数重启所有mongos进程。
例如,将一台配置服务器增至三台是最常见的任务之一。为实现此操作,首先应关闭所有的mongos进程、配置服务器,以及所有的分片。然后将配置服务器的数据目录复制到两台新的配置服务器上(这样三台配置服务器就可以拥有完全相同的数据目录)。接着,启动这三台配置服务器和所有分片。然后,将--configdb选项指定为这三台配置服务器,最后重启所有的mongos进程。
4.数据均衡
通常来说,MongoDB会自动处理数据均衡。本节将学习如何启用和禁用自动均衡,以及如何人为干涉均衡过程。
4.1 均衡器
在执行几乎所有的数据库管理操作之前,都应先关闭均衡器。可使用下列shell辅助函数关闭均衡器:
> sh.setBalancerState(false)
均衡器关闭后,系统则不会再进入均衡过程,但该命令并不能立即终止进行中的均衡过程:迁移过程通常无法立即停止。因此,应检査config.locks集合,以查看均衡过程是否仍在进行中:
>db.locks.find({"_id" : "balancer"})["state"] 0
此处的0表明均衡器已被关闭。
均衡过程会增加系统负载:目标分片必须査询源分片块中的所有文档,将文档插入目标分片的块中,源分片最后必须删除这些文档。在以下两种特殊情况下,迁移会导致性能问题。
(1) 使用热点片键可保证定期迁移(因为所有的新块都是创建在热点上的)。系统必须有能力处理源源不断写入到热点分片上的数据。
(2) 向集群中添加新的分片时,均衡器会试图为该分片写入数据,从而触发一系列的迁移过程。
如果发现数据迁移过程影响了应用程序性能,可在config.settings集合中为数据均 衡指定一个时间窗口。执行下列更新语句,均衡则只会在下午1点到4点间发生:
> db.settings.update({"_id" : "balancer"}, ... {"$set" : {"activeWindow" : {"start" : "13:00", "stop" : "16:00"}}}, ... true )
如指定了均衡时间窗,则应对其进行严密监控,以确保mongos确实只在指定的时间内做均衡。
如需混用手动均衡和自动均衡,必须格外小心。因为自动均衡器总是根据数据集的当前状态来决定数据迁移,而不考虑数据集的历史状态。例如,假设有两个分片shardA和shardB,每个分片都有500个块。由于shardA上的写请求比较多,因此我们关闭了均衡器,从最活跃的块中取出30个移至shardB。此时如再启用均衡器,则会立即将30个块(很可能不是刚刚的30块)从shardB移至shardA,以均衡两个分片拥有的块数量。
为防止这种情况发生,可在启用均衡器之前从shardB选取30个不活跃的块移至 shardA。这样两个分片间就不会存在不均衡,均衡器也不会进行数据块的移动了。另外,也可在shardA上拆分出一些块,以实现shardA和shardB的均衡。
注意:均衡器只使用块的数量,而非数据大小,作为衡量分片间是否均衡的指标。 因此,如果A分片只拥有几个较大的数据块,而B分片拥有许多较小的块(但总数据大小比A小),那么均衡器会将B分片的一些块移至A分片,从而实现均衡。
4.2 修改块大小
块中的文档数量可能为0,也可能多达数百万。通常情况下,块越大,迁移至分片的耗时就越长。有案例使用的是1 MB的块,所以块移动起来非常容易与迅速。但在实际系统中,这通常是不现实的。MongoDB需要做大量非必要的工作,才能将分片大小维持在几MB以内。块的大小默认为64 MB,这个大小的块既易于迁移,又不会导致过多的流失。
有时可能会发现移动64 MB的块耗时过长。可通过减小块的大小,提高迁移速度。使用shell连接到mongos,然后修改config.settings集合,从而完成块大小的修改:
> db.settings.findOne() { "_id" : "chunksize", "value" : 64 } > db.settings.save({"_id" : "chunksize", "value" : 32})
以上修改操作将块的大小减至32 MB。已经存在的块不会立即发生改变,执行块拆分操作时,这些块即可拆分成32 MB大小。mongos进程会自动加载新的块大小。
注意:该设置的有效范围是整个集群:它会影响所有集合和数据库。因此,如需对一个集合使用较小的块,而对另一集合使用较大的块,比较好的解决方式是取一个折中的值(或者将这两个集合放在不同的集群中)。
如果MongoDB频繁进行数据迁移或文档较大,则可能需要增加块的大小。
4.3 移动块
如前所述,同一块内的所有数据都位于同一分片上。如该分片的块数量比其他分片多,则MongoDB会将其中的一部分块移至其他块数量较少的分片上。移动块的过程叫做迁移(migration), MongoDB就是这样在集群中实现数据均衡的。
可在shell中使用moveChunk辅助函数,手动移动块:
> sh.moveChunk("test.users", {"user_id" : NumberLong("1844674407370955160")}, ... "spock") { "millis" : 4079, "ok" : 1 }
以上命令会将包含文档user_id为1844674407370955160的块移至名为spock的分片上。必须使用片键来找出所需移动的块(本例中的片键是user_id)。通常,指定一个块最简单的方式是指定它的下边界,不过指定块范围内的任何值都可以(块的上边界值除外,因为其并不包含在块范围内)。该命令在块移动完成后才会返回,因此需一定耗时才能看到输出信息。
如某个操作耗时较长,可在日志中详细査看问题所在。如某个块的大小超出了系统指定的最大值,mongos则会拒绝移动这个块:
> sh.moveChunk("test.users", {"user_id" : NumberLong("1844674407370955160")}, ... "spock") { "cause" : { "chunkTooBig" : true, "estimatedChunkSize" : 2214960, "ok" : 0, "errmsg" : "chunk too big to move" }, "ok" : 0, "errmsg" : "move failed" }
本例中,移动这个块之前,必须先手动拆分这个块。可使用splitAt命令对块进行拆分:
> db.chunks.find({"ns" : "test.users", ... "min.user_id" : NumberLong("1844674407370955160")}) { "_id" : "test.users-user_id_NumberLong(\"1844674407370955160\")", "ns" : "test.users", "min" : { "user_id" : NumberLong("1844674407370955160") }, "max" : { "user_id" : NumberLong("2103288923412120952") }, "shard" : "test-rs2" } > sh.splitAt("test.ips", {"user_id" : NumberLong("2000000000000000000")}) { "ok" : 1 } > db.chunks.find({"ns" : "test.users", ... "min.user_id" : {"$gt" : NumberLong("1844674407370955160")}, ... "max.user_id" : {"$lt" : NumberLong("2103288923412120952")}}) { "_id" : "test.users-user_id_NumberLong(\"1844674407370955160\")", "ns" : "test.users", "min" : { "user_id" : NumberLong("1844674407370955160") }, "max" : { "user_id" : NumberLong("2000000000000000000") }, "shard" : "test-rs2" } { "_id" : "test.users-user_id_NumberLong(\"2000000000000000000\")", "ns" : "test.users", "min" : { "user_id" : NumberLong("2000000000000000000") }, "max" : { "user_id" : NumberLong("2103288923412120952") }, "shard" : "test-rs2" }
块被拆分为较小的块后,就可以被移动了。也可以调高最大块的大小,然后再移动这个较大的块。不过应尽可能地将大块拆分为小块。不过有时有些块无法被拆分,这些块被称作特大块。
4.4 特大块
假设使用date字段作为片键。集合中的date字段是一个日期字符串,格式为year/month/day,也就是说,mongos—天最多只能创建一个块。最初的一段时间内一切正常,直到有一天,应用程序的业务量突然出现病毒式增长,流量比平常大了上千倍!
这一天的块要比其他日期的大得多,但由于块内所有文档的片键值都是一样的,因此这个块是不可拆分的。
如果块的大小超出了config.settings中设置的最大块大小,那么均衡器就无法移动 这个块了。这种不可拆分和移动的块就叫做特大块,这种块相当难对付。
举例来说,假如有3个分片shard1、shard2和shard3。如果使用热点片键模式,假设shard1是热点片键,则所有写请求都会被分发到shard1上。mongos会试图将块均衡地分发在这些分片上。但是,均衡器只能移动非特大块,因此它只会将所有较小块从热点分片迁移到其他分片。
现在,所有分片上的块数基本相同,但shard2和shard3上的所有块都小于64MB。如shard1上出现了特大块,则shard1上会有越来越多的块大于64MB。这样,即使三个分片的块数非常均衡,但shard1会比另两个分片更早被填满。
出现特大块的表现之一是,某分片的大小增长速度要比其他分片快得多。也可使用sh.status()来检查是否出现了特大块:特大块会存在一个jumbo属性。
> sh.status() ... { "x" : -7 } -->> { "x" : 5 } on : shard0001 { "x" : 5 } -->> { "x" : 6 } on : shard0001 jumbo { "x" : 6 } -->> { "x" : 7 } on : shard0001 jumbo { "x" : 7 } -->> { "x" : 339 } on : shard0001 ...
可使用dataSize命令检查块大小。
首先,使用config.chunks集合,查看块范围:
> use config > var chunks = db.chunks.find({"ns" : "acme.analytics"}).toArray()
然后根据这些块范围,找出可能的特大块:
> use dbName > db.runCommand({"dataSize" : "dbName.collName", ... "keyPattern" : {"date" : 1}, // shard key ... "min" : chunks[0].min, ... "max" : chunks[0].max}) { "size" : 11270888, "numObjects" : 128081, "millis" : 100, "ok" : 1 }
但要小心,因为dataSize命令要扫描整个块的数据才能知道块的大小。因此如果可能,应首先根据自己对数据的了解,尽可能缩小搜索范围:特大块是在特定日期出现的吗?例如,如果11月1号的时候系统非常繁忙,则可尝试检查这一天创建的块的片键范围。如使用了GridFS,而且是依据files_id字段进行分片的,则可通过fs.files集合查看文件大小。
1.分发特大块
为修复由特大块引发的集群不均衡,就必须将特大块均衡地分发到其他分片上。
这是一个非常复杂的手动过程,而且不应引起停机(可能会导致系统变慢,因为要迁移大量的数据)。接下来,我们以from分片来指代拥有特大块的分片,以to分片来指代特大块即将移至的目标分片。注意,如有多个from分片,则需对每个from分片重复下列步骤:
(1) 关闭均衡器,以防其在这一过程中出来捣乱:
> sh.setBalancerState(false)
(2) MongoDB不允许移动大小超出最大块大小设定值的块,所以需临时调高最大块大小的设定值。记下特大块的大小,然后将最大块大小设定值调整为比特大块大一些的数值,比如10000。块大小的单位是MB:
> use config > db.settings.findOne({"_id" : "chunksize"}) { "_id" : "chunksize", "value" : 64 } > db.settings.save({"_id" : "chunksize", "value" : 10000})
(3) 使用moveChunk命令将特大块从from分片移至to分片。如担心迁移会对应用程序的性能造成影响,可使用secondaryThrootle选项,放慢迁移的过程,减缓对系统性能的影响:
> db.adminCommand({"moveChunk" : "acme.analytics", ... "find" : {"date" : new Date("10/23/2012")}, ... "to" : "shard0002", ... "secondaryThrottle" : true})
secondaryThrottle会强制要求迁移过程间歇进行,每迁移完一些数据,需等待集群中的大多数分片成功完成数据复制后再进行下一次迁移。该选项只有在使用副本集分片时才会生效。如使用单机服务器分片,则该选项不会生效。
(4) 使用splitChunk命令对from分片剩余的块进行拆分,这样可以增加from分片的块数,直到实现from分片与其他分片块数的均衡。
(5) 将块大小修改回最初值:
> db.settings.save({"_id" : "chunksize", "value" : 64})
(6) 启用均衡器。
> sh.setBalancerState(true)
均衡器被再次启用后,仍旧不能移动特大块,不过此时那些特大块都已位于合适的位置了。
2.防止出现特大块
随着存储数据量的增长,上一节提到的手动过程变得不再可行。因此,如在特大块方面存在问题,应首先想办法避免特大块的出现。
为防止特大块的出现,可修改片键,细化片键的粒度。应尽可能保证每个文档都拥有唯一的片键值,或至少不要出现某个片键值的数据块超出最大块大小设定值的情况。
例如,如使用前面所述的年/月/日片键,可通过添加时、分、秒来细化片键粒度。类似地,如使用粒度较大的片键,如日志级別,则可添加一个粒度较细的字段作为片键的第二个字段,如MD5散列值或UDID。这样一来,即使有许多文档片键的第一个字段值是相同的,也可一直对块进行拆分,也就防止了特大块的出现。
4.5 刷新配置
最后一点,mongos有时无法从配置服务器正确更新配置。如发现配置有误,mongos的配置过旧或无法找到应有数据,可使用flushRouterConfig命令手动刷新所有缓存:
> db.adminCommand({"flushRouterConfig" : 1})
如flushRouterConfig命令没能解决问题,则应重启所有的mongos或mongod 进程,以便清除所有可能的缓存。