MongoDB7.0--复制集

摘要

  • 本文介绍Linux下MongoDB7.0复制集的安装和使用

  • MongoDB官方文档

  • 本文基于CentOS8(x86_64)

  • Mongodb分片集群搭建参看MongoDB 分片集群搭建,虽然是基于4.4版本的,但是安装方式差别不大,只是配置文件中个别的属性名称发生了变化。

复制集节点类型

  • 一个复制集最多支持50个节点,且做多只能有7个节点参与投票,节点类型分为PrimarySecondaryArbiter

  • Primary:主节点,其接收所有的写请求,然后把修改同步到所有 Secondary 节点。一个复制集只能有一个主节点,当主节点挂掉后,其他节点会重新选举出来一个主节点。

  • Secondary:备节点,与主节点保持同样的数据集。当主节点挂掉时,参与竞选主节点。分为以下三个不同类型:

    • Hidden = false:正常的只读节点,是否可选为主,是否可投票,取决于 PriorityVote 的值;
    • Hidden = true:隐藏节点,对客户端不可见, 可以参与选举,但是 Priority 必须为 0,即不能被提升为主。 由于隐藏节点不会接受业务访问,因此可通过隐藏节点做一些数据备份、离线计算的任务,这并不会影响整个复制集。
    • Delayed :延迟节点,必须同时具备隐藏节点Priority=0的特性,会延迟一定的时间(secondaryDelaySecs 配置决定)从上游复制增量,常用于快速回滚场景。
  • Arbiter:仲裁节点,只用于参与选举投票,本身不承载任何数据,只作为投票角色。比如你部署了2个节点的复制集,1个 Primary,1个Secondary,任意节点宕机,复制集将不能提供服务了(无法选出Primary),这时可以给复制集添加⼀个 Arbiter节点,即使有节点宕机,仍能选出Primary。 Arbiter本身不存储数据,是非常轻量级的服务,当复制集成员为偶数时,最好加入⼀个Arbiter节点,以提升复制集可用性。

搭建基于3台机器的复制集

1
2
3
10.1.2.26
10.1.2.142
10.1.2.41
  • mongod.conf配置文件,需要创建好相关目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
systemLog:
destination: file
path: /mongodb/log/mongodb.log
logAppend: true
storage:
dbPath: /mongodb/data
net:
bindIp: 0.0.0.0
port: 27017
processManagement:
fork: true
replication:
replSetName: rs0
security:
keyFile: /mongodb/mongo.key
  • 创建key文件

1
2
3
openssl rand -base64 756 > /mongodb/mongo.key
# 修改权限
chmod 600 /mongodb/mongo.key
  • 分别启动3个节点

1
mongod -f /mongodb/mongo.conf

初始化

  • 登录任意节点

1
mongosh 10.1.2.26:27017
  • 初始化复制集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 必须切换到admin数据库
test> use admin
switched to db admin

