MongoDB 通过建立在几个核心架构基础之上的开发者数据平台,使您能够满足现代应用程序的需求。它让您能够以最佳方式进行创新,构建事务性、操作性和分析性应用程序。本章将特别强调两个关键元素:复制和分片,来审视 MongoDB 的架构。
复制是 MongoDB 分布式架构中的关键组成部分,确保数据的可访问性和对故障的韧性。它使您能够将相同的数据集分布在不同的数据库服务器上,防止单个服务器的故障。
此外,您还将了解到分片,这是一种将数据分布在多台机器上的水平扩展策略。随着应用程序的普及和它们产生的数据量增加,跨机器扩展变得至关重要,以确保足够的读写吞吐量。
本章将涵盖以下主题:
- 复制和分片如何提高可靠性和可用性。
- MongoDB 中复制和分片的各种方法。
- MongoDB 7.0 中新的分片集群特性。
- 复制与分片的比较。
人们经常将复制与分片混淆。虽然两者都是数据库管理中使用的系统集,但它们服务于不同的目的,并因不同的原因而使用。复制是一个数据被复制并存储在多个位置以确保冗余和可靠性的过程,在数据保护和可访问性中扮演着至关重要的角色。
另一方面,分片涉及将较大的数据库划分为更小、更易于管理的部分,称为分片。每个分片在单独的数据库服务器实例上存储数据集的一部分。然而,需要注意的是,每个分片还必须实现复制以维护数据的完整性和可用性。
结合分片和复制的目标是确保数据的持久性和高可用性。当分片的服务器实例失败,且该分片上只有单个数据副本时,可能会导致数据不可用,直到服务器恢复或更换为止。通过在每个分片内采用复制,分片集群可以保持数据可用性,并防止服务器故障引起的任何中断。这种方法使分片集群能够在不停机的情况下进行滚动升级,允许平稳且不间断的系统维护。
接下来的几节将深入探讨复制和分片及其各自的组件。
复制 在 MongoDB 中,副本集是指维护相同数据集的一组 mongod 进程。它们提供冗余和高可用性,是所有生产实施的基础。通过在多个数据库服务器上拥有多个数据副本,复制确保了一定程度的容错能力,保护免受单个数据库服务器故障的影响。
图 2.1:一个副本集
主节点处理所有写操作,并将所有数据集更改记录在其操作日志(oplog)中。MongoDB 副本集只能有一个主节点。
辅助节点复制主节点的 oplog,并在自己的数据集上实现操作,确保它们镜像主节点的数据集。如果主节点变得不可访问,一个合格的辅助节点将启动选举成为新的主节点。
通过在多个服务器上存储数据,复制提高了系统的可靠性。此外,支持滚动升级允许您在不中断的情况下升级单个服务器的软件或硬件,确保数据库的持续可用性。复制显著提高了读取密集型应用程序的性能,将读取负载分布在多个服务器上,以确保快速的数据检索。
副本集选举 MongoDB 使用建立在 Raft 共识算法之上的协议来协调副本集选举,确保分布式系统的数据一致性。该协议包括副本集用于选择哪个成员将承担主角色的投票机制。
可以启动选举的几个事件包括:
- 向副本集添加或删除节点
- 初始化副本集
- 任何辅助节点和主节点之间的心跳故障超过预设的超时持续时间(默认情况下,自托管主机为 10 秒,MongoDB Atlas 为 5 秒)
您的应用程序的连接处理逻辑应该设计得能够考虑自动故障转移和随后的选举。MongoDB 驱动程序有能力识别主节点的丢失,并自动重试特定的读取或写入操作,为选举提供了额外的内置韧性层。
当主副本变得不可用时,辅助副本会为新的主节点投票。具有最近写入时间戳的副本更有可能赢得选举。这种策略最小化了先前的主节点重新加入集时的回滚机会。选举后,节点进入冻结期,在这期间它们不能启动另一次选举。这是为了防止连续、快速的选举,这可能会破坏系统的稳定性。目前,MongoDB 副本集仅支持称为协议版本 1(pv1)的协议。
图 2.2:选举新的主节点
副本集在选举成功结束之前无法执行写操作。然而,如果它们被配置为针对辅助节点,副本集仍然可以处理读取操作。
在正常情况下,默认副本集配置设置下,集群选举新主节点所需的平均时间不应超过 12 秒。这个持续时间包括宣布主节点不可访问所需的时间,以及启动和完成选举所需的时间。可以通过更改设置来调整这个时间框架。选举超时配置选项settings.electionTimeoutMillis 可以调整这个时间。
成员优先级:一旦建立了稳定的主节点,选举算法允许最高优先级的辅助节点启动选举。成员优先级影响选举的时机和结果,优先级更高的辅助节点可能会更早地启动选举并获胜。然而,尽管有优先级更高的成员可用,优先级较低的成员可能会短暂地担任主节点。选举过程持续进行,直到最高优先级的成员成为主节点。优先级值为 0 的成员不能成为主节点,也不寻求选举。
由于副本集最多可以容纳 50 名成员,其中只有 7 名是投票成员,包括非投票成员可以使集超过 7 名的限制。非投票成员的特征是拥有零票,必须具有优先级为 0。
副本集 oplog:oplog 是一个独特的固定集合,它维护了一个连续的日志,记录了所有改变 MongoDB 数据库中数据的操作。它可以扩展超过其设置的大小限制,以防止删除大多数提交点。
MongoD 中的写操作在主节点上执行,随后记录在主节点的 oplog 中。辅助成员异步复制并应用这些操作。副本集的所有成员都持有 oplog 的副本,位于 local.oplog.rs 集合中,使它们能够跟上数据库的当前状态。
为了支持复制,副本集的所有成员都相互交换心跳(ping)。任何辅助成员都可以从任何其他成员导入 oplog 条目。oplog 中的每个操作都是幂等的,这意味着无论对目标数据集应用一次还是多次,oplog 操作都会产生相同的结果。
oplog 窗口:Oplog 条目带有时间戳。oplog 窗口指的是 oplog 中最近和最早时间戳之间的时间间隔。如果辅助节点与主节点失去连接,只有在重新建立连接的时间在 oplog 窗口的持续时间内,它才能使用复制重新同步。MongoDB 允许定义保留 oplog 条目的最小持续时间(以小时为单位),以及特定大小。系统仅在以下条件下才会丢弃 oplog 条目:
- oplog 已填满到其最大配置容量,并且 oplog 条目已超过基于主机系统时钟指定的保留期限。
- 没有指定的最小 oplog 保留期限,MongoDB 默认采用其标准行为。它从最旧的条目开始截断 oplog,确保 oplog 不超过配置的最大大小。
副本集部署架构:生产系统的标准部署涉及一个三成员的副本集。这些集提供了冗余和对故障的韧性。虽然建议避免不必要的复杂性,但架构最终应由应用程序的需求指导。通常,应遵循以下规则:
- 确保副本集具有奇数个投票成员,以防止网络分区期间的分裂决策,使更大的部分可以接受写入。
- 如果您的投票成员集是偶数,请考虑添加另一个携带数据的投票成员。如果限制阻止了这一点,请引入一个仲裁者。
- 包含隐藏或延迟的成员以支持专用功能,如备份或报告。
- 为了在数据中心故障的情况下保护您的数据,请确保至少有一个成员位于备用数据中心。
副本集仲裁者:在您拥有一个主节点和一个辅助节点,但预算限制阻止您添加另一个辅助节点的情况下,您可以将仲裁者纳入您的副本集。仲裁者参与主节点选举,但它不持有数据集的副本,也无法成为主节点。仲裁者在选举中携带一票。默认情况下,其优先级设置为0。
隐藏的副本集成员:隐藏成员构成副本集的一个重要部分;然而,它们不能担任主节点角色,并且在客户端应用程序中不可见。尽管不可见,隐藏成员仍然能够参与并投票选举。隐藏节点可以作为专用副本集成员,用于执行备份操作或运行某些报告查询。要设置一个隐藏节点并防止成员被提升为主节点,您可以将其优先级设置为0,并将隐藏参数配置为true,如下例所示:
cfg = rs.conf()
cfg.members[n].hidden = true
cfg.members[n].priority = 0
rs.reconfig(cfg)
延迟的副本集成员延迟成员也是隐藏成员,它们以故意的延迟从源oplog复制和应用操作,代表集群的先前状态。例如,如果当前时间是08:01,并且一个成员的延迟设置为一小时,那么延迟成员上的最新操作不会晚于07:01:
cfg = rs.conf()
cfg.members[n].hidden = true
cfg.members[n].priority = 0
cfg.members[0].secondaryDelaySecs = 3600
rs.reconfig(cfg)
延迟成员充当持续的备份或数据集的实时历史记录,为防止人为错误提供安全网。例如,它们可以促进从失败的应用程序更新和操作员错误中恢复,如意外删除数据库和集合。
写入关注 写入关注确定MongoDB在副本集上确认写入操作的方式。它提供了一种机制,以确保在确认写入操作之前,数据被写入指定数量的节点。
写入关注的组成部分 以下字段可以包含在写入关注中:
{ w: , j: , wtimeout: }
- w:指定必须确认写入的副本集成员的所需数量。以下w: 写入关注可用:0:无需确认。这提供了最少的持久性,但最高的性能。1:来自主节点的确认。majority:来自大多数副本集成员的确认。从MongoDB 5.0起,默认值;之前,默认值为1。:来自特定数量的副本集成员的确认。
- j:请求确认写入操作已被写入磁盘上的日志。如果设置为true,写入操作将等待日志确认。
- wtimeout:设置写入关注的时间限制,以毫秒为单位。如果在这段时间内未达到指定的确认级别,写入操作将返回错误。这个错误并不意味着写入不会完成或被回滚;它防止写入超过指定的阈值阻塞。
从MongoDB 4.4开始,副本集和分片集群都有能力建立全局默认写入关注。任何未定义特定写入关注的操作都将自动采用此全局默认写入关注的设置。使用管理命令setDefaultRWConcern来建立全局默认配置,用于读取或写入关注:
db.adminCommand({
setDefaultRWConcern : 1,
defaultReadConcern: { },
defaultWriteConcern: { },
writeConcern: { },
comment:
})
从MongoDB 5.0起,默认写入关注设置为 { w: "majority" }。然而,当部署包括仲裁者时,有例外:
- 投票多数被计算为投票成员数量的一半加1,向下取整。如果承载数据的投票成员数不超过这个投票多数,则默认写入关注调整为 { w: 1 }。
- 在任何其他情况下,默认写入关注保持为 { w: "majority" }。
写入关注的重要性 写入关注的选择可以影响数据的性能和持久性:
- 性能:较低的写入关注(例如,w: 0)可以通过减少写入操作的延迟来提高性能。然而,它冒着数据持久性的风险。
- 持久性:较高的写入关注(例如,w: "majority")通过等待大多数节点的确认来确保数据的持久性。这可能会在写入操作中引入轻微的延迟。
读取偏好 在MongoDB中,读取偏好确定MongoDB客户端如何将读取操作路由到副本集的成员。默认情况下,MongoDB将所有读取操作定向到主成员。然而,通过调整读取偏好,可以将读取负载分布到辅助成员,提高系统的整体性能和可用性。
读取偏好的组成部分 MongoDB支持五种读取偏好模式:
- primary:所有读取操作都定向到主成员(默认)。
- primaryPreferred:如果可用,读取定向到主成员;否则,它们被路由到辅助成员。
- secondary:所有读取操作都定向到辅助成员。
- secondaryPreferred:如果有任何可用的辅助成员,读取定向到辅助成员;否则,它们被路由到主成员。
- nearest:读取操作定向到网络延迟最低的成员,无论成员的状态如何。
除了读取偏好模式,您还可以指定以下选项:
- 标签集:标签集允许通过将自定义标签与副本集成员关联来定制读取偏好。然后,客户端可以将读取操作定向到具有特定标签的成员。
- 最大陈旧度:陈旧度指的是从主节点到辅助节点的复制延迟。此选项指定辅助成员在客户端停止将其用于读取操作之前可以有多陈旧。
读取偏好的重要性 读取偏好的选择可以影响数据的性能和可用性:
- 性能:将读取操作分布到辅助成员可以通过减少主节点的负载来提高性能。
- 可用性:如果主节点不可用,如果读取偏好模式允许,读取操作仍可由辅助成员提供。
读取关注 读取关注决定了从副本集和分片集群中读取的数据的一致性和隔离属性。通过调整读取关注,可以控制读取操作中数据的可见性,从而确保所需的一致性和隔离级别。
读取关注的组成部分 MongoDB 支持以下读取关注级别:
- "local":返回在查询开始时 MongoDB 实例可用的最新数据,不考虑复制的状态(默认)。
- "available":返回在查询时分布式系统中当前可用的数据。这个级别提供最小的延迟,但不保证一致性。
- "majority":返回已被大多数副本集成员确认的数据。这确保了高水平的一致性。
- "linearizable":提供最高级别的一致性,通过返回反映在读取操作开始之前完成的所有成功的大多数确认写入的数据。
- "snapshot":返回所有副本集成员的特定时间点的数据。
读取关注的重要性 读取关注的选择可以影响数据的一致性和隔离性:
- 一致性:更高的读取关注级别(例如,“majority”或“linearizable”)确保返回的数据在副本集成员之间是一致的。
- 隔离性:通过使用“snapshot”读取关注,可以隔离事务,使其与并发写入隔离,确保在整个事务中数据的一致视图。
复制方法 MongoDB Shell (mongosh) 提供了一系列 shell 辅助方法,旨在促进与副本集的交互。它们是管理复制 MongoDB 部署的关键工具。让我们检查一些管理 MongoDB 复制所需的重要方法:
- rs.add(): 向副本集添加成员
- rs.addArb(): 向副本集添加仲裁者
- rs.conf(): 显示副本集配置文档
- rs.initiate(): 初始化一个新的副本集
- rs.printReplicationInfo(): 从主节点的角度总结副本集的状态
- rs.status(): 提供一份详细当前副本集状态的文档
- rs.reconfig(): 重新配置现有的副本集,覆盖现有的副本集配置
- rs.stepDown(): 触发当前主节点变成辅助节点,触发选举
有关复制方法的详细列表,请访问以下资源:复制方法
除了上述复制方法,您还可以使用复制命令进行特定操作。例如,replSetResizeOplog 改变副本集中变量 oplog 的大小。该命令接受以下字段:
- replSetResizeOplog: 设置为 1。
- size: 指定 oplog 的最大大小,以 MB 为单位。可以设置的最小大小是 990 MB,最大是 1 PB。在 mongosh 中使用 Double() 显式将大小转换为双精度浮点数。
- minRetentionHours: 表示保留 oplog 条目的最小小时数,小数值表示小时的分数(例如,1.5 表示 1 小时和 30 分钟)。该值必须等于或大于 0。值为 0 意味着 mongod 应该从最旧的条目开始截断 oplog,以维持配置的最大 oplog 大小。
db.adminCommand({
replSetResizeOplog: ,
size: ,
minRetentionHours:
})
要了解更多关于复制命令的信息,请访问以下资源:复制命令
分片 MongoDB 通过分片支持水平扩展。分片涉及将数据分布在众多进程中,并在管理和组织大规模数据中发挥重要作用。这种方法将较大的数据库划分为更小、更易管理的组件,称为分片。每个分片存储在单独的数据库服务器实例上,这分散了负载并提供了有效的数据管理方法。此外,该技术允许创建分布式数据库以支持地理分布的应用程序,实现强制数据在特定区域内居住的策略。
为什么需要分片? 考虑一个数据快速扩展且数据库接近其最大容量的场景。这种情况可能会出现许多挑战。通常,最紧迫的问题是性能恶化。随着数据库的增长,查询和检索数据所需的时间可能会显著增加。这种减速对用户体验产生负面影响,使应用程序看起来缓慢或无响应。
另一个潜在障碍是存储限制。大多数系统都有一个它们可以有效管理的数据量上限。如果您的数据增长超过了系统的存储能力,它可能导致系统故障。在管理系统增长方面,有两种主要策略——垂直扩展和水平扩展:
- 垂直扩展增强单个服务器的容量,例如,通过升级 CPU、增加 RAM 或扩展存储空间。然而,技术限制可能阻止单个机器处理某些工作负载,因为存在严格的上限。因此,垂直扩展有实际限制。
- 水平扩展涉及将系统的数据和工作负载分散在多个服务器上,增加了所需的服务器数量。每台机器只处理总工作负载的一部分,这可能比一台强大的服务器更有效。虽然这种方法可能比投资昂贵的硬件更具成本效益,但它增加了基础设施和系统维护的复杂性。
分片集群的关键元素 MongoDB 分片集群由以下关键元素组成:
- Shard:这是一个存储集群中部分数据的副本集。集群可以有 1 到 n 个分片,每个分片包含整体数据的不同子集。MongoDB Atlas 根据集群层级提供不同的存储容量:M10-M40 为 4 TB,M50-M60 为 8 TB,M80+ 为 14 TB。
- mongos:作为客户端应用程序的查询路由器,有效管理读写操作。它将客户端请求引导到适当的分片,并将来自多个分片的结果汇总到一个连贯的客户端响应中。客户端与 mongos 实例建立连接,而不是直接与单个分片建立连接。建议在生产部署中运行多个 mongos 实例以确保高可用性。
- Config servers:作为副本集运作,充当专门数据库的角色,作为分片元数据的主要存储库。这些元数据包括分片数据的状态和结构,包括分片集合的列表和路由详细信息等重要信息。它在实现集群内高效的数据管理和查询路由中起着至关重要的作用。
图 2.3 展示了分片集群内组件的交互。
图 2.3:一个分片集群
在 MongoDB 中,数据分片发生在集合级别,这意味着集合的数据分布在集群内的多个分片中。需要注意的是,分片集群中的每个数据库都有自己的主分片,负责存储该数据库内所有未分片的集合。值得一提的是,分片集群中的主分片概念与副本集中的主节点不同;它们服务于不同的目的,彼此之间没有关联。
创建新数据库时,由 mongos 进程选择主分片。它选择集群中数据量最少的分片。listDatabases 命令返回的 totalSize 字段被用作选择标准的一个因素。
使用 movePrimary 命令可以在初次分配后移动主分片。movePrimary 命令最初改变集群元数据中的主分片,然后迁移所有未分片的集合到指定的分片。
微分片 在某些情况下,MongoDB 提供了一种更先进的分片配置,称为微分片。与传统的将每个分片分配给专用机器的方法不同,微分片允许通过在单个主机上共置多个分片来实现更细粒度的控制和资源优化。
微分片可以是一种有效的策略,特别是当处理每个分片的较小数据大小时。通过在单个主机上合并多个分片,可以最大化资源利用率,从而提高硬件效率。
图 2.4:微分片配置
注意: 这种方法需要仔细的资源管理,以防止同一主机上运行的不同MongoDB进程之间发生冲突。每个MongoDB进程都需要分配足够的资源以有效运行。在实施微分片策略时,正确配置每个MongoDB进程的缓存大小至关重要。可以通过设置storage.wiredTiger.engineConfig.cacheSizeGB来调整MongoDB中WiredTiger存储引擎的缓存大小。这个缓存应该足够大,以容纳每个MongoDB进程的工作集。如果缓存缺乏必要的空间来加载更多数据,WiredTiger将从缓存中移除页面以腾出空间。
默认情况下,storage.wiredTiger.engineConfig.cacheSizeGB 配置为总RAM的50%,少于1GB。然而,当在同一台机器上运行多个MongoDB进程时,您应该调整此设置,以确保所有MongoDB进程的总内存使用量不超过可用RAM。
例如, 如果您在一台拥有16GB RAM的机器上运行两个MongoDB进程,您可以为每个进程将storage.wiredTiger.engineConfig.cacheSizeGB设置为25%(或4GB),而不是默认的50%。这可以防止MongoDB进程集体消耗超过总可用RAM的内存,为操作系统和其他应用程序留出足够的内存。
分片的优势: 分片提供了几个好处,例如:
- 增强的读写速度: 将数据集分布在多个分片上允许并行处理。每个额外的分片都会增加您的总吞吐量,并且良好分布的数据集可以提高查询性能。多个分片可以同时处理查询,加快响应时间。
- 扩展的存储容量: 增加分片的数量可以提高您的总存储量。如果一个分片可以存储4TB的数据,每个额外的分片就会增加另外4TB。这允许几乎无限的存储容量,随着您数据需求的增长,可以实现可扩展性。
- 数据本地性: 区域分片允许数据库分布在不同的地理位置,非常适合分布式应用。策略可以将数据限制在特定区域内,每个区域持有一个或多个分片,提高数据管理的效率和适应性。在区域分片中,不同的分片键值范围可以与每个区域相关联,根据地理位置的相关性实现更快、更精确的数据访问。
数据分布: 在分片的MongoDB集群中,正确的数据分布对于平衡工作负载、提高性能和增强可扩展性至关重要,它可以防止瓶颈并确保资源的有效利用。
分片键: MongoDB在集合级别执行分片,允许您选择要分片的集合。选择分片键对于分片集群至关重要,因为不当的选择可能导致数据分布不均、分片之间负载分配不均衡和查询性能下降。这些问题可能会使某些分片过载,而其他分片则未充分利用,降低系统效率。在极端情况下,一个被称为热分片的单个分片可能会成为瓶颈,严重影响集群性能。因此,选择正确的分片键对于分片环境中的最优性能至关重要。
通过使用由一个或多个文档字段组成的分片键,MongoDB将集合的文档分布在各个分片上。数据通过分解分片键值的范围被划分为不重叠的块。目标是在集群的分片中均匀分布这些块,确保有效的分布。
在MongoDB的近期版本中,对分片功能进行了显著修改:
- 从MongoDB 4.4开始,分片集合中的文档可以省略分片键字段。在这种情况下,这些缺失的分片键字段在文档分布到分片时被视为空值。
- 从MongoDB 4.4开始,您可以通过向现有的分片键添加后缀字段或字段来增强分片键。
- 在MongoDB 5.0中,您可以通过修改其分片键来重新分片集合。
分片策略: MongoDB支持两种分片策略,用于在分片集群中分布数据:
- 范围分片: 涉及根据分片键值将数据分割成连续的序列。具有相似分片键值的文档可能位于同一个分片或块中,有助于有效查询连续序列。然而,不当的分片键选择可能会影响读写性能。范围分片是默认方法,除非配置了哈希分片或区域的选项。当分片键表现出以下特征时,范围分片最有效:
- 高基数:分片键的基数为平衡器可以产生的块的数量设定了上限。只要可能,选择具有高基数的分片键。
- 低频率:分片键的频率表示数据中特定分片键值的复发率。当大部分文档只持有一小部分潜在的分片键值时,容纳这些文档的块可能会变成集群瓶颈。
- 非单调变化值:基于逐渐增加或减少的值的分片键更有可能将插入操作导向集群内的单个块。
- 哈希分片: 通过计算分片键字段值的哈希值,然后根据哈希分片键值分配每个块的范围。尽管一系列分片键在值上看起来可能很接近,但它们的哈希值不太可能落在同一个块中。值得注意的是,哈希分片对于执行基于范围的操作并不高效。哈希键适用于具有单调变化字段的分片键,例如ObjectId值或时间戳。一个典型的例子是默认的_id字段,假设它只包含ObjectId值。
MongoDB 4.4引入了使用单个哈希字段构建复合索引的能力。要创建这样的复合哈希索引,在索引创建期间,将任何单个索引键的值指定为哈希。复合哈希索引为复合索引中的单个字段计算哈希值;这个值连同索引中的其他字段一起用作您的分片键。以下是一个示例:
db.planets.createIndex({ "name":1, "_id": "hashed" })
sh.shardCollection("sample_guides.planets", { "name": 1, "_id": "hashed" })
上述命令在name字段上创建了一个升序的复合哈希索引,_id作为哈希字段,并分片了planets集合。
分片键索引: 要分片一个已填充的集合,该集合需要有一个以分片键开头的索引。然而,在分片一个空集合时,如果集合还没有为指定的分片键准备合适的索引,MongoDB会自动生成所需的支持索引。
块: MongoDB将分片数据组织成不同的部分,称为块或范围(从MongoDB 5.2开始,默认大小为128MB;在此版本之前为64MB)。每个块由分片键定义的包含下限和不包含上限所特征。这些块包含特定分片内一系列不间断的分片键值。从MongoDB 6.1开始,块不再自动分割。相反,块仅在跨分片移动时分割。
注意: 在MongoDB 6.1之前,当块大小超过最大块大小时,块会被自动分割器分割。
平衡器和均匀块分布: 从MongoDB 6.0.3开始,分片集群中的数据分布基于数据大小而非块的数量。为了确保块的平衡分布,一个名为平衡器的后台进程跟踪每个分片上每个分片集合的数据量,并在不同分片之间移动块。当特定分片上的分片集合的数据量达到特定的迁移限制时,平衡器尝试重新分配跨分片的数据,目标是在遵守区域的同时实现每个分片上的数据均匀分布。默认情况下,这个平衡过程是持续活跃的。
平衡器在配置服务器副本集(CSRS)的主节点上运行。对于分片集群的平衡过程对用户和应用层是完全透明的,尽管在执行期间可能会观察到轻微的性能影响。
为了减轻这种影响,平衡器尝试:
- 限制一个分片一次只参与一个迁移。换句话说,一个分片不能同时参与多个数据迁移。平衡器会一个接一个地执行范围迁移。
- MongoDB可以执行并行数据迁移;但是,一个分片在任何时刻仅限于参与一个迁移。在由n个分片组成的分片集群中,MongoDB可以执行多达n/2(向下取整)的并发迁移。
当特定分片集合的最重载分片与最轻载分片之间的数据差异达到迁移阈值时,开始一轮平衡。当分片之间的数据差异(对于该特定集合)小于集合的已建立范围大小的三倍时,认为集合是平衡的。如果范围大小设置为默认的128MB,则如果某个集合的任何两个分片之间的数据大小差异至少为384MB,将触发迁移。
可以为平衡器的操作设置特定时间框架,以防止其干扰生产流量。这称为平衡窗口或平衡器窗口。
块管理: 在某些情况下,需要手动管理块。
- 巨型块(Jumbo chunks): 在MongoDB中,超过指定范围大小并且不能由MongoDB自动分割的块被标记为巨型块。虽然MongoDB自动管理块分割和平衡,但在某些情况下需要手动干预来管理巨型块。清除巨型块标志的首选方法是尝试分割块。如果块可以被分割,MongoDB在成功分割块后将移除标志。可以使用sh.splitAt()或sh.splitFind()方法来分割巨型块。
- 不可分割的块(Indivisible chunks): 在某些情况下,MongoDB无法分割不再属于巨型块的块,例如,一个包含单个分片键值的范围的块。在这种情况下,您无法分割块以清除标志。在这些情况下,您可以修改分片键(您可以重新分片集合)以使块可分割,或者您可以手动移除标志。
要手动清除标志,在管理数据库中,发起clearJumboFlag命令,提供分片集合的命名空间和以下任一选项:
- 巨型块的边界:
db.adminCommand({
clearJumboFlag: "sample.customers",
bounds: [{ "x": 5 }, { "x": 6 }]
});
- 落在巨型块内的具有分片键和值的find文档:
db.adminCommand({
clearJumboFlag: "sample.customers",
find: { "x": 5 }
});
如果集合使用哈希分片键,请避免在使用clearJumboFlag时使用find字段。对于具有哈希分片键的集合,使用边界字段更为合适。
- 预分割范围(Pre-splitting the ranges): 在大多数情况下,分片集群将自动生成、分割和分配数据范围,无需手动监督。然而,在某些情况下,MongoDB无法产生足够的范围,或者以能够跟上必要吞吐量的速度分散数据。
如果您即将向新的分片集群加载大量数据,预分割可以帮助从一开始就将数据均匀分布在各个分片上。这可以防止单个分片成为瓶颈。
注意: 建议仅对当前为空的集合预分割范围。尝试为已经包含数据的集合手动分割范围可能导致不规则的范围边界和大小,并且也可能导致平衡行为运行效率低下或根本不运行。
要手动分割空范围,可以使用分割命令。该命令将分片集群中的一个块分割成两个单独的块。分割命令必须在管理数据库中执行。
让我们来看一个例子。假设您有一个名为myapp.products的集合,您希望根据指定的分割点将范围预分割为四个不同的价格类别。分片键设置在价格字段上。可以使用以下代码在mongosh中实现:
// 根据指定的分割点将集合分割为块
var splitPoints = [20, 50, 100];
// 注意:'split'命令可以采用middle、find或bounds作为选项。
// 在这个例子中,您使用middle选项来指定分割点。
for(var i = 0; i < splitPoints.length; i++) {
db.adminCommand({
split: "myapp.products",
middle: {
price: splitPoints[i]
}
});
}
这段代码将数据分为四个价格类别:
- 价格低于20美元的产品
- 价格在20至50美元之间的产品
- 价格在50至100美元之间的产品
- 价格超过100美元的产品
从MongoDB 6.0开始,平衡器根据数据大小在分片之间分配数据。仅仅分割范围可能无法确保数据在分片之间均匀分布。因此,您必须手动移动块以确保平衡分布:
// 您希望将块移动到的分片列表
var shards = ["shard0000", "shard0001", "shard0002", "shard0003"];
for (var i = 0; i 0) {
lowerBound = { price: splitPoints[i-1] };
}
var upperBound = { price: MaxKey };
if (i < splitPoints.length - 1) {
upperBound = { price: splitPoints[i] };
}
// 手动将块移动到所需的分片
db.adminCommand({
moveChunk: "myapp.products",
find: lowerBound,
to: shards[i],
bounds: [lowerBound, upperBound]
});
}
这种方法有助于从一开始就在分片之间均匀分配数据。
查询分片数据: 查询MongoDB分片集群中的数据与在单服务器部署或副本集中查询数据不同。您不是连接到单个服务器或副本集的主节点,而是连接到mongos,它充当查询路由器并决定向哪个分片请求数据。在下一节中,您将探索mongos路由器的工作原理。
mongos路由器: mongos实例提供对您的MongoDB集群的唯一接口和入口点。应用程序连接到mongos而不是直接连接到底层的分片。mongos执行查询,收集结果,并将它们传递给应用程序。mongos进程不持有任何持久状态,通常也不会使用大量的系统资源。它作为请求的代理。当查询进来时,mongos检查它,决定需要在哪些分片上执行查询,并在所有目标分片上建立游标。
find: 如果查询包含分片键或分片键的前缀,mongos将执行针对性操作,只查询包含您要查找的键的分片。
假设用户集合的复合分片键是_id, email, country:
db.user.find({ _id: 1 })
db.user.find({ _id: 1, "email": "packt@packt.com" })
db.user.find({ _id: 1, "email": "packt@packt.com" , "country": "UK" })
这些查询包含前缀(前两个查询的情况)或完整的分片键。另一方面,对{ email, country }或{ country }的查询将无法定位正确的分片,导致广播操作。广播操作是任何不包含分片键或分片键前缀的操作,导致mongos查询每个分片。它们也称为散布-收集操作或扇出查询。
sort(), limit(), 和 skip(): 如果您想对结果进行排序,您有两种选择:
- 如果您在排序条件中使用分片键,那么mongos可以确定它必须以何种顺序查询一个或多个分片。这导致了一个高效和有针对性的操作。
- 如果您在排序条件中不使用分片键,那么就像没有任何排序条件的查询一样,它将是一个扇出查询。当您在不使用分片键的情况下对结果进行排序时,主分片在将排序后的结果集传递给mongos之前,会在本地执行分布式合并排序。
查询的limit在每个单独的分片上强制执行,然后在mongos级别再次执行,因为可能有来自多个分片的结果。另一方面,skip运算符不能传递给单个分片,并将由mongos在检索所有结果后本地应用。
如果您组合使用skip()和limit()游标方法,mongos将通过将两个值传递给单个分片来优化查询。这在分页等情况下特别有用。如果您在没有sort()的情况下进行查询,并且结果来自多个分片,mongos将为结果在分片之间进行轮询。
更新和删除 从MongoDB 7.0开始,文档修改操作(如更新和删除)的处理流程已被简化。如果修改操作中的find()部分包含分片键,mongos可以继续将查询路由到适当的分片。然而,如果find部分中缺少分片键,它不再像早期版本那样触发扇出操作。
对于updateOne()、deleteOne()或findAndModify()操作,不再严格要求包含分片键或_id值。可以使用任何字段来匹配文档,类似于非分片集合。然而,使用分片键进行这些操作仍然更有效率,因为它允许针对性查询。
例如,在MongoDB 6.0中,您必须传递一个分片键来执行updateOne():
db.cities.updateOne({ "city": "New York City" }, { $set: { "population" : 8500000 }});
在这个例子中,city代表分片键。然而,在MongoDB 7.0中,您可以在不包含过滤条件中的分片键的情况下运行updateOne()操作:
db.cities.updateOne({ "population" : 293200 }, { $set: { "areaSize" : 211 }});
表2.3 总结了MongoDB 7.0中可用于分片的操作:
操作 |
描述 |
insert() |
必须包含分片键 |
update() |
可以包含分片键,但不是必须的 |
查询带分片键 |
针对性操作 |
查询不带分片键 |
后台进行散布-收集操作 |
索引排序,查询带分片键 |
针对性操作 |
索引排序,查询不带分片键 |
分布式排序合并 |
updateOne(), replaceOne(), deleteOne() |
可以使用任何字段进行匹配,但使用分片键更有效 |
表2.1: 分片操作
保护性读取 从MongoDB 4.4开始,mongos实例可以对使用非主读取偏好的读取操作进行保护性读取。mongos实例将每个查询分片的读取操作定向到副本集中的两个成员,然后返回每个分片的第一个响应结果。
支持保护性读取的操作包括:collStats、count、dataSize、dbStats、distinct、filemd5、find、listCollections、listIndexes和planCacheListFilters。
分片方法 为了管理数据的分布,MongoDB提供了一组辅助方法。这些方法用于启用分片、定义数据应如何分布以及监控分片的状态。它们是管理分片MongoDB部署的重要工具,允许在多台机器上高效地扩展数据。以下是各种mongosh shell辅助方法的列表:
- sh.shardCollection()对于在MongoDB中设置分片至关重要。一旦集合被分片,MongoDB不提供方法来取消分片。但是,如果需要,稍后可以更改分片键。
- db.collection.getShardDistribution()为特定分片集合提供了数据在分片上分布的详细分解。以下是此命令显示的输出信息摘要:
表2.2:db.collection.getShardDistribution()的输出信息。
输出 |
类型 |
描述 |
字符串 |
保存分片名称 |
|
字符串 |
保存主机名称(s) |
|
数字 |
包括数据大小,包括度量单位 |
|
数字 |
报告分片中的文档数量 |
|
数字 |
报告分片中的块数量 |
|
/ |
计算值 |
反映分片的每个块的估计数据大小,包括度量单位 |
/ |
计算值 |
反映分片的每个块的估计文档数量 |
数字 |
报告分片集合中数据的总大小,包括度量单位 |
|
数字 |
报告分片集合中的总文档数量 |
|
计算值 |
报告所有分片的块数量 |
|
计算值 |
表示每个分片的估计数据大小占集合总数据大小的百分比 |
|
计算值 |
反映每个分片的估计文档数量占集合总文档数量的百分比 |
- sh.status()提供关于分片集群的信息。可以通过verbose参数调整输出的详细程度,允许提供高级概览或详细报告。此命令提供的信息包括分片版本、每个分片的详细信息、活动mongos实例的状态、自动分割的状态、平衡器的状态以及数据库和分片集合的信息。
- 从MongoDB 5.0开始,sh.reshardCollection()方法使得修改集合的分片键以改变数据在集群中的分布成为可能。然而,在开始重新分片操作之前,请确保您的应用程序能够承受重新分片期间可能增加延迟的两秒写入阻塞。如果这是不可接受的,请考虑调整您的分片键。您的数据库服务器还应该拥有必要的资源:
- 存储:确保每个分片上可用的存储至少是将要重新分片的集合大小的2.2倍。例如,对于1TB的集合,每个分片至少应有2.2TB的可用存储。
- 计算每个分片所需的存储,使用以下公式:每个分片所需的可用存储 = (集合存储大小 * 2.2) / 集合将分布的分片数量
- 假设一个集合占用2TB的存储,并且分布在四个分片上,每个分片至少需要1.1TB的可用存储。
- 示例计算:(2TB * 2.2) / 4个分片 = 1.1TB/分片
- I/O:您的I/O容量不应超过50%。
- CPU使用率:保持CPU使用率低于80%。
- sh.getBalancerState()返回一个布尔值,指示平衡器当前是否处于活动状态。
- $shardedDataDistribution,在聚合阶段,返回有关分片集群中数据分布的信息。
MongoDB 7.0中的新分片集群特性
MongoDB 7.0继续简化了分片集群的管理和理解,无论是对于运维还是开发用例。这个版本提供了额外的洞察力,帮助为初始和未来的分片键选择做出最佳决策。此外,从MongoDB 7.0开始,开发者在使用这些命令时可以在分片和非分片集群上体验到一致的接口,同时在必要时仍然保留优化性能的选项。让我们看看MongoDB 7.0中引入的新分片特性。
分片键顾问命令
由于复杂的数据模式和权衡,选择分片键是一项复杂的任务。然而,MongoDB 7.0中的新特性旨在简化这项任务:
- analyzeShardKey 提供了评估候选分片键与现有数据的能力。MongoDB 7.0中的分片键分析提供了评估分片键适用性的指标,包括其唯一性(基数)、频率,以及它是否稳定增加或减少(单调性)。此命令可以在副本集以及分片集群上运行。
- configureQueryAnalyzer 提供了跨集群查询路由模式的指标,帮助发现负载不均衡和热分片。它返回一个文档,包含描述旧配置的字段(如果存在的话),以及描述新配置的字段。有了这两个命令,可以有信心地设置初始分片键,或为实时重新分片做好准备,背后有确保对所选分片键有信心的必要数据。
- mergeAllChunksOnShard 命令旨在整合或合并特定分片拥有的给定集合的所有可合并块。它通过查找特定分片上的所有可合并块,然后将它们合并,有助于解决分片维护操作期间性能下降的问题。这可以通过减少需要在分片维护操作期间查询的块数量,来帮助减少碎片化并提高性能。
AutoMerger
AutoMerger是一个自动合并符合某些可合并要求的块的特性。此过程作为平衡操作的一部分在后台运行。除非被禁用,否则AutoMerger在首次启用平衡器时启动,并在每次运行后暂停固定间隔(autoMergerIntervalSecs)。它在启用时的每个间隔执行自动合并。对于每个集合,它确保连续合并之间的最小延迟(autoMergerThrottlingMS)。如果定义了平衡窗口,AutoMerger仅在该窗口内运行。在满足以下所有条件的情况下,同一集合中的两个或多个连续块是可合并的:
- 属于同一个分片。
- 不是巨型块(不能参与迁移)。
- 具有可以安全清除的迁移历史,而不会干扰事务或快照读取。这意味着块的最后一次迁移至少发生在minSnapshotHistoryWindowInSeconds和transactionLifetimeLimitSeconds值之前。
您可以使用以下方法来控制AutoMerger的行为:
- sh.startAutoMerger()
- sh.stopAutoMerger()
- sh.enableAutoMerger()
- sh.disableAutoMerger()
无需分片键的命令支持
在分片和非分片集群上使用updateOne、deleteOne和findAndModify等命令的使用将是一致的,同时在需要时提供优化性能的选项。
总结
MongoDB中的复制是一个通过在多个服务器间同步数据来提供冗余和提高数据可用性的过程。这是通过副本集实现的,副本集是一组维护相同数据集的MongoDB服务器。在副本集中,一个节点作为主节点,接收所有写操作,而辅助节点将主节点的操作复制到它们自己的数据集中。这种结构为故障转移和恢复提供了一个健壮的系统。如果主节点失败,辅助节点之间的选举将确定一个新的主节点,允许客户端操作的连续性。
MongoDB中的分片是一种将数据分割和分布到多个服务器或分片上的方法。每个分片是一个独立的副本集,并且分片共同组成一个单一的逻辑数据库——分片集群。这种方法用于支持具有非常大数据集和高吞吐量操作的部署,有效地解决可扩展性问题。