MongoDB复制集

在生产环境中一般不会使用单机的MongoDB

  • 单机节点一旦宕机,业务将直接不可用。
  • 单机节点如果硬盘存坏,数据则永久丢失。

所以我们一般会搭建分片集群或者 复制集来确保服务高可用。

1. 复制集架构

复制集是由 一组MongoDB实例组成的,包含了一个Primary节点和多个Secondary节点。MongoDB 客户端的数据会写入到Primary中,Secondary会从Primary的opLog文件同步数据,而Secondary提供数据冗余的作用。

复制集可以理解为主从架构,拥有Primary节点(提供读写)和Secondary节点(冗余备份),当Primary宕机后 Secondary会马上进行选举,选举出新的Primary来对外提供读写服务。

注意:mongoDB集群最多支持50个节点,参与选举的节点只能有7个

复制集架构优点:

  • 高可用:当Primary节点宕机后,Secondary节点会进行选举,选举出新的Primary对外提供服务,确保服务的可用性
  • 冗余备份:所有的数据在Secondary都会提供备份
  • 数据分发:将一个区域的数据快速复制到另外一个区域
  • 读写分离:默认情况下,读写服务均由 Primary 节点提供
    • 必要情况下可以进行读写分离,Primary节点提供写入服务,Secondary提供读取服务
  • 异地容灾:同城跨机房备份,或者两地三中心,当数据中心故障了进行快速切换

关于复制集的一些注意事项

  • 只有一个单机节点也要搭建复制集

    • 为了便于以后的扩展,即便是单机节点也要以单节点启动复制集,复制集里只存在一个Primary节点,以后扩展的话只需要加入其他的节点到复制集就可以。
  • 为了避免出现不可预知的问题,集群中每个节点的版本号必须一致

  • 增加节点无法提高写性能

    • 因为写操作只能通过 主节点,从节点不能提供写操作。在怎么增加节点,也只能增加从节点。如果想提高写入的性能,只能采用分片架构,进行写热点分散。

2. 三节点复制集模式

三节点是最常见的复制集架构,三节点也存在两种模式:1、PSS 2、PSA。

PSS模式(官方推荐模式)

PSS模式由一个主节点和两个备用节点组成,即 Primary+Secondary+Secondary

当Primary节点宕机后,Secondary节点会进行选举,选举出新的Primary对外提供服务。

PSA模式(基本没人用)

PSA模式由一个主节点、一个备节点和一个仲裁者节点组成,即Primary+Secondary+Arbiter

Arbiter节点(仲裁节点)不存储数据,也不提供读写服务,仅影响选举,目的是为了在Secondary中选举出新的节点。

3. 复制集群搭建

image-20240115192536341

环境准备

  • 需要3台Linux服务器,并且配置好MongoDB环境变量,且3台节点 MongoDB版本必须保持一致
  • 确保每台节点至少有10G以上的空间

1)配置别名解析

vim /etc/hosts 在3个节点上都修改hosts,这样就可以通过别名直接访问对应的IP节点

1
2
3
4
5
6
7
8
vim /etc/hosts

192.168.31.18 S1
192.168.31.148 S2
192.168.31.142 S3

# 刷新hosts
source /etc/hosts

2)初始化MongoDB服务端

启动mongoDB有两种方式:

  1. 命令行的形式直接启动
  2. 通过配置文件启动,这里我们用后者

初始化mongoDB服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 下载MongoDB并解压
[root@S1 ~]# wget https://files.javaxing.com/mongoDB/mongodb-linux-x86_64-rhel70-4.4.26.tgz && tar -zxvf mongodb-linux-x86_64-rhel70-4.4.26.tgz

# 创建数据库和日志目录
[root@S1 ~]# mkdir -p /data/db /data/log

# 配置MongoDB环境变量
[root@S1 ~]# vim /etc/profile

export MONGODB_HOME=/root/mongodb-linux-x86_64-rhel70-4.4.26
PATH=$PATH:$MONGODB_HOME/bin

# 刷新环境变量
[root@S1 ~]# source /etc/profile

修改配置文件

在3个节点上都新增配置文件 vim /data/db/mongod.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
systemLog:
destination: file # 日志保存成文件
path: /data/log/mongod.log # log path 日志保存路径
logAppend: true # 以文件末尾追加的形式写入
storage:
dbPath: /data/db # data directory
net:
bindIp: 0.0.0.0 # 监听IP
port: 27017 # port
replication:
replSetName: rs0
processManagement:
fork: true