# 初始化
admin> rs.initiate(
{
_id: "rs0",
members: [
{ _id : 0, host : "10.1.2.26" },
{ _id : 1, host : "10.1.2.142" },
{ _id : 2, host : "10.1.2.41" }
]
}
);
  • 查看谁是主节点

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
38
39
40
41
# 查看谁是主节点,可以看到此时主节点是 10.1.2.26
> rs.isMaster()
{
topologyVersion: { # 拓扑版本,包含进程 ID 和计数器。
processId: ObjectId('660a1cdf2e7a827807cac6f9'), # 进程 ID
counter: Long('6') # 计数器,用于标识拓扑版本
},
hosts: [ '10.1.2.26:27017', '10.1.2.142:27017', '10.1.2.41:27017' ], # 复制集中所有成员的主机和端口号列表。
setName: 'rs0', # 复制集的名称
setVersion: 1, # 复制集的版本号
ismaster: true, # 当前节点是否为主节点
secondary: false, # 当前节点是否为从节点
primary: '10.1.2.26:27017', # 主节点的主机名和端口号
me: '10.1.2.26:27017', # 当前节点的主机名和端口号
electionId: ObjectId('7fffffff0000000000000001'), # 选举 ID
lastWrite: { # 最后一次写入操作的时间信息
opTime: { ts: Timestamp({ t: 1711939636, i: 7 }), t: Long('1') }, # 操作时间戳
lastWriteDate: ISODate('2024-04-01T02:47:16.000Z'), # 最后一次写入操作的时间
majorityOpTime: { ts: Timestamp({ t: 1711939636, i: 7 }), t: Long('1') }, # 大多数写操作的时间戳
majorityWriteDate: ISODate('2024-04-01T02:47:16.000Z') # 大多数写操作的时间
},
maxBsonObjectSize: 16777216, # 最大 BSON 对象大小
maxMessageSizeBytes: 48000000, # 最大消息大小(以字节为单位))
maxWriteBatchSize: 100000, # 最大写批次大小
localTime: ISODate('2024-04-01T02:47:20.408Z'), # 本地时间
logicalSessionTimeoutMinutes: 30, # 登录超时时间(以分钟为单位)
connectionId: 7, # 连接 ID
minWireVersion: 0, # 最小Wire版本
maxWireVersion: 21, # 最大Wire版本
readOnly: false, # 当前节点是否为只读节点
ok: 1, # 操作是否成功
'$clusterTime': { # 集群时间
clusterTime: Timestamp({ t: 1711939636, i: 7 }), # 集群时间戳
signature: { # 签名信息
hash: Binary.createFromBase64('OOXSWapWStZk2n4cyJ7jdBS87do=', 0), # 签名哈希
keyId: Long('7352724745051176965') # 密钥 ID
}
},
operationTime: Timestamp({ t: 1711939636, i: 7 }), # 操作时间戳
isWritablePrimary: true # 当前主节点是否可写
}
  • 查看集群状态

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
> rs.status()
# 这里只列出节点的信息
…………………………
members: [ # 复制集成员列表
{
_id: 0, # 节点 ID
name: '10.1.2.26:27017', # 节点主机名和端口号
health: 1, # 节点健康状态,这里是 1,表示健康
state: 1, # 节点状态,这里是 1,表示主节点
stateStr: 'PRIMARY', # 节点状态字符串
uptime: 88366, # 节点运行时间
optime: { ts: Timestamp({ t: 1712044133, i: 1 }), t: Long('5') }, # 节点操作时间戳
optimeDate: ISODate('2024-04-02T07:48:53.000Z'), # 节点操作时间
lastAppliedWallTime: ISODate('2024-04-02T07:48:53.125Z'), # 节点最后应用时间
lastDurableWallTime: ISODate('2024-04-02T07:48:53.125Z'), # 节点最后持久化时间
syncSourceHost: '', # 同步源主机名
syncSourceId: -1, # 同步源节点 ID
infoMessage: 'Could not find member to sync from', # 节点信息消息
electionTime: Timestamp({ t: 1712044063, i: 1 }), # 选举时间戳
electionDate: ISODate('2024-04-02T07:47:43.000Z'), # 选举时间
configVersion: 4, # 配置版本
configTerm: 5, # 选举任期
self: true, # 是否为主节点
lastHeartbeatMessage: '' # 节点心跳消息
},
{
_id: 1,
name: '10.1.2.142:27017',
health: 1,
state: 2, # 节点状态,这里是 2,表示从节点
stateStr: 'SECONDARY',
uptime: 88363,
optime: { ts: Timestamp({ t: 1712044133, i: 1 }), t: Long('5') },
optimeDurable: { ts: Timestamp({ t: 1712044133, i: 1 }), t: Long('5') },
optimeDate: ISODate('2024-04-02T07:48:53.000Z'),
optimeDurableDate: ISODate('2024-04-02T07:48:53.000Z'),
lastAppliedWallTime: ISODate('2024-04-02T07:48:53.125Z'),
lastDurableWallTime: ISODate('2024-04-02T07:48:53.125Z'),
lastHeartbeat: ISODate('2024-04-02T07:48:53.140Z'), # 最后一次心跳时间
lastHeartbeatRecv: ISODate('2024-04-02T07:48:53.655Z'), # 最后一次接收心跳时间
pingMs: Long('0'), # 节点心跳间隔时间
lastHeartbeatMessage: '', # 节点心跳消息
syncSourceHost: '10.1.2.26:27017', # 同步源主机名
syncSourceId: 0, # 同步源节点 ID
infoMessage: '', # 节点信息消息
configVersion: 4, # 配置版本
configTerm: 5 # 选举任期
},
{
_id: 2,
name: '10.1.2.41:27017',
health: 0, # 节点健康状态,这里是0,表示节点不可达,因为该节点的mongo服务被关掉了,这里只是为了展示不同的状态
state: 8, # 节点状态,这里是8,表示不可达
stateStr: '(not reachable/healthy)', # 节点状态字符串,提示节点不可达
uptime: 0,
optime: { ts: Timestamp({ t: 0, i: 0 }), t: Long('-1') },
optimeDurable: { ts: Timestamp({ t: 0, i: 0 }), t: Long('-1') },
optimeDate: ISODate('1970-01-01T00:00:00.000Z'),
optimeDurableDate: ISODate('1970-01-01T00:00:00.000Z'),
lastAppliedWallTime: ISODate('2024-04-02T07:47:23.818Z'),
lastDurableWallTime: ISODate('2024-04-02T07:47:23.818Z'),
lastHeartbeat: ISODate('2024-04-02T07:48:53.163Z'),
lastHeartbeatRecv: ISODate('2024-04-02T07:47:32.215Z'),
pingMs: Long('0'),
lastHeartbeatMessage: 'Error connecting to 10.1.2.41:27017 :: caused by :: onInvoke :: caused by :: Connection refused', # 节点心跳消息,这里是连接失败
syncSourceHost: '',
syncSourceId: -1,
infoMessage: '',
configVersion: 4,
configTerm: 4
}
]
………………………………

state的值及对应的含义
0: Startup - 成员正在启动。
1: Primary - 成员是主副本集节点。可以接收写入操作。每个副本集有且仅有一个主节点。
2: Secondary - 成员是从副本集节点。复制主节点的数据变更。
3: Recovering - 成员已经接收到新的数据,但还无法提供读或投票服务。此状态通常是短暂的。
4: Fatal - 成员发生了不可恢复的错误,已停止接收和复制数据。人工干预需要重启成员或者副本集。
5: Startup2 - 成员正在初始化副本集的内部数据结构。
6: Unknown - 因为与此成员的同步被打断,导致此成员状态未知。
7: Arbiter - 成员是仲裁者。
8: Down - 这个成员目前不能到达。
9: Rollback - 这个成员正在滚动回数据以达到一致的状态。
10: Removed - 这个成员已经从副本集中删除。

授权

  • 创建用户

授权后需要重新登录或进行认证后才能继续操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 登录主节点
rs0 [direct: primary] test> use admin
switched to db admin

# 创建用户
rs0 [direct: primary] admin> db.createUser({
user: "user",
pwd: "password",
roles: [
{ role: "clusterAdmin", db: "admin" } ,
{ role: "userAdminAnyDatabase", db: "admin"},
{ role: "dbAdminAnyDatabase", db: "admin" },
{ role: "readWriteAnyDatabase", db: "admin"}
]
});

登录

  • 方法1,连接的同时认证

