标准 专业
多元 极客

MongoDB研究院(3)——Replication——配置管理

单机模式运行成员

单机模式运行副本集中的成员的情况会经常发生,因为许多维护工作不能再备份节点上进行,也不能在主节点上进行,所以在进行维护工作时,需要以单机模式启动副本集成员。

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

sunshine:PRIMARY>  db.serverCmdLineOpts()
{
        "argv" : [
                "mongod",
                "--replSet",
                "sunshine",
                "-f",
                "mongodPrimary.conf",
                "--keyFile",
                "keyfile"
        ],
        "parsed" : {
                "config" : "mongodPrimary.conf",
                "net" : {
                        "port" : 27017
                },
                "replication" : {
                        "replSet" : "sunshine"
                },
                "security" : {
                        "authorization" : "enabled",
                        "keyFile" : "keyfile"
                },
                "storage" : {
                        "dbPath" : "D:\\Database\\MongoDB\\Data\\data"
                },
                "systemLog" : {
                        "destination" : "file",
                        "logAppend" : true,
                        "path" : "D:\\Database\\MongoDB\\Data\\log\\mongoDB.log"
                }
        },
        "ok" : 1
}

如果要对这台服务器进行维护,重启服务时,不使用–replSet参数即可。这样,它就会成为一个单机的mongod,无论它之前是主节点还是副本节点,现在都可以对它进行读和写。

如果我们不希望这台单机的mongod被副本集的成员通过端口号联系到,可以更改它所监听的端口。现在,我们的启动服务命令如下:

mongod -f mongod.conf

目前服务正在以单机模式运行,并且监听27020端口,副本集中的成员依然会对端口27017端口进行连接,但是始终连接失败,副本集中的成员会认为27017端口的服务已经挂掉。

当这个端口的服务已经维护完毕,可以以最原始的参数重新启动它。启动之后,它会自动与副本集中的其他成员进行同步。

副本集配置

创建副本集

在本系列文章的第一部分中,我们已经了解到使用rs.initiate(config)命令来实例化成为一个副本集,这里需要注意的一点是:

应该总是传递一个配置对象给rs.initiate()命令,否则MongoDB会自动生成一个针对单成员副本集的配置,其中的各项参数可能不是你想要的,而后,需要手动的一个一个添加及配置副本集中的其他成员。

修改副本集成员

在本系列文章的第一部分中,我们已经了解到可以使用rs.add()和rs.remove()命令来添加和删除副本集成员,这里需要注意的几点是:

  1. 不能修改成员的”_id”字段。
  2. 不能将接收rs.reconfig(config)命令的成员(通常是主节点)的优先级设为0(如果已经遗忘优先级为0的含义,可以对本系列第二部分内容进行回顾)。
  3. 不能将仲裁成员变为非仲裁成员,反之亦然。
  4. 不能将”buildIndexes”:false的成员修改为”buildIndexes”:true。

创建比较大的副本集

副本集不是无限制的大,MongoDB对副本集的大小做了限制。

在正常情况下,副本集最多只能拥有12个成员,其中只有7个成员拥有选举权,这么做的主要目的是减少心跳请求的网络流量和选举花费的时间。如果需要11个以上的副本节点,则需要将副本集切换为主从模式。如果要创建七个以上的副本集成员,则需要将超过七个成员的部分的投票数量设置为0,也就是第八个成员加入时的执行命令是:

sunshine:PRIMARY> rs.add({"_id" : 7, "host" : "Sunshine:27024", "votes" : 0})

这样可以阻止这些成员在选举中投主动票,但是它们依旧可以投反对票。应该尽量避免修改副本集成员的投票数量。不恰当的修改可能会对选举的结果产生怪异的或者难以理解的结果。

应该只在创建包含7个成员以上的副本集或者是防止故障转移时修改”votes”选项。修改”votes”参数并不会让某个成员优先成为主节点。如果希望某个成员可以优先被选举为主节点,应该使用优先级参数。

强制重新配置

