MongoDB 复制(副本集)

MongoDB复制是将数据同步在多个服务器的过程。

复制提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性, 并可以保证数据的安全性。

复制还允许您从硬件故障和服务中断中恢复数据。

为什么要复制?

  • 为了让数据安全
  • 高(24* 7)数据可用性
  • 灾难恢复
  • 无停机维护(如备份,索引重建,压实)
  • 读缩放(额外的副本读取)
  • 副本集对应用程序是透明

MongoDB中复制的工作原理

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

MongoDB各个节点常见的搭配方式为:一主一从、一主多从。

主节点记录在其上的所有操作oplog,从节点定期轮询主节点获取这些操作,然后对自己的数据副本执行这些操作,从而保证从节点的数据与主节点一致。

MongoDB复制结构图如下所示:

MongoDB 复制(副本集) - 图1

以上结构图中,客户端在主节点读取数据,在客户端写入数据到主节点,主节点与从节点进行数据交互保障数据的一致性

副本集特点

  • N 个节点的集群
  • 任何节点可作为主节点
  • 所有写入操作都在主节点上
  • 自动故障转移
  • 自动恢复

设置一个副本集

将mongod实例转换成独立的副本集。要转换到副本设置遵循以下步骤:

关闭停止已经运行的MongoDB服务器

现在启动MongoDB服务器通过指定 —replSet 选项。

—replSet 基本语法如下:

  1. sudo mongod --port "PORT" --dbpath "YOUR_DB_DATA_PATH" --replSet "REPLICA_SET_INSTANCE_NAME"

示例

1. 用适当的选项启动副本集的每个成员

启动副本集名称 rs0

sudo rm -rf /MongoDB/node1 /MongoDB/node2 /MongoDB/node3sudo mkdir -p /MongoDB/node1 /MongoDB/node2 /MongoDB/node3sudo mongod —bind_ip 192.168.17.129 —port 27020 —dbpath "/MongoDB/node1" —replSet rs0sudo mongod —bind_ip 192.168.17.129 —port 27021 —dbpath "/MongoDB/node2" —replSet rs0sudo mongod —bind_ip 192.168.17.129 —port 27022 —dbpath "/MongoDB/node3" —replSet rs0</div>还可以通过配置文件中指定副本集名称。启动mongod使用配置文件,与配置选项指定的文件:

2. mongo shell连接副本集

  1. mongo -port 27020 --host 192.168.17.129

3. 初始化initiate副本集

利用rs.initiate()在副本集的一个成员上:

  1. rs.initiate()

MongoDB会初始化一个默认的复制集配置。

4. 验证初始副本集配置

使用 rs.conf() 显示副本集配置对象:

  1. rs.conf()

副本集配置对象如下:

  1. rs0:OTHER> rs.conf()
  2. {
  3. "_id" : "rs0",
  4. "version" : 1,
  5. "protocolVersion" : NumberLong(1),
  6. "members" : [
  7. {
  8. "_id" : 0,
  9. "host" : "192.168.17.129:27020",
  10. "arbiterOnly" : false,
  11. "buildIndexes" : true,
  12. "hidden" : false,
  13. "priority" : 1,
  14. "tags" : {
  15. },
  16. "slaveDelay" : NumberLong(0),
  17. "votes" : 1
  18. }
  19. ],
  20. "settings" : {
  21. "chainingAllowed" : true,
  22. "heartbeatIntervalMillis" : 2000,
  23. "heartbeatTimeoutSecs" : 10,
  24. "electionTimeoutMillis" : 10000,
  25. "getLastErrorModes" : {
  26. },
  27. "getLastErrorDefaults" : {
  28. "w" : 1,
  29. "wtimeout" : 0
  30. },
  31. "replicaSetId" : ObjectId("579b3500299da8059cc5fb99")
  32. }
  33. }
  34. rs0:PRIMARY>

复制(副本集)当前的状态 rs.status()

此输出反映了副本集的当前状态,使用来自副本集的其他成员发送的心跳数据包的数据。

  1. rs0:PRIMARY> rs.status()
  2. {
  3. "set" : "rs0",
  4. "date" : ISODate("2016-07-29T11:09:58.433Z"),
  5. "myState" : 1,
  6. "term" : NumberLong(1),
  7. "heartbeatIntervalMillis" : NumberLong(2000),
  8. "members" : [
  9. {
  10. "_id" : 0,
  11. "name" : "192.168.17.129:27020",
  12. "health" : 1,
  13. "state" : 1,
  14. "stateStr" : "PRIMARY",
  15. "uptime" : 1200,
  16. "optime" : {
  17. "ts" : Timestamp(1469789441, 1),
  18. "t" : NumberLong(1)
  19. },
  20. "optimeDate" : ISODate("2016-07-29T10:50:41Z"),
  21. "electionTime" : Timestamp(1469789440, 2),
  22. "electionDate" : ISODate("2016-07-29T10:50:40Z"),
  23. "configVersion" : 1,
  24. "self" : true
  25. }
  26. ],
  27. "ok" : 1
  28. }
  29. rs0:PRIMARY>

5. 将剩下的成员添加到副本集

必须连接到副本集primary主节点, 才能使用rs.add()添加剩余的成员。

  1. `rs.add()`在某些情况下,触发一个选举。如果你连接到主节点primary成为从节点secondary,你需要连接Mongo shell到主节点primary继续增加新的副本集成员
  2. 利用`rs.status()`识别副本集主节点primary

下面的示例添加了两个成员:

添加节点 192.168.17.129:27021

  1. rs0:PRIMARY> rs.add('192.168.17.129:27021')
  2. { "ok" : 1 }
  3. rs0:PRIMARY>

添加节点 192.168.17.129:27022

  1. rs0:PRIMARY> rs.add('192.168.17.129:27022')
  2. { "ok" : 1 }
  3. rs0:PRIMARY> rs.status()
  4. {
  5. "set" : "rs0",
  6. "date" : ISODate("2016-07-29T11:17:28.721Z"),
  7. "myState" : 1,
  8. "term" : NumberLong(1),
  9. "heartbeatIntervalMillis" : NumberLong(2000),
  10. "members" : [
  11. {
  12. "_id" : 0,
  13. "name" : "192.168.17.129:27020",
  14. "health" : 1,
  15. "state" : 1,
  16. "stateStr" : "PRIMARY",
  17. "uptime" : 1650,
  18. "optime" : {
  19. "ts" : Timestamp(1469791047, 1),
  20. "t" : NumberLong(1)
  21. },
  22. "optimeDate" : ISODate("2016-07-29T11:17:27Z"),
  23. "electionTime" : Timestamp(1469789440, 2),
  24. "electionDate" : ISODate("2016-07-29T10:50:40Z"),
  25. "configVersion" : 3,
  26. "self" : true
  27. },
  28. {
  29. "_id" : 1,
  30. "name" : "192.168.17.129:27021",
  31. "health" : 1,
  32. "state" : 2,
  33. "stateStr" : "SECONDARY",
  34. "uptime" : 115,
  35. "optime" : {
  36. "ts" : Timestamp(1469790932, 1),
  37. "t" : NumberLong(1)
  38. },
  39. "optimeDate" : ISODate("2016-07-29T11:15:32Z"),
  40. "lastHeartbeat" : ISODate("2016-07-29T11:17:27.171Z"),
  41. "lastHeartbeatRecv" : ISODate("2016-07-29T11:17:27.159Z"),
  42. "pingMs" : NumberLong(0),
  43. "configVersion" : 3
  44. },
  45. {
  46. "_id" : 2,
  47. "name" : "192.168.17.129:27022",
  48. "health" : 1,
  49. "state" : 0,
  50. "stateStr" : "STARTUP",
  51. "uptime" : 1,
  52. "optime" : {
  53. "ts" : Timestamp(0, 0),
  54. "t" : NumberLong(-1)
  55. },
  56. "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
  57. "lastHeartbeat" : ISODate("2016-07-29T11:17:27.168Z"),
  58. "lastHeartbeatRecv" : ISODate("2016-07-29T11:17:28.182Z"),
  59. "pingMs" : NumberLong(1),
  60. "configVersion" : -2
  61. }
  62. ],
  63. "ok" : 1
  64. }
  65. rs0:PRIMARY>

