你的位置:首页 > 数据库

[数据库]搭建高可用的mongodb集群(副本集)


搭建高可用的mongodb集群(副本集)

                                    转自

  1. 副本集


 

在上一篇文章《搭建高可用MongoDB集群(一)——配置MongoDB》 提到了几个问题还没有解决。

  • 主节点挂了能否自动切换连接?目前需要手工切换。
  • 主节点的读写压力过大如何解决?
  • 从节点每个上面的数据都是对数据库全量拷贝,从节点压力会不会过大?
  • 数据压力大到机器支撑不了的时候能否做到自动扩展?

这篇文章看完这些问题就可以搞定了。NoSQL的产生就是为了解决大数据量、高扩展性、高性能、灵活数据模型、高可用性。但是光通过主从模式的架构远远达不到上面几点,由此MongoDB设计了副本集和分片的功能。这篇文章主要介绍副本集

mongoDB官方已经不建议使用主从模式了,替代方案是采用副本集的模式,点击查看 ,如图:

  1. 定义

那什么是副本集呢?打魔兽世界总说打副本,其实这两个概念差不多一个意思。游戏里的副本是指玩家集中在高峰时间去一个场景打怪,会出现玩家暴多怪物少的情况,游戏开发商为了保证玩家的体验度,就为每一批玩家单独开放一个同样的空间同样的数量的怪物,这一个复制的场景就是一个副本,不管有多少个玩家各自在各自的副本里玩不会互相影响。 mongoDB的副本也是这个,主从模式其实就是一个单副本的应用,没有很好的扩展性和容错性。而副本集具有多个副本保证了容错性,就算一个副本挂掉了还有很多副本存在,并且解决了上面第一个问题"主节点挂掉了,整个集群内会自动切换"。难怪mongoDB官方推荐使用这种模式。我们来看看mongoDB副本集的架构图:

由图可以看到客户端连接到整个副本集,不关心具体哪一台机器是否挂掉。主服务器负责整个副本集的读写,副本集定期同步数据备份,一但主节点挂掉,副本节点就会选举一个新的主服务器,这一切对于应用服务器不需要关心。我们看一下主服务器挂掉后的架构:

副本集中的副本节点在主节点挂掉后通过心跳机制检测到后,就会在集群内发起主节点的选举机制,自动选举一位新的主服务器。看起来很牛X的样子,我们赶紧操作部署一下!官方推荐的副本集机器数量为至少3个,那我们也按照这个数量配置测试。

  1. 配置测试

1、准备两台机器192.168.1.136、192.168.1.137、192.168.1.138。 192.168.1.136 当作副本集主节点,192.168.1.137、192.168.1.138作为副本集副本节点

2、分别在每台机器上建立mongodb副本集测试文件夹

 

#存放整个mongodb文件

mkdir -p /data/mongodbtest/replset

#存放mongodb数据文件

mkdir -p /data/mongodbtest/replset/data

#进入mongodb文件夹

cd /data/mongodbtest

3

、下载mongodb的安装程序包
 

wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-2.4.8.tgz

注意linux生产环境不能安装32位的mongodb,因为32位受限于操作系统最大2G的文件限制。

 

#解压下载的压缩包

tar xvzf mongodb-linux-x86_64-2.4.8.tgz

4

、分别在每台机器上启动mongodb
 

/data/mongodbtest/mongodb-linux-x86_64-2.4.8/bin/mongod --dbpath /data/mongodbtest/replset/data --replSet repset

可以看到控制台上显示副本集还没有配置初始化信息。

 

Sun Dec 29 20:12:02.953 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)

Sun Dec 29 20:12:02.953 [rsStart] replSet info you may need to run replSetInitiate -- rs.initiate() in the shell -- if that is not already done

5

、初始化副本集

在三台机器上任意一台机器登陆mongodb

 

/data/mongodbtest/mongodb-linux-x86_64-2.4.8/bin/mongo

#使用admin数据库

use admin

#

定义副本集配置变量,这里的 _id:"repset" 和上面命令参数" –replSet repset" 要保持一样。
 