强制重新配置一般发生在副本集无法满足“大多数的要求”,这种情况就无法选举出主节点,这时就无法进行写操作,一般情况下就需要重新配置副本集。但是我们通常都是想配置文件发送至主节点。在这种情况下,我们可以在备份节点上强制配置副本集,具体命令如下:

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

普通配置与强制配置需要遵守相关规定:必须使用正确的reconfig选项将有效的、格式完好的配置文件发送至成员。”force”选项不允许无效的配置,而且只允许将配置发送至副本节点。

副本节点收到配置文件后,就会修改自身配置,并且将新的配置发送给副本集中的其他成员。副本集中的其他成员收到新的配置文件后,会判断配置文件的发送者是否是它们配置中的一个成员,如果是,才会用的配置文件进行重新配置。所以,如果新的配置文件包含修改副本集成员的主机名,那么需要将配置文件发送至主机名不会发生改变的副本节点上。

若是将要修改所有的副本节点的主机名,则需要单机启动其中一个副本节点,手动修改local.system.replset文档,然后重新启动。

修改成员状态

把主节点变为副本节点

可以使用setDown函数将主节点降级为副本节点:

sunshine:PRIMARY> rs.stepDown()

这个命令可以使主节点退化成副本节点,并且维持60秒,如果这段时间内没有新的主节点被选举出来,这个节点就可以要求重新进行选举,如果希望主节点退化为副本节点的时间持续的更长,该函数也支持指定时间(单位:秒):

sunshine:PRIMARY> rs.stepDown(600)

阻止选举

如果需要对主节点做一些维护,但是不希望这个时间段内,其他副本集成员选举成为主节点,那么可以在每个备份节点上使用freeze函数,以强制它们始终处于副本节点状态(单位:秒):

sunshine:PRIMARY> rs.freeze(1000000)

但是维护完成之后,发现当时手抖,多输了几个零,导致还有几亿秒之后才能恢复副本节点的选举权,怎么办?执行以下命令即可:

sunshine:PRIMARY> rs.freeze(0)

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

使用维护模式

什么是维护模式?

当在副本集成员上执行某个非常耗时的操作时,这个成员就会进入维护模式,也就是强制该节点进入RECOVERING状态。

如果你想做一些耗时的查询,比如说查询上亿集合的文档总数,可以通过以下命令强制使一个成员进入维护模式:

sunshine:PRIMARY> db.adminCommand({"replSetMaintenanceMode" : "true"})

从命令就可以看出,这个命令具有很高的执行权限。所以,在非常必要的情况,使用这个命令需要专业人士进场指导,维护完毕后,可以使用以下命令将成员从维护模式中恢复:

sunshine:PRIMARY> db.adminCommand({"replSetMaintenanceMode" : "false"})

监控复制

监控副本集的状态非常重要,不仅要监控副本集成员是否可用,还需要监控每个成员处于什么状态,以及每个成员数据的新旧程度。

获取状态

replSetGetStatus是一个非常有用的命令,可以返回副本集中的每个成员当前的信息,示例如下:

sunshine:PRIMARY> db.runCommand("replSetGetStatus")
{
        "set" : "sunshine",
        "date" : ISODate("2016-07-27T14:53:43.518Z"),
        "myState" : 1,
        "term" : NumberLong(9),
        "heartbeatIntervalMillis" : NumberLong(2000),
        "members" : [
                {
                        "_id" : 0,
                        "name" : "Sunshine:27017",
                        "health" : 1,
                        "state" : 1,
                        "stateStr" : "PRIMARY",
                        "uptime" : 261,
                        "optime" : {
                                "ts" : Timestamp(1469631031, 1),
                                "t" : NumberLong(9)
                        },
                        "optimeDate" : ISODate("2016-07-27T14:50:31Z"),
                        "electionTime" : Timestamp(1469631030, 1),
                        "electionDate" : ISODate("2016-07-27T14:50:30Z"),
                        "configVersion" : 3,
                        "self" : true
                },
                {
                        "_id" : 1,
                        "name" : "Sunshine:27018",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 195,
                        "optime" : {
                                "ts" : Timestamp(1469631031, 1),
                                "t" : NumberLong(9)
                        },
                        "optimeDate" : ISODate("2016-07-27T14:50:31Z"),
                        "lastHeartbeat" : ISODate("2016-07-27T14:53:42.870Z"),
                        "lastHeartbeatRecv" : ISODate("2016-07-27T14:53:43.487Z"),
                        "pingMs" : NumberLong(0),
                        "syncingTo" : "Sunshine:27017",
                        "configVersion" : 3
                },
                {
                        "_id" : 2,
                        "name" : "Sunshine:27019",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 171,
                        "optime" : {
                                "ts" : Timestamp(1469631031, 1),
                                "t" : NumberLong(9)
                        },
                        "optimeDate" : ISODate("2016-07-27T14:50:31Z"),
                        "lastHeartbeat" : ISODate("2016-07-27T14:53:41.843Z"),
                        "lastHeartbeatRecv" : ISODate("2016-07-27T14:53:43.038Z"),
                        "pingMs" : NumberLong(0),
                        "syncingTo" : "Sunshine:27018",
                        "configVersion" : 3
                }
        ],
        "ok" : 1
}

你对上面输出的内容有点似曾相识的感觉,对,没错,这个命令还有个对应的辅助函数:rs.status()。
下面对输入内容中的字段进行解析。

  • self
    这个字段只会出现执行这个命令的成员上,比如我现在是在Sunshine:27017上执行这个命令,所以输出内容中的Sunshine:27017对象中就会存在这个属性。
  • stateStr
    用于描述服务器状态。
  • uptime
    从成员可到一直到现在所经历的时间(单位:秒)。对于Sunshine:27018,在过去的195秒内处于可用状态,对于Sunshine:27017,服务已经启动了261秒。
  • optimeData
    成员oplog中最后一个操作发生的时间。注意,这里的状态是指每个成员通过心跳连接报告上来的,所以跟实际时间可能会有一段时间的误差。
  • lastHeartBeat
    当前节点最后一次收到其他成员心跳的时间。
  • pingMs
    心跳从当前服务器到达某个成员所花费的平均时间,可以根据这个字段选择从哪个成员上进行复制操作。
  • errmsg
    成员在心跳请求中返回的状态信息。
  • syncingTo
    该节点目前从哪个成员上进行复制操作。
  • health
    该节点是否可达。
    注意,statestateStr都是表示成员的状态。optimeoptimeData的值也是相同的,只是表示的方式不同。

复制图谱

上一小节中提到了replSetGetStatus命令中的syncingTo参数,我们分别在每个成员上运行replSetGetStatus命令,就可以弄清楚复制图谱。
根据上面的输入内容,我们可以看出,Sunshine:27017是Sunshine:27018的同步源,Sunshine:27018是Sunshine:27019的同步源。
MongoDB会根据ping时间选择同步源。一个节点向另一个节点发送心跳请求,就可以知道心跳请求所耗费的时间。MongoDB委会这不同节点间请求的平均花费时间。选择同步源时,会选择一个离自己比较近而且数据比较新的成员。如果在数据中心中添加一个新成员,那么它很有可能会从当前数据中心的某个节点上进行复制操作。
这种自动复制连也有一些缺点,有的时候副本节点越多,复制链越长,将写操作复制到所有服务器所花费的时间越长。如果我们的复制链中的每个副本节点都比前面的副本节点落后一点点,那么我们可以使用以下命令强制修改复制源:

sunshine:PRIMARY> secondary.adminCommand({"replSetSyncFrom" : "Sunshine:27017"})

复制循环

如果复制链中出现了循环,那么就称为发生了复制循环。举个例子,如果A从B处复制数据,B从C数据,C从A处复制数据,那么这就是一个复制循环。因为复制循环中的节点都不会被选举成为主节点,所以这些成员无法复制新的写操作,就会落后于主节点,影响系统的整体性能。但在一般的情况下,如果我们设置每个节点都是自动选取复制源,那么复制循环是不可能发生的。所以在我们手动修改复制源的时候,需要注意当前副本集的状态信息,避免造成复制循环,必要的时候请专业人士进场。

