前一部分的文章:
MongoDB 快速入门实战教程基础篇 一 :文档的 CR操作
MongoDB 快速入门实战教程基础篇 一 :文档的 UD操作
MongoDB 快速入门实战教程基础篇 二: 流式聚合操作
MongoDB 快速入门实战教程基础篇 三:执行计划与索引
MongoDB 快速入门实战教程进阶篇 一:数据模型
进阶篇 二 提高数据服务可用性的复制集
MongoDB 中的复制指的是将数据同步在多个服务器。复制操作将会在多个服务器上建立数据副本,这些副本的集合称为复制集,它们存储的内容与主服务器上的内容一致。建立了复制集之后,就可以在主服务器出现故障或无法连接的情况下保证数据服务可用。
复制集成员
MongoDB 的复制集可以支持多个节点,但要求至少有两个节点。任何情况下都只有一个主节点(即主服务器),它负责处理客户端发出的命令。除主节点之外的所有节点都称为从节点,它们会主动同步主节点中的数据。在 MongoDB 中,主节点称为 Primary,从节点称为 Secondarie。
主节点 Primary
主节点是复制集中唯一一个可以接收写操作的成员。MongoDB 在主节点上执行写操作,并将操作记录在主节点的 oplog 中,从节点会复制主节点的操作记录,然后执行相同操作以实现数据同步的目的。下图描述了由三个成员组成的复制集的成员关系:这个复制集中有两个从节点和一个主节点。主节点接收客户端发起的写操作,从节点通过 oplog 实现数据同步。要注意的是,复制集中的所有从节点都可以接收读操作,但默认情况下读取操作依然是交给主节点。如果想要更改读取规则,可以查阅官方文档 Read Preference。要注意的是,每个复制集最多可拥有 50 名成员。
从节点 Secondary
从节点中存储的是主节点的数据副本。从节点将主节点中的 oplog 操作应用于自身,从而实现数据同步。要注意的是,这个“数据同步”的操作是异步进行的。下图描述了由三个成员组成的复制集的数据同步关系:子节点会同步主节点上的操作,以实现数据同步。另外,各节点之间通过心跳(Heartbeat)来判断是否可用,假如某个节点在 10 秒内没有相应其他节点发出的 Heartbeat,那么它将会被标记为“掉线”,意为不可用或不可访问。
复制的基石—操作日志
上面提到,从节点的数据同步操作其实是执行主节点中执行过的操作。所有从节点都会拷贝主节点上的 local.oplog.rs
文件,即 oplog。oplog 记录主节点中的改动操作,但不记录读取操作。oplog 是一个特殊的上限集合,它支持基于顺序插入和检索文档的高吞吐操作。上限集合的大小是固定的,在达到最大记录数之后,如果再有新的记录传入,它会覆盖掉最早的记录。
从 MongoDB4.0 开始,我们可以使用 oplogSizeMB
在创建时设置 oplog 的大小,或者使用 replSetResizeOplog
使其能够突破上限集合的限制。假设我们想要将 oplog 的大小设置为 16000
兆字节,对应命令如下:
db.adminCommand({replSetResizeOplog: 1, size: 16000})
当第一次启用复制集,且未指定 oplog 大小时,MongoDB 将会创建一个默认大小的 oplog。oplog 的大小与操作系统和存储引擎相关,以下默认大小规则适用于类 Unix 操作系统和 Windows 操作系统:
存储引擎 | 默认的 oplog 大小 | 下限 | 上限 |
---|---|---|---|
内存存储引擎 | 物理内存的 5% | 50 MB | 50 GB |
WiredTiger 存储引擎 | 可用磁盘空间的 5% | 990 MB | 50 GB |
MMAPv1 存储引擎 | 可用磁盘空间的 5% | 990 MB | 50 GB |
对于 64 位的 macOS,oplog 的大小是 192M 的物理内存或磁盘空间,上述三种存储引擎的默认值均相同。
主节点故障的应对机制
在主节点出现故障时,整个系统应该有一个应对机制。MongoDB 为复制集提供了主节点选举和数据回滚来确保数据服务可用和避免数据丢失。
主节点选举
当主节点被标记为“掉线”,那么就意味着复制集需要一个新的主节点,否则将会导致服务不可用。这种情况下,复制集通过选举的方式来确定哪个成员会成为主节点。除了被标记为“掉线”之外,会触发选举的情况还有以下几种:
- 复制集中添加了新的节点;
- 初始化复制集;
- 使用
rs.stepDown()
或者rs.reconfig()
等方法维护复制集。
绿色背景的节点表示节点可用,灰色背景表示节点“掉线”。votes 代表票权,对应的数值代表初始票数。如果投票成员数量为偶数,就有可能会造成多个节点的票数相同,甚至陷入无限选举的泥潭。为了避免这种情况,我们就需要增加一个仲裁(Arbiter)节点。仲裁节点拥有投票权,但它没有存储数据副本,也不能成为主节点。新增仲裁节点后,票数就会从偶数变成奇数。
默认情况下,从标记主节点“掉线”到选举出新的主节点的时间不会超过 12 秒,但 MongoDB 也提供了可修改的配置来调整该时间。
数据回滚
当主节点发生故障,并选举出新的主节点时,MongoDB 将会在之前的主节点上执行回滚写操作。当“掉线”的主节点重新连接时,它将会以从节点的身份加入到复制集中,并回滚写操作,以便与其他成员的数据保持一致。MongoDB 4.0 版本对数据回滚进行了一些调整:
- 回滚操作会在后台索引构建完成后进行;
- 不限制回滚的数据量;
- 回滚时间默认为 24 小时,且可以配置。
4.0 版本之前,回滚的最大数据量为 300 兆字节,超过上限的数据量需要进行手动干预;回滚时间默认为 30 分钟,且不可配置。
复制集部署实践
我们将介绍由三个成员组成的复制集部署过程,本次部署演示使用 MongoDB 官方的 Docker 镜像,并且不启用访问控制。如果想要了解有访问控制的复制集部署知识,可查阅官方文档 Deploy New Replica Set With Keyfile Access Control。
注意:本次部署演示将在同一台计算机上启动多个 Docker 镜像,但实际工作中则是在多台不同的云服务器上部署 MongoDB。
部署复制集
在开始学习本节之前,请确保按照附章 Docker 官方文档 的指引安装 Docker。
首先,我们需要从 DockerHub 中拉取 MongoDB 官方提供的 mongo 服务镜像。在版本选择方面,我们选择最新版,即 latest
。对应命令如下:
$ docker pull mongo:latest
将镜像拉取到本地后,分别使用 run
命令启动三个容器。启动时,我们需要为容器指定名称,以便后期使用,同时还需要指定复制集的名称,并设置容器的 bind_ip
。对应命令如下:
$ docker run --name mongoFir -d mongo:latest --replSet "mongoRepas" --bind_ip_all
$ docker run --name mongoSec -d mongo:latest --replSet "mongoRepas" --bind_ip_all
$ docker run --name mongoThr -d mongo:latest --replSet "mongoRepas" --bind_ip_all
这里将容器分别命名为 mongoFir
、mongoSec
和 mongoThr
,复制集的名称指定为 mongoRepas
,并解除 mongo 对于bind_ip
的限制。由于在启动时未绑定 IP,所以我们需要使用 grep
命令找到每个容器对应的 IP。对应命令如下:
$ docker inspect mongoFir | grep IPAddress
命令执行后,终端返回信息如下:
"SecondaryIPAddresses": null,
"IPAddress": "172.17.0.2",
"IPAddress": "172.17.0.2"
返回结果说明容器 mongoFir
绑定的 IP 为 172.17.0.2
,mongo 服务的默认端口为 27017,所以名为 mongoFir
的容器中的 mongo 服务完整地址为 172.17.0.2:27017
。接着依次查找容器 mongoSec
和 mongoThr
对应的 mongo 服务地址。最终,三个容器对应的 mongo 服务地址依次如下:
172.17.0.2:27017
172.17.0.3:27017
172.17.0.4:27017
容器启动成功后,就可以开始初始化复制集的工作了。首先,连接任意一个容器的 MongoShell,例如容器 mongoFir
。对应命令如下:
$ docker exec -it mongoFir mongo
命令执行后,就会连接上容器 mongoFir
的 MongoShell。然后在 MongoShell 中执行复制集初始化的命令:
> rs.initiate({
_id: "mongoRepas",
members:[
{_id: 0, host: "172.17.0.2"},
{_id: 1, host: "172.17.0.3"},
{_id: 2, host: "172.17.0.4"}
]
})
在初始化复制集的时候指定了复制集的名称,并制定了成员的 _id
和对应的 IP 地址。命令执行后,MongoShell 输出如下文档:
{
"ok" : 1,
"operationTime" : Timestamp(1564287051, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1564287051, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
返回结果中的 ok: 1
代表复制集初始化成功。此时,MongoShell 的命令行标识符从 >
变为 mongoRepas:SECONDARY>
,即复制集的 Shell。在复制集 Shell 中使用 rs.status()
命令查看当前复制集的状态信息,命令执行后输出如下内容: