第12章 管理

本章介绍副本集管理的相关知识,包括:

  • 维护独立的成员;
  • 在多种不同情况下配置副本集;
  • 获取oplog相关信息,以及调整oplog大小;
  • 特殊的副本集配置;
  • 从主从模式切换到副本集模式 。

12.1 以单机模式启动成员

许多维护工作不能在备份节点上进行(因为要执行写操作),也不能在主节点上进行。后面几节会经常提到以单机模式(standalone mode)启动服务器。这是指要重启成员服务器,让它成为一个单机运行的服务器,而不再是一个副本集成员(这只是临时的)。

在以单机模式启动服务器之前,先看一下服务器的命令行参数:

  1. > db.serverCmdLineOpts()
  2. {
  3. "argv" : [ "mongod", "-f", "/var/lib/mongod.conf" ],
  4. "parsed" : {
  5. "replSet": "mySet",
  6. "port": "27017",
  7. "dbpath": "/var/lib/db"
  8. },
  9. "ok" : 1
  10. }

如果要对这台服务器进行维护,可以重启服务器,重启时不使用replSet选项。这样它就会成为一个单机的mongod,可以对其进行读和写。我们不希望副本集中的其他服务器联系到这台服务器,所以可以让它监听不同的端口(这样副本集的其他成员就找不到它了)。最后,保持dbpath的值不变,因为重启后要对这台服务器的数据做一些操作。好了,我们最终可以用下面这样的参数启动服务器:

  1. $ mongod --port 30000 --dbpath /var/lib/db

现在这台服务器已经在单机模式中运行了,监听着30000端口的连接请求。副本集中的其他成员仍然会试图连接到它的27017端口,所以会连接失败,其他成员就会以为这台服务器挂掉了。

当在这台服务器上执行完维护工作之后,可以以最原始的参数重新启动它。启动之后,它会自动与副本集中的其他成员进行同步,将维护期间落下的操作全部复制过来。

12.2 副本集配置

副本集配置总是以一个文档的形式保存在local.system.replSet集合中。副本集中所有成员的这个文档都是相同的。绝对不要使用update更新这个文档,应该使用rs辅助函数或者replSetReconfig命令修改副本集配置。

12.2.1 创建副本集

创建副本集的步骤很简单,首先启动所有成员服务器,然后使用rs.initiate命令将配置文件传递给其中一个成员:

  1. > var config = {
  2. ... "_id" : setName,
  3. ... "members" : [
  4. ... {"_id" : 0, "host" : host1},
  5. ... {"_id" : 1, "host" : host2},
  6. ... {"_id" : 2, "host" : host3}
  7. ... ]}
  8. > rs.initiate(config)

应该总是传递一个配置对象给rs.initiate,否则MongoDB会自动生成一个针对单成员副本集的配置,其中的主机名可能不是你希望的。

只需要对副本集中的一个成员调用rs.initiate就可以了。收到initiate命令的成员会自动将配置文件传递给副本集中的其他成员。

12.2.2 修改副本集成员

向副本集中添加新成员时,这个新成员的数据目录要么是空的(在这种情况下,新成员会执行初始化同步),要么新成员拥有一份其他成员的数据副本。关于副本集成员备份和恢复相关的知识,可以查看第22章。

连接到主节点并且添加新成员:

  1. > rs.add("spock:27017")

也可以以文档的形式为新成员指定更复杂的配置:

  1. > rs.add({"_id" : 5, "host" : "spock:27017", "priority" : 0, "hidden" : true})

可以根据"host"字段将成员从副本集中移除:

  1. > rs.remove("spock:27017")

可以通过rs.reconfig修改副本集成员的配置。修改副本集成员配置时,有几个限制需要注意:

  • 不能修改成员的"_id"字段;
  • 不能将接收rs.reconfig命令的成员(通常是主节点)的优先级设为0;
  • 不能将仲裁者成员变为非仲裁者成员,反之亦然;
  • 不能将"buildIndexes" : false的成员修改为"buildIndexes" : true

需要注意的是,可以修改成员的"host"字段。这意味着,如果为副本集成员指定了不正确的主机名(比如使用了公网IP而不是内网IP),之后可以重新修改成员的主机名。

下面是一个修改主机名的例子:

  1. > var config = rs.config()
  2. > config.members[0].host = "spock:27017"
  3. spock:27017
  4. > rs.reconfig(config)

修改其他选项的方式也是一样的:使用rs.config得到当前配置文件,修改配置文件,将修改后的配置文件传递给rs.reconfig就可以了。

12.2.3 创建比较大的副本集

副本集最多只能拥有12个成员,其中只有7个成员拥有投票权。这是为了减少心跳请求的网络流量(每个成员都要向其他所有成员发送心跳请求)和选举花费的时间。实际上,副本集还有更多的限制,如果需要11个以上的备份节点,可以查看12.5节。

