第16章 分片管理

对数据库管理员来说,分片集群是最困难的部署类型。本章我们将学习在集群上执行管理任务的方方面面,内容包括:

  • 检查集群状态:集群有哪些成员?数据保存在哪里?哪些连接是打开的?
  • 如何添加、删除和修改集群的成员;
  • 管理数据移动和手动移动数据 。

16.1 检查集群状态

有一些辅助函数可用于找出数据保存位置、所在分片,以及集群正在进行的操作。

16.1.1 使用sh.status查看集群摘要信息

使用sh.status()可查看分片、数据库和分片集合的摘要信息。如果块的数量较少,则该命令会打印出每个块的保存位置。否则它只会简单地给出集合的片键,以及每个分片的块数:

  1. > sh.status()
  2. --- Sharding Status ---
  3. sharding version: { "_id" : 1, "version" : 3 }
  4. shards:
  5. { "_id" : "shard0000", "host" : "localhost:30000",
  6. "tags" : [ "USPS" , "Apple" ] }
  7. { "_id" : "shard0001", "host" : "localhost:30001" }
  8. { "_id" : "shard0002", "host" : "localhost:30002", "tags" : [ "Apple" ] }
  9. databases:
  10. { "_id" : "admin", "partitioned" : false, "primary" : "config" }
  11. { "_id" : "test", "partitioned" : true, "primary" : "shard0001" }
  12. test.foo
  13. shard key: { "x" : 1, "y" : 1 }
  14. chunks:
  15. shard0000 4
  16. shard0002 4
  17. shard0001 4
  18. { "x" : { $minKey : 1 }, "y" : { $minKey : 1 } } -->>
  19. { "x" : 0, "y" : 10000 } on : shard0000
  20. { "x" : 0, "y" : 10000 } -->> { "x" : 12208, "y" : -2208 }
  21. on : shard0002
  22. { "x" : 12208, "y" : -2208 } -->> { "x" : 24123, "y" : -14123 }
  23. on : shard0000
  24. { "x" : 24123, "y" : -14123 } -->> { "x" : 39467, "y" : -29467 }
  25. on : shard0002
  26. { "x" : 39467, "y" : -29467 } -->> { "x" : 51382, "y" : -41382 }
  27. on : shard0000
  28. { "x" : 51382, "y" : -41382 } -->> { "x" : 64897, "y" : -54897 }
  29. on : shard0002
  30. { "x" : 64897, "y" : -54897 } -->> { "x" : 76812, "y" : -66812 }
  31. on : shard0000
  32. { "x" : 76812, "y" : -66812 } -->> { "x" : 92793, "y" : -82793 }
  33. on : shard0002
  34. { "x" : 92793, "y" : -82793 } -->> { "x" : 119599, "y" : -109599 }
  35. on : shard0001
  36. { "x" : 119599, "y" : -109599 } -->> { "x" : 147099, "y" : -137099 }
  37. on : shard0001
  38. { "x" : 147099, "y" : -137099 } -->> { "x" : 173932, "y" : -163932 }
  39. on : shard0001
  40. { "x" : 173932, "y" : -163932 } -->>
  41. { "x" : { $maxKey : 1 }, "y" : { $maxKey : 1 } } on : shard0001
  42. test.ips
  43. shard key: { "ip" : 1 }
  44. chunks:
  45. shard0000 2
  46. shard0002 3
  47. shard0001 3
  48. { "ip" : { $minKey : 1 } } -->> { "ip" : "002.075.101.096" }
  49. on : shard0000
  50. { "ip" : "002.075.101.096" } -->> { "ip" : "022.089.076.022" }
  51. on : shard0002
  52. { "ip" : "022.089.076.022" } -->> { "ip" : "038.041.058.074" }
  53. on : shard0002
  54. { "ip" : "038.041.058.074" } -->> { "ip" : "055.081.104.118" }
  55. on : shard0002
  56. { "ip" : "055.081.104.118" } -->> { "ip" : "072.034.009.012" }
  57. on : shard0000
  58. { "ip" : "072.034.009.012" } -->> { "ip" : "090.118.120.031" }
  59. on : shard0001
  60. { "ip" : "090.118.120.031" } -->> { "ip" : "127.126.116.125" }
  61. on : shard0001
  62. { "ip" : "127.126.116.125" } -->> { "ip" : { $maxKey : 1 } }
  63. on : shard0001
  64. tag: Apple { "ip" : "017.000.000.000" } -->> { "ip" : "018.000.000.000" }
  65. tag: USPS { "ip" : "056.000.000.000" } -->> { "ip" : "057.000.000.000" }
  66. { "_id" : "test2", "partitioned" : false, "primary" : "shard0002" }

块的数量较多时,sh.status()命令会概述块的状态,而非打印出每个块的相关信息。如需查看所有的块,可使用sh.status(true)命令(true参数要求sh.status()命令打印出尽可能详尽的信息)。