1
2
3
4
5
6
7
# 默认登录test数据库
mongosh 10.1.2.26:27017 -u user -p password --authenticationDatabase admin
# 或者
mongosh "mongodb://user:password@10.1.2.26:27017/test?authSource=admin"

# 集群连接,推荐,会自动登录到主节点
mongosh "mongodb://user:password@10.1.2.26:27017,10.1.2.142:27017,10.1.2.41:27017/test?authSource=admin&replicaSet=rs0"
  • 方法2,先连接,再认证

1
2
3
4
5
6
7
8
9
10
11
12
mongosh 10.1.2.26:27017
> use admin
switched to db admin
> db.auth("user","password")
{ ok: 1 }

# 集群连接,推荐,会自动登录到主节点
mongosh "mongodb://10.1.2.26:27017,10.1.2.142:27017,10.1.2.41:27017/test?authSource=admin&replicaSet=rs0"
> use admin
switched to db admin
> db.auth("user","password")
{ ok: 1 }

查看用户信息

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
38
39
40
41
42
43
44
45
46
47
# 先要切换到 admin 数据库
rs0 [direct: primary] admin> db.system.users.find()
[
{
_id: 'admin.user',
userId: UUID('19dcd146-6872-41fe-8e39-46bb98b9db5c'),
user: 'user',
db: 'admin',
credentials: {
'SCRAM-SHA-1': {
iterationCount: 10000,
salt: 'DLOOhoEKeG2RKfW8hU4caw==',
storedKey: 'ohQOJeAS3XWUwlFJAGduuiUJptc=',
serverKey: 'rDjqq/nF3awasdaPr2ocJskm7p0='
},
'SCRAM-SHA-256': {
iterationCount: 15000,
salt: '6YpcJH2pcL3yC726rpIq6VWSQLSV68i+oqURkw==',
storedKey: '1nIijUrLAL0lQZ6++O0cxM3VbsprdwRQucVFYkDQWbA=',
serverKey: 'nk8TKbK19zLaD7wJn+991euF9ILE08iPopmJ8YAAqfQ='
}
},
roles: [
{ role: 'userAdminAnyDatabase', db: 'admin' },
{ role: 'clusterAdmin', db: 'admin' },
{ role: 'dbAdminAnyDatabase', db: 'admin' },
{ role: 'readWriteAnyDatabase', db: 'admin' }
]
}
]

rs0 [direct: primary] admin> show users
[
{
_id: 'admin.user',
userId: UUID('19dcd146-6872-41fe-8e39-46bb98b9db5c'),
user: 'user',
db: 'admin',
roles: [
{ role: 'userAdminAnyDatabase', db: 'admin' },
{ role: 'clusterAdmin', db: 'admin' },
{ role: 'dbAdminAnyDatabase', db: 'admin' },
{ role: 'readWriteAnyDatabase', db: 'admin' }
],
mechanisms: [ 'SCRAM-SHA-1', 'SCRAM-SHA-256' ]
}
]

开启从库读取数据的权限

  • 使用当前最新版mongosh-2.2.2连接从库时,默认就支持读取数据,其默认的readPreferenceprimaryPreferred

  • 如果使用之前的版本连接从库时,默认是不能读取数据的,需要手工开启

1
2
3
4
5
6
7
8
# 提示非主节点,不能查询数据,
rs0 [direct: secondary] test> show users
MongoServerError[NotPrimaryNoSecondaryOk]: not primary - consider using db.getMongo().setReadPref() or readPreference in the connection string

# 早期版本是运行 rs.secondaryOk(),不过现在提示已经不推荐了
rs0 [direct: secondary] test> rs.secondaryOk()
DeprecationWarning: .setSecondaryOk() is deprecated. Use .setReadPref("primaryPreferred") instead
Setting read preference from "primary" to "primaryPreferred"
  • 全局设置readPreference,对本次链接都有效

1
2
# 开启从库读取数据权限
rs0 [direct: secondary] test> db.getMongo().setReadPref("primaryPreferred")
  • 也可以在链接参数中指定readPreference

1
2
3
4
5
# 单节点连接
mongosh "mongodb://user:password@10.1.2.142:27017/test?authSource=admin&readPreference=primaryPreferred"

# 集群连接
mongosh "mongodb://user:password@10.1.2.26:27017,10.1.2.142:27017,10.1.2.41:27017/test?authSource=admin&replicaSet=rs0&readPreference=primaryPreferred"
  • 可以在查询时指定readPreference,只针对当前查询,对后续查询无效

1
rs0 [direct: secondary] admin> db.system.users.find().readPref("secondary")

readPreference

readPreference决定使用哪一类节点(主或从节点)来满足正在发起的读请求。可选值包括:

  • primary:默认值,只读主节点的数据。

  • primaryPreferred:优先读主节点的数据,如果主节点不可用,则读从节点的数据。

  • secondary:只读从节点的数据。

  • secondaryPreferred:优先读从节点的数据,如果从节点不可用,则读主节点的数据。

  • nearest:优先读距离最近的节点的数据。

指定 readPreference 时应注意高可用问题。例如将 readPreference 指定 primary,则发生故障转移不存在 primary 期间将没有节点可读。如果业务允许,则应选择 primaryPreferred

Tag

  • 集群中节点的标签,用于选择节点

  • readPreference 只能控制使用一类(主或从节点)节点。Tag 则可以将节点选择控制到一个或几个节点。

  • 为节点添加Tag

1
2
3
4
5
6
# 必须在主节点下才能修改配置
conf = rs.conf()
conf.members[0].tags = { purpose: "online" }
conf.members[1].tags = { purpose: "online" }
conf.members[2].tags = { purpose: "analyse"}
rs.reconfig(conf)
  • 全局指定Tag

1
rs0 [direct: primary] admin> db.getMongo().setReadPref("primaryPreferred", [ {purpose: "online"} ])
  • 在链接参数中指定Tag