如果要创建7个以上成员的副本集,只有7个成员可以拥有投票权,需要将其他成员的投票数量设置为0:

  1. > rs.add({"_id" : 7, "host" : "server-7:27017", "votes" : 0})

这样可以阻止这些成员在选举中投主动票,虽然它们仍然可以投否决票。

应该尽量避免修改成员的投票数量。投票可能会对选举和一致性产生怪异的、不直观的影响。应该只在创建包含7个以上成员的副本集或者是希望阻止自动故障转移(详见12.5.2节)时,使用"votes"选项。很多开发者会误以为让成员拥有更多投票权会使这个成员更容易被选为主节点(实际上根本不会)。如果希望某个成员可以优先被选举为主节点,应该使用优先级(详见9.6.2节)。

12.2.4 强制重新配置

如果副本集无法再达到“大多数”要求的话,那么它就无法选举出新的主节点,这时你可能会希望重新配置副本集。这看起来有点奇怪,因为通常都是将配置文件发送给主节点。在这种情况下,可以在备份节点上调用rs.reconfig强制重新配置(force reconfigure)副本集。在shell中连接到一个备份节点,使用"force"选项执行rs.reconfig命令:

  1. > rs.reconfig(config, {"force" : true})

强制重新配置与普通的重新配置要遵守同样的规则:必须使用正确的reconfig选项将有效的、格式完好的配置文件发送给成员。"force"选项不允许无效的配置,而且只允许将配置发送给备份节点。

强制重新配置会跳过大量的数值直接将副本集的"version"设为一个比较大的值。可能会见到跳过数千的情况,这很正常:这是为了防止"version"字段冲突(以防不同的网络域中都在进行重新配置)。

备份节点收到新的配置文件之后,就会修改自身的配置,并且将新的配置发送给副本集中的其他成员。副本集的其他成员收到新的配置文件之后,会判断配置文件的发送者是否是它们当前配置中的一个成员,如果是,才会用新的配置文件对自己进行重新配置。所以,如果新的配置会修改某些成员的主机名,应该将新的配置发送给主机名不发生变化的成员。如果新的配置文件修改了所有成员的主机名,应该关闭副本集的每一个成员,以单机模式启动,手动修改local.system.replset文档,然后重新启动。

12.3 修改成员状态

为进行维护或响应加载,有多种方式可以手动修改成员的状态。注意,无法强制将某个成员变成主节点,除非对副本集做适当的配置。

12.3.1 把主节点变为备份节点

可以使用stepDown函数将主节点降级为备份节点:

  1. > rs.stepDown()

这个命令可以让主节点退化为备份节点,并维持60秒。如果这段时间内没有新的主节点被选举出来,这个节点就可以要求重新进行选举。如果希望主节点退化为备份节点并持续更长(或者更短)的时间,可以自己指定时间(以秒为单位):

  1. > rs.stepDown(600) // 10分钟

12.3.2 阻止选举

如果需要对主节点做一些维护,但是不希望这段时间内将其他成员选举为主节点,那么可以在每个备份节点上执行freeze命令,以强制它们始终处于备份节点状态:

  1. > rs.freeze(10000)

这个命令也会接受一个以秒表示的时间,表示在多长时间内保持备份节点状态。

维护完成之后,如果想“释放”其他成员,可以再次执行freeze命令,将时间指定为0即可:

  1. > rs.freeze(0)

这样,其他成员就可以在必要时申请被选举为主节点。

也可以在主节点上执行rs.freeze(0),这样可以将退位的主节点重新变为主节点。

12.3.3 使用维护模式

当在副本集成员上执行某个非常耗时的操作时,这个成员就人进入维护模式(maintenance mode):强制成员进入RECOVERING状态。有时,成员会自动进入维护模式,比如在成员上做压缩时。压缩开始之后,成员会进入RECOVERING状态,这样就不会有读请求发送给这个成员。客户端会停止从这个成员读取数据(如果之前有从这个成员读数据的话),这个成员也不能再作为复制源。

也可以通过执行replSetMaintenanceMode命令强制一个成员进入维护模式。如果一个成员远远落后于主节点,你不希望它继续处理读请求时,可以强制让这个成员进入维护模式。例如,下面这个脚本会自动检测成员是否落后于主节点30秒以上,如果是,就强制将这个成员转入维护模式:

  1. function maybeMaintenanceMode() {
  2. var local = db.getSisterDB("local");
  3. // 如果成员不是备份节点(它可能是主节点
  4. //或者已经处于维护状态),就直接返回
  5. if (!local.isMaster().secondary) {
  6. return;
  7. }
  8. // 查找这个成员最后一次操作的时间
  9. var last = local.oplog.rs.find().sort({"$natural" : -1}).next();
  10. var lastTime = last['ts']['t'];
  11. // 如果落后主节点30秒以上
  12. if (lastTime < (new Date()).getTime()-30) {
  13. db.adminCommand({"replSetMaintenanceMode" : true});
  14. }
  15. };