该配置等同于命令行

mongod –bind_ip 0.0.0.0 –replSet rs0 –dbpath /data/db –logpath /data/log/mongod.log –port 27010 –fork wiredTigerCacheSizeGB 1

replication 等同于 –replSet

启动MongoDB服务

1
2
3
4
# 在3个节点上都启动mongoDB
[root@S1 /]# mongod -f /data/db/mongod.conf
[root@S2 /]# mongod -f /data/db/mongod.conf
[root@S3 /]# mongod -f /data/db/mongod.conf

3)配置复制集

初始化复制集

只需要在一个节点上 进行初始化就行,members 其他的节点会自动参与选举并完成初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 进入mongod shell,注意:这里实验我们没有开启auth 认证模式,可以根据实际情况开启
[root@S1 ~]# mongo --port=27010

# 初始化分片集群节点
> rs.initiate({
_id: "rs0",
"members" : [
{
"_id": 0,
"host" : "S1:27010"
},
{
"_id": 1,
"host" : "S2:27010"
},
{
"_id": 2,
"host" : "S3:27010"
}
]
})

# 初始化返回
{ "ok" : 1 }

初始化的_id 必须是 启动时指定的复制集名称

后续如果需要增加节点的话,可以通过命令:rs.add(“xxxx:27010”) 来动态添加节点

查看复制集状态

1
rs0:SECONDARY> rs.status()

查看各成员状态,健康与否,全量同步信息,心跳信息,增量同步,选举信息等。

image-20240115165813140

  • members:复制集成员状态和信息
    • health 健康状态,1 健康 0 不健康,节点之间会通过心跳进行检测
    • state/stateStr 节点状态,PRIMARY表示主节点,而SECONDARY则表示备节点,当节点故障后会显示其他状态
    • uptime 成员启动时间
    • optime/optimeDate:成员最后一条同步oplog的时间。
    • optimeDurable/optimeDurableDate:成员最后一条同步oplog的时间。
    • pingMs:成员与当前节点的ping时延。
    • syncingTo:成员的同步来源。

查看Master节点

1
rs0:SECONDARY> db.isMaster()

image-20240115165939021

4)验证测试

从Master节点写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
rs0:PRIMARY> db.employee.insert([{name:"xing"},{name:"monkey"}])

# 插入结果
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 2,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})

从Slave节点读取数据

slave节点默认是没有读取数据权限的,如果直接访问的话会报错:

image-20240115171015439
1
2
3
4
5
6
7
# 指定可以从slave节点读取数据
rs0:SECONDARY> rs.secondaryOk()

# 查询 employee 数据
rs0:SECONDARY> db.employee.find()
{ "_id" : ObjectId("65a4f616bdcc3f5d89ade0ce"), "name" : "xing" }
{ "_id" : ObjectId("65a4f616bdcc3f5d89ade0cf"), "name" : "monkey" }

查看同步进度oplog信息

1
2
3
4
5
6
7
8
rs0:SECONDARY> rs.printSecondaryReplicationInfo()

source: S1:27010
syncedTo: Mon Jan 15 2024 17:01:50 GMT+0800 (CST)
0 secs (0 hrs) behind the primary
source: S3:27010
syncedTo: Mon Jan 15 2024 17:01:50 GMT+0800 (CST)
0 secs (0 hrs) behind the primary

返回oplog 大小、保留时长、 起始时间等信息

1
2
3
4
5
6
7
rs0:SECONDARY> rs.printReplicationInfo()

configured oplog size: 1253.8376951217651MB
log length start to end: 551secs (0.15hrs)
oplog first event time: Mon Jan 15 2024 16:53:05 GMT+0800 (CST)
oplog last event time: Mon Jan 15 2024 17:02:16 GMT+0800 (CST)
now: Mon Jan 15 2024 17:02:26 GMT+0800 (CST)

5)开启安全认证

默认情况下mongoDB是不进行认证的,但是这样安全隐患很高,所以我们要开启认证模式。

需要开启认证模式的话,有2个主要步骤:

  1. 创建用户并授予权限
  2. 创建keyFile文件
    • 在集群中想开启安全认证的话,除了要创建用户外,还要创建keyFile文件(集群节点之前通信的秘钥)