sh.status()显示的所有信息都来自config数据库。

运行sh.status()命令,使MapReduce获取这一数据,因此,如果启动数据库时指定了--noscripting选项,则无法运行sh.status()命令。

16.1.2 检查配置信息

集群相关的所有配置信息都保存在配置服务器上config数据库的集合中。可直接访问该数据库,不过shell提供了一些辅助函数,并通过这些函数获取更适于阅读的信息。不过,可始终通过直接查询config数据库的方式,获取集群的元数据。

第16章 分片管理 - 图1永远不要直接连接到配置服务器,以防配置服务器数据被不小心修改或删除。应先连接到mongos,然后通过config数据库来查询相关信息,方法与查询其他数据库一样:

  1. mongos> use config

如果通过mongos操作配置数据(而不是直接连接到配置服务器),mongos会保证将修改同步到所有配置服务器,也会防止危险操作的发生,如意外删除config数据库等。

总的来说,不应直接修改config数据库的任何数据(例外情况下面会提到)。如果确实修改了某些数据,通常需要重启所有的mongos服务器,才能看到效果。

config数据库中有一些集合,本节将介绍这些集合的内容和使用方法。

1. config.shards

shards集合跟踪记录集群内所有分片的信息。shards集合中的一个典型文档结构如下:

  1. > db.shards.findOne()
  2. {
  3. "_id" : "spock",
  4. "host" : "spock/server-1:27017,server-2:27017,server-3:27017",
  5. "tags" : [
  6. "us-east",
  7. "64gb mem",
  8. "cpu3"
  9. ]
  10. }

分片的"_id"来自于副本集的名称,所以集群中的每个副本集名称都必须是唯一的。

更新副本集配置的时候(比如添加或删除成员),host字段会自动更新。

2. onfig.databases

databases集合跟踪记录集群中所有数据库的信息,不管数据库有没有被分片:

  1. > db.databases.find()
  2. { "_id" : "admin", "partitioned" : false, "primary" : "config" }
  3. { "_id" : "test1", "partitioned" : true, "primary" : "spock" }
  4. { "_id" : "test2", "partitioned" : false, "primary" : "bones" }

如果在数据库上执行过enableSharding,则此处的"partitioned"字段值就是true"primary"是“主数据库”(home base)。数据库的所有新集合均默认被创建在数据库的主分片上。

3. config.collections

collections集合跟踪记录所有分片集合的信息(非分片集合信息除外)。其中的文档结构如下:

  1. > db.collections.findOne()
  2. {
  3. "_id" : "test.foo",
  4. "lastmod" : ISODate("1970-01-16T17:53:52.934Z"),
  5. "dropped" : false,
  6. "key" : { "x" : 1, "y" : 1 },
  7. "unique" : true
  8. }

下面是一些重要字段。

  • _id

集合的命名空间。

  • key

片键。本例中指由xy组成的复合片键。

  • unique

    表明片键是一个唯一索引。该字段只有当值为true时才会出现(表明片键是唯一的)。片键默认不是唯一的。

4. config.chunks

chunks集合记录有集合中所有块的信息。chunks集合中的一个典型文档结构如下所示:

  1. {
  2. "_id" : "test.hashy-user_id_-1034308116544453153",
  3. "lastmod" : { "t" : 5000, "i" : 50 },
  4. "lastmodEpoch" : ObjectId("50f5c648866900ccb6ed7c88"),
  5. "ns" : "test.hashy",
  6. "min" : { "user_id" : NumberLong("-1034308116544453153") },
  7. "max" : { "user_id" : NumberLong("-732765964052501510") },
  8. "shard" : "test-rs2"
  9. }

下面这些字段最为有用。

  • _id

    块的唯一标识符。该标识符通常由命名空间、片键和块的下边界值组成。

  • ns

    块所属的集合名称。

  • min

    块范围的最小值(包含)。

  • max

    块范围的最大值(不包含)。

  • shard

    块所属的分片。

这里的lastmodlastmodEpoch字段用于记录块的版本。例如,如一个名为foo.bar-_id-1的块被拆分为两个块,原本的foo.bar-_id-1会成为一个较小的新块,我们需要一种方式来区别该块与之前的块。因此,我们用ti字段表示块的(major)版本和(minor)版本:主版本会在块被迁移至新的分片时发生改变,副版本会在块被拆分时发生改变。

sh.status()获取的大部分信息都来自于config.chunks集合。

5. config.changelog

changelog集合可用于跟踪记录集群的操作,因为该集合会记录所有的拆分和迁移操作。

拆分记录的文档结构如下:

  1. {
  2. "_id" : "router1-2013-02-09T18:08:12-5116908cab10a03b0cd748c3",
  3. "server" : "spock-01",
  4. "clientAddr" : "10.3.1.71:62813",
  5. "time" : ISODate("2013-02-09T18:08:12.574Z"),
  6. "what" : "split",
  7. "ns" : "test.foo",
  8. "details" : {
  9. "before" : {
  10. "min" : { "x" : { $minKey : 1 }, "y" : { $minKey : 1 } },
  11. "max" : { "x" : { $maxKey : 1 }, "y" : { $maxKey : 1 } },
  12. "lastmod" : Timestamp(1000, 0),
  13. "lastmodEpoch" : ObjectId("000000000000000000000000")
  14. },
  15. "left" : {
  16. "min" : { "x" : { $minKey : 1 }, "y" : { $minKey : 1 } },
  17. "max" : { "x" : 0, "y" : 10000 },
  18. "lastmod" : Timestamp(1000, 1),
  19. "lastmodEpoch" : ObjectId("000000000000000000000000")
  20. },
  21. "right" : {
  22. "min" : { "x" : 0, "y" : 10000 },
  23. "max" : { "x" : { $maxKey : 1 }, "y" : { $maxKey : 1 } },
  24. "lastmod" : Timestamp(1000, 2),
  25. "lastmodEpoch" : ObjectId("000000000000000000000000")
  26. }
  27. }
  28. }

details字段中可以看到文档在拆分前和拆分后的内容。

这里显示的是集合第一个块被拆分后的情景。注意,每个新块的副版本都发生了增长:新块的lastmod分别是Timestamp(1000,1)Timestamp(1000,2).

数据迁移的操作比较复杂,每次迁移实际上会创建4个独立的changelog文档:一条是迁移开始时的状态,一条是from分片的文档,一条是to分片的文档,还有一条是迁移完成时的状态。中间的两个文档比较有参考价值,因为可从中看出每一步操作耗时多久。这样就可得知,造成迁移瓶颈的到底是磁盘、网络还是其他什么原因了。

例如,from分片的文档结构如下:

  1. {
  2. "_id" : "router1-2013-02-09T18:15:14-5116923271b903e42184211c",
  3. "server" : "spock-01",
  4. "clientAddr" : "10.3.1.71:27017",
  5. "time" : ISODate("2013-02-09T18:15:14.388Z"),
  6. "what" : "moveChunk.to",
  7. "ns" : "test.foo",
  8. "details" : {
  9. "min" : { "x" : 24123, "y" : -14123 },
  10. "max" : { "x" : 39467, "y" : -29467 },
  11. "step1 of 5" : 0,
  12. "step2 of 5" : 0,
  13. "step3 of 5" : 900,
  14. "step4 of 5" : 0,
  15. "step5 of 5" : 142
  16. }
  17. };

details字段中的每一步表示的都是时间,stepN of 5信息以毫秒为单位,显示了步骤的耗时长短。当from分片收到mongos发来的moveChunk命令时,它会:

  • 检查命令的参数;
  • 向配置服务器申请获得一个分布锁,以便进入迁移过程;
  • 尝试连接到to分片;
  • 数据复制,这是整个过程的“临界区”(critical section);
  • to分片和配置服务器一起确认迁移是否成功完成。

注意,step4 of 5中的tofrom分片间进行的是直接通信:每个分片都是直接连接到另一个分片和配置服务器上,以进行迁移。如果from分片在迁移过程的最后一步出现短暂的网络连接问题,它可能会处于无法撤销迁移操作也无法继续进行下去的状态。在这种情况下,mongod会关闭。

to分片的changloe文档与from分片类似,但步骤有些许不同:

  1. {
  2. "_id" : "router1-2013-02-09T18:15:14-51169232ab10a03b0cd748e5",
  3. "server" : "spock-01",
  4. "clientAddr" : "10.3.1.71:62813",
  5. "time" : ISODate("2013-02-09T18:15:14.391Z"),
  6. "what" : "moveChunk.from",
  7. "ns" : "test.foo",
  8. "details" : {
  9. "min" : { "x" : 24123, "y" : -14123 },
  10. "max" : { "x" : 39467, "y" : -29467 },
  11. "step1 of 6" : 0,
  12. "step2 of 6" : 2,
  13. "step3 of 6" : 33,
  14. "step4 of 6" : 1032,
  15. "step5 of 6" : 12,
  16. "step6 of 6" : 0
  17. }
  18. }

to分片收到from分片发来的命令时,它会执行如下操作。

  • 迁移索引。如果该分片不包含任何来自迁移集合的块,则需知道有哪些字段上建立过索引。如果在此之前to分片已有来自于该集合的块,则可忽略此步骤。
  • 删除块范围内已存在的任何数据。之前失败的迁移(如果有的话)可能会留有数据残余,或者是正处于恢复过程当中,此时我们不希望残留数据与新数据混杂在一起。
  • 将块中的所有文档复制到to分片。
  • 复制期间,在to分片上重新执行曾在这些文档上执行过的操作。
  • 等待to分片将新迁移过来的数据复制到集群的大多数服务器上。
  • 修改块的元数据以完成迁移过程,表明数据已被成功迁移到to分片上 。