将成员从维护模式中恢复,可以使用如下命令:

  1. > db.adminCommand({"replSetMaintenanceMode" : false});

12.4 监控复制

监控副本集的状态非常重要:不仅要监控是否所有成员都可用,也要监控每个成员处于什么状态,以及每个成员的数据新旧程度。有多个命令可以用来查看副本集相关信息。MMS(详见第21章)也维护着一些与复制相关的信息。

与复制相关的故障通常都是很短暂的:一个服务器刚才还连接不到另一个服务器,但是现在又可以连上了。要查看这样的问题,最简单的方式就是查看日志。确保自己知道日志的保存位置(而且真的被保存下来),确保能够访问到它们。

12.4.1 获取状态

replSetGetStatus是一个非常有用的命令,可以返回副本集中每个成员的当前信息(这里的“当前”是从每个成员自身的角度来说的)。这个命令还有一个对应的辅助函数rs.status

  1. > rs.status()
  2. {
  3. "set" : "spock",
  4. "date" : ISODate("2012-10-17T18:17:52Z"),
  5. "myState" : 2,
  6. "syncingTo" : "server-1:27017",
  7. "members" : [
  8. {
  9. "_id" : 0,
  10. "name" : "server-1:27017",
  11. "health" : 1,
  12. "state" : 1,
  13. "stateStr" : "PRIMARY",
  14. "uptime" : 74824,
  15. "optime" : { "t" : 1350496621000, "i" : 1 },
  16. "optimeDate" : ISODate("2012-10-17T17:57:01Z"),
  17. "lastHeartbeat" : ISODate("2012-10-17T17:57:00Z"),
  18. "pingMs" : 3,
  19. },
  20. {
  21. "_id" : 1,
  22. "name" : "server-2:27017",
  23. "health" : 1,
  24. "state" : 2,
  25. "stateStr" : "SECONDARY",
  26. "uptime" : 161989,
  27. "optime" : { "t" : 1350377549000, "i" : 500 },
  28. "optimeDate" : ISODate("2012-10-17T17:57:00Z"),
  29. "self" : true
  30. },
  31. {
  32. "_id" : 2,
  33. "name" : "server-3:27017",
  34. "health" : 1,
  35. "state" : 3,
  36. "stateStr" : "RECOVERING",
  37. "uptime" : 24300,
  38. "optime" : { "t" : 1350411407000, "i" : 739 },
  39. "optimeDate" : ISODate("2012-10-16T18:16:47Z"),
  40. "lastHeartbeat" : ISODate("2012-10-17T17:57:01Z"),
  41. "pingMs" : 12,
  42. "errmsg" : "still syncing, not yet to minValid optime 507e9a30:851"
  43. }
  44. ],
  45. "ok" : 1
  46. }

下面分别介绍几个最有用的字段。

  • self

    这个字段只会出现在执行rs.status()函数的成员信息中,在本例中是server-2。

  • stateStr

    用于描述服务器状态的字符串。关于成员不同状态的描述,可以查看10.2.1节。

  • uptime

    从成员可达一直到现在所经历的时间,单位是秒。对于"self"成员,这个值是从成员启动一直到现在的时间。因此,server-2已经启动161 989秒了(大约45小时)。server-1在过去的21小时中一直处于可用状态,server-3在过去7小时中一直处于可用状态。

  • optimeDate

    每个成员的oplog中最后一个操作发生的时间(也就是操作被同步过来的时间)。注意,这里的状态是每个成员通过心跳报告上来的状态,所以optime跟实际时间可能会有几秒钟的偏差。

  • lastHeartbeat

    当前服务器最后一次收到其他成员心跳的时间。如果网络故障或者当前服务器比较繁忙,这个时间可能会是2秒钟之前。

  • pingMs

    心跳从当前服务器到达某个成员所花费的平均时间,可以根据这个字段选择从哪个成员进行同步。

  • errmsg

    成员在心跳请求中返回的状态信息。这个字段的内容通常只是一些状态信息,而不是错误信息。例如,server-3的"errmsg"字段表示它正处于初始化同步过程中。这里的十六进制数字507e9a30:851是某个操作对应的时间戳,server-3至少要同步完这个操作才能完成同步过程。