在主节点上创建用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 进入admin数据库
rs0:PRIMARY> use admin
# 创建用户
rs0:PRIMARY> db.createUser({user:"xing",pwd:"123123",roles:[
{role:"clusterAdmin",db:"admin" },
{role:"clusterMonitor",db:"admin" },
{role:"userAdminAnyDatabase",db:"admin" },
{role:"userAdminAnyDatabase",db:"admin" },
{role:"readWriteAnyDatabase",db:"admin" }]
})

# 创建用户结果
Successfully added user: {
"user" : "xing",
"roles" : [
{
"role" : "clusterAdmin",
"db" : "admin"
},
{
"role" : "clusterMonitor",
"db" : "admin"
},
{
"role" : "userAdminAnyDatabase",
"db" : "admin"
},
{
"role" : "userAdminAnyDatabase",
"db" : "admin"
},
{
"role" : "readWriteAnyDatabase",
"db" : "admin"
}
]
}

关闭服务

关闭服务的话,不可以直接kill,而是应该通过命令关闭服务端

1
2
[root@S2 bin]# mongod -f /data/db/mongod.conf --shutdown
killing process with pid: 29784

创建keyFile文件

keyFile用于集群节点之间通信数据加密,保证集群节点安全。如果开启了auth模式的话,集群节点必须设置keyFile,否则无法启动服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 通过openssl创建秘钥
[root@S1 /]# openssl rand -base64 756 > /data/mongo.key

# 查看秘钥
[root@S1 /]# cat /data/mongo.key
JsK6C7vNhJEOnJD0OuoPuO0Q12fCcnf/zByvcbqPr0bihbbkE8ef4Y143ckcC3Mj
3u9SEY5sISto0aid85TbBt4rfdmfvsy+31Ge32lSj62uRWRk9BMs/5epDQiCOUT5
LqB8r9tcs5Nn2U0DjVXnnLmDA2cux4R/+n5PWtEYESQtj9UvwdOR9JVeUjFQzKpU
dWjSAA51EnONOcwXXIb8bk+RSDMh91rCL1Tvr8vZ7b5mTr1MVqP5NRJsxFr/KFq4
1a12QMaP/nowiDA9YMPt1wVXYCm/T6cPKQf228d0CEoTEI5F+atK0Ocw+kdtybaf
5BVMeWRKk3GXggFdki1+C08mpwUDzDk8UOfou+7/LK0p0ogqvYPx8lmgWsUkU5UG
8wIoJrNkkrE35tjyiNFcPwG35Y10tkHMcCWMY6niVUatfac+DpNUJCQRpv6N/xjY
MVjsxSQwwbxOVIwMj168o6gBwCPLlam391aG+GBXIsmEtSyshT4PL5VNPPdo7jxP
aBSmemyGSCmyV9k+UNn5ahL3MDLHRLo+I6xE04mw3leg0/RmX0HjcUhBiT1AamTY
oWBEZ1IhVARz2hZZWzgBKjt5Piw5Oz9rMJtOujZdRHzV44ph+Wd1tV9GDwnY4gWz
7pXUsay6x9TUMan0HrTYFozq/Lh2xoF0gVHv/SGKcDW6nPWLMTllhsrSPgwWRr+w
9EmAesBn0cA71IsoGZDltaLObWfaOarig4OwotIPLXwDW2BwXQVCfjSblC1HgWgH
dw85XW/QN94ogr4I585geoBwhiyKAZnQ2Un/yR2rW5XiphmBocrHN8tPqFAlcy96
dhtew5pnMlXVrL+JNSdH4VkKqsVaYWAb818+Et/u/8nSeoQiIxsI9jBLe2w+nSja
2u+TShbyt6mn9r1nOQALq7Ke44c1F5sxDtjk5wBf4HQAmZP2TG0T4b/E2GrSxdRW
NDXEexOZXQ6024bm3+veor04fBQ0IrpwdZR0Jz7b9jOxDRNC

注意:创建keyFile之前必须要停掉集群节点内所有的mongoDB服务,否则可能会出现 服务启动不了的情况。

同步keyFile文件

将master节点生成好的keyFile文件同步到其他集群节点上,并设置权限为600,文件权限必须设置否则会无法运行

1
2
3
4
5
6
7
# 为keyFile 设置600 可运行的权限
[root@S1 data]# chmod 600 /data/mongo.key