1
mongosh "mongodb://user:password@10.1.2.26:27017,10.1.2.142:27017,10.1.2.41:27017/test?authSource=admin&replicaSet=rs0&readPreference=primaryPreferred&readPreferenceTags=purpose:online"
  • 在查询时指定Tag

1
2
3
4
5
rs0 [direct: secondary] admin> db.system.users.find().readPref("secondary", [ {purpose: "analyse"} ])

# 注意,`Tag`只能用于从节点,主节点不支持。
rs0 [direct: primary] admin> db.system.users.find().readPref("primary", [ {purpose: "online"} ])
MongoInvalidArgumentError: Primary read preference cannot be combined with tags
  • 使用 Tag 时应注意高可用问题,如果只有一个节点拥有一个特定 Tag,则在这个节点失效时将无节点可读。这在有时候是期望的结果,有时候不是。例如:

    • 如果报表使用的节点失效,即使不生成报表,通常也不希望将报表负载转移到其他节点上,此时只有一个节点有报表 Tag 是合理的选择;
    • 如果线上节点失效,通常希望有替代节点,所以应该保持多个节点有同样的 Tag;
  • Tag 有时需要与优先级、选举权综合考虑。例如做报表的节点通常不会希望它成为主节点,则优先级应为 0。

  • 通过rs.conf()可以查看复制集中各个节点的Tag信息

优先级(priority) 与 选举权(vote)

  • Priority 等于 0 时,它不可以被复制集选举为主,Priority 的值越高,则被选举为主的概率更大。

  • Vote 等于 0 时,它不可以参与选举投票,此时该节点的 Priority 也必须为 0,即它也不能被选举为主。

  • 由于一个复制集中最多只有7个投票成员,因此多出来的成员则必须将其vote属性值设置为0,即这些成员将无法参与投票。

  • 通过rs.conf()可以查看复制集中各个节点的相关配置

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
> rs.conf()
{
_id: 'rs0', # 复制集名称
version: 2, # 配置版本号,即conf被修改的次数,每次更改配置时,版本号都会递增。
term: 3, # 选举轮次(election term)。在进行选举时,MongoDB 使用一个单调递增的 term 值来标识选举的周期。这个值会随着选举的进行而增加。
members: [ # 成员数组,包含了复制集中的所有成员的详细配置信息。
{
_id: 0, # 成员的唯一标识符,从0开始递增。
host: '10.1.2.26:27017', # 节点ip+port
arbiterOnly: false, # 是否为仲裁节点
buildIndexes: true, # 是否在此节点上创建索引
hidden: false, # 是否为隐藏节点,即不被客户端发现。
priority: 1, # 选举优先级,值越大优先级越高,0表示不能被选举为主节点
tags: { purpose: 'online' }, # Tag
secondaryDelaySecs: Long('0'), # 如果存在延迟复制,表示复制延迟的时间量(以秒为单位),如果为0则表示不是延迟节点。
votes: 1 # 成员的投票数,大于0表示参与投票。
},
],
protocolVersion: Long('1'), # 复制集使用的协议版本。
writeConcernMajorityJournalDefault: true, # 是否在写入操作中默认启用大多数写关注和日志记录
settings: { # 复制集的全局设置,包含以下属性:
chainingAllowed: true, # 是否允许从其他成员读取数据。
heartbeatIntervalMillis: 2000, # 心跳间隔,即成员之间检测状态的时间间隔。
heartbeatTimeoutSecs: 10, # 心跳超时时间,如果在此时间内未收到心跳,则认为成员不可用。
electionTimeoutMillis: 10000, # 选举超时时间,即进行选举的时间间隔。
catchUpTimeoutMillis: -1, # 成员在追赶操作时的超时时间。
catchUpTakeoverDelayMillis: 30000, # 在追赶操作期间,发生主节点故障时,延迟节点接管主节点之前的延迟时间。
getLastErrorModes: {}, # 指定写操作的确认模式。
getLastErrorDefaults: { w: 1, wtimeout: 0 }, # 指定写操作的默认确认参数。
replicaSetId: ObjectId('660a20282e7a827807cac803') # 复制集的唯一标识符。
}
}
………………
  • 修改节点优先级

1
2
3
4
5
# 必须在主节点执行
cfg = rs.conf()
# 保存成功后,由于当前节点的优先级最高,所以集群会发起重新选举将当前节点设置为主节点
cfg.members[2].priority = 10
rs.reconfig(cfg)
  • 设置隐藏节点

1
2
3
4
5
# 必须在主节点执行
cfg = rs.conf()
cfg.members[2].priority = 0
cfg.members[2].hidden = true
rs.reconfig(cfg)
  • 配置延时节点

1
2
3
4
5
6
cfg = rs.conf()
cfg.members[2].priority = 0
cfg.members[2].hidden = true
#延迟60秒
cfg.members[2].secondaryDelaySecs = 60
rs.reconfig(cfg)

读关注(readConcern)

  • readPreference 选择了指定的节点后,readConcern 决定这个节点上的数据哪些是可读的

  • 类似于关系数据库的隔离级别,可选值包括:

1
2
3
4
5
- available:读取所有可用的数据;
- local:读取所有可用且属于当前分片的数据。在复制集中 local 和 available 是没有区别的,两者仅在分片集群中才有区别;
- majority:读取在大多数节点上提交完成的数据;
- linearizable:可线性化读取文档,仅支持从主节点读。只读取大多数节点确认过的数据。和 majority 最大差别是保证绝对的操作线性顺序;
- snapshot:读取最近快照中的数据,仅可用于多文档事务,隔离级别最高;
  • 主节点读取数据时默认 readConcernlocal,从节点读取数据时默认 readConcernavailable