config = { _id:"repset", members:[

... {_id:0,host:"192.168.1.136:27017"},

... {_id:1,host:"192.168.1.137:27017"},

... {_id:2,host:"192.168.1.138:27017"}]

... }

#

输出
 

{

"_id" : "repset",

"members" : [

{

"_id" : 0,

"host" : "192.168.1.136:27017"

},

{

"_id" : 1,

"host" : "192.168.1.137:27017"

},

{

"_id" : 2,

"host" : "192.168.1.138:27017"

}

]

}

 

#初始化副本集配置

rs.initiate(config);


#

输出成功
 

{

"info" : "Config now saved locally. Should come online in about a minute.",

"ok" : 1

}

#

查看日志,副本集启动成功后,138为主节点PRIMARY,136、137为副本节点

 

SECONDARY

 

Sun Dec 29 20:26:13.842 [conn3] replSet replSetInitiate admin command received from client

Sun Dec 29 20:26:13.842 [conn3] replSet replSetInitiate config object parses ok, 3 members specified

Sun Dec 29 20:26:13.847 [conn3] replSet replSetInitiate all members seem up

Sun Dec 29 20:26:13.848 [conn3] ******

Sun Dec 29 20:26:13.848 [conn3] creating replication oplog of size: 990MB...

Sun Dec 29 20:26:13.849 [FileAllocator] allocating new datafile /data/mongodbtest/replset/data/local.1, filling with zeroes...

Sun Dec 29 20:26:13.862 [FileAllocator] done allocating datafile /data/mongodbtest/replset/data/local.1, size: 1024MB, took 0.012 secs

Sun Dec 29 20:26:13.863 [conn3] ******

Sun Dec 29 20:26:13.863 [conn3] replSet info saving a newer config version to local.system.replset

Sun Dec 29 20:26:13.864 [conn3] replSet saveConfigLocally done

Sun Dec 29 20:26:13.864 [conn3] replSet replSetInitiate config now saved locally. Should come online in about a minute.

Sun Dec 29 20:26:23.047 [rsStart] replSet I am 192.168.1.138:27017

Sun Dec 29 20:26:23.048 [rsStart] replSet STARTUP2

Sun Dec 29 20:26:23.049 [rsHealthPoll] replSet member 192.168.1.137:27017 is up

Sun Dec 29 20:26:23.049 [rsHealthPoll] replSet member 192.168.1.136:27017 is up

Sun Dec 29 20:26:24.051 [rsSync] replSet SECONDARY

Sun Dec 29 20:26:25.053 [rsHealthPoll] replset info 192.168.1.136:27017 thinks that we are down

Sun Dec 29 20:26:25.053 [rsHealthPoll] replSet member 192.168.1.136:27017 is now in state STARTUP2

Sun Dec 29 20:26:25.056 [rsMgr] not electing self, 192.168.1.136:27017 would veto with 'I don't think 192.168.1.138:27017 is electable'

Sun Dec 29 20:26:31.059 [rsHealthPoll] replset info 192.168.1.137:27017 thinks that we are down

Sun Dec 29 20:26:31.059 [rsHealthPoll] replSet member 192.168.1.137:27017 is now in state STARTUP2

Sun Dec 29 20:26:31.062 [rsMgr] not electing self, 192.168.1.137:27017 would veto with 'I don't think 192.168.1.138:27017 is electable'

Sun Dec 29 20:26:37.074 [rsMgr] replSet info electSelf 2

Sun Dec 29 20:26:38.062 [rsMgr] replSet PRIMARY

Sun Dec 29 20:26:39.071 [rsHealthPoll] replSet member 192.168.1.137:27017 is now in state RECOVERING

Sun Dec 29 20:26:39.075 [rsHealthPoll] replSet member 192.168.1.136:27017 is now in state RECOVERING

Sun Dec 29 20:26:42.201 [slaveTracking] build index local.slaves { _id: 1 }