6. config.tags

该集合的创建是在为系统配置分片标签时发生的。每个标签都与一个块范围相关联:

  1. > db.tags.find()
  2. {
  3. "_id" : {
  4. "ns" : "test.ips",
  5. "min" : {"ip" : "056.000.000.000"}
  6. },
  7. "ns" : "test.ips",
  8. "min" : {"ip" : "056.000.000.000"},
  9. "max" : {"ip" : "057.000.000.000"},
  10. "tag" : "USPS"
  11. }
  12. {
  13. "_id" : {
  14. "ns" : "test.ips",
  15. "min" : {"ip" : "017.000.000.000"}
  16. },
  17. "ns" : "test.ips",
  18. "min" : {"ip" : "017.000.000.000"},
  19. "max" : {"ip" : "018.000.000.000"},
  20. "tag" : "Apple"
  21. }

7. config.settings

该集合含有当前的均衡器设置和块大小的文档信息。通过修改该集合的文档,可开启或关闭均衡器,也可以修改块的大小。注意,应总是连接到mongos修改该集合的值,而不应直接连接到配置服务器进行修改。

16.2 查看网络连接

集群的各组成部分间存在大量的连接。本节我们将学习与分片相关的连接信息。网络信息会在第23章详细介绍。

16.2.1 查看连接统计

可使用connPoolStats命令,查看mongos和mongod之间的连接信息,并可得知服务器上打开的所有连接:

  1. > db.adminCommand({"connPoolStats" : 1})
  2. {
  3. "createdByType": {
  4. "sync": 857,
  5. "set": 4
  6. },
  7. "numDBClientConnection": 35,
  8. "numAScopedConnection": 0,
  9. "hosts": {
  10. "config-01:10005,config-02:10005,config-03:10005": {
  11. "created": 857,
  12. "available": 2
  13. },
  14. "spock/spock-01:10005,spock-02:10005,spock-03:10005": {
  15. "created": 4,
  16. "available": 1
  17. }
  18. },
  19. "totalAvailable": 3,
  20. "totalCreated": 861,
  21. "ok": 1
  22. }

形如"host1,host2,host3"的主机名是来自配置服务器的连接,也就是用于“同步”的连接。形如"name/host1, host2,…,hostN"的主机是来自分片的连接。available的值表明当前实例的连接池中有多少可用连接。

注意,只有在分片内的mongos和mongod上运行这个命令才会有效。

在一个分片上执行connPoolStats,输出信息中可看到该分片与其他分片间的连接,包括连接到其他分片做数据迁移的连接。分片的主连接会直接连接到另一分片的主连接上,然后从目标分片吸取数据。

进行迁移时,分片会建立一个ReplicaSetMonitor(该进程用于监控副本集的健康状况),用于追踪记录迁移另一端分片的健康状况。由于mongod不会销毁这个监控器,所以有时会在一个副本集的日志中看到其他副本集成员的信息。这是很正常的,不会对应用程序造成任何影响。

16.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进程的数量×3)−(每个副本集的成员数量×3)−(其他/mongos进程的数量)

以下为公式的相关说明。

  • (mongos进程的数量×3)

    每个mongos会为每个mongod创建3个连接:一个用于转发客户端请求,一个用于追踪错误信息,即写回监听器(writeback listener),一个用于监控副本集状态。

  • (每个副本集的成员数量×3)

    主节点会与每个备份节点创建一个连接,而每个备份节点会与主节点创建两个连接,因此总共是3个连接。

  • (其他 /mongos进程的数量)

    这里的其他指其他可能连接到mongod的进程数量,这种连接包括MMS代理、shell的直接连接(管理员用),或者是迁移时连接到其他分片的连接。

注意,maxConns只会阻止mongos创建多于maxConns数量的连接,但并不会帮助处理连接耗尽的问题。连接耗尽时,请求会发生阻塞,等待某些连接被释放。因此,必须防止应用程序使用超过maxConns数量的连接,尤其是在mongos进程数量不断增加时。

MongoDB实例在安全退出时,会在终止运行之前关闭所有连接。已经连接到MongoDB的成员会立即收到套接字错误(socket error),并能够重新刷新连接。但是,如果MongoDB实例由于断电、崩溃或者网络问题突然离线,那些已经打开的套接字很可能没有被关闭。在这种情况下,集群内的其他服务器很可能会认为这个MongoDB实例仍在有效运转,但是当试图在该MongoDB实例上执行操作时,就会遇到错误,继而刷新连接(如果此时该MongoDB实例再次上线且运转正常的话)。

连接数量较少时,可快速检测到某台MongoDB实例是否已离线。但是,当有成千上万个连接时,每个连接都需要经历被尝试、检测失败,并重新建立连接的过程,此过程中会得到大量的错误。在出现大量重新连接时,除了重启进程,没有其他特殊有效的方法。

16.3 服务器管理

随着集群的增长,我们可能需要增加集群容量或者是修改集群配置。本节我们将学习向集群添加服务器以及从集群删除服务器的方法。

16.3.1 添加服务器

可随时向集群中添加新的mongos。只要保证在mongos的--configdb选项中指定了一组正确的配置服务,mongos即可立即与客户端建立连接。

如14章所示,可使用addShard命令,向集群添加新分片。

16.3.2 修改分片的服务器

使用分片集群时,我们可能会希望修改某单独分片的服务器。要修改分片的成员,需直接连接到分片的主服务器上(而不是通过mongos),然后对副本集进行重新配置。集群配置会自动检测更改,并将其更新到config.shards上。不要手动修改config.shards。

只有在使用单机服务器作为分片,而不是使用副本集作为分片时,才需手动修改config.shards。

将单机服务器分片修改为副本集分片

最简单的方式是添加一个新的空副本集分片,然后移除单机服务器分片(参见16.3.3节)。

如果希望把单机服务器分片转换为副本集分片,过程会复杂得多,而且需要停机。

  • 停止向系统发送请求。
  • 关闭单机服务器(这里称其为server-1)和所有的mongos进程。
  • 以副本集模式重启server-1(使用--replSet选项)。
  • 连接到server-1,将其作为一个单成员副本集进行初始化。
  • 连接到配置服务器,替换该分片的入口,在config.shards中将分片名称替换为setName/server-1:27017的形式。确保三个配置服务器都拥有相同的配置信息。手动修改配置服务器是有风险的!

可在每个配置服务器上执行dbhash命令,以确保配置信息相同:

  1. > db.runCommand({"dbhash" : 1})

这样可以得到每个集合的MD5散列值。不同配置服务器上,config数据库的集合可能会有所不同,但config.shards应始终保持一致。

  • 重启所有mongos进程。它们会在启动时从配置服务器读取分片数据,然后将副本集当作分片对待。

  • 重启所有分片的主服务器,刷新其配置数据。

  • 再次向系统发送请求。
  • 向server-1副本集中添加新成员。

这一过程非常复杂,而且很容易出错,因此不建议使用。应尽可能地将空的副本集作为新的分片添加到集群中,数据迁移的事情交给集群去做就好了。

16.3.3 删除分片

通常来说,不应从集群中删除分片。如果经常在集群中添加和删除分片,会给系统带来很多不必要的压力。如果向集群中添加了过多的分片,最好是什么也不做,系统早晚会用到这些分片,而不应该将多余的分片删掉,等以后需要的时候再将其重新添加到集群中。不过,在必要的情况下,是可以删除分片的。

首先保证均衡器是开启的。在排出数据(draining)的过程中,均衡器会负责将待删除分片的数据迁移至其他分片。执行removeShard命令,开始排出数据。removeShard将待删除分片的名称作为参数,然后将该分片上的所有块都移至其他分片上:

  1. > db.adminCommand({"removeShard" : "test-rs3"})
  2. {
  3. "msg" : "draining started successfully",
  4. "state" : "started",
  5. "shard" : "test-rs3",
  6. "note" : "you need to drop or movePrimary these databases",
  7. "dbsToMove" : [
  8. "blog",
  9. "music",
  10. "prod"
  11. ],
  12. "ok" : 1
  13. }

如果分片上的块较多,或者有较大的块需要移动,排出数据的过程可能会耗时更长。如果存在特大块(jumbo chunk,参见16.4.4节),可能需临时提高其他分片的块大小,以便能够将特大块迁移到其他分片。

如需查看哪些块已完成迁移,可再次执行removeShard命令,查看当前状态:

  1. > db.adminCommand({"removeShard" : "test-rs3"})
  2. {
  3. "msg" : "draining ongoing",
  4. "state" : "ongoing",
  5. "remaining" : {
  6. "chunks" : NumberLong(5),
  7. "dbs" : NumberLong(0)
  8. },
  9. "ok" : 1
  10. }

在一个处于排出数据过程的分片上,可执行removeShard任意多次。

块在移动前可能需要被拆分,所以有可能会看到系统中的块数量在排出数据时发生了增长。假设有一个拥有5个分片的集群,块的分布如下:

  1. test-rs0 10
  2. test-rs1 10
  3. test-rs2 10
  4. test-rs3 11
  5. test-rs4 11

该集群共有52个块。如果删除test-rs3分片,最终的结果可能会是:

  1. test-rs0 15
  2. test-rs1 15
  3. test-rs2 15
  4. test-rs4 15

集群现在拥有60个块,其中18个来自test-rs3分片(原本有11个,还有7个是在排出数据的过程中创建的)。