分片集群中 local 和 available 的区别

  • 如果一个 chunk x 正在从 shard1 向 shard2 迁移;
  • 整个迁移过程中 chunk x 中的部分数据会在 shard1 和 shard2 中同时存在,但源分片 shard1仍然是chunk x 的负责方:
    • 所有对 chunk x 的读写操作仍然进入 shard1;
    • config 中记录的信息 chunk x 仍然属于 shard1;
  • 此时如果读 shard2,则会体现出 local 和 available 的区别:
    • local:只取应该由 shard2 负责的数据(不包括 x);
    • available:shard2 上有什么就读什么(包括 x);
  • 示例

1
2
3
4
db.user.find().readConcern('local')
db.user.find().readConcern('majority')
# 读取从节点上已经被大多数节点提交完成的数据
db.user.find().readPref("secondary").readConcern("majority")

写关注(writeConcern)

  • writeConcern 决定一个写操作落到多少个节点上才算成功。

  • MongoDB支持客户端灵活配置写入策略(writeConcern),以满足不同场景的需求。

1
2
3
4
5
6
7
8
9
10
11
12
{ w: <value>, j: <boolean>, wtimeout: <number> }

w: 数据写入到number个节点才向用客户端确认
- {w: 0} 对客户端的写入不需要发送任何确认,适用于性能要求高,但不关注正确性的场景
- {w: 1} 默认的writeConcern,数据写入到Primary就向客户端发送确认
- {w: "majority"} 数据写入到副本集大多数成员后向客户端发送确认,适用于对数据安全性要求比较高的场景,该选项会降低写入性能

j: 写入操作的journal持久化后才向客户端确认
- 默认为{j: false},如果要求Primary写入持久化了才向客户端确认,则指定该选项为true

wtimeout: 写入超时时间,仅w的值大于1时有效。
- 当指定{w: }时,数据需要成功写入number个节点才算成功,如果写入过程中有节点故障,可能导致这个条件一直不能满足,从而一直不能向客户端发送确认结果,针对这种情况,客户端可设置wtimeout选项来指定超时时间,当写入过程持续超过该时间仍未结束,则认为写入失败。
  • 通常重要数据应用 {w: "majority"},普通数据可以应用 {w: 1} 以确保最佳性能。

  • 示例

1
2
3
4
# 等等大多数节点写入成功
db.user.insertOne({name:"李四"},{writeConcern:{w:"majority"}})
# 等待3个节点写入成功,若超时则写入失败
db.user.insertOne({name:"小明"},{writeConcern:{w:3,wtimeout:3000}})

优雅的重启复制集

  • 逐个重启复制集里所有的Secondary节点

1
2
3
4
# 关闭服务
mongod --shutdown -f /mongodb/mongo.conf
# 重启启动
mongod -f /mongodb/mongo.conf
  • 对Primary发送rs.stepDown()命令,等待primary降级为Secondary

1
2
3
4
5
6
7
8
9
10
11
12
rs0 [primary] local> rs.stepDown()
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1712046433, i: 1 }),
signature: {
hash: Binary.createFromBase64('dklQyCuunlYhWYkKuNtiNcGNom8=', 0),
keyId: Long('7352724745051176965')
}
},
operationTime: Timestamp({ t: 1712046433, i: 1 })
}
  • 重启降级后的Primary

oplog

  • 在复制集架构中,主节点与备节点之间是通过oplog来同步数据的,这里的oplog是一个特殊的固定集合,当主节点上的一个写操作完成后,会向oplog集合写入一条对应的日志,而备节点则通过这个oplog不断拉取到新的日志,在本地进行回放以达到数据同步的目的。

  • 查询oplog

1
2
3
4
5
6
7
8
9
10
11
use local
# 查询最近10条oplog,这里 $natural:-1 是按照时间降序排列
db.oplog.rs.find().sort({$natural:-1}).limit(10)
# 查询最近60分钟oplog
db.oplog.rs.find({
"ts": {
"$gt": Timestamp(Math.floor(new Date().getTime() / 1000) - 60*60, 1)
}
}).sort({
"$natural": -1
}).limit(10)
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
{
op: 'u',
ns: 'config.system.sessions',
ui: UUID('4f90005d-a546-4d59-971d-7954c3acd0f8'),
o: {
'$v': 2,
diff: { u: { lastUse: ISODate('2024-04-02T08:31:10.148Z') } }
},
o2: {
_id: {
id: UUID('3f881052-ac21-4677-816c-33934322fbd6'),
uid: Binary.createFromBase64('eOZyqfHfiVpfviPDWgAERmzNFnUp8UBdlWKN50Fv9fo=', 0)
}
},
ts: Timestamp({ t: 1712046670, i: 1 }),
t: Long('6'),
v: Long('2'),
wall: ISODate('2024-04-02T08:31:10.148Z')
}

ts: 操作时间,当前timestamp + 计数器,计数器每秒都被重置
v:oplog版本信息
op:操作类型:
i:插⼊操作
u:更新操作
d:删除操作
c:执行命令(如createDatabase,dropDatabase)
n:空操作,特殊用途
ns:操作针对的集合
o:操作内容
o2:操作查询条件,仅update操作包含该字段
  • oplog集合的大小默认为min(磁盘可用空间*5%,50GB),可以通过如下命令修改

