MongoDB 复制(副本集)

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

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

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

为什么要复制?

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

MongoDB中复制的工作原理

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

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

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

MongoDB复制结构图如下所示:

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

副本集特点

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

设置一个副本集

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

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

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

--replSet 基本语法如下:

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

示例

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

启动副本集名称 rs0

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

2. mongo shell连接副本集

mongo -port 27020 --host 192.168.17.129

3. 初始化initiate副本集

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

rs.initiate()

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

4. 验证初始副本集配置

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

rs.conf()

副本集配置对象如下:

rs0:OTHER> rs.conf()
{
    "_id" : "rs0",
    "version" : 1,
    "protocolVersion" : NumberLong(1),
    "members" : [
        {
            "_id" : 0,
            "host" : "192.168.17.129:27020",
            "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("579b3500299da8059cc5fb99")
    }
}
rs0:PRIMARY>

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

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

rs0:PRIMARY> rs.status()
{
    "set" : "rs0",
    "date" : ISODate("2016-07-29T11:09:58.433Z"),
    "myState" : 1,
    "term" : NumberLong(1),
    "heartbeatIntervalMillis" : NumberLong(2000),
    "members" : [
        {
            "_id" : 0,
            "name" : "192.168.17.129:27020",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 1200,
            "optime" : {
                "ts" : Timestamp(1469789441, 1),
                "t" : NumberLong(1)
            },
            "optimeDate" : ISODate("2016-07-29T10:50:41Z"),
            "electionTime" : Timestamp(1469789440, 2),
            "electionDate" : ISODate("2016-07-29T10:50:40Z"),
            "configVersion" : 1,
            "self" : true
        }
    ],
    "ok" : 1
}
rs0:PRIMARY>

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

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

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

利用`rs.status()`识别副本集主节点primary

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

添加节点 192.168.17.129:27021

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

添加节点 192.168.17.129:27022

rs0:PRIMARY> rs.add('192.168.17.129:27022')
{ "ok" : 1 }
rs0:PRIMARY> rs.status()
{
    "set" : "rs0",
    "date" : ISODate("2016-07-29T11:17:28.721Z"),
    "myState" : 1,
    "term" : NumberLong(1),
    "heartbeatIntervalMillis" : NumberLong(2000),
    "members" : [
        {
            "_id" : 0,
            "name" : "192.168.17.129:27020",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 1650,
            "optime" : {
                "ts" : Timestamp(1469791047, 1),
                "t" : NumberLong(1)
            },
            "optimeDate" : ISODate("2016-07-29T11:17:27Z"),
            "electionTime" : Timestamp(1469789440, 2),
            "electionDate" : ISODate("2016-07-29T10:50:40Z"),
            "configVersion" : 3,
            "self" : true
        },
        {
            "_id" : 1,
            "name" : "192.168.17.129:27021",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 115,
            "optime" : {
                "ts" : Timestamp(1469790932, 1),
                "t" : NumberLong(1)
            },
            "optimeDate" : ISODate("2016-07-29T11:15:32Z"),
            "lastHeartbeat" : ISODate("2016-07-29T11:17:27.171Z"),
            "lastHeartbeatRecv" : ISODate("2016-07-29T11:17:27.159Z"),
            "pingMs" : NumberLong(0),
            "configVersion" : 3
        },
        {
            "_id" : 2,
            "name" : "192.168.17.129:27022",
            "health" : 1,
            "state" : 0,
            "stateStr" : "STARTUP",
            "uptime" : 1,
            "optime" : {
                "ts" : Timestamp(0, 0),
                "t" : NumberLong(-1)
            },
            "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
            "lastHeartbeat" : ISODate("2016-07-29T11:17:27.168Z"),
            "lastHeartbeatRecv" : ISODate("2016-07-29T11:17:28.182Z"),
            "pingMs" : NumberLong(1),
            "configVersion" : -2
        }
    ],
    "ok" : 1
}
rs0:PRIMARY>

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

6. 检查副本集的状态

使用rs.status() 操作:

rs.status()

7. 删除副本

rs.remove("192.168.17.129:27021")

副本集成员

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

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

测试性能

批量入库

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

mongo --port 27021 --host 192.168.17.129
rs0:SECONDARY> show dbs
2016-09-14T23:02:41.228+0800 E QUERY    [thread1] Error: listDatabases failed:{ "ok" : 0, "errmsg" : "not master and slaveOk=false", "code" : 13435 } :
_getErrorWithCode@src/mongo/shell/utils.js:25:13
Mongo.prototype.getDBs@src/mongo/shell/mongo.js:62:1
shellHelper.show@src/mongo/shell/utils.js:760:19
shellHelper@src/mongo/shell/utils.js:650:15
@(shellhelp2):1:1

rs0:SECONDARY>  rs.slaveOk()
rs0:SECONDARY> show dbs
example  0.000GB
local    0.000GB

rs0:SECONDARY> db.mycol.count()
10000

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

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

解决方法:

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

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

Provides a shorthand for the following operation:
db.getMongo().setSlaveOk()
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.

测试

测试一下集群是否可用:

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

python@ubuntu:~$ ps -ef | grep mongod
python     6731   2119  0 9月13 ?       00:01:02 /opt/sublime_text/sublime_text /var/log/mongodb/mongod.log
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
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
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
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
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
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
python    41115  28977  0 23:08 pts/6    00:00:00 grep --color=auto mongod
python@ubuntu:~$ sudo kill -9 37131

再来查看集群状态

mongo --port 27021 --host 192.168.17.129
rs0:SECONDARY> rs.status()
{
    "set" : "rs0",
    "date" : ISODate("2016-09-14T15:17:34.915Z"),
    "myState" : 2,
    "term" : NumberLong(2),
    "syncingTo" : "192.168.17.129:27022",
    "heartbeatIntervalMillis" : NumberLong(2000),
    "members" : [
        {
            "_id" : 0,
            "name" : "192.168.17.129:27020",
            "health" : 0,
            "state" : 8,
            "stateStr" : "(not reachable/healthy)",
            "uptime" : 0,
            "optime" : {
                "ts" : Timestamp(0, 0),
                "t" : NumberLong(-1)
            },
            "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
            "lastHeartbeat" : ISODate("2016-09-14T15:17:33.613Z"),
            "lastHeartbeatRecv" : ISODate("2016-09-14T15:16:53.392Z"),
            "pingMs" : NumberLong(0),
            "lastHeartbeatMessage" : "Connection refused",
            "configVersion" : -1
        },
        {
            "_id" : 1,
            "name" : "192.168.17.129:27021",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 2778,
            "optime" : {
                "ts" : Timestamp(1473866226, 1),
                "t" : NumberLong(2)
            },
            "optimeDate" : ISODate("2016-09-14T15:17:06Z"),
            "syncingTo" : "192.168.17.129:27022",
            "configVersion" : 3,
            "self" : true
        },
        {
            "_id" : 2,
            "name" : "192.168.17.129:27022",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 2613,
            "optime" : {
                "ts" : Timestamp(1473866226, 1),
                "t" : NumberLong(2)
            },
            "optimeDate" : ISODate("2016-09-14T15:17:06Z"),
            "lastHeartbeat" : ISODate("2016-09-14T15:17:34.603Z"),
            "lastHeartbeatRecv" : ISODate("2016-09-14T15:17:33.643Z"),
            "pingMs" : NumberLong(0),
            "electionTime" : Timestamp(1473866225, 1),
            "electionDate" : ISODate("2016-09-14T15:17:05Z"),
            "configVersion" : 3
        }
    ],
    "ok" : 1
}