禁用复制链

为了避免人为操作对系统性能造成的影响,我们可以禁用复制链,强制要求每个节点都是从主节点复制数据,示例如下:

sunshine:PRIMARY> var config = rs.config()
sunshine:PRIMARY> config.settings = config.settings || {} //如果子对象不存在,那么就创建一个空对象
sunshine:PRIMARY> config.settings.allowChaining = false
sunshine:PRIMARY> rs.reconfig(config)

计算延迟

跟踪复制情况的一个重要的指标就是主节点与备份节点之间的延迟程度。

那么什么是延迟?

延迟(lag)是指副本节点相对于主节点的落后程度,是主节点最后一次操作时间戳与副本节点最后一次操作时间戳之差。

除了使用rs.status()函数或者是db.runCommand(“replSetGetStatus”)命令来查看optimeDate之外,也可以在主节点上执行db.printReplicationInfo(),在副本节点上执行db.printSlaveReplicationInfo()快速得到一份摘要信息,具体示例如下:

sunshine:PRIMARY> db.printReplicationInfo()
configured oplog size:   3186.72802734375MB
log length start to end: 826764secs (229.66hrs)
oplog first event time:  Mon Jul 18 2016 09:11:07 GMT+0800
oplog last event time:   Wed Jul 27 2016 22:50:31 GMT+0800
now:                     Wed Jul 27 2016 23:40:56 GMT+0800

输出内容重包括oplog的大小,oplog操作时间范围。

在副本节点上执行db.printSlaveReplicationInfo()命令:

sunshine:SECONDARY> db.printSlaveReplicationInfo()
source: Sunshine:27018
        syncedTo: Wed Jul 27 2016 22:50:31 GMT+0800
        0 secs (0 hrs) behind the primary
source: Sunshine:27019
        syncedTo: Wed Jul 27 2016 22:50:31 GMT+0800
        0 secs (0 hrs) behind the primary

就可以得到同步时间,落后于主节点时间等信息。

调整oplog大小

主节点oplog的长度可以看做维护工作的时间长度。如果主节点的oplog长度是一小时,那么你只有一小时的时间用来修复各种错误,不然的话,副本节点可能会落后主节点太多,导致必须进行完全同步,影响各个节点的负载。
每个可能成为主节点的节点都应该拥有足够大的oplog,用来预留出足够多的维护时间。
如何增加oplog的长度?

  1. 如果当前节点是主节点,使用stepDown()命令或者其他方法使其退位,一遍让其他成员的数据能够尽快地更新到与其一致。
  2. 关闭当前节点,使用单机模式启动此节点。
  3. 临时将oplog中的最后一条记录保存到其他集合中。
  4. 删除当前的oplog。
    > db.oplog.rs.drop()
    
  5. 创建一个新的oplog。
    > db.createCollection("oplog.rs", {"capped" : true, "size" : 10000})
    
  6. 将最后一条记录写入oplog中。注意,确保将最后一条操作记录成功插入到oplog中。如果插入失败,把当前节点重新加回副本集后,该节点会删除所有的数据,然后重新进行一次完整的同步。
  7. 将当前节点以副本集模式重新启动。

从延迟备份节点中恢复

如果线上操作人员不小心删除了一个数据库或者是一个集合,而幸好你又有一个延迟备份节点,这口大锅就可以暂时卸下了。

我们可以明确将延迟节点指定为数据源,放弃其他成员的数据,下面介绍一种最简单的方法

  1. 关闭除延迟节点外的成员。
  2. 删除其他成员数据目录中的所有数据。
  3. 重启所有成员,然后他们会自动从延迟备份节点中复制数据。
    这种方式可能简单粗暴。但是,在其他成员完成初始化同步之前,副本集中将只有一个节点可用,而这个节点很有可能过载。
    根据实际情况,我们对上面的方法进行一些优化
  4. 关闭副本集中的所有成员(包括延迟节点)。
  5. 删除出延迟节点外的其他成员的数据目录。
  6. 将延迟节点的数据复制到其他节点的数据目录下。
  7. 重启启动副本集。
    但是这样做的弊端是,所有的节点都拥有和延迟节点同样大小的oplog,如果需要调整oplog大小,请查看本篇文章的“调整oplog大小”中的内容。