当完成时,您有一个完整功能的副本集。新的副本集将选出一个primary主节点。

6. 检查副本集的状态

使用rs.status() 操作:

  1. rs.status()

7. 删除副本

  1. rs.remove("192.168.17.129:27021")

副本集成员

副本集的每个成员都有一个反映它在集合中的配置的状态。

NumberNameState Description
0STARTUP还没有任何一个的活跃成员。所有成员在这个状态下都开始。副本集的符号解析配置文件在启动。
1PRIMARY主节点,唯一可以接受写操作的成员
2SECONDARY备份节点,复制数据存储的成员
3RECOVERING成员要么执行启动自我检查,或过渡完成回滚或重新同步。
5STARTUP2该成员已加入该组,并运行一个初始同步。
6UNKNOWN成员的状态,是从其他集合的成员看,目前还不知道。
7ARBITER仲裁者不复制数据,只是为了参加选举。
8DOWN节点不可到达
9ROLLBACK该成员正在积极执行回滚。数据不可用于读取。
10REMOVED该成员曾经在一个副本集,但随后被删除。

测试性能

批量入库

  1. db.eval(function, arguments)
ParameterTypeDescription
functionfunction要执行的一个JavaScript函数
argumentslist参数列表传递给JavaScript函数。省略函数如果不带参数。
  1. use example
  2. db.eval(
  3. function(num) {
  4. for (var i=0;i<num;i++)
  5. {
  6. db.mycol.save({'_id':i})
  7. }
  8. },10000
  9. )
  • 查看一下状态
  1. rs0:PRIMARY> rs.status()
  • 查看从节点数据情况

MongoDB 复制(副本集) - 图2

  1. mongo --port 27021 --host 192.168.17.129
  2. rs0:SECONDARY> show dbs
  3. 2016-09-14T23:02:41.228+0800 E QUERY [thread1] Error: listDatabases failed:{ "ok" : 0, "errmsg" : "not master and slaveOk=false", "code" : 13435 } :
  4. _getErrorWithCode@src/mongo/shell/utils.js:25:13
  5. Mongo.prototype.getDBs@src/mongo/shell/mongo.js:62:1
  6. shellHelper.show@src/mongo/shell/utils.js:760:19
  7. shellHelper@src/mongo/shell/utils.js:650:15
  8. @(shellhelp2):1:1
  9. rs0:SECONDARY> rs.slaveOk()
  10. rs0:SECONDARY> show dbs
  11. example 0.000GB
  12. local 0.000GB
  13. rs0:SECONDARY> db.mycol.count()
  14. 10000

注意: 主从启动之后,连接slave可以成功连上,但是在slave中执行 show dbs 的时候就报错了:

  1. QUERY Error: listDatabases failed:{ "note" : "from execCommand", "ok" : 0, "errmsg" : "not master" }

解决方法:

在报错的slave机器上执行 rs.slaveOk() 方法即可。

解释一下具体slaveOk方法是什么意思?

  1. Provides a shorthand for the following operation:
  2. db.getMongo().setSlaveOk()
  3. This allows the current connection to allow read operations to run on secondary members. See the readPref() method for more fine-grained control over read preference in the mongo shell.

测试

测试一下集群是否可用:

关掉主节点,模拟主节点宕机的情景

  1. python@ubuntu:~$ ps -ef | grep mongod
  2. python 6731 2119 0 913 ? 00:01:02 /opt/sublime_text/sublime_text /var/log/mongodb/mongod.log
  3. root 37130 6046 0 22:31 pts/22 00:00:00 sudo mongod --bind_ip 192.168.17.129 --port 27020 --dbpath /MongoDB/node1 --replSet rs0
  4. root 37131 37130 0 22:31 pts/22 00:00:21 mongod --bind_ip 192.168.17.129 --port 27020 --dbpath /MongoDB/node1 --replSet rs0
  5. root 37180 37153 0 22:31 pts/12 00:00:00 sudo mongod --bind_ip 192.168.17.129 --port 27021 --dbpath /MongoDB/node2 --replSet rs0
  6. root 37181 37180 0 22:31 pts/12 00:00:15 mongod --bind_ip 192.168.17.129 --port 27021 --dbpath /MongoDB/node2 --replSet rs0
  7. root 37229 37202 0 22:31 pts/21 00:00:00 sudo mongod --bind_ip 192.168.17.129 --port 27022 --dbpath /MongoDB/node3 --replSet rs0
  8. root 37230 37229 0 22:31 pts/21 00:00:15 mongod --bind_ip 192.168.17.129 --port 27022 --dbpath /MongoDB/node3 --replSet rs0
  9. python 41115 28977 0 23:08 pts/6 00:00:00 grep --color=auto mongod
  10. python@ubuntu:~$ sudo kill -9 37131

再来查看集群状态

  1. mongo --port 27021 --host 192.168.17.129
  2. rs0:SECONDARY> rs.status()
  3. {
  4. "set" : "rs0",
  5. "date" : ISODate("2016-09-14T15:17:34.915Z"),
  6. "myState" : 2,
  7. "term" : NumberLong(2),
  8. "syncingTo" : "192.168.17.129:27022",
  9. "heartbeatIntervalMillis" : NumberLong(2000),
  10. "members" : [
  11. {
  12. "_id" : 0,
  13. "name" : "192.168.17.129:27020",
  14. "health" : 0,
  15. "state" : 8,
  16. "stateStr" : "(not reachable/healthy)",
  17. "uptime" : 0,
  18. "optime" : {
  19. "ts" : Timestamp(0, 0),
  20. "t" : NumberLong(-1)
  21. },
  22. "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
  23. "lastHeartbeat" : ISODate("2016-09-14T15:17:33.613Z"),
  24. "lastHeartbeatRecv" : ISODate("2016-09-14T15:16:53.392Z"),
  25. "pingMs" : NumberLong(0),
  26. "lastHeartbeatMessage" : "Connection refused",
  27. "configVersion" : -1
  28. },
  29. {
  30. "_id" : 1,
  31. "name" : "192.168.17.129:27021",
  32. "health" : 1,
  33. "state" : 2,
  34. "stateStr" : "SECONDARY",
  35. "uptime" : 2778,
  36. "optime" : {
  37. "ts" : Timestamp(1473866226, 1),
  38. "t" : NumberLong(2)
  39. },
  40. "optimeDate" : ISODate("2016-09-14T15:17:06Z"),
  41. "syncingTo" : "192.168.17.129:27022",
  42. "configVersion" : 3,
  43. "self" : true
  44. },
  45. {
  46. "_id" : 2,
  47. "name" : "192.168.17.129:27022",
  48. "health" : 1,
  49. "state" : 1,
  50. "stateStr" : "PRIMARY",
  51. "uptime" : 2613,
  52. "optime" : {
  53. "ts" : Timestamp(1473866226, 1),
  54. "t" : NumberLong(2)
  55. },
  56. "optimeDate" : ISODate("2016-09-14T15:17:06Z"),
  57. "lastHeartbeat" : ISODate("2016-09-14T15:17:34.603Z"),
  58. "lastHeartbeatRecv" : ISODate("2016-09-14T15:17:33.643Z"),
  59. "pingMs" : NumberLong(0),
  60. "electionTime" : Timestamp(1473866225, 1),
  61. "electionDate" : ISODate("2016-09-14T15:17:05Z"),
  62. "configVersion" : 3
  63. }
  64. ],
  65. "ok" : 1
  66. }