一、MongoDB复制集
Mongodb
复制集由一组Mongod
实例(进程)组成,包含一个Primary
节点和多个Secondary
节点,Mongodb Driver
(客户端)的所有数据都写入Primary
,Secondary
从Primary
同步写入的数据,以保持复制集内所有成员存储相同的数据集,提供数据的高可用。
二、复制集的作用
MongoDB的复制集主要是为了实现服务的高可用。
他的实现依赖于两个方面的功能:
- 数据写入是将数据迅速复制到另一个独立节点上
- 在接受写入节点发生故障时自动选举出一个新的替代节点
在实现高可用的同时,复制集实现了几个附加功能:
- 数据分发:将数据从一个区域复制到另一个区域,减少另一个区域的读延迟
- 读写分离:不同类型的压力分别在不同的节点上执行
- 异地容灾:在数据中心故障时候快速切换到异地
三、常见复制集结构
一个典型的复制集由三个以上具有投票权的节点组成:
- 一个主节点:接受写入操作和选举时投票(读写都可以)
- 两个(或多个)从节点:复制主节点上的新数据和选举时投票(只读)
- 不推荐使用
Arbiter
(投票节点:空节点不接受读写只作用于投票)
四、复制集搭建
4.1. 先创建文件夹和日志文件
mkdir data/node{1,2,3} # 创建 node1 node2 node3 文件夹 存放数据
mkdir config/mongodb{1,2,3}.conf # 创建三个配置文件
touch logs/node{1,2,3}.log # 创建node1.log node2.log node3.log 日志文件
4.2. 配置文件修改
这里的dbpath
、logpath
和port
修改为指定的文件夹、文件和端口。
# 数据库的数据存放目录 注意需要写绝对路径
dbpath=/usr/share/mongodb/mongodb_replica_set/data/node1
# 日志目录 注意需要写绝对路径
logpath=/usr/share/mongodb/mongodb_replica_set/logs/node1.log
# 日志追加方式
logappend=true
# 端口
port=27017
# 是否认证
# auth=true
# 已守护进程在后台运行
fork=true
# 远程链接
bind_ip=0.0.0.0
# 设置复制集名字 同一个复制集名字相同
replSet=long
4.3. 启动三个实例
# !/bin/bash
echo "启动节点一:"
./bin/mongod -f ./config/mongodb1.conf
echo "启动节点二:"
./bin/mongod -f ./config/mongodb2.conf
echo "启动节点三:"
./bin/mongod -f ./config/mongodb3.conf
4.4. 配置复制集
4.4.1. 方法一:
选择一个节点进入MongoDB shell
:
./bin/mongo localhost:27017
进入之后进行初始化
> rs.initiate()
{
"info2" : "no configuration specified. Using a default configuration for the set",
"me" : "ubuntu:27017",
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1616932776, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1616932776, 1)
}
long:SECONDARY>
这样就是进入了一个复制集配置状态:long
就是我们上面的复制集的名字。接着加入其他两个从节点节点:
long:PRIMARY> rs.add("ubuntu:27018")
{
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1616933662, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1616933662, 1)
}
long:PRIMARY> rs.add("ubuntu:27019")
{
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1616933671, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1616933671, 1)
}
long:PRIMARY>
接着查看复制集状态:
long:PRIMARY> rs.status()
{
"set" : "long",
...,
"members" : [
{
"_id" : 0,
"name" : "ubuntu:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
...
},
{
"_id" : 1,
"name" : "ubuntu:27018",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
...
},
{
"_id" : 2,
"name" : "ubuntu:27019",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
...
}
],
...
}
long:PRIMARY>
注意:复制集是可以只有一个节点的。
验证一下:主节点写,从节点读。
这里我们需要注意,直接在从节点上读是会报错的,原因在于mongo
副本集实现了读写分离,默认的副本无法读写。我们可以使用rs.slaveOk()
允许在从节点上读取数据。(rs.slaveOk
是过期的后续版本可能会删除,我们可以使用rs.secondaryOk()
)
long:SECONDARY> db.test.find()
Error: error: {
"topologyVersion" : {
"processId" : ObjectId("60606829f25506cbae73ca52"),
"counter" : NumberLong(4)
},
"operationTime" : Timestamp(1616934386, 1),
"ok" : 0,
"errmsg" : "not master and slaveOk=false",
"code" : 13435,
"codeName" : "NotPrimaryNoSecondaryOk",
"$clusterTime" : {
"clusterTime" : Timestamp(1616934386, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
long:SECONDARY> rs.slaveOk()
WARNING: slaveOk() is deprecated and may be removed in the next major release. Please use secondaryOk() instead.
long:SECONDARY> db.test.find()
long:SECONDARY>
接着我们进行写读操作:
long:PRIMARY> db.test.insert({"name": "zhangsan"})
WriteResult({ "nInserted" : 1 })
long:SECONDARY> db.test.find()
{ "_id" : ObjectId("606076f3b0347ba762bc1e81"), "name" : "zhangsan" }
4.4.2. 方法二
区别与方法一我们可以直接一次就配置好复制集的主从节点
> rs.initiate({
... _id: "long",
... members: [
... {
... _id: 0,
... host: "localhost:27017"
... },
... {
... _id: 1,
... host: "localhost:27018"
... },
... {
... _id: 2,
... host: "localhost:27019"
... }
... ]
... })
{
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1616935792, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1616935792, 1)
}
long:SECONDARY>
接着我们查看复制集状态:
long:PRIMARY> rs.status()
{
"set" : "long",
...,
"members" : [
{
"_id" : 0,
"name" : "localhost:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
...
},
{
"_id" : 1,
"name" : "localhost:27018",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
...
},
{
"_id" : 2,
"name" : "localhost:27019",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
...
}
],
...
}
long:PRIMARY>
验证啥的和第一种方法一样!
五、复制集原理与故障恢复
5.1. 节点之间数据复制
当一个修改操作,无论是插入、更新或者删除,到达主节点时,它对数据的操作将被记录下来(经过一些转换),这些记录称为oplog
从节点通过在主节点上打开一个tailable
游标不断获取新进入主节点的oplog
,并在自己的数据上回放,以此保持跟主节点的数据一致。
5.2. 故障恢复过程
具有投票权的节点之间两两互相发送心跳,当五次心跳未收到时判断节点失联;如果失联的是主节点,从节点就会发起选举,选出新的主节点;如果失联的是从节点则不会产生新的选举。
选举基于RAFT一致性算法实现,选举成功的必要条件是大多数投票节点存活。
复制集中最多可以有50个节点,但是具有投票权的节点最多有7个。
影响选举的因素:
- 整个集群必须有大多数节点存活
- 被选举为主节点的节点必须能够被多数节点建立链接,具有较新的
oplog
,具有较高的优先级(手动设置)
5.3. 复制集节点常用配置
在配置复制集的时候可以添加这些参数来控制节点的状态:
priority: 副本集中通过设置priority
的值来决定优先权的大小,这个值的范围是0--100
,值越大,优先权越高。默认的值是1。如果值是0,那么不能成为primay
。
hidden: 将一个副节点配置成隐藏节点,首先要将members[n].priority
值设置成0和members[n].hidden
值设置为true
,隐藏节点既不能变成Primary
也不能被客户端访问,但是拥有投票选举的权利。
slaveDelay: 延迟,复制n秒前的数据,保持与主节点的时间差。这样当操作失误的时候可以用来恢复数据。
v: 是否具有投票权
六、复制集注意事项
硬件:
- 因为正常的复制集节点都有可能成为主节点,他们的地位是一样的,因此硬件配置要一致
- 为了保证节点不会宕机,各节点使用的硬件必须具有独立性
软件:
- 复制集各节点软件版本必须一致,以避免出现不可预知的问题
注意:增加节点不能增加系统的写性能,增加读性能(扩展写性能可以通过分片集群)