所有的块都完成迁移后,如果仍有数据库将该分片作为主分片,需在删除分片前将这些数据库移除掉。removeShard命令的输出结果可能如下:

  1. > db.adminCommand({"removeShard" : "test-rs3"})
  2. {
  3. "msg" : "draining ongoing",
  4. "state" : "ongoing",
  5. "remaining" : {
  6. "chunks" : NumberLong(0),
  7. "dbs" : NumberLong(3)
  8. },
  9. "note" : "you need to drop or movePrimary these databases",
  10. "dbsToMove" : [
  11. "blog",
  12. "music",
  13. "prod"
  14. ],
  15. "ok" : 1
  16. }

为完成分片的删除,需先使用movePrimary命令将这些数据库移走:

  1. > db.adminCommand({"movePrimary" : "blog", "to" : "test-rs4"})
  2. {
  3. "primary " : "test-rs4:test-rs4/ubuntu:31500,ubuntu:31501,ubuntu:31502",
  4. "ok" : 1
  5. }

然后再次执行removeShard命令:

  1. > db.adminCommand({"removeShard" : "test-rs3"})
  2. {
  3. "msg" : "removeshard completed successfully",
  4. "state" : "completed",
  5. "shard" : "test-rs3",
  6. "ok" : 1
  7. }

最后一步不是必需的,但可确保已确实完成了分片的删除。如果不存在将该分片作为主分片的数据库,则块的迁移完成后,即可看到分片删除成功的输出信息。

注意,如果分片开始排出数据,就没有内置办法停止这一过程了。

16.3.4 修改配置服务器

修改配置服务器是非常困难的,而且有风险,通常还需要停机。注意,修改配置服务器前,应做好备份。

在运行期间,所有mongos进程的--configdb选项值都必须相同。因此,要修改配置服务器,首先必须关闭所有的mongos进程(mongos进程在使用旧的--configdb参数时,无法继续保持运行状态),然后使用新的--configdb参数重启所有mongos进程。

例如,将一台配置服务器增至三台是最常见的任务之一。为实现此操作,首先应关闭所有的mongos进程、配置服务器,以及所有的分片。然后将配置服务器的数据目录复制到两台新的配置服务器上(这样三台配置服务器就可以拥有完全相同的数据目录)。接着,启动这三台配置服务器和所有分片。然后,将--configdb选项指定为这三台配置服务器,最后重启所有的mongos进程。

16.4 数据均衡

通常来说,MongoDB会自动处理数据均衡。本节我们将学习如何启用和禁用自动均衡,以及如何人为干涉均衡过程。

16.4.1 均衡器

在执行几乎所有的数据库管理操作之前,都应先关闭均衡器。可使用下列shell辅助函数关闭均衡器:

  1. > sh.setBalancerState(false)

均衡器关闭后,系统则不会再进入均衡过程,但该命令并不能立即终止进行中的均衡过程:迁移过程通常无法立即停止。因此,应检查config.locks集合,以查看均衡过程是否仍在进行中:

  1. > db.locks.find({"_id" : "balancer"})["state"]
  2. 0

此处的0表明均衡器已被关闭。可翻阅“均衡器”一节查看均衡器状态相关内容。

均衡过程会增加系统负载:目标分片必须查询源分片块中的所有文档,将文档插入目标分片的块中,源分片最后必须删除这些文档。在以下两种特殊情况下,迁移会导致性能问题。

  • 使用热点片键可保证定期迁移(因为所有的新块都是创建在热点上的)。系统必须有能力处理源源不断写入到热点分片上的数据。

  • 向集群中添加新的分片时,均衡器会试图为该分片写入数据,从而触发一系列的迁移过程。

如果发现数据迁移过程影响了应用程序性能,可在config.settings集合中为数据均衡指定一个时间窗口。执行下列更新语句,均衡则只会在下午1点到4点间发生:

  1. > db.settings.update({"_id" : "balancer"},
  2. ... {"$set" : {"activeWindow" : {"start" : "13:00", "stop" : "16:00"}}},
  3. ... true )

如指定了均衡时间窗,则应对其进行严密监控,以确保mongos确实只在指定的时间内做均衡。

如需混用手动均衡和自动均衡,必须格外小心。因为自动均衡器总是根据数据集的当前状态来决定数据迁移,而不考虑数据集的历史状态。例如,假设有两个分片shardA和shardB,每个分片都有500个块。由于shardA上的写请求比较多,因此我们关闭了均衡器,从最活跃的块中取出30个移至shardB。此时如再启用均衡器,则会立即将30个块(很可能不是刚刚的30块)从shardB移至shardA,以均衡两个分片拥有的块数量。

为防止这种情况发生,可在启用均衡器之前从shardB选取30个不活跃的块移至shardA。这样两个分片间就不会存在不均衡,均衡器也不会进行数据块的移动了。另外,也可在shardA上拆分出一些块,以实现shardAshardB的均衡。

注意,均衡器只使用块的数量,而非数据大小,作为衡量分片间是否均衡的指标。因此,如果A分片只拥有几个较大的数据块,而B分片拥有许多较小的块(但总数据大小比A小),那么均衡器会将B分片的一些块移至A分片,从而实现均衡。

