精通-MongoDB-4-x-五-

68 阅读55分钟

精通 MongoDB 4.x(五)

原文:zh.annas-archive.org/md5/BEDE8058C8DB4FDEC7B98D6DECC4CDE7

译者:飞龙

协议:CC BY-NC-SA 4.0

第十三章:分片

分片是通过将数据集分区到不同服务器(分片)上来横向扩展我们的数据库的能力。这是 MongoDB 自 2010 年 8 月发布 1.6 版本以来的一个特性。Foursquare 和 Bitly 是 MongoDB 最著名的早期客户之一,从其推出一直到其正式发布都使用了分片功能。

在本章中,我们将学习以下主题:

  • 如何设计分片集群以及如何做出关于其使用的最重要决定——选择分片键

  • 不同的分片技术以及如何监视和管理分片集群

  • mongos路由器及其用于在不同分片之间路由我们的查询的方式

  • 我们如何从分片中恢复错误

为什么要使用分片?

在数据库系统和计算系统中,我们有两种方法来提高性能。第一种方法是简单地用更强大的服务器替换我们的服务器,保持相同的网络拓扑和系统架构。这被称为垂直扩展

垂直扩展的优点是从操作的角度来看很简单,特别是像亚马逊这样的云服务提供商只需点击几下就可以用m2.extralarge服务器实例替换m2.medium。另一个优点是我们不需要进行任何代码更改,因此几乎没有什么东西会出现灾难性的错误。

垂直扩展的主要缺点是存在限制;我们只能获得与云服务提供商提供给我们的服务器一样强大的服务器。

相关的缺点是获得更强大的服务器通常会带来成本的增加,这种增加不是线性的而是指数级的。因此,即使我们的云服务提供商提供更强大的实例,我们在部门信用卡的成本效益限制之前就会遇到成本效益的障碍。

提高性能的第二种方法是使用相同容量的相同服务器并增加它们的数量。这被称为水平扩展

水平扩展的优点在于理论上能够呈指数级扩展,同时对于现实世界的应用来说仍然足够实用。主要缺点是在操作上可能更加复杂,需要进行代码更改并在系统设计上进行仔细设计。水平扩展在系统方面也更加复杂,因为它需要在不太可靠的网络链接上的不同服务器之间进行通信,而不是在单个服务器上进行进程间通信。以下图表显示了水平和垂直扩展之间的区别:

要理解扩展,重要的是要了解单服务器系统的限制。服务器通常受以下一个或多个特征的限制:

  • CPU:CPU 受限系统是受 CPU 速度限制的系统。例如,可以放入 RAM 的矩阵相乘任务将受到 CPU 限制,因为 CPU 必须执行特定数量的步骤,而不需要进行任何磁盘或内存访问即可完成任务。在这种情况下,CPU 使用率是我们需要跟踪的指标。

  • I/O:输入输出受限系统同样受到存储系统(HDD 或 SSD)速度的限制。例如,从磁盘读取大文件加载到内存中的任务将受到 I/O 限制,因为在 CPU 处理方面几乎没有什么要做的;大部分时间都花在从磁盘读取文件上。需要跟踪的重要指标是与磁盘访问相关的所有指标,每秒读取次数和每秒写入次数,与我们存储系统的实际限制相比。

  • 内存和缓存:受内存限制和缓存限制的系统受到可用 RAM 内存和/或我们分配给它们的缓存大小的限制。一个将矩阵乘以大于我们 RAM 大小的任务将受到内存限制,因为它将需要从磁盘中分页数据来执行乘法。要跟踪的重要指标是已使用的内存。在 MongoDB MMAPv1 中,这可能会产生误导,因为存储引擎将通过文件系统缓存分配尽可能多的内存。

另一方面,在 WiredTiger 存储引擎中,如果我们没有为核心 MongoDB 进程分配足够的内存,内存不足的错误可能会导致其崩溃,这是我们要尽一切努力避免的。

监控内存使用量必须通过操作系统直接进行,并间接地通过跟踪分页数据来进行。增加的内存分页数通常表明我们的内存不足,操作系统正在使用虚拟地址空间来跟上。

作为数据库系统的 MongoDB 通常受到内存和 I/O 的限制。为我们的节点投资 SSD 和更多内存几乎总是一个不错的投资。大多数系统是前述限制的一个或多个组合。一旦我们增加了更多内存,我们的系统可能会变得 CPU 受限,因为复杂的操作几乎总是 CPU、I/O 和内存使用的组合。

MongoDB 的分片设置和操作非常简单,这也是它多年来取得巨大成功的原因,因为它提供了横向扩展的优势,而不需要大量的工程和运营资源。

也就是说,从一开始就正确地进行分片非常重要,因为一旦设置好了,从操作的角度来看,要更改配置是非常困难的。分片不应该是一个事后想法,而应该是设计过程早期的一个关键架构设计决策。

架构概述

一个分片集群由以下元素组成:

  • 两个或更多分片。每个分片必须是一个副本集。

  • 一个或多个查询路由器(mongos)。mongos提供了我们的应用程序和数据库之间的接口。

  • 一个副本集的配置服务器。配置服务器存储整个集群的元数据和配置设置。

这些元素之间的关系如下图所示:

从 MongoDB 3.6 开始,分片必须实现为副本集。

开发、持续部署和暂存环境

在预生产环境中,使用完整服务器集可能是过度的。出于效率原因,我们可能选择使用更简化的架构。

我们可以为分片部署的最简单配置如下:

  • 一个mongos路由器

  • 一个分片副本集,有一个 MongoDB 服务器和两个仲裁者

  • 一个配置服务器的副本集,有一个 MongoDB 服务器和两个仲裁者

这应严格用于开发和测试,因为这种架构违背了副本集提供的大多数优势,如高可用性、可扩展性和数据复制。

强烈建议在暂存环境中镜像我们的生产环境,包括服务器、配置和(如果可能)数据集要求,以避免在部署时出现意外。

提前计划分片

正如我们将在接下来的部分中看到的,分片在操作上是复杂且昂贵的。重要的是要提前计划,并确保我们在达到系统极限之前很久就开始分片过程。

一些关于何时需要开始分片的大致指导原则如下:

  • 当平均 CPU 利用率低于 70%时

  • 当 I/O(尤其是写入)容量低于 80%时

  • 当平均内存利用率低于 70%时

由于分片有助于写入性能,重要的是要关注我们的 I/O 写入容量和应用程序的要求。

不要等到最后一刻才开始在已经忙碌到极致的 MongoDB 系统中进行分片,因为这可能会产生意想不到的后果。

分片设置

分片是在集合级别执行的。我们可以有一些我们不想或不需要分片的集合,有几个原因。我们可以将这些集合保持为未分片状态。

这些集合将存储在主分片中。在 MongoDB 中,每个数据库的主分片都不同。在分片环境中创建新数据库时,MongoDB 会自动选择主分片。MongoDB 将选择在创建时存储数据最少的分片。

如果我们想在任何其他时间更改主分片,我们可以发出以下命令:

> db.runCommand( { movePrimary : "mongo_books", to : "UK_based" } )

有了这个,我们将名为mongo_books的数据库移动到名为UK_based的分片中。

选择分片键

选择我们的分片键是我们需要做出的最重要的决定:一旦我们分片我们的数据并部署我们的集群,更改分片键就变得非常困难。首先,我们将经历更改分片键的过程。

更改分片键

在 MongoDB 中,没有命令或简单的程序可以更改分片键。更改分片键的唯一方法涉及备份和恢复所有数据,这在高负载生产环境中可能从极其困难到不可能。

以下是我们需要经历的步骤,以更改分片键:

  1. 从 MongoDB 导出所有数据

  2. 删除原始的分片集合

  3. 使用新键配置分片

  4. 预先拆分新的分片键范围

  5. 将我们的数据恢复到 MongoDB 中

在这些步骤中,步骤 4 是需要进一步解释的步骤。

MongoDB 使用块来分割分片集合中的数据。如果我们从头开始引导 MongoDB 分片集群,MongoDB 将自动计算块。然后,MongoDB 将这些块分布到不同的分片上,以确保每个分片中有相等数量的块。

唯一不能真正做到这一点的时候是当我们想要将数据加载到新的分片集合中。

这样做的原因有三个:

  • MongoDB 仅在insert操作后创建拆分。

  • 块迁移将从一个分片复制该块中的所有数据到另一个分片。

  • floor(n/2)块迁移可以在任何时间发生,其中n是我们拥有的分片数量。即使有三个分片,这也只是一次floor(1.5)=1块迁移。

这三个限制意味着让 MongoDB 自行解决这个问题肯定会花费更长时间,并且可能最终导致失败。这就是为什么我们希望预先拆分我们的数据,并为 MongoDB 提供一些关于我们的块应该放在哪里的指导。

在我们的示例中,mongo_books数据库和books集合如下:

> db.runCommand( { split : "mongo_books.books", middle : { id : 50 } } )

middle命令参数将在我们的键空间中拆分文档,这些文档的id小于或等于50,以及id大于50的文档。我们的集合中没有必要存在id等于50的文档,因为这只会作为我们分区的指导值。

在这个例子中,我们选择了50,因为我们假设我们的键在值范围从0100中遵循均匀分布(即,每个值的键数量相同)。

我们应该努力创建至少 20-30 个块,以赋予 MongoDB 在潜在迁移中的灵活性。如果我们想手动定义分区键,我们也可以使用boundsfind而不是middle,但是这两个参数在应用它们之前需要数据存在于我们的集合中。

选择正确的分片键

在前面的部分之后,现在很明显我们需要考虑我们的分片键的选择,因为这是一个我们必须坚持的决定。

一个很好的分片键具有三个特点:

  • 高基数

  • 低频率

  • 值的非单调变化

我们将首先介绍这三个属性的定义,以了解它们的含义:

  • 高基数:这意味着分片键必须具有尽可能多的不同值。布尔值只能取true/false,因此不是一个好的分片键选择。一个可以取从−(2⁶³)2⁶³−1的任何值的 64 位长值字段在基数方面是一个好的选择。

  • 低频率:它直接关系到高基数的论点。低频率的分片键将具有接近完全随机/均匀分布的值分布。以我们 64 位长值的例子,如果我们一直观察到零和一这样的值,那么它对我们几乎没有用处。事实上,这和使用布尔字段一样糟糕,因为布尔字段也只能取两个值。如果我们有一个高频率值的分片键,我们最终会得到不可分割的块。这些块无法进一步分割,并且会增长,负面影响包含它们的分片的性能。

  • 非单调变化的值:这意味着我们的分片键不应该是一个每次新插入都增加的整数,例如。如果我们选择一个单调递增的值作为我们的分片键,这将导致所有写入最终都进入我们所有分片中的最后一个,从而限制我们的写入性能。

如果我们想要使用单调变化的值作为分片键,我们应该考虑使用基于哈希的分片。

在下一节中,我们将描述不同的分片策略,包括它们的优点和缺点。

基于范围的分片

默认和最广泛使用的分片策略是基于范围的分片。这种策略将把我们集合的数据分成块,将具有相邻值的文档分组到同一个分片中。

对于我们的示例数据库和集合,分别是mongo_booksbooks,我们有以下内容:

> sh.shardCollection("mongo_books.books", { id: 1 } )

这将在id上创建一个基于范围的分片键,并且是升序的。我们的分片键的方向将决定哪些文档将最终出现在第一个分片中,哪些文档出现在随后的分片中。

如果我们计划进行基于范围的查询,这是一个很好的策略,因为这些查询将被定向到保存结果集的分片,而不必查询所有分片。

基于哈希的分片

如果我们没有一个达到前面提到的三个目标的分片键(或者无法创建一个),我们可以使用替代策略,即使用基于哈希的分片。在这种情况下,我们正在用数据分布来交换查询隔离。

基于哈希的分片将获取我们的分片键的值,并以一种接近均匀分布的方式进行哈希。这样,我们可以确保我们的数据将均匀分布在分片中。缺点是只有精确匹配查询将被路由到持有该值的确切分片。任何范围查询都必须从所有分片中获取数据。

对于我们的示例数据库和集合(分别是mongo_booksbooks),我们有以下内容:

> sh.shardCollection("mongo_books.books", { id: "hashed" } )

与前面的示例类似,我们现在将id字段作为我们的哈希分片键。

假设我们使用浮点值字段进行基于哈希的分片。如果我们的浮点数的精度超过2⁵³,那么我们将会遇到碰撞。在可能的情况下,应该避免使用这些字段。

提出我们自己的键

基于范围的分片不需要局限于单个键。事实上,在大多数情况下,我们希望结合多个键来实现高基数和低频率。

一个常见的模式是将低基数的第一部分(但仍具有两倍于我们拥有的分片数量的不同值的数量)与高基数键作为其第二字段组合。这既从分片键的第一部分实现了读取和写入分布,又从第二部分实现了基数和读取局部性。

另一方面,如果我们没有范围查询,那么我们可以在主键上使用基于哈希的分片,因为这将精确地定位我们要查找的分片和文档。

使事情变得更加复杂的是,这些考虑因我们的工作负载而改变。几乎完全由读取(比如 99.5%)组成的工作负载不会关心写入分布。我们可以使用内置的_id字段作为我们的分片键,这只会给最后一个分片增加 0.5%的负载。我们的读取仍然会分布在各个分片上。不幸的是,在大多数情况下,情况并不简单。

基于位置的数据

由于政府法规和希望将数据尽可能靠近用户,通常存在对特定数据中心中的数据进行限制和需要的约束。通过将不同的分片放置在不同的数据中心,我们可以满足这一要求。

每个分片本质上都是一个副本集。我们可以像连接副本集一样连接到它进行管理和维护操作。我们可以直接查询一个分片的数据,但结果只会是完整分片结果集的子集。

分片管理和监控