# 通过scp 完成文件同步
[root@S1 /]# scp /data/mongo.key root@S2:/data/mongo.key
[root@S1 /]# scp /data/mongo.key root@S3:/data/mongo.key

以keyFile文件启动服务

1
[root@S1 /]# mongod -f /data/db/mongod.conf --keyFile /data/mongo.key

登录mongo shell

1
2
3
4
5
6
7
8
[root@S1 data]# mongo --port=27010 -uxing -p123123 --authenticationDatabase=admin

rs0:SECONDARY> use user
switched to db user

# 查询employee集合
rs0:SECONDARY> db.employee.find()
{ "_id" : ObjectId("65a4f616bdcc3f5d89ade0ce"), "name" : "xing" }

6)MongoDB客户端连接方式

连接Primary节点

MongoDB 客户端直接连接Primary节点,但是当Primary节点宕机后,就会无法正常访问

高可用Uri连接方式(推荐)

通过高可用 Uri 的方式连接 MongoDB,当 Primary 故障切换后,MongoDB Driver 可自动感知并把流量路由到新的 Primary 节点。

SpringBoot application.yaml

1
2
3
4
spring:
data:
mongodb:
uri: mongodb://xing:123123@192.168.65.174:28017,192.168.65.174:28018,192.168.65.174:28019/test?authSource=admin&replicaSet=rs0

在uri里面将所有的主节点和备用节点都写上,当主节点故障后,Mongo DB客户端会将流量 转发到 到新的主节点

4. 复制集成员角色

rs.conf

  • Priority 选举优先级,Priority = 0 不可以选举成主节点。Priority越高,选举成主节点概率越大。
  • Vote 选举权限,Vote = 0 表示该节点不可以参与选举(Priority 也必须为0),Vote = 1 表示可以参与选举。
    • 一个复制集中最多有50个成员,只有7个成员可以参与选举,其他的成员vote必须设置成0。

1)成员角色

  • Primary 主节点,负责所有写请求。一个复制集只存在一个主节点,当主节点挂掉后,其他Secondary会选举出新的Primary节点。
  • Secondary 从节点,拥有和主节点同样的数据集,当主节点宕机后,从节点会参与选举并选举出新的主节点
    • Hidden = false (默认):正常是 不隐藏的只读节点,是否参与选举,优先级具体看Priority、Vote
    • Hidden = true 隐藏节点,不会接收客户端的请求
      • 可以参与选举,但是 Priority 必须为 0,不可以选举为主节点
      • 一般用于 冗余备份,数据处理任务
    • Delayed 延迟节点,必须同时满足 隐藏节点+不可以选举成主节点(Priority = 0)
      • 延迟同步:延迟一定时间(取决于 slaveDelay)从上游同步数据
        • image-20240118160030628
        • 我们将C节点设置延迟同步一小时,当主节点A 误删数据后,需要一个小时后 才会同步到C节点上。那么就可以在这一小时内进行数据回滚。
  • Arbiter 仲裁节点,只参与投票(给Secondary投票),不处理也不同步任何数据。

2)配置隐藏节点

隐藏节点更多情况下是为了协助配置 延迟节点的,配置隐藏节点后 该节点不会接收客户端请求。

1
2
3
4
5
6
7
8
9
10
11
12
# 将配置文件保存到config变量
rs0:PRIMARY> config = rs.conf()

# 优先级设置成0,不可以选举成主节点
rs0:PRIMARY> config.members[1].priority = 0
0
# 开启隐藏节点
rs0:PRIMARY> config.members[1].hidden = true
true

# 重新加载配置文件
rs0:PRIMARY> rs.reconfig(config)

没有修改配置之前

image-20240118180610238

修改配置之后

image-20240118180715116

3)配置延时节点

配置延时节点 如 延时60秒,同步时间将会比复制集中其他的Secondary节点 延时60秒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 将配置文件保存到config变量
rs0:PRIMARY> config = rs.conf()

# 优先级设置成0,不可以选举成主节点
rs0:PRIMARY> config.members[1].priority = 0
0
# 开启隐藏节点
rs0:PRIMARY> config.members[1].hidden = true
true

# 设置延时时间 60
rs0:PRIMARY> config.members[1].slaveDelay = 60

