文章目录
1. 什么是 MongoDB 复制(副本)集
MongoDB 复制集是一个包含多个 MongoDB 实例的集群,其中有一个主节点(Primary)和多个备份节点(Secondary)。主节点负责处理所有的写请求,并将写入的数据复制到备份节点上。备份节点通过从主节点同步数据来保持数据的一致性。如果主节点发生故障,备份节点中会选出一个节点成为新的主节点,保持系统的高可用性。
下面是一个 MongoDB 复制集的官网示意图:
1.1 组成部分
副本集是由多个mongod实例组成,这些实例维护相同的数据集。副本集包含多个数据承载节点和一个可选的仲裁节点。在数据承载节点中,一个且仅一个成员被视为主节点,而其他节点则被视为次要节点。
- 主节点接收所有写操作。在一个副本集中,只能有一个能够使用{w:“majority”}写关注来确认写操作的主节点;尽管在某些情况下,另一个mongod实例可能暂时认为自己也是主节点。主节点在其操作日志(即oplog)中记录其数据集的所有更改。
- 次要节点复制主节点的操作日志,并将操作应用于其数据集,使得次要节点的数据集反映主节点的数据集。如果主节点不可用,一个合格的次要节点将进行选举,选举自己成为新的主节点。
1.2 三节点复制模式
1.2.1 PSS
- 组成:1个主节点和2个备节点,即Primary+Secondary+Secondary。
- 特点:在这个模式下,主节点接收所有写操作,备节点复制主节点的操作日志并应用这些操作到它们自己的数据集中。如果主节点不可用,复制集会选择其中一个备节点作为新的主节点并继续正常的操作,直到旧的主节点重新加入复制集。这个模式的优点是它简单易懂,对于数据集较小的情况下,提供了一定程度的容错能力。但是,当数据集变得非常大时,备节点的同步可能会成为性能瓶颈,因为每个备节点都要复制主节点的所有写操作。
- 结构图:
1.2.2 PSA
- 组成:1个主节点、1个备节点和1个仲裁者节点,即Primary+Secondary+Arbiter
- 特点:Arbiter节点是副本集中的一种特殊节点,它不存储数据副本,也不提供业务的读写操作。相反,它只参与副本集的选举过程,帮助判断哪个节点应该成为主节点。Arbiter节点通常部署在与数据节点不同的机器上,这样即使数据节点所在机器发生故障,Arbiter节点仍然能够参与选举。由于Arbiter节点不存储数据,因此它的故障不会影响业务的正常运行,只会影响选举投票。使用Arbiter节点的模式只提供数据集的一个完整副本,如果主节点不可用,则复制集将选择备节点作为主节点。
- 结构图:
1.3 复制集的优点
MongoDB 复制集有以下几个优点:
-
高可用性
MongoDB 复制集可以保证数据在多个节点上的冗余备份,当主节点(Primary)宕机或网络异常时,备份节点(Secondary)可以自动接管主节点的工作,确保系统的高可用性。通过设置副本集成员的投票权重,可以进一步提高系统的可用性和容错性。 -
数据可靠性
MongoDB 复制集可以保证数据在多个节点上的冗余备份,可以有效地避免数据丢失或损坏的情况。当主节点写入数据时,备份节点会自动复制数据,确保数据在多个节点上的一致性。在节点宕机或网络异常时,系统会自动进行故障转移,确保数据的可靠性。 -
数据读写分离
在 MongoDB 复制集中,备份节点可以承担读取数据的任务,减轻了主节点的压力,提高了系统的并发读取能力。备份节点也可以用来支持查询操作,从而实现数据读写分离,进一步提高系统的性能和可用性。 -
数据灾备
MongoDB 复制集可以实现跨地理位置的数据灾备,将数据备份到不同的地理位置上,以防止因为自然灾害等因素导致的数据损失或不可用。同时,在跨地理位置备份数据时,也需要考虑网络延迟和带宽限制等问题。
MongoDB 复制集可以提高系统的可用性、容错性、性能和可靠性,对于需要高可用性、数据可靠性和数据读写分离的应用场景特别适用。
2. 注意事项
在搭建 MongoDB 复制集时,需要注意以下事项:
2.1 软件
-
版本:选择稳定版本的 MongoDB,并且各个节点的版本需要保持一致。
-
配置文件:在启动 MongoDB 节点时,需要指定相应的配置文件,包括端口号、数据目录、日志文件等信息。
-
安全认证:建议启用 MongoDB 的安全认证功能,以保护数据的安全性。
-
监控:建议使用监控工具对 MongoDB 进行监控和性能优化,以及及时发现和解决问题。
2.2 硬件
-
CPU:选择高性能的多核心 CPU,以提高系统的吞吐量和响应速度。
-
内存:分配足够的内存到数据库缓存和操作系统缓存中,以提高数据读写的性能。
-
存储:使用固态硬盘,并使用 RAID 技术进行数据冗余备份,以提高数据的可靠性。
-
网络:选择高速、稳定的网络设备和服务商,以确保数据同步的效率和可靠性。
在搭建 MongoDB 复制集时,需要将各个节点分配到不同的物理机器上,以提高系统的可用性和可靠性。此外,还需要进行节点间的网络配置(能同个局域网性能最佳)和通信测试,以确保数据同步的正确性和效率。同时,还需要定期进行备份和恢复测试,以确保数据的可靠性和完整性。
3. 准备工作
3.1 版本说明
系统版本:centos7.6
mongodb版本:6.0.5
客户端版本:1.8.2
搭建前最好先熟悉单机的安装,可以参考MongoDB安装与配置完全指南
注意:复制集中各节点的软件版本必须保持一致,以免出现不可预知的问题
3.2 系统要求
SELinux是一种安全增强的Linux内核模块,用于保护系统安全和完整性。但在某些情况下,它可能会影响MongoDB的正常运行,因此有时需要关闭SELinux。
3.2.1 检查SELinux状态
如果SELinux状态为enabled,则表示SELinux处于开启状态。
[root@localhost ~]# sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
Policy MLS status: enabled
Policy deny_unknown status: allowed
Max kernel policy version: 31
[root@localhost ~]#
3.2.2 临时关闭SELinux
如果 SELinux 处于 Permissive 模式,那么即使将其设置为 0,它也只会输出警告信息而不会阻止任何操作。所以临时关闭时不行的,只能执行3.2.3的永久关闭SELinux
[root@localhost ~]# setenforce 0
[root@localhost ~]# getenforce
Permissive
3.2.3 永久关闭SELinux
修改配置文件/etc/selinux/config。使用文本编辑器打开该文件,将SELINUX的值设置为disabled:
# This file controls the state of SELinux on the system.
# SELINUX= can take one of these three values:
# enforcing - SELinux security policy is enforced.
# permissive - SELinux prints warnings instead of enforcing.
# disabled - No SELinux policy is loaded.
#SELINUX=enforcing
SELINUX=disabled
# SELINUXTYPE= can take one of three values:
# targeted - Targeted processes are protected,
# minimum - Modification of targeted policy. Only selected processes are protected.
# mls - Multi Level Security protection.
SELINUXTYPE=targeted
保存退出,然后重启系统
重启后查询状态
[root@localhost ~]# sestatus
SELinux status: disabled
3.3 创建节点
3.3.1 我的目录
client:mongodb的客户端
server:mongodb的服务端
standalone:单节点模式的配置和数据文件
[root@localhost mongodb]# ll
total 0
drwxr-xr-x. 3 1000 1000 130 Apr 27 18:06 client
drwxr-xr-x. 3 root root 100 May 11 21:35 server
drwxr-xr-x 4 root root 48 May 16 00:10 standalone
[root@localhost mongodb]#
3.3.2 配置环境变量
配置环境变量会方便我们使用mongod
的命令,也可以不用配置,如果不配置就使用全路径操作
执行 vi /etc/profile
,我的服务文件路径是:/usr/local/mongodb/server,所以在文件的末尾添加
export MONGODB_HOME=/usr/local/mongodb/server
PATH=$PATH:$MONGODB_HOME/bin
添加完成后执行source /etc/profile
是配置立马生效,配置完成后,就可以使用mongod
替代/usr/local/mongodb/server/bin/mongod
3.3.3 创建节点的目录
在mongodb的目录下创建复制集的节点1目录
[root@localhost mongodb]# mkdir -p rs/rs_1/{data,logs}
[root@localhost mongodb]# ll
total 0
drwxr-xr-x. 3 1000 1000 130 Apr 27 18:06 client
drwxr-xr-x 3 root root 18 May 16 00:21 rs
drwxr-xr-x. 3 root root 100 May 11 21:35 server
drwxr-xr-x 4 root root 48 May 16 00:10 standalone
3.3.4 创建配置文件
在rs/rs_1目录下创建文件 mongo.conf
[root@localhost rs_1]# vi mongo.conf
文件输入内容
systemLog:
destination: file
path: /usr/local/mongodb/rs/rs_1/logs/mongod.log # 日志路径,这里设置相对于当前文件的路径,也可以使用绝对路径
logAppend: true
storage:
dbPath: /usr/local/mongodb/rs/rs_1/data # 数据目录
engine: wiredTiger #存储引擎
journal: #是否启用journal日志
enabled: true
net:
bindIp: 0.0.0.0
port: 28001 # port
replication:
replSetName: test #副本集的名称
processManagement:
fork: true
security:
authorization: disabled #是否开启安全校验,默认不开启,可填disabled(不开启)/enabled (开启)
保存后退出,现在节点1已经配置完成了
3.3.5 创建其他节点
执行cp -r rs_1/ rs_2
复制rs/rs_1整个目录为 rs_2,修改配置文件mongo.conf的端口为28002,同时修改数据目录和日志目录,完整的配置文件内容
systemLog:
destination: file
path: /usr/local/mongodb/rs/rs_2/logs/mongod.log # 日志路径,这里设置相对于当前文件的路径,也可以使用绝对路径
logAppend: true
storage:
dbPath: /usr/local/mongodb/rs/rs_2/data # 数据目录
engine: wiredTiger #存储引擎
journal: #是否启用journal日志
enabled: true
net:
bindIp: 0.0.0.0
port: 28002 # port
replication:
replSetName: test #副本集的名称
processManagement:
fork: true
security:
authorization: disabled #是否开启安全校验,默认不开启,可填disabled(不开启)/enabled (开启)
参照此方式创建出节点rs_3,端口修改为28003,数据目录改为rs_3
最后总结一下三个节点的信息
rs_1:端口 28001 目录 /usr/local/mongodb/rs/rs_1
rs_2:端口 28002 目录 /usr/local/mongodb/rs/rs_2
rs_3:端口 28003 目录 /usr/local/mongodb/rs/rs_3
4. 构建 MongoDB 复制集群
通过前面我们已经把三个节点需要的目录和配置文件都已经准备好了,跟普通节点一样分别启动
mongod -f /usr/local/mongodb/rs/rs_1/mongo.conf
mongod -f /usr/local/mongodb/rs/rs_2/mongo.conf
mongod -f /usr/local/mongodb/rs/rs_3/mongo.conf
启动成功会显示
[root@localhost ~]# mongod -f /usr/local/mongodb/rs/rs_1/mongo.conf
about to fork child process, waiting until server is ready for connections.
forked process: 7330
child process started successfully, parent exiting
[root@localhost ~]# mongod -f /usr/local/mongodb/rs/rs_2/mongo.conf
about to fork child process, waiting until server is ready for connections.
forked process: 7412
child process started successfully, parent exiting
[root@localhost ~]# mongod -f /usr/local/mongodb/rs/rs_3/mongo.conf
about to fork child process, waiting until server is ready for connections.
forked process: 7478
child process started successfully, parent exiting
也可以通过查询进程查看各节点是否启动成功
[root@localhost ~]# ps -ef|grep mongod
root 6870 1 1 21:37 ? 00:00:01 /usr/local/mongodb/server/bin/mongod -f /usr/local/mongodb/standalone/mongo.conf
root 7330 1 2 21:37 ? 00:00:02 mongod -f /usr/local/mongodb/rs/rs_1/mongo.conf
root 7412 1 3 21:38 ? 00:00:03 mongod -f /usr/local/mongodb/rs/rs_2/mongo.conf
root 7478 1 4 21:38 ? 00:00:03 mongod -f /usr/local/mongodb/rs/rs_3/mongo.conf
root 7609 7307 0 21:39 pts/0 00:00:00 grep --color=auto mongod
4.1 初始化复制集
先使用客户端登陆其中一台,我就先登陆28001
/usr/local/mongodb/client/bin/mongosh --port 28001
登陆后需要初始化集群,初始化集群的语法
rs.initiate({
_id: "集群的名字",
members: [
{_id: {节点A的id}, host: "节点A的地址:端口"},
{_id: {节点B的id}, host: "节点B的地址:端口"}
.....
]
})
根据语法构建我们的集群节点信息:
rs.initiate({
_id: "test",
members: [
{_id: 1, host: "localhost:28001"},
{_id: 2, host: "localhost:28002"},
{_id: 3, host: "localhost:28003"},
]
})
在客户端里执行
test> rs.initiate({
... _id: "test",
... members: [
... {_id: 1, host: "localhost:28001"},
... {_id: 2, host: "localhost:28002"},
... {_id: 3, host: "localhost:28003"},
... ]
... })
{ ok: 1 }
4.2 查看复制集状态
执行rs.status()
查看集群信息,经过4.1初始化集群后,底层会经过raft算法选出主节点,这个过程需要时间,如果打印出下面的信息则表示集群已经选举出主节点,
set:就是我们设置的集群名称test
members:是表示该集群拥有的所有节点信息
stateStr:是节点的类型,PRIMARY是主节点,SECONDARY是备份节点,所以可以看出来我们的主节点是rs_1,备份节点是rs_2和rs_3
注意:想执行该menbers的属性请移步到 4.4
test [direct: primary] test> rs.status()
{
set: 'test',
...
members: [
{
_id: 1,
name: 'localhost:28001',
health: 1,
state: 1,
stateStr: 'PRIMARY',
uptime: 1019,
optime: { ts: Timestamp({ t: 1684418091, i: 1 }), t: Long("1") },
optimeDate: ISODate("2023-05-18T13:54:51.000Z"),
lastAppliedWallTime: ISODate("2023-05-18T13:54:51.637Z"),
lastDurableWallTime: ISODate("2023-05-18T13:54:51.637Z"),
syncSourceHost: '',
syncSourceId: -1,
infoMessage: '',
electionTime: Timestamp({ t: 1684417851, i: 1 }),
electionDate: ISODate("2023-05-18T13:50:51.000Z"),
configVersion: 1,
configTerm: 1,
self: true,
lastHeartbeatMessage: ''
},
{
_id: 2,
name: 'localhost:28002',
health: 1,
state: 2,
stateStr: 'SECONDARY',
uptime: 258,
optime: { ts: Timestamp({ t: 1684418091, i: 1 }), t: Long("1") },
optimeDurable: { ts: Timestamp({ t: 1684418091, i: 1 }), t: Long("1") },
optimeDate: ISODate("2023-05-18T13:54:51.000Z"),
optimeDurableDate: ISODate("2023-05-18T13:54:51.000Z"),
lastAppliedWallTime: ISODate("2023-05-18T13:54:51.637Z"),
lastDurableWallTime: ISODate("2023-05-18T13:54:51.637Z"),
lastHeartbeat: ISODate("2023-05-18T13:54:57.726Z"),
lastHeartbeatRecv: ISODate("2023-05-18T13:54:57.221Z"),
pingMs: Long("0"),
lastHeartbeatMessage: '',
syncSourceHost: 'localhost:28001',
syncSourceId: 1,
infoMessage: '',
configVersion: 1,
configTerm: 1
},
{
_id: 3,
name: 'localhost:28003',
health: 1,
state: 2,
stateStr: 'SECONDARY',
uptime: 258,
optime: { ts: Timestamp({ t: 1684418091, i: 1 }), t: Long("1") },
optimeDurable: { ts: Timestamp({ t: 1684418091, i: 1 }), t: Long("1") },
optimeDate: ISODate("2023-05-18T13:54:51.000Z"),
optimeDurableDate: ISODate("2023-05-18T13:54:51.000Z"),
lastAppliedWallTime: ISODate("2023-05-18T13:54:51.637Z"),
lastDurableWallTime: ISODate("2023-05-18T13:54:51.637Z"),
lastHeartbeat: ISODate("2023-05-18T13:54:57.726Z"),
lastHeartbeatRecv: ISODate("2023-05-18T13:54:57.190Z"),
pingMs: Long("0"),
lastHeartbeatMessage: '',
syncSourceHost: 'localhost:28001',
syncSourceId: 1,
infoMessage: '',
configVersion: 1,
configTerm: 1
}
],
...
}
4.3 主节点添加数据
很幸运,我们当前登陆的节点(rs_1)是主节点,如果不是的就需要切换到对应的主节点,在主节点添加一行记录
test [direct: primary] test> show dbs
admin 80.00 KiB
config 160.00 KiB
local 436.00 KiB
test [direct: primary] test> use rs
switched to db rs
test [direct: primary] rs> db.user.insertOne({name:"Tom"})
{
acknowledged: true,
insertedId: ObjectId("64662f80d455144ad8ec8620")
}
4.3 初始化备份节点
主节点是rs_1,备份节点是rs_2和rs_3,我们先登陆rs_2,执行rs.isMaster()
查看集群信息
[root@localhost ~]# /usr/local/mongodb/client/bin/mongosh --port 28002
...
test [direct: secondary] test> rs.isMaster()
{
topologyVersion: {
processId: ObjectId("64662a4edbddbfe2e5df4f0c"),
counter: Long("4")
},
hosts: [ 'localhost:28001', 'localhost:28002', 'localhost:28003' ],
setName: 'test',
setVersion: 1,
ismaster: false,
secondary: true,
primary: 'localhost:28001',
me: 'localhost:28002',
...
}
在这个方法返回的数据中,也显示了我们所有节点的host地址信息和当前节点的角色和主节点是哪个,我们查询刚刚主节点添加的数据
test [direct: secondary] test> show dbs
admin 80.00 KiB
config 224.00 KiB
local 452.00 KiB
rs 40.00 KiB
test [direct: secondary] test> use rs
switched to db rs
test [direct: secondary] rs> show tables
user
test [direct: secondary] rs> db.user.find()
MongoServerError: not primary and secondaryOk=false - consider using db.getMongo().setReadPref() or readPreference in the connection string
test [direct: secondary] rs>
可以看到user
表是存在的,但是不给我们查询,提示我们要执行设置模式方法,旧的版本可以直接执行rs.secondaryOk()
,但是新的服务端版本推荐使用db.getMongo().setReadPref("secondary")
test [direct: secondary] rs> db.getMongo().setReadPref("secondary")
test [direct: secondary] rs> db.user.find()
[ { _id: ObjectId("64662f80d455144ad8ec8620"), name: 'Tom' } ]
执行db.getMongo().setReadPref("secondary")
或者rs.secondaryOk()
它会启用该节点的读操作,使其可以接收来自应用程序的读请求。这个方法并不会触发具体的底层操作,它只是告诉MongoDB该节点允许在副本集中扮演辅助角色,并接受读取操作。
参照设置方法,登陆到rs_3进行同样的配置
4.4 节点的属性
在使用rs.status()
集群命令返回的信息里,members 元素是一个数组,用于定义复制集中的节点信息。每个 members 元素都包含一组属性,用于配置节点的角色、主机和端口、优先级、投票权重等。下面是 members 元素内常用的属性及其含义:
-
_id: 每个节点在复制集中的唯一标识符。这个值是一个整数,并且必须在复制集中是唯一的。
-
host: 节点的主机名或 IP 地址,用于指定节点的位置。
-
port: 节点的端口号,用于指定节点的连接端口。
-
priority: 优先级,指定节点在选举中的优先级。值较大的节点在选举中具有更高的优先级。默认情况下,Primary 节点的优先级为 1,Secondary 节点的优先级为 0。
-
votes: 投票权重,用于决定选举结果。每个节点都有一个投票权重,默认为 1。值较大的节点在选举中具有更大的投票权重。
-
arbiterOnly: 指定节点是否为仲裁节点(Arbiter)。设置为 true 表示该节点是一个仲裁节点,它不存储数据,只参与选举投票。
-
buildIndexes: 指定节点是否在复制数据时构建索引。设置为 true 表示该节点会在复制数据时构建索引,以提高复制集的性能。
-
hidden: 指定节点是否为隐藏节点。设置为 true 表示该节点是一个隐藏节点,不会出现在复制集的成员列表中。
-
slaveDelay: 如果节点是延迟从节点(Delayed Secondary),可以设置延迟的时间(以秒为单位)。该节点会延迟一段时间后才能复制来自 Primary 节点的操作。
-
tags: 标签,用于将节点分组。可以使用标签来控制读操作的路由,以实现数据的本地化或分区。
4.4.1 修改使用中的配置
如果已经启动或者已经存在的配置需要修改,可以参考该模板:
# 获取配置属性
cfg = rs.conf()
# 将menbers的第一个元素的priority 设置为0
cfg.members[0].priority = 0
# 将menbers的第二个元素的hidden 设置为true
cfg.members[1].hidden = true
# 调用rs提供的刷新命令让配置生效
rs.reconfig(cfg)
mongodb提供的客户端时支持js语法的,所以修改配置思路实质上也就是使用js的思路。在后面的实践中会有使用例子。
4.5 节点的角色
在 MongoDB 副本集中,节点可以具有以下角色:
-
Primary(主节点):主节点是副本集的主要节点,负责处理所有的写入操作,并将写入操作的结果复制到副本集中的其他节点。主节点还负责处理客户端的读取请求,提供实时数据。
-
Secondary(从节点):从节点是副本集中的备用节点,通过复制来保持与主节点的数据同步。从节点从主节点复制数据并应用写入操作,以保持数据一致性。从节点可以处理客户端的读取请求,提供数据的可扩展性和高可用性。
-
Hidden(隐藏节点):隐藏节点是从节点的一种特殊类型,它不会出现在副本集的成员列表中。隐藏节点用于执行特定任务,例如备份或报表生成,而不会干扰副本集的正常操作。隐藏节点可以处理客户端的读取请求,但不会被选为新的主节点。
-
Delayed Secondary(延迟从节点):延迟从节点是从节点的一种特殊类型,它具有一定的数据复制延迟。延迟从节点会延迟一段时间后才复制主节点的操作,这可以用于创建数据恢复点或执行延迟任务。
-
Arbiter(仲裁节点):仲裁节点是一种特殊类型的节点,它不存储数据,只参与选举过程。仲裁节点用于帮助解决副本集中的选举问题,确保副本集的高可用性和数据一致性。
-
Priority 0(优先级为0):优先级为 0 的节点是从节点,但不会被选为新的主节点。这样的节点在副本集中存在,但不会影响选举过程。
-
Priority 1+(优先级为1+):优先级为 1 或更高的节点是从节点,并且可以成为新的主节点。在选举中,优先级较高的节点具有更高的优先级,更有可能成为新的主节点。
需注意的时副本集中的节点角色是根据节点的配置和运行状态来确定的。主节点和从节点的角色是动态的,可以根据选举过程和节点状态的变化而改变。仲裁节点、隐藏节点和延迟从。在后面的实践中会有使用例子。
5. MongoDB 复制集的高可用性实践
5.1 写数据与读数据
在MongoDB的副本集中,通常有一个Primary节点(主节点)和多个Secondary节点(辅助节点)。Primary节点负责处理写操作,并将写操作的结果复制到Secondary节点上。Secondary节点则复制Primary节点的数据,并且默认情况下不会接收来自应用程序的读请求。
我们尝试在rs_2(副本节点)写入数据时会给我们抛出一个不是主节点的错误
[ { _id: ObjectId("64662f80d455144ad8ec8620"), name: 'Tom' } ]
test [direct: secondary] rs> db.user.insertOne({name:"Jerry"})
MongoServerError: not primary
test [direct: secondary] rs>
回到主节点rs_1写数据,没有意外就写入了
test [direct: primary] rs> db.user.insertOne({name:"Jerry"})
{
acknowledged: true,
insertedId: ObjectId("646632d2d455144ad8ec8621")
}
回到复制节点读取数据,可以读取到主节点写入的数据
test [direct: secondary] rs> db.user.find()
[
{ _id: ObjectId("64662f80d455144ad8ec8620"), name: 'Tom' },
{ _id: ObjectId("646632d2d455144ad8ec8621"), name: 'Jerry' }
]
test [direct: secondary] rs>
5.2 安全机制
5.2.1 基本概念
安全机制是指在 MongoDB 复制集环境中实施的一系列安全措施,以确保数据的机密性、完整性和可用性。以下是复制集安全机制的关键要点:
-
认证和授权:MongoDB 支持用户名和密码的身份验证机制。通过为用户分配角色和权限,可以限制对复制集中数据库的访问。只有经过授权的用户才能连接到复制集并执行操作。
-
数据传输加密:通过使用 TLS/SSL 协议对复制集成员之间的通信进行加密,可以保护数据在传输过程中的安全性。这样可以防止敏感数据在网络上被拦截或篡改。
-
数据库访问控制:通过设置角色和权限,可以限制用户对复制集中数据库的操作。管理员可以根据需要授予用户读取、写入、管理集合或索引等特定权限,并防止未经授权的用户执行敏感操作。
-
安全审计:MongoDB 提供了审计功能,记录和跟踪复制集中的关键事件,如用户登录、命令执行、数据更改等。审计日志可以用于监测异常活动、识别安全威胁和进行故障排除。
-
网络隔离:将复制集成员部署在受信任的网络环境中,如虚拟专用网络(VPC)或受限网络中,可以减少受到未经授权访问的风险。
通过综合应用这些安全机制,MongoDB 复制集可以提供一定级别的数据安全和保护,确保数据在复制集中的存储、传输和访问过程中受到适当的保护。为了确保最佳的安全性,建议根据实际需求和最佳实践来配置和调整安全设置。
5.2.2 常用的安全配置
通过5.2.1的说明,最佳实践一般只要做到三个点即可
- 配置服务的ip访问白名单
- 节点之前的安全通信
- 客户端的安全认证
第一点般都是由运维人员进行配置实现,我们这里就从第二和第三点来实践
5.2.3 客户端的安全认证
注意:5.2.3和5.2.4配置顺序不能乱,因为配置5.2.4是默认开启客户端认证的
先登陆主节点,创建一个用于登陆操作集群的用户rs
db.createUser( {
user: "rs",
pwd: "rs",
roles: [ { role: "clusterAdmin", db: "admin" } ,
{ role: "userAdminAnyDatabase", db: "admin"},
{ role: "userAdminAnyDatabase", db: "admin"},
{ role: "readWriteAnyDatabase", db: "admin"}]
})
执行结果
test [direct: primary] test> use admin
switched to db admin
test [direct: primary] admin> db.createUser( {
... user: "rs",
... pwd: "rs",
... roles: [ { role: "clusterAdmin", db: "admin" } ,
... { role: "userAdminAnyDatabase", db: "admin"},
... { role: "userAdminAnyDatabase", db: "admin"},
... { role: "readWriteAnyDatabase", db: "admin"}]
... })
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1684424590, i: 4 }),
signature: {
hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
keyId: Long("0")
}
},
operationTime: Timestamp({ t: 1684424590, i: 4 })
}
也可以创建一个root
角色,这样就可以拥有root的所有权限
db.createUser( {
user: "rs",
pwd: "rs",
roles: [{ role: "root", db: "admin" }]
})
查询用户,可以看到rs
已经创建成功
test [direct: primary] rs> use admin
switched to db admin
test [direct: primary] admin> show users
[
{
_id: 'admin.rs',
userId: new UUID("784042fa-f765-41bc-a91e-b5e081b15e04"),
user: 'rs',
db: 'admin',
roles: [
{ role: 'clusterAdmin', db: 'admin' },
{ role: 'userAdminAnyDatabase', db: 'admin' },
{ role: 'readWriteAnyDatabase', db: 'admin' }
],
mechanisms: [ 'SCRAM-SHA-1', 'SCRAM-SHA-256' ]
}
]
5.2.4节点间的安全通信
注意:5.2.3和5.2.4配置顺序不能乱,因为配置5.2.4是默认开启客户端认证的
关闭正在运行的复制集
,然后执行cd /usr/local/mongodb/rs/rs_1
进入到我们其中一个节点目录,创建一个key文件
[root@localhost ~]# cd /usr/local/mongodb/rs/rs_1
[root@localhost rs_1]# openssl rand -base64 756 > mongo.key
[root@localhost rs_1]# chmod 600 mongo.key
[root@localhost rs_1]#
将mongo.key
文件复制到其他节点目录中,然后关闭正在运行的mongod进程,如果不存在就执行启动命令
mongod -f /usr/local/mongodb/rs/rs_1/mongo.conf --keyFile /usr/local/mongodb/rs/rs_1/mongo.key
mongod -f /usr/local/mongodb/rs/rs_2/mongo.conf --keyFile /usr/local/mongodb/rs/rs_2/mongo.key
mongod -f /usr/local/mongodb/rs/rs_3/mongo.conf --keyFile /usr/local/mongodb/rs/rs_3/mongo.key
如果不想使用上面再末尾添加参数就修改mongo.conf
配置文件,再security
节点下添加对应的key文件
security:
keyFile: {keyfile的路径}
例如rs_1的配置是
security:
keyFile: /usr/local/mongodb/rs/rs_1/mongo.key
配置后的启动方式就不需要添加 --keyFile
参数,如果配置了keyFile找不到.key文件,启动会报错
[root@localhost rs_2]mongod -f /usr/local/mongodb/rs/rs_2/mongo.conf
about to fork child process, waiting until server is ready for connections.
forked process: 13268
ERROR: child process failed, exited with 1
To see additional information in this output, start without the "--fork" option.
5.3 添加新的节点
5.3.1 创建配置目录和配置文件
先进入rs
目录,执行mkdir -p rs_4/{data,logs}
,
[root@localhost ~]# cd /usr/local/mongodb/rs
[root@localhost rs]# mkdir -p rs_4/{data,logs}
[root@localhost rs]# ll rs_4
total 0
drwxr-xr-x 2 root root 6 May 18 23:58 data
drwxr-xr-x 2 root root 6 May 18 23:58 logs
[root@localhost rs]#
复制rs_1目录中的mongo.conf
和mongo.key
到rs_4中,修改rs_4的mongo.conf
配置文件,修改后的完整配置
systemLog:
destination: file
path: /usr/local/mongodb/rs/rs_4/logs/mongod.log # 日志路径,这里设置相对于当前文件的路径,也可以使用绝对路径
logAppend: true
storage:
dbPath: /usr/local/mongodb/rs/rs_4/data # 数据目录
engine: wiredTiger #存储引擎
journal: #是否启用journal日志
enabled: true
net:
bindIp: 0.0.0.0
port: 28004 # port
replication:
replSetName: test #副本集的名称
processManagement:
fork: true
security:
keyFile: /usr/local/mongodb/rs/rs_4/mongo.key
authorization: disabled #是否开启安全校验,默认不开启,可填disabled(不开启)/enabled (开启)
5.3.2 启动节点
执行启动rs_4的节点
[root@localhost ~]# mongod -f /usr/local/mongodb/rs/rs_4/mongo.conf
about to fork child process, waiting until server is ready for connections.
forked process: 16075
child process started successfully, parent exiting
[root@localhost ~]#
5.3.3 添加节点
添加节点的语法rs.add(hostport, arbiterOnly?)
:第一个参数数地址和端口,第二个是可选,如果设为true则该节点表示仲裁角色,不参与读写数据,只负责参与选举主节点
进入主节点的客户端,执行添加节点命令
test [direct: primary] admin> rs.add("127.0.0.1:28004")
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1684425967, i: 1 }),
signature: {
hash: Binary(Buffer.from("667e90765de0730dad66e0a6dfea63885fb40590", "hex"), 0),
keyId: Long("7234519582843600902")
}
},
operationTime: Timestamp({ t: 1684425967, i: 1 })
}
test [direct: primary] admin>
5.3.4 初始化新节点
登陆新的节点,带校验启动
[root@localhost ~]# /usr/local/mongodb/client/bin/mongosh --port 28004 -u rs -p rs --authenticationDatabase admin
Current Mongosh Log ID: 64664d4222b936e5a4226210
...
test [direct: secondary] test> db.getMongo().setReadPref("secondary")
执行后就已经加入了新节点,可以查看集群状态信息
test [direct: secondary] rs> rs.status()
{
set: 'test',
...
members: [
{
_id: 1,
...
},
{
_id: 2,
...
},
{
_id: 3,
...
},
{
_id: 4,
name: '127.0.0.1:28004',
health: 1,
state: 2,
stateStr: 'SECONDARY',
uptime: 485,
optime: { ts: Timestamp({ t: 1684426231, i: 1 }), t: Long("4") },
optimeDate: ISODate("2023-05-18T16:10:31.000Z"),
lastAppliedWallTime: ISODate("2023-05-18T16:10:31.782Z"),
lastDurableWallTime: ISODate("2023-05-18T16:10:31.782Z"),
syncSourceHost: 'localhost:28003',
syncSourceId: 3,
infoMessage: '',
configVersion: 3,
configTerm: 4,
self: true,
lastHeartbeatMessage: ''
}
],
...
}
使用新节点查询数据
test [direct: secondary] test> use rs
switched to db rs
test [direct: secondary] rs> db.user.find()
[
{ _id: ObjectId("64662f80d455144ad8ec8620"), name: 'Tom' },
{ _id: ObjectId("646632d2d455144ad8ec8621"), name: 'Jerry' }
]
至此,新节点已经成功加入集群
5.4 移除副本节点
移除节点有下面两种方式
- 通过
rs.status()
命令找到需要移除的节点的hostname属性,即域名加端口,然后执行rs.remove(hostname)
,
如:移除 127.0.0.1:28001 则执行(需要在可写的节点里执行)
test [direct: primary] rs> rs.remove("127.0.0.1:28004")
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1684568847, i: 1 }),
signature: {
hash: Binary(Buffer.from("088d4a90293b366b16d565ed09c14f7f59c3f7de", "hex"), 0),
keyId: Long("7234519582843600902")
}
},
operationTime: Timestamp({ t: 1684568847, i: 1 })
}
- 通过修改conf配置移除节点
如:移除members的第一个节点
test [direct: primary] rs> cfg = rs.conf()
...
test [direct: primary] rs> cfg.members.splice(0,1)
[
{
_id: 1,
host: 'localhost:28001',
arbiterOnly: false,
buildIndexes: true,
hidden: false,
priority: 1,
tags: {},
secondaryDelaySecs: Long("0"),
votes: 1
}
]
test [direct: primary] rs> rs.reconfig(cfg)
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1684569146, i: 1 }),
signature: {
hash: Binary(Buffer.from("2bab40af256b287fba36eeb4f48ff14f4486f4bd", "hex"), 0),
keyId: Long("7234519582843600902")
}
},
operationTime: Timestamp({ t: 1684569146, i: 1 })
}
splice(0,1)
是js的函数,可以移除数组下标从0开始的1个节点
5.5 添加仲裁节点
显示当前的节点,
test [direct: primary] rs> rs.status()
{
set: 'test',
...
members: [
{
_id: 2,
name: 'localhost:28002',
health: 1,
state: 1,
stateStr: 'PRIMARY',
uptime: 14528,
optime: { ts: Timestamp({ t: 1684569224, i: 1 }), t: Long("6") },
optimeDate: ISODate("2023-05-20T07:53:44.000Z"),
lastAppliedWallTime: ISODate("2023-05-20T07:53:44.749Z"),
lastDurableWallTime: ISODate("2023-05-20T07:53:44.749Z"),
syncSourceHost: '',
syncSourceId: -1,
infoMessage: '',
electionTime: Timestamp({ t: 1684554713, i: 1 }),
electionDate: ISODate("2023-05-20T03:51:53.000Z"),
configVersion: 5,
configTerm: 6,
self: true,
lastHeartbeatMessage: ''
},
{
_id: 3,
name: 'localhost:28003',
health: 1,
state: 2,
stateStr: 'SECONDARY',
uptime: 14520,
optime: { ts: Timestamp({ t: 1684569224, i: 1 }), t: Long("6") },
optimeDurable: { ts: Timestamp({ t: 1684569224, i: 1 }), t: Long("6") },
optimeDate: ISODate("2023-05-20T07:53:44.000Z"),
optimeDurableDate: ISODate("2023-05-20T07:53:44.000Z"),
lastAppliedWallTime: ISODate("2023-05-20T07:53:44.749Z"),
lastDurableWallTime: ISODate("2023-05-20T07:53:44.749Z"),
lastHeartbeat: ISODate("2023-05-20T07:53:48.082Z"),
lastHeartbeatRecv: ISODate("2023-05-20T07:53:48.082Z"),
pingMs: Long("0"),
lastHeartbeatMessage: '',
syncSourceHost: 'localhost:28002',
syncSourceId: 2,
infoMessage: '',
configVersion: 5,
configTerm: 6
}
]
...
复制一份我们的rs_1 ,复制后记得清理原来data和log目录内的文件
[root@localhost rs]# cp -r rs_1 arb
[root@localhost rs]# ll
total 0
drwxr-xr-x 4 root root 65 May 20 15:56 arb
drwxr-xr-x 4 root root 65 May 18 23:47 rs_1
drwxr-xr-x 4 root root 65 May 18 23:47 rs_2
drwxr-xr-x 4 root root 65 May 18 23:47 rs_3
drwxr-xr-x 4 root root 65 May 19 00:01 rs_4
[root@localhost rs]#
修改配置文件
systemLog:
destination: file
path: /usr/local/mongodb/rs/arb/logs/mongod.log # 日志路径,这里设置相对于当前文件的路径,也可以使用绝对路径
logAppend: true
storage:
dbPath: /usr/local/mongodb/rs/arb/data # 数据目录
engine: wiredTiger #存储引擎
journal: #是否启用journal日志
enabled: true
net:
bindIp: 0.0.0.0
port: 28005 # port
replication:
replSetName: test #副本集的名称
processManagement:
fork: true
security:
keyFile: /usr/local/mongodb/rs/arb/mongo.key
authorization: disabled #是否开启安全校验,默认不开启,可填disabled(不开启)/enabled (开启)
启动节点
mongod -f /usr/local/mongodb/rs/arb/mongo.conf
登陆集群的主节点,执行添加仲裁命令rs.addArb(hostname)
test [direct: primary] rs> rs.addArb("127.0.0.1:28005")
MongoServerError: 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.
执行报错,推荐我们使用setDefaultRWConcern
配置,通过查找官网文档找到一个执行的语法
db.adminCommand( {"setDefaultRWConcern" : 1, "defaultWriteConcern" : { "w" : “majority” } } )
执行过程
test [direct: primary] rs> db.adminCommand( {"setDefaultRWConcern" : 1, "defaultWriteConcern" : { "w" : "majority" } } )
{
defaultReadConcern: { level: 'local' },
defaultWriteConcern: { w: 'majority', wtimeout: 0 },
updateOpTime: Timestamp({ t: 1684571614, i: 1 }),
updateWallClockTime: ISODate("2023-05-20T08:33:35.147Z"),
defaultWriteConcernSource: 'global',
defaultReadConcernSource: 'implicit',
localUpdateWallClockTime: ISODate("2023-05-20T08:33:35.152Z"),
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1684571615, i: 2 }),
signature: {
hash: Binary(Buffer.from("0bc25b8ee44b8b69a9f0f03a516080c8d9f699d6", "hex"), 0),
keyId: Long("7234519582843600902")
}
},
operationTime: Timestamp({ t: 1684571615, i: 2 })
}
test [direct: primary] rs> rs.addArb("127.0.0.1:28005")
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1684571660, i: 1 }),
signature: {
hash: Binary(Buffer.from("e7188aea177a5918fab04f710bae23570cb0faf4", "hex"), 0),
keyId: Long("7234519582843600902")
}
},
operationTime: Timestamp({ t: 1684571660, i: 1 })
}
....
这是因为我们没有在连接客户端时配置读写关注属性,如果已经配置了就不需要执行改命令在添加仲裁节点
查看集群状态, stateStr: 'ARBITER',
信息看出我们的仲裁节点已经添加成功
test [direct: primary] rs> rs.status()
...
{
_id: 3,
name: '127.0.0.1:28005',
health: 1,
state: 7,
stateStr: 'ARBITER',
uptime: 8,
lastHeartbeat: ISODate("2023-05-20T09:04:20.368Z"),
lastHeartbeatRecv: ISODate("2023-05-20T09:04:20.367Z"),
pingMs: Long("3"),
lastHeartbeatMessage: '',
syncSourceHost: '',
syncSourceId: -1,
infoMessage: '',
configVersion: 2,
configTerm: 1
}`
5.6 主节点正确的转移
在 MongoDB 副本集中,当主节点不可用或需要维护时,可以手动或自动进行主节点转移。主节点转移是将副本集中的一个从节点(通常是最优的从节点)升级为新的主节点的过程。
虽然可以使用kill进程的方式强制剔除当前的主节点触发选举出新的主节点,但是不建议这样做,下面是在 MongoDB 副本集中进行主节点转移的正确步骤:
-
检查副本集状态:使用 rs.status() 命令来查看副本集的状态,并确定当前主节点的状态和可用性。
-
验证从节点状态:确保其他从节点处于健康状态,并具备成为主节点的条件。从节点应该处于同步状态,并且具备足够的复制进程。
-
选择新的主节点:从健康的从节点中选择一个作为新的主节点。可以考虑节点的复制延迟、网络延迟和硬件资源等因素进行选择。
-
执行主节点转移:使用
rs.stepDown()
命令在当前主节点上规定时间里(默认60秒)触发主节点转移。这将使当前主节点自愿放弃主节点身份,以便新的主节点可以接管。 -
监控状态变化:监视副本集状态变化,并确保新的主节点已成功选举并接管主节点角色。
5.6.1 主节点转移的作用
-
高可用性:在主节点不可用时,能够快速自动或手动选择一个从节点作为新的主节点,确保系统的持续可用性。
-
容错能力:如果主节点出现故障或需要维护,主节点转移可以保证副本集中的其他节点能够接管主节点的角色,避免服务中断。
-
负载均衡:通过选择最优的从节点作为新的主节点,可以根据节点的状态和资源情况实现负载均衡,提高系统性能和吞吐量。
-
数据一致性:主节点转移过程中,MongoDB会确保数据的一致性和可靠性。从节点会按照复制集协议进行数据同步,保证数据的完整性。
在进行主节点转移时,要确保副本集中的所有节点都能够正常工作,并且网络连接稳定。此外,主节点转移可能会在一定程度上影响系统的读写性能,因此在进行转移时应该进行充分的计划和测试。
6. MongoDB 复制集的其他特性和功能
6.1 选举机制
MongoDB 复制集的选举机制用于在主节点不可用时选择新的主节点,确保复制集的正常运行和数据的一致性。下面是复制集选举的详细过程:
-
成员状态检查:每个成员(节点)定期向其他成员发送心跳信号,以检查其状态。如果一个成员在一定时间内没有接收到来自主节点的心跳信号,它将认为主节点不可用,触发选举过程。
-
候选人状态:触发选举的成员将成为候选人,它会向其他成员发送选举请求,并等待其他成员的投票。候选人会维护一个计票箱以记录收到的选票。
-
投票过程:每个成员在收到选举请求后,会验证候选人的合法性,并投票给候选人。每个成员只能投一票,而且只能在一次选举中投给同一个候选人。
-
选票计数:候选人将统计收到的选票数量。选票计数遵循以下规则:
- 如果候选人收到了超过半数成员的选票,它将成为新的主节点。
- 如果没有候选人收到半数以上的选票,选举失败,复制集保持当前的主节点状态。
- 主节点选举结果:如果候选人成为新的主节点,它会发送选举结果给其他成员,通知它们主节点的变化。其他成员将更新自己的配置,并切换到新的主节点。
在选举过程中,每个成员都有一个优先级(priority)值
。优先级较高的成员在选举中具有更高的权重,更有可能成为主节点。优先级还影响复制集中的数据同步过程。
复制集选举的目标是选择一个稳定的主节点来确保数据的一致性和可靠性。选举机制保证了复制集在主节点故障或变更时能够快速地选择新的主节点,并且在选举过程中考虑了成员的优先级,以确保整个复制集的正常运行。
6.1.1 选举算法优化
MongoDB 的选举算法并没有直接采用 Raft 算法。MongoDB 复制集选举算法的设计与 Raft 算法有所不同,并且在实现上进行了一些优化。以下是一些 MongoDB 选举算法的优化方面:
-
快速选举:MongoDB 的选举算法旨在快速选举新的主节点以保证高可用性。它通过在选举过程中设置超时时间和投票轮数的方式来加速选举。如果选举进程超过一定时间或轮数仍未达到结果,选举会中止并重新开始。
-
优先级投票:每个成员在选举中都有一个优先级(priority)值。这个值可以通过配置进行调整,较高的优先级会提高成员成为主节点的机会。这种机制可以确保具有更高优先级的成员更有可能成为主节点,从而更好地利用资源。
-
多数票机制:为了确保选举结果的一致性,MongoDB 使用了多数票机制。在复制集中,成员的投票结果需要超过半数才能选举成功。这样可以防止出现分裂的情况,即多个成员同时宣布自己是主节点。
-
数据同步优化:选举过程中,新的主节点需要保证它的数据副本与其他成员的数据副本保持一致。为了减少数据同步的延迟和网络带宽的消耗,MongoDB 使用了增量同步的机制。主节点只需将未同步的操作日志(oplog)发送给其他成员,而不需要复制整个数据集。
虽然 MongoDB 的选举算法与 Raft 算法有所不同,但它们都旨在提供一致性和高可用性。MongoDB 的选举算法通过上述优化来确保选举的快速性和正确性,并且能够在复制集中进行高效的数据同步。
6.2 复制集是怎么处理故障转移
MongoDB 底层在处理故障转移时使用了以下机制:
-
心跳检测:MongoDB 复制集中的成员之间会定期发送心跳信号以检测彼此的可用性。每个成员会定期向其他成员发送心跳请求,并等待心跳响应。如果成员在一定时间内没有收到心跳响应,就会认为对方不可用,并将其标记为故障状态。
-
主节点故障检测:当一个成员检测到当前的主节点不可用时,它会尝试发起选举过程以选出新的主节点。选举过程包括成员之间的投票和候选人的竞选。成员会通过投票表达对候选人的支持,并根据多数票原则选出新的主节点。
-
数据同步和复制:一旦新的主节点选举完成,其他成员会与新的主节点进行数据同步。主节点会将操作日志(oplog)中的操作记录发送给其他成员,以确保数据的一致性。成员会按照主节点的顺序应用这些操作,以保持数据的同步。
-
故障转移完成:当新的主节点选举完成并且数据同步完成后,故障转移过程就完成了。此时,复制集中的所有成员都知道新的主节点,并且数据副本也与新的主节点保持同步。复制集将恢复到正常的工作状态,并可以继续处理客户端的读写请求。
MongoDB 通过上述机制实现了自动化的故障转移。一旦主节点发生故障,其他成员会迅速检测到,并自动选举出新的主节点
。数据同步机制确保了新的主节点和其他成员之间的数据一致性
。这样,即使在故障发生时,复制集仍能保持高可用性
和数据的可靠性
。
故障转移过程可能会导致一小段时间的服务中断或读写延迟,取决于数据同步的速度和网络延迟等因素。但 MongoDB 的故障转移机制被设计为尽可能快速和可靠,以最小化对应用程序的影响。
7. 总结
最后,我们来做一个总结,前面介绍了 MongoDB 复制集的概念、构建和管理步骤以及相关的注意事项。复制集由主节点和备份节点组成,提供高可用性和数据冗余。通过初始化复制集并设置主节点,我们可以构建复制集群并添加数据。还介绍了节点的属性和角色,以及复制集的高可用性实践,如安全机制、添加/移除节点和主节点转移。此外,还涵盖了选举机制和故障转移处理。希望通过本文,可以帮助你全面了解 MongoDB 复制集的配置和管理,以实现数据的可靠性和持续可用性。