与单服务器或副本集部署相比,分片的 MongoDB 环境具有一些独特的挑战和限制。在本节中,我们将探讨 MongoDB 如何使用 chunks 平衡我们的数据跨分片,并在需要时如何调整它们。我们将一起探讨一些分片设计的限制。

平衡数据-如何跟踪和保持我们的数据平衡

在 MongoDB 中分片的一个优点是,它对应用程序基本上是透明的,并且需要最少的管理和运营工作。

MongoDB 需要不断执行的核心任务之一是在分片之间平衡数据。无论我们实现基于范围还是基于哈希的分片,MongoDB 都需要计算哈希字段的边界,以便确定将每个新文档插入或更新到哪个分片。随着数据的增长,这些边界可能需要重新调整,以避免出现大部分数据都集中在一个热分片上的情况。

为了举例说明,假设有一种名为extra_tiny_int的数据类型,其整数值范围为-12, 12)。如果我们在这个extra_tiny_int字段上启用分片,那么我们数据的初始边界将是由$minKey: -12$maxKey: 11表示的整个值范围。

在我们插入一些初始数据后,MongoDB 将生成 chunks 并重新计算每个 chunk 的边界,以尝试平衡我们的数据。

默认情况下,MongoDB 创建的初始 chunk 数量是2 × 分片数量

在我们的情况下,有两个分片和四个初始 chunk,初始边界将如下计算:

Chunk1: [-12..-6)

Chunk2:  [-6..0)

Chunk3:  [0..6)

*Chunk4:  [6,12)其中'['是包含的,')'*是不包含的

以下图表说明了前面的解释:

![在我们插入一些数据后,我们的 chunks 将如下所示:+ ShardA:+ Chunk1:* -12,-8,-7*+ Chunk2:*  -6*+ ShardB:+ Chunk3:* 0, 2      *+ Chunk4: 7,8,9,10,11,11,11,11以下图表说明了前面的解释:

在这种情况下,我们观察到 chunk4的项比任何其他 chunk 都多。MongoDB 将首先将chunk4分成两个新的 chunk,试图保持每个 chunk 的大小在一定的阈值以下(默认为 64 MB)。

现在,我们有chunk4A7,8,9,10chunk4B11,11,11,11,而不是 chunk4

以下图表说明了先前的解释:

其新边界如下:

  • chunk4A: 6,11)

  • chunk4B: [11,12)

请注意,chunk4B只能容纳一个值。这现在是一个不可分割的分片,无法再分割成更小的分片,并且将无限增长,可能会导致性能问题。

这解释了为什么我们需要使用高基数字段作为我们的分片键,以及为什么像布尔值这样只有true/false值的字段是分片键的不良选择。

在我们的情况下,ShardA现在有两个分片,ShardB有三个分片。让我们看看下表:

分片数量迁移阈值
≤192
20-794
≥808

我们还没有达到迁移阈值,因为3-2 = 1

迁移阈值是根据拥有最多分片的分片和拥有最少分片的分片数量计算得出的,如下所示:

  • Shard1 -> 85 chunks

  • Shard2 -> 86 chunks

  • Shard3 -> 92 chunks

在上面的例子中,直到Shard3(或Shard2)达到93个分片之前,平衡都不会发生,因为迁移阈值对于≥80个分片是8,而Shard1Shard3之间的差距仍然是7个分片(92-85)。

如果我们继续在chunk4A中添加数据,它最终将被分割成chunk4A1chunk4A2

现在ShardB有四个分片(chunk3chunk4A1chunk4A2chunk4B),ShardA有两个分片(chunk1chunk2)。

以下图表说明了分片与分片之间的关系:

![MongoDB 平衡器现在将从ShardB迁移一个分片到ShardA,因为4-2 = 2,达到了少于20个分片的迁移阈值。平衡器将调整两个分片之间的边界,以便能够更有效地查询(有针对性的查询)。以下图表说明了先前的解释:

从上图表中可以看出,MongoDB 将尝试将*>64* MB 的分片一分为二。如果我们的数据分布不均匀,那么两个结果分片之间的边界可能会完全不均匀。MongoDB 可以将分片分割成更小的分片,但不能自动合并它们。我们需要手动合并分片,这是一个复杂且操作成本高昂的过程。

分片管理

大多数情况下,我们应该让 MongoDB 来管理分片。我们应该在开始时手动管理分片,在接收到初始数据负载时,当我们将配置从副本集更改为分片时。

移动分片

要手动移动一个分片,我们需要在连接到mongosadmin数据库后发出以下命令:

> db.runCommand( { moveChunk : 'mongo_books.books' ,
 find : {id: 50},
 to : 'shard1.packtdb.com' } )

使用上述命令,我们将包含id: 50的文档(这必须是分片键)从mongo_books数据库的books集合移动到名为shard1.packtdb.com的新分片。

我们还可以更明确地定义我们要移动的分片的边界。现在的语法如下:

> db.runCommand( { moveChunk : 'mongo_books.books' ,
 bounds :[ { id : <minValue> } ,
 { id : <maxValue> } ],
 to : 'shard1.packtdb.com' } )

在这里,minValuemaxValue是我们从db.printShardingStatus()中获取的值。

在先前的示例中,对于chunk2minValue将是-6maxValue将是0

在基于哈希的分片中不要使用find。使用bounds代替。

更改默认的分片大小

要更改默认的分片大小,我们需要连接到mongos路由器,因此连接到config数据库。

然后我们发出以下命令将我们的全局chunksize更改为16 MB:

> db.settings.save( { _id:"chunksize", value: 16 } )

更改chunksize的主要原因来自于默认的 64 MB chunksize可能会导致比我们的硬件处理能力更多的 I/O。在这种情况下,定义较小的chunksize将导致更频繁但数据密度较小的迁移。

更改默认块大小有以下缺点:

  • 通过定义较小的块大小创建更多的拆分无法自动撤消。

  • 增加块大小不会强制进行任何块迁移;相反,块将通过插入和更新而增长,直到达到新的大小。

  • 降低块大小可能需要相当长的时间才能完成。

  • 如果较低的块大小需要遵守新的块大小,那么只有在插入或更新时才会自动拆分。我们可能有一些块不会进行任何写操作,因此大小不会改变。

块大小可以为 1 到 1024 MB。

巨型块

在罕见情况下,我们可能会遇到巨型块,即大于块大小且无法由 MongoDB 拆分的块。如果我们的块中的文档数量超过最大文档限制,也可能遇到相同的情况。

这些块将启用jumbo标志。理想情况下,MongoDB 将跟踪它是否可以拆分块,并且一旦可以,它将被拆分;但是,我们可能决定在 MongoDB 之前手动触发拆分。

这样做的方法如下:

  1. 通过 shell 连接到您的mongos路由器并运行以下命令:
> sh.status(true)
  1. 使用以下代码标识具有jumbo的块:
databases:
…
mongo_books.books
...
chunks:
…
 shardB  2
 shardA  2
 { "id" : 7 } -->> { "id" : 9 } on : shardA Timestamp(2, 2) jumbo
  1. 调用splitAt()splitFind()手动在mongo_books数据库的books集合上拆分id等于8的块,使用以下代码:
> sh.splitAt( "mongo_books.books", { id: 8 })

splitAt()函数将根据我们定义的拆分点进行拆分。两个新的拆分可能平衡也可能不平衡。

或者,如果我们想让 MongoDB 找到拆分块的位置,我们可以使用splitFind,如下所示:

> sh.splitFind("mongo_books.books", {id: 7})

splitFind短语将尝试找到id:7查询所属的块,并自动定义拆分块的新边界,使它们大致平衡。

在这两种情况下,MongoDB 将尝试拆分块,如果成功,它将从中删除jumbo标志。

  1. 如果前面的操作不成功,那么只有在这种情况下,我们应该首先尝试停止平衡器,同时验证输出并等待任何待处理的迁移完成,如下所示:
> sh.stopBalancer()
> sh.getBalancerState() > use config
while( sh.isBalancerRunning() ) {
 print("waiting...");
 sleep(1000);
} 

这应该返回false

  1. 等待任何waiting…消息停止打印,然后以与之前相同的方式找到带有jumbo标志的块。

  2. 然后在mongos路由器的config数据库中更新chunks集合,如下所示:

> db.getSiblingDB("config").chunks.update(
 { ns: "mongo_books.books", min: { id: 7 }, jumbo: true },
 { $unset: { jumbo: "" } }
)

前面的命令是一个常规的update()命令,第一个参数是find()部分,用于查找要更新的文档,第二个参数是要应用于它的操作($unset: jumbo flag)。

  1. 完成所有这些操作后,我们重新启用平衡器,如下所示:
> sh.setBalancerState(true)
  1. 然后,我们连接到admin数据库,将新配置刷新到所有节点,如下所示:
> db.adminCommand({ flushRouterConfig: 1 } )

在手动修改任何状态之前,始终备份config数据库。

合并块

正如我们之前所看到的,通常情况下,MongoDB 将调整每个分片的块边界,以确保我们的数据均匀分布。在某些情况下,这可能不起作用,特别是当我们手动定义块时,如果我们的数据分布出奇地不平衡,或者我们的分片中有许多delete操作。

拥有空块将引发不必要的块迁移,并使 MongoDB 对需要迁移的块产生错误印象。正如我们之前解释的那样,块迁移的阈值取决于每个分片持有的块的数量。拥有空块可能会触发平衡器,也可能不会在需要时触发平衡器。

只有当至少有一个块为空时,块合并才会发生,并且只会发生在相邻块之间。

要找到空块,我们需要连接到要检查的数据库(在我们的情况下是mongo_books),并使用runCommand,设置dataSize如下:

> use mongo_books
> db.runCommand({
 "dataSize": "mongo_books.books",
 "keyPattern": { id: 1 },
 "min": { "id": -6 },
 "max": { "id": 0 }
})

dataSize短语遵循database_name.collection_name模式,而keyPattern是我们为这个集合定义的分片键。

minmax值应该由我们在这个集合中拥有的数据块计算得出。在我们的情况下,我们已经在本章前面的示例中输入了chunkB的详细信息。

如果我们的查询边界(在我们的情况下是chunkB的边界)没有返回任何文档,结果将类似于以下内容:

{ "size" : 0, "numObjects" : 0, "millis" : 0, "ok" : 1 }

现在我们知道chunkB没有数据,我们可以像这样将它与另一个数据块(在我们的情况下,只能是chunkA)合并:

> db.runCommand( { mergeChunks: "mongo_books.books",
 bounds: [ { "id": -12 },
 { id: 0 } ]
 } )

成功后,这将返回 MongoDB 的默认ok状态消息,如下所示:

{ "ok" : 1 }

然后,我们可以通过再次调用sh.status()来验证ShardA上只有一个数据块。

添加和移除分片

向我们的集群添加一个新的分片就像连接到mongos,连接到admin数据库,并使用以下命令调用runCommand一样简单:

> db.runCommand( {
addShard: "mongo_books_replica_set/rs01.packtdb.com:27017", maxSize: 18000, name: "packt_mongo_shard_UK"
} )

这将从rs01.packtdb.com主机的端口27017上运行的mongo_books_replica_set复制集中添加一个新的分片。我们还将为这个分片定义数据的maxSize18000 MB(或者我们可以将其设置为0以不设限),新分片的名称为packt_mongo_shard_UK

这个操作将需要相当长的时间来完成,因为数据块将需要重新平衡和迁移到新的分片。

另一方面,移除一个分片需要更多的参与,因为我们必须确保在这个过程中不会丢失任何数据。我们按照以下步骤进行:

  1. 首先,我们需要确保负载均衡器已启用,使用sh.getBalancerState()。然后,在使用sh.status()db.printShardingStatus()listShards admin命令中任何一个来识别我们想要移除的分片后,我们连接到admin数据库并按以下方式调用removeShard
> use admin
> db.runCommand( { removeShard: "packt_mongo_shard_UK" } )

输出应该包含以下内容:

...
 "msg" : "draining started successfully",
 "state" : "started",
...
  1. 然后,如果我们再次调用相同的命令,我们会得到以下结果:
> db.runCommand( { removeShard: "packt_mongo_shard_UK" } )
…
"msg" : "draining ongoing",
 "state" : "ongoing",
 "remaining" : {
 "chunks" : NumberLong(2),
 "dbs" : NumberLong(3)
 },
…

结果中剩下的文档包含仍在传输的chunksdbs的数量。在我们的情况下,分别是23

所有命令都需要在admin数据库中执行。

移除分片时可能会出现额外的复杂情况,如果我们要移除的分片作为它包含的一个或多个数据库的主分片。主分片是在我们启用分片时由 MongoDB 分配的,因此当我们移除分片时,我们需要手动将这些数据库移动到一个新的分片。

  1. 我们可以通过查看removeShard()结果中的以下部分来确定是否需要执行此操作:
...
"note" : "you need to drop or movePrimary these databases",
 "dbsToMove" : [
 "mongo_books"
 ],
...

我们需要删除或movePrimary我们的mongo_books数据库。首先要确保我们连接到admin数据库。

在运行此命令之前,我们需要等待所有数据块完成迁移。

  1. 在继续之前,请确保结果包含以下内容:
 ..."remaining" : {
 "chunks" : NumberLong(0) }...
  1. 只有在我们确保要移动的数据块已经减少到零后,我们才能安全地运行以下命令:
> db.runCommand( { movePrimary: "mongo_books", to: "packt_mongo_shard_EU" })
  1. 这个命令将调用一个阻塞操作,当它返回时,应该有以下结果:
{ "primary" : "packt_mongo_shard_EU", "ok" : 1 }
  1. 在我们完成所有操作后再次调用相同的removeShard()命令应该返回以下结果:
> db.runCommand( { removeShard: "packt_mongo_shard_UK" } )

... "msg" : "removeshard completed successfully",
 "state" : "completed",
 "shard" : "packt_mongo_shard_UK"
 "ok" : 1
...
  1. 一旦statecompletedok1,就可以安全地移除我们的packt_mongo_shard_UK分片。

移除分片自然比添加分片更复杂。在对我们的实时集群执行潜在破坏性操作时,我们需要留出一些时间,希望一切顺利,并为最坏的情况做好准备。

分片限制

分片提供了很大的灵活性。不幸的是,在执行一些操作的方式上存在一些限制。

我们将在以下列表中突出显示最重要的部分:

  • group()数据库命令不起作用。无论如何都不应该使用group()命令;而是使用aggregate()和聚合框架,或者mapreduce()

  • db.eval()命令不起作用,出于安全原因,在大多数情况下应禁用。

  • 更新的$isolated选项不起作用。这是分片环境中缺少的功能。update()$isolated选项提供了保证,即如果我们一次更新多个文档,其他读者和写入者将不会看到一些文档被更新为新值,而其他文档仍将保留旧值。在非分片环境中实现这一点的方式是通过保持全局写锁和/或将操作序列化到单个线程,以确保update()受影响的文档的每个请求不会被其他线程/操作访问。这种实现意味着它不具备性能,并且不支持任何并发,这使得在分片环境中允许$isolated运算符成本过高。

  • 不支持查询的$snapshot运算符。find()游标中的$snapshot运算符防止文档在更新后由于移动到磁盘上的不同位置而出现多次在结果中。$snapshot运算符在操作上是昂贵的,通常不是硬性要求。替代它的方法是在查询中使用一个字段的索引,这个字段的键在查询期间不会改变。

  • 如果我们的查询不包含分片键,索引将无法覆盖我们的查询。在分片环境中,结果将来自磁盘,而不仅仅来自索引。唯一的例外是如果我们仅在内置的_id字段上进行查询,并且仅返回_id字段,那么 MongoDB 仍然可以使用内置索引来覆盖查询。

  • update()remove()操作的工作方式不同。在分片环境中,所有update()remove()操作必须包括要受影响的文档的_id或分片键;否则,mongos路由器将不得不在所有集合、数据库和分片上进行全表扫描,这在操作上将非常昂贵。

  • 跨分片的唯一索引需要包含分片键作为索引的前缀。换句话说,为了实现跨分片的文档唯一性,我们需要遵循 MongoDB 为分片所遵循的数据分布。

  • 分片键的大小必须达到 512 字节。分片键索引必须按照分片的关键字段以及可选的其他字段的升序排列,或者对其进行哈希索引。

文档中的分片键值也是不可变的。如果我们的User集合的分片键是email,那么在设置后我们就不能更新任何用户的email值。

查询分片数据

使用 MongoDB 分片查询我们的数据与单服务器部署或副本集不同。我们不是连接到单个服务器或副本集的主服务器,而是连接到决定要请求我们的数据的分片的mongos路由器。在本节中,我们将探讨查询路由器的操作方式,并使用 Ruby 来说明对开发人员来说这与副本集有多相似。

查询路由器

查询路由器,也称为mongos进程,充当我们 MongoDB 集群的接口和入口。应用程序连接到它,而不是连接到底层的分片和副本集;mongos执行查询,收集结果并将其传递给我们的应用程序。

mongos进程不持有任何持久状态,通常对系统资源要求较低。

mongos进程通常托管在与应用程序服务器相同的实例中。

它充当请求的代理。当查询进来时,mongos将检查并决定哪些分片需要执行查询,并在每个分片中建立一个游标。

查找

如果我们的查询包括分片键或分片键的前缀,mongos将执行有针对性的操作,只查询持有我们寻找的键的分片。

例如,在我们的User集合上使用{ _id,email,address }的复合分片键,我们可以使用以下任何查询进行有针对性的操作:

> db.User.find({_id: 1})
> db.User.find({_id: 1, email: 'alex@packt.com'})
> db.User.find({_id: 1, email: 'janluc@packt.com', address: 'Linwood Dunn'})

这些查询由前缀(与前两个情况相同)或完整的分片键组成。

另一方面,对{email,address}{address}的查询将无法定位正确的分片,导致广播操作。广播操作是不包括分片键或分片键前缀的任何操作,并且它们导致mongos查询每个分片并从中收集结果。它们也被称为分散和聚集操作扇出查询

这种行为是索引组织方式的直接结果,并且类似于我们在索引章节中确定的行为。

排序/限制/跳过

如果我们想对结果进行排序,我们有以下两个选项:

  • 如果我们在排序标准中使用分片键,那么mongos可以确定查询分片的顺序。这将导致高效且再次是有针对性的操作。

  • 如果我们在排序标准中不使用分片键,那么与没有任何排序标准的查询一样,它将成为一个扇出查询。在不使用分片键时对结果进行排序时,主分片在将排序结果集传递给mongos之前在本地执行分布式合并排序。

对每个单独的分片强制执行查询的限制,然后再次在mongos级别进行,因为可能来自多个分片的结果。另一方面,skip操作符无法传递给各个分片,并且在本地检索所有结果后由mongos应用。

如果我们结合skiplimit操作符,mongos将通过将两个值传递给各个分片来优化查询。这在分页等情况下特别有用。如果我们在没有sort的情况下查询,而结果来自多个分片,mongos将在分片之间进行轮询以获取结果。

更新/删除

在文档修改操作中,例如updateremove,我们与find中看到的情况类似。如果我们在修改器的find部分中有分片键,那么mongos可以将查询定向到相关的分片。

如果我们在find部分没有分片键,那么它将再次成为一个扇出操作。

UpdateOnereplaceOneremoveOne操作必须具有分片键或_id值。

以下表总结了我们可以在分片中使用的操作:

操作类型查询拓扑
插入必须具有分片键
更新可以具有分片键
具有分片键的查询目标操作
没有分片键的查询分散和聚集操作/扇出查询
具有分片键的索引、排序查询目标操作
具有分片键的索引、排序查询而没有分片键分布式排序合并

使用 Ruby 进行查询

使用 Ruby 连接到分片集群与连接到副本集没有区别。使用官方的 Ruby 驱动程序,我们必须配置client对象以定义一组mongos服务器,如下面的代码所示:

client = Mongo::Client.new('mongodb://key:password@mongos-server1-host:mongos-server1-port,mongos-server2-host:mongos-server2-port/admin?ssl=true&authSource=admin')

然后mongo-ruby-driver将返回一个client对象,这与从 Mongo Ruby 客户端连接到副本集没有区别。然后我们可以像在之前的章节中一样使用client对象,包括关于分片行为与独立服务器或具有查询和性能方面的副本集不同的所有注意事项。

与副本集的性能比较

开发人员和架构师总是在寻找比较副本集和分片配置性能的方法。

MongoDB 实现分片的方式是基于副本集。生产中的每个分片都应该是一个副本集。性能上的主要区别来自于扇出查询。当我们在没有分片键的情况下进行查询时,MongoDB 的执行时间受到最差表现的副本集的限制。此外,当使用没有分片键的排序时,主服务器必须在整个数据集上实现分布式归并排序。这意味着它必须收集来自不同分片的所有数据,对它们进行归并排序,并将它们作为排序后的数据传递给mongos。在这两种情况下,网络延迟和带宽限制可能会减慢操作的速度,而在副本集中则不会出现这种情况。

另一方面,通过拥有三个分片,我们可以将工作集需求分布在不同的节点上,从而从 RAM 中提供结果,而不是访问底层存储,HDD 或 SSD。

另一方面,写操作可以显著加快,因为我们不再受限于单个节点的 I/O 容量,而且我们可以在所有分片中进行写操作。总而言之,在大多数情况下,特别是在使用分片键的情况下,查询和修改操作都将因分片而显著加快。

分片键是分片中最重要的决定,并且应反映和应用于我们最常见的应用程序用例。

分片恢复

在本节中,我们将探讨不同的故障类型以及在分片环境中如何进行恢复。在分布式系统中,故障可能以多种形式出现。在本节中,我们将涵盖所有可能的情况,从像mongos这样的无状态组件失败到整个分片宕机的最简单情况。

mongos

mongos进程是一个相对轻量级的进程,不保存状态。如果该进程失败,我们只需重新启动它或在不同的服务器上启动一个新进程。建议将mongos进程放置在与我们的应用程序相同的服务器上,因此从我们的应用程序使用我们在应用程序服务器中共同放置的一组mongos服务器连接是有意义的,以确保mongos进程的高可用性。

mongod

在分片环境中,mongod进程失败与在副本集中失败没有区别。如果是一个 secondary,primary 和其他 secondary(假设是三节点副本集)将继续正常运行。

如果是一个mongod进程充当 primary,那么选举将开始选举该分片(实际上是一个副本集)中的新 primary。

在这两种情况下,我们应该积极监视并尽快修复节点,因为我们的可用性可能会受到影响。

配置服务器

从 MongoDB 3.4 开始,配置服务器也被配置为副本集。配置服务器的故障与常规的mongod进程故障没有区别。我们应该监视、记录和修复该进程。

一个分片宕机

失去整个分片是非常罕见的,在许多情况下可以归因于网络分区而不是失败的进程。当一个分片宕机时,所有会发送到该分片的操作都将失败。我们可以(而且应该)在应用程序级别实现容错,使我们的应用程序能够恢复完成的操作。

选择一个可以轻松映射到我们操作方面的分片键也可以帮助;例如,如果我们的分片键是基于位置的,我们可能会失去 EU 分片,但仍然能够通过我们的 US 分片写入和读取关于美国客户的数据。

整个集群宕机

如果我们失去整个集群,除了尽快恢复运行之外,我们无法做任何其他事情。重要的是要进行监视,并制定适当的流程,以了解如果发生这种情况,需要在何时以及由谁来完成。

当整个集群崩溃时,恢复基本上涉及从备份中恢复并设置新的分片,这很复杂并且需要时间。在测试环境中进行干测试也是明智的选择,另外,通过 MongoDB Ops Manager 或任何其他备份解决方案进行定期备份也是明智的选择。

为了灾难恢复目的,每个分片的副本集成员可能位于不同的位置。

进一步阅读

以下资源建议您深入学习分片:

摘要

在本章中,我们探讨了 MongoDB 最有趣的功能之一,即分片。我们从分片的架构概述开始,然后讨论了如何设计分片并选择正确的分片键。

我们学习了监控、管理和分片带来的限制。我们还学习了mongos,MongoDB 的分片路由器,它将我们的查询定向到正确的分片。最后,我们讨论了在 MongoDB 分片环境中从常见故障类型中恢复。

下一章关于容错和高可用性将提供一些有用的技巧和窍门,这些内容在其他 11 章中没有涉及。

第十四章:容错和高可用性

在本章中,我们将尝试整合我们在之前章节中没有讨论的信息,并且我们将强调一些其他主题。在之前的 13 章中,我们从基本概念一直到有效查询,到管理和数据管理,到扩展和高可用性概念都有所涉及。

在本章中,我们将涵盖以下主题:

  • 我们将讨论我们的应用程序设计应该如何适应和积极应对我们的数据库需求。

  • 我们还将讨论日常运营,包括可以帮助我们避免未来不愉快的惊喜的提示和最佳实践。

  • 鉴于勒索软件最近试图感染和挟持 MongoDB 服务器,我们将提供更多关于安全性的建议。

  • 最后,我们将尝试总结已经给出的一系列应该遵循以确保最佳实践得到适当设置和遵循的建议清单。

应用程序设计

在本节中,我们将描述一些应用设计的有用提示,这些提示在之前的章节中我们没有涵盖或强调足够。

无模式并不意味着无模式设计

MongoDB 成功的一个重要原因是其 ORM/ODM 的日益流行。特别是对于像 JavaScript 和 MEAN 堆栈这样的语言,开发人员可以从前端(Angular/Express)到后端(Node.js)再到数据库(MongoDB)使用 JavaScript。这经常与一个 ODM 结合使用,它将数据库的内部抽象出来,将集合映射到 Node.js 模型。

主要优点是开发人员不需要纠缠数据库模式设计,因为这是由 ODM 自动提供的。缺点是数据库集合和模式设计留给了 ODM,它没有不同领域和访问模式的业务领域知识。

在 MongoDB 和其他基于 NoSQL 的数据库的情况下,这归结为基于不仅是即时需求,还有未来需求的架构决策。在架构层面上,这可能意味着我们可以通过使用图数据库进行图相关查询,使用关系数据库进行分层、无限数据的查询,以及使用 MongoDB 进行 JSON 检索、处理和存储,而不是采用单块方法。

事实上,MongoDB 成功的许多用例来自于它并不被用作一刀切的解决方案,而只用于有意义的用例。

读取性能优化

在本节中,我们将讨论一些优化读取性能的提示。读取性能与查询数量及其复杂性直接相关。在没有复杂嵌套数据结构和数组的模式中执行较少的查询通常会导致更好的读取性能。然而,很多时候,为了优化读取性能可能意味着写入性能会下降。这是需要记住并在进行 MongoDB 性能优化时不断测量的事情。

整合读取查询

我们应该尽量减少查询。这可以通过将信息嵌入子文档中而不是拥有单独的实体来实现。这可能会导致写入负载增加,因为我们必须在多个文档中保留相同的数据点,并在一个地方更改时在所有地方维护它们的值。

这里的设计考虑如下:

  • 读取性能受益于数据复制/去规范化。

  • 数据完整性受益于数据引用(DBRef或在应用程序代码中,使用属性作为外键)。

我们应该去规范化,特别是如果我们的读/写比太高(我们的数据很少更改值,但在中间被多次访问),如果我们的数据可以承受短暂时间的不一致,最重要的是,如果我们绝对需要我们的读取尽可能快,并且愿意以一致性/写入性能为代价。

我们应该对需要去规范化(嵌入)的字段进行特别处理。如果我们有一个属性或文档结构,我们不打算单独查询它,而只作为包含属性/文档的一部分,那么将其嵌入而不是放在单独的文档/集合中是有意义的。

使用我们的 MongoDB books示例,一本书可以有一个相关的数据结构,指的是书的读者的评论。如果我们最常见的用例是显示一本书以及其相关的评论,那么我们可以将评论嵌入到书的文档中。

这种设计的缺点是,当我们想要找到用户的所有书评时,这将是昂贵的,因为我们将不得不迭代所有书籍以获取相关的评论。对用户进行去规范化并嵌入他们的评论可以解决这个问题。

反例是可以无限增长的数据。在我们的例子中,将评论与大量元数据一起嵌入可能会导致问题,如果我们达到了 16 MB 文档大小限制。解决方案是区分我们预期会快速增长的数据结构和那些不会快速增长的数据结构,并通过监控过程来关注它们的大小,这些监控过程在非高峰时间查询我们的实时数据集,并报告可能会在未来造成风险的属性。

不要嵌入可以无限增长的数据。

当我们嵌入属性时,我们必须决定是使用子文档还是封闭数组。

当我们有一个唯一标识符来访问子文档时,我们应该将其嵌入为子文档。如果我们不确定如何访问它,或者我们需要灵活性来查询属性的值,那么我们应该将其嵌入到数组中。

例如,对于我们的 books 集合,如果我们决定将评论嵌入到每个书籍文档中,我们有以下两种设计选项:

  • 带有数组的书籍文档:
{
Isbn: '1001',
Title: 'Mastering MongoDB',
Reviews: [
{ 'user_id': 1, text: 'great book', rating: 5 },
{ 'user_id': 2, text: 'not so bad book', rating: 3 },
]
}
  • 嵌入文档的书籍:
{
Isbn: '1001',
Title: 'Mastering MongoDB',
Reviews:
{ 'user_id': 1, text: 'great book', rating: 5 },
{ 'user_id': 2, text: 'not so bad book', rating: 3 },
}

数组结构具有优势,我们可以通过嵌入的数组 reviews 直接查询 MongoDB 中所有评分大于 4 的评论。

另一方面,使用嵌入文档结构,我们可以以与使用数组相同的方式检索所有评论,但如果我们想要对其进行过滤,则必须在应用程序端进行,而不是在数据库端进行。

防御性编码

更多的是一个通用原则,防御性编码是指一组确保软件在意外情况下继续功能的实践和软件设计。

它优先考虑代码质量、可读性和可预测性可读性是由 John F. Woods 在 1991 年 9 月 24 日的comp.lang.c++*帖子中最好地解释的:

“编码时要像最终维护您的代码的人是一个知道您住在哪里的暴力精神病患者一样编码。为了可读性而编码。”

我们的代码应该对人类可读和理解,也应该对机器可读。通过静态分析工具派生的代码质量指标、代码审查和报告/解决的错误,我们可以估计我们的代码库的质量,并在每个冲刺或准备发布时,以及在每个冲刺或准备发布时,都可以达到一定的阈值。另一方面,代码的可预测性意味着我们应该始终期望在意外输入和程序状态下获得结果。

这些原则适用于每个软件系统。在使用 MongoDB 进行系统编程的情况下,我们必须采取一些额外的步骤,以确保代码的可预测性,以及随后的质量由产生的错误数量来衡量。

应该定期监控并评估导致数据库功能丧失的 MongoDB 限制,如下所示:

  • 文档大小限制:我们应该密切关注我们预计文档增长最多的集合,运行后台脚本来检查文档大小,并在接近限制(16 MB)的文档或平均大小自上次检查以来显着增长时向我们发出警报。

  • 数据完整性检查:如果我们使用反规范化进行读取优化,那么检查数据完整性是一个很好的做法。通过软件错误或数据库错误,我们可能会在集合中得到不一致的重复数据。

  • 模式检查:如果我们不想使用 MongoDB 的文档验证功能,而是想要一个宽松的文档模式,定期运行脚本来识别文档中存在的字段及其频率仍然是一个好主意。然后,结合相对访问模式,我们可以确定这些字段是否可以被识别和合并。如果我们从另一个系统中摄取数据,其中数据输入随时间变化,这可能导致我们端上文档结构变化很大,这个检查就非常有用。

  • 数据存储检查:这主要适用于使用 MMAPv1 时,其中文档填充优化可以提高性能。通过关注文档大小相对于其填充的情况,我们可以确保我们的大小修改更新不会导致文档在物理存储中移动。

这些是我们在为 MongoDB 应用程序进行防御性编码时应该实施的基本检查。除此之外,我们还需要在应用程序级别的代码上进行防御性编码,以确保当 MongoDB 发生故障时,我们的应用程序将继续运行——可能会有性能下降,但仍然可以运行。

一个例子是副本集故障转移和故障恢复。当我们的副本集主服务器失败时,会有一个短暂的时间来检测这个故障,并选举、提升和运行新的主服务器。在这个短暂的时间内,我们应该确保我们的应用程序继续以只读模式运行,而不是抛出 500 错误。在大多数情况下,选举新的主服务器只需要几秒钟,但在某些情况下,我们可能会处于网络分区的少数端,并且长时间无法联系主服务器。同样,一些次要服务器可能会处于恢复状态(例如,如果它们在复制方面落后于主服务器);在这种情况下,我们的应用程序应该能够选择另一个次要服务器。

设计用于次要访问的是防御性编码中最有用的例子之一。我们的应用程序应该权衡只能由主服务器访问的字段,以确保数据一致性,以及可以在几乎实时而不是实时更新的字段,在这种情况下,我们可以从次要服务器读取这些字段。通过使用自动化脚本跟踪我们次要服务器的复制延迟,我们可以了解我们集群的负载情况以及启用此功能的安全性。

另一个防御性编码实践是始终使用日志记录进行写入。日志记录有助于从服务器崩溃和电源故障中恢复。

最后,我们应该尽早使用副本集。除了性能和工作负载的改进外,它们还可以帮助我们从服务器故障中恢复。

监控集成

所有这些加起来都导致了对监控工具和服务的更广泛采用。尽管我们可以对其中一些进行脚本编写,但与云和本地监控工具集成可以帮助我们在更短的时间内取得更多成果。

我们跟踪的指标应该做到以下几点:

  • 检测故障:故障检测是一个被动的过程,我们应该制定清晰的协议,以应对每个故障检测标志触发时会发生什么。例如,如果我们失去了一个服务器、一个副本集或一个分片,应该采取什么恢复步骤?

  • 预防故障:另一方面,故障预防是一种积极的过程,旨在帮助我们在将来成为潜在故障源之前捕捉问题。例如,CPU/存储/内存使用情况应该被积极监控,并且应该制定清晰的流程,以确定在达到任一阈值时我们应该做什么。

操作

连接到我们的生产 MongoDB 服务器时,我们希望确保我们的操作尽可能轻量级(并且肯定不会破坏性地)并且不会以任何方式改变数据库状态。

我们可以将以下两个有用的实用程序链接到我们的查询中:

> db.collection.find(query).maxTimeMS(999)

我们的query将最多花费999毫秒的时间,然后返回超过时间限制的错误:

> db.collection.find(query).maxScan(1000)

我们的query将最多检查1000个文档,以查找结果然后返回(不会引发错误)。

在我们可以的情况下,我们应该通过时间或文档结果大小来限制我们的查询,以避免运行意外长时间的查询,这可能会影响我们的生产数据库。访问我们的生产数据库的常见原因是故障排除降级的集群性能。这可以通过云监控工具进行调查,正如我们在前几章中所描述的。

通过 MongoDB shell 的db.currentOp()命令,我们可以得到所有当前操作的列表。然后,我们可以分离出具有较大.secs_running值的操作,并通过.query字段对其进行识别。

如果我们想要终止长时间运行的操作,我们需要注意.opid字段的值,并将其传递给db.killOp(<opid>)

最后,从运营的角度来看,重要的是要认识到一切都可能出错。我们必须有一个一致实施的备份策略。最重要的是,我们应该练习从备份中恢复,以确保它按预期工作。

安全

在最近的勒索软件波之后,这些勒索软件锁定了不安全的 MongoDB 服务器,并要求管理员以加密货币支付赎金来解锁 MongoDB 服务器,许多开发人员变得更加注重安全。安全是我们作为开发人员可能没有高度优先考虑的检查表上的一项,这是由于我们乐观地认为这种情况不会发生在我们身上。事实上,在现代互联网环境中,每个人都可能成为自动化或有针对性攻击的目标,因此安全性应该始终被考虑在内,从设计的早期阶段到生产部署之后。

默认情况下启用安全性

每个数据库(除了本地开发服务器,也许)都应该在mongod.conf文件中设置如下内容:

auth = true

应该始终启用 SSL,正如我们在相关第八章中所描述的,监控、备份和安全

REST 和 HTTP 状态接口应通过向mongod.conf添加以下行来禁用:

nohttpinterface = true
rest = false

访问应该仅限于应用服务器和 MongoDB 服务器之间的通信,并且仅限于所需的接口。使用bind_ip,我们可以强制 MongoDB 监听特定接口,而不是默认绑定到每个可用接口的行为:

bind_ip = 10.10.0.10,10.10.0.20

隔离我们的服务器

我们应该使用 AWS VPC 或我们选择的云提供商的等效物来保护我们的基础设施边界。作为额外的安全层,我们应该将我们的服务器隔离在一个独立的云中,只允许外部连接到达我们的应用服务器,永远不允许它们直接连接到我们的 MongoDB 服务器:

我们应该投资于基于角色的授权。安全性不仅在于防止外部行为者造成的数据泄漏,还在于确保内部行为者对我们的数据具有适当的访问级别。通过 MongoDB 级别的基于角色的授权,我们可以确保我们的用户具有适当的访问级别。

考虑企业版用于大规模部署。企业版提供了一些方便的安全功能,更多地集成了知名工具,并且应该在大规模部署中进行评估,以满足随着我们从单个副本集过渡到企业复杂架构的不断变化的需求。

检查表

运营需要完成许多任务和复杂性。一个好的做法是保持一套包含所有需要执行的任务及其重要性顺序的检查表。这将确保我们不会漏掉任何事情。例如,部署和安全检查表可能如下所示:

  • 硬件

  • 存储:每个节点需要多少磁盘空间?增长率是多少?

  • 存储技术:我们是否需要 SSD 还是 HDD?我们的存储吞吐量是多少?

  • RAM:预期的工作集是多少?我们能否将其放入 RAM 中?如果不能,我们是否可以接受 SSD 而不是 HDD?增长率是多少?

  • CPU:这通常对 MongoDB 不是一个问题,但如果我们计划在我们的集群中运行 CPU 密集型作业(例如,聚合或 MapReduce),它可能是一个问题。

  • 网络:服务器之间的网络链接是什么?如果我们使用单个数据中心,这通常是微不足道的,但如果我们有多个数据中心和/或用于灾难恢复的离站服务器,情况可能会变得复杂。

  • 安全

  • 启用认证。

  • 启用 SSL。

  • 禁用 REST/HTTP 接口。

  • 隔离我们的服务器(例如,VPC)。

  • 已启用授权。伴随着强大的权力而来的是巨大的责任。确保强大的用户是您信任的用户。不要将潜在破坏性的权力赋予经验不足的用户。

监控和运营检查表可能如下所示:

  • 监控

  • 使用硬件(CPU、内存、存储和网络)。

  • 健康检查,使用 Pingdom 或等效服务,以确保我们在其中一个服务器失败时收到通知。

  • 客户端性能监控:定期集成神秘购物者测试,以客户的方式手动或自动化地进行,从端到端的角度,以找出它是否表现如预期。我们不希望从客户那里了解应用性能问题。

  • 使用 MongoDB Cloud Manager 监控;它有免费层,可以提供有用的指标,是 MongoDB 工程师在我们遇到问题并需要他们的帮助时可以查看的工具,特别是作为支持合同的一部分。

  • 灾难恢复

  • 评估风险:从业务角度来看,丢失 MongoDB 数据的风险是多少?我们能否重新创建这个数据集?如果可以,从时间和精力方面来看,成本是多少?

  • 制定计划:针对每种故障场景制定计划,包括我们需要采取的确切步骤。

  • 测试计划:对每个恢复策略进行干预与实施一样重要。在灾难恢复中可能会出现许多问题,拥有一个不完整的计划(或者在每个目的中失败的计划)是我们在任何情况下都不应该允许发生的事情。

  • 制定计划的备选方案:无论我们制定计划和测试计划有多么完善,计划、测试或执行过程中都可能出现问题。我们需要为我们的计划制定备用计划,以防我们无法使用计划 A 恢复我们的数据。这也被称为计划 B,或最后的后备计划。它不必高效,但应该减轻任何业务声誉风险。

  • 负载测试:我们应该确保在部署之前对我们的应用进行端到端的负载测试,使用真实的工作负载。这是确保我们的应用行为符合预期的唯一方法。

进一步阅读

您可以参考以下链接获取更多信息:

摘要

在本章中,我们涵盖了一些在之前章节中没有详细介绍的主题。根据我们的工作负载要求,应用最佳实践非常重要。阅读性能通常是我们要优化的内容;这就是为什么我们讨论了查询合并和数据去规范化。

当我们从部署转向确保集群的持续性能和可用性时,运营也很重要。安全性是我们经常忽视直到它影响我们的东西。这就是为什么我们应该事先投入时间来计划,并确保我们已经采取措施足够安全。

最后,我们介绍了清单的概念,以跟踪我们的任务,并确保在主要运营事件(部署、集群升级、从副本集迁移到分片等)之前完成所有任务。