标准 专业
多元 极客

MongoDB研究院(1)——Replication——初识副本集

复制简介

在之前的学习中,我们一直使用的是单台服务器,一个mongod服务器进程。如果以这种方式应用到生产环境,那么风险会很高。当服务器宕机时,会造成一段时间的服务不可访问,数据丢失等故障,这会对我们提供的服务造成很大的影响。
MongoDB的复制功能,可以将数据副本同步的到多态服务器上,即使一台服务器出错,也可以程序的正常运行和数据安全。
MongoDB中,创建一个副本集之后就可以使用复制功能了。副本集是一组服务器,其中包含一个主服务器和若干备份服务器。主服务器用于处理程序请求,备份服务器用于保存主服务器的数据副本。如果主服务器发生故障,备份服务器会从中选举出一个主服务器,继续处理程序请求。

快速建立一个副本集

首先使用–nodb选项启动一个mongo shell,这个命令可以在不连接任何mongod的情况下启动shell。

mongo --nodb

通过下面的命令就可以创建一个副本集:

replicaSet = new ReplSetTest({"nodes":3})

然后命令行会出现一长串的运行命令,这是MongoDB正在配置副本集。这行代码创建了一个包含了三个服务器的副本集:一个主服务器和两个备份服务器。
执行下列命令,启动和初始化副本集:

// 启动副本集
replicaSet.startSet();
// 初始化副本集
replicaSet.initiate();

现在已经有了3个mongod进程,分别运行在20000、20001、20002端口,在这三个进程中的任意一个进程上进行数据库操作,操作记录都会打到当前的控制台上,为了方便操作,我们可以将当前的控制台窗口当做后台,我们开启一个新的命令行窗口用于操作。
在新的命令行窗口中,运行以下命令:

connOne = new Mongo("localhost:20000")

当我们连接到一个副本集成员时,提示符变成了“testReplSet:PRIMARY>”,其中,”PRIMARY”代表当前状态,“testReplSet”代表副本集的标识符,“testReplSet”是ReplSetTest使用的默认名称。
在当前主节点上执行isMaster()命令,可以查看副本集的状态信息

> primaryDB.isMaster()
{
    "hosts" : [
            "Sunshine:20000",
            "Sunshine:20001",
            "Sunshine:20002"
    ],
    "setName" : "testReplSet",
    "setVersion" : 1,
    "ismaster" : true,
    "secondary" : false,
    "primary" : "Sunshine:20000",
    "me" : "Sunshine:20000",
    "electionId" : ObjectId("7fffffff0000000000000001"),
    "maxBsonObjectSize" : 16777216,
    "maxMessageSizeBytes" : 48000000,
    "maxWriteBatchSize" : 1000,
    "localTime" : ISODate("2016-07-13T14:36:50.471Z"),
    "maxWireVersion" : 4,
    "minWireVersion" : 0,
    "ok" : 1
}

其中有一个很重的字段指明了当前节点是主节点。

{"ismaster" : true}

hosts数组中列出了该副本集中的服务器IP及端口号。

如果isMaster()命令中返回的是false,证明当前节点不是主节点。这时,可以根据键primary的值来获取主节点服务器地址及端口,重新创建连接到主节点服务器即可。

接下来,我们向主节点中的一个集合中插入1000条数据。

> for(var i = 0; i < 1000; i++){
        primaryDB.test.insert({number:i})
}
WriteResult({ "nInserted" : 1 })
> primaryDB.test.count()
1000

检查其中一个副本集成员,验证下是否有刚刚插入的数据,(注意:备份节点可能会落后于主节点,可能没有最新写入的数据,所以可能会拒绝读取请求,以防止应用程序获取到过期的数据)。

> secondary.test.find().pretty()
Error: error: { "ok" : 0, "errmsg" : "not master and slaveOk=false", "code" : 13435 }

这时,我们需要设置setSlaveOk()指令来告诉副本节点从副本节点中读取数据没有问题:

> connTwo.setSlaveOk()

然后使用刚才的命令继续查询:

> secondaryDB.test.find().pretty()
{ "_id" : ObjectId("578655359ab19c68f1e1ceca"), "number" : 0 }
{ "_id" : ObjectId("578655359ab19c68f1e1cecb"), "number" : 1 }
{ "_id" : ObjectId("578655359ab19c68f1e1cecc"), "number" : 2 }
{ "_id" : ObjectId("578655359ab19c68f1e1cecd"), "number" : 3 }
{ "_id" : ObjectId("578655359ab19c68f1e1cece"), "number" : 4 }
{ "_id" : ObjectId("578655359ab19c68f1e1ced3"), "number" : 9 }

尝试着向副本节点中执行写操作:

> primaryDB.test.insert({"number":1001})
WriteResult({ "writeError" : { "code" : 10107, "errmsg" : "not master" } })