有几个字段的信息是重复的:"state""stateStr"都表示成员的状态,只是"state"的值是状态的内部表示法。"health"仅仅表示给定的服务器是否可达(可达是1,不可达是0),而从"state""stateStr"字段也可以得到这样的信息(如果服务器不可达,它们的值会是UNKNOWN或者DOWN)。类似地,"optime""optimeDate"的值也是相同的,只是表示方式不同:一个是用从新纪元开始的毫秒数表示的,另一个用一种更适合阅读的方式表示。

注意,这份报告是以执行rs.status()命令的成员的角度得出的:由于网络故障,这份报告可能不准确或者有些过时。

12.4.2 复制图谱

如果在备份节点上运行rs.status(),输出信息中会有一个名为"syncingTo"的顶级字段,用于表示当前成员正在从哪个成员处进行复制。如果在每个成员上运行replSetGetStatus命令,就可以弄清楚复制图谱(replication graph)。假设server1表示连接到server1的数据库连接,server2表示连接到server2的数据库连接,以此类推,然后分别在这些连接上执行下面的命令:

  1. > server1.adminCommand({replSetGetStatus: 1})['syncingTo']
  2. server0:27017
  3. > server2.adminCommand({replSetGetStatus: 1})['syncingTo']
  4. server1:27017
  5. > server3.adminCommand({replSetGetStatus: 1})['syncingTo']
  6. server1:27017
  7. > server4.adminCommand({replSetGetStatus: 1})['syncingTo']
  8. server2:27017

所以,server0是server1的同步源,server1是server2和server3的同步源,server2是server4的同步源。

MongoDB根据ping时间选择同步源。一个成员向另一个成员发送心跳请求,就可以知道心跳请求所耗费的时间。MongoDB维护着不同成员间请求的平均花费时间。选择同步源时,会选择一个离自己比较近而且数据比自己新的成员(所以,不会出现循环复制的问题,每个成员要么从主节点复制,要么从数据比它新的成员处复制)。

因此,如果在备份数据中心中添加一个新成员,它很可能会从与自己同在一个数据中心内的其他成员处复制,而不是从位于另一个数据中心的主节点处复制(这样可以减少网络流量),如图12-1所示。

第12章 管理 - 图1

图12-1 新的备份节点通常会从与自己处于同一个数据中心的其他成员进行复制

但是,自动复制链(automatic replication chaining)也有一些缺点:复制链越长,将写操作复制到所有服务器所花费的时间就越长。假设所有服务器都位于同一个数据中心内,然后,由于网络速度异常,新添加一个成员之后,MongoDB的复制链如图12-2所示。

第12章 管理 - 图2

图12-2 复制链越长,将数据同步到全部服务器花费的时间就越长

通常不太可能发生这样的情况,但是并非不可能。但这种情况通常是不可取的:复制链中的每个备份节点都要比它前面的备份节点稍微落后一点。只要出现这种状况,可以用replSetSyncFrom(或者是它对应的辅助函数rs.syncFrom())命令修改成员的复制源进行修复。

连接到需要修改复制源的备份节点,运行这个命令,为其指定一个复制源:

  1. > secondary.adminCommand({"replSetSyncFrom" : "server0:27017"})

可能要花费几秒钟的时间才能切换到新的复制源。如果在这个成员上再次执行rs.status(),会发现"syncingTo"字段的值已经变成了"server0:27017"

现在,server4会一直从server0进行复制,直到server0不可用或者远远落后于其他成员为止。

12.4.3 复制循环

如果复制链中出现了环,那么就称为发生了复制循环。例如,A从B处同步数据,B从C处同步数据,C从A处同步数据,这就是一个复制循环。因为复制循环中的成员都不可能成为主节点,所以这些成员无法复制新的写操作,就会越来越落后。另一方面,如果每个成员都是自动选取复制源,那么复制循环是不可能发生的。

但是,使用replSetSyncFrom强制为成员设置复制源时,就可能会出现复制循环。在手动修改成员的复制源时,应该仔细查看rs.status()的输出信息,避免造成复制循环。当用replSetSyncFrom为成员指定一个并不比它领先的成员作为复制源时,系统会给出警告,但是仍然允许这么做。

12.4.4 禁用复制链

当一个备份节点从另一个备份节点(而不是主节点)复制数据时,就会形成复制链。前面说过,成员会自动选择其他成员作为复制源。

可以禁用复制链,强制要求每个成员都从主节点进行复制,只需要将"allowChaining"设置为false即可(如果不指定这个选项,默认是true):

  1. > var config = rs.config()
  2. > // 如果设置子对象不存在,就自动创建一个空的
  3. > config.settings = config.settings || {}
  4. > config.settings.allowChaining = false
  5. > rs.reconfig(config)

allowChaining设置为false之后,所有成员都会从主节点复制数据。如果主节点变得不可用,那么各个成员就会从其他备份节点处复制数据。

