关于MongoDb Replica Set的故障转移集群——实战篇

时间:2024-10-05 21:37:08

如果你还不了解Replica Set的相关理论,请猛戳传送门阅读笔者的上一篇博文。

因为Replica Set已经属于MongoDb的进阶应用,下文中关于MongoDb的基础知识笔者就不再赘述了,请参考MongoDb Manual

下面分各种场景讲述如何创建一个Replica Set。

Standalone到Replica Set

这是相对简单的一种情况。如果你刚刚在生产环境应用MongoDb,很有可能适用于这种场景。

一*立的MongoDb实例变为Replica Set的首位成员很容易,需要两个步骤:

1. 运行参数中加入--replicaSet <set name>。如:

mongod --dbpath /var/lib/mongo/ --replicaSet rs0 --fork

如果开启了认证模式则需要额外的一步:为实例配置一个Key,作为今后不同实例间认证的根据。

Key的原理很简单,只要不同实例拥有相同的Key,则认证成功。没有公私钥交换等等麻烦的过程。

生成Key也很简单,任何一个文件存了字符串都可以成为Key。只要保证不同实例使用相同的Key文件即可。

添加Key请使用参数 --keyFile=<file path>。生成Key请参考官方文档

2. 进入MongoDb命令行进行初始化,大致情况如下:

$ mongo localhost:
MongoDB shell version: 2.4.
connecting to: localhost:/test
> rs.initiate()
{
"info2" : "no configuration explicitly specified -- making one",
"me" : "YX-ARCH:27011",
"info" : "Config now saved locally. Should come online in about a minute.",
"ok" :
}

稍等片刻,集群初始化完成,回车后提示符从">"变为

rs0:PRIMARY> 

表示该实例已经成为一个Primary结点。此时集群中只有一个结点,不存在投票问题,所以无论怎么折腾这个结点都会保持Primary状态。但到下一步就要小心了。

rs0:PRIMARY> rs.add("YX-ARCH:27012")
{ "ok" : }

此时第二个结点YX-ARCH:27012已加入集群,可以使用以下命令查看集群状态:

rs0:PRIMARY> rs.status()
{
"set" : "rs0",
"date" : ISODate("2014-01-24T07:21:01Z"),
"myState" : ,
"members" : [
{
"_id" : ,
"name" : "YX-ARCH:27011",
"health" : ,
"state" : ,
"stateStr" : "PRIMARY",
"uptime" : ,
"optime" : Timestamp(, ),
"optimeDate" : ISODate("2014-01-24T07:20:12Z"),
"self" : true
},
{
"_id" : ,
"name" : "YX-ARCH:27012",
"health" : ,
"state" : ,
"stateStr" : "SECONDARY",
"uptime" : ,
"optime" : Timestamp(, ),
"optimeDate" : ISODate("2014-01-24T07:20:12Z"),
"lastHeartbeat" : ISODate("2014-01-24T07:21:00Z"),
"lastHeartbeatRecv" : ISODate("2014-01-24T07:21:00Z"),
"pingMs" : ,
"syncingTo" : "YX-ARCH:27011"
}
],
"ok" :
}

可见27011此时是Primary身份,而27012则是Secondary身份。如果想改变结点的角色,则需要修改集群的配置。首先将集群配置调出:

rs0:PRIMARY> conf = rs.conf()
{
"_id" : "rs0",
"version" : ,
"members" : [
{
"_id" : ,
"host" : "YX-ARCH:27011"
},
{
"_id" : ,
"host" : "YX-ARCH:27012"
}
]
}

上篇讲过每个集群结点都有一个priority属性,默认为1。要改变一个实例的角色,我们只需要给新的实例设置一个高于当前Primary的priority就可以实现了。

注意因为只有Primary结点是可写的,所以重新配置群集的操作只能在Primary结点上进行。

rs0:PRIMARY> conf.members[].priority = 

rs0:PRIMARY> rs.reconfig(conf)
Fri Jan ::36.069 DBClientCursor::init call() failed
Fri Jan ::36.070 trying reconnect to localhost:
Fri Jan ::36.070 reconnect localhost: ok
reconnected to server after rs command (which is normal) rs0:SECONDARY>

可见短暂的罢工后命令行自动重新连接到当前实例,但从提示符可以看出,当前实例已经变为Secondary角色。我们来随便逛逛Secondary里面的内容(Test是笔者自己建立的库)

rs0:SECONDARY> show dbs
Test .203125GB
local .0771484375GB
rs0:SECONDARY> use Test
switched to db Test
rs0:SECONDARY> show collections
Fri Jan ::32.697 error: { "$err" : "not master and slaveOk=false", "code" : } at src/mongo/shell/query.js:

如果用过Master Slave集群的读者应该发现了,Slave结点是可读的,而Secondary结点默认却不可读。要解决这个问题,只需要简单的一步:

rs0:SECONDARY> rs.slaveOk()
rs0:SECONDARY> show collections
system.indexes
test

至此一个拥有2个实例的MongoDb Replica Set就建立完成了。那么我们来试试传说中的故障恢复特性。

假设Primary结点因故停止工作了(我们手动干掉它)

$ ps awx | grep mongo
? Sl : mongod --config mongodb1.conf
? Sl : mongod --config mongodb2.conf <--Primary在这
pts/ Sl+ : mongo localhost:
pts/ S+ : grep --color=auto mongo
$ kill

在美好的幻想中,Secondary应该马上即位成为新的Primary吧?

$ mongo localhost:
MongoDB shell version: 2.4.
connecting to: localhost:/test
rs0:SECONDARY>

神马?还是Secondary?说好的故障恢复呢?

或许是姿势不对?那我们重新启动两个实例,这回先当掉Secondary试试

$ ps awx | grep mongo
? Sl : mongod --config mongodb1.conf <-- Secondary在这
pts/ Sl+ : mongo localhost:
? Sl : mongod --config mongodb2.conf
pts/ S+ : grep --color=auto mongo
$ kill 12546
$ mongo localhost:27012
MongoDB shell version: 2.4.9
connecting to: YX-ARCH:27012/test
rs0:SECONDARY>

神马?Primary降级为Secondary了?如果没看上篇的读者心里应该暗暗骂娘了吧?”禽兽,这算什么错误恢复集群?明明就是两个都死了!“

那么我们再把死掉那个Secondary复活试试?你会发现Primary又回来了。上篇说过,提升到Primary是一个很严格的过程,一旦出现2个Primary的情况,集群就废了。所以宁可错杀1000也不可放过一个。

所以实际发生的事情是这样的:当集群中仅有的2个实例挂掉一个,剩下的一个并不能判断自己的存活状况,因为也可能是自己跟网络断开了连接造成的。另外一个实例说不定还活得好好的呢。因此没有办法,暂时让自己成为Secondary活下去吧。

一旦另外一个实例恢复生存,两个实例就可以互相证明对方存活,因此还是priority较高的那个继续扮演Primary。

为了避免这种悲剧的发生,Arbiter的必要性就体现出来了。

自然你可以在群集中再加一个Secondary,让三个实例互相证明存活性,这样不仅刚才的问题可以解决 ,自动切换也是可以达成的。但是要成为Secondary的服务器可是对性能有一定要求的,现实情况下这样做可能性价比并不高。那么还是添加一个Arbiter吧,它的存在只是为了投票选举,不占用额外资源,放在资源有限的虚拟机中就足够了。如果有多个Arbiter为不同的集群服务,也可以放进同一台虚拟机中以节省资源。

rs0:PRIMARY> rs.addArb("YX-ARCH:27013")
{ "ok" : }

顺带一提,刚才我们的操作中一直使用的是我的机器名"YX-ARCH",而不是localhost。这里localhost是被禁止使用的,原因是这样:

如果在命令行查看集群信息

rs0:PRIMARY> rs.status()
{
"set" : "rs0",
"date" : ISODate("2014-01-24T08:08:32Z"),
"myState" : ,
"members" : [
{
"_id" : ,
"name" : "YX-ARCH:27011",
"health" : ,
"state" : ,
"stateStr" : "SECONDARY",
"uptime" : ,
"optime" : Timestamp(, ),
"optimeDate" : ISODate("2014-01-24T08:07:22Z"),
"lastHeartbeat" : ISODate("2014-01-24T08:08:30Z"),
"lastHeartbeatRecv" : ISODate("2014-01-24T08:08:32Z"),
"pingMs" : ,
"syncingTo" : "YX-ARCH:27012"
},
{
"_id" : ,
"name" : "YX-ARCH:27012",
"health" : ,
"state" : ,
"stateStr" : "PRIMARY",
"uptime" : ,
"optime" : Timestamp(, ),
"optimeDate" : ISODate("2014-01-24T08:07:22Z"),
"self" : true
},
{
"_id" : ,
"name" : "YX-ARCH:27013",
"health" : ,
"state" : ,
"stateStr" : "ARBITER",
"uptime" : ,
"lastHeartbeat" : ISODate("2014-01-24T08:08:31Z"),
"lastHeartbeatRecv" : ISODate("2014-01-24T08:08:32Z"),
"pingMs" :
}
],
"ok" :
}

我们可以看到集群中有三个实例,分别是

YX-ARCH:
YX-ARCH:
YX-ARCH:

当使用某种语言,比如C#来连接MongoDb的时候,哪些服务器可用的信息并不是从连接字符串中来的,而是Driver会得到一份类似以上的信息来判断哪些服务器可用。可见,如果添加到集群中的地址是localhost:27011,那么Driver也会认为localhost:27011是一台可用的实例。但多数情况下应用和数据库都是分开的,这会造成应用徒劳地从自己机器上的27011端口去找MongoDb服务,显然是不可能成功的。关于获取可用实例的问题,笔者之前写过一篇日志,请戳传送门

以上已经建立了一个完整的集群,望大家用得开心。但作为摸爬滚打了多年的IT人,我是万万不敢在没有考虑后路的情况下就往生产环境上使用一个自己不知根知底的系统的。进兵前先考虑退路是常识,所以还是要考虑一下万一的情况下,如何从Replica Set退回Standalone的场景。

Replica Set 到 Standalone

考虑上述3个实例的Replica Set,它虽然可以在任何一个实例当掉的情况下保持正常工作,那如果2个实例同时当掉呢?什么,不可能有这么衰?前人的经验告诉我们:在IT的世界里,只要有可能发生的事情就一定会发生。

所以当灾难来临的时候如果你两手空空,怎么能不被打个鼻青脸肿?那么,操家伙开干。

无论Primary还是Secondary,只要去掉--replicaSet即可马上变回Standalone的状态。如果要彻底回到Standalone状态,还应该删除数据库目录下的local.*文件(注意删除操作必须在MongoDb停止服务的情况下进行)。

如果不删这些文件,MongoDb也会正常工作,但TTL Collection会工作不正常。此外笔者还没发现有什么问题。

现实是,在多数情况下我们是不会希望从Replica Set变回Standalone的。如果你真的这么做了,那大概只有一个原因:踩了上文中的某个雷,导致集群变成完全只读的了。你期望暂时变回Standalone状态,以便不影响生产环境的日常操作,等其他的实例恢复之后再加上--replicaSet参数变回Replica Set状态。很不幸,如果你是这个目的,那么你马上会踩第二个雷。

我们知道Replica Set的原理是传送oplog并重做,而oplog只有在master/slave或replica set模式下才会生成。所以当你回到standalone模式下时所做的任何事情都是没有oplog的,这意味着当你再次回到Replica Set模式中时会丢失部分数据。

更气人的是:MongoDb不会阻止你做这样的事情,它会让你成功回到Replica Set状态。无论是Primary还是Secondary都可以。你甚至可以在Secondary变成Standalone期间修改它的内容后再让它回到ReplicaSet,这样它的内容就跟其他实例不一致!

知道这个坑之后来看看简单粗暴的解决办法:

1. 停止MongoDb实例

$ sudo systemctl stop mongodb

2. 进入数据库文件夹删除local.*

$ cd /var/lib/mongodb/
$ rm local.* -rf

3. 把--replicaSet参数重新添加回配置文件

4. 重启启动MongoDb

sudo systemctl start mongodb

此时MongoDb变回普通的非Replica Set实例

5. 按上文的方法重新初始化Replica Set并建立集群。

虽然复杂了点,但这也是没有办法的事情,千万不可走捷径。

Master/Slave到Replica Set

Master/Slave到Replica Set其实和Standalone到Replica Set并无两样,所有步骤完全相同。要注意的只有一点,一旦Master变成Primary,Slave将不再从它同步数据。也就是说从执行

> rs.initiate()

的一刻开始,Slave就成为了当时数据库的快照,不再更新。所以在生产环境中应用这个操作时,要注意数据不同步可能带来的危害,必要时必须停机维护。遇到这类问题时:

方案一:可以把原来使用Slave的应用切换为使用Master,优点是可以全程不间断服务。缺点是要求你的Master性能足够好。

方案二:如果想使用方案一但Master的性能又不够好,可以使用一台足够强大的新机器先做为Slave,然后所有系统停机维护,这时把Slave变为Master,所有服务使用新的Master继续工作。之后再从这台Master开始向Replica Set的转换工作。优点是停机时间可以比较短。缺点嘛……服务中断了……另外所有系统指向新的Master时视系统规模可能修改比较多,容易有疏漏。

方案三:完全停机进行master/slave到replica set的转换。所有Secondary完成复制后再开始重新服务。这当然是最轻松的方案了,自然停机时间也是最长的。

后记

MongoDb作为新兴的非关系型数据库近年来发展迅速。新特性层出不穷。想了解它的方方面面,最简单的方法是读它的User manual。虽然是件费力的事情,但也是没有办法的事情,就像上面提到的一样,想走捷径的结果往往是掉进坑里。

希望笔者的血泪史为小伙伴们提供前车之鉴,本文如有不正确之处欢迎指正。