如果正在构建应用程序,并且使用了MongoDB的复制功能,那么你需要了解三个特定于应用的话题。第一个主题与连接和故障转移有关;随后是写关注允许你决定在应用程序继续下一步之前写操作的复制程度;最后是读扩展,允许应用程序将读请求分布在多个副本之间。我会依次讨论这些话题。
8.4.1 连接与故障转移
MongoDB的驱动提供了一套相对统一的界面来连接副本集。
1. 单节点连接
你总是可以连接到副本集里的单个节点上。连接到副本集的主节点和连接到普通的单机节点(正如我们全书中的例子那样)没有什么区别。这两种情况下,驱动都会初始化一个TCP套接字连接,运行isMaster
命令。这条命令会返回如下文档:
{ /"ismaster/" : true, /"maxBsonObjectSize/" : 16777216, /"ok/" : 1 }
对于驱动而言,最重要的是该节点的isMaster
字段是设置为true
的,这表明指定节点可以是单机、主从复制里的主节点或者副本集的主节点。1在所有这些情况里,节点都能写入,驱动的用户能执行各种CRUD操作。
1. isMaster
命令还会返回该版本服务器的最大BSON对象大小。随后,驱动会在插入BSON对象前验证所有这些对象是否满足此限制。
但在直接连接到副本集的从节点时,必须标明你知道自己正在连接从节点(至少对大多数驱动而言需要如此)。在Ruby驱动里,你可以带上:slave_ok
参数。于是,直接连接本章之前创建的第一个从节点的Ruby代码是这样的:
@con = Mongo::Connection.new(/'arete/', 40001, :slave_ok => true)
没有:slave_ok
参数,驱动会抛出一个异常,指出无法连接到主节点。这个检查是为了避免无意中向从节点进行写操作。虽然这种写操作会被服务器拒绝,但你看不到任何异常,除非使用安全模式进行操作。
MongoDB假设你通常都会连接主节点;:slave_ok
参数可以用来作为一道强制的健康检查。
2. 副本集连接
虽然你能单独连接副本集的各个成员,但一般都会希望连接整个副本集。这能让驱动确定哪个节点是主节点,并在故障转移时重新连接新的主节点。
大多数官方支持的驱动都提供了连接副本集的方法。在Ruby驱动里,可以创建一个ReplSetConnection
实例,传入种子节点(seed node)列表:
Mongo::ReplSetConnection.new([/'arete/', 40000], [/'arete/', 40001])
驱动内部会尝试连接各个种子节点,并调用isMaster
命令,该命令会返回一些重要的集合细节:
> db.isMaster{ /"setName/" : /"myapp/", /"ismaster/" : true, /"secondary/" : false, /"hosts/" : [ /"arete:40000/", /"arete:40001/" ], /"arbiters/" : [ /"arete:40002/" ], /"maxBsonObjectSize/" : 16777216, /"ok/" : 1}
一旦某个种子节点返回如上信息,驱动就拿到它需要的所有信息了。现在它能连接主节点,再次验证该成员依然是主节点,然后允许用户通过该节点进行读写操作。响应对象还允许驱动缓存剩余的从节点和仲裁节点的地址。如果主节点上的操作失败,那么后续的请求中,驱动都会尝试连接剩余的某个节点,直到它能重新连上主节点。
请牢记一点,虽然副本集的故障转移是自动的,但驱动不会隐藏发生故障这一事实。处理过程大致是这样的:首先,主节点发生故障或者发生了新的选举。后续的请求会显示套接字连接已断开,驱动就抛出一个连接异常,关闭那些打开的连接数据库的套接字。随后由应用程序开发者来决定该怎么办,这一决定依赖于要执行的操作和应用程序的特定需求。
请记住,在处理后续请求时,驱动会自动尝试重新连接,让我们想象几个场景。首先,假设你只向数据库发送读请求。在这种情况下,重试失败的读操作不会产生危害,因为它不会改变数据库的状态。但是,再假设通常还会向数据库发送写请求。之前提到过多次,无论是否开启安全模式,你都能写数据库。在安全模式下,驱动在每次写入后会追加一次getlasterror
命令调用,这能确保写操作已安全到达并向应用程序报告各种服务器错误。不使用安全模式时,驱动只是简单地向TCP套接字做写操作。
如果应用在没有使用安全模式时执行写入并发生故障转移,就会产生不确定的状态。最近向服务器做了多少写操作?有多少是丢失在套接字缓存里的?向TCP套接字做写操作的不确定性让你无法回答这些问题。这个问题有多严重取决于应用程序。对日志而言,不安全的写入也许是可接受的,因为丢失几条日志不会影响日志的全貌;但对于用户创建的数据,这就是一场灾难。
开启安全模式后,只有最后一次的写操作会有问题;可能它已经到服务器了,也可能没有。有时可能会重试,也可能会抛出一个应用程序错误。驱动始终会抛出一个异常;然后,开发者能够决定如何处理这些异常。
不管什么情况,重试一个操作都会让驱动尝试重新连接副本集。由于不同的驱动在副本集的连接行为上稍有不同,你应该查看驱动的文档了解详细信息。
8.4.2 写关注
现在情况已经很明朗了,默认运行安全模式对于大多数应用程序都是合理的,因为能够知道写操作正确无误地到达主节点是很重要的。但人们通常都会希望有更高级别的保证,写关注就能做到这点,它允许开发者指定应用程序执行后续操作前写操作应该被复制的范围。严格说来,你是通过getlastError
命令的两个参数来控制写关注的:w
和wtimeout
。
第一个参数w
,接受的值通常都是最近的写操作应该被复制到的服务器的总数;第二个参数是超时,如果写操作在指定毫秒内无法复制,该命令就会返回一个错误。
例如,如果你希望写操作至少要复制到一台服务器上,可以将w指定为2。如果希望在500 ms内无法完成该复制就超时,可以将wtimeout
指定为500。请注意,如果不指定wtimeout
的值,而复制又出于某些原因一直没有发生,那么该操作会一直阻塞下去。
在使用驱动时,不是通过显式调用getLastError
开启写关注的,而是创建一个写关注对象,或者设置合适的安全模式选项;这依赖于特定驱动的API。2在Ruby里可以像这样为一个操作设置写关注:
2. 附录D中包含在Java、PHP和C++里设置写关注的例子。
@collection.insert(doc, :safe => {:w => 2, :wtimeout => 200})
有时,你只是想确保写操作被复制到了大部分可用节点上,这时可以简单地将w
值设置为majority
:
@collection.insert(doc, :safe => {:w => /"majority/"})
还有更高级的选项。举例来说,如果已经开启了Journaling日志,还可以通过j
选项强制让Journaling日志同步到磁盘上:
@collection.insert(doc, :safe => {:w => 2, :j => true})
很多驱动还支持为指定连接或数据库设置写关注的默认值。要了解如何在具体场景中设置写关注,请查看所用驱动的文档。附录D中能找到更多语言的例子。
写关注既能用于副本集,也能用于主从复制。如果查看local
数据库,你会看到两个集合,从节点上的me和主节点上的slaves
,它们就是用来实现写关注的。每当从节点从主节点同步数据时,主节点都会在slaves
集合里记录下应用到从节点上的最新oplog
条目。因此,主节点总是能知道每个从节点复制了什么东西,可以准确地响应带getlastError
命令的写请求。
请记住,使用写关注时w
值大于1会引入额外的延时。可配置的写关注让你能够在速度和持久性之间做出权衡。如果使用了Journaling日志,那么w等于1就已经能满足大多数应用程序的需要了。另一方面,对于日志或分析型的应用程序,你可能会选择同时禁用Journaling日志和写关注,仅依靠复制来保证持久性,这在发生故障时可能会丢失一些写入的数据。请仔细考虑这些因素,在设计应用程序时测试不同的场景。
8.4.3 读扩展
经复制的数据库能很好地适用于读扩展。如果单台服务器无法承担应用程序的读负载,那么可以将查询路由到更多的副本上。大多数驱动都内置了将查询发送到从节点的功能。在Ruby驱动中,ReplSetConnection
构造方法的一个选项就提供了对该功能的支持:
Mongo::ReplSetConnection.new([/'arete/', 40000], [/'arete/', 40001], :read => :secondary )
当:read
参数被设置为:secondary
时,连接对象会随机选择一个附近的从节点读取数据。
其他驱动可以通过设置slaveOk
选项进行配置,读取从节点数据。当使用Java驱动连接副本集时,将slaveOk
设置为true
将以每个线程为基础,开启从节点的负载均衡。驱动中的负载均衡实现是为普通应用设计的,因此可能无法适用于所有应用。遇到这种情况时,用户通常会定制自己的负载均衡实现。同样的,请查看你的驱动文档了解更多细节。
很多MongoDB用户在生产环境中通过复制进行扩展。但是,有三种情况复制无法应对。第一种情况与所需的服务器数量有关,自MongoDB v2.0起,副本集最多支持12个成员,其中7个可以投票。如果需要更多副本来做扩展,可以使用主从复制。但如果既不想牺牲自动故障转移,又要超过副本集的成员上限,那就需要迁移到分片集群上了。
第二种情况涉及那些写负载较高的应用程序。正如本章开篇时所说的那样,从节点必须跟上这个写负载。向那些满负荷做写操作的从节点发送读请求可能会妨碍复制。
第三种副本扩展无法处理的情况是一致性读。因为复制是异步的,副本无法始终反映主节点最新的写操作。因此,如果应用程序任意地从多个从节点读取数据,那么呈现给最终用户的内容不能始终保证是完全一致的。对于那些主要用来显示内容的应用程序而言,这几乎从来都不是问题。但对于其他应用而言,用户是在主动操作数据,这就要求一致性读。在这些情况下,你有两个选择。第一是将那些需要一致性读的应用程序部分从那些不需要的部分里分离出来。前者总是从主节点读取数据,后者可以从多个从节点读取数据。当这种策略太复杂或者无法扩展时,就该采取分片策略。3
3. 注意,要从分片集群中获得一致性读,必须始终读取每个分片的主节点,而且必须发起安全写操作。
8.4.4 标签
如果正在使用写关注或者读扩展,你可能会想要更细粒度地进行控制,控制哪个从节点接收写或读请求。例如,假设部署了一个五节点副本集,跨两个数据中心:NY和FR。主数据中心NY包含三个节点,从数据中心FR包含剩下的两个节点。假设希望通过写关注阻塞请求,直到写操作被复制到数据中心FR的至少一个节点上。以目前你所了解的写关注知识来看,没有什么好办法实现这一需求。w
值为majority
是没用的,因为这会被翻译成值3,最可能的情况是NY里的三个节点先发出响应。也可以将值设置为4,但如果每个数据中心各损失一个节点,那这种方法也会有问题。
副本集标签可以解决这个问题,它允许针对带有特定标签的副本集成员定义特殊的写关注模式。要知道这是如何实现的,先要了解如何为副本集成员打标签。在配置文档里,每个成员都有一个名为tags
的键指向一个包含键值对的对象。下面就是一个例子:
{ /"_id/" : /"myapp/", /"version/" : 1, /"members/" : [ { /"_id/" : 0, /"host/" : /"ny1.myapp.com:30000/", /"tags/": { /"dc/": /"NY/", /"rackNY/": /"A/" } }, { /"_id/" : 1, /"host/" : /"ny2.myapp.com:30000/", /"tags/": { /"dc/": /"NY/", /"rackNY/": /"A/" } }, { /"_id/" : 2, /"host/" : /"ny3.myapp.com:30000/", /"tags/": { /"dc/": /"NY/", /"rackNY/": /"B/" } }, { /"_id/" : 3, /"host/" : /"fr1.myapp.com:30000/", /"tags/": { /"dc/": /"FR/", /"rackFR/": /"A/" } }, { /"_id/" : 4, /"host/" : /"fr2.myapp.com:30000/", /"tags/": { /"dc/": /"FR/", /"rackFR/": /"B/" } } ], settings: { getLastErrorModes: { multiDC: { dc :2}}, multiRack: { rackNY:2}}, } }}
这个带标签的配置文档适用于之前假设的跨两个数据中心的副本集。请注意,每个成员的标签文档有两个键值对:第一个标识了数据中心,第二个是指定节点服务器所在机架的名称。请记住,这里使用的名称是完全任意的,而且仅在本应用程序的上下文中有意义;你可以在标签文档中放置任何东西。重要的是如何使用它。
这时getLastErrorModes
该登场了。它们允许为getLastError
命令定义模式,这些模式实现了特殊的写关注要求。在本例中,你定义了两个模式,第一个是multiDC
,定义为{/"dc/":2}
,表示写操作应该复制到至少有两个不同dc
值的节点上。如果这时检查标签,你会看到它能确保写操作已经传播到了两个数据中心。第二个模式规定了至少要有两个NY的机架接收到了写操作。这同样也能通过标签加以实现。
一般来说,一个getLastErrorModes
条目包含一个文档,其中有一或多个键(本例中是dc
和rackNY
),它们的值是整数。这些整数表示某个键的不同标签值数量,在getLastError
命令成功完成时必须满足这些值。一旦定义好了这些模式,就能在应用程序里将其用作w
的值。例如,在Ruby中使用第一个模式,如下:
@collection.insert(doc, :safe => {:w => /"multiDC/"})
除了能让写关注更加精细,标签还能提供更粒度化的控制,决定哪个副本用于读扩展。可惜在本书编写时,针对标签进行读操作的语义尚未定义或实现在官方MongoDB驱动里。要了解最新进展,请查看Ruby驱动的JIRA问题单,参见https://jira.mongodb.org/browse/RUBY-326。