12.4.5 计算延迟

跟踪复制情况的一个重要指标是备份节点与主节点之间的延迟程度。延迟(lag)是指备份节点相对于主节点的落后程度,是主节点最后一次操作的时间戳与备份节点最后一次操作的时间戳的差。

可以使用rs.status()查看成员的复制状态,也可以通过在主节点上执行db.printReplicationInfo()(这个命令的输出信息中包括oplog相关信息),或者在备份节点上执行db.printSlaveReplicationInfo()快速得到一份摘要信息。注意,这两个都是db的函数,而不是rs的。

db.printReplicationInfo的输出中包括主节点的oplog信息:

  1. > db.printReplicationInfo();
  2. configured oplog size: 10.48576MB
  3. log length start to end: 34secs (0.01hrs)
  4. oplog first event time: Tue Mar 30 2010 16:42:57 GMT-0400 (EDT)
  5. oplog last event time: Tue Mar 30 2010 16:43:31 GMT-0400 (EDT)
  6. now: Tue Mar 30 2010 16:43:37 GMT-0400 (EDT)

上面的输出信息中包含了oplog的大小,以及oplog中包含的操作的时间范围。在本例中,oplog的大小大约是10 MB,而且只包含大约最近30秒的操作。

在实际的部署中,oplog会大得多(12.4.6节会讲述如何修改oplog的大小)。我们希望oplog的长度至少要能够容纳一次完整同步的所有操作。这样,备份节点就不会在完成初始化同步之前与oplog脱节。

第12章 管理 - 图3oplog中第一条操作与最后一条操作的时间差就是操作日志的长度。如果服务器才刚刚启动,刚启动时的oplog是空的,那么oplog中的第一条操作会距离现在非常近。在这种情况下,日志长度会比较小,即使oplog仍然有可用空间。对于那些已经运行了比较长的时间,oplog已经至少被填满一次的服务器来说,日志长度是一个非常有用的度量指标。

在备份节点上运行db.printSlaveReplicationInfo(),可以得到当前成员的复制源,以及当前成员相对复制源的落后程度等信息:

  1. > db.printSlaveReplicationInfo();
  2. source: server-0:27017
  3. syncedTo: Tue Mar 30 2012 16:44:01 GMT-0400 (EDT)
  4. = 12secs ago (0hrs)

这样就可以知道当前成员正在从哪个成员处复制数据。在这个例子中,备份节点比主节点落后12秒。

注意,副本集成员的延迟是相对于主节点来说的,而不是表示需要多长时间才能更新到最新。在一个写操作非常少的系统中,有可能会造成延迟过大的幻觉。假设一小时执行一次写操作。刚刚执行完这次写操作之后,复制之前,备份节点会落后于主节点一小时。但是,只需要几毫秒时,备份节点就可以追上主节点。当监控低吞吐量的系统时,这个值可能会造成迷惑。

12.4.6 调整oplog大小

可以将主节点的oplog长度看作维护工作的时间窗。如果主节点的oplog长度是一小时,那么你就只有一小时的时间可以用于修复各种错误,不然的话备份节点可能会落后于主节点太多,导致不得不重新进行完全同步。所以,你通常可能希望oplog能够保存几天或者一个星期的数据,从而给自己预留足够的时间,用于处理各种突发状况。

可惜,在oplog被填满之前很难知道它的长度,也没有办法在服务器运行期间调整oplog大小。但是,可以依次将每台服务器下线,调整它的oplog,然后重新把它添加到副本集中。记住,每一个可能成为主节点的服务器都应该拥有足够大的oplog,以预留足够的时间窗用于进行维护。

如果要增加oplog大小,可以按照如下步骤。

  • 如果当前服务器是主节点,让它退位,以便让其他成员的数据能够尽快更新到与它一致。
  • 关闭当前服务器。
  • 将当前服务器以单机模式启动。
  • 临时将oplog中的最后一条insert操作保存到其他集合中:
  1. > use local
  2. > // op:"i"用于查找最后一条Insert操作
  3. > var cursor = db.oplog.rs.find({"op" : "i"})
  4. > var lastInsert = cursor.sort({"$natural" : -1}).limit(1).next()
  5. > db.tempLastOp.save(lastInsert)
  6. >
  7. > // 确保保存成功,这非常重要!
  8. > db.tempLastOp.findOne()

也可以使用最后一项update或者delete操作,但是$操作符不能插入到集合中。

  • 删除当前的oplog:
  1. > db.oplog.rs.drop()
  • 创建一个新的oplog:
  1. > db.createCollection("oplog.rs", {"capped" : true, "size" : 10000})
  • 将最后一条操作记录写回oplog:
  1. > var temp = db.tempLastOp.findOne()
  2. > db.oplog.rs.insert(temp)
  3. >
  4. > // 要确保插入成功
  5. > db.oplog.rs.findOne()