Sun Dec 29 20:26:42.207 [slaveTracking] build index done. scanned 0 total records. 0.005 secs

Sun Dec 29 20:26:43.079 [rsHealthPoll] replSet member 192.168.1.136:27017 is now in state SECONDARY

Sun Dec 29 20:26:49.080 [rsHealthPoll] replSet member 192.168.1.137:27017 is now in state SECONDARY

 

#查看集群节点的状态

rs.status();

#

输出

 

{

"set" : "repset",

"date" : ISODate("2013-12-29T12:54:25Z"),

"myState" : 1,

"members" : [

{

"_id" : 0,

"name" : "192.168.1.136:27017",

"health" : 1,

"state" : 2,

"stateStr" : "SECONDARY",

"uptime" : 1682,

"optime" : Timestamp(1388319973, 1),

"optimeDate" : ISODate("2013-12-29T12:26:13Z"),

"lastHeartbeat" : ISODate("2013-12-29T12:54:25Z"),

"lastHeartbeatRecv" : ISODate("2013-12-29T12:54:24Z"),

"pingMs" : 1,

"syncingTo" : "192.168.1.138:27017"

},

{

"_id" : 1,

"name" : "192.168.1.137:27017",

"health" : 1,

"state" : 2,

"stateStr" : "SECONDARY",

"uptime" : 1682,

"optime" : Timestamp(1388319973, 1),

"optimeDate" : ISODate("2013-12-29T12:26:13Z"),

"lastHeartbeat" : ISODate("2013-12-29T12:54:25Z"),

"lastHeartbeatRecv" : ISODate("2013-12-29T12:54:24Z"),

"pingMs" : 1,

"syncingTo" : "192.168.1.138:27017"

},

{

"_id" : 2,

"name" : "192.168.1.138:27017",

"health" : 1,

"state" : 1,

"stateStr" : "PRIMARY",

"uptime" : 2543,

"optime" : Timestamp(1388319973, 1),

"optimeDate" : ISODate("2013-12-29T12:26:13Z"),

"self" : true

}

],

"ok" : 1

}

整个副本集已经搭建成功了。

 

6、测试副本集数据复制功能

 

#在主节点192.168.1.138 上连接到终端:

mongo 127.0.0.1

#建立test 数据库。

use test;

往testdb表插入数据。

> db.testdb.insert({"test1":"testval1"})

#在副本节点 192.168.1.136、192.168.1.137 上连接到mongodb查看数据是否复制过来。

/data/mongodbtest/mongodb-linux-x86_64-2.4.8/bin/mongo 192.168.1.136:27017

#使用test 数据库。

repset:SECONDARY> use test;

repset:SECONDARY> show tables;

#

输出
 

Sun Dec 29 21:50:48.590 error: { "$err" : "not master and slaveOk=false", "code" : 13435 } at src/mongo/shell/query.js:128

 

#mongodb默认是从主节点读写数据的,副本节点上不允许读,需要设置副本节点可以读。

repset:SECONDARY> db.getMongo().setSlaveOk();

#可以看到数据已经复制到了副本集。

repset:SECONDARY> db.testdb.find();

 

#输出

{ "_id" : ObjectId("52c028460c7505626a93944f"), "test1" : "testval1" }

7

、测试副本集故障转移功能

先停掉主节点mongodb 138,查看136、137的日志可以看到经过一系列的投票选择操作,137 当选主节点,136从137同步数据过来。

 

Sun Dec 29 22:03:05.351 [rsBackgroundSync] replSet sync source problem: 10278 dbclient error communicating with server: 192.168.1.138:27017

Sun Dec 29 22:03:05.354 [rsBackgroundSync] replSet syncing to: 192.168.1.138:27017

Sun Dec 29 22:03:05.356 [rsBackgroundSync] repl: couldn't connect to server 192.168.1.138:27017

Sun Dec 29 22:03:05.356 [rsBackgroundSync] replSet not trying to sync from 192.168.1.138:27017, it is vetoed for 10 more seconds

Sun Dec 29 22:03:05.499 [rsHealthPoll] DBClientCursor::init call() failed