创建索引

如果向主节点发送创建索引命令,主节点会正常创建索引,然后备份节点在复制“创建索引”操作时也会创建索引。但是创建索引是一个需要消耗大量资源的操作,可能会导致成员不可用。因此,我们可能希望每次只在一个节点上创建索引,以降低对系统的影响。那么,我们应该如何做呢?做法其实很简单,就是在单机模式下创建索引。

现在副本集中除了主节点之外的成员都拥有你想要的索引了,如何在不影响系统性能的前提下对主节点创建索引?你有两个选择:

  1. 在主节点上创建索引。如果系统会有一段负载比较小的空闲期,那会是非常棒的创建索引的最佳时刻。同时修改连接驱动的首选项,在主节点创建索引期间,将读请求发送至副本节点上。主节点创建索引之后,其实副本节点依然会复制这个操作,但是由于副本节点中已经有了同样的索引,实际上不会再次创建索引。
  2. 让主节点退化成为副本节点。以单机模式启动主节点,然后进行创建索引操作,这个时候,副本集会自动选举出新的主节点。待索引创建完成后,重新以副本集的身份启动该节点。注意,可以使用这种方式为某个副本几点创建与其他成员不同的索引,这种方式做离线数据处理时非常有用。但是,如果某个副本节点的索引与其他成员不同,那么它永远不能成为主节点,应该将它的优先级设为0。

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

如果预算有限,无法配备多态高性能服务器,可以考虑将副本节点只用于灾难恢复。这样的副本节点不需要太大的内存和高性能CPU,也不需要硬盘具有较好的IO性能。这样,使用高性能服务器作为主节点,比较便宜的服务器只用于备份,所有的请求都路由至主节点。如果你需要这样的备份,应该基于下面这样的配置。

  • priority : 0优先级为0的副本节点永远不会成为主节点。
  • hidden : true将副本节点设为隐藏,应用就永远不会将请求路由到此节点,以防配置过程中出现差池,影响线上服务的运行。
  • buildIndexes : false如果在副本节点上创建索引的话,会极大地降低副本节点的性能。如果没有在副本节点上创建索引,那么从副本节点中恢复数据后,需要重新创建索引。
  • votes : 0在只有两个节点情况下,如果将副本节点的投票数设为0,那么当副本节点挂掉后,主节点仍然会满足“大多数”的要求。如果有三个以上的节点,如果(节点个数-只作备份节点个数) % 2 = 0,那么应该在剩余的除了主节点外的其他节点上 运行一个仲裁者,用来满足选举过程中“大多数”情况的出现。

主节点如何跟踪延迟

作为同步源的节点,会维护一个local.slaves的集合,这个集合中保存着所有正从当前节点进行数据同步的成员,以及每个成员数据的新旧程度。local.slaves集合实际上是内存数据结构中的回声,所有数据可能会有几秒钟的延迟。

sunshine:SECONDARY> db.slaves.find()
{"_id" : ObjectId("578c2c5737cfcca1e8aed46a"), "hosts" : "Sunshine:27017", "ns" : "local.oplog.rs", "syncedTo" : {"t" : 1232131243254, "i" : 1}}
{"_id" : ObjectId("578c2c72241b09ce33d53946"), "hosts" : "Sunshine:27018", "ns" : "local.oplog.rs", "syncedTo" : {"t" : 1232131243254, "i" : 1}}

如果由于网络的原因,可能会发现有多台服务器拥有相同的标识符。在这种情况下,我们只能知道其中一台服务器相对于主节点的新旧程度。所以,这可能会导致线上服务故障(如果写操作需要复制到特定数量的节点)和分片问题(数据迁移被复制到“大多数”备份节点之前,无法继续做数据迁移)。凡是都有解决方法,针对这种多个节点拥有相同的”_id”的情况,我们可以一次登录到每个节点,删除local.me集合,然后重新启动mongod,启动时,mongod会自动生成新的”id”,并重新创建local.me集合。