# 重新加载配置文件
rs0:PRIMARY> rs.reconfig(config)

配置延时节点的话,必须先设置成 隐藏节点 + 延时时间 。

查询所有备节点成员的同步延迟情况

1
rs0:SECONDARY> rs.printSecondaryReplicationInfo()
image-20240119145702171

4)复制集节点操作

添加复制集节点

1
2
3
# 1.关闭节点实例
# 2.连接主节点,执行下面命令
rs.add("ip:port")

更改复制集节点

1
2
3
config = rs.conf()
config.members[0].host = "ip:port"
rs.reconfig(config)

移除复制集节点

1
2
3
4
5
# 1.关闭节点实例
# 2.连接主节点,执行下面命令
config = rs.conf()
config.members.splice(2,1) #从2开始移除1个元素
rs.reconfig(config)

5. 复制集高级特性

1)复制集选举

MongoDB的复制集选举使用Raft算法(https://raft.github.io/)来实现,在Raft基础之上增加了一些扩展:

  • 支持chainingAllowed链式复制,即备节点不只是从主节点上同步数据,还可以选择一个离自己最近(心跳延时最小)的节点来复制数据。
  • 增加了预投票阶段,即preVote,这主要是用来避免网络分区时产生Term(任期)值激增的问题
  • 支持投票优先级,如果备节点发现自己的优先级比主节点高,则会主动发起投票并尝试成为新的主节点。

一个复制集最多可以有50 个成员,但只有 7 个投票成员。如果投票成员过多,反而会造成性能下降。

投票成员数 大多数 容忍失效数
1 1 0
2 2 0
3 2 1
4 3 1
5 3 2
6 4 2
7 4 3

如果有3个投票成员,损失1个投票节点,也可以完成选举。

如果有5个投票成员,损失2个投票节点,也可以完成选举。以此类推。

注意:当复制集内一半以上的节点宕机后,整个集群都无法选举出新的主节点,没有主节点就无法对外提供写入服务,但是从节点依然可以提供读服务。

为了避免出现选举平票的情况,搭建的集群节点数量最好是 奇数节点,如3、5、7等。

2)Raft算法选举规则

  1. 每台节点 每次选举的时候 都会有一个随机计时器,第一次选举时 先往自身投一票,然后谁的计时器先过期,在往自身再投一票,此时一共有2票,然后向集群中所有节点发送拉票请求

    1. image-20240119153038917
  2. 其他节点收到拉票请求后,如果自身计时器还没过期的话,就会重置 随机计时器,并成为从节点,然后 返回ACK给拉票方

  3. S5第一次选举收到所有节点的投票回复后,自己就会成为PRIMARY节点。

  4. 后续Primary节点会持续的给集群中所有从节点发送心跳检测,从节点收到心跳检测后 会重置随机计时器并回复Primary节点。

    1. image-20240119160420575
    2. 如果有哪台从节点没有 返回ACK的话,Primary节点就会认定 该节点挂了。
  5. 如果S5 主节点挂掉了,那其他的从节点 随机计时器到期后 会重复第一个动作,给自己再投一票(那就有2票),然后给集群中其他从节点发送拉票请求,重新选举主节点。

    1. image-20240119161116120

      主节点宕机后,从节点的计时器到期后会尝试将自身选举为主节点

选举时特殊情况

image-20240119160523039
  1. S5和S1的计时器很接近,但是S5的计时器先到期随后就是S1到期。
    1. S5和S1都会向其他节点发送拉票请求。至于最后谁成为了主节点,就看谁先收到半数以上的节点 ACK确认。
  2. 谁先成为主节点(如 S5)后,会给集群中所有节点发送心跳检测告知 他自己成为了主节点,而之前和它竞争的S1节点 就会主动变成 从节点。

平票问题

假设S5和S1 计时器时间一致,就会同时向集群中其他从节点发送拉票请求,其他节点如果回复的时间也一致的话,就很可能出现 S5和S1收到的票数一样,就会出现 平票的情况。

那么S5和S1就会Term(任期)加1,用于记录选举的次数,然后继续开启新的一轮投票,直到选举出新的主节点。

网络分区导致的问题

image-20240119165054175

1、第一次选举时,S1成了主节点,S2、S3成了从节点,S1正常给其他从节点发送心跳检测。

image-20240119165358123