确保最后一条操作记录成功插入oplog。如果没有插入成功,把当前服务器添加到副本集之后,它会删除所有数据,然后重新进行一次完整同步。

  • 最后,将当前服务器作为副本集成员重新启动。注意,由于这时它的oplog只有一条记录,所以在一段时间内无法知道oplog的真实长度。另外,这个服务器现在也并不适合作为其他成员的复制源。

通常不应该减小oplog的大小:即使oplog可能会有几个月那么长,但是通常总是有足够的硬盘空间来保存oplog,oplog并不会占用任何珍贵的资源(比如CPU或RAM)。

12.4.7 从延迟备份节点中恢复

假设有人不小心删除了一个数据库,幸好你有一个延迟备份节点。现在,需要放弃其他成员的数据,明确将延迟备份节点指定为数据源。有几种方法可以使用。

下面介绍最简单的方法。

  • 关闭所有其他成员。
  • 删除其他成员数据目录中的所有数据。确保每个成员(除了延迟备份节点)的数据目录都是空的。
  • 重启所有成员,然后它们会自动从延迟备份节点中复制数据。

这种方式非常简单,但是,在其他成员完成初始化同步之前,副本集中将只有一个成员可用(延迟备份节点)而且这个成员很可能会过载。

根据数据量的不同,第二种方式可能更好,也可能更差。

  • 关闭所有成员,包括延迟备份节点。
  • 删除其他成员(除了延迟备份节点)的数据目录。
  • 将延迟备份节点的数据文件复制到其他服务器。
  • 重启所有成员。

注意,这样会导致所有服务器都与延迟备份节点拥有同样大小的oplog,这可能不是你想要的。

12.4.8 创建索引

如果向主节点发送创建索引的命令,主节点会正常创建索引,然后备份节点在复制“创建索引”操作时也会创建索引。这是最简单的创建索引的方式,但是创建索引是一个需要消耗大量资源的操作,可能会导致成员不可用。如果所有备份节点都在同一时间开始创建索引,那么几乎所有成员都会不可用,一直到索引创建完成。

因此,可能你会希望每次只在一个成员上创建索引,以降低对应用程序的影响。如果要这么做,有下面几个步骤。

  • 关闭一个备份节点服务器。
  • 将这个服务器以单机模式启动。
  • 在单机模式下创建索引。
  • 索引创建完成之后,将服务器作为副本集成员重新启动。
  • 对副本集中的每个备份节点重复第(1)步~第(4)步。

现在副本集的每个成员(除了主节点)都已经成功创建了索引。现在你有两个选择,应该根据自己的实际情况选择一个对生产系统影响最小的方式。

  • 在主节点上创建索引。如果系统会有一段负载比较小的“空闲期”,那会是非常好的创建索引的时机。也可以修改读取首选项,在主节点创建索引期间,将读操作发送到备份节点上。

主节点创建索引之后,备份节点仍然会复制这个操作,但是由于备份节点中已经有了同样的索引,实际上不会再次创建索引。

  • 让主节点退化为备份节点,对这个服务器执行上面的4步。这时就会发生故障转移,在主节点退化为备份节点创建索引期间,会有新的节点被选举为主节点,保证系统正常运转。索引创建完成之后,可以重新将服务器添加到副本集。

注意,可以使用这种技术为某个备份节点创建与其他成员不同的索引。这种方式在做离线数据处理时会非常有用,但是,如果某个备份节点的索引与其他成员不同,那么它永远不能成为主节点:应该将它的优先级设为0。

如果要创建唯一索引,需要先确保主节点中没有被插入重复的数据,或者应该首先为主节点创建唯一索引。否则,可能会有重复数据插入主节点,这会导致备份节点复制时出错,如果遇到这样的错误,备份节点会将自己关闭。你不得不以单机模式启动这台服务器,删除唯一索引,然后重新将其加入副本集。

12.4.9 在预算有限的情况下进行复制

如果预算有限,不能使用多台高性能服务器,可以考虑将备份节点只用于灾难恢复,这样的备份节点不需要太大的RAM和太好的CPU,也不需要太高的磁盘IO。这样,始终将高性能服务器作为主节点,比较便宜的服务器只用于备份,不处理任何客户端请求(将客户端配置为将全部读请求发送到主节点)。对于这样的备份节点,应该设置这些选项。

  • "priority" : 0

    优先级为0的备份节点永远不会成为主节点。

  • "hidden" : true

    将备份节点设为隐藏,客户端就无法将读请求发送给它了。

  • "buildIndexes" : false

    这个选项是可选的,如果在备份节点上创建索引的话,会极大地降低备份节点的性能。如果不在备份节点上创建索引,那么从备份节点中恢复数据之后,需要重新创建索引。

  • "votes" : 0

    在只有两台服务器的情况下,如果将备份节点的投票数设为0,那么当备份节点挂掉之后,主节点仍然会一直是主节点,不会因为达不到“大多数”的要求而退位。如果还有第三台服务器(即使它是你的应用服务器),那么应该在第三台服务器上运行一个仲裁者成员,而不是将第三台服务器的投票数量设为0。