如果节点的地址发生了改变(hosts名称发生改变),可能会在本地数据库的日志中看到键重复异常(duplicate key exception)。遇到这种情况时,删除local.slaves集合即可(不需要处理数据冲突)。

mongod不会清理local.slaves集合,所以,它可能会列出一段时间之前就不再把该节点作为同步源的服务器。由于这个集合只是用于报告副本集状态,所以这个集合中的过时数据并不会有什么影响,如果将集合删除,再有新的节点将当前节点作为复制源,local.slaves集合就会重新创建。

local数据库只用于维护复制相关信息,它并不会被复制。如果希望某些数据只保留在特定的节点上,可以将这些数据保存在local数据库中。

主从模式

主从模式是MongoDB最初支持的一种比较传统的分布式负载模式。在这种模式下,MongoDB不会做自动故障转移,而且需要明确规定主节点和从节点。既然MongoDB仍然没有取消主从模式,证明MongoDB认为它仍有可用之处。有两种情形下需要使用主从模式:需要多于11个备份节点,或是需要复制单个数据库。否则,除非迫不得已,否则都应该使用副本集。副本集更易维护,而且功能齐全,如果副本集在今后能够支持无限个成员时,主从模式可能会被完全替代。

怎样使用主从模式?

如果要将节点设为主节点,可以使用–master参数启动服务。

mongod --master -f mongodMaster.conf

如果要将节点设为从节点,可以使用–slave和–source ip:port

mongod --slave --source Sunshine:27030 -f mongodSlaveOne.conf

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

主从模式也可以用于复制单个数据库,可以使用–only参数选择需要进行复制的数据:

mongod --slave --source Sunshine:27030 --only test

驱动程序不会自动将读请求路由至从节点。如果需要从从节点上读取数据,需要显示地创建一个到从节点的连接。

主从模式切换副本集模式

从主从模式切换至副本集模式,需要停机一段时间并执行以下步骤:

  1. 停止对数据库的写操作。在主从模式下,从节点并不会维护操作日志,切换期间的写操作日志无法同步到副本集上。
  2. 关闭所有的mongod服务。
  3. 使用–replSet参数,也就是副本集服务启动参数,重新启动mongod服务。
  4. 初始化该节点,这个成员会自动成为副本集的主节点。
  5. 使用–replSet和–fastsync参数启动从节点。如果一个新成员加入到副本集中,该成员会立即进入完全的初始化同步过程。fastsync参数是让从节点从主节点最新的操作开始同步即可。
  6. 在主节点上使用rs.add()命令将已经启动的从节点加入副本集中。
  7. 当所有从节点都变为副本节点后,恢复服务的写操作。
  8. 从配置文件中删除fastsync参数。这个参数会使成员在启动时,跳过一些需要同步的操作,会影响数据的一致性。只有在主从模式切换到副本集时,才会使用这个命令,既然已经完成切换,就需在当前副本集中完全删除这个参数。

副本集模仿主从模式

如果我们希望在主节点出现问题时,手动选择新的主节点,不允许进行自动故障转移,就需要重新配置副本集,将所有成员的priority和votes的值设为0。如果主节点挂掉,就不会有任何成员选举成为主节点。这时,我们就需要手动选举出新主节点。

我们需要连接到希望被选举成为主节点的服务器和端口,然后执行强制重新配置,将它的priority和votes的值修改为1,同时将前一个主节点的priority和votes的值修改为0,最后检查下副本集配置,确保无误。

这样,目前副本集的行为就跟主从模式一样,对于这种情况,我个人建议使用主从模式而非副本集模式,因为这样的配置极易影响线上服务,对配置操作要求也很高,不利于对数据库的维护。

赞(1) 投币

评论 抢沙发

慕勋的实验室慕勋的研究院

码字不容易,路过请投币

支付宝扫一扫

微信扫一扫