1
2
3
4
5
6
# 将复制集成员的oplog大小修改为1G,这里单位是M
> db.adminCommand({replSetResizeOplog: 1, size: 1024})
# 查看oplog大小
> use local
> db.oplog.rs.stats().maxSize/1024/1024
1024
  • 这里要注意每个节点的oplog大小都需要单独的配置,并且必须一致,否则有可能会出现同步失败的情况。

  • 查看当前节点的oplog的状态

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
rs0 [primary] test> rs.printReplicationInfo()
actual oplog size
'1024 MB'
---
configured oplog size
'1024 MB'
---
log length start to end
'112923 secs (31.37 hrs)'
---
oplog first event time
'Mon Apr 01 2024 02:47:04 GMT+0000 (Coordinated Universal Time)'
---
oplog last event time
'Tue Apr 02 2024 10:09:07 GMT+0000 (Coordinated Universal Time)'
---
now
'Tue Apr 02 2024 10:09:08 GMT+0000 (Coordinated Universal Time)'


actual oplog size:表示实际使用的 oplog 大小。Oplog 是一个 circurlar buffer,用于存储主节点上的操作日志。这个属性指示了 oplog 的当前大小。在示例中,oplog 大小为 '1024 MB'

configured oplog size:表示配置的 oplog 大小。这是在配置复制集时指定的 oplog 大小。在示例中,配置的 oplog 大小也是 '1024 MB',说明实际 oplog 大小与配置的 oplog 大小相匹配。

log length start to end:表示 oplog 的记录长度,从开始到结束的时间跨度。在示例中,oplog 记录了 '112923 secs (31.37 hrs)',表示从 oplog 开始记录到结束的时间跨度为约 31.37 小时。

oplog first event time:表示 oplog 记录的第一个事件的时间戳。在示例中,第一个事件的时间为 'Mon Apr 01 2024 02:47:04 GMT+0000 (Coordinated Universal Time)'

oplog last event time:表示 oplog 记录的最后一个事件的时间戳。在示例中,最后一个事件的时间为 'Tue Apr 02 2024 10:09:07 GMT+0000 (Coordinated Universal Time)'

now:表示当前时间。在示例中,当前时间为 'Tue Apr 02 2024 10:09:08 GMT+0000 (Coordinated Universal Time)'
  • 以从节点视角查看oplog复制状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rs0 [primary] local> rs.printSecondaryReplicationInfo()
source: 10.1.2.142:27017
{
syncedTo: 'Tue Apr 02 2024 10:10:07 GMT+0000 (Coordinated Universal Time)',
replLag: '0 secs (0 hrs) behind the primary '
}
---
source: 10.1.2.41:27017
{
syncedTo: 'Tue Apr 02 2024 10:10:07 GMT+0000 (Coordinated Universal Time)',
replLag: '0 secs (0 hrs) behind the primary '
}

source:从节点的主机名和端口号,即复制数据的来源节点。

syncedTo:表示从节点已经复制到的最新操作的时间戳。它指示了从节点上数据的同步程度,即从节点已经复制了到主节点的哪个时间点的数据。在示例中,两个从节点的 syncedTo 时间戳都是 'Tue Apr 02 2024 10:10:07 GMT+0000 (Coordinated Universal Time)',表示它们都已经与主节点同步到了相同的时间点。

replLag:表示从节点与主节点之间的复制延迟。它显示了从节点当前落后于主节点的时间量,以秒和小时为单位。在示例中,两个从节点的 replLag 都是 '0 secs (0 hrs) behind the primary',表示它们与主节点保持实时同步,没有任何复制延迟。

既然oplog的大小有限制,新的数据会覆盖旧的数据,那么新加入的节点如何全量同步数据呢?

  • 当MongoDB的从库节点需要进行数据同步时,会执行以下步骤:

    • 初始同步: 当一个新节点加入到副本集中时,它会执行初始同步以获取集合中的所有数据。初始同步会复制所有数据库的所有数据,包括系统数据库。之后,新节点会开始从oplog里获取更改历史,应用这些更改历史到数据,以保证与其他从库节点一致。注意,初始同步中的数据包括在新加入节点之前的数据以及新节点加入之后其他节点产生的新数据。
    • oplog落后: 如果从库在初始同步执行期间不能及时获取oplog(例如,oplog的数据超出了大小限制并被覆盖,或者初次同步时间过长),新节点需要重新进行初始同步操作。
  • 这些步骤确保了MongoDB副本集的数据在所有节点间的一致性,尽管oplog有大小限制并且旧的内容会被新的操作覆盖。

复制集相关操作

命令 描述
rs.add(“hostname:port”) 为复制集新增节点
rs.addArb(“hostname:port”) 为复制集新增一个 arbiters
rs.conf() 返回复制集配置信息
rs.config() 同 rs.conf()
rs.freeze(3600) 防止当前节点在一段时间内选举成为主节点,如这里是3600秒,则当前节点在3600秒内不会选举成为主节点
rs.help() 返回 replica set 的命令帮助
rs.initiate() 初始化一个新的复制集
rs.printReplicationInfo() 以当前节点的视角返回复制的状态报告
rs.printSecondaryReplicationInfo() 以从节点的视角返回复制状态报告
rs.reconfig() 通过重新应用复制集配置来为复制集更新配置
rs.remove(“hostname:port”) 从复制集中移除一个节点
rs.secondaryOk() 为当前的连接设置从节点可读,该方法已经过时了,使用 db.getMongo().setReadPref() 代替
rs.status() 返回复制集状态信息
rs.stepDown() 让当前的 primary 变为从节点并触发新一轮的主节点选举
rs.syncFrom(“hostname:port”) 设置复制集节点从哪个节点处同步数据,将会覆盖默认选取逻辑
rs.reconfigForPSASet() 在主-从-仲裁器(PSA)副本集或正在更改为PSA架构的副本集上安全地执行一些重新配置更改。

移除节点

1
2
3
4
5
6
7
8
9
10
11
12
rs0 [primary] local> rs.remove("10.1.2.41:27017")
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1712049022, i: 1 }),
signature: {
hash: Binary.createFromBase64('Qg3Ymzp3k+hhzaDo3jZQimc4WCI=', 0),
keyId: Long('7352724745051176965')
}
},
operationTime: Timestamp({ t: 1712049022, i: 1 })
}

