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 进程,以便清除所有可能的缓存。

 

posted @ 2021-10-10 13:42  小家电维修  阅读(237)  评论(0编辑  收藏  举报