前言

       数据库技术是信息系统的一个核心技术,发展很快,各种功能类型数据库层出不穷,之前工作中使用过关系型数据库(mysql、oracle等)、面相对象数据库(db4o)、key-value存储(Memcached 、Redis)、嵌入式关系数据库(SQLite)。最近学习和研究了一下MongoDB,整理了一下分享,本文主要介绍 如何搭建MongoDb复制集群,实现自动故障转移,读写分离。

Replica Set复制集简介

      MongoDB复制集(Replica Set)是MongoDB的核心高可用特性之一,是将数据同步在多个服务器的过程。它基于主节点的oplog日志持续传送到辅助节点,并重放得以实现主从节点一致。再结合心跳机制,当感知到主节点不可访问或宕机的情形下,辅助节点通过选举机制来从剩余的辅助节点中推选一个新的主节点从而实现自动切换。可以很容易的实现数据的读写分离,自动故障转移,并保持数据库集群自动容灾和高可用性。       

       MongoDB的复制集至少需要两个节点。其中一个是主节点,负责处理客户端请求,其余的都是从节点,负责复制主节点上的数据。

复制集节点类型

Replica Set复制集 主要有如下几种类型:

a、PRIMARY  主节点

b、SECONDARY 从节点

c、ARBITER 投票节点

d、Hidden 隐藏节点

e、Delayed 延迟节点

这几个类型的如何配置、之间差异、功能会在下面的具体搭建过程中进行说明。

 

搭建复制集群具体操作

1.安装MongoDB,启动多个实例

MongoDB安装过程此文略过,安装完成后

执行:

mkdir  -p /data/db1 #创建MongoDB数据目录

vim  db1.cnf #创建MongoDB配置文件,内容如下:



port=27021
fork=true dbpath=/data/db1 logpath=/data/db1/mongod.log httpinterface=true rest=true logappend=true replSet=mydbs # 复制集群名称 oplogSize=512



 保存退出(注意红色字体)

继续执行:

mkdir  -p /data/db2

vim  db2.cnf#内容如下:



port=27022 #!!!端口号依次增加
fork=true dbpath=/data/db2 logpath=/data/db2/mongod.log httpinterface=true rest=true logappend=true replSet=mydbs oplogSize=512



 保存退出(注意红色字体)

依次创建至db5。这样就有5个MongoDB实例目录。执行如下命令启动5个实例



mongod -f /data/db1/db1.cnf 
mongod -f /data/db2/db2.cnf mongod -f /data/db3/db3.cnf mongod -f /data/db4/db4.cnf mongod -f /data/db4/db5.cnf



 

2. Replica Set复制集基本配置

在控制台执行:



mongo --port 27021



  登录第一个MongoDB实例后执行下面的语句:



var rs_conf = {
    "_id": "mydbs",#注意这里的_id 是上面配置的复制集名称 "members": [ { "_id": 1, "host": "127.0.0.1:27021", priority: 99 #优先级 }, { "_id": 2, "host": "127.0.0.1:27022", priority: 1 }, { "_id": 3, "host": "127.0.0.1:27023", priority: 0 }, { "_id": 4, "host": "127.0.0.1:27024", priority: 1 } ] }



 

注明:priority优先级用于确定一个倾向成为主节点的程度。取值范围为0-100,Priority 0节点的选举优先级为0,不会被选举为Primary,这样的成员称为被动成员standby,由于硬件配置较差,设置为0以使用不可能成为主。对于跨机房复制集的情形,如A,B机房,最好将『大多数』节点部署在首选机房,以确保能选择合适的Primary。



 





 


 然后继续执行:


rs.initiate(rs_conf)


 等待约2-3s后执行:


rs.status()


 查看当前复制集的状态:


mydbs:PRIMARY> rs.status()
{
    "set" : "mydbs",
	"date" : ISODate("2018-09-17T13:25:59.655Z"), "myState" : 1, "members" : [ { "_id" : 1, "name" : "127.0.0.1:27021", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 3717, "optime" : Timestamp(1537190710, 1), "optimeDate" : ISODate("2018-09-17T13:25:10Z"), "electionTime" : Timestamp(1537190714, 1), "electionDate" : ISODate("2018-09-17T13:25:14Z"), "configVersion" : 1, "self" : true }, { "_id" : 2, "name" : "127.0.0.1:27022", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 49, "optime" : Timestamp(1537190710, 1), "optimeDate" : ISODate("2018-09-17T13:25:10Z"), "lastHeartbeat" : ISODate("2018-09-17T13:25:58.597Z"), "lastHeartbeatRecv" : ISODate("2018-09-17T13:25:58.607Z"), "pingMs" : 0, "configVersion" : 1 }, { "_id" : 3, "name" : "127.0.0.1:27023", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 49, "optime" : Timestamp(1537190710, 1), "optimeDate" : ISODate("2018-09-17T13:25:10Z"), "lastHeartbeat" : ISODate("2018-09-17T13:25:58.603Z"), "lastHeartbeatRecv" : ISODate("2018-09-17T13:25:58.615Z"), "pingMs" : 1, "configVersion" : 1 }, { "_id" : 4, "name" : "127.0.0.1:27024", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 49, "optime" : Timestamp(1537190710, 1), "optimeDate" : ISODate("2018-09-17T13:25:10Z"), "lastHeartbeat" : ISODate("2018-09-17T13:25:58.617Z"), "lastHeartbeatRecv" : ISODate("2018-09-17T13:25:58.614Z"), "pingMs" : 0, "configVersion" : 1 } ], "ok" : 1 } mydbs:PRIMARY> mydbs:PRIMARY>


 到此我们已经配置了一个简单的主从复制集群。

"stateStr" : "PRIMARY" 说明这个节点是主节点。可以读写

"stateStr" : "SECONDARY" 说明这个节点是从节点,默认是不可以读写的,

SECONDARY从节点默认不可读取,需要在SECONDARY中执行以下代码,设置可读取:

第一种方法:db.getMongo().setSlaveOk();
第二种方法:rs.slaveOk();

3.Replica Set复制集节点管理

增加节点:
rs.add({"_id":5,"host":"127.0.0.1:27024","priority":20}).
删除节点:
rs.remove("127.0.0.1:27014");   
执行完后可以通过
rs.conf() 查看当前的最新配置信息,rs.status()查看当前节点状态。

 4.添加ARBITER 投票节点

ARBITER 投票节点 并不保存数据,不可能成为主节点,不要求硬件配置很高,主要负责主节点挂了后,从SECONDARY 从节点中选举出primary主节点。为了确保复制集中有奇数的投票成员(包括primary),需要添加节点做为投票,否则primary不能运行时从节点不会自动切换primary。
Mongodb 3.0里,复制集成员最多50个,参与Primary选举投票的成员最多7个
对于超出7个的其他成员(Vote0)的vote属性必须设置为0,即不参与投票
添加投票节点可以执行:


rs.addArb("127.0.0.1:27025")

 执行完后可以通过rs.conf() 查看当前的最新配置信息,rs.status()查看当前节点状态。发现配置接增加了一个段:

{
		    "_id" : 5,
		    "name" : "127.0.0.1:27025",
		    "health" : 1, "state" : 7, "stateStr" : "ARBITER", "uptime" : 3, "lastHeartbeat" : ISODate("2018-09-17T14:20:46.165Z"), "lastHeartbeatRecv" : ISODate("2018-09-17T14:20:46.170Z"), "pingMs" : 0, "configVersion" : 2 }


 "stateStr" : "ARBITER" 说明新加入的节点是投票节点


5.添加Hiden隐藏节点

隐藏节点可以在选举中投票,但是不能被客户端引用,也不能成为主节点。也就是说这个节点不能用于读写分离的场景。
将27023设置为隐藏节点。注意,只有优先级为0的成员才能设置为隐藏节点。
如果设置优先级不为0的节点为隐藏节点,则会报错。


cfg=rs.conf();
cfg.members[2].hidden=1
rs.reconfig(cfg);

 执行完如上代码后执行 rs.conf() 查看发现:27023已经变成了hidden节点

{
			"_id" : 3,
			"host" : "127.0.0.1:27023",
			"arbiterOnly" : false, "buildIndexes" : true, "hidden" : true, "priority" : 0, "tags" : { }, "slaveDelay" : 0, "votes" : 1 },

6.添加Delayed 延迟节点

延迟节点包含复制集的部分数据,是复制集数据的子集,延迟节点上的数据通常落后于Primary一段时间(可配置,比如1个小时)。当人为错误或者无效的数据写入Primary时,可通过Delayed节点的数据进行回滚
延迟节点的要求:
优先级别为0(priority 0),避免参与primary选举
应当设置为隐藏节点(以避免应用程序查询延迟节点)
可以作为一个投票节点,设置 members[n].votes 值为1
延迟节点注意事项:
延迟时间应当等于和大于维护窗口持续期
应当小于oplog容纳数据的时间窗口。

我们下面把27024改成Delayed 延迟节点执行如下命令:


cfg = rs.conf()
cfg.members[3].priority = 0
cfg.members[3].hidden = true cfg.members[3].slaveDelay = 3600 rs.reconfig(cfg)

 执行完运行 rs.conf()查看27024最新配置:

{
			"_id" : 4,
			"host" : "127.0.0.1:27024",
			"arbiterOnly" : false, "buildIndexes" : true, "hidden" : true, "priority" : 0, "tags" : { }, "slaveDelay" : 3600, "votes" : 1 }

slaveDelay 显示延迟1小时,并且是隐藏节点程序无法访问

7.总结

至此,我们已经有个一个功能较为齐全的,可以在生产环境使用的复制集群:

27021:主节点

27022:从节点(实际生产中可以配置多个从节点)

27023:隐藏节点

27024:延迟节点

27025:投票节点

 8.读写分离实验

首先执行:

mongo --port 27021  #登录27021 主实例

在27021实例上面创建db:testdb ,创建t_table 表,插入一条记录。具体操作如下:

mydbs:PRIMARY> use testdb
switched to db testdb
mydbs:PRIMARY> db.t_table.insert({name:qiangyu,age:18}) 2018-09-18T00:47:38.887+0800 E QUERY ReferenceError: qiangyu is not defined at (shell):1:25 mydbs:PRIMARY> mydbs:PRIMARY> mydbs:PRIMARY> show dbs local 1.078GB mydbs:PRIMARY> use testdb switched to db testdb mydbs:PRIMARY> db.t_table.insert({name:"qiangyu",age:18}) WriteResult({ "nInserted" : 1 }) mydbs:PRIMARY> show dbs local 1.078GB testdb 0.078GB mydbs:PRIMARY> show tables system.indexes t_table mydbs:PRIMARY> db.t_table.find() { "_id" : ObjectId("5b9fdac675833ffb6316956a"), "name" : "qiangyu", "age" : 18 } mydbs:PRIMARY>

 

然后执行:

mongo --port 27022 #登录27022  从实例,查看db、表、数据是否已经同步过来。

执行: show dbs 会发现报错

执行:db.getMongo().setSlaveOk() 设置从节点可读。

具体操作如下:

mydbs:SECONDARY> mydbs:SECONDARY> show dbs
2018-09-18T00:50:04.528+0800 E QUERY SyntaxError: Unexpected identifier mydbs:SECONDARY> 2018-09-18T00:49:18.601+0800 E QUERY Error: listDatabases failed:{ "note" : "from execCommand", "ok" : 0, "errmsg" : "not master" } 2018-09-18T00:50:04.531+0800 E QUERY SyntaxError: Unexpected token ILLEGAL mydbs:SECONDARY> at Error (<anonymous>) 2018-09-18T00:50:04.532+0800 E QUERY SyntaxError: Unexpected identifier mydbs:SECONDARY> at Mongo.getDBs (src/mongo/shell/mongo.js:47:15) 2018-09-18T00:50:04.533+0800 E QUERY SyntaxError: Unexpected identifier mydbs:SECONDARY> at shellHelper.show (src/mongo/shell/utils.js:630:33) 2018-09-18T00:50:04.534+0800 E QUERY SyntaxError: Unexpected identifier mydbs:SECONDARY> at shellHelper (src/mongo/shell/utils.js:524:36) 2018-09-18T00:50:04.535+0800 E QUERY SyntaxError: Unexpected identifier mydbs:SECONDARY> at (shellhelp2):1:1 at src/mongo/shell/mongo.js:47 2018-09-18T00:50:04.537+0800 E QUERY SyntaxError: Unexpected token : mydbs:SECONDARY> db.getMongo().setSlaveOk() mydbs:SECONDARY> mydbs:SECONDARY> mydbs:SECONDARY> show dbs local 1.078GB testdb 0.078GB mydbs:SECONDARY> use testdb switched to db testdb mydbs:SECONDARY> show tables system.indexes t_table mydbs:SECONDARY> db.t_table.find() { "_id" : ObjectId("5b9fdac675833ffb6316956a"), "name" : "qiangyu", "age" : 18 } mydbs:SECONDARY>

可以看到从实例里面也已经有了 主实例相同的的 db、表、数据。

9.自动故障转移实验

执行ps aux|grep mongo 查看当前MongoDB所有的实例

[root@qiang db1]# ps aux|grep mongo
root       5187  0.8  1.9 3046544 73624 ? Rl Sep17 2:07 mongod -f /data/db1/db1.cnf root 5205 0.8 1.5 3037312 60556 ? Sl Sep17 2:07 mongod -f /data/db2/db2.cnf root 5224 0.8 1.6 3028088 63528 ? Rl Sep17 2:06 mongod -f /data/db3/db3.cnf root 5242 0.8 1.6 3031148 63772 ? Sl Sep17 2:04 mongod -f /data/db4/db4.cnf root 5461 0.8 1.5 605168 60828 ? Sl Sep17 1:32 mongod -f /data/db5/db5.cnf

 

执行kill 5187 关闭实例,这种方式关闭有可能导致MongoDB异常关闭不能启动。

最好是在client的shell里,use admin,执行db.shutdownServer()方式关闭。

执行:

mongo --port 27022  #登录27022实例


[root@qiang db1]# mongo --port 27022
MongoDB shell version: 3.0.6
connecting to: 127.0.0.1:27022/test Server has startup warnings: 2018-09-17T20:24:08.701+0800 I CONTROL [initandlisten] ** WARNING: You are running this process as the root user, which is not recommended. 2018-09-17T20:24:08.701+0800 I CONTROL [initandlisten] 2018-09-17T20:24:08.701+0800 I CONTROL [initandlisten] 2018-09-17T20:24:08.701+0800 I CONTROL [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/enabled is 'always'. 2018-09-17T20:24:08.701+0800 I CONTROL [initandlisten] ** We suggest setting it to 'never' 2018-09-17T20:24:08.701+0800 I CONTROL [initandlisten] 2018-09-17T20:24:08.701+0800 I CONTROL [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/defrag is 'always'. 2018-09-17T20:24:08.701+0800 I CONTROL [initandlisten] ** We suggest setting it to 'never' 2018-09-17T20:24:08.701+0800 I CONTROL [initandlisten] mydbs:PRIMARY> mydbs:PRIMARY> mydbs:PRIMARY>

 运行:

rs.status()

 显示如下:

可以看到 27021实例已经not reachable 不可使用执行:

27022实例变成了主节点。

到这里我们重新把27021节点启动:

mongod -f /data/db1/db1.cnf

 

在查看会发现27021 由之前的主节点变成了从节点。