2、如果突然网络出现了波动,出现了网络分区。主节点的心跳检测无法发送给从节点了。那么从节点计时器到期后就会开始新的一轮选举。

而主节点由于一直选举失败,开启新的选举时,Term 会自增。当网络分区消失后,S2成了新的主节点,而S1也是主节点,但是S1的Term 比较高,所以 S1 依然会成为 主节点。这里就会存在一个问题,因为S1的数据是旧的,就会导致 数据不一致。

MongoDB为了解决这个问题,就引入了一个新的概念:预投票阶段 preVote,如果主节点发送投票失败的话,就不会增加Term 任期。

3)自动故障转移

  • 复制集组建完成后,各成员 根据 心跳间隔时间(默认2s) 向其他成员发送心跳检测。
    • 如果收到正确响应则持续以2s的频率保持心跳
    • 如果心跳失败,则立即重试 直到成功或超时
    • 如果超过 心跳超时时间(默认10s) 没有收到响应就认定对方节点已经宕机

如果超过10s没有收到 主节点正确响应,则认为主节点已经宕机,触发electionTimeout任务进行主节点选举。

触发electionTimeout任务必备条件:

  1. 当前节点 必须是备用节点且具备选举权限
  2. 超过心跳超时时间(默认10s)后,依然没有与主节点心跳检测成功

rs.conf()

image-20240122113532136

可重试写入降低主从切换时间

当主节点宕机后,从节点开始进行选举,当选举出主节点后 进行主备切换。在切换的过程中会 主节点无法对外提供写入服务。

为了避免等待过程太长,我们可以开启retryWrite来降低等待时间:

开启retryWrite来降低影响

1
2
3
4
# MongoDB Drivers 启用可重试写入
mongodb://localhost/?retryWrites=true
# mongo shell
mongo --retryWrites

如何不丢数据重启复制集

如果正常重启复制集的话,会丢失部分数据。如果想不丢失数据的话,我们应该按以下流程 进行重启复制集:

  1. 逐个重启复制集所有的Secondary节点
  2. 对Primary发送rs.stepDown()命令,等待primary降级为Secondary,降级成功后进行重启
  3. Secondary节点会重新选举Primary节点

4)复制集数据同步机制

什么是oplog

  • oplog是mongodb local库下的一个 固定集合,用来保存 写操作产生的增量日志,从节点根据oplog完成 数据同步。

  • 它是固定集合,当写入的数据超出了集合最大限制,会自动删除之前的旧数据。

在复制集架构中,主节点与备节点之间是通过oplog来同步数据的。

同步流程:

  1. 客户端向主节点写入数据,主节点会 插入数据、插入索引(如果有的话)、插入oplog日志 到local下的local.oplog.rs集合中
  2. 从节点会与主节点建立长连接,然后不断去主节点中读取oplog数据 并存到自己本地进行重放,完成数据同步

查看oplog日志

local数据库里面包含的表:

image-20240122150342698
1
2
3
4
5
rs0:SECONDARY> use local
rs0:SECONDARY> db.oplog.rs.find().sort({$natural:-1}).pretty()

# 指定数据库和集合
rs0:SECONDARY> db.oplog.rs.find({ ns:"user.user" }).sort({$natural:-1}).pretty()

image-20240122151915273

ts: 日志写入时间,当前timestamp + 计数器,计数器每秒都被重置。

v:oplog版本信息

op:操作类型:

​ i:插⼊操作

​ u:更新操作

​ d:删除操作

​ c:执⾏命令(如createDatabase,dropDatabase)

​ n:空操作,特殊⽤途

ns:操作针对的集合

o:操作内容

o2:操作查询条件,仅update操作包含该字段

每个从节点都会维护一个offset(游标),就是oplog日志的ts时间戳字段。从节点拉取数据会保存时间戳,下次再次拉取时在该时间戳之后拉取新的数据。

oplog集合的大小

oplog集合的大小可以通过参数replication.oplogSizeMB设置,默认值:oplogSizeMB = min(磁盘可用空间*5%,50GB)。

MongoDB在4.0版本之后可以实现不重启服务器 动态修改oplogSize:

1
2
3
4
5
# 将复制集成员的oplog大小修改为60g  指定大小必须大于990M
db.adminCommand({replSetResizeOplog: 1, size: 60000})
# 查看oplog大小
use local
db.oplog.rs.stats().maxSize

幂等性