16.4.2 修改块大小

块中的文档数量可能为0,也可能多达数百万。通常情况下,块越大,迁移至分片的耗时就越长。在第13章中,我们使用的是1 MB的块,所以块移动起来非常容易与迅速。但在实际系统中,这通常是不现实的。MongoDB需要做大量非必要的工作,才能将分片大小维持在几MB以内。块的大小默认为64 MB,这个大小的块既易于迁移,又不会导致过多的流失。

有时可能会发现移动64 MB的块耗时过长。可通过减小块的大小,提高迁移速度。使用shell连接到mongos,然后修改config.settings集合,从而完成块大小的修改:

  1. > db.settings.findOne()
  2. {
  3. "_id" : "chunksize",
  4. "value" : 64
  5. }
  6. > db.settings.save({"_id" : "chunksize", "value" : 32})

以上修改操作将块的大小减至32 MB。已经存在的块不会立即发生改变,执行块拆分操作时,这些块即可拆分成32 MB大小。mongos进程会自动加载新的块大小。

注意,该设置的有效范围是整个集群:它会影响所有集合和数据库。因此,如需对一个集合使用较小的块,而对另一集合使用较大的块,比较好的解决方式是取一个折中的值(或者将这两个集合放在不同的集群中)。

如果MongoDB频繁进行数据迁移或文档较大,则可能需要增加块的大小。

16.4.3 移动块

如前所述,同一块内的所有数据都位于同一分片上。如该分片的块数量比其他分片多,则MongoDB会将其中的一部分块移至其他块数量较少的分片上。移动块的过程叫做迁移(migration),MongoDB就是这样在集群中实现数据均衡的。

可在shell中使用moveChunk辅助函数,手动移动块:

  1. > sh.moveChunk("test.users", {"user_id" : NumberLong("1844674407370955160")},
  2. ... "spock")
  3. { "millis" : 4079, "ok" : 1 }

以上命令会将包含文档user_id为1844674407370955160的块移至名为spock的分片上。必须使用片键来找出所需移动的块(本例中的片键是user_id)。通常,指定一个块最简单的方式是指定它的下边界,不过指定块范围内的任何值都可以(块的上边界值除外,因为其并不包含在块范围内)。该命令在块移动完成后才会返回,因此需一定耗时才能看到输出信息。如某个操作耗时较长,可在日志中详细查看问题所在。

如某个块的大小超出了系统指定的最大值,mongos则会拒绝移动这个块:

  1. > sh.moveChunk("test.users", {"user_id" : NumberLong("1844674407370955160")},
  2. ... "spock")
  3. {
  4. "cause" : {
  5. "chunkTooBig" : true,
  6. "estimatedChunkSize" : 2214960,
  7. "ok" : 0,
  8. "errmsg" : "chunk too big to move"
  9. },
  10. "ok" : 0,
  11. "errmsg" : "move failed"
  12. }

本例中,移动这个块之前,必须先手动拆分这个块。可使用splitAt命令对块进行拆分:

  1. > db.chunks.find({"ns" : "test.users",
  2. ... "min.user_id" : NumberLong("1844674407370955160")})
  3. {
  4. "_id" : "test.users-user_id_NumberLong(\"1844674407370955160\")",
  5. "ns" : "test.users",
  6. "min" : { "user_id" : NumberLong("1844674407370955160") },
  7. "max" : { "user_id" : NumberLong("2103288923412120952") },
  8. "shard" : "test-rs2"
  9. }
  10. > sh.splitAt("test.ips", {"user_id" : NumberLong("2000000000000000000")})
  11. { "ok" : 1 }
  12. > db.chunks.find({"ns" : "test.users",
  13. ... "min.user_id" : {"$gt" : NumberLong("1844674407370955160")},
  14. ... "max.user_id" : {"$lt" : NumberLong("2103288923412120952")}})
  15. {
  16. "_id" : "test.users-user_id_NumberLong(\"1844674407370955160\")",
  17. "ns" : "test.users",
  18. "min" : { "user_id" : NumberLong("1844674407370955160") },
  19. "max" : { "user_id" : NumberLong("2000000000000000000") },
  20. "shard" : "test-rs2"
  21. }
  22. {
  23. "_id" : "test.users-user_id_NumberLong(\"2000000000000000000\")",
  24. "ns" : "test.users",
  25. "min" : { "user_id" : NumberLong("2000000000000000000") },
  26. "max" : { "user_id" : NumberLong("2103288923412120952") },
  27. "shard" : "test-rs2"
  28. }

块被拆分为较小的块后,就可以被移动了。也可以调高最大块的大小,然后再移动这个较大的块。不过应尽可能地将大块拆分为小块。不过有时有些块无法被拆分,这些块被称作特大块

16.4.4 特大块

假设使用date字段作为片键。集合中的date字段是一个日期字符串,格式为year/month/day,也就是说,mongos一天最多只能创建一个块。最初的一段时间内一切正常,直到有一天,应用程序的业务量突然出现病毒式增长,流量比平常大了上千倍!