添加仲裁节点

当集群中的节点数>=3时,无论是添加仲裁节点还是从节点都没有问题。

  • 上面删除了集群中的一个节点,此时集群中的节点说是2(一主一从),此时如果主发生故障集群就没办法工作了,此时我们可以为其添加一个仲裁节点(只投票不存储数据)。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 添加仲裁节点,但是此时添加仲裁节点会报错
rs0 [primary] local> rs.addArb("10.1.2.41:27017")
MongoServerError[NewReplicaSetConfigurationIncompatible]: Reconfig attempted to install a config that would change the implicit default write concern. Use the setDefaultRWConcern command to set a cluster-wide write concern and try the reconfig again.

# 报错原因:由于默认的写关注为 majority,而当前节点中只有2个节点,没有办法确认“大多数”是多少了,所以我们需要修改写关注的设置
# 查询默认的读写关注
rs0 [primary] admin> db.adminCommand({"getDefaultRWConcern" : 1})
{
defaultReadConcern: { level: 'local' },
defaultWriteConcern: { w: 'majority', wtimeout: 0 },
updateOpTime: Timestamp({ t: 1712050437, i: 1 }),
updateWallClockTime: ISODate('2024-04-02T09:15:04.462Z'),
defaultWriteConcernSource: 'global',
defaultReadConcernSource: 'implicit',
localUpdateWallClockTime: ISODate('2024-04-02T09:15:04.462Z'),
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1712109448, i: 1 }),
signature: {
hash: Binary.createFromBase64('oUiR9xdXN4mzQqdz9IXHGdCmNto=', 0),
keyId: Long('7352724745051176965')
}
},
operationTime: Timestamp({ t: 1712109448, i: 1 })
}

# 修改默认写关注 defaultWriteConcern
# setDefaultRWConcern:用于指定设置默认读写关注级别(RW Concern)的操作。值为 1 表示执行设置操作。
# defaultWriteConcern:用于指定默认的写入关注级别。在这个例子中,写入关注级别被设置为 { "w" : 2 },意味着在写入数据时,MongoDB 驱动程序会等待至少两个节点确认写入成功后才返回成功。其实这里设置为1也可以,表示写入主节点就返回成功。
rs0 [primary] local> db.adminCommand( {"setDefaultRWConcern" : 1, "defaultWriteConcern" : { "w" : 2 } } )
{
defaultReadConcern: { level: 'local' },
defaultWriteConcern: { w: 2, wtimeout: 0 },
updateOpTime: Timestamp({ t: 1712049370, i: 1 }),
updateWallClockTime: ISODate('2024-04-02T09:16:22.715Z'),
defaultWriteConcernSource: 'global',
defaultReadConcernSource: 'implicit',
localUpdateWallClockTime: ISODate('2024-04-02T09:16:22.726Z'),
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1712049382, i: 2 }),
signature: {
hash: Binary.createFromBase64('04nzjl47CMumvguEzUq7RnY55+s=', 0),
keyId: Long('7352724745051176965')
}
},
operationTime: Timestamp({ t: 1712049382, i: 2 })
}


# 再次添加仲裁节点成功
rs0 [primary] local> rs.addArb("10.1.2.41:27017")
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1712049480, i: 1 }),
signature: {
hash: Binary.createFromBase64('4q9s6mvT0boa6Q+vjKms8vnTR6Y=', 0),
keyId: Long('7352724745051176965')
}
},
operationTime: Timestamp({ t: 1712049480, i: 1 })
}
  • 此时关闭主节点,就会看到另一个从节点成为主节点了

添加新的节点

当集群中的节点数>=3时,无论是添加仲裁节点还是从节点都没有问题。

  • 如果当前集群中有两个节点(一主一从),我们也可以不添加仲裁节点,而是直接添加一个新的从节点

1
2
3
4
5
6
7
8
9
10
11
12
rs0 [primary] test> rs.add("10.1.2.41:27017")
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1712051880, i: 1 }),
signature: {
hash: Binary.createFromBase64('DuuorhNH4MWqa4DHCifPzxcMN3I=', 0),
keyId: Long('7352724745051176965')
}
},
operationTime: Timestamp({ t: 1712051880, i: 1 })
}
  • 当集群中只有一个主节点和一个仲裁节点时,此时若直接添加新的节点会报错

1
2
rs0 [primary] local> rs.add("10.1.2.142:27017")
MongoServerError[NewReplicaSetConfigurationIncompatible]: Rejecting reconfig where the new config has a PSA topology and the secondary is electable, but the old config contains only one writable node. Refer to https://docs.mongodb.com/manual/reference/method/rs.reconfigForPSASet/ for next steps on reconfiguring a PSA set.
  • 根据给出的网址的提示,执行以下操作

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# 获取当前配置
rs0 [primary] test> cfg = rs.conf();
{
_id: 'rs0',
version: 7,
term: 8,
members: [
{
_id: 0,
host: '10.1.2.26:27017',
arbiterOnly: false,
buildIndexes: true,
hidden: false,
priority: 1,
tags: { purpose: 'online' },
secondaryDelaySecs: Long('0'),
votes: 1
},
{
_id: 2,
host: '10.1.2.41:27017',
arbiterOnly: true,
buildIndexes: true,
hidden: false,
priority: 0,
tags: {},
secondaryDelaySecs: Long('0'),
votes: 1
}
],
protocolVersion: Long('1'),
writeConcernMajorityJournalDefault: true,
settings: {
chainingAllowed: true,
heartbeatIntervalMillis: 2000,
heartbeatTimeoutSecs: 10,
electionTimeoutMillis: 10000,
catchUpTimeoutMillis: -1,
catchUpTakeoverDelayMillis: 30000,
getLastErrorModes: {},
getLastErrorDefaults: { w: 1, wtimeout: 0 },
replicaSetId: ObjectId('660a20282e7a827807cac803')
}
}

# 修改配置,增加新的节点信息
rs0 [primary] test> cfg["members"] = [
{
_id: 0,
host: '10.1.2.26:27017',
arbiterOnly: false,
buildIndexes: true,
hidden: false,
priority: 1,
tags: { purpose: 'online' },
secondaryDelaySecs: Long('0'),
votes: 1
},
{
_id: 1,
host: '10.1.2.142:27017', # 新的节点
arbiterOnly: false,
buildIndexes: true,
hidden: false,
priority: 1,
tags: { purpose: 'online' },
secondaryDelaySecs: Long('0'),
votes: 1
},
{
_id: 2,
host: '10.1.2.41:27017',
arbiterOnly: true,
buildIndexes: true,
hidden: false,
priority: 0,
tags: {},
secondaryDelaySecs: Long('0'),
votes: 1
}
]

# 执行配置
rs0 [primary] test> rs.reconfigForPSASet(1, cfg);
Running first reconfig to give member at index 1 { votes: 1, priority: 0 }
Running second reconfig to give member at index 1 { priority: 1 }
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1712051518, i: 2 }),
signature: {
hash: Binary.createFromBase64('+tgYZKeeGP0KGt3YVpDCAjW3xTc=', 0),
keyId: Long('7352724745051176965')
}
},
operationTime: Timestamp({ t: 1712051518, i: 2 })
}

# 查看新节点的状态
rs0 [primary] test> rs.status()
………………
{
_id: 1,
name: '10.1.2.142:27017',
health: 1,
state: 2,
stateStr: 'SECONDARY',
uptime: 87,
optime: { ts: Timestamp({ t: 1712051597, i: 1 }), t: Long('8') },
optimeDurable: { ts: Timestamp({ t: 1712051597, i: 1 }), t: Long('8') },
optimeDate: ISODate('2024-04-02T09:53:17.000Z'),
optimeDurableDate: ISODate('2024-04-02T09:53:17.000Z'),
lastAppliedWallTime: ISODate('2024-04-02T09:53:17.058Z'),
lastDurableWallTime: ISODate('2024-04-02T09:53:17.058Z'),
lastHeartbeat: ISODate('2024-04-02T09:53:24.535Z'),
lastHeartbeatRecv: ISODate('2024-04-02T09:53:24.535Z'),
pingMs: Long('0'),
lastHeartbeatMessage: '',
syncSourceHost: '10.1.2.26:27017',
syncSourceId: 0,
infoMessage: '',
configVersion: 10,
configTerm: 8
}
………………

故障转移

  • 集群组建完成后,集群内的各个节点会开启心跳响应定时器,默认每隔2秒(heartbeatIntervalMillis)会向其它成员发起心跳。

  • 同时各成员节点还会启动一个选举超时检测定时器,默认每隔10秒(electionTimeoutMillis)会发起一轮选举调度

  • 当主节点心跳响应成功,则当前节点就会取消上一次的选举调度,即会重置选举超时检测定时器的倒计时

  • 若主节点心跳响应失败,并且在选举超时检测的时间内仍然没有响应成功,则当前节点就会发起一轮选举调度

  • MongoDB的复制集选举使用Raft算法来实现,选举成功的必要条件是大多数投票节点存活。简单来说就是如果超过半数的节点都给我投票了,那么我就会成为主节点。

为什么推荐集群连接的方式?

  • 考虑这样的场景,如果我连接主节点并且插入10000条说,若此时主节点宕机,则客户端会立刻停止操作。

1
2
3
4
5
6
7
# 只连接主节点
mongosh "mongodb://user:password@10.1.2.26:27017/test?authSource=admin"

# 执行插入
rs0 [direct: primary] test> for(i=0;i<10000;i++){db.emp.insertOne({name:"test"+i})}
# 此时关闭主节点的mongo服务,则客户端会立刻给出报错信息
MongoServerError[NotWritablePrimary]: not primary
  • 若连接采用集群的方式连接,则不会中断操作,其原因就是集群连接方式会自动完成故障转移

1
2
3
4
5
6
7
8
9
10
mongosh "mongodb://user:password@10.1.2.26:27017,10.1.2.142:27017,10.1.2.41:27017/test?authSource=admin&replicaSet=rs0"
# 插入操作没有被中断
rs0 [primary] test> for(i=0;i<10000;i++){db.emp.insertOne({name:"test"+i})}
{
acknowledged: true,
insertedId: ObjectId('660cce829918b823661879c6')
}
# 查询结果正确
rs0 [primary] test> db.emp.countDocuments()
10000
  • 这里有一点需要注意,当你创建集群时使用的是各个节点的内网ip地址,则客户端必须可以访问这些内网ip才能通过集群连接,否则会出现连接失败的情况。因为通过集群连接的方式,实际上是会先在集群内部判断出主节点的ip地址返回给客户端,然后客户端再通过这个主节点ip访问主节点。

小贴士

  • 在复制集发生主备节点切换的情况下,会出现短暂的无主节点阶段,此时无法接受业务写操作。
  • 早期的版本中需要添加retryWrites参数,现在已经默认开启了
1
mongosh "mongodb://user:password@10.1.2.26:27017,10.1.2.142:27017,10.1.2.41:27017/test?authSource=admin&replicaSet=rs0&retryWrites=true"

SpringBoot集成MongoDB集群

1
2
3
4
spring:
data:
mongodb:
uri: mongodb://user:password@127.0.0.1:27040,127.0.0.1:27041,127.0.0.1:27042/mytest?authSource=admin&readPreference=primaryPreferred