MongoDB 主从复制是将数据同步到多个 MongoDB 服务器的过程。复制提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性, 并可以保证数据的安全性。复制还允许您从硬件故障和服务中断中恢复数据。
主从复制是 MongoDB 最常用的复制方式,也是一个简单的数据库同步备份的集群技术。这种方式很灵活,可用于备份、故障恢复、读扩展等。
MongoDB 的主从复制至少需要两个节点。其中一个是主节点(Primary),负责处理客户端请求,其余的都是从节点(Secondary),负责复制主节点上的数据。
MongoDB 节点常见的搭配方式为:
一主一从
一主多从
主节点记录在其上的所有操作日志,从节点定期轮询主节点获取这些操作。然后对自己的数据副本执行这些操作,从而保证从节点的数据与主节点一致。MongoDB 主从复制结构图如下所示:
以上结构图中,客户端从主节点(Primary)读取数据,在客户端写入数据到主节点(Primary)时, 主节点与从节点进行数据交互保障数据的一致性。
下面将介绍一个实例来了解 MongoDB 的主从复制。步骤如下:
在开始之前,我们准备了 3 个 CentOS6.4 系统(均为VMWare虚拟机)。服务器IP地址和主机名分别如下:
主节点(S1):192.168.238.201
从节点(S2):192.168.238.202
从节点(S3):192.168.238.203
在开始启动 MongoDB 时,需要先安装 MongoDB,采用 mongodb-org-unstable-server-4.3.3-1.el6.x86_64.rpm 包进行安装。然后分别去配置每个 MongoDB 服务。
a、配置和启动主节点S1
主节点的配置信息如下:
systemLog: destination: file logAppend: true path: /var/log/mongodb/mongod.log storage: dbPath: /var/lib/mongo journal: enabled: true processManagement: fork: true pidFilePath: /var/run/mongodb/mongod.pid timeZoneInfo: /usr/share/zoneinfo net: port: 27017 bindIp: localhost,192.168.238.201 # 配置复制集 replication: replSetName: rs0
启动主节点,如下:
[root@S1 lib]# service mongod start Starting mongod: [ OK ]
b、配置和启动从节点S2
主节点的配置信息如下:
systemLog: destination: file logAppend: true path: /var/log/mongodb/mongod.log storage: dbPath: /var/lib/mongo journal: enabled: true processManagement: fork: true pidFilePath: /var/run/mongodb/mongod.pid timeZoneInfo: /usr/share/zoneinfo net: port: 27017 bindIp: localhost,192.168.238.202 # 配置复制集 replication: replSetName: rs0
启动主节点,如下:
[root@S2 lib]# service mongod start Starting mongod: [ OK ]
c、配置和启动从节点S3
主节点的配置信息如下:
systemLog: destination: file logAppend: true path: /var/log/mongodb/mongod.log storage: dbPath: /var/lib/mongo journal: enabled: true processManagement: fork: true pidFilePath: /var/run/mongodb/mongod.pid timeZoneInfo: /usr/share/zoneinfo net: port: 27017 bindIp: localhost,192.168.238.203 # 配置复制集 replication: replSetName: rs0
启动主节点,如下:
[root@S3 lib]# service mongod start Starting mongod: [ OK ]
(3)初始化MongoDB复制集合
使用 mongo 工具连接到主节点,如下:
mongo --host 192.168.238.201 --port 27017
在 Mongo 客户端使用命令 rs.initiate() 来启动一个新的副本集。如下:
> rs.initiate() { "info2" : "no configuration specified. Using a default configuration for the set", "me" : "192.168.238.201:27017", "ok" : 1, "$clusterTime" : { "clusterTime" : Timestamp(1530620665, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) } }, "operationTime" : Timestamp(1530620665, 1) } rs0:SECONDARY> rs0:PRIMARY>
上面,ok 为 1,表示启动成功。启动成功后命令提示符为“rs0:SECONDARY>”按一下回车键将看见“rs0:PRIMARY>”。
此时,我们就可以使用 rs.add() 指令为 rs0 复制集添加成员。如下:
rs0:PRIMARY> rs.add("192.168.238.202:27017") { "ok" : 1, "$clusterTime" : { "clusterTime" : Timestamp(1530620749, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) } }, "operationTime" : Timestamp(1530620749, 1) } rs0:PRIMARY> rs.add("192.168.238.203:27017") { "ok" : 1, "$clusterTime" : { "clusterTime" : Timestamp(1530620753, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) } }, "operationTime" : Timestamp(1530620753, 1) }
上面向 rs0 复制集中添加了两个成员,分别为 192.168.238.202 和 192.168.238.203。
你可以使用 rs.status() 指令查看 mongodb 状态。如下:
rs0:PRIMARY> rs.status() { "set" : "rs0", "date" : ISODate("2018-07-03T12:26:04.252Z"), "myState" : 1, "term" : NumberLong(1), "syncSourceHost" : "", "syncSourceId" : -1, "heartbeatIntervalMillis" : NumberLong(2000), "majorityVoteCount" : 2, "writeMajorityCount" : 2, "optimes" : { "lastCommittedOpTime" : { "ts" : Timestamp(1530620753, 1), "t" : NumberLong(1) }, "lastCommittedWallTime" : ISODate("2018-07-03T12:25:53.165Z"), "readConcernMajorityOpTime" : { "ts" : Timestamp(1530620753, 1), "t" : NumberLong(1) }, "readConcernMajorityWallTime" : ISODate("2018-07-03T12:25:53.165Z"), "appliedOpTime" : { "ts" : Timestamp(1530620753, 1), "t" : NumberLong(1) }, "durableOpTime" : { "ts" : Timestamp(1530620753, 1), "t" : NumberLong(1) }, "lastAppliedWallTime" : ISODate("2018-07-03T12:25:53.165Z"), "lastDurableWallTime" : ISODate("2018-07-03T12:25:53.165Z") }, "lastStableRecoveryTimestamp" : Timestamp(1530620716, 1), "electionCandidateMetrics" : { "lastElectionReason" : "electionTimeout", "lastElectionDate" : ISODate("2018-07-03T12:24:25.867Z"), "electionTerm" : NumberLong(1), "lastCommittedOpTimeAtElection" : { "ts" : Timestamp(0, 0), "t" : NumberLong(-1) }, "lastSeenOpTimeAtElection" : { "ts" : Timestamp(1530620665, 1), "t" : NumberLong(-1) }, "numVotesNeeded" : 1, "priorityAtElection" : 1, "electionTimeoutMillis" : NumberLong(10000), "newTermStartDate" : ISODate("2018-07-03T12:24:26.881Z"), "wMajorityWriteAvailabilityDate" : ISODate("2018-07-03T12:24:26.904Z") }, "members" : [ { "_id" : 0, "name" : "192.168.238.201:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 159, "optime" : { "ts" : Timestamp(1530620753, 1), "t" : NumberLong(1) }, "optimeDate" : ISODate("2018-07-03T12:25:53Z"), "syncSourceHost" : "", "syncSourceId" : -1, "infoMessage" : "could not find member to sync from", "electionTime" : Timestamp(1530620665, 2), "electionDate" : ISODate("2018-07-03T12:24:25Z"), "configVersion" : 3, "self" : true, "lastHeartbeatMessage" : "" }, { "_id" : 1, "name" : "192.168.238.202:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 14, "optime" : { "ts" : Timestamp(1530620753, 1), "t" : NumberLong(1) }, "optimeDurable" : { "ts" : Timestamp(1530620753, 1), "t" : NumberLong(1) }, "optimeDate" : ISODate("2018-07-03T12:25:53Z"), "optimeDurableDate" : ISODate("2018-07-03T12:25:53Z"), "lastHeartbeat" : ISODate("2018-07-03T12:26:03.170Z"), "lastHeartbeatRecv" : ISODate("2018-07-03T12:26:03.693Z"), "pingMs" : NumberLong(0), "lastHeartbeatMessage" : "", "syncSourceHost" : "192.168.238.201:27017", "syncSourceId" : 0, "infoMessage" : "", "configVersion" : 3 }, { "_id" : 2, "name" : "192.168.238.203:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 11, "optime" : { "ts" : Timestamp(1530620753, 1), "t" : NumberLong(1) }, "optimeDurable" : { "ts" : Timestamp(1530620753, 1), "t" : NumberLong(1) }, "optimeDate" : ISODate("2018-07-03T12:25:53Z"), "optimeDurableDate" : ISODate("2018-07-03T12:25:53Z"), "lastHeartbeat" : ISODate("2018-07-03T12:26:03.215Z"), "lastHeartbeatRecv" : ISODate("2018-07-03T12:26:03.760Z"), "pingMs" : NumberLong(6), "lastHeartbeatMessage" : "", "syncSourceHost" : "192.168.238.201:27017", "syncSourceId" : 0, "infoMessage" : "", "configVersion" : 3 } ], "ok" : 1, "$clusterTime" : { "clusterTime" : Timestamp(1530620753, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) } }, "operationTime" : Timestamp(1530620753, 1) }
上面文档中的 members 字段是一个数组,保存了所有复制集中的节点信息。
使用 mongo 客户端连接到主节点,插入数据。如下:
# 连接到主节点 mongo --host 192.168.238.201 --port 27017 # 向 test 数据库中的 test 集合插入一个文档 rs0:PRIMARY> db.test.insert({name:"test", version:"1.1.1"}) WriteResult({ "nInserted" : 1 })
再次,使用 mongo 客户端连接到从节点 192.168.238.202。如下:
# 连接到从节点 mongo --host 192.168.238.202 --port 27017 # 执行后允许进行读取操作 rs0:SECONDARY> rs.slaveOk() # 显示所有的数据库 rs0:SECONDARY> show dbs admin 0.000GB config 0.000GB local 0.000GB test 0.000GB # 切换到 test 数据库 rs0:SECONDARY> use test switched to db test # 查询 test 集合中的文档 rs0:SECONDARY> db.test.find() { "_id" : ObjectId("5e5b884159de83945fb5d3c6"), "name" : "test", "version" : "1.1.1" } rs0:SECONDARY>
连接到从节点 192.168.238.203,如下:
# 连接到从节点 mongo --host 192.168.238.203 --port 27017 # 执行后允许进行读取操作 rs0:SECONDARY> rs.slaveOk() # 显示所有的数据库 rs0:SECONDARY> show dbs admin 0.000GB config 0.000GB local 0.000GB test 0.000GB # 切换到 test 数据库 rs0:SECONDARY> use test switched to db test # 显示 test 数据库中的所有集合 rs0:SECONDARY> show collections test # 查询 test 集合中的文档 rs0:SECONDARY> db.test.find() { "_id" : ObjectId("5e5b884159de83945fb5d3c6"), "name" : "test", "version" : "1.1.1" }
如果你在执行其他操作之前没有执行 rs.slaveOk() 命令,则会抛出如下错误:
rs0:SECONDARY> show dbs 2020-03-01T18:27:49.612+0800 E QUERY [js] uncaught exception: Error: listDatabases failed:{ "topologyVersion" : { "processId" : ObjectId("5b381d08b1311857257fef9f"), "counter" : NumberLong(3) }, "operationTime" : Timestamp(1530647569, 1), "ok" : 0, "errmsg" : "not master and slaveOk=false", "code" : 13435, "codeName" : "NotMasterNoSlaveOk", "$clusterTime" : { "clusterTime" : Timestamp(1530647569, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) } } } : _getErrorWithCode@src/mongo/shell/utils.js:25:13 Mongo.prototype.getDBs/<@src/mongo/shell/mongo.js:135:19 Mongo.prototype.getDBs@src/mongo/shell/mongo.js:87:12 shellHelper.show@src/mongo/shell/utils.js:906:13 shellHelper@src/mongo/shell/utils.js:790:15 @(shellhelp2):1:1
如果你在从节点中执行insert命令,则抛出如下错误:
rs0:SECONDARY> db.test.insert({name:"123"}) WriteCommandError({ "topologyVersion" : { "processId" : ObjectId("5b381d08b1311857257fef9f"), "counter" : NumberLong(3) }, "operationTime" : Timestamp(1530647929, 1), "ok" : 0, "errmsg" : "not master", "code" : 10107, "codeName" : "NotMaster", "$clusterTime" : { "clusterTime" : Timestamp(1530647929, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) } } })
注意:主节点才能插入和读取数据,从节点只能读取数据。