Sun Dec 29 22:03:05.499 [rsHealthPoll] replset info 192.168.1.138:27017 heartbeat failed, retrying

Sun Dec 29 22:03:05.501 [rsHealthPoll] replSet info 192.168.1.138:27017 is down (or slow to respond):

Sun Dec 29 22:03:05.501 [rsHealthPoll] replSet member 192.168.1.138:27017 is now in state DOWN

Sun Dec 29 22:03:05.511 [rsMgr] not electing self, 192.168.1.137:27017 would veto with '192.168.1.136:27017 is trying to elect itself but 192.168.1.138:27017 is already primary and more up-to-date'

Sun Dec 29 22:03:07.330 [conn393] replSet info voting yea for 192.168.1.137:27017 (1)

Sun Dec 29 22:03:07.503 [rsHealthPoll] replset info 192.168.1.138:27017 heartbeat failed, retrying

Sun Dec 29 22:03:08.462 [rsHealthPoll] replSet member 192.168.1.137:27017 is now in state PRIMARY

Sun Dec 29 22:03:09.359 [rsBackgroundSync] replSet syncing to: 192.168.1.137:27017

Sun Dec 29 22:03:09.507 [rsHealthPoll] replset info 192.168.1.138:27017 heartbeat failed, retrying

查看整个集群的状态,可以看到138为状态不可达。

 

/data/mongodbtest/mongodb-linux-x86_64-2.4.8/bin/mongo 192.168.1.136:27017

repset:SECONDARY> rs.status();

#

输出
 

{

"set" : "repset",

"date" : ISODate("2013-12-29T14:28:35Z"),

"myState" : 2,

"syncingTo" : "192.168.1.137:27017",

"members" : [

{

"_id" : 0,

"name" : "192.168.1.136:27017",

"health" : 1,

"state" : 2,

"stateStr" : "SECONDARY",

"uptime" : 9072,

"optime" : Timestamp(1388324934, 1),

"optimeDate" : ISODate("2013-12-29T13:48:54Z"),

"self" : true

},

{

"_id" : 1,

"name" : "192.168.1.137:27017",

"health" : 1,

"state" : 1,

"stateStr" : "PRIMARY",

"uptime" : 7329,

"optime" : Timestamp(1388324934, 1),

"optimeDate" : ISODate("2013-12-29T13:48:54Z"),

"lastHeartbeat" : ISODate("2013-12-29T14:28:34Z"),

"lastHeartbeatRecv" : ISODate("2013-12-29T14:28:34Z"),

"pingMs" : 1,

"syncingTo" : "192.168.1.138:27017"

},

{

"_id" : 2,

"name" : "192.168.1.138:27017",

"health" : 0,

"state" : 8,

"stateStr" : "(not reachable/healthy)",

"uptime" : 0,

"optime" : Timestamp(1388324934, 1),

"optimeDate" : ISODate("2013-12-29T13:48:54Z"),

"lastHeartbeat" : ISODate("2013-12-29T14:28:35Z"),

"lastHeartbeatRecv" : ISODate("2013-12-29T14:28:23Z"),

"pingMs" : 0,

"syncingTo" : "192.168.1.137:27017"

}

],

"ok" : 1

}

再启动原来的主节点 138,发现138 变为 SECONDARY,还是137 为主节点 PRIMARY。

 

Sun Dec 29 22:21:06.619 [rsStart] replSet I am 192.168.1.138:27017

Sun Dec 29 22:21:06.619 [rsStart] replSet STARTUP2

Sun Dec 29 22:21:06.627 [rsHealthPoll] replset info 192.168.1.136:27017 thinks that we are down

Sun Dec 29 22:21:06.627 [rsHealthPoll] replSet member 192.168.1.136:27017 is up

Sun Dec 29 22:21:06.627 [rsHealthPoll] replSet member 192.168.1.136:27017 is now in state SECONDARY

Sun Dec 29 22:21:07.628 [rsSync] replSet SECONDARY