在没有足够的预算购买多台高性能服务器的情况下,可以用这样的备份节点来保证系统和数据安全。

12.4.10 主节点如何跟踪延迟

作为其他成员的同步源的成员会维护一个名为local.slaves的集合,这个集合中保存着所有正从当前成员进行数据同步的成员,以及每个成员的数据新旧程度。如果使用w参数执行查询,MongoDB会根据这些信息确定是否有足够多、足够新的备份节点可以用来处理查询。

local.slaves集合实际上是内存中数据结构的“回声”,所以其中的数据可能会有几秒钟的延迟:

  1. > db.slaves.find()
  2. { "_id" : ObjectId("4c1287178e00e93d1858567c"), "host" : "10.4.1.100",
  3. "ns" : "local.oplog.rs", "syncedTo" : { "t" : 1276282710000, "i" : 1 } }
  4. { "_id" : ObjectId("4c128730e6e5c3096f40e0de"), "host" : "10.4.1.101",
  5. "ns" : "local.oplog.rs", "syncedTo" : { "t" : 1276282710000, "i" : 1 } }

每个服务器的"_id"字段非常重要:它是所有正在从当前成员进行数据同步的服务器的标识符。连接到一个成员,然后查询local.me集合就可以知道一个成员的标识符:

  1. > db.me.findOne()
  2. { "_id" : ObjectId("50e6edb517c789e46695212f"), "host" : "server-1" }

非常偶然的情况下,由于网络故障,可能会发现有多台服务器拥有相同的标识符。在这种情况下,只能知道其中一台服务器相对于主节点的新旧程度。所以,这可能会导致应用程序故障(如果应用程序需要等待特定数量的服务器完成写操作)和分片问题(数据迁移被复制到“大多数”备份节点之前,无法继续做数据迁移)。如果多台服务器拥有相同的"_id",可以依次登录到每台服务器,删除local.me集合,然后重新启动mongod。启动时,mongod会使用新的"_id"重新生成local.me集合。

如果服务器的地址发生了改变(假定"_id"没有变,但是主机名变了),可能会在本地数据库的日志中看到键重复异常(duplicate key exception)。遇到这种情况时,删除local.slaves集合即可(这比之前的例子简单,因为只需要清除旧数据即可,不需要处理数据冲突)。

mongod不会清理local.slaves集合,所以,它可能会列出某个几个月之前就不再把该成员作为同步源的服务器(或者是已经不在副本集内的成员)。由于MongoDB只是把这个集合用于报告复本集状态,所以这个集合中的过时数据并不会有什么影响。如果你觉得这个集合中的旧数据会造成困惑或者是过于混乱,可以将整个集合删除。几秒钟之后,如果有新的服务器将当前成员作为复制源的话,这个集合就会重新生成。

如果备份节点之间形成了复制链,你可能会注意到某个特定的服务器在主节点的local.slaves集合中有多个文档。这是因为,每个备份节点都会将复制请求转发给它的复制源,这样主节点就能够知道每个备份节点的同步源。这称为“影同步”(ghost syncs),因为这些请求并不会要求进行数据同步,只是把每个备份节点的同步源报告给主节点。

