MongoDB-权威指南-五-

97 阅读44分钟

MongoDB 权威指南(五)

原文:The Definitive Guide to Mongodb

协议:CC BY-NC-SA 4.0

十一、复制

Abstract

像它的许多关系表兄弟一样,MongoDB 支持实时或接近实时地将数据库的内容复制到另一个服务器。MongoDB 的复制特性设置和使用都很简单。它们也是 MongoDB 中的关键特性之一,与分片一起,支持数据库既是 Web 2.0 又是基于云的数据存储的说法。

像它的许多关系表兄弟一样,MongoDB 支持实时或接近实时地将数据库的内容复制到另一个服务器。MongoDB 的复制特性设置和使用都很简单。它们也是 MongoDB 中的关键特性之一,与分片一起,支持数据库既是 Web 2.0 又是基于云的数据存储的说法。

在很多情况下,您可能需要使用复制,因此 MongoDB 中的复制支持必须足够灵活,以便能够应对所有这些情况。MongoDB,Inc .的 MongoDB 架构师竭尽全力确保其复制实现满足当今的所有需求。

在本章中,我们将介绍 MongoDB 中复制的基础知识,包括以下主题:

  • MongoDB 中的复制是什么?
  • 什么是初选?
  • 什么是次要的?
  • 什么是操作日志?

Note

复制是 MongoDB 中继续发展的一个特性,随着产品的发展,您可以预期复制的工作方式会发生一些变化。对于数据库服务器的集群来说尤其如此。这本书的第一版和第二版之间已经有了一些变化。MongoDB,Inc .投入了相当大的努力来确保 MongoDB 满足并超越每个人对可伸缩性和可用性的期望;复制支持是 MongoDB 的关键特性之一。Inc 正指望帮助它满足这些期望。

在详细查看复制设置之前,让我们回顾一下各种设置要实现的目标。我们还将概述复制目前在 MongoDB 中如何工作的一些基础知识,并查看操作日志及其在副本集成员之间的数据复制中的作用。这些主题构成了理解复制的基础。

阐明 MongoDB 的复制目标

其中,复制可用于实现可伸缩性、持久性/可靠性和隔离。在接下来的部分中,我们将探索如何使用复制来实现这些目标,同时指出要避免的潜在陷阱和错误。

提高可扩展性

特别是对于 web 应用,可伸缩性是一个关键的设计要求,尤其是那些严重依赖后端数据库的应用。复制可以通过两种方式帮助您创建更具伸缩性的应用:

  • 提高冗余度:复制可以让您在多个数据中心托管一个应用,从而帮助您提高冗余度。在这种方法中,您确保每个数据中心都有数据的本地副本,以便应用可以高速访问它。然后,用户可以连接到离他们最近的数据中心,从而最大限度地减少延迟。
  • 提高性能:在某些情况下,复制可以帮助您提高应用的原始性能。当您有一个大型 web 应用,其中的数据集主要是基于读取的,并且您希望将查询分布到多个数据库服务器以提高并行性时,这种情况尤其如此。或者具有非常不同的工作集的查询负载,例如报告或聚合。

Note

MongoDB 还支持一个称为分片的特性,该特性旨在帮助您创建更具可伸缩性的应用,无论有无复制都可以实现真正的高可伸缩性。参见第 12 章了解更多关于在 MongoDB 中一起使用分片和复制的信息。

提高耐用性/可靠性

复制通常用于帮助防范硬件故障或数据库损坏,并在执行备份或其他潜在高影响维护活动时提供灵活性,影响很小或没有影响,因为这些任务可以在集合成员上单独执行,而不会影响整个集合。人们以这种方式使用复制的一些具体示例包括:

  • 当您希望拥有延迟运行的数据库的副本时。您可能希望保护自己免受应用缺陷的影响,或者提供一种简单的机制,通过突出显示两个数据集的查询结果之间的差异来提供趋势信息。这还可以为人为错误提供安全缓冲,并避免从备份中完全恢复的需要。
  • 当你需要一个备份系统以防失败时。如果在系统出现故障时,正常的备份方案需要很长时间才能恢复,您可能需要运行副本作为备份。
  • 当您出于管理目的需要冗余系统时。您可能希望运行一个复制副本,以便可以在节点之间轮换执行管理任务,如备份或升级。

提供隔离

如果对生产数据库运行某些进程,将会显著影响该数据库的性能或可用性。您可以使用复制来创建将流程与生产数据库隔离的同步副本,例如:

  • 当您希望在不影响生产系统性能的情况下运行报告或备份时:维护隐藏的辅助副本使您能够将查询与报告系统隔离开来,并确保月末报告不会延迟或影响您的正常操作。

复制基础

正如您所看到的,副本集(或 replSet)是一种设置多个 MongoDB 实例的方法,以包含相同的数据用于冗余和其他相关措施。除了了解这些,您还应该了解 MongoDB 是如何完成其复制的,这样您就知道如何最好地管理您自己的副本集。

您已经知道了 MongoDB 中复制的目标,如果您已经阅读了这本书的第一版或者从早期就开始使用 MongoDB,您将会知道有许多不同的方法可以完成复制,包括:

  • 主/从复制
  • 主/主复制
  • 复制对

这些复制方法都已被副本集的概念所取代。在 MongoDB 中,一个副本集由一个主节点和一些辅助或仲裁节点组成。副本集应该由奇数个成员组成,即最少三个。出现这一需求是因为 MongoDB 副本集有一个规则,即主节点必须能够看到大多数其他节点,以便允许它继续作为主节点。实施这一规则是为了避免“裂脑”的情况,即由于网络中的潜在故障,你有两个主要的,如图 11-1 所示。

A978-1-4302-5822-3_11_Fig1_HTML.jpg

图 11-1。

The Split-brain problem

什么是初选?

就副本集而言,主要副本是副本集在给定时刻的真实来源。它是集合中唯一可以写入数据的节点,所有其他节点都可以从该节点复制数据。初选由所有投票成员的多数票选举产生,即法定人数。

一旦选择了主节点,所有辅助节点将使用它作为其复制的真实源,因此所有写入都必须定向到该成员。

什么是次要的?

次要成员是携带数据的非主要成员,理论上可以(除了少数例外)成为主要成员。它是一个节点,可以以尽可能接近实时的方式从其数据集中的主节点读取和复制数据。默认情况下,如果在没有任何读取首选项的情况下直接连接到辅助节点,则无法执行读取操作。这样做是为了强调,对于任何对非主数据库的读取,如果复制有延迟,您可能会从较旧的数据中读取。您可以使用命令rs.slaveOk()来设置当前连接,以便从辅助节点读取数据。或者,如果你正在使用一个驱动程序,你可以设置一个读优先选项,我们将在本章后面讨论。

Note

初选的概念是短暂的,也应该是短暂的。也就是说,理想情况下,您应该没有哪个节点是主节点的“固定”概念。在副本集中,所有辅助节点都在写入与主节点相同的数据,以便跟上复制。因此,如果辅助节点的能力大大降低,它们可能无法应对升级为主节点的情况。

什么是仲裁人?

仲裁器是一个非数据承载节点,用于提供额外的投票,以帮助维持副本集选举的多数。它不投决定性的一票,也不指示哪个节点是主要节点,但是参与并可以成为决定主要节点的法定成员。仲裁器最好用于帮助避免前面描述的“裂脑”问题。考虑图 11-2 所示的图表。通过给 A 站点添加一个仲裁者,我们总是可以有一方可以创建多数。这意味着在网络中断的情况下,我们不会有两个初选!我们可以通过在第三个站点 c 中设置仲裁器来进一步增加冗余。这样,如果站点 A 发生故障,我们仍然可以从站点 B 和 c 中的节点中形成多数。使用这样的第三个站点,我们可以在失去与任何一个站点的连接时继续运行。

A978-1-4302-5822-3_11_Fig2_HTML.jpg

图 11-2。

Split Brain Problem Solved

深入操作日志

简而言之,操作日志(operation log)是一个有上限的集合,其中包含主实例对其数据库所做更改的滚动记录,目的是向辅助实例重放这些更改,以确保数据库是相同的。副本集的每个成员维护其自己的操作日志,并且辅助节点向主节点(或其他更新的辅助节点)的操作日志查询新条目以应用于它们自己的所有数据库副本。

操作日志为每个条目创建一个时间戳。这使得辅助节点能够跟踪它在前一次读取期间从操作日志读取了多远,以及它需要传输哪些条目来赶上。如果您停止辅助节点并在相对较短的时间后重新启动它,它将使用主节点的操作日志来检索它在离线时错过的所有更改。

因为拥有无限大的操作日志是不实际的,所以操作日志被限制或限定在特定的大小。

您可以将操作日志视为主实例最近活动的窗口;如果该窗口太小,操作将在应用到辅助节点之前从操作日志中丢失。如果尚未在当前实例上创建操作日志,默认情况下- oplogSize启动选项允许您以 MB 为单位设置操作日志的大小。对于 Linux 或 Windows 64 位系统,oplogSize将被设置为可用于数据存储的可用磁盘空间的 5%。如果您的系统是写/更新密集型的,那么您可能需要增加这个大小,以确保从机可以脱机一段合理的时间而不丢失数据。

例如,如果您有一个需要一个小时才能完成的从机每日备份,则必须设置操作日志的大小,以允许从机在该小时内保持离线,再加上额外的时间量,以提供安全余量。

在为操作日志计算合适的大小时,考虑主服务器上所有数据库的更新率是非常重要的。

您可以通过使用 db 来了解适合您的操作日志的大小。在主实例上运行的printReplicationInfo()命令:

$mongo

>db.printReplicationInfo()

configured oplog size: 15000MB

log length start to end: 6456672secs (1793.52hrs)

oplog first event time: Wed Mar 20 2013 17:00:43 GMT+1100 (EST)

oplog last event time: Mon Jun 03 2013 09:31:55 GMT+1000 (EST)

now: Mon Jun 03 2013 20:22:20 GMT+1000 (EST)

此命令显示您的操作日志的当前大小,以及以当前更新速率填满所需的时间。根据这些信息,您可以估计是否需要增加或减少操作日志的大小。您还可以通过查看 MongoDB Monitoring Service (MMS)中的repl lag部分来查看副本集的给定成员离主成员有多远。如果您还没有安装 MMS,我真的建议您现在就安装,因为您的 MongoDB 集群变得越来越大,MMS 提供的统计信息就变得越来越重要。要了解更多背景知识,您应该查看第 9 章的彩信部分。

实现副本集

在本节中,您将学习如何设置一个简单的副本集配置。您还将学习如何在集群中添加和删除成员。如前所述,副本集基于单个主服务器和多个辅助或仲裁服务器的概念,这些辅助或仲裁服务器将从主服务器复制写入(见图 11-3 )。