Sun Dec 29 22:21:08.623 [rsHealthPoll] replSet member 192.168.1.137:27017 is up

Sun Dec 29 22:21:08.624 [rsHealthPoll] replSet member 192.168.1.137:27017 is now in state PRIMARY

8

、java程序连接副本集测试。三个节点有一个节点挂掉也不会影响应用程序客户端对整个副本集的读写!
 

public class TestMongoDBReplSet {

public static void main(String[] args) {

try {

List<ServerAddress> addresses = new ArrayList<ServerAddress>();

ServerAddress address1 = new ServerAddress("192.168.1.136" , 27017);

ServerAddress address2 = new ServerAddress("192.168.1.137" , 27017);

ServerAddress address3 = new ServerAddress("192.168.1.138" , 27017);

addresses.add(address1);

addresses.add(address2);

addresses.add(address3);

MongoClient client = new MongoClient(addresses);

DB db = client.getDB( "test");

DBCollection coll = db.getCollection( "testdb");

// 插入

BasicDBObject object = new BasicDBObject();

object.append( "test2", "testval2" );

coll.insert(object);

DBCursor dbCursor = coll.find();

while (dbCursor.hasNext()) {

DBObject dbObject = dbCursor.next();

System. out.println(dbObject.toString());

}

} catch (Exception e) {

e.printStackTrace();

}

}

}

目前看起来支持完美的故障转移了,这个架构是不是比较完美了?其实还有很多地方可以优化,比如开头的第二个问题:主节点的读写压力过大如何解决?常见的解决方案是读写分离,mongodb副本集的读写分离如何做呢?

看图说话:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

常规写操作来说并没有读操作多,所以一台主节点负责写,两台副本节点负责读。

1、设置读写分离需要先在副本节点SECONDARY 设置 setSlaveOk。
2、在程序中设置副本节点负责读操作,如下代码:

 

public class TestMongoDBReplSetReadSplit {

public static void main(String[] args) {

try {

List<ServerAddress> addresses = new ArrayList<ServerAddress>();

ServerAddress address1 = new ServerAddress("192.168.1.136" , 27017);

ServerAddress address2 = new ServerAddress("192.168.1.137" , 27017);

ServerAddress address3 = new ServerAddress("192.168.1.138" , 27017);

addresses.add(address1);

addresses.add(address2);

addresses.add(address3);

MongoClient client = new MongoClient(addresses);

DB db = client.getDB( "test" );

DBCollection coll = db.getCollection( "testdb" );

BasicDBObject object = new BasicDBObject();

object.append( "test2" , "testval2" );

//读操作从副本节点读取

ReadPreference preference = ReadPreference. secondary();

DBObject dbObject = coll.findOne(object, null , preference);

System. out .println(dbObject);

} catch (Exception e) {

e.printStackTrace();

}

}

}

读参数除了secondary一共还有五个参数:primary、primaryPreferred、secondary、secondaryPreferred、nearest。

primary:默认参数,只从主节点上进行读取操作;
primaryPreferred:大部分从主节点上读取数据,只有主节点不可用时从secondary节点读取数据。
secondary:只从secondary节点上进行读取操作,存在的问题是secondary节点的数据会比primary节点数据"旧"。
secondaryPreferred:优先从secondary节点进行读取操作,secondary节点不可用时从主节点读取数据;
nearest:不管是主节点、secondary节点,从网络延迟最低的节点上读取数据。

好,读写分离做好我们可以数据分流,减轻压力解决了"主节点的读写压力过大如何解决?"这个问题。不过当我们的副本节点增多时,主节点的复制压力会加大有什么办法解决吗?mongodb早就有了相应的解决方案。

看图:

其中的仲裁节点不存储数据,只是负责故障转移的群体投票,这样就少了数据复制的压力。是不是想得很周到啊,一看mongodb的开发兄弟熟知大数据架构体系,其实不只是主节点、副本节点、仲裁节点,还有Secondary-Only、Hidden、Delayed、Non-Voting。