第12章 管理 - 图4`local数据库只用于维护复制相关信息,它并不会被复制。因此,如果希望某些数据只存在于特定的机器上,可以将这些数据保存在local数据库的集合中。

12.5 主从模式

MongoDB最初支持一种比较传统的主从模式(master-slave),在这种模式下,MongoDB不会做自动故障转移,而且需要明确声明主节点和从节点。有两种情形应该使用主从模式而不是副本集:需要多于11个备份节点,或者是需要复制单个数据库。除非迫不得已,否则都应该使用副本集。副本集更易维护,而且功能齐全。主从模式以后会被废弃,当副本集能够支持无限数据的成员时,主从模式很可能会被立即废弃。

但是,有时可能确实需要11台以上的备份节点(从节点),或者是需要复制单个数据库。这些情况下,应该使用主从模式。

如果要将服务器设为主节点,可以使用--master选项启动服务器。对于从节点,有两个可用的选项:--slave--source master--source用于指定同步源的主机名和端口号。注意,不要使用--replSet选项,因为现在是要设置主从模式,而不是副本集。

假如有两台服务器,server-0server-1,可以这么做:

  1. $ # server-0
  2. $ mongod --master
  3. $
  4. $ # server-1
  5. $ mongod --slave --source server-0:27017

这样,主从模式就设置成功了,不需要其他的设置。在主节点执行的写操作,会被复制到从节点上。

主从模式也可以用于复制单个数据库。可以使用--only选项选择需要进行复制的数据库。

  1. $ mongod --slave --source server-1:27017 --only super-important-db

驱动程序不会自动将读请求发送给从节点。如果要从从节点读取数据,需要显式地创建一个连接到从节点的数据库连接。

12.5.1 从主从模式切换到副本集模式

从主从模式切换到副本集模式,需要停机一段时间,步骤如下。

  • 停止系统的所有写操作。这非常重要,因为在主从模式下,从节点并不会维护一份oplog,所以它无法将升级期间落下的操作同步过来。
  • 关闭所有的mongod服务器。
  • 使用--replSet选项重启主节点,不再使用--master
  • 初始化这个只有一个成员的副本集,这个成员会成为副本集中的主节点。
  • 使用--replSet--fastsync选项启动从节点。通常,如果向副本集中添加一个没有oplog的成员,这个成员会立即进入完全的初始化同步过程。fastsync选项用于告诉新成员不会担心oplog的问题,直接从主节点最新的操作开始同步即可。
  • 使用rs.add()将之前的从节点加入副本集。
  • 对每个从节点,重复第5步和第6步。
  • 当所有从节点都变为备份节点之后,就可以开启系统的写功能了。
  • 从配置文件、命令行别名和内存中删除fastsync选项。这是一个非常危险的选项,它会使成员启动时跳过一些需要同步的操作。只有在从主从模式切换到副本集时才可以使用这个命令。现在已经切换完成了,不再需要这个选项了。

现在,主从模式已经被切换为副本集了。

12.5.2 让副本集模仿主从模式的行为

通常你会希望主节点长时间可用,因此,万一主节点不可用,应该允许自动故障转移。但是,对于某些副本集,你可能会要求手动选择新的主节点,不允许进行自动故障转移。这样的话,副本集的行为就跟主从模式一样了(对于这种情况,建议使用主从模式,而不是使用副本集)。

为了实现这个目的,需要重新配置副本集,将所有成员(除主节点之外)的priorityvotes设为0。这样一来,如果主节点挂了,不会有任何成员寻求被选举为主节点。另外,如果所有备份节点都挂了,主节点也仍然会一直保持主节点状态,不会退位(因为它是整个系统中唯一一个拥有投票权的成员)。

下面的配置文件会创建一个具有5个成员的副本集,其中server-0会始终作为主节点,其他4个成员会始终作为备份节点:

  1. {
  2. "_id" : "spock",
  3. "members" : [
  4. {"_id" : 0, "host" : "server-0:27017"},
  5. {"_id" : 1, "host" : "server-1:27017", "priority" : 0, "votes" : 0},
  6. {"_id" : 2, "host" : "server-2:27017", "priority" : 0, "votes" : 0},
  7. {"_id" : 3, "host" : "server-3:27017", "priority" : 0, "votes" : 0},
  8. {"_id" : 4, "host" : "server-4:27017", "priority" : 0, "votes" : 0}
  9. ]
  10. }

如果主节点挂了,管理员必须手动选出新的主节点。

如果要手动将某个备份节点提升为主节点,首先要连接到这个备份节点,然后执行强制重新配置,将它的priorityvotes修改为1,同时将先前的主节点的priorityvotes修改为0。

例如,如果server-0挂了,可以连接到希望提升为新的主节点的备份节点(比如server-1),然后以下面的方式修改配置:

  1. > var config = rs.config()
  2. > config.members[1].priority = 1
  3. > config.members[1].votes = 1
  4. > config.members[0].priority = 0
  5. > config.members[0].votes = 0
  6. > rs.reconfig(config, {"force" : true})

现在,如果运行rs.config(),就可以看到新的副本集配置信息了:

  1. > rs.config()
  2. {
  3. "_id" : "spock",
  4. "version" : 3
  5. "members" : [
  6. {"_id" : 0, "host" : "server-0:27017", "priority" : 0, "votes" : 0},
  7. {"_id" : 1, "host" : "server-1:27017"},
  8. {"_id" : 2, "host" : "server-2:27017", "priority" : 0, "votes" : 0},
  9. {"_id" : 3, "host" : "server-3:27017", "priority" : 0, "votes" : 0},
  10. {"_id" : 4, "host" : "server-4:27017", "priority" : 0, "votes" : 0}
  11. ]
  12. }

如果新的主节点又挂了,可以重复上面的步骤,手工将某个备份节点提升为新的主节点。