一、副本集简介
副本集是一组服务器,其中有一个主节点(Primary),用于处理客户的请求;还有多个备份节点(Secondary),用于保存主节点的数据副本。如果主节点崩溃了,备份节点会自动将其中一个成员升级为新的主节点。所以,在生产中,可以使用副本集将数据保存到多台服务器上,即使其中一台或者多台服务器崩溃了,也能保证应用程序的正常运行和数据安全。
二、副本集复制原理
1.数据写入主节点,同时向local.oplog.rs集合中写入一条oplog。
2.备份节点第一次同步数据时,会先进行init sync,然后从主节点同步全量数据
3.随后,备份节点不断通过tailable cursor从主节点的local.oplog.rs集合中查询最新的oplog并应用到自身
三、副本集的搭建
本文为了方便使用单台服务器,指定不同的端口号来模拟副本集的搭建,进行测试学习。
1、环境准备
主节点:192.168.1.73:28000
备份节点1:192.168.1.73:28001
备份节点2:192.168.1.73:28002
2、安装mongodb
1)下载安装包并解压
# curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.0.6.tgz # 下载
# tar -zxvf mongodb-linux-x86_64-3.0.6.tgz # 解压
# mv mongodb-linux-x86_64-3.0.6/ /usr/local/mongodb # 将解压包拷贝到指定目录
2)将MongoDB 添加到 PATH 路径中:
# cat /root/.bash_profile
PATH=$PATH:$HOME/bin:/usr/local/mongodb/bin
# source /root/.bash_profile
3、运行三个MongoDB服务
1)配置文件
dbpath=/usr/local/repl0/ #数据目录
logpath=/var/log/mongodb/repl0.log #日志文件
logappend=true #日志追加
port = 28000 #端口
maxConns = 50 #最大连接数
pidfilepath = /var/run/repl0.pid
journal = true
journalCommitInterval = 200 #刷写提交机制
fork = true #守护进程模式
syncdelay = 60 #刷写数据到日志的频率
oplogSize = 1000 #操作日志,单位M
nssize = 16 #命名空间的文件大小,默认16M,最大2G。
noauth = true
unixSocketPrefix = /tmp
replSet=rs0 #副本集名称,同一个副本集,名称必须一致
需要注意的是:
1.数据目录和日志文件目录需要提前创建,否则启动MongoDB找不到路径会启动失败
2.由于搭建在同一台服务器上,三台MongoDB服务之间的dbpath、logpath、port需要不同,且端口无占用。
2)运行MongoDB服务
运行MongoDB服务可以直接使用命令行启动,也可以根据配置文件来启动,本文选择后者。
# mongod -f mongod_28000.conf
# mongod -f mongod_28001.conf
# mongod -f mongod_28002.conf
3)查看进程是否运行
# ps -ef | grep mongo
3、配置副本集
1)登录master服务器
# mongo 192.168.1.73:28000
2)初始化副本集
> cnf = {"_id":"rs0","members":[
... {"_id":1,"host":"192.168.1.73:28000","priority":1 },
... {"_id":2, "host":"192.168.1.73:28001","priority":1},
... {"_id":3, "host":"192.168.1.73:28002","priority":1}]
... }
{
"_id" : "rs0", #副本集名称
"members" : [ #指定副本集成员
{
"_id" : 1, #服务器的唯一id
"host" : "192.168.1.73:28000", #服务器主机
"priority" : 1 #优先级,默认为1,可设着为0-100,值越高优先级越大。优先级0为被动节点,不能成为活跃节点。
},
{
"_id" : 2,
"host" : "192.168.1.73:28001",
"priority" : 1
},
{
"_id" : 3,
"host" : "192.168.1.73:28002",
"priority" : 1
}
]
}
> rs.initiate(cnf) #初始化副本集
{ "ok" : 1 }
3)查看副本集是否配置成功
rs0:OTHER> rs.status()
{
"set" : "rs0", #副本集名称
"date" : ISODate("2018-06-28T16:55:50.130Z"),
"myState" : 1,
"members" : [
{
"_id" : 1,
"name" : "192.168.1.73:28000",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY", #主服务器
"uptime" : 669,
"optime" : Timestamp(1530204929, 1),
"optimeDate" : ISODate("2018-06-28T16:55:29Z"),
"electionTime" : Timestamp(1530204931, 1),
"electionDate" : ISODate("2018-06-28T16:55:31Z"),
"configVersion" : 1, #配置版本,每次更新配置会自动+1
"self" : true
},
{
"_id" : 2,
"name" : "192.168.1.73:28001",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY", #备份服务器
"uptime" : 20,
"optime" : Timestamp(1530204929, 1),
"optimeDate" : ISODate("2018-06-28T16:55:29Z"),
"lastHeartbeat" : ISODate("2018-06-28T16:55:49.612Z"),
"lastHeartbeatRecv" : ISODate("2018-06-28T16:55:49.625Z"),
"pingMs" : 0,
"configVersion" : 1
},
{
"_id" : 3,
"name" : "192.168.1.73:28002",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 20,
"optime" : Timestamp(1530204929, 1),
"optimeDate" : ISODate("2018-06-28T16:55:29Z"),
"lastHeartbeat" : ISODate("2018-06-28T16:55:49.612Z"),
"lastHeartbeatRecv" : ISODate("2018-06-28T16:55:49.625Z"),
"pingMs" : 0,
"configVersion" : 1
}
],
"ok" : 1
}
四、功能测试
1、复制功能
1)登录主节点,并在主节点中插入测试数据
rs0:PRIMARY> use test
switched to db test
rs0:PRIMARY> db.test.insert({"name":"aa",age:123}) #插入测试数据
WriteResult({ "nInserted" : 1 })
rs0:PRIMARY> db.test.insert({"name":"bb",age:132})
WriteResult({ "nInserted" : 1 })
rs0:PRIMARY> db.test.insert({"name":"cc",age:143})
WriteResult({ "nInserted" : 1 })
rs0:PRIMARY> db.test.find().pretty() #查看数据插入成功
{ "_id" : ObjectId("5b351827a131286ef6528c33"), "name" : "aa", "age" : 123 }
{ "_id" : ObjectId("5b351830a131286ef6528c34"), "name" : "bb", "age" : 132 }
{ "_id" : ObjectId("5b351836a131286ef6528c35"), "name" : "cc", "age" : 143 }
2)登录备份节点、检查数据是否同步
# mongo 192.168.1.73:28001 #登录备份节点
rs0:SECONDARY> show dbs #查看当前节点所有数据库
2018-06-28T13:18:45.565-0400 E QUERY Error: listDatabases failed:{ "note" : "from execCommand", "ok" : 0, "errmsg" : "not master" }
at Error (<anonymous>)
at Mongo.getDBs (src/mongo/shell/mongo.js:47:15)
at shellHelper.show (src/mongo/shell/utils.js:630:33)
at shellHelper (src/mongo/shell/utils.js:524:36)
at (shellhelp2):1:1 at src/mongo/shell/mongo.js:47
rs0:SECONDARY> use test
switched to db test
rs0:SECONDARY> db.test.find().pretty() #查看test数据库下所有数据
Error: error: { "$err" : "not master and slaveOk=false", "code" : 13435 }
在备份节点中执行查看数据库的命令发现以上报错,这是为什么呢?
这是因为备份节点默认是不对外提供读写功能的,如果想在备份节点提供读操作的话,需要执行rs.slaveOk() 。
rs0:SECONDARY> rs.slaveOk()
rs0:SECONDARY> use test
switched to db test
rs0:SECONDARY> db.test.find().pretty() #数据同步成功
{ "_id" : ObjectId("5b351827a131286ef6528c33"), "name" : "aa", "age" : 123 }
{ "_id" : ObjectId("5b351830a131286ef6528c34"), "name" : "bb", "age" : 132 }
{ "_id" : ObjectId("5b351836a131286ef6528c35"), "name" : "cc", "age" : 143 }
2、主从故障切换
对于配置在不同服务器上MongoDB在联网状态下可以通过在主机器添加防火墙规则,模拟主服务器宕机。对于本文,在同一服务器上部署了三个MongoDB的情况,我们选择杀死指令杀死主节点进程来模拟主节点的宕机。
1)模拟主节点宕机
# ps -ef | grep mongod_28000
root 48971 1 0 12:44 ? 00:00:07 mongod -f mongod_28000.conf
root 49086 1 0 12:45 ? 00:00:06 mongod -f mongod_28001.conf
root 49369 1 0 12:46 ? 00:00:06 mongod -f mongod_28002.conf
root 60690 33319 0 13:34 pts/0 00:00:00 grep --color=auto mongo
# kill 48971
# ps -ef | grep mongo
root 49086 1 0 12:45 ? 00:00:06 mongod -f mongod_28001.conf
root 49369 1 0 12:46 ? 00:00:06 mongod -f mongod_28002.conf
root 60739 33319 0 13:34 pts/0 00:00:00 grep --color=auto mongo
2)登录备份节点,查看其状态是否被提升为主节点
# mongo 192.168.1.73:28001
rs0:SECONDARY> quit() #该服务器仍然为备份节点
# mongo 192.168.1.73:28002
rs0:PRIMARY> #发现28002端口的备份节点被提升为主节点
3)查看当前副本集状态
rs0:PRIMARY> rs.status()
{
"set" : "rs0",
"date" : ISODate("2018-06-28T17:37:39.897Z"),
"myState" : 1,
"members" : [
{
"_id" : 1,
"name" : "192.168.1.73:28000",
"health" : 0,
"state" : 8,
"stateStr" : "(not reachable/healthy)", #状态不健康
"uptime" : 0,
"optime" : Timestamp(0, 0),
"optimeDate" : ISODate("1970-01-01T00:00:00Z"),
"lastHeartbeat" : ISODate("2018-06-28T17:37:39.102Z"),
"lastHeartbeatRecv" : ISODate("2018-06-28T17:34:24.858Z"),
"pingMs" : 0,
"lastHeartbeatMessage" : "Failed attempt to connect to 192.168.1.73:28000; couldn't connect to server 192.168.1.73:28000 (192.168.1.73), connection attempt failed",
"configVersion" : -1
},
{
"_id" : 2,
"name" : "192.168.1.73:28001",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 2530,
"optime" : Timestamp(1530206262, 1),
"optimeDate" : ISODate("2018-06-28T17:17:42Z"),
"lastHeartbeat" : ISODate("2018-06-28T17:37:39.050Z"),
"lastHeartbeatRecv" : ISODate("2018-06-28T17:37:38.968Z"),
"pingMs" : 0,
"configVersion" : 1 #配置文件版本+1
},
{
"_id" : 3,
"name" : "192.168.1.73:28002",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY", #新的主节点
"uptime" : 3065,
"optime" : Timestamp(1530206262, 1),
"optimeDate" : ISODate("2018-06-28T17:17:42Z"),
"electionTime" : Timestamp(1530207267, 1),
"electionDate" : ISODate("2018-06-28T17:34:27Z"),
"configVersion" : 1,
"self" : true
}
],
"ok" : 1
}
4)重新启动宕机的主节点192.168.1.73:28000
rs0:PRIMARY> rs.status()
{
"set" : "rs0",
"date" : ISODate("2018-07-01T16:12:15.964Z"),
"myState" : 1,
"members" : [
{
"_id" : 1,
"name" : "192.168.1.73:28000",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY", #重新启动宕机的主节点,自动将其将为备份节点
"uptime" : 21,
"optime" : Timestamp(1530461212, 1),
"optimeDate" : ISODate("2018-07-01T16:06:52Z"),
"lastHeartbeat" : ISODate("2018-07-01T16:12:14.297Z"),
"lastHeartbeatRecv" : ISODate("2018-07-01T16:12:15.467Z"),
"pingMs" : 0,
"configVersion" : 1 #配置文件版本为1
},
{
"_id" : 2,
"name" : "192.168.1.73:28001",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 359,
"optime" : Timestamp(1530461212, 1),
"optimeDate" : ISODate("2018-07-01T16:06:52Z"),
"lastHeartbeat" : ISODate("2018-07-01T16:12:14.289Z"),
"lastHeartbeatRecv" : ISODate("2018-07-01T16:12:14.264Z"),
"pingMs" : 0,
"configVersion" : 1
},
{
"_id" : 3,
"name" : "192.168.1.73:28002",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 421,
"optime" : Timestamp(1530461212, 1),
"optimeDate" : ISODate("2018-07-01T16:06:52Z"),
"electionTime" : Timestamp(1530461444, 1),
"electionDate" : ISODate("2018-07-01T16:10:44Z"),
"configVersion" : 1,
"self" : true
}
],
"ok" : 1
}
3、主从手动切换
手动切换主节点为192.168.1.73:28000
1)重新定义配置文件
1.将192.168.1.73:28000优先级的优先级调大
rs0:PRIMARY> cfg=rs.conf()
rs0:PRIMARY> cfg.members[0].priority=2 #将192.168.1.73:28000节点的优先级设置为2
2.重新加载配置文件
rs0:PRIMARY> rs.reconfig(cfg)
3.查看当前配置文件是否修改成功
rs0:SECONDARY> rs.conf() #可以看到当前节点已经由rs0:PRIMARY变为rs0:SECONDARY
4.登录192.168.1.73:28000节点,发现其变为主节点
五、如何设计副本集
副本集遵循“大多数”原则,“大多数”即一半以上的成员,副本集在选择主节点的时候需要进行选举投票,只有大多数成员支持的情况下,该节点才能成为主节点。
1、怎么才算大多数
假设复制集内投票成员数量为N,则大多数为 N/2 + 1,当复制集内存活成员数量不足大多数时,整个复制集将无法选举出Primary,复制集将无法提供写服务,处于只读状态。
2、配置方式一
将“大多数”成员放在同一个数据中心,并将副本集的主节点放置在该数据中心,该数据节点的优先级大于另一数据节点。
如5个节点,3个节点放置待数据中心A,两个节点放置在数据中心B。
情况1:主节点在数据中心A,主节点宕机,A、B两数据中心可以联通,4个节点进行投票选举,“大多数”支持的节点被提升为主节点。
情况2:主节点在数据中心A,A、B两数据中心因为网络原因不可以联通,A数据区域正常运行,B数据区域因为只有2个节点,不能通过选举提升其中一个备份节点为主节点,保证不会同时出现两个主节点
情况3:主节点在数据中心A,主节点宕机且A、B两数据中心因为网络原因导致不可以联通,所有成员无法提升为主节点
3、配置方式二
将两个数据中心放置相同数量的成员,同时增加一个成员(选举仲裁者)放置在其他服务器上,该成员可以不需要复制数据,只是用来进行投票选举。
如:共5个节点,数据A、B区域各放置2个节点,另外一台服务器上放置选举仲裁者。
情况1:主节点在数据中心A,A、B两数据中心可以联通,主节点宕机后,4个节点进行投票选举,“大多数”支持的节点被提升为主节点,仲裁者不能成为主节点,只参与投票选举。
情况2:主节点在数据中心A,主节点宕机且A、B两数据中心可以联通,这时A数据中心2个节点都变为备份节点,B数据中心2个节点+仲裁者进行投票选举提升一个节点为主节点,保证主节点的唯一性。
情况3:主节点在数据中心A,主节点宕机且A、B两数据中心因为网路原因导致不可以联通,这时A数据中心2个节点都变为备份节点,B数据中心2个节点+仲裁者进行投票选举提升一个节点为主节点,保证主节点的唯一性。