这一天的块要比其他日期的大得多,但由于块内所有文档的片键值都是一样的,因此这个块是不可拆分的。

如果块的大小超出了config.settings中设置的最大块大小,那么均衡器就无法移动这个块了。这种不可拆分和移动的块就叫做特大块,这种块相当难对付。

举例来说,假如有3个分片shard1、shard2和shard3。如果使用热点片键模式(参见15.2.1节),假设shard1是热点片键,则所有写请求都会被分发到shard1上。mongos会试图将块均衡地分发在这些分片上。但是,均衡器只能移动非特大块,因此它只会将所有较小块从热点分片迁移到其他分片。

现在,所有分片上的块数基本相同,但shard2和shard3上的所有块都小于64 MB。如shard1上出现了特大块,则shard1上会有越来越多的块大于64 MB。这样,即使三个分片的块数非常均衡,但shard1会比另两个分片更早被填满。

出现特大块的表现之一是,某分片的大小增长速度要比其他分片快得多。也可使用sh.status()来检查是否出现了特大块:特大块会存在一个jumbo属性。

  1. > sh.status()
  2. ...
  3. { "x" : -7 } -->> { "x" : 5 } on : shard0001
  4. { "x" : 5 } -->> { "x" : 6 } on : shard0001 jumbo
  5. { "x" : 6 } -->> { "x" : 7 } on : shard0001 jumbo
  6. { "x" : 7 } -->> { "x" : 339 } on : shard0001
  7. ...

可使用dataSize命令检查块大小。

首先,使用config.chunks集合,查看块范围:

  1. > use config
  2. > var chunks = db.chunks.find({"ns" : "acme.analytics"}).toArray()

然后根据这些块范围,找出可能的特大块:

  1. > use dbName
  2. > db.runCommand({"dataSize" : "dbName.collName",
  3. ... "keyPattern" : {"date" : 1}, // 片键
  4. ... "min" : chunks[0].min,
  5. ... "max" : chunks[0].max})
  6. { "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)
  • MongoDB不允许移动大小超出最大块大小设定值的块,所以需临时调高最大块大小的设定值。记下特大块的大小,然后将最大块大小设定值调整为比特大块大一些的数值,比如10 000。块大小的单位是MB:
  1. > use config
  2. > db.settings.findOne({"_id" : "chunksize"})
  3. {
  4. "_id" : "chunksize",
  5. "value" : 64
  6. }
  7. > db.settings.save({"_id" : "chunksize", "value" : 10000})
  • 使用moveChunk命令将特大块从from分片移至to分片。如担心迁移会对应用程序的性能造成影响,可使用secondaryThrootle选项,放慢迁移的过程,减缓对系统性能的影响:
  1. > db.adminCommand({"moveChunk" : "acme.analytics",
  2. ... "find" : {"date" : new Date("10/23/2012")},
  3. ... "to" : "shard0002",
  4. ... "secondaryThrottle" : true})

secondaryThrottle会强制要求迁移过程间歇进行,每迁移完一些数据,需等待集群中的大多数分片成功完成数据复制后再进行下一次迁移。该选项只有在使用副本集分片时才会生效。如使用单机服务器分片,则该选项不会生效。

  • 使用splitChunk命令对from分片剩余的块进行拆分,这样可以增加from分片的块数,直到实现from分片与其他分片块数的均衡。

  • 将块大小修改回最初值:

  1. > db.settings.save({"_id" : "chunksize", "value" : 64})
  • 启用均衡器。
  1. > sh.setBalancerState(true)

均衡器被再次启用后,仍旧不能移动特大块,不过此时那些特大块都已位于合适的位置了。

2. 防止出现特大块

随着存储数据量的增长,上一节提到的手动过程变得不再可行。因此,如在特大块方面存在问题,应首先想办法避免特大块的出现。

为防止特大块的出现,可修改片键,细化片键的粒度。应尽可能保证每个文档都拥有唯一的片键值,或至少不要出现某个片键值的数据块超出最大块大小设定值的情况。

例如,如使用前面所述的年/月/日片键,可通过添加时、分、秒来细化片键粒度。类似地,如使用粒度较大的片键,如日志级别,则可添加一个粒度较细的字段作为片键的第二个字段,如MD5散列值或UDID。这样一来,即使有许多文档片键的第一个字段值是相同的,也可一直对块进行拆分,也就防止了特大块的出现。

16.4.5 刷新配置

最后一点,mongos有时无法从配置服务器正确更新配置。如发现配置有误,mongos的配置过旧或无法找到应有数据,可使用flushRouterConfig命令手动刷新所有缓存:

  1. >db.adminCommand({"flushRouterConfig" : 1})

flushRouterConfig命令没能解决问题,则应重启所有的mongos或mongod进程,以便清除所有可能的缓存。