为了避免oplog 多次重放,导致 数据不一致的情况发生,对于oplog来说必须保证幂等性。不论多少次重放,数据始终保持不变。

oplog如何保证幂等性

不论是新增还是修改数据,主节点都只会把最终数据写入oplog。这样不论重放多少次,数据始终不变。

举个例子:age = 23 , 客户端发送了一条自增命令 {$inc: {age: 1}},primary会自动转换成{$set: {age: 24 } 。 把修改之后的结果 set到oplog日志。

幂等性带来的代价

简单元素的操作,$inc 转成$set并没有什么影响。但是如果是数组元祖的话且数据量大的话,就会带来较大的开销。

实验如下:

插入数组数据

1
2
3
4
5
6
7
rs0:PRIMARY> db.user.insertOne({name:"test-02",tags:[1,2,3]})

# 插入结果
{
"acknowledged" : true,
"insertedId" : ObjectId("65ae2224ff7763cced2441b0")
}

往尾部push2个元素

1
2
3
rs0:PRIMARY> db.user.update({ "_id" : ObjectId("65ae2224ff7763cced2441b0")}, {$push: {tags: { $each: [4, 5] }}})
# 更新结果
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
image-20240122161154730

往数组头部push元素(出问题的点)

1
2
3
4
rs0:PRIMARY> db.user.update({ "_id" : ObjectId("65ae2224ff7763cced2441b0")}, {$push: {tags: { $each: [-1,0],$position: 0 }}})

# 更新结果
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
image-20240122161507271

从数组头部插入数据的话,为了保证幂等性,oplog日志 会整个数组 以$set的形式 写入到oplog,如果数据量大的话,这样效率非常低,$sort,$slice,$pull, $addToSet等更新操作符也是类似。

大数组更新导致的oplog覆写

当对大数组进行更新时,哪怕只是一个微小的更新,也可能会将整个数组写入到oplog中。导致oplog日志没同步完就又被写满了,旧的数据会被删除掉(覆盖写),导致部分从节点更新不上,就会出现同步中断的情况。

案例

在生产环境中,用户文档有一个数组字段,数组里面有1000个元素 64KB,这个数组的元素按时间反序存储,每次插入数据都会放到数组的最前面。

Primary每次往数组里面插入新元素时,oplog会将整个数组以$set的形式 写入到oplog。Secondary会拉取oplog日志进行重放达到同步数据。当有大量的修改或插入操作时,oplog很快就会被占满,部分Secondary节点还没来得及同步,Primary的oplog日志就就被覆写了,导致部分Secondary节点数据同步不一致。

如何避免oplog覆写问题

  1. 数组的元素不要太多
  2. 尽量避免对数组的更新操作
  3. 如果一定要更新,只能在尾部插入元素

复制延迟

由于oplog集合时固定大小的,如果Secondary节点因为 延迟或其他原因 同步不够快而oplogoplog日志满了被Primary节点删除掉的话,就会出现 复制延迟的情况,导致最终结果 主从数据不一致。

为了避免复制延迟的情况发生,我们可以根据实际情况采取以下的手段:

  1. 增加oplog容量
  2. 优化主从节点之间的线路,提高访问速度
  3. 避免字段使用大数据数组从而导致oplog覆写

数据回滚

由于复制延迟是不可避免的,这意味着主备节点之间的数据无法保持绝对的同步。当复制集中的主节点宕机时,备节点会重新选举成为新的主节点。

那么,当旧的主节点重新加入时,必须回滚掉之前的一些“脏日志数据”,以保证数据集与新的主节点一致。主备复制集合的差距越大,发生大量数据回滚的风险就越高,这个回滚的动作MongoDB会自动完成。

同步源选择

如果从节点与主节点之间延迟较大或网络不稳定,可以考虑 通过其他的备用节点进行同步数据。

默认情况下,mongoDB不一定会优先同步主节点数据,而会选择 最近的一个节点(ping 延迟最低) 进行同步数据。

如果我们想临时指定一个节点进行同步的话,可以通过以下命令进行指定:

1
2
3
4
5
6
7
cfg = rs.config()
# 关闭自动选择节点
cfg.settings.chainingAllowed = false
rs.reconfig(cfg)

# 使用replSetSyncFrom命令临时更改当前节点的同步源
db.adminCommand( { replSetSyncFrom: "hostname:port" })