会提示:我是副本节点,写入的功能需要去找主节点来完成,副本节点只能通过复制功能写入数据。
这时候就有一个问题了,如果主节点挂了,那么我们就无法写入了数据了?伟大的架构师们总有他们的解决办法,我们先将主节点关掉:

> primaryDB.adminCommand({"shutdown":1})

然后使用isMaster()命令:

> secondaryDB.isMaster()
{
    "hosts" : [
            "Sunshine:20000",
            "Sunshine:20001",
            "Sunshine:20002"
    ],
    "setName" : "testReplSet",
    "setVersion" : 1,
    "ismaster" : false,
    "secondary" : true,
    "primary" : "Sunshine:20002",
    "me" : "Sunshine:20001",
    "maxBsonObjectSize" : 16777216,
    "maxMessageSizeBytes" : 48000000,
    "maxWriteBatchSize" : 1000,
    "localTime" : ISODate("2016-07-13T15:09:29.471Z"),
    "maxWireVersion" : 4,
    "minWireVersion" : 0,
    "ok" : 1
}

可以看见,主节点已经由之前的Sunshine:20000,变成现在的Sunshine:20002了。
这个功能就是MongoDB自由的自动故障转移功能(automantic failover),如果主节点挂了,其中一个副本节点会自动选举为主节点,不影响写入操作。
isMaster()早在MongoDB只支持主从复制的时候就存在了,它与副本集的术语不完全,比如:isMaster()中的主节点(master)相当于副本集中的主节点(primary),从节点(slave)相当于副本节点(secondary)。
初步认识副本集已经基本结束,接下来我们停止副本集:

replicaSet.stopSet()

MongoDB的复制功能,有几个关键点:

  • 客户端在单台服务器上可以执行的请求,都可以发送到主节点上进行(读,写,执行命令,创建索引等)。
  • 客户端不能再副本节点上执行写入操作。
  • 默认情况下,客户端不能从备份节点中读取数据。在备份节点上显式地执行setSlaveOk()命令后,客户端才可以从副本节点中读取数据。

配置副本集

在实际的部署中,我们会在不同服务器,不同机房上建立复制功能,从而实现分地域部署、机房互备等运维层面的功能。
首先,我们先写好三个mongod.conf,然后我们通过以下命令,,使用创建好的mongod.conf文件,已经–keyFile命令参数启动mongod进程:

mongod --replSet sunshine -f mongod.conf

注意:windows下没有–fork命令,sunshine代表标识符。
现在已经启动了三个mongod实例,但是不知道彼此的存在。

接下来,我们声明一个config对象,里面是将要实例化为一个副本集的所有成员的信息:

> config = {

}
{
        "_id" : "sunshine",
        "members" : [
                {
                        "_id" : 0,
                        "host" : "Sunshine:27017"                  
                },
                {
                        "_id" : 1,
                        "host" : "Sunshine:27018"
                },
                {
                        "_id" : 2,
                        "host" : "Sunshine:27019"
                }
        ]
}

接下来,就应该在预设的主节点上实现这个配置,然后通过主节点,将配置同步到其他的副本节点上,完成副本集的配置:

> rs.initiate(config)
{ "ok" : 1 }  

稍等片刻,如果发现前缀名称变为“sunshine:primary>”就代表实例化成功了,火速去连接下其他两个副本节点,发现是不是前缀名称已经变为“sunshine:secondary>”了?
然后我们查看下当前副本集的状态:

sunshine:PRIMARY> rs.status()
{
        "set" : "sunshine",
        "date" : ISODate("2016-07-18T01:12:00.164Z"),
        "myState" : 1,
        "term" : NumberLong(1),
        "heartbeatIntervalMillis" : NumberLong(2000),
        "members" : [
                {
                        "_id" : 0,
                        "name" : "Sunshine:27017",
                        "health" : 1,
                        "state" : 1,
                        "stateStr" : "PRIMARY",
                        "uptime" : 187,
                        "optime" : {
                                "ts" : Timestamp(1468804278, 2),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2016-07-18T01:11:18Z"),
                        "infoMessage" : "could not find member to sync from",
                        "electionTime" : Timestamp(1468804278, 1),
                        "electionDate" : ISODate("2016-07-18T01:11:18Z"),
                        "configVersion" : 1,
                        "self" : true
                },
                {
                        "_id" : 1,
                        "name" : "Sunshine:27018",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 52,
                        "optime" : {
                                "ts" : Timestamp(1468804278, 2),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2016-07-18T01:11:18Z"),
                        "lastHeartbeat" : ISODate("2016-07-18T01:11:59.348Z"),
                        "lastHeartbeatRecv" : ISODate("2016-07-18T01:11:59.018Z"),
                        "pingMs" : NumberLong(0),
                        "syncingTo" : "Sunshine:27017",
                        "configVersion" : 1
                },
                {
                        "_id" : 2,
                        "name" : "Sunshine:27019",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 52,
                        "optime" : {
                                "ts" : Timestamp(1468804278, 2),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2016-07-18T01:11:18Z"),
                        "lastHeartbeat" : ISODate("2016-07-18T01:11:58.305Z"),
                        "lastHeartbeatRecv" : ISODate("2016-07-18T01:11:59.017Z"),
                        "pingMs" : NumberLong(0),
                        "syncingTo" : "Sunshine:27017",
                        "configVersion" : 1
                }
        ],
        "ok" : 1
}

Anything is under control!
既然副本集已经配置完成了,这就需要我们来验证下副本集的工作情况了,Testing项目如下:

sunshine:PRIMARY> for(var i=0; i < 1000; i++){
                         db.test.insert({"count":i})
                     }
WriteResult({ "nInserted" : 1 })
sunshine:SECONDARY> db.getMongo().setSlaveOk()
sunshine:SECONDARY> show collections
test
sunshine:SECONDARY> db.test.find().count()
1000

小贴士 rs是一个全局变量,其中包含了与复制相关的辅助函数(可以通过执行rs.help()查看可用的辅助函数),这些函数大多是数据库命令的包装器,比如:db.adminCommand({“replSetInitiate” : config})等价于rs.initiate(config),对辅助函数和底层的数据库命令做些了解是非常好的,有时直接使用数据库命令比使用辅助函数要简单。

rs.add(“ip:port”)与rs.remove(“ip:port”)用于从副本集中添加和删除成员,重新配置副本集时,主节点需要先退化为副本节点,以便接受新的配置,然后会恢复。重新配置完副本集之后,副本集中会暂时没有主节点,之后会一切恢复正常。恢复正常后,可以使用rs.config()命令查看配置修改是否成功。

每次配置成功后,配置信息中的“version”字段都会自增(它的初始值为1)。

我们不仅可以添加或者删除副本集成员,还可以修改现有的成员,可以通过如下方式对当前配置进行修改:

sunshine:PRIMARY> var config = rs.config()
sunshine:PRIMARY> config.members[1].host = "localhost:27018"
sunshine:PRIMARY> rs.reconfig(config)

对于复杂的副本集配置修改,rs.reconfig()通常比rs.add()和rs.remove()更为实用。

设计副本集

副本集中很重要的一个概念就是“大多数”(majority),即我们平时所说的“少数服从多数”,“大多数”在MongoDB中定义为“副本中一半以上的成员”,具体表现在:选择主节点时需要由大多数决定,主节点只有在得到大多数支持时才能继续作为主节点,写操作被复制到大多数副本节点时这个写操作就是安全的。网络任何一端无法达到“大多数”的条件,这个副本集将会退化为拥有两个副本节点的副本集(没有主节点)。

下面是常用的两种副本集推荐配置,仅供参考:

  • 将大多数成员放在同一个数据中心。这样的话,你将拥有一个主数据中心,并且主节点总是位于主数据中心。
  • 在两个数据中心放置数量相等的成员,在第三个位置放置一个用于天平走向的副本集成员。如果两个数据中心同等重要,这样的配置比较好,因为任意一个数据中心的服务器都可以找到另一台服务器以达到大多数。

选举机制

选举机制

当一个备份节点无法与主节点连通时,它就会联系并请求其他的副本集成员将自己选举为主节点。
其他成员会做几项理性的分析:自身能否与主节点连通,希望选举成为主节点的副本节点的数据是否最新,有没有其他更高优先级的成员可以被选举成为主节点。

如果要求被选举成为主节点的成员能够得到副本集中“大多数”成员的投票,它就会成为主节点。如果成员发现任何原因,表明当前希望成为主节点的成员不应该成为主节点,那么它就会否决此次选举。希望成为主节点的候选人,必须使用复制将自己的数据更新为最新,副本集中的其他成员会对此进行检查。每个成员都只能要求自己被选举为主节点,简单起见,不能推荐其他成员选举为主节点,只能为申请成为主节点的候选人投票。

下面是一个副本集失去主节点后,成员选举的日志记录:

[ReplicationExecutor] Starting an election, since we've seen no PRIMARY in the past 10000ms
[ReplicationExecutor] conducting a dry run election to see if we could be elected
[ReplicationExecutor] dry election run succeeded, running for election
[ReplicationExecutor] election succeeded, assuming primary role in term 6
[ReplicationExecutor] transition to PRIMARY
[rsSync] transition to primary complete; database writes are now permitted

选举仲裁者

仲裁者是MongoDB支持的一种特殊类型的成员,面对两个成员的副本集在“大多数”要求上的缺点,仲裁者的唯一作用就是:参与选举。仲裁者并不保存数据,也不会为客户端提供服务,他只是为了帮助具有偶数个成员的副本集能够满足“大多数”这个条件。

因为仲裁者不需要履行传统mongod服务器的责任,所以可以将仲裁者作为轻量级进程,运行在配置较差的服务器上。如果可能,应该将仲裁者放在单独的故障域(failure domain)中,与其他成员分开,这样就可以以“外部视角”来观察副本集中的成员。
可以使用如下两个命令添加仲裁者:

sunshine:PRIMARY> rs.addArb("ip:port")
sunshine:PRIMARY> rs.add({"id" : 4, "host" : "ip:port", "arbiterOnly" : true})

成员一旦以仲裁者的身份添加到副本集中,它就永远只能是仲裁者,无法将仲裁者重新配置为非仲裁者。
使用仲裁者的注意事项:
最多只能使用一个仲裁者,如果当前副本集节点的个数是奇数,就不需要仲裁者;如果是偶数,在主节点选举时,这时候就需要仲裁者投出关键性的一票。添加额外的仲裁者,并不能加快选举速度,也不能提供更好的数据安全性。

优先级

优先级用于表示一个成员渴望成为主节点的程度。

优先级的取值范围可以是0~100,默认值是1。如果将默认值设置为0,那么该成员将永远不能够成为主节点,这样的成员成为被动成员(passive member)。

拥有最高优先级的成员会有限选举为主节点,只要它能够得到集合中的“大多数”的选票,并且数据是最新的。

假设在副本集中添加一个优先级为10的成员:

sunshine:PRIMARY> rs.add({"id" : 4, "host" : "ip:port", "priority" : 10})

目前副本集中的其他成员,由于没有设置优先级,所以他们的优先级都是1,只要新加入的成员拥有最新的数据,那么当前的主节点就会退化为副本节点,这个新成员会被选举成为主节点。
使用优先级时需要注意,修改副本集配置时,新的配置必须要发送给在新配置下的可能成为主节点的成员。因此,无法再一次reconfig操作中奖当前主节点的优先级设置为0,也不能对所有成员优先级都为0的副本集执行reconfig操作。

隐藏成员

顾名思义,隐藏成员就是不可见成员(相对来说)。客户端不会向隐藏成员发送请求,隐藏成员也不会作为复制源,因此,很多人会将配置不够的服务器或者备份服务器隐藏起来。注意,只有优先级为0的成员才能被隐藏起来,而且,主节点不能够被隐藏。

将Sunshine:27019副本节点隐藏:

sunshine:PRIMARY> var config = rs.config()
sunshine:PRIMARY> config.members[2].hidden = 0
0
sunshine:PRIMARY> config.members[2].priority = 0
0
sunshine:PRIMARY> rs.reconfig(config)
{ "ok" : 1 }
sunshine:PRIMARY> rs.isMaster()
{
        "hosts" : [
                "Sunshine:27017",
                "Sunshine:27018"
        ],
        ...
}

使用rs.status()和rs.config()能够看到隐藏成员,之所以隐藏是相对来说的,就是因为,如果真的隐藏起来,那么该节点将永远不可见,这将会是一个愚蠢的设计,所以MongoDB将隐藏成员设计为只对isMaster()不可见。你可能会问,三个命令中有两个命令可以看到隐藏成员,这还叫隐藏吗?事实上,对调用方来讲,客户端连接到副本集时,首先会调用isMaster()来查看可用成员,因此,隐藏成员对调用方是不可见的。

将隐藏成员恢复为可见:

sunshine:PRIMARY> config.members[2].hidden = false
false
sunshine:PRIMARY> config.members[2].priority = 1
1
sunshine:PRIMARY> rs.reconfig(config)
{ "ok" : 1 }

其他配置

  1. 延迟备份节点
    如果有人不小心删除了某个表,或者线上服务出现问题,造成了成吨的垃圾数据,为了防止这个问题,可以使用slaveDelay设置一个延迟的备份节点。延迟备份节点的数据会比主节点延迟指定的时间(单位:秒)。
    slaveDelay要求成员的优先级是0,。如果你的应用会将读请求路由到备份节点,那么应该将延迟备份节点隐藏起来,避免发生脏读。
  2. 创建索引。
    有些时候,备份节点只适用于备份数据,这个时候,备份节点就不需要与主节点拥有相同的索引,甚至不需要任何索引。这个时候可以在成员配置时指定”buildIndexes” : false,来阻止备份节点创建索引。
    这是一个永久的参数,指定了该参数的成员将永远无法恢复为可以创建索引的状态,同时,这个参数也要求成员的优先级为0。

 

赞(1) 投币

评论 抢沙发

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

码字不容易,路过请投币

支付宝扫一扫

微信扫一扫