A978-1-4302-5822-3_11_Fig3_HTML.jpg

图 11-3。

A cluster implemented with a replica set

副本集还具有主动和被动成员的概念。当当前主服务器不可用时,被动辅助服务器不参与新主服务器的选举;相反,它们的作用与隐藏成员相同,可以用作报告或备份数据集。

启动时,副本集的成员服务器不需要被指定为集成员。相反,配置是通过普通服务器界面发送的服务器级命令来完成的。这使得创建允许动态配置和管理机器集群的配置管理工具变得更加容易。

在接下来的部分中,您将学习如何完成以下任务:

Create a replica set.   Add a server to a replica set.   Add an arbiter to a replica set.   Inspect and perform operations on a replica set.   Configure individual members of a replica set.   Connect to a replica set from your application.   Set Read Preference from within your application   Set Write Concern from within your application   Use Replica Set tags with Read Preference and Write Concern   Use the web interface to inspect the status of your replica set  

创建副本集

学习如何创建副本集的最好方法是看一个例子。在下面的例子中,您将创建一个名为testset的副本集。该集合将有三个成员(两个主动成员和一个被动成员)。表 11-1 列出了这个集合的成员。

表 11-1。

Configuring the Replica Set

| 服务 | 守护进程 | 地址 | 数据库路径 | | --- | --- | --- | --- | | 活动成员 1 | `mongod` | `[hostname]:27021` | `/db/active1/data` | | 活动成员 2 | `mongod` | `[hostname]:27022` | `/db/active2/data` | | 被动构件 1 | `mongod` | `[hostname]:27023` | `/db/passive1/data` |

副本集将允许您使用 localhost 作为标识符,但仅当所有机器都位于一台服务器上时。发生这种情况是因为副本集的每个成员必须能够通过主机名联系所有其他 MongoDB 实例,复制才能正常工作。

通常当使用副本集时,我们使用主机名;您可以使用the hostname命令找到当前主机名,如下例所示:

$hostname

Pixl.local

在接下来的例子中,通过在您自己的系统上运行hostname命令,将术语[hostname]替换为返回的任何值。

启动并运行副本集成员

第一步是让第一个活动成员启动并运行。为此,请打开终端窗口并键入以下内容:

$ mkdir -p /db/active1/data

$ mongod --dbpath /db/active1/data --port 27021 --replSet testset

--replSet选项告诉实例它正在加入的副本集的名称。这是副本集的第一个成员,因此您可以给它任何其他成员的地址,即使该成员尚未启动。只需要一个成员地址,但您也可以提供其他成员的名称,方法是用逗号分隔他们的地址,如下例所示:

$ mongod --dbpath /db/active1/data --port 27021 --replSet testset

Note

如果您不希望在自己的 shell 实例中运行这些 MongoDB 实例,您可以添加–-fork–-logpath < file >选项,告诉这个实例在后台打开自己,并将其日志记录到指定的文件。

为了简单起见,这个例子只依赖一个地址。下一步是让其他成员开始工作。为此,请再打开两个终端窗口,然后在第一个窗口中键入以下内容,以启动并运行第二个成员:

$ mkdir -p /db/active2/data

$ mongod --dbpath /db/active2/data --port 27022 --replSet testset

接下来,在第二个窗口中键入以下内容,启动并运行最后一个(被动)成员:

$ mkdir -p /db/passive1/data

$ mongod --dbpath /db/passive1/data --port 27023 --replSet testset

此时,您有三个服务器实例正在运行并相互通信;但是,您还没有运行您的副本集,因为您还没有初始化副本集,也没有告诉每个成员它的角色和职责。

为此,您需要连接到其中一个服务器并初始化副本集。以下代码选择要连接的第一台服务器:

$mongo [``hostname

接下来,您需要初始化该集合的第一个成员,以创建它的操作日志和默认配置文档。您可以在日志文件中看到 MongoD 实例建议您需要这样做:

Mon Jun 3 21:25:23.712 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)

Mon Jun 3 21:25:23.712 [rsStart] replSet info you may need to run replSetInitiate -- rs.initiate() in the shell -- if that is not already done

所以运行rs.initiate命令:

> rs.initiate()

{

"info2" : "no configuration explicitly specified -- making one"

"me" : "[hostname]:27021"

"info" : "Config now saved locally. Should come online in about a minute."

"ok" : 1

}

最后,您应该检查副本集的状态,以确定其设置是否正确:

>rs.status()

{

"set" : "testset"

"date" : ISODate("2013-06-03T11:28:58Z")

"myState" : 1

"members" : [

{

"_id" : 0

"name" : "[hostname]:27021"

"health" : 1

"state" : 1

"stateStr" : "PRIMARY"

"uptime" : 264

"optime" : {

"t" : 1370258919

"i" : 1

}

"optimeDate" : ISODate("2013-06-03T11:28:39Z")

"self" : true

}

]

"ok" : 1

}

这里的输出表明一切正常:您已经成功地配置、设置和初始化了一个新的副本集。请记住,您应该使用您自己的机器名称来代替[hostname],因为“localhost”和“127.0.0.1”都不起作用。

将服务器添加到副本集

现在您已经启动了新的副本集,您需要开始向其中添加成员。让我们从添加第一个辅助节点开始。只需添加rs.add()命令并提供该实例的主机名和端口,就可以做到这一点。要添加,请连接到您的主服务器并运行以下命令:

$ mongo [``hostname

> rs.add("[``hostname

{ "ok" : 1 }

您需要等待一两分钟,因为该节点会使自己联机,创建自己的操作日志,并准备好自己。您可以使用rs.status()监控进度,同时等待该节点作为辅助节点联机:

>use admin

>rs.status() {

"set" : "testset"

"date" : ISODate("2013-06-03T11:36:37Z")

"myState" : 1

"members" : [

{

"_id" : 0

"name" : "[hostname]:27021"

"health" : 1

"state" : 1

"stateStr" : "PRIMARY"

"uptime" : 723

"optime" : {

"t" : 1370259331

"i" : 1

}

"optimeDate" : ISODate("2013-06-03T11:35:31Z")

"self" : true

}

{

"_id" : 1

"name" : "[hostname]:27022"

"health" : 1

"state" : 2

"stateStr" : "SECONDARY"

"uptime" : 66

"optime" : {

"t" : 1370259331

"i" : 1

}

"optimeDate" : ISODate("2013-06-03T11:35:31Z")

"lastHeartbeat" : ISODate("2013-06-03T11:36:35Z")

"lastHeartbeatRecv" : ISODate("2013-06-03T11:36:36Z")

"pingMs" : 0

"syncingTo" : "[hostname]:27021"

}

]

"ok" : 1

}

现在让我们利用第三个被动成员。首先像往常一样用rs.add()添加成员:

$ mongo [hostname]:27022

> rs.add("[hostname]:27022")

{ "ok" : 1 }

现在我们需要制作配置文件的副本并修改它。运行下面的命令创建一个名为conf的文档,其中包含您当前的副本集配置。

> conf = rs.conf()

{

"_id" : "testset"

"version" : 3

"members" : [

{

"_id" : 0

"host" : "[hostname]:27021"

}

{

"_id" : 1

"host" : "[hostname]:27022"

}

{

"_id" : 2

"host" : "[hostname]:27023"

}

]

}

现在您的配置文档已经加载,您需要修改它。我们希望将节点设置为隐藏,优先级为 0,因此它不会被选为主要节点。注意,文档有一个members数组,其中包含副本集每个成员的文档。您需要使用数组操作符[]选择您希望访问的成员。因此,要为第三个成员创建一个值hidden : true,我们需要在 2 处更新数组元素(记住,数组从 0 开始)。运行以下命令:

> conf.members[2].hidden = true

true

现在,我们可以使用相同的命令将优先级值设置为 0:

> conf.members[2].priority = 0

0

您可以通过简单地执行放置配置文档的变量的名称来输出这个配置文档:

> conf

{

"_id" : "testset"

"version" : 3

"members" : [

{

"_id" : 0

"host" : "[hostname]:27021"

}

{

"_id" : 1

"host" : "[hostname]:27022"

}

{

"_id" : 2

"host" : "[hostname]:27023"

"hidden" : true

"priority" : 0

}

]

}

如您所见,这个成员现在拥有隐藏值集和优先级 0。现在我们需要做的就是更新副本集配置来使用这个文档。我们通过使用新的配置文档作为参数发出rs.reconfig()命令来实现这一点。

> rs.reconfig(conf)

Tue Jun 4 20:01:45.234 DBClientCursor::init call() failed

Tue Jun 4 20:01:45.235 trying reconnect to 127.0.0.1:27021

Tue Jun 4 20:01:45.235 reconnect 127.0.0.1:27021 ok

reconnected to server after rs command (which is normal)

您的整个副本集失去连接,然后重新连接!发生这种情况是因为重新配置。对副本集的任何更改都可能导致副本集重新配置自己并进行新的选举,在大多数情况下,这将使先前的主要步骤回到其角色中。现在,如果我们重新运行rs.conf()命令,您可以看到新的副本集配置正在运行。

> rs.conf()

{

"_id" : "testset"

"version" : 4

"members" : [

{

"_id" : 0

"host" : "[hostname]:27021"

}

{

"_id" : 1

"host" : "[hostname]:27022"

}

{

"_id" : 2

"host" : "[hostname]:27023"

"priority" : 0

"hidden" : true

}

]

}

请注意,此副本集配置的版本号现在已经增加。这作为重新配置的一部分自动发生,以确保任何副本集成员没有错误的配置文档。

现在,您应该有一个完全配置好的三成员副本集,其中有一个活动的主副本集和一个隐藏的“被动”副本集

添加仲裁人

添加仲裁器作为副本集的投票成员是一个简单的过程。让我们从培养一个新成员开始。

$ mkdir -p /db/arbiter1/data

$ mongod --dbpath /db/ arbiter1/data --port 27024 --replSet testset –rest

现在您已经创建了一个新成员,只需使用rs.addArb()命令添加新的仲裁器:

>rs.addArb("[hostname]:27024")

{ "ok" : 1 }

如果您现在运行rs.status(),您将在输出中看到您的仲裁器:

{

"_id" : 3

"name" : "Pixl.local:27024"

"health" : 1

"state" : 7

"stateStr" : "ARBITER"

"uptime" : 721

"lastHeartbeat" : ISODate("2013-06-07T11:21:01Z")

"lastHeartbeatRecv" : ISODate("2013-06-07T11:21:00Z")

"pingMs" : 0

}

你可能已经意识到了这里的一个问题;我们现在有四个节点。那是一个偶数,而且偶数是不好的!如果我们继续这样运行,您的 MongoDB 节点将开始记录以下内容:

[rsMgr] replSet total number of votes is even - add arbiter or give one member an extra vote

为了解决这个问题,我们知道我们需要奇数个成员;因此,一个潜在的解决方案是按照日志消息的建议添加另一个仲裁器,但这并不完美,因为我们添加了不必要的额外复杂性。最佳解决方案是阻止现有节点之一投票并被视为仲裁成员。我们可以通过将隐藏二级选举的投票数设置为零来做到这一点。我们这样做的方式与设置隐藏值和优先级值的方式相同。

conf = rs.conf()

conf.members[2].votes = 0

rs.reconfig(conf)

仅此而已。我们现在已经将被动节点设置为真正的被动节点:它永远不会成为主节点;它被客户端视为副本集的一部分;它不能参加选举或被算作多数。为了测试这一点,您可以尝试关闭被动节点,仲裁器和您的其他两个节点将继续在主节点上运行,而不会发生变化;而在以前,主节点可能会退出,理由是它看不到大多数节点。

副本集链接

您已经看到,通常情况下,副本集的成员会尝试从该副本集中的主副本同步数据。但这不是副本集辅助设备可以同步的唯一位置;它们也可以从其他辅助节点同步。通过这种方式,您的辅助服务器可以形成一个“同步链”,其中每个辅助服务器都会同步副本集中其他辅助服务器的最新数据。

管理副本集

MongoDB 提供了许多命令来管理副本集的配置和状态。表 11-2 显示了可用于创建、操作和检查副本集中集群状态的命令。

表 11-2。

Commands for Manipulating and Inspecting Replica Sets

| 命令 | 描述 | | --- | --- | | `rs.help()` | 返回此表中的命令列表。 | | `rs.status()` | 返回有关副本集当前状态的信息。此命令列出每个成员服务器及其状态信息,包括上次联系的时间。此调用可用于对整个集群进行简单的运行状况检查。 | | `rs.initiate()` | 使用默认参数初始化副本集。 | | `rs.initiate(``replSetcfg` | 使用配置描述初始化副本集。 | | `rs.add("``host``:``port` | 使用提供主机名和(可选)特定端口的简单字符串将成员服务器添加到副本集。 | | `rs.add(``membercfg` | 使用配置描述将成员服务器添加到副本集。如果要指定特定属性(例如,新成员服务器的优先级),必须使用此方法。 | | `rs.addArb("host:port")` | 添加一个新的成员服务器作为仲裁服务器。成员不需要已经用`--replSet`选项启动;在任何可到达的机器上运行的任何`mongod`实例都可以执行这个任务。请注意,副本集的所有成员都可以访问该服务器。 | | `rs.stepDown()` | 当您对副本集的主要成员运行此命令时,使主服务器放弃其角色,并强制在群集中选择新的主服务器。请注意,只有活动的辅助服务器可以作为候选服务器成为新的主服务器,并且如果在 60 秒的等待后没有其他候选成员可用,则可以重新选择原始主服务器。 | | `rs.syncFrom("host:port")` | 从给定成员进行辅助同步。可用于形成同步链。 | | `rs.freeze(secs)` | 冻结给定成员,并使其在指定的秒数内没有资格成为主要成员。 | | `rs.remove("host:port")` | 从副本集中删除给定成员。 | | `rs.slaveOk()` | 设置此连接,以便允许从辅助节点读取。 | | `rs.conf()` | 重新显示当前副本集的配置结构。此命令对于获取副本集的配置结构非常有用。该配置结构可被修改,然后再次提供给`rs.initiate()`以改变该结构的配置。这项技术提供了从副本集中删除成员服务器的唯一受支持的方法;目前还没有直接的方法可以做到这一点。 | | `db.isMaster()` | 这个函数不是特定于副本集的;相反,它是一种通用的复制支持功能,允许应用或驱动程序确定特定的连接实例是否是复制拓扑中的主/主要服务器。 |

以下章节详细介绍了表 11-2 中列出的一些更常用的命令,提供了关于它们的功能和使用方法的更多细节。

用 rs.status()检查实例的状态

从我们之前向副本集添加成员的经历中,您应该知道,rs.status()可能是您在处理副本集时最常使用的命令。它允许您检查当前连接的实例的状态,包括它在副本集中的角色:

>rs.status()

> rs.status();

{

"set" : "testset"

"date" : ISODate("2013-06-04T10:57:24Z")

"myState" : 1

"members" : [

{

"_id" : 0

"name" : "[hostname]:27021"

"health" : 1

"state" : 1

"stateStr" : "PRIMARY"

"uptime" : 4131

"optime" : Timestamp(1370340105, 1)

"optimeDate" : ISODate("2013-06-04T10:01:45Z")

"self" : true

}

{

"_id" : 1

"name" : "[hostname]:27022"

"health" : 1

"state" : 2

"stateStr" : "SECONDARY"

"uptime" : 3339

"optime" : Timestamp(1370340105, 1)

"optimeDate" : ISODate("2013-06-04T10:01:45Z")

"lastHeartbeat" : ISODate("2013-06-04T10:57:23Z")

"lastHeartbeatRecv" : ISODate("2013-06-04T10:57:23Z")

"pingMs" : 0

"syncingTo" : "[hostname]:27021"

}

{

"_id" : 2

"name" : "[hostname]:27023"

"health" : 1

"state" : 2

"stateStr" : "SECONDARY"

"uptime" : 3339

"optime" : Timestamp(1370340105, 1)

"optimeDate" : ISODate("2013-06-04T10:01:45Z")

"lastHeartbeat" : ISODate("2013-06-04T10:57:22Z")

"lastHeartbeatRecv" : ISODate("2013-06-04T10:57:23Z")

"pingMs" : 0

"syncingTo" : "[hostname]:27021"

}

]

"ok" : 1

}

示例中显示的每个字段都有一个含义,如表 11-3 中所述。这些值可用于了解副本集当前成员的状态。

表 11-3。

Values for the rs.status Fields

| 价值 | 描述 | | --- | --- | | `_id` | 作为副本集一部分的该成员的 ID | | `Name` | 成员的主机名 | | `Health` | `replSet`的健康值 | | `State` | 状态的数值 | | `StateStr` | 这个复制集成员状态的字符串表示 | | `Uptime` | 该成员已经存在多久了 | | `optime` | 对此成员应用的最后一次操作的时间,采用时间戳和整数值的格式 | | `optimeDate` | 上次应用操作的日期 | | `lastHeartbeat` | 上次发送心跳的日期 | | `lastHeartbeatRecv` | 收到的最后一个心跳的日期 | | `pingMs` | 运行`rs.status()`的成员和每个远程成员之间的 ping 时间 | | `syncingTo` | 此给定节点要同步匹配的副本集的成员 |

在前面的示例中,rs.status()命令是针对主服务器成员运行的。该命令返回的信息显示主服务器正在以1myState值运行;换句话说,“成员作为主要(主)成员运行。”

强行举行新的选举,让卢比下台( )

您可以使用rs.stepDown()命令强制主服务器停止运行 60 秒;该命令还强制选择新的主服务器。该命令在下列情况下很有用:

  • 当您需要使托管主实例的服务器离线时,无论是调查服务器还是实施硬件升级或维护。
  • 当您想要对数据结构运行诊断过程时。
  • 当您想要模拟主要故障的影响并强制您的集群进行故障转移时,您可以测试您的应用如何响应这样的事件。

以下示例显示了对testset副本集运行rs.stepDown()命令时返回的输出:

> rs.stepDown()

> rs.status()

{

"set" : "testset"

"date" : ISODate("2013-06-04T11:19:01Z")

"myState" : 2

"members" : [

{

"_id" : 0

"name" : "[hostname]:27021"

"health" : 1

"state" : 2

"stateStr" : "SECONDARY"

"uptime" : 5428

"optime" : Timestamp(1370340105, 1)

"optimeDate" : ISODate("2013-06-04T10:01:45Z")

"self" : true

}

{

"_id" : 1

"name" : "[hostname]:27022"

"health" : 1

"state" : 2

"stateStr" : "SECONDARY"

"uptime" : 4636

"optime" : Timestamp(1370340105, 1)

"optimeDate" : ISODate("2013-06-04T10:01:45Z")

"lastHeartbeat" : ISODate("2013-06-04T11:19:00Z")

"lastHeartbeatRecv" : ISODate("2013-06-04T11:19:00Z")

"pingMs" : 0

"syncingTo" : "[hostname]:27021"

}

{

"_id" : 2

"name" : "[hostname]:27023"

"health" : 1

"state" : 2

"stateStr" : "SECONDARY"

"uptime" : 4636

"optime" : Timestamp(1370340105, 1)

"optimeDate" : ISODate("2013-06-04T10:01:45Z")

"lastHeartbeat" : ISODate("2013-06-04T11:19:01Z")

"lastHeartbeatRecv" : ISODate("2013-06-04T11:19:00Z")

"pingMs" : 0

"lastHeartbeatMessage" : "db exception in producer: 10278 dbclient error communicating with server: [hostname]:27021"

"syncingTo" : "[hostname]:27021"

}

]

"ok" : 1

}

在本例中,您对主服务器运行了rs.stepDown()命令。rs.status()命令的输出显示副本集的所有成员现在都是次要的。如果您随后运行rs.status(),您应该看到另一个成员已经升级为主要成员(假设有一个成员符合条件)。

确定成员是否为主服务器

db.isMaster()命令并不严格适用于副本集。然而,这个命令非常有用,因为它允许应用测试当前连接是否连接到主服务器:

>db.isMaster()

{

"setName" : "testset"

"ismaster" : true

"secondary" : false

"hosts" : [

"[hostname]:27022"

"[hostname]:27021"

]

"primary" : "[hostname]:27022"

"me" : "[hostname]:27022"

"maxBsonObjectSize" : 16777216

"maxMessageSizeBytes" : 48000000

"localTime" : ISODate("2013-06-04T11:22:28.771Z")

"ok" : 1

}

如果您在此时对您的testset副本集集群运行isMaster(),这表明您运行它的服务器不是主/主服务器("ismaster" == false)。如果运行该命令的服务器实例是副本集的成员,该命令还将返回副本集中已知服务器实例的映射,包括该副本集中各个服务器的角色。

为副本集成员配置选项

副本集功能包括许多选项,可用于控制副本集成员的行为。当您运行rs.initiate( replSetcfg )rs.add( membercfg )选项时,您必须提供一个描述副本集成员特征的配置结构:

{

_id : <setname>

members: [

{

_id : <ordinal>

host : <hostname[:port]>

[ priority: <priority>, ]

[arbiterOnly : true, ]

[ votes : <n>, ]

[ hidden: true, ]

[ tags: { document }, ]

[ slaveDelay: <seconds>, ]

[ buildIndexes: true, ]

}

, ...

]

settings: {

[ chainingAllowed : <boolean>, ]

[ getLastErrorModes: <modes>, ]

[ getLastErrorDefaults: <lasterrdefaults>, ]

}

}

对于rs.initiate(),您应该提供完整的配置结构,如下所示。配置结构本身的最顶层包括三个层次:_idmemberssettings_id是副本集的名称,当您创建副本集成员时,--replSet命令行选项会提供这个名称。members数组由一组描述集合中每个成员的结构组成;这是在将单个服务器添加到集合中时提供给rs.add()命令的成员结构。最后,settings数组包含适用于整个副本集的选项。

成员结构的组织

members结构包含配置副本集的每个成员实例所需的所有条目;您可以在表 11-4 中看到所有这些条目。

表 11-4。

Configuring Member Server Properties

| [计]选项 | 描述 | | --- | --- | | `members.$._id` | (强制)Integer:该元素指定成员结构在`member`数组中的序号位置。此元素的可能值包括大于或等于`0`的整数。该值使您能够处理特定的成员结构,因此您可以执行添加、移除和覆盖操作。 | | `members.$.host` | (必需)String:该元素以`host:port`的形式指定服务器的名称;注意,主机部分不能是`localhost`或`127.0.0.1`。 | | `members.$.priority` | (可选)Float:该元素表示在选举新的主服务器时分配给服务器的权重。如果主服务器变得不可用,则将基于该值升级辅助服务器。任何非零值的辅助服务器都被认为是活动的,有资格成为主服务器。因此,将该值设置为零会强制辅助节点成为被动节点。如果多个辅助服务器共享相同的优先级,那么将进行投票,并且可以调用仲裁器(如果配置的话)来解决任何死锁。该元素的默认值是`1.0`。 | | `members.$.arbiterOnly` | (可选)Boolean:该成员作为仲裁人选举新的主服务器。它不涉及副本集的任何其他功能,并且不需要使用`--replSet`命令行选项启动。系统中任何正在运行的`mongod`进程都可以执行这个任务。这个元素的默认值是`false`。 | | `members.$.votes` | (可选)Integer:此元素指定实例可以投票选举其他实例作为主服务器的票数;这个元素的默认值是`1`。 | | `members.$. hidden` | (可选)Boolean:这将从`db.isMaster()`的输出中隐藏节点,从而防止在节点上发生读操作,即使有第二个读首选项。 | | `members.$.tags` | (可选)文档:这允许您为副本集标记的读取首选项设置标记。 | | `members.$.slaveDelay` | (可选)Integer:这允许您将从服务器设置为比主服务器“延迟”指定的秒数。 | | `members.$.buildIndexes` | (可选)Boolean:该选项用于禁止建立索引。它不应该更好地设置在理论上可以成为主节点节点上。当索引不重要并且您希望节省空间时,此功能对于备份节点等非常有用。 |
探索设置结构中可用的选项

11-5 列出了Settings结构中可用的副本集属性。这些设置全局应用于整个副本集;您可以使用这些属性来配置副本集成员如何相互通信。

表 11-5。

Inter-server Communication Properties for the Settings Structure

| [计]选项 | 描述 | | --- | --- | | `settings.chainingAllowed` | (可选)Boolean:允许您指定是否允许成员从其他辅助节点复制。默认为真 | | `settings.getLastErrorModes` | (可选)模式:用于设置自定义写入问题,如本章后面所述。 | | `Settings.getLastErrorDefaults` | (可选)默认值:用于设置自定义写入问题 |

从应用连接到副本集

从 PHP 连接到副本集类似于连接到单个 MongoDB 实例。唯一的区别是它可以提供一个副本集实例地址或一个副本集成员列表;连接库将确定哪个服务器是主服务器,并将查询定向到该机器,即使主服务器不是您提供的成员之一。因此,无论如何,最好在连接字符串中指定多个成员;这样,您就消除了尝试只从一个可能离线的成员发现的风险。以下示例显示了如何从 PHP 应用连接到副本集:

<?php

$m = new MongoClient("mongodb://localhost:27021

localhost:27022", array("replicaSet" => "testSet"));

...

?>

在应用中设置阅读首选项

MongoDB 中的读取偏好是一种选择您希望从副本集中读取哪些成员的方式。通过为您的驱动程序指定读取首选项,您可以告诉它对副本集的特定成员(或多个成员)运行查询。目前有五种模式可以在您的驱动程序上设置为读取偏好,如表 11-6 中所列。

表 11-6。

Read Preference Options

| [计]选项 | 描述 | | --- | --- | | `Primary` | 读取将只针对主节点。如果明确与标记的读取首选项一起使用,此读取首选项将被阻止。这也是默认的读取首选项。 | | `PrimaryPreferred` | 除非没有可用的主节点,否则读取将定向到主节点;那么读取将被定向到辅助节点。 | | `Secondary` | 读取将仅定向到辅助节点。如果没有可用的辅助,此选项将生成一个异常。 | | `SecondaryPreferred` | 除非没有可用的辅助节点,否则读取将定向到辅助节点;则读取将针对主节点。这对应于旧的“slaveOk”辅助读取方法的行为。 | | `Nearest` | 从最近的节点读取,不管它是主节点还是辅助节点。`Nearest`使用网络延迟来确定使用哪个节点。 |

Note

如果您设置的读取首选项意味着您的读取可能来自辅助节点,那么您必须意识到这些数据可能不是完全最新的;某些操作可能没有从您的主服务器复制。

您可以在 PHP 中使用setReadPreference()命令对一个连接对象设置读取首选项,如下所示:

<?php

$m = new MongoClient("mongodb://localhost:27021

localhost:27022", array("replicaSet" => "testSet"));

$m->setReadPreference(MongoClient::RP_SECONDARY_PREFERRED, array());

...

?>

从现在开始,您在该连接上进行的任何查询都将针对集群中的辅助节点运行。您还可以通过向 URI 添加阅读首选项标签来设置阅读首选项。指定了读取首选项nearest的 URI 如下所示:

mongodb://localhost:27021,localhost:27022?readPreference=nearest

从应用内部设置写问题

写关注是一个类似于读偏好的概念。您可以使用写问题来指定该数据在被视为“完整”之前需要安全提交到多少个节点这个测试使用 MongoDB 的 Get Last Error (GLE)机制来检查连接上发生的最后一个错误。您可以设置多种写操作模式,这些模式允许您配置在执行写操作时对其持久性的确定程度。每一个都在表 11-7 中列出。

表 11-7。

MongoDB Write Concern Levels

| [计]选项 | 描述 | | --- | --- | | `W=0`或`Unacknowledged` | 一个一发而不可收拾的作品。将发送写操作,但不会尝试确认它是否已提交。 | | `W=1`或`Acknowledged` | 写操作必须由主要人员确认。这是默认值。 | | `W=` `N`或`Replica Set Acknowledged` | 主节点必须确认写入,N–1 成员必须从主节点复制此写入。此选项更可靠,但如果副本集中的成员存在复制延迟,或者如果由于停机等原因在提交写入时没有足够的成员可用,则可能会导致延迟。 | | `W=Majority` | 写操作必须写入主节点,并由足够多的成员复制,以使集合中的大多数成员都确认该写操作。与`w=n`一样,这可能会在停机期间或存在复制延迟时导致问题。 | | `J=true`或`Journaled` | 可与`w=`写入问题一起使用,以指定写入必须持续到日志才能被视为已确认。 |

为了在插入中使用写操作,只需在给定的insert()函数中添加w选项,如下所示:

$col->insert($ documentarray ("w" => 1));

这将尝试向我们的集合中插入一个带有确认写入的w=1值的文档。

将标签用于读取偏好和写入关注

除了刚才讨论的“读取首选项”和“写入问题”选项之外,还有另一种方法—标记。这种机制允许您在副本集的成员上设置自定义标记,然后使用这些标记以及您的读取首选项和写入关注设置,以更细粒度的方式指导操作。所以,事不宜迟,我们开始吧。通过将标签添加到副本集配置文件的标签部分,可以在副本集上设置标签。让我们首先将asitesb的标签添加到我们的副本集配置中:

conf=rs.conf()

conf.members[0].tags = {site : "a"}

conf.members[1].tags = {site : "b"}

conf.members[2].tags = {site : "a"}

rs.reconfigure(conf)

现在,我们可以检查我们的新配置,您可以看到我们已经将两个站点设置到位;它们在每个配置文件的tags部分中定义。

rs.conf()

{

"_id" : "testset"

"version" : 8

"members" : [

{

"_id" : 0

"host" : "Pixl.local:27021"

"tags" : {

"site" : "a"

}

}

{

"_id" : 1

"host" : "Pixl.local:27022"

"tags" : {

"site" : "b"

}

}

{

"_id" : 2

"host" : "Pixl.local:27023"

"priority" : 0

"hidden" : true

"tags" : {

"site" : "a"

}

}

{

"_id" : 3

"host" : "Pixl.local:27024"

"arbiterOnly" : true

}

]

}

现在让我们开始使用我们的新标签吧!我们可以设置站点a中副本集的最近成员的读取首选项。

$m->setReadPreference(MongoClient::RP_NEAREST, array( array('site' => 'a'),));

既然我们已经解决了读取偏好,让我们开始关注写入。写问题稍微复杂一些,因为我们首先需要修改我们的副本集配置来添加额外的getLastErrorModes。在这种情况下,我们希望创建一个写问题,说明给定的写操作必须提交到足够多的节点,才能写入到两个不同的站点。这意味着写入必须至少提交到站点a和站点b。要做到这一点,我们需要将getLastErrorModes变量设置为一个文档,该文档包含我们新的写关注点的名称和一个规则,该规则表示我们希望将它写入两个不同的“site”标签。这是按如下方式完成的:

conf = rs.conf()

conf.settings. getLastErrorModes = { bothSites : { "site": 2 } } }

rs.reconfig(conf)

现在我们需要插入我们的文档,并指定我们新的写关注点。

$col->insert($document, array("w" => "bothSites"));

就这么简单。现在,我们可以保证我们的写入同时提交到两个站点!现在,假设我们希望将此设置为对我们的群集进行任何写入的默认写入问题。

conf = rs.conf()

conf.settings.getLastErrorDefaults = { bothSites : 1 } }

rs.reconfig(conf)

现在,我们所做的任何写操作都将使用默认的写关注点bothSites来完成。所以如果我们只是做一个普通的插入!

摘要

MongoDB 提供了一组丰富的工具来实现冗余和健壮的复制拓扑。在本章中,您了解了许多这些工具,包括使用它们的一些原因和动机。您还了解了如何设置许多不同的副本集拓扑。此外,您还学习了如何使用命令行工具和内置的 web 界面来检查复制系统的状态。最后,您学习了如何设置和配置读首选项和写关注点,以确保从正确的位置读取和写入。

请花点时间评估本章中描述的每个选项和功能,以确保在生产环境中尝试使用副本集之前,您构建了最适合您特定需求的副本集。使用 MongoDB 在单台机器上创建测试床非常容易;正如我们在本章中所做的那样。因此,强烈建议您尝试每种方法,以确保您完全了解每种方法的优势和局限性,包括它将如何处理您的特定数据和应用。

十二、分片

Abstract

无论你是在构建下一个脸书还是一个简单的数据库应用,如果它成功了,你可能需要在某个时候扩展你的应用。如果您不想不断更换硬件(或者您开始接近在一个硬件上所能做的极限),那么您将希望使用一种技术,允许您在需要时逐渐增加系统的容量。分片是一种允许您将数据分布在多台机器上的技术,但这种方式类似于一个应用访问一个数据库。

无论你是在构建下一个脸书还是一个简单的数据库应用,如果它成功了,你可能需要在某个时候扩展你的应用。如果您不想不断更换硬件(或者您开始接近在一个硬件上所能做的极限),那么您将希望使用一种技术,允许您在需要时逐渐增加系统的容量。分片是一种允许您将数据分布在多台机器上的技术,但这种方式类似于一个应用访问一个数据库。

MongoDB 实现的分片非常适合基于云的计算平台,非常适合动态的、负载敏感的自动扩展,在这种情况下,您可以根据需要增加容量,在不需要时减少容量。

本章将向您介绍如何在 MongoDB 中实现分片,并查看 MongoDB 分片实现中提供的一些高级功能,如标签分片和散列分片键。

探索分享的需求

当万维网刚刚起步时,网站、用户和网上可用信息的数量都很少。Web 由几千个站点和仅仅几万或几十万用户组成,主要集中在学术和研究社区。在早期,数据往往很简单:手工维护的 HTML 文档通过超链接连接在一起。组成 Web 的协议的最初设计目标是提供一种方法,为存储在互联网上不同服务器上的文档创建可导航的引用。

即使是目前的大品牌,如雅虎!与今天的产品相比,在网络上的存在微不足道。该公司最初的产品是雅虎目录,只不过是一个手工编辑的热门网站链接网络。这些链接是由一小群热情的冲浪者维护的。Yahoo 目录中的每个页面都是一个简单的 HTML 文档,存储在文件系统目录树中,并使用简单的文本编辑器进行维护。

但是随着网络的规模开始爆炸式增长——网站和访问者的数量开始近乎垂直地攀升——大量的可用资源迫使早期的网络先驱从简单的文档转向从独立的数据存储中生成更复杂的动态页面。

搜索引擎开始在网络上爬行,将链接的数据库聚集在一起,如今这些数据库拥有数千亿个链接和数百亿个存储页面。

这些发展促使人们转向通过不断发展的内容管理系统来管理和维护数据集,这些内容管理系统主要存储在数据库中以便于访问。

与此同时,新类型的服务不断发展,不仅仅存储文档和链接集。例如,音频、视频、事件和各种其他数据开始进入这些巨大的数据存储库。这一过程通常被描述为“数据工业化”——在许多方面,它与 19 世纪以制造业为中心的工业革命有相似之处。

最终,每个成功的网络公司都面临着如何访问存储在这些庞大数据库中的数据的问题。他们发现一台数据库服务器每秒只能处理这么多的查询,网络接口和磁盘驱动器每秒只能在 web 服务器之间传输这么多兆字节。提供基于 web 的服务的公司会很快发现自己已经超越了单个服务器、网络或驱动器阵列的性能。在这种情况下,他们被迫分割和分发他们收集的大量数据。通常的解决方案是将这些庞大的数据块分割成更小的块,以便更可靠、更快速地管理。同时,这些公司需要保持跨其大型机器集群中保存的全部数据执行操作的能力。

您在第 11 章中详细了解了复制,它是克服这些扩展问题的有效工具,使您能够在多台服务器上创建多个相同的数据副本。这使您能够(在正确的情况下)将服务器负载分散到更多的机器上。

然而,没过多久,您就遇到了另一个问题,组成数据集的各个表或集合变得如此之大,以至于它们的大小超出了单个数据库系统有效管理它们的能力。例如,脸书宣称每天接收超过 3 亿张照片!这个网站已经运营了将近 10 年。

一年之内有 1095 亿张照片,在一张表中包含这么多数据是不可行的。因此,脸书,像他们之前的许多公司一样,寻找将那组记录分布在大量数据库服务器上的方法。脸书采用的解决方案是现实世界中更好记录(和公开)的分片实现之一。

划分水平和垂直数据

数据分区是将数据分割到多个独立数据存储库的机制。这些数据存储可以是共存的(在同一系统上)或远程的(在不同的系统上)。共驻分区的动机是减小单个索引的大小,并减少更新记录所需的 I/O 数量。远程分区的动机是增加访问数据的带宽,方法是使用更多的 RAM 来存储数据,避免磁盘访问,或者提供更多的网络接口和磁盘 I/O 通道。

垂直划分数据

在传统的数据库视图中,数据存储在行和列中。垂直分区包括在列边界上分解记录,并将各部分存储在单独的表或集合中。可以说,使用具有一对一关系的连接表的关系数据库设计是共存垂直数据分区的一种形式。

然而,MongoDB 并不适合这种形式的分区,因为它的记录(文档)结构并不适合整洁的行列模型。因此,很少有机会根据列边界完全分隔一行。MongoDB 还促进了嵌入式文档的使用,并且它不直接支持在服务器上将相关集合连接在一起的能力(这些可以在您的应用中完成)。

水平划分数据

使用 MongoDB 时,水平分区是唯一的选择,而分片是一种流行的水平分区形式的通用术语。分片允许您将集合分割到多个服务器上,以提高包含大量文档的集合的性能。

一个简单的分片示例是将用户记录集合划分到一组服务器上,这样姓氏以字母 A-G 开头的人的所有记录都在一台服务器上,H-M 在另一台服务器上,依此类推。分割数据的规则被称为分片键。

简单地说,分片允许您将分片云视为单个集合,应用不需要知道数据分布在多台机器上。传统的分片实现要求应用主动确定特定文档存储在哪个服务器上,以便正确地路由其请求。传统上,有一个库绑定到应用,这个库负责存储和查询分片数据集中的数据。

MongoDB 有一个独特的分片方法,其中 MongoS 路由进程管理数据的分割和请求到所需分片服务器的路由。如果一个查询需要来自多个分片的数据,那么 MongoS 将管理将从每个分片获得的数据合并回单个游标的过程。

这个特性比其他任何特性都更能让 MongoDB 成为一个云或面向 web 的数据库。

分析简单的共享场景

让我们假设您想要为一个虚构的盖尔语社交网络实现一个简单的分片解决方案。图 12-1 显示了该应用如何分片的简化表示。

A978-1-4302-5822-3_12_Fig1_HTML.jpg

图 12-1。

Simple sharding of a User collection

这个简化的应用视图存在许多问题。让我们看看最明显的。

首先,如果您的盖尔语网络面向世界各地的爱尔兰和苏格兰社区,那么数据库将有大量以 Mac 和 Mc 开头的姓名(MacDonald、McDougal 等)用于苏格兰人口,以 O' (O'Reilly、O'Conner 等)用于爱尔兰人口。因此,使用基于姓氏首字母的简单分片键会在支持字母范围“M–o”的分片上放置过多的用户记录。类似地,支持字母范围“X–Z”的分片将执行很少的工作。

分片系统的一个重要特征是,它必须确保数据均匀地分布在可用的一组分片服务器上。这可以防止影响群集整体性能的热点发展。让我们称这个需求为 1:在所有分片中均匀分布数据的能力。

另一件要记住的事情是:当您将数据集分割到多个服务器上时,您实际上增加了数据集对硬件故障的脆弱性。也就是说,当您添加服务器时,单个服务器故障影响数据可用性的可能性会增加。同样,可靠的分片系统的一个重要特征是——像通常与磁盘驱动器一起使用的 RAID 系统一样——它将每个数据块存储在多个服务器上,并且它可以容忍单个分片服务器变得不可用。让我们称这个需求为 2:以容错方式存储分片数据的能力。

最后,您希望确保可以从一组分片中添加或删除服务器,而不必备份和恢复数据,并在一组更小或更大的分片中重新分配数据。此外,您需要能够在不导致集群停机的情况下做到这一点。让我们称这个需求为 3:在系统运行时添加或删除分片的能力。

接下来的章节将介绍如何解决这些需求。

用 MongoDB 实现分片

MongoDB 使用代理机制来支持分片(见图12-2);提供的mongos守护进程充当多个基于mongod的分片服务器的控制器。您的应用附加到mongos进程,就好像它是一个 MongoDB 数据库服务器;此后,您的应用将其所有命令(比如更新、查询和删除)发送给那个mongos进程。

A978-1-4302-5822-3_12_Fig2_HTML.jpg

图 12-2。

A simple sharding setup without redundancy

mongos进程负责管理哪个 MongoDB 服务器从您的应用接收命令,这个守护进程将跨多个分片向多个服务器重新发出查询,并将结果聚合在一起。

MongoDB 在集合级别实现分片,而不是在数据库级别。在许多系统中,只有一两个集合可能会增长到需要分片的程度。因此,应该明智地使用分片;如果不需要,您不希望为较小的集合增加管理数据分布的开销。

让我们回到虚构的盖尔语社交网络的例子。在这个应用中,user集合包含了关于用户及其个人资料的详细信息。这个集合可能会增长到需要分片的程度。然而,其他集合,如eventscountriesstates,不太可能变得如此之大,以至于分片不会带来任何好处。

分片系统使用分片键将数据映射成块,块是文档键的逻辑连续范围(参见第 5 章了解更多关于块的信息)。每个组块识别具有特定连续范围的分片键值的多个文档;这些值使mongos控制器能够快速找到包含它需要处理的文档的块。然后,MongoDB 的分片系统将这些数据块存储在一个可用的分片存储中;配置服务器跟踪哪个块存储在哪个分片服务器上。这是实现的一个重要特性,因为它允许您在集群中添加和删除分片,而无需备份和恢复数据。

当您向集群中添加一个新的分片时,系统将在新的服务器集合中迁移大量的分片,以便均匀地分布它们。类似地,当您删除一个分片时,分片控制器将从脱机的分片中排出数据块,并将它们重新分配给剩余的分片服务器。

MongoDB 的分片设置还需要一个地方来存储其分片的配置,以及一个地方来存储集群中每个分片服务器的信息。为此,需要一个名为 config server 的 MongoDB 服务器;这个服务器实例是一个以特殊角色运行的mongod服务器。如前所述,配置服务器还充当允许确定每个块的位置的目录。集群中可以有一个(开发)或三个(生产)配置服务器。我们总是建议在生产环境中运行三个配置服务器,因为配置服务器的丢失将意味着您不再能够确定您的分片数据的哪些部分在哪些分片上!

乍一看,似乎实现一个依赖于分片的解决方案需要大量的服务器!然而,您可以在数量相对较少的物理服务器上共同托管创建分片设置所需的每种不同服务的多个实例(类似于您在第十一章关于复制的介绍中看到的),但是您需要实现严格的资源管理,以避免 MongoDB 进程相互竞争 RAM 等资源。图 12-3 显示了一个完全冗余的分片系统,它使用副本集作为分片存储和配置服务器,以及一组mongos来管理集群。它还展示了如何将这些服务压缩到仅在三台物理服务器上运行。

小心地放置 shard 存储实例,使它们正确地分布在物理服务器中,这样可以确保您的系统能够承受集群中一个或多个服务器的故障。这反映了 RAID 磁盘控制器在条带中的多个驱动器之间分发数据的方法,使 RAID 配置能够从故障驱动器中恢复。

A978-1-4302-5822-3_12_Fig3_HTML.jpg

图 12-3。

A redundant sharding configuration

设置共享配置

为了有效地使用分片,理解它的工作原理是很重要的。下一个例子将带您在一台机器上设置一个测试配置。你将像图 12-2 所示的简单分片系统一样配置这个例子,有两个不同之处:这个例子将通过只使用两个分片来保持简单,并且这些分片将是单个的mongod而不是完整的副本集。最后,您将学习如何创建一个分片集合和一个演示如何使用这个集合的简单 PHP 测试程序。

在这个测试配置中,您将使用表 12-1 中列出的服务。

表 12-1。

Server Instances in the Test Configuration

| 服务 | 守护进程 | 港口 | 数据库路径 | | --- | --- | --- | --- | | 分片控制器 | `mongos` | Twenty-seven thousand and twenty-one | 不适用的 | | 配置服务器 | `mongod` | Twenty-seven thousand and twenty-two | `/db/config/data` | | 沙尔多 | `mongod` | Twenty-seven thousand and twenty-three | `/db/shard1/data` | | Shard1 | `mongod` | Twenty-seven thousand and twenty-four | `/db/shard2/data` |

让我们从设置配置服务器开始。为此,请打开一个新的终端窗口,并键入以下代码:

$ mkdir -p /db/config/data

$ mongod --port 27022 --dbpath /db/config/data --configsvr

一旦您启动并运行了配置服务器,请确保您的终端窗口是打开的,或者随意将-–fork–-logpath选项添加到您的命令中。接下来,你需要设置分片控制器(mongos)。为此,请打开一个新的终端窗口,并键入以下内容:

$ mongos --configdb localhost:27022 --port 27021 --chunkSize 1

这将启动 shard 控制器,它应该会宣布正在监听端口 27021。如果您查看配置服务器的终端窗口,您应该看到 shard 服务器已经连接到它的配置服务器并向它注册了自己。

在本例中,您将块大小设置为可能的最小大小,1MB。对于现实世界的系统来说,这不是一个实用的值,因为这意味着块存储小于文档的最大大小(16MB)。然而,这只是一个演示,小块大小允许您创建许多块来练习分片设置,而不必加载大量数据。默认情况下,chunkSize设置为 64MB,除非另有说明。

最后,您已经准备好启动两台 shard 服务器。为此,您需要两个新的终端窗口,每个服务器一个。在一个窗口中键入以下内容,启动第一台服务器:

$ mkdir -p /db/shard0/data

$ mongod --port 27023 --dbpath /db/shard0/data

并在第二个窗口中键入以下内容,以打开第二台服务器:

$ mkdir -p /db/shard1/data

$ mongod --port 27024 --dbpath /db/shard1/data

您已经启动并运行了服务器。接下来,您需要告诉分片系统分片服务器的位置。为此,您需要使用服务器的主机名连接到您的分片控制器(mongos)。您可以使用 localhost,但是这将您的集群的可伸缩性限制在这台机器上。在运行下面的示例时,您应该用自己的主机名替换<hostname>标记。重要的是要记住,即使mongos不是一个完整的 MongoDB 实例,它对您的应用来说也是一个完整的实例。因此,您可以使用mongo命令 shell 连接到 shard 控制器并添加您的两个 shard,如下所示:

$``mongo``<hostname>

> sh.addShard("``<hostname>

{ "shardAdded" : "shard0000", "ok" : 1 }

> sh.addShard( "``<hostname>

{ "shardAdded" : "shard0001", "ok" : 1 }

您的两台 shard 服务器现已激活;接下来,您需要使用listshards命令检查分片:

> db.printShardingStatus();

--- Sharding Status ---

sharding version: {

"_id" : 1

"version" : 3

"minCompatibleVersion" : 3

"currentVersion" : 4

"clusterId" : ObjectId("5240282df4ee9323185c70b2")

}

shards:

{ "_id" : "shard0000", "host" : "<hostname>:27023" }

{ "_id" : "shard0001", "host" : "<hostname>:27024" }

databases:

{ "_id" : "admin", "partitioned" : false, "primary" : "config" }

{ "_id" : "test", "partitioned" : false, "primary" : "shard0000" }

您现在有了一个可工作的分片环境,但是没有分片数据;接下来,您将创建一个名为testdb的新数据库,然后在这个数据库中激活一个名为testcollection的集合。您将切分这个集合,因此您将为这个集合提供一个名为testkey的条目,您将使用它作为切分函数:

> sh.enableSharding("testdb")

{ "ok" : 1 }

> sh.shardCollection("testdb.testcollection", {testkey : 1})

{ "collectionsharded" : "testdb.testcollection", "ok" : 1 }

到目前为止,您已经创建了一个带有两个分片存储服务器的分片集群。您还在其上创建了一个带有分片集合的数据库。一个没有任何数据的服务器对任何人来说都是没有用的,所以是时候将一些数据放入这个集合了,这样你就可以看到分片是如何分布的。

为此,您将使用一个小的 PHP 程序来加载带有一些数据的分片集合。您将加载的数据由一个名为testkey的字段组成。这个字段包含一个随机数和第二个字段,其中有一个固定的文本块(第二个字段的目的是确保您可以创建一个合理数量的文本块进行切分)。这个集合是一个名为 TextAndARandomNumber.com 的虚构网站的主数据表。以下代码创建了一个 PHP 程序,将数据插入到您的分片服务器中:

<?php

// Open a database connection to the mongos daemon

$mongo = new MongoClient("localhost:27021");

// Select the test database

$db = $mongo->selectDB('testdb');

// Select the TestIndex collection

$collection = $db->testcollection;

for($i=0; $i < 100000 ; $i++){

$data=array();

$data['testkey'] = rand(1,100000);

$data['testtext'] = "Because of the nature of MongoDB, many of the more "

. "traditional functions that a DB Administrator "

. "would perform are not required. Creating new databases, "

. "collections and new fields on the server are no longer necessary, "

. "as MongoDB will create these elements on-the-fly as you access them."

. "Therefore, for the vast majority of cases managing databases and "

. "schemas is not required.";

$collection->insert($data);

}

这个小程序会连接到 shard 控制器(mongos)上,用随机testkeys和一些testtext插入 100000 条记录来填充文档。如前所述,这个示例文本导致这些文档占据足够多的块,使得使用分片机制变得可行。

以下命令运行测试程序:

$php testshard.php

一旦程序完成运行,您可以用命令 shell 连接到mongos实例,并验证数据是否已经存储:

$mongo localhost:27021

>use testdb

>db.testcollection.count()

100000

此时,您可以看到您的服务器已经存储了 100,000 条记录。现在,您需要连接到每个分片,并查看每个分片在testdb.testcollection中存储了多少项。下面的代码使您能够连接到第一个分片,并查看有多少来自testcollection集合的记录存储在其中:

$mongo localhost:27023

>use testdb

>db.testcollection.count()

48875

这段代码使您能够连接到第二个分片,并查看有多少来自testcollection集合的记录存储在其中:

$mongo localhost:27024

>use testdb

>db.testcollection.count()

51125

Note

您可能会看到每个分片中文档数量的不同值,这取决于您查看单个分片的确切时间。mongos实例最初可能会将所有的块放在一个分片上,但是随着时间的推移,它会通过移动块来重新平衡分片集,从而在所有的分片之间均匀地分布数据。因此,存储在给定分片中的记录数量可能会随时变化。这满足了“需求 1:跨所有分片均匀分布数据的能力”

向集群添加新分片

让我们假设商业真的在 TextAndARandomNumber.com 跳跃。为了满足需求,您决定向集群中添加一个新的 shard 服务器,以进一步分散负载。

添加新分片很容易;你只需要重复前面描述的步骤。首先创建新的 shard 存储服务器,并将其放在端口 27025 上,这样它就不会与您现有的服务器发生冲突:

$ sudo mkdir -p /db/shard2/data

$ sudo mongod --port 27025 --dbpath /db/shard2/data

接下来,您需要将新的 shard 服务器添加到集群中。您可以登录到分片控制器(mongos),然后使用管理命令addshard:

$mongo localhost:27021

>sh.addShard("localhost:27025")

{ "shardAdded" : "shard0002", "ok" : 1 }

此时,您可以运行listshards命令来验证分片已经被添加到集群中。这样做揭示了一个新的分片服务器(shard2)现在出现在shards数组中:

> db.printShardingStatus();

--- Sharding Status ---

sharding version: {

"_id" : 1

"version" : 3

"minCompatibleVersion" : 3

"currentVersion" : 4

"clusterId" : ObjectId("5240282df4ee9323185c70b2")

}

shards:

{ "_id" : "shard0000", "host" : "<hostname>:27023" }

{ "_id" : "shard0001", "host" : "<hostname>:27024" }

{ "_id" : "shard0002", "host" : "<hostname>:27025" }

databases:

{ "_id" : "admin", "partitioned" : false, "primary" : "config" }

{ "_id" : "test", "partitioned" : false, "primary" : "shard0000" }

如果您登录到您在端口 27025 上创建的新 shard 存储服务器并查看testcollection,您将看到一些有趣的东西:

$mongo localhost:27025

> use testdb

switched to db testdb

> show collections

system.indexes

testcollection

> db.testcollection.count()

4657

> db.testcollection.count()

4758

> db.testcollection.count()

6268

这表明新的shard2存储服务器上的testcollection中的项目数量正在慢慢增加。您所看到的证明了分片系统正在扩展的群集中重新平衡数据。随着时间的推移,分片系统将从shard0shard1存储服务器迁移数据块,以在组成集群的三台服务器之间创建均匀的数据分布。这个过程是自动的,即使没有新数据插入到testcollection集合中,它也会发生。在这种情况下,mongos shard 控制器将块移动到新的服务器,然后将它们注册到配置服务器。

这是选择块大小时要考虑的因素之一。如果你的chunkSize值非常大,你将得到一个不太均匀的数据分布;相反,您的chunkSize值越小,您的数据分布就越均匀。

从集群中移除分片

当它持续的时候,它是伟大的,但是现在假设 TextAndARandomNumber.com 是昙花一现,它的光芒已经失败了。经过几周的疯狂活动后,网站的流量开始下降,所以你不得不开始寻找降低运营成本的方法——换句话说,新的分片服务器必须淘汰!

在下一个例子中,您将删除之前添加的 shard 服务器。要启动这个过程,登录到分片控制器(mongos)并发出removeShard命令:

$ mongo localhost:27021

> use admin

switched to db admin

> db.runCommand({removeShard : "localhost:27025"})

{

"msg" : "draining started successfully"

"state" : "started"

"shard" : "shard0002"

"ok" : 1

}

removeShard命令响应一条消息,指示移除过程已经开始。它还表明mongos已经开始将目标分片服务器上的块重新定位到集群中的其他分片服务器。这个过程被称为耗尽分片。

您可以通过重新发出removeShard命令来检查排水过程的进度。响应将告诉您还有多少块和数据库需要从分片中排出:

> db.runCommand({removeShard : "localhost:27025"})

{

"msg" : "draining ongoing"

"state" : "ongoing"

"remaining" : {

"chunks" : NumberLong( 12 )

"dbs" : NumberLong( 0 )

}

"ok" : 1

}

最后,removeShard进程将终止,您将收到一条消息,指示删除进程已完成:

> db.runCommand({removeShard : "localhost:27025"})

{

"msg" : "removeshard completed successfully"

"state" : "completed"

"shard" : "shard0002"

"ok" : 1

}

为了验证removeShard命令是否成功,您可以运行listshards来确认所需的 shard 服务器已经从集群中删除。例如,以下输出显示您之前创建的shard2服务器不再列在shards数组中:

>db.runCommand({listshards:1})

{

"shards" : [

{

"_id" : "shard0000"

"host" : "localhost:27023"

}

{

"_id" : "shard0001"

"host" : "localhost:27024"

}

]

"ok" : 1

}

此时,您可以终止 Shard2 mongod进程并删除它的存储文件,因为它的数据已经被迁移回其他服务器。

Note

在不使集群离线的情况下向集群添加和从集群中删除分片的能力是 MongoDB 支持高可伸缩性、高可用性、大容量数据存储的能力的关键组成部分。这满足了最终的需求:“需求 3:在系统运行时添加或移除分片的能力。”

确定您的联系方式

您的应用可以连接到标准的非共享数据库(mongod)或共享控制器(mongos)。MongoDB 实现了这两个过程;除了少数用例,数据库和分片控制器的外观和行为完全相同。但是,有时确定您连接的系统类型可能很重要。

MongoDB 提供了isdbgrid命令,您可以使用它来询问连接的数据系统,以确定它是否被分片。下面的代码片段显示了如何使用这个命令,以及它的输出是什么样子的:

$mongo

>use testdb

>db.runCommand({ isdbgrid : 1});

{ "isdbgrid" : 1, "hostname" : "Pixl.local", "ok" : 1 }

该响应包括isdbgrid:1字段,它告诉您所连接的数据库支持分片。对isdbgrid:0的响应表明您连接到了一个非共享的数据库。

列出分片集群的状态

MongoDB 还包括一个简单的命令,用于转储分片集群的状态:printShardingStatus()

这个命令可以让您深入了解分片系统的内部。下面的代码片段显示了如何调用printShardingStatus()命令,但是去掉了一些返回的输出以便于阅读:

$mongo localhost:27021

>sh.status();

--- Sharding Status ---

sharding version: {

"_id" : 1

"version" : 3

"minCompatibleVersion" : 3

"currentVersion" : 4

"clusterId" : ObjectId("51c699a7dd9fc53b6cdc4718")

}

shards:

{ "_id" : "shard0000", "host" : "localhost:27023" }

{ "_id" : "shard0001", "host" : "localhost:27024" }

databases:

{ "_id" : "admin", "partitioned" : false, "primary" : "config" }

{ "_id" : "test", "partitioned" : false, "primary" : "shard0000" }

{ "_id" : "testdb", "partitioned" : true, "primary" : "shard0000" }

testdb.testcollection

shard key: { "testkey" : 1 }

chunks:

shard0000 2

shard0001 3

{ "testkey" : { "$minKey" : 1 } } -->> { "testkey" : 0 } on : shard0000 Timestamp(4, 0)

{ "testkey" : 0 } -->> { "testkey" : 14860 } on : shard0000 Timestamp(3, 1)

{ "testkey" : 14860 } -->> { "testkey" : 45477 } on : shard0001 Timestamp(4, 1)

{ "testkey" : 45477 } -->> { "testkey" : 76041 } on : shard0001 Timestamp(3, 4)

{ "testkey" : 76041 } -->> { "testkey" : { "$maxKey" : 1 } } on : shard0001 Timestamp(3, 5)

该输出列出了分片服务器、每个分片数据库/集合的配置以及分片数据集中的每个块。因为您使用了一个小的chunkSize值来模拟一个更大的分片设置,所以这个报告列出了很多块。从这个清单中可以获得的一条重要信息是与每个块相关联的分片键的范围。输出还显示了特定块存储在哪个 shard 服务器上。您可以使用该命令返回的输出作为工具的基础,来分析 shard 服务器的键和块的分布。例如,您可以使用此数据来确定数据集中是否有任何数据聚集。

使用副本集实现分片

到目前为止,您所看到的例子依赖于一个单独的mongod实例来实现每个分片。在《??》第 11 章中,你学习了如何创建副本集,副本集是由mongod个实例组成的集群,它们一起工作以提供冗余和故障安全存储。

当向分片集群添加分片时,您可以提供一个副本集的名称和该副本集成员的地址,该分片将在每个副本集成员上被实例化。Mongos 将跟踪哪个实例是副本集的主服务器;它还将确保对该实例进行所有的分片写入。

将分片和副本集结合起来,使您能够创建高性能、高可靠性的集群,能够容忍多机故障。它还使您能够最大限度地提高廉价的商品级硬件的性能和可用性。

Note

使用副本集作为分片存储机制的能力满足了“需求 2:以容错方式存储分片数据的能力”

平衡

我们之前已经讨论过 MongoDB 如何自动将您的工作负载分布在集群中的所有分片上。虽然您可能认为这是通过某种形式的专利 MongoDB-Magic 实现的,但事实并非如此。您的 MongoS 进程中有一个称为平衡器的元素,它在您的集群中移动数据的逻辑块,以确保它们均匀地分布在您的所有分片中。平衡器与分片对话,告诉它们将数据从一个分片迁移到另一个分片。在下面的例子中,您可以看到sh.status()输出中块的分布。您可以看到,我的数据在shard0000上分为两个区块,在shard0001上分为三个区块。

{ "_id" : "testdb", "partitioned" : true, "primary" : "shard0000" }

testdb.testcollection

shard key: { "testkey" : 1 }

chunks:

shard0000 2

shard0001 3

虽然平衡器代表您自动完成所有这些工作,但您确实可以决定它何时运行。您可以根据需要停止和启动平衡器,并设置它可以运行的窗口。要停止平衡器,您需要连接到 MongoS 并发出sh.stopBalancer()命令:

> sh.stopBalancer();

Waiting for active hosts...

Waiting for the balancer lock...

Waiting again for active hosts after balancer is off...

如你所见,平衡器现在关闭了;该命令已将平衡器状态设置为off,并等待和确认平衡器已完成所有正在运行的迁移。启动平衡器也是同样的过程;我们运行sh.startBalancer()命令:

> sh.startBalancer();

现在,这两个命令可能需要一些时间来完成和返回,因为它们都在等待确认平衡器是否启动并实际运行。如果您遇到困难或希望自己手动确认状态,您可以执行以下检查。首先,您可以检查平衡器标志设置为什么。这是作为平衡器开/关开关的文档,它位于配置数据库中。

> use config

switched to db config

db.settings.find({_id:"balancer"})

{ "_id" : "balancer", "stopped" : true }

现在您可以看到这里的文档,其_id值为balancer被设置为stopped : true,这意味着平衡器没有运行(停止)。然而,这并不意味着还没有迁移在运行;为了证实这一点,我们需要检查“平衡器锁”

平衡器锁的存在是为了确保在给定时间只有一个平衡器可以执行平衡操作。您可以使用以下命令找到平衡器锁:

> use config

switched to db config

> db.locks.find({_id:"balancer"});

{ "_id" : "balancer", "process" : "Pixl.local:40000:1372325678:16807", "state" : 0, "ts" : ObjectId("51cc11c57ce3f0ee9684caff"), "when" : ISODate("2013-06-27T10:19:49.820Z"), "who" : "Pixl.local:40000:1372325678:16807:Balancer:282475249", "why" : "doing balance round" }

您可以看到这是一个比设置文档复杂得多的文档。然而,最重要的是state条目,它表示锁是否被占用,0 表示“空闲”或“未被占用”,其他的表示“正在使用”您还应该注意时间戳,它表示锁被取出的时间。将刚刚显示的“自由”锁与接下来的“被占用”锁进行比较,这表明平衡器是活动的。

> db.locks.find({_id:"balancer"});

{ "_id" : "balancer", "process" : "Pixl.local:40000:1372325678:16807", "state" : 1, "ts" : ObjectId("51cc11cc7ce3f0ee9684cb00"), "when" : ISODate("2013-06-27T10:19:56.307Z"), "who" : "Pixl.local:40000:1372325678:16807:Balancer:282475249", "why" : "doing balance round" }

现在,您知道了如何启动和停止平衡器,以及如何检查平衡器在某个给定点正在做什么。您还将希望能够设置一个窗口,当平衡器将被激活。例如,让我们将我们的平衡器设置为在晚上 8 点到早上 6 点之间运行,这样,当我们的集群(假设)不太活跃时,它可以整夜运行。为此,我们更新了之前的平衡器设置文档,因为它控制平衡器是否正在运行。交换看起来是这样的:

> use config

switched to db config

>db.settings.update({_id:"balancer"}, { $set : { activeWindow : { start : "20:00", stop : "6:00" } } }

这就够了。您的平衡器文档现在将有一个activeWindow,它将在晚上 8 点启动,在早上 6 点停止。现在,您应该能够启动和停止平衡器,确认其状态以及上次运行的时间,最后设置平衡器处于活动状态的时间窗口。

散列分片密钥

前面我们讨论了选择正确的分片密钥有多重要。如果选择了错误的分片键,可能会导致各种性能问题。以_id上的分片为例,它是一个不断增加的值。您所做的每个插入都将被发送到您的集合中当前拥有最高_id值的分片。因为每个新插入的值都是已经插入的“最大”值,所以您将总是在相同的位置插入数据。这意味着您的集群中有一个“热”分片,它接收所有插入,并将所有文档从它迁移到其他分片——效率不是很高。

为了解决这个问题,MongoDB 2.4 引入了一个新特性——散列分片键!散列分片键将为给定字段上的每个值创建一个散列,然后使用这些散列来执行分块和分片操作。这允许您获取一个递增的值,比如一个_id字段,并为每个 given _id 值生成一个散列,这将赋予值随机性。添加这种级别的随机性通常可以让您将写操作平均分配到所有分片上。然而,代价是您还会有随机读取,如果您希望对一系列文档执行操作,这可能会降低性能。因此,在某些工作负载下,与用户选择的分片密钥相比,哈希分片可能效率较低。

Note

由于哈希的实现方式,在对浮点(十进制)数进行分片时会有一些限制,这意味着像 2.3、2.4 和 2.9 这样的值将成为相同的哈希值。

因此,要创建散列分片,我们只需运行shardCollection并创建一个"hashed"索引!

sh.shardCollection( " testdb.testhashed", { _id: "hashed" } )

就这样!现在您已经创建了一个散列分片键,它将散列传入的_id值,以便以更“随机”的方式分发您的数据。现在,记住所有这些,你们中的一些人可能会说——为什么不总是使用散列分片密钥呢?

好问题;答案是分片只是“那些”黑魔法中的一种。最佳的分片键是一个允许你的写操作被很好地分布在多个分片上的键,所以写操作是有效并行的。它也是一个键,允许您进行分组,以便只对一个或有限数量的分片进行写入,并且它必须允许您更有效地利用单个分片上的索引。所有这些因素都将由您的使用情形、您存储的内容以及检索方式决定。

标签分片

有时,在某些情况下,说“我希望我能在这个分片上拥有所有的数据”是有意义的。这就是 MongoDB 的标签分片可以大放异彩的地方。您可以设置标签,使 shard 键的给定值指向集群中的特定分片!这一过程的第一步是确定您希望通过标签设置实现的目标。在下一个示例中,我们将完成一个简单的设置,我们希望根据地理位置分布数据,一个位置在美国,另一个在欧盟。

这个过程的第一步是向我们现有的分片中添加一些新的标签。我们用sh.addShardTag函数来做这件事,简单地添加我们的 shard 的名字和我们希望给它的标签。在这个例子中,我将shard0000设为美国分片,将shard0001设为欧盟分片:

> sh.addShardTag("shard0000","US");

> sh.addShardTag("shard0001","EU");

现在,为了查看这些更改,我们可以运行sh.status()命令并查看输出:

> sh.status();

--- Sharding Status ---

sharding version: {

"_id" : 1

"version" : 3

"minCompatibleVersion" : 3

"currentVersion" : 4

"clusterId" : ObjectId("51c699a7dd9fc53b6cdc4718")

}

shards:

{ "_id" : "shard0000", "host" : "localhost:27023", "tags" : [ "US" ] }

{ "_id" : "shard0001", "host" : "localhost:27024", "tags" : [ "EU" ] }

...

正如你所看到的,我们的分片现在有了美国和欧盟的标签,但是仅仅这些是没有用的;我们需要告诉 MongoS 根据一些规则将我们给定集合的数据路由到这些分片。这就是棘手的地方;我们需要配置我们的分片,以便我们分片的数据包含一些我们可以执行规则评估的内容,以便正确地路由它们。除此之外,我们还想保持和以前一样的分发逻辑。如果你回想一下前面的讨论,你会发现,在大多数情况下,我们只需要让这种按地区的划分在“之前”发生

这里的解决方案是向我们的分片键添加一个额外的键,表示数据所属的区域,并将它作为分片键的第一个元素。因此,现在我们需要切分一个新的集合,以便添加这些标签:

> sh.shardCollection("testdb.testtagsharding", {region:1, testkey:1})

{ "collectionsharded" : "testdb.testtagsharding", "ok" : 1 }

Note

虽然键的标记部分不需要成为第一个元素,但它通常是最好的;这样,组块首先被标签分解。

现在,我们已经设置好了标签,我们已经有了 shard 键,它将把我们的块分成很好的区域块,现在我们所需要的就是规则了!要添加这些,我们使用sh.addTagRange命令。这个命令接受集合的名称空间、最小和最大范围值,以及数据应该发送到的标签。MongoDB 的标记范围是最小包含和最大排除。因此,如果我们想要将值为 EU 的任何内容发送到标记 EU,我们需要一个从 EU 到 EV 的范围。对我们来说,我们想要从我们到 UT 的范围。这为我们提供了以下命令:

> sh.addTagRange("testdb.testtagsharding", {region:"EU"}, {region:"EV"}, "EU")

> sh.addTagRange("testdb.testtagsharding", {region:"US"}, {region:"UT"}, "US")

从现在开始,任何符合这些标准的文档都将被发送到这些分片中。所以为了测试东西,我们来介绍几个文档。我编写了一个短循环,将 10,000 个与我们的 shard 键匹配的文档引入集群。

for(i=0;i<10000;i++){db.getSiblingDB("testdb").testtagsharding.insert({region:"EU",testkey:i})}

现在我们运行sh.status(),可以看到分片分块分解:

testdb.testtagsharding

shard key: { "region" : 1, "testkey" : 1 }

chunks:

shard0000 3

{ "region" : { "$minKey" : 1 }, "testkey" : { "$minKey" : 1 } } -->> { "region" : "EU", "testkey" : { "$minKey" : 1 } } on : shard0000 Timestamp(1, 3)

{ "region" : "EU", "testkey" : { "$minKey" : 1 } } -->> { "region" : "EU", "testkey" : 0 } on : shard0000 Timestamp(1, 4)

{ "region" : "EU", "testkey" : 0 } -->> { "region" : { "$maxKey" : 1 }, "testkey" : { "$maxKey" : 1 } } on : shard0000 Timestamp(1, 2)

tag: EU { "region" : "EU" } -->> { "region" : "EV" }

tag: US { "region" : "US" } -->> { "region" : "UT" }

由此我们可以看出哪些组块在哪里;欧盟分片上有三大块,美国分片上没有。从这些范围中,我们可以看到其中两个块应该是空的。如果你进入每个单独的分片服务器,你会发现我们插入的所有 10,000 个文档都在一个分片上。您可能已经注意到日志文件中的以下消息:

Sun Jun 30 12:11:16.549 [Balancer] chunk { _id: "testdb.testtagsharding-region_"EU"testkey_MinKey", lastmod: Timestamp 1000|2, lastmodEpoch: ObjectId('51cf7c240a2cd2040f766e38'), ns: "testdb.testtagsharding", min: { region: "EU", testkey: MinKey }, max: { region: MaxKey, testkey: MaxKey }, shard: "shard0000" } is not on a shard with the right tag: EU

Sun Jun 30 12:11:16.549 [Balancer] going to move to: shard0001

出现此消息是因为我们已将标记范围设置为仅适用于欧盟和美国的值。鉴于我们现在所知道的,我们可以稍微修改它们,以覆盖所有的标签范围。让我们删除这些标签范围,并添加新的范围;我们可以使用以下命令删除旧文档:

> use config

> db.tags.remove({ns:"testdb.testtagsharding"});

现在我们可以添加回标签,但是这次我们可以从minKey运行到US,从US运行到maxKey,就像前面例子中的块范围一样!为此,使用特殊的MinKeyMaxKey操作符,它们代表分片键范围的最小和最大可能值。

> sh.addTagRange("testdb.testtagsharding", {region:MinKey}, {region:"US"}, "EU")

> sh.addTagRange("testdb.testtagsharding", {region:"US"}, {region:MaxKey}, "US")

现在,如果我们再次运行sh.status(),您可以看到范围;这一次,事情看起来运行得更好:

testdb.testtagsharding

shard key: { "region" : 1, "testkey" : 1 }

chunks:

shard0001 3

shard0000 1

{ "region" : { "$minKey" : 1 }, "testkey" : { "$minKey" : 1 } } -->> { "region" : "EU", "testkey" : { "$minKey" : 1 } } on : shard0001 Timestamp(4, 0)

{ "region" : "EU", "testkey" : { "$minKey" : 1 } } -->> { "region" : "EU", "testkey" : 0 } on : shard0001 Timestamp(2, 0)

{ "region" : "EU", "testkey" : 0 } -->> { "region" : "US", "testkey" : { "$minKey" : 1 } } on : shard0001 Timestamp(3, 0)

{ "region" : "US", "testkey" : { "$minKey" : 1 } } -->> { "region" : { "$maxKey" : 1 }, "testkey" : { "$maxKey" : 1 } } on : shard0000 Timestamp(4, 1)

tag: EU { "region" : { "$minKey" : 1 } } -->> { "region" : "US" }

tag: US { "region" : "US" } -->> { "region" : { "$maxKey" : 1 } }

我们的数据分布得更好,涉及的范围覆盖了从最小值到最大值的整个分片键范围。如果我们进一步向集合中插入条目,它们的数据将被正确地路由到我们想要的分片。没有混乱和大惊小怪。

摘要

分片使您能够扩展您的数据存储,以处理非常大的数据集。它还使您能够根据系统的增长来扩展集群。MongoDB 提供了一个简单的自动分片配置,可以很好地满足大多数需求。尽管这个过程是自动化的,但是您仍然可以对其特性进行微调,以支持您的特定需求。分片是 MongoDB 区别于其他数据存储技术的关键特性之一。阅读本章之后,您应该理解如何在多个 MongoDB 实例上分割您的数据,管理和维护一个分割的集群,以及如何利用标签分割和散列分割键。我们希望这本书能够帮助您了解 MongoDB 的许多设计方式,与使用更传统的数据库工具相比,MongoDB 能够更好地满足现代基于 web 的应用的严格要求。

您在本书中学到的主题包括以下内容:

  • 如何在多种平台上安装和配置 MongoDB?
  • 如何从各种开发语言访问 MongoDB?
  • 如何与围绕产品的社区建立联系,包括如何获得帮助和建议。
  • 如何设计和构建利用 MongoDB 独特优势的应用。
  • 如何对基于 MongoDB 的数据存储进行优化、管理和故障排除。
  • 如何创建跨多台服务器的可伸缩容错安装。

强烈建议您探究本书中提供的许多样本和示例。其他 PHP 示例可以在位于 www.php.net/manual/en/book.mongo.php 的 PHP MongoDB 驱动文档中找到。MongoDB 是一个非常容易接近的工具,其安装和操作的简易性鼓励了实验。所以不要犹豫:转动曲柄,开始玩吧!很快你也会开始欣赏这款迷人产品为你的应用带来的所有可能性。

第一部分:MongoDB 基础知识

第二部分:使用 MongoDB 开发

第三部分:大数据和高级 MongoDB