本教程的早期版本由 Justin Ellingwood 编写。
介绍
MongoDB 是许多现代 Web 应用程序中使用的面向文档的数据库。 它被归类为 NoSQL 数据库,因为它不依赖于传统的基于表的关系数据库结构。 相反,它使用具有动态模式的类似 JSON 的文档。 这意味着,与 关系数据库 不同,MongoDB 在将数据添加到数据库之前不需要预定义模式。
使用数据库时,拥有多个数据副本通常很有用。 这在其中一个数据库服务器发生故障时提供了冗余,并且可以提高数据库的可用性和可伸缩性,并减少读取延迟。 跨多个独立数据库同步数据的做法称为 复制 。 在 MongoDB 中,通过复制维护相同数据集的一组服务器称为 副本集 。
本教程简要概述了复制在 MongoDB 中的工作原理,并概述了如何配置和启动具有三个成员的副本集。 在此示例配置中,副本集的每个成员都是在不同的 Ubuntu 20.04 服务器上运行的不同 MongoDB 实例。
注意:请注意,本指南中概述的过程旨在演示如何快速设置和运行副本。 完成本教程后,您将拥有一个正常运行的副本集,但它不会启用任何安全功能。 此设置 不推荐用于生产环境 。
MongoDB 的社区版带有两种身份验证方法,可以帮助确保您的数据库安全, 密钥文件身份验证 和 x.509 身份验证。 对于使用复制的生产部署, MongoDB 文档建议 使用 x.509 身份验证,并将密钥文件描述为“最适合测试或开发环境的最低安全形式”。 但是,获取和配置 x.509 证书的过程伴随着一些必须根据具体情况做出的警告和决定,这超出了 DigitalOcean 教程的范围。
如果您打算使用您的副本集进行测试或开发,我们 强烈建议 您按照我们的 如何在 Ubuntu 20.04 上为 MongoDB 副本集配置密钥文件身份验证的教程。
先决条件
要完成本指南,您需要:
- 三台服务器,每台运行 Ubuntu 20.04。 所有这三个服务器都应该有一个非 root 管理用户和一个配置了 UFW 的防火墙。 要进行设置,请按照我们的 Ubuntu 20.04 初始服务器设置指南进行操作。
- MongoDB 安装在您的每台 Ubuntu 服务器上。 为此,请按照我们关于 如何在 Ubuntu 20.04 上安装 MongoDB 的教程进行操作,确保在所有三台服务器上完成每个步骤。
请注意,为清楚起见,本指南将三个服务器称为 mongo0、mongo1 和 mongo2。 任何显示在 mongo0 上执行的命令或文件更改的示例都将具有蓝色背景,如下所示:
在 mongo1 上执行的命令和文件更改将具有粉红色背景:
mongo2 上的示例操作将具有绿色背景:
最后,必须在 每个 服务器上运行的命令或必须进行的文件更改将具有标准的黑色背景,如下所示:
了解 MongoDB 副本集
正如介绍中提到的,MongoDB 通过一个名为 副本集 的实现来处理复制。 作为给定副本集一部分的每个运行的 MongoDB 实例都称为其 成员 之一。 每个副本集必须有一个 primary 成员和至少一个 secondary 成员。
主成员是与副本集进行事务的主要访问点,并且是唯一可以接受写操作的成员。 每个副本集一次只能有一个主成员,因为复制是通过复制主节点的 oplog(“操作日志”的缩写)并在辅助节点各自的数据集上重复记录的更改来进行的。 多个接受写操作的原色会导致数据冲突。
默认情况下,应用程序只会查询主要成员的读写操作。 您可以将设置配置为从一个或多个辅助成员中读取,但由于数据是异步传输的,因此从辅助节点读取可能会导致提供旧数据。 因此,这样的配置并不适合每个用例。
将 MongoDB 的副本集与其他复制实现区分开来的一个特性是它们的自动故障转移机制。 如果主要成员不可用,则会在辅助节点之间进行自动选举过程以选择新的主要成员。 一个副本集最多可以有 50 个成员,但在一次选举中最多可以有 7 个成员投票。
但是,如果辅助成员池包含偶数个节点,则可能由于投票僵局而无法选出新的主成员。 这将需要在副本集中包含第三种类型的成员:arbiter。 仲裁者是副本集的可选成员,在这种情况下投票以确保该集能够做出决定。 但是请注意,仲裁者没有数据集的副本,并且被禁止成为副本集的主节点。 如果一个副本集只有一个次要成员,则需要一个仲裁器。
有时您可能不希望所有辅助节点都遵循副本集辅助成员的标准规则。 MongoDB 允许您将副本集的辅助成员配置为承担以下非标准角色:
- Priority 0 Replication Members:在某些情况下,将某些集合成员选为主要位置可能会对应用程序的性能产生负面影响。 例如,如果您正在将数据复制到远程数据中心,或者某个次要成员的硬件不足以作为该集合的主要访问点,则将其优先级设置为
0
可以确保该成员不会成为主节点,但可以继续复制数据。 - 隐藏的复制成员:某些情况要求您保持一组成员可访问且对您的客户端可见,同时隐藏具有不同目的且不应用于读取操作的后台成员。 例如,您可能需要一个辅助成员作为分析工作的基础,这将受益于最新的数据集,但会给工作成员造成压力。 通过将此成员设置为隐藏,它不会干扰副本集的一般操作。 隐藏成员必须设置为
0
的优先级以避免成为主要成员,但他们可以在选举中投票。 - 延迟复制成员:通过设置次要成员的延迟选项,您可以控制次要等待执行从主要操作日志复制的每个操作的时间。 如果您想防止意外删除或从破坏性操作中恢复,这很有用。 例如,如果您将辅助服务器延迟半天,它不会立即对其自己的数据集执行意外操作,并且可以用于恢复更改。 延迟成员不能成为主要成员,但可以在选举中投票。 在大多数情况下,它们也应该被隐藏以防止应用程序进程读取过时的数据。
第 1 步 — 配置 DNS 解析
当需要在第 4 步中初始化副本集时,您需要提供一个地址,其中每个副本集成员都可以被集合中的其他两个成员访问。 MongoDB 文档建议不要在配置副本集时使用 IP 地址,因为 IP 地址可能会意外更改。 相反,MongoDB 建议 在配置副本集时使用逻辑 DNS 主机名。
一种方法是 为每个复制成员 配置子域。 虽然配置子域对于生产环境或其他长期解决方案来说是理想的,但本教程将概述如何通过编辑每个服务器各自的 hosts
文件来配置 DNS 解析。
hosts
是一个特殊文件,允许您将人类可读的主机名分配给数字 IP 地址。 这意味着,如果您的任何服务器的 IP 地址发生变化,您只需更新三台服务器上的 hosts
文件,而不是重新配置副本集。
在 Linux 和其他类 Unix 系统上,hosts
存储在 /etc/
目录中。 在您的三个服务器 中的 上,使用您喜欢的文本编辑器编辑文件。 在这里,我们将使用 nano
:
sudo nano /etc/hosts
在配置 localhost 的前几行之后,为副本集的每个成员添加一个条目。 这些条目采用 IP 地址的形式,后跟您选择的人类可读名称,如下例所示:
/etc/hosts
IP_address any_hostname
您可以将服务器配置为使用您喜欢的任何主机名,但使每个主机名具有描述性会很有帮助。 在本指南的示例中,三台服务器将使用以下主机名:
- mongo0.replset.member
- mongo1.replset.member
- mongo2.replset.member
使用这些主机名,您的 /etc/hosts
文件将类似于以下突出显示的行:
/etc/hosts
. . . 127.0.0.1 localhost 203.0.113.0 mongo0.replset.member 203.0.113.1 mongo1.replset.member 203.0.113.2 mongo2.replset.member . . .
如果您不知道服务器的 IP 地址,可以在每台服务器上运行以下 curl
命令来检索它们。 icanhazip.com
是一个网站,显示用于访问它的任何计算机的 IP 地址。 通过将其 URL 作为参数提供给 curl
命令,该命令会将运行它的服务器的 IP 地址打印到标准输出:
curl -4 icanhazip.com
如果您使用的是 DigitalOcean Droplets,您还可以在 控制面板 中找到您服务器的 IP 地址。
您在此处添加的新行在您的集合中的三个主机上应该是相同的。 保存并关闭每个服务器上的文件。 如果您使用 nano
编辑这些文件,请按 CTRL + X
、Y
,然后按 ENTER
。
在您的每台服务器上编辑、保存和关闭 hosts
文件后,您将完成为您的副本集配置 DNS 解析。 您现在可以继续更新每台服务器的防火墙规则,以允许它们相互通信。
第 2 步 — 使用 UFW 更新每个服务器的防火墙配置
假设您遵循先决条件 初始服务器设置指南 ,您将在每个安装了 MongoDB 的服务器上设置防火墙并启用对 OpenSSH
UFW 配置文件的访问。 这是一项重要的安全措施,因为这些防火墙当前阻止与服务器上任何端口的连接,除了 ssh
连接,这些连接提供的密钥与每个服务器各自的 authorized_keys
文件中的密钥一致。
但是,这些防火墙也会阻止每台服务器上的 MongoDB 实例相互通信,从而阻止您启动副本集。 要更正此问题,您需要添加新的防火墙规则,以允许每台服务器访问 MongoDB 正在侦听连接的其他两台服务器上的端口。
在 mongo0 上,运行以下 ufw
命令以允许 mongo1 访问 mongo0 上的端口 27017
:
sudo ufw allow from mongo1_server_ip to any port 27017
请务必更改 mogno1_server_ip
以反映您的 mongo1 服务器的实际 IP 地址。 请注意,ufw
命令不适用于 hosts
文件中配置的主机名,因此请务必在此命令和以下命令中使用服务器的实际 IP 地址。 此外,如果您已将此服务器上的 Mongo 实例更新为使用非默认端口,请务必更改 27017
以反映您的 MongoDB 实例实际使用的端口。
然后添加另一个防火墙规则,让 mongo2 访问同一端口:
sudo ufw allow from mongo2_server_ip to any port 27017
接下来,更新其他两台服务器的防火墙规则。 在 mongo1 上运行以下命令,确保更改 IP 地址以分别反映 mongo0 和 mongo2 的 IP 地址:
sudo ufw allow from mongo0_server_ip to any port 27017 sudo ufw allow from mongo2_server_ip to any port 27017
最后,在 mongo2 上运行这两个命令。 同样,请确保为每台服务器输入正确的 IP 地址:
sudo ufw allow from mongo0_server_ip to any port 27017 sudo ufw allow from mongo1_server_ip to any port 27017
添加这些 UFW 规则后,您的三个 MongoDB 服务器中的每一个都将被允许访问其他两个服务器上 MongoDB 使用的端口。 但是,您还不能对此进行测试,因为每个服务器上的 Mongo 实例当前正在阻止任何外部连接。 在下一步通过更新每个 MongoDB 实例的配置文件启用复制后,您将能够执行此测试。
第 3 步 — 在每个服务器的 MongoDB 配置文件中启用复制
此时,您已经编辑了服务器的 /etc/hosts
文件以配置将解析为每个人的 IP 地址的主机名。 您还打开了每台服务器的防火墙,以允许其他两台服务器访问默认的 MongoDB 端口 27107
。 现在您已准备好开始在每台服务器上配置 MongoDB 安装以启用复制。
此步骤概述了如何通过编辑 MongoDB 的配置文件 /etc/mongod.conf
来执行此操作。 您必须在每台服务器上完成此步骤中的每个过程,但出于演示目的,我们将在示例中使用 mongo0。
在 mongo0 上,在首选文本编辑器中打开 MongoDB 配置文件:
sudo nano /etc/mongod.conf
即使您打开了每台服务器的防火墙以允许其他服务器访问端口 27017
,MongoDB 目前仍绑定到本地环回网络接口 127.0.0.1
。 这意味着 MongoDB 只能接受来自安装它的服务器上的连接。
要允许远程连接,除了 127.0.0.1
之外,您还必须将 MongoDB 绑定到服务器的可公开路由的 IP 地址。 这样,您的 MongoDB 安装将能够侦听从远程机器到您的 MongoDB 服务器的连接。
找到 network interfaces
部分。 默认情况下它看起来像这样:
/etc/mongod.conf
. . . # network interfaces net: port: 27017 bindIp: 127.0.0.1 . . .
将逗号附加到以 bindIp:
开头的行,后跟 mongo0 的主机名或公共 IP 地址。 此示例使用在步骤 1 中配置的主机名:
/etc/mongod.conf
. . . # network interfaces net: port: 27017 bindIp: 127.0.0.1,mongo0.replset.member . . .
接下来,找到文件底部的 #replication:
行。 它看起来像这样:
/etc/mongod.conf
. . . #replication: . . .
通过删除井号 (#
) 取消注释此行。 然后在此行下方添加一个 replSetName
指令,后跟一个名称,MongoDB 将使用该名称来标识副本集:
/etc/mongod.conf
. . . replication: replSetName: "rs0" . . .
在此示例中,replSetName
指令的值为 "rs0"
。 您可以在此处提供您想要的任何名称,但使用描述性名称会很有帮助。 但是请记住,每个服务器的 mongod.conf
文件必须在 replSetName
指令之后具有相同的名称,以便它们的每个 MongoDB 实例成为同一副本集的成员。
请注意,replSetName
指令之前有两个空格,并且名称包含在引号中("
),这两个空格都是正确读取此配置所必需的。
更新文件的这两个部分 net
和 replication
后,它们将如下所示:
/etc/mongod.conf
. . . # network interfaces net: port: 27017 bindIp: 127.0.0.1,mongo0.replset.member . . . replication: replSetName: "rs0" . . .
保存并关闭文件。 然后对 mongo1 和 mongo2 上的 /etc/mongod.conf
文件进行相同的更改。 这样做之后,这些更新的部分在 mongo1 的配置文件中将如下所示:
/etc/mongod.conf
. . . # network interfaces net: port: 27017 bindIp: 127.0.0.1,mongo1.replset.member . . . replication: replSetName: "rs0" . . .
以下是这些部分在 mongo2 的配置文件中的外观:
/etc/mongod.conf
. . . # network interfaces net: port: 27017 bindIp: 127.0.0.1,mongo2.replset.member . . . replication: replSetName: "rs0" . . .
重申一下,您添加到每个服务器的 bindIp
指令的 IP 地址或主机名必须是您正在编辑其 mongod.conf
文件的服务器的地址或主机名。
对每个服务器的 mongod.conf
文件进行这些更改后,保存并关闭每个文件。 然后,通过发出以下命令,在每个服务器 上重新启动 mongod
服务 :
sudo systemctl restart mongod
这样,您就为每个服务器的 MongoDB 实例启用了复制。
注意:此时可以使用nc
命令测试你在步骤2中添加的防火墙规则是否正确。 nc
是 netcat 的缩写,是一个用于与 TCP 或 UDP 建立网络连接的实用程序。 在这种情况下进行测试很有用,因为它允许您在建立连接时同时指定 IP 地址和端口号。
以下示例 nc
命令包含 -z
选项,该选项将实用程序限制为仅扫描目标服务器上的侦听守护程序而不向其发送任何数据。 回想一下 先决条件安装教程 中,MongoDB 作为服务守护进程运行,这使得该选项对于测试连接性很有用。 它还包括 v
选项,该选项增加了命令的详细程度,导致它返回比其他方式更多的信息。
此示例 nc
命令显示了从 mongo0 到达 mongo1 的尝试:
nc -zv mongo1.replset.member 27017
以下输出表明 mongo0 能够在 MongoDB 使用的端口上访问 mongo1:
OutputConnection to mongo1.replset.member 27017 port [tcp/*] succeeded!
您可以通过在每台服务器上重复此命令并指定适当的主机名或 IP 地址来测试每对服务器之间的连接。
在编辑每个服务器的 mongod.conf
文件以启用复制并重新启动 mongod
服务后,您就可以启动副本集并将每个 Mongo 实例添加为成员。
第 4 步 - 启动副本集并添加成员
现在您已经配置了三个 MongoDB 安装中的每一个,您可以打开一个 MongoDB shell 来启动复制并将每个都添加为成员。
出于演示目的,此步骤中的示例将使用 mongo0 上的 MongoDB 实例来启动副本集。 但是,您可以从其 mongod.conf
文件已适当配置的任何服务器启动复制。
在 mongo0 上,打开 MongoDB shell:
mongo
在提示符下,您可以通过运行 rs.initiate()
方法从 mongo
shell 启动副本集。 但是,单独运行此方法只会启动运行该方法的机器的复制,然后您需要通过为每个成员发出 rs.add()
方法来添加其他 Mongo 实例。
回想一下,MongoDB 将其数据存储在称为 documents 的类似 JSON 的结构中。 因为您已经在每台服务器上编辑了 mongod.conf
文件以配置三个 Mongo 实例以进行复制,因此您可以在 rs.initiate
方法中包含一个包含每个成员的配置详细信息的文档。 这将允许您启动副本集并立即添加每个成员,而不必运行多个单独的方法。
为此,通过键入以下内容并按 ENTER
开始 rs.initiate()
方法:
rs.initiate(
在您输入右括号之前,Mongo 不会将 rs.initiate
方法注册为完整的。 在您这样做之前,提示将从大于号 (>
) 变为省略号 (...
)。
与 JSON 中的对象一样,MongoDB 中的文档以大括号({
和 }
)开始和结束。 要开始添加副本集的配置文档,请输入一个左大括号:
{
MongoDB 文档由任意数量的 field-and-value 对组成,其形式为 field: value
。 这个特定文档的第一个字段值对必须是一个 _id:
字段,它提供一个名称来标识副本集; 此字段的值必须与您在 mongod.conf
文件中设置的 replSetName
指令相同,在我们的示例中为 "rs0"
。
输入此字段-值对,在其后加上逗号,然后按 ENTER
开始新行:
_id: "rs0",
接下来,添加一个 members:
字段。 但是,在这个 members:
字段后面不是一个值,而是一个包含多个文档的数组,每个文档代表一个要添加的副本集成员。 在 MongoDB 文档中,数组总是放在一对方括号内([
和 ]
)。
添加 members:
字段,后跟左方括号以开始数组,然后按 ENTER
移动到下一行:
members: [
现在添加一个包含两个字段值对的文档,以逗号分隔,以表示副本集的第一个成员。 本文档的第一个字段是另一个 _id:
字段,它接受用于在内部标识成员的整数。 第二个是 host:
字段,后面必须跟一个包含主机名的字符串,该主机名将解析为可以访问成员 Mongo 实例的地址:
{ _id: 0, host: "mongo0.replset.member" },
注意:如果您的任何 Mongo 实例在 MongoDB 默认端口以外的端口上运行 - 27017
- 您必须在主机名后面加上冒号 (:
),然后端口号,如本例所示:
{ _id: 0, host: "mongo0.replset.member:27018" },
输入第一个后,为您的副本集的其他成员输入其他文档。 确保用逗号分隔每个文档:
{ _id: 1, host: "mongo1.replset.member" }, { _id: 2, host: "mongo2.replset.member" }
接下来,通过输入右方括号来结束数组:
]
最后,用右大括号结束配置文档,然后用右括号关闭方法:
})
总之,rs.initiate()
方法将如下所示:
> rs.initiate( ... { ... _id: "rs0", ... members: [ ... { _id: 0, host: "mongo0.replset.member" }, ... { _id: 1, host: "mongo1.replset.member" }, ... { _id: 2, host: "mongo2.replset.member" } ... ] ... })
假设您正确输入了所有详细信息,在键入右括号后按 ENTER
后,该方法将运行并启动副本集。 如果该方法在输出中返回 "ok" : 1
,则表示副本集已正确启动:
Output{ "ok" : 1, "$clusterTime" : { "clusterTime" : Timestamp(1612389071, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) } }, "operationTime" : Timestamp(1612389071, 1) }
如果副本集按预期启动,您会注意到 MongoDB 客户端的提示将从一个大于号 (>
) 更改为以下内容:
MongoDB 安装了一些内置方法,您可以使用它们来管理和检索有关副本集的信息。 其中,rs.help()
方法特别有用,因为它返回了这些副本集方法的列表以及它们的作用的描述:
rs.help()
Output rs.status() { replSetGetStatus : 1 } checks repl set status rs.initiate() { replSetInitiate : null } initiates set with default settings rs.initiate(cfg) { replSetInitiate : cfg } initiates set with configuration cfg rs.conf() get the current configuration object from local.system.replset rs.reconfig(cfg) updates the configuration of a running replica set with cfg (disconnects) rs.add(hostportstr) add a new member to the set with default attributes (disconnects) rs.add(membercfgobj) add a new member to the set with extra attributes (disconnects) rs.addArb(hostportstr) add a new member which is arbiterOnly:true (disconnects) rs.stepDown([stepdownSecs, catchUpSecs]) step down as primary (disconnects) rs.syncFrom(hostportstr) make a secondary sync from the given member rs.freeze(secs) make a node ineligible to become primary for the time specified rs.remove(hostportstr) remove a host from the replica set (disconnects) rs.secondaryOk() allow queries on secondary nodes rs.printReplicationInfo() check oplog size and time range rs.printSecondaryReplicationInfo() check replica set members and replication lag db.isMaster() check who is primary db.hello() check who is primary reconfiguration helpers disconnect from the database so the shell will display an error, even if the command succeeds.
运行 rs.help()
或其中一种方法后,您可能会看到客户端提示再次变为以下内容:
这意味着您连接的 MongoDB 实例被选为主要集成员。
请注意,如果您将来有其他节点想要添加到副本集中,您可以在配置它们之后使用 rs.add()
方法来完成,就像您之前对当前副本集成员所做的那样脚步:
rs.add( "mongo3.replset.member" )
您现在可以通过按 CTRL + C
或运行 exit
命令关闭 MongoDB 客户端:
exit
您的副本集现已启动并运行,您可以开始将其与您的应用程序集成。
Warning:当您打开 MongoDB 提示以启动副本集时,您可能已经注意到如下警告消息:
. . . 2021-02-03T21:45:48.379+00:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted . . .
此消息表明您尚未为数据库启用访问控制。 根据 MongoDB 文档:
MongoDB 使用基于角色的访问控制 (RBAC) 来管理对 MongoDB 系统的访问。 用户被授予一个或多个角色,这些角色决定了用户对数据库资源和操作的访问权限。
由于尚未在您的任何 MongoDB 实例上启用访问控制,任何有权访问副本集中三个服务器中的任何一个的人也可以访问该服务器上的 Mongo 实例。 这带来了重要的安全风险,因为这意味着他们还可以访问您的应用程序数据。
删除此警告并向副本集添加一层安全性的一种方法是配置 密钥文件身份验证 。 然而,正如在介绍中提到的,MongoDB 文档将 密钥文件描述为“最适合测试或开发环境”的“最低限度的安全形式”。
请注意,对于生产部署, MongoDB 文档建议 使用 x.509 证书进行内部成员身份验证。 获取和配置 x.509 证书的过程伴随着许多必须根据具体情况做出的警告和决定,这超出了本教程的范围。
如果您打算使用您的副本集进行测试或开发,我们 强烈建议 您按照我们的 如何在 Ubuntu 20.04 上为 MongoDB 副本集配置密钥文件身份验证的教程。
结论
数据库复制已被广泛用作提高性能、可用性和数据安全性的策略,以至于建议在生产环境中使用的任何数据库都启用某种形式的复制。 副本也是通用的,并且可以在数据架构中承担许多不同的角色,例如报告或灾难恢复。 MongoDB 副本集中的自动故障转移功能使其在帮助确保您的数据在发生中断时保持高可用性方面特别有价值。
如果您想了解有关 MongoDB 的更多信息,我们鼓励您查看 我们的整个 MongoDB 教程合集 。