Secondary-Only:不能成为primary节点,只能作为secondary副本节点,防止一些性能不高的节点成为主节点。
Hidden:这类节点是不能够被客户端制定IP引用,也不能被设置为主节点,但是可以投票,一般用于备份数据。
Delayed可以指定一个时间延迟从primary节点同步数据。主要用于备份数据,如果实时同步,误删除数据马上同步到从节点,恢复又恢复不了。
Non-Voting没有选举权的secondary节点,纯粹的备份数据节点。

到此整个mongodb副本集搞定了两个问题:

  • 主节点挂了能否自动切换连接?目前需要手工切换。

  • 主节点的读写压力过大如何解决?

还有这两个问题后续解决:

  • 从节点每个上面的数据都是对数据库全量拷贝,从节点压力会不会过大?

  • 数据压力大到机器支撑不了的时候能否做到自动扩展?

做了副本集发现又一些问题:

  • 副本集故障转移,主节点是如何选举的?能否手动干涉下架某一台主节点。

  • 官方说副本集数量最好是奇数,为什么?

  • mongodb副本集是如何同步的?如果同步不及时会出现什么情况?会不会出现不一致性?

  • mongodb的故障转移会不会无故自动发生?什么条件会触发?频繁触发可能会带来系统负载加重
  1. 副本集内部机制

在上一篇文章《搭建高可用mongodb集群(二)—— 副本集》 介绍了副本集的配置,这篇文章深入研究一下副本集的内部机制。还是带着副本集的问题来看吧!

  • 副本集故障转移,主节点是如何选举的?能否手动干涉下架某一台主节点。
  • 官方说副本集数量最好是奇数,为什么?
  • mongodb副本集是如何同步的?如果同步不及时会出现什么情况?会不会出现不一致性?
  • mongodb的故障转移会不会无故自动发生?什么条件会触发?频繁触发可能会带来系统负载加重?
  1. 选举算法

    Bully算法 mongodb副本集故障转移功能得益于它的选举机制。选举机制采用了Bully算法,可以很方便从分布式节点中选出主节点。一个分布式集群架构中一般都有一个所谓的主节点,可以有很多用途,比如缓存机器节点元数据,作为集群的访问入口等等。主节点有就有吧,我们干嘛要什么Bully算法?要明白这个我们先看看这两种架构:

    1. 指定主节点的架构,这种架构一般都会申明一个节点为主节点,其他节点都是从节点,如我们常用的mysql就是这样。但是这样架构我们在第一节说了整个集群如果主节点挂掉了就得手工操作,上架一个新的主节点或者从从节点恢复数据,不太灵活。

    2. 不指定主节点,集群中的任意节点都可以成为主节点。mongodb也就是采用这种架构,一但主节点挂了其他从节点自动接替变成主节点。如下图:

    好了,问题就在这个地方,既然所有节点都是一样,一但主节点挂了,怎么选择出来下一个节点是谁来做为主节点呢?这就是Bully算法解决的问题。

    那什么是Bully算法,Bully算法是一种协调者(主节点)竞选算法,主要思想是集群的每个成员都可以声明它是主节点并通知其他节点。别的节点可以选择接受这个声称或是拒绝并进入主节点竞争。被其他所有节点接受的节点才能成为主节点。节点按照一些属性来判断谁应该胜出。这个属性可以是一个静态ID,也可以是更新的度量像最近一次事务ID(最新的节点会胜出)。详情请参考NoSQL数据库分布式算法的协调者竞选还有维基百科的解释 。

    选举 那mongodb是怎进行选举的呢?官方这么描述:

    We use a consensus protocol to pick a primary. Exact details will be spared here but that basic process is:

    1. get maxLocalOpOrdinal from each server.
    2. if a majority of servers are not up (from this server's POV), remain in Secondary mode and stop.
    3. if the last op time seems very old, stop and await human intervention.
    4. else, using a consensus protocol, pick the server with the highest maxLocalOpOrdinal as the Primary.

    大致翻译过来为使用一致协议选择主节点。基本步骤为:

    1. 得到每个服务器节点的最后操作时间戳。每个mongodb都有oplog机制会记录本机的操作,方便和主服务器进行对比数据是否同步还可以用于错误恢复。
    2. 如果集群中大部分服务器down机了,保留活着的节点都为 secondary状态并停止,不选举了。
    3. 如果集群中选举出来的主节点或者所有从节点最后一次同步时间看起来很旧了,停止选举等待人来操作。
    4. 如果上面都没有问题就选择最后操作时间戳最新(保证数据是最新的)的服务器节点作为主节点。

    这里提到了一个一致协议(其实就是bully算法),这个和数据库的一致性协议还是有些区别,一致协议主要强调的是通过一些机制保证大家达成共识;而一致性协议强调的是操作的顺序一致性,比如同时读写一个数据会不会出现脏数据。一致协议在分布式里有一个经典的算法叫"Paxos算法",后续再介绍。

    上面有个问题,就是所有从节点的最后操作时间都是一样怎么办?就是谁先成为主节点的时间最快就选谁。

    选举触发条件

    选举不是什么时刻都会被触发的,有以下情况可以触发。

    1. 初始化一个副本集时。
    2. 副本集和主节点断开连接,可能是网络问题。
    3. 主节点挂掉。

    选举还有个前提条件,参与选举的节点数量必须大于副本集总节点数量的一半,如果已经小于一半了所有节点保持只读状态。
    日志将会出现:

    can't see a majority of the set, relinquishing primary

    主节点挂掉能否人为干预?答案是肯定的。

    1. 可以通过replSetStepDown命令下架主节点。这个命令可以登录主节点使用

      db.adminCommand({replSetStepDown : 1})

      如果杀不掉可以使用强制开关

      db.adminCommand({replSetStepDown : 1, force : true})

      或者使用 rs.stepDown(120)也可以达到同样的效果,中间的数字指不能在停止服务这段时间成为主节点,单位为秒。

    2. 设置一个从节点有比主节点有更高的优先级。
      先查看当前集群中优先级,通过rs.conf()命令,默认优先级为1是不显示的,这里标示出来。

      rs.conf();

      {
      "_id" : "rs0",
      "version" : 9,
      "members" : [
      {
      "_id" : 0,
      "host" : "192.168.1.136:27017" },
      {
      "_id" : 1,
      "host" : "192.168.1.137:27017" },
      {
      "_id" : 2,
      "host" : "192.168.1.138:27017" }
      ]
      }

      我们来设置,让id为1的主机可以优先成为主节点。

      cfg = rs.conf()
      cfg.members[0].priority = 1
      cfg.members[1].priority = 2
      cfg.members[2].priority = 1
      rs.reconfig(cfg)

      然后再执行rs.conf()命令查看优先级已经设置成功,主节点选举也会触发。

      {
      "_id" : "rs0",
      "version" : 9,
      "members" : [
      {
      "_id" : 0,
      "host" : "192.168.1.136:27017" },
      {
      "_id" : 1,
      "host" : "192.168.1.137:27017",
      "priority" : 2
      },
      {
      "_id" : 2,
      "host" : "192.168.1.138:27017" }
      ]
      }

      如果不想让一个从节点成为主节点可以怎么操作?
      a、使用rs.freeze(120)冻结指定的秒数不能选举成为主节点。
      b、按照上一篇设置节点为Non-Voting类型。

    3. 当主节点不能和大部分从节点通讯。把主机节点网线拔掉,嘿嘿:)

      优先级还可以这么用,如果我们不想设置什么hidden节点,就用secondary类型作为备份节点也不想让他成为主节点怎么办?看下图,共三个节点分布在两个数据中心,数据中心2的节点设置优先级为0不能成为主节点,但是可以参与选举、数据复制。架构还是很灵活吧!

  2. 奇数成员

    官方推荐副本集的成员数量为奇数,最多12个副本集节点,最多7个节点参与选举。最多12个副本集节点是因为没必要一份数据复制那么多份,备份太多反而增加了网络负载和拖慢了集群性能;而最多7个节点参与选举是因为内部选举机制节点数量太多就会导致1分钟内还选不出主节点,凡事只要适当就好。这个"12"、"7"数字还好,通过他们官方经过性能测试定义出来可以理解。具体还有哪些限制参考官方文档《 MongoDB Limits and Thresholds 》。 但是这里一直没搞懂整个集群为什么要奇数,通过测试集群的数量为偶数也是可以运行的,参考这个文章http://www.itpub.net/thread-1740982-1-1.html。后来突然看了一篇stackoverflow的文章终于顿悟了,mongodb本身设计的就是一个可以跨IDC的分布式数据库,所以我们应该把它放到大的环境来看。

    假设四个节点被分成两个IDC,每个IDC各两台机器,如下图。但这样就出现了个问题,如果两个IDC网络断掉,这在广域网上很容易出现的问题,在上面选举中提到只要主节点和集群中大部分节点断开链接就会开始一轮新的选举操作,不过mongodb副本集两边都只有两个节点,但是选举要求参与的节点数量必须大于一半,这样所有集群节点都没办法参与选举,只会处于只读状态。但是如果是奇数节点就不会出现这个问题,假设3个节点,只要有2个节点活着就可以选举,5个中的3个,7个中的4个。。。

  3. 心跳检测

    综上所述,整个集群需要保持一定的通信才能知道哪些节点活着哪些节点挂掉。mongodb节点会向副本集中的其他节点每两秒就会发送一次pings包,如果其他节点在10秒钟之内没有返回就标示为不能访问。每个节点内部都会维护一个状态映射表,表明当前每个节点是什么角色、日志时间戳等关键信息。如果是主节点,除了维护映射表外还需要检查自己能否和集群中内大部分节点通讯,如果不能则把自己降级为secondary只读节点。

  4. 副本同步

    副本集同步分为初始化同步和keep复制。初始化同步指全量从主节点同步数据,如果主节点数据量比较大同步时间会比较长。而keep复制指初始化同步过后,节点之间的实时同步一般是增量同步。初始化同步不只是在第一次才会被处罚,有以下两种情况会触发:

    1. secondary第一次加入,这个是肯定的。
    2. secondary落后的数据量超过了oplog的大小,这样也会被全量复制。

    那什么是oplog的大小?前面说过oplog保存了数据的操作记录,secondary复制oplog并把里面的操作在secondary执行一遍。但是oplog也是mongodb的一个集合,保存在local.oplog.rs里,但是这个oplog是一个capped collection也就是固定大小的集合,新数据加入超过集合的大小会覆盖。所以这里需要注意,跨IDC的复制要设置合适的oplogSize,避免在生产环境经常产生全量复制。oplogSize 可以通过–oplogSize设置大小,对于linux 和windows 64位,oplog size默认为剩余磁盘空间的5%。

    同步也并非只能从主节点同步,假设集群中3个节点,节点1是主节点在IDC1,节点3、节点3在IDC2,初始化节点2、节点3会从节点1同步数据。后面节点2、节点3会使用就近原则从当前IDC的副本集中进行复制,只要有一个节点从IDC1的节点1复制数据。

    设置同步还要注意以下几点:

    1. secondary不会从delayed和hidden成员上复制数据。
    2. 只要是需要同步,两个成员的buildindexes必须要相同无论是否是true和false。buildindexes主要用来设置是否这个节点的数据用于查询,默认为true。
    3. 如果同步操作30秒都没有反应,则会重新选择一个节点进行同步。

    到此,本章前面提到的问题全部解决了,不得不说mongodb的设计还真是强大!后续继续解决上一节这几个问题:

  • 主节点挂了能否自动切换连接?目前需要手工切换。
  • 主节点的读写压力过大如何解决?

还有这两个问题后续解决:

  • 从节点每个上面的数据都是对数据库全量拷贝,从节点压力会不会过大?
  • 数据压力大到机器支撑不了的时候能否做到自动扩展?