搭建步骤见:MongoDB的搭建。
规划如下:
ip | MongoDB端口 | 副本角色 | Mongodb版本 |
---|---|---|---|
172.16.113.137 | 27017 | Primary | 3.2.10 |
172.16.113.137 | 27018 | Secondary | 3.2.10 |
172.16.113.129 | 27017 | Secondary | 3.2.10 |
参数说明:
参数 | 说明 | 示例 |
---|---|---|
replSet | 副本集名称 | rs0 |
oplogSize | 操作日志大小 | 128 |
我们上面搭建的是MongoDB的单实例的方式,有很多参数没有加,所以启动的时候一定要指定一些参数,如上表中。
mongod --replSet rs0 --port 27017 --bind_ip localhost,<hostname(s)|ip address(es)> --dbpath /data/mongodb/rs0-0 --oplogSize 128
我这边选择将使用到的参数写入配置文件再进行启动
vim mongod.conf ... replSet=rs0 oplogSize=128 keyFile=/usr/bin/mongo/mongodb-keyfile ....
必须要配置keyFile,不然不能通信,具体配置方法可见mongodb的主从复制
然后启动即可
使用 mongo 进入第一个 mongod 示例,使用 rs.initiate() 进行初始化
rsconf = { _id: "rs0", members: [ { _id: 0, host: "172.16.113.137:27017" }, { _id: 1, host: "172.16.113.137:27018" }, { _id: 2, host: "172.16.113.129:27017" } ] } rs.initiate( rsconf )
查看
在 mongo shell 中执行 rs.conf() 可以看到每个节点中 host、arbiterOnly、hidden、priority、 votes、slaveDelay等属性。
rs0:PRIMARY> rs.conf() { "_id" : "rs0", "version" : 3, "protocolVersion" : NumberLong(1), "members" : [ { "_id" : 0, "host" : "172.16.113.137:27017", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : { }, "slaveDelay" : NumberLong(0), "votes" : 1 }, { "_id" : 1, "host" : "172.16.113.137:27018", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : { }, "slaveDelay" : NumberLong(0), "votes" : 1 }, { "_id" : 2, "host" : "172.16.113.129:27017", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : { }, "slaveDelay" : NumberLong(0), "votes" : 1 } ], "settings" : { "chainingAllowed" : true, "heartbeatIntervalMillis" : 2000, "heartbeatTimeoutSecs" : 10, "electionTimeoutMillis" : 10000, "getLastErrorModes" : { }, "getLastErrorDefaults" : { "w" : 1, "wtimeout" : 0 }, "replicaSetId" : ObjectId("61502b5d8887b4e8bf7c27b6") } } rs0:PRIMARY>
查看一下当前副本集的状态及角色分配
rs0:PRIMARY> rs.status(); { "set" : "rs0", "date" : ISODate("2021-09-26T08:40:27.280Z"), "myState" : 1, "term" : NumberLong(1), "heartbeatIntervalMillis" : NumberLong(2000), "members" : [ { "_id" : 0, "name" : "172.16.113.137:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 1850, "optime" : { "ts" : Timestamp(1632645189, 1), "t" : NumberLong(1) }, "optimeDate" : ISODate("2021-09-26T08:33:09Z"), "electionTime" : Timestamp(1632643933, 2), "electionDate" : ISODate("2021-09-26T08:12:13Z"), "configVersion" : 7, "self" : true }, { "_id" : 1, "name" : "172.16.113.137:27018", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 509, "optime" : { "ts" : Timestamp(1632645189, 1), "t" : NumberLong(1) }, "optimeDate" : ISODate("2021-09-26T08:33:09Z"), "lastHeartbeat" : ISODate("2021-09-26T08:40:26.016Z"), "lastHeartbeatRecv" : ISODate("2021-09-26T08:40:27Z"), "pingMs" : NumberLong(0), "syncingTo" : "172.16.113.137:27017", "configVersion" : 7 }, { "_id" : 2, "name" : "172.16.113.129:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 319, "optime" : { "ts" : Timestamp(1632645189, 1), "t" : NumberLong(1) }, "optimeDate" : ISODate("2021-09-26T08:33:09Z"), "lastHeartbeat" : ISODate("2021-09-26T08:40:26.253Z"), "lastHeartbeatRecv" : ISODate("2021-09-26T08:40:24.166Z"), "pingMs" : NumberLong(0), "configVersion" : 7 } ], "ok" : 1 } rs0:PRIMARY>
到这里副本集就搭建成功了
我们在Primary插入一点数据
rs0:PRIMARY> show dbs admin 0.000GB gengjin 0.000GB hello 0.001GB kobe 0.001GB local 0.000GB nihao 0.000GB rs0:PRIMARY> use nihao switched to db nihao rs0:PRIMARY> show tables; nihao rs0:PRIMARY> db.nihao.insert({"name":"gengjin"}) WriteResult({ "nInserted" : 1 }) rs0:PRIMARY>
在两个SECONDARY查
rs0:SECONDARY> use admin switched to db admin rs0:SECONDARY> db.auth("root","root") 1 rs0:SECONDARY> show dbs admin 0.000GB gengjin 0.000GB hello 0.000GB kobe 0.001GB local 0.000GB nihao 0.000GB rs0:SECONDARY> db.getMongo().setSlaveOk() rs0:SECONDARY> use nihao switched to db nihao rs0:SECONDARY> db.nihao.find() { "_id" : ObjectId("614fedaae07683d6c0444a00"), "nihao" : "shazi" } { "_id" : ObjectId("615033b83f5bc322a1cf609b"), "name" : "gengjin" } rs0:SECONDARY>
数据同步正常
可以直接停掉主节点172.16.113.137:27017 来测试下主节点挂掉后,副本节点重新选举出新的主节点,即自动故障转移(Automatic Failover)
rs0:PRIMARY> use admin switched to db admin rs0:PRIMARY> db.shutdownServer() server should be down... 2021-09-26T16:54:29.518+0800 I NETWORK [thread1] trying reconnect to 127.0.0.1:27017 (127.0.0.1) failed 2021-09-26T16:54:30.600+0800 I NETWORK [thread1] Socket recv() errno:104 Connection reset by peer 127.0.0.1:27017 2021-09-26T16:54:30.600+0800 I NETWORK [thread1] SocketException: remote: (NONE):0 error: 9001 socket exception [RECV_ERROR] server [127.0.0.1:27017] 2021-09-26T16:54:30.600+0800 I NETWORK [thread1] reconnect 127.0.0.1:27017 (127.0.0.1) failed failed 2021-09-26T16:54:30.602+0800 I NETWORK [thread1] trying reconnect to 127.0.0.1:27017 (127.0.0.1) failed 2021-09-26T16:54:30.603+0800 W NETWORK [thread1] Failed to connect to 127.0.0.1:27017, reason: errno:111 Connection refused 2021-09-26T16:54:30.603+0800 I NETWORK [thread1] reconnect 127.0.0.1:27017 (127.0.0.1) failed failed > >
查看rs.status();可以发现172.16.113.137:27018已经提升为Primary了,旧Primary的health=0
查看新Primary的晋升日志
在1000毫秒之内未发现Primary,所以决定竞选Primary,并且成功。后面重新将旧主启动起来,它会重新被新主拉入集群。
#添加 rs.add("192.168.199.164:27020") #删除 rs.remove("192.168.199.164:27020") 删除副本集之后要记得删除对应数据目录哦
cfg = rs.conf() cfg.members[0].host = "192.168.199.164:27021" rs.reconfig(cfg)
将指定的节点的优先级priority加到最大,即可成为主节点:
我们通过rs.conf()中的priority可以看到,每个节点都可以配置优先级,优先级最高的将成为主节点
#修改优先级,让 27017 成为主节点
#修改优先级 rs0:PRIMARY> conf.members[0].priority = 2 #如果改其他的就调整[0/1/2] 2 rs0:PRIMARY> #重新加载配置文件,强制进行一次选举,期间所有节点均为从节点Secondary rs0:PRIMARY> rs.reconfig(conf) { "ok" : 1 }
可以看到已经发生了切换,调整优先级为2的实例变成主了
添加仲裁节点与添加数据节点是一样的。只是在添加时需调用,实例配置跟SECONDARY节点一致
rs.addArb("172.16.113.129:27018")
副本集要求参与选举投票(vote)的节点数为奇数。当我门数据集节点为偶数时,可以添加一个仲裁节点组成奇数。仲裁节点只参加投票不拥有数据,它对物理资源需要很少。
通过实际测试发现,当整个副本集集群中达到50%的节点(包括仲裁节点)不可用的时候,剩下的节点只能成为secondary节点,整个集群只能读不能 写。比如集群中有1个primary节点,2个secondary节点,加1个arbit节点时:当两个secondary节点挂掉了,那么剩下的原来的 primary节点也只能降级为secondary节点;当集群中有1个primary节点,1个secondary节点和1个arbit节点,这时即使 primary节点挂了,剩下的secondary节点也会自动成为primary节点。因为仲裁节点不复制数据,因此利用仲裁节点可以实现最少的机器开销达到两个节点热备的效果。
hidden(成员用于支持专用功能):这样设置后此机器在读写中都不可见,并且不会被选举为Primary,但是可以投票,一般用于备份数据。
资源有限、我们这边把刚才添加的仲裁节点删掉,重新再已专用功能节点加进来:
#删除 rs0:PRIMARY> rs.remove("172.16.113.129:27018") { "ok" : 1 } rs0:PRIMARY> #添加 rs0:PRIMARY> rs.add({"_id":3,"host":"172.16.113.129:27018","priority":0,"hidden":true}) { "ok" : 1 } #查看 { "_id" : 3, "name" : "172.16.113.129:27018", "health" : 1, "state" : 9, "stateStr" : "ROLLBACK", "uptime" : 16, "optime" : { "ts" : Timestamp(1632650626, 1), "t" : NumberLong(3) }, "optimeDate" : ISODate("2021-09-26T10:03:46Z"), "lastHeartbeat" : ISODate("2021-09-26T10:12:58.614Z"), "lastHeartbeatRecv" : ISODate("2021-09-26T10:12:57.652Z"), "pingMs" : NumberLong(0), "syncingTo" : "172.16.113.129:27017", "configVersion" : 11 } ], "ok" : 1
Delayed(成员用于支持专用功能):可以指定一个时间延迟从primary节点同步数据。主要用于处理误删除数据马上同步到从节点导致的不一致问题。
$ rs.add({"_id":3,"host":"172.16.113.129:27018","priority":0,"hidden":true,"slaveDelay":60}) #单位 s
具体如下:
角色 | primary(能否) | 客户端可见 | 参与投票 | 延迟同步 | 复制数据 |
---|---|---|---|---|---|
Default | √ | √ | √ | X | √ |
Secondary-Only | X | √ | √ | X | √ |
Hidden | X | X | √ | X | √ |
Delayed | X | √ | √ | √ | √ |
Arbiters | X | X | √ | X | X |
Non-Voting | √ | √ | X | X | √ |
MongoDB副本集对读写分离的支持是通过Read Preferences特性进行支持的,这个特性非常复杂和灵活。设置读写分离需要先在从节点SECONDARY 设置 setSlaveOk
应用程序驱动通过read reference来设定如何对副本集进行读取操作,默认的,客户端驱动所有的读操作都是直接访问primary节点的,从而保证了数据的严格一致性。
有如下几种模式:
模式 | 描述 |
---|---|
primary | 主节点,默认模式,读操作只在主节点,如果主节点不可用,报错或者抛出异常。 |
primaryPreferred | 首选主节点,大多情况下读操作在主节点,如果主节点不可用,如故障转移,读操作在从节点。 |
secondary | 从节点,读操作只在从节点, 如果从节点不可用,报错或者抛出异常。 |
secondaryPreferred | 首选从节点,大多情况下读操作在从节点,特殊情况(如单主节点架构)读操作在主节点。 |
nearest | 最邻近节点,读操作在最邻近的成员,可能是主节点或者从节点,关于最邻近的成员请参考官网nearest |