副本集是对主从复制的一种完善,也是推荐的MongoDB复制策略。我们会从配置一个示例副本集开始,然后描述复制是如何工作的,这些知识对于诊断线上问题是极为重要的。最后会讨论一些高级配置细节、故障转移与恢复,还有最佳部署实践。
8.2.1 配置
最小的推荐副本集配置由三个节点组成。其中两个节点是一等的、持久化mongod
实例,两者都能作为副本集的主节点,都有完整的数据副本。集合里的第三个节点是仲裁节点,不复制数据,只是中立观察者。正如其名所示,仲裁节点是进行仲裁的:在要求故障转移时,仲裁节点会帮助选出新的主节点。图8-1描绘了要配置的副本集。
先为副本集里的每个成员创建数据目录:
mkdir /data/node1mkdir /data/node2mkdir /data/arbiter
接下来,分别为每个成员启动独立的mongod
。因为要在同一台机器上运行这些进程,最好在独立的终端窗口里启动各个mongod
:
mongod --replSet myapp --dbpath /data/node1 --port 40000mongod --replSet myapp --dbpath /data/node2 --port 40001mongod --replSet myapp --dbpath /data/arbiter --port 40002
图8-1 由一个主节点、一个从节点和一个仲裁节点组成的基本副本集
如果查看mongod
的日志输出,注意到的第一件事是错误消息(提示找不到配置)。这完全正常:
[startReplSets] replSet can't get local.system.replsetconfig from self or any seed (EMPTYCONFIG)[startReplSets] replSet info you may need to run replSetInitiate
继续下一步,需要配置副本集。先连接到一个刚启动的非仲裁节点的mongod
上。这里的例子都是在本地运行mongod
进程的,因此将通过本地主机名来进行连接,本例中是arête
。
连接后运行rs.initiate
命令:
> rs.initiate{ "info2" : "no configuration explicitly specified -- making one", "me" : "arete:40000", "info" : "Config now saved locally. Should come online in about a minute .", "ok" : 1}
一分钟左右,你就能拥有一个单成员的副本集了。现在再通过rs.add
添加其他两个成员:
> rs.add("localhost:40001"){ "ok" : 1 }> rs.add("arete.local:40002", {arbiterOnly: true}){ "ok" : 1 }
注意,在添加第二个节点时指定了arbiterOnly
参数,以此创建一个仲裁节点。不久之后(1分钟内),所有的成员就都在线了。要获得副本集状态的摘要信息,可以运行db.isMaster
命令:
> db.isMaster{ "setName" : "myapp", "ismaster" : false, "secondary" : true, "hosts" : [ "arete:40001", "arete:40000" ], "arbiters" : [ "arete:40002" ], "primary" : "arete:40000", "maxBsonObjectSize" : 16777216, "ok" : 1}
rs.status
方法能提供更详细的系统信息,可以看到每个节点的状态信息。下面是完整的状态信息:
> rs.status{ "set" : "myall", "date" : ISODate("2011-09-27T22:09:04Z"), "myState" : 1, "members" : [ { "_id" : 0, "name" : "arete:40000", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "optime" : { "t" : 1317161329000, "i":1 }, "optimeDate" : ISODate("2011-09-27T22:08:49Z"), "self" : true }, { "_id" : 1, "name" : "arete:40001", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 59, "optime" : { "t" : 1317161329000, "i":1 }, "optimeDate" : ISODate("2011-09-27T22:08:49Z"), "lastHeartbeat" : ISODate("2011-09-27T22:09:03Z"), "pingMs" : 0 }, { "_id" : 2, "name" : "arete:40002", "health" : 1, "state" : 7, "stateStr" : "ARBITER", "uptime" : 5, "optime" : { "t":0, "i":0 }, "optimeDate" : ISODate("1970-01-01T00:00:00Z"), "lastHeartbeat" : ISODate("2011-09-27T22:09:03Z"), "pingMs" : 0 } ], "ok" : 1}
除非你的MongoDB数据库里包含很多数据,否则副本集应该能在30 s内上线。在此期间,每个节点的stateStr
字段应该会从RECOVERING
变为PRIMARY
、SECONDARY
或ARBITER
。
就算副本集的状态“宣称”复制已经在运行了,你可能还是希望能看到一些证据。因此,接下来在Shell里连接到主节点,插入一个文档:
$ mongo arete:40000> use bookstoreswitched to db bookstore> db.books.insert({title: "Oliver Twist"})> show dbsadmin (empty)bookstore 0.203125GBlocal 0.203125GB
初始的复制几乎是立即发生的。在另一个终端窗口中开启一个新的Shell实例,这次要指向从节点。查询刚才插入的文档,应该有如下输出:
$ mongo arete:40001> show dbsadmin (empty)bookstore 0.203125GBlocal 0.203125GB> use bookstore switched to db bookstore> db.books.find{ "_id" : ObjectId("4d42ebf28e3c0c32c06bdf20"), "title" : "Oliver Twist" }
如果复制确实如显示的那样已经在运作了,那就说明已经成功配置了副本集。
能实实在在地看到复制让人觉得很满意,但也许自动故障转移会更有趣一些。现在就来做点测试。要模拟网络分区需要点技巧,所以我们会选择一个简单的方法,杀掉一个节点。你可以杀掉从节点,这只会停止复制,剩余的节点仍旧保持其当前状态。如果希望看到系统状态发生改变,就需要杀掉主节点。标准的CTRL-C或kill -2
就能办到这点。你还可以连上主节点,在Shell里运行db.shutdownServer
。
一旦杀掉了主节点,从节点会发现检测不到主节点的“心跳”了,随后会把自己“选举”为主节点。这样的“选举”是可行的,因为原始节点中的大多数节点(仲裁者节点和原始的从节点)仍能ping到对方。以下是从节点日志的片段:
[ReplSetHealthPollTask] replSet info arete:40000 is down (or slow to respond)Mon Jan 31 22:56:22 [rs Manager] replSet info electSelf 1Mon Jan 31 22:56:22 [rs Manager] replSet PRIMARY
如果连接到新的主节点上检查副本集状态,你会发现无法访问到老的主节点:
> rs.status{ "_id" : 0, "name" : "arete:40000", "health" : 1, "state" : 6, "stateStr" : "(not reachable/healthy)", "uptime" : 0, "optime" : { "t" : 1296510078000, "i":1 }, "optimeDate" : ISODate("2011-01-31T21:43:18Z"), "lastHeartbeat" : ISODate("2011-02-01T03:29:30Z"), "errmsg": "socket exception"}
故障转移后,副本集就只有两个节点了。因为仲裁节点没有数据,只要应用程序只和主节点通信,它就能继续运作。1即使如此,复制停止了,现在不能再做故障转移了。老的主节点必须恢复。假设它是正常关闭的,可以让它再度上线,它会自动以从节点的身份重新加入副本集。你可以试一下,现在就重启老的主节点。
1. 应用程序有时会查询从节点来做读扩展。如果是这样,此类故障就会导致读故障。因此在设计应用程序时要时刻把故障转移放在心头。本章末尾会有更多相关内容。
以上就是副本集的完整概述,毫无悬念,你会觉得其中一些细节有点棘手。在接下来的两节里,你会看到副本集实际是如何运作的,了解它的部署、高级配置以及如何处理生产环境中可能出现的复杂场景。
8.2.2 复制的工作原理
副本集依赖于两个基础机制:oplog和“心跳”(heartbeat)。oplog让数据的复制成为可能,而“心跳”则监控健康情况并触发故障转移,后续将看到这些机制是如何轮流运作的。你应该已经逐渐开始理解并能预测副本集的行为了,尤其是在故障的情况下。
1. 关于oplog
oplog是MongoDB复制的关键。oplog是一个固定集合,位于每个复制节点的local
数据库里,记录了所有对数据的变更。每次客户端向主节点写入数据,就会自动向主节点的oplog里添加一个条目,其中包含了足够的信息来再现数据。一旦写操作被复制到某个从节点上,从节点的oplog也会保存一条关于写入的记录。每个oplog条目都由一个BSON时间戳进行标识,所有从节点都使用这个时间戳来追踪它们最后应用的条目。2
2. BSON时间戳是一个唯一标识符,由从纪元算起的秒数和一个递增的计数器值构成。
为了更好地了解其原理,让我们仔细看看真实的oplog以及其中记录的操作。先在Shell里连接到上一节启动的主节点,切换到local
数据库:
> use localswitched to db local
local
数据库里保存了所有的副本集元数据和oplog。当然,这个数据库本身不能被复制。正如其名,local
数据库里的数据对本地节点而言是唯一的,因此不该复制。
如果查看local
数据库,你会看到一个名为oplog.rs
的集合,每个副本集都会把oplog保存在这个集合里。你还会看到一些系统集合,以下就是完整的输出:
> show collectionsmeoplog.rsreplset.minvalidslavessystem.indexessystem.replset
replset.minvalid
包含了指定副本集成员的初始同步信息,system.replset
保存了副本集配置文档。me
和slaves
用来实现写关注(本章最后会介绍)。system.indexes
是标准索引说明容器。
我们先把精力集中在oplog上,查询与上一节里你所添加图书文档相关的oplog条目。为此,输入如下查询,结果文档里会有四个字段,我们将依次讨论这些字段:
> db.oplog.rs.findOne({op: "i"}){ "ts" : { "t" : 1296864947000, "i":1}, "op" : "i", "ns" :"bookstores.books", "o" : { "_id" : ObjectId("4d4c96b1ec5855af3675d7a1"),"title" : "Oliver Twist" }}
第一个字段是ts
,保存了该条目的BSON时间戳。这里特别要注意,Shell是用子文档来显示时间戳的,包含两个字段,t
是从纪元开始的秒数,i
是计数器。也许你会觉得可以像下面这样来查询这个条目:
db.oplog.rs.findOne({ts: {t: 1296864947000, i: 1}})
实际上,这条查询返回null
。要在查询中使用时间戳,需要显式构造一个时间戳对象。所有的驱动都有自己的BSON时间戳构造器,JavaScript也是如此。可以这样做:
db.oplog.rs.findOne({ts: new Timestamp(1296864947000, 1)})
回到那条oplog条目上,第二个字段op
表示操作码(opcode),它告诉从节点该条目表示了什么操作,本例中的i
表示插入。op
后的ns
标明了有关的命名空间(数据库和集合),o
对插入操作而言包含了所插入文档的副本。
在查看oplog条目时,你可能会注意到,对于那些影响多个文档的操作,oplog会将各个部分都分析到位。对于多项更新和大批量删除来说,会为每个影响到的文档创建单独的oplog条目。例如,假设你向集合里添加了几本狄更斯的书:
> use bookstoredb.books.insert({title: "A Tale of Two Cities"})db.books.insert({title: "Great Expectations"})
现在集合里有四本书,让我们通过一次多项更新来设置作者的名称:
db.books.update({}, {$set: {author: "Dickens"}}, false, true)
在oplog里会出现什么呢?
> use local> db.oplog.$main.find({op: "u"}){ "ts" : { "t" : 1296944149000, "i":1}, "op" : "u","ns" : "bookstore.books","o2" : { "_id" : ObjectId("4d4dcb89ec5855af365d4283") },"o" : { "$set " : { "author" : "Dickens"}}}{ "ts" : { "t" : 1296944149000, "i":2}, "op" : "u","ns" : "bookstore.books","o2" : { "_id" : ObjectId("4d4dcb8eec5855af365d4284") },"o" : { "$set " : { "author" : "Dickens"}}}{ "ts" : { "t" : 1296944149000, "i":3}, "op" : "u","ns" : "bookstore.books","o2" : { "_id" : ObjectId("4d4dcbb6ec5855af365d4285") },"o" : { "$set " : { "author" : "Dickens"}}}
如你所见,每个被更新的文档都有自己的oplog条目。这种正规化是更通用策略中的一部分,它会保证从节点总是能和主节点拥有一样的数据。要确保这一点,每次应用的操作都必须是幂等的——一个指定的oplog条目被应用多少次都无所谓;结果总是一样的。其他多文档操作的行为是一样的,比如删除。你可以试试不同的操作,看看它们在oplog里最终是什么样的。
要取得oplog当前状态的基本信息,可以运行Shell的db.getReplicationInfo
方法:
> db.getReplicationInfo{ "logSizeMB" : 50074.10546875, "usedMB" : 302.123, "timeDiff" : 294, "timeDiffHours" : 0.08, "tFirst" : "Thu Jun 16 2011 21:21:55 GMT-0400 (EDT)", "tLast" : "Thu Jun 16 2011 21:26:49 GMT-0400 (EDT)", "now" : "Thu Jun 16 2011 21:27:28 GMT-0400 (EDT)"}
这里有oplog中第一条和最后一条的时间戳,你可以使用$natural
排序修饰符手工找到这些oplog条目。例如,下面这条查询能获取最后一个条目:db.oplog.rs.find.sort({$natural: -1}).limit(1)
。
关于复制,还有一件重要的事情,即从节点是如何确定它们在oplog里的位置的。答案在于从节点自己也有一份oplog。这是对主从复制的一项重大改进,因此值得花些时间深究其中的原理。
假设向副本集的主节点发起写操作,接下来会发生什么?写操作先被记录下来,添加到主节点的oplog里。与此同时,所有从节点从主节点复制oplog。因此,当某个从节点准备更新自己时,它做了三件事:首先,查看自己oplog里最后一条的时间戳;其次,查询主节点oplog里所有大于此时间戳的条目;最后,把那些条目添加到自己的oplog里并应用到自己的库里。3也就是说,万一发生故障,任何被提升为主节点的从节点都会有一个oplog,其他从节点能以它为复制源进行复制。这项特性对副本集的恢复而言是必需的。
3. 开启Journaling日志时,文档会在一个原子事务里被同时写入核心数据文件和oplog。
从节点使用长轮询(long polling)立即应用来自主节点oplog的新条目。因此从节点的数据通常都是最新的。由于网络分区或从节点本身进行维护造成数据陈旧时,可以使用从节点oplog里最新的时间戳来监测复制延迟。
2. 停止复制
如果从节点在主节点的oplog里找不到它所同步的点,那么会永久停止复制。发生这种情况时,你会在从节点的日志里看到如下异常:
repl: replication data too stale, haltingFri Jan 28 14:19:27 [replsecondary] caught SyncException
回忆一下,oplog是一个固定集合,也就是说集合里的条目最终都会过期。一旦某个从节点没能在主节点的oplog里找到它已经同步的点,就无法再保证这个从节点是主节点的完美副本了。因为修复停止复制的唯一途径是重新完整同步一次主节点的数据,所以要竭尽全力避免这个状态。为此,要监测从节点的延时情况,针对你的写入量要有足够大的oplog。在第10章里能了解到更多与监控有关的内容。接下来我们将讨论如何选择合适的oplog大小。
3. 调整复制OPLOG大小
因为oplog是一个固定集合,所以一旦创建就无法重新设置大小(至少自MongoDB v2.0起是这样的),4为此要慎重选择初始oplog大小。
4. 增加固定集合大小的选项已列入计划特性之列,详见https://jira.mongodb.org/browse/SERVER-1864。
默认的oplog大小会随着环境发生变化。在32位系统上,oplog默认是50 MB,而在64位系统上,oplog会增大到1 GB或空余磁盘空间的5%。5对于多数部署环境,空余磁盘空间的5%绰绰有余。对于这种尺寸的oplog,要意识到一旦重写20次,磁盘就可能满了。
5. 如果运行的是OS X,这时oplog将是192 MB。这个值较小,原因是会假设OS X的机器是开发机。
因此默认大小并非适用于所有应用程序。如果知道应用程序写入量会很大,在部署之前应该做些测试。配置好复制,然后以生产环境的写入量向主节点发起写操作,像这样对服务器施压起码一小时。完成之后,连接到任意副本集成员上,获取当前复制信息:
db.getReplicationInfo
一旦了解了每小时会生成多少oplog,就能决定分配多少oplog空间了。你应该为从节点下线至少八小时做好准备。发生网络故障或类似事件时,要避免任意节点重新同步完整数据,增加oplog大小能为你争取更多时间。
如果要改变默认oplog大小,必须在每个成员节点首次启动时使用mongod
的--oplogSize
选项,其值的单位是兆。可以像这样启动一个1 GB oplog的mongod
实例:
mongod --replSet myapp --oplogSize 1024
4.“心跳”检测与故障转移
副本集的“心跳”检测有助于选举和故障转移。默认情况下,每个副本集成员每两秒钟ping一次其他所有成员。这样一来,系统可以弄清自己的健康状况。在运行rs.status
时,你可以看到每个节点上次“心跳”检测的时间戳和健康状况(1表示健康,0表示没有应答)。
只要每个节点都保持健康且有应答,副本集就能快乐地工作下去。但如果哪个节点失去了响应,副本集就会采取措施。每个副本集都希望确认无论何时都恰好存在一个主节点。但这仅在大多数节点可见时才有可能。例如,回顾上一节里构建的副本集,如果杀掉从节点,大部分节点依然存在,副本集不会改变状态,只是简单地等待从节点重新上线。如果杀掉主节点,大部分节点依然存在,但没有主节点了。因此从节点被自动提升为主节点。如果碰巧有多个从节点,那么会推选状态最新的从节点提升为主节点。
但还有其他可能的场景。假设从节点和仲裁节点都被杀掉了,只剩下主节点,但没有多数节点——原来的三个节点里只有一个仍处于健康状态。在这种情况下,在主节点的日志里会有如下消息:
Tue Feb 1 11:26:38 [rs Manager] replSet can't see a majority of the set, relinquishing primaryTue Feb 1 11:26:38 [rs Manager] replSet relinquishing primary stateTue Feb 1 11:26:38 [rs Manager] replSet SECONDARY
没有了多数节点,主节点会把自己降级为从节点。这让人有点费解,但仔细想想,如果该节点仍然作为主节点的话会发生什情况?如果出于某些网络原因心跳检测失败了,那么其他节点仍然是在线的。如果仲裁节点和从节点依然健在,并且能看到对方,那么根据多数节点原则,剩下的从节点会变成主节点。要是原来的主节点并未降级,那么你顿时就陷入了不堪一击的局面:副本集中有两个主节点。如果应用程序继续运行,就可能对两个不同的主节点做读写操作,肯定会有不一致,并伴随着奇怪的现象。因此,当主节点看不到多数节点时,必须降级为从节点。
5. 提交与回滚
关于副本集,还有最后一点需要理解,那就是提交的概念。本质上,你可以一直向主节点做写操作,但那些写操作在被复制到大多数节点前,都不会被认为是已提交的。这里所说的已提交是什么意思呢?最好举个例子来做说明。仍以上一节构建的副本集为例,你向主节点发起一系列写操作,出于某些原因(连接问题、从节点为备份而下线、从节点有延迟等)没被复制到从节点。现在假设从节点突然被提升为主节点了,你向新的主节点写数据,而最终老的主节点再次上线,尝试从新的主节点做复制。这里的问题在于老的主节点里有一系列写操作并未出现在新主节点的oplog里。这就会触发回滚。
在回滚时,所有未复制到大多数节点的写操作都会被撤销。也就是说会将它们从从节点的oplog和它们所在的集合里删掉。要是某个从节点里登记了一条删除,那么该节点会从其他副本里找到被删除的文档并进行恢复。删除集合以及更新文档的情况也是一样的。
相关节点数据路径的rollback子目录中保存了被回滚的写操作。针对每个有回滚写操作的集合,会创建一个单独的BSON文件,文件名里包含了回滚的时间。在需要恢复被回滚的文档时,可以用bsondump
工具来查看这些BSON文件,并可以通过mongorestore
手工进行恢复。
万一你真的不得不恢复被回滚的数据,你就会意识到应该避免这种情况,幸运的是,从某种程度上来说,这是可以办到的。要是应用程序能容忍额外的写延时,那么就能用上稍后会介绍的写关注,以此确保每次(也可能是每隔几次)写操作都能被复制到大多数节点上。使用写关注,或者更通用一点,监控复制的延迟,能帮助你减轻甚至避免回滚带来的全部问题。
本节中你了解了很多复制的内部细节,可能比预想的还要多,但这些知识迟早会派上用处的。在生产环境里诊断问题时,理解复制是如何工作的会非常有用。
8.2.3 管理
虽然MongoDB提供了自动化功能,但副本集其实还有些潜在的复杂配置选项,接下来,我将详细介绍这些选项。为了让配置简单一些,我也会就哪些选项是能被安全忽略的给出建议。
1. 配置细节
这里我会介绍一些与副本集相关的mongod
启动选项,并且描述副本集配置文档的结构。
- 复制选项
先前,你学习了如何使用Shell的rs.initiate
和rs.add
方法初始化副本集。这些方法很方便,但它们隐藏了某些副本集配置选项。这里你将看到如何使用配置文档初始化并修改一个副本集的配置。
配置文档里说明了副本集的配置。要创建配置文档,先为_id
添加一个值,要和传给--replSet
参数的值保持一致:
> config = {_id: "myapp", members: }{ "_id" : "myapp", "members" : [ ] }
members
也是配置文档的一部分,可以像下面这样进行定义:
config.members.push({_id: 0, host: 'arete:40000'})config.members.push({_id: 1, host: 'arete:40001'})config.members.push({_id: 2, host: 'arete:40002', arbiterOnly: true})
你的配置文档看起来应该是这样的:
> config{ "_id" : "myapp", "members" : [ { "_id" : 0, "host" : "arete:40000" }, { "_id" : 1, "host" : "arete:40001" }, { "_id" : 2, "host" : "arete:40002", "arbiterOnly" : true } ]}
随后可以把该文档作为rs.initiate
的第一个参数,用这个方法来初始化副本集。
严格说来,该文档由以下部分组成:包含副本集名称的_id
字段、members
数组(指定了3~12个成员),以及一个可选的子文档(用来指定某些全局设置)。示例副本集里使用了最少的配置参数,外加可选的arbiterOnly
设置。
文档中要求有一个_id
字段,与副本集的名称相匹配。初始化命令会验证每个成员节点在启动时是否都在--replSet
选项里用了这个名称。每个副本集成员都要有一个_id
字段,包含从0开始递增的整数,还要有一个host
字段,提供主机名和可选的端口。
这里通过rs.initiate
方法初始化了副本集,它是对replSetInitiate
命令的简单封装。因此,可以像这样启动副本集:
db.runCommand({replSetInitiate: config});
config
就是一个简单的变量,持有配置文档。一旦初始化完毕,每个集合成员都会在local
数据库的system.replset
集合里保存一份配置文档的副本。如果查询该集合,你会看到该文档现在有一个版本号了。每次修改副本集的配置,都必须递增这一版本号。
要修改副本集的配置,有一个单独的方法replSetReconfig
,它接受一个新的配置文档。新文档可以添加或删除集合成员,还可以修改成员说明和全局配置选项。修改配置文档、增加版本号,以及把它传给replSetReconfig
方法,这整个过程很麻烦,所以在Shell里有一些辅助方法来简化这个过程。可以在Shell里输入rs.help
,查看这些辅助方法的列表。注意,你已经用过rs.add
了。
请牢记一点,无论何时,要是重新配置副本集导致重新选举新的主节点,那么所有客户端的连接都会被关闭。这是为了确保客户端不会向从节点发送fire-and-forget风格的写操作。
如果你对通过驱动配置副本集感兴趣的话,可以了解一下rs.add
是如何实现的。在Shell提示符里输入rs.add
(不带括号的方法),看看这个方法的工作原理。
- 配置文档选项
到目前为止,我们都局限在最简单的副本集配置文档里。但这些文档还支持很多选项,无论是针对副本集成员还是整个副本集。我们将从成员选项开始进行介绍。注意,你已经见过_id
、host
和arbiterOnly
了,下面还会一起详细介绍其他选项。
_id
(必填) 唯一的递增整数,表示成员ID。这些_id
值从0开始,每添加一个成员就加1。host
(必填) 保存了成员主机名的字符串,带有可选的端口号。如果提供了端口号,需要用冒号与主机名分隔(例如arete:30000
)。如果没有指定端口号,则使用默认端口27017。arbiterOnly
一个布尔值,true
或false
,标明该成员是否是仲裁节点。仲裁节点只保存配置数据。它们是轻量级成员,参与主节点选举但本身不参与复制。priority
一个0~1000的整数,帮助确定该节点被选举为主节点的可能性。在副本集初始化和故障转移时,集合会尝试将优先级最高的节点推选为主节点,只要它的数据是最新的。也有一些场景里,你希望某个节点永远都不会成为主节点(比方说,一个位于从数据中心的灾难恢复节点)。在这些情况中,可以把优先级设置为0。遇到isMaster
命令,带有优先级0的节点会被标记为被动节点,永远都不会被选举为主节点。votes
所有副本集成员默认都有一票。votes
设置让你能给某个单独的成员更多投票。如果要使用该选项,请格外小心。首先,在各个成员的投票数不一致时,很难推测副本集的故障转移行为。其次,绝大多数生产部署环境里,每个成员只有一票的配置工作得都十分理想。因此,要是确定要修改某个指定成员的投票数,一定要经过深思熟虑,并仔细模拟各种故障场景。hidden
一个布尔值,如果为true
,在isMaster
命令生成的响应里则不会出现该节点。因为MongoDB驱动依赖于isMaster
来获取副本集的拓扑情况,所以隐藏一个成员能避免驱动自动访问它。该设置能同buildIndexes
协同使用,使用时必须有slaveDelay
。buildIndexes
一个布尔值,默认为true
,确定该成员是否会构建索引。仅当该成员永远不会成为主节点时(那些优先级为0的节点),才能将它设置为false
。该选项是为那些只会用作备份的节点设计的。如果备份索引很重要,那么就不要使用它。slaveDelay
指定从节点要比主节点延迟的秒数。该选项只能用于永远不会成为主节点的节点。所以如果要把slaveDelay
设置为大于0的值,务必保证将优先级设置为0。可以通过延迟从节点来抵御某些用户错误。例如,如果有一个延迟30分钟的从节点,管理员不小心删除了数据库,那么在问题扩散之前,你有30分钟做出反应。tags
包含一个任意键值对集合的文档,通常用来标识成员在某个数据中心或机架的位置。标签被用来指定写关注的粒度和读设置(8.4.9节里会做详细的讨论)。
以上就是针对单个副本集成员的所有选项。还有两个全局副本集配置参数,位于settings
键中。在副本集配置文档里,它们是这样的:
{ settings: { getLastErrorDefaults: {w: 1}, getLastErrorModes: { multiDC: { dc: 2 } } }}
getLastErrorDefaults
当客户端不带参数调用getLastError
时,默认的参数是由这个文档指定的。要谨慎对待该选项,因为它也可能设置了驱动中getLastError
的全局默认值,你可以想象这样一种情况:应用程序开发者调用了getLastError
,但他没有意识到管理员在服务器上指定了一个默认值。
关于getLastError
更详细的信息,可以查看3.2.3节与写关注相关的部分。简单起见,要指定所有写操作都要在500 ms内被复制到至少两个成员上,可以像这样进行配置:
settings: { getLastErrorDefaults: {w: 2, wtimeout: 500} }。
getLastErrorModes
为getLastError
命令定义了额外模式的文档。这个特性依赖于副本集标签,详见8.4.4节。
2. 副本集状态
通过replSetGetStatus
命令能够看到副本集及其成员的状态。要在Shell里调用该命令,可以运行rs.status
辅助方法。结果文档标识了现存成员及其各自的状态、正常运行时间和oplog时间。了解副本集成员的状态是非常重要的;在表8-1里可以看到完整的状态值列表。
表8-1 副本集状态
STARTUP
表示节点正在通过ping与其他节点沟通,分享配置数据1PRIMARY
这是主节点。副本集总是有且仅有一个主节点2SECONDARY
这是只读的从节点。该节点在故障转移时可能会成为主节点,当且仅当其优先级大于0并且没有被标记为隐藏时3RECOVERING
该节点不能用于读写操作。通常会在故障转移或添加新节点后看到这个状态。在恢复时,数据文件通常正在同步中;可以查看正在恢复的节点的日志进行验证4FATAL
网络连接仍然建立着,但节点对ping没响应了。节点被标记为FATAL,通常说明托管该节点的机器发生了致命错误5STARTUP2
初始数据文件正在同步中6UNKNOWN
还在等待建立网络连接7ARBITER
该节点是仲裁节点8DOWN
该节点早些时候还能访问并正常运行,但现在对“心跳”检测没应答了9ROLLBACK
正在进行回滚当所有节点的状态都是1、2或7,并且至少有一个节点是主节点时,可以认为副本集是稳定且在线的。可以在外部脚本里使用replSetGetStatus
命令来监控全局状态、复制延时以及正常运行时间,建议在生产环境部署中这样做。6
6. 除了运行状态命令,还可以通过Web控制台看到有用的信息。第10章讨论了Web控制台,并给出了一些结合副本集的使用示例。
3. 故障转移与恢复
你在示例副本集里已经看过几个故障转移的例子了。这里,我总结一下故障转移的规则,提供几个处理恢复的建议。
当配置中的所有成员都能和其他成员通信时,副本集就能上线了。每个节点默认都有一票投票,那些投票最终会帮助得出投票结果,选出主节点。这意味着只要两个节点(和投票)就能启动副本集了。但初始的投票数还能决定发生故障转移时,什么才能构成多数节点。
让我们假设你配置了一个由三个完整副本(没有仲裁节点)组成的副本集,这也达到了自动故障转移的推荐最小配置。如果主节点发生故障了,剩下的从节点仍能看到对方,那么就能选出新的主节点。如何做出选择呢?拥有最新oplog(或更高优先级)的从节点会被选为主节点。
- 故障模式与恢复
恢复是在故障后将副本集还原到原始状态的过程。有两大类故障需要处理。第一类包含所谓的无损故障(clean failure),仍然可以认为该节点的数据文件是完好无损的。网络分区(network partition)就是一个例子,若某个节点失去了与其他节点的连接,你只需要等待重新建立连接就行了,被分割开的节点也会重新变为副本集中的成员。还有一个类似的情况,某个节点的mongod
进程出于某些原因被终止了,但它可以恢复正常在线状态。7同样的,一旦进程重启,它就能重新加入集合了。
7. 举例来说,如果MongoDB是正常关闭的,那你肯定知道数据文件是好的。或者,如果使用了Journaling日志,不管是如何结束的,MongoDB实例都能恢复。
第二类故障包含所有明确故障(categorical failure),某个节点的数据文件不存在或者必须假设已经损坏。非正常关闭mongod
进程,又没有开启Journaling日志,以及硬盘崩溃都属于此类故障。恢复明确故障节点的唯一途径就是重新同步或利用最近的备份完全替换数据文件,让我们轮流看下这两种策略。
要完全重新同步,在故障节点上的某个空数据目录里启动一个mongod
。只要主机名和端口号没有改变,新的mongod
会重新加入副本集,随后重新同步全部现有数据。如果主机名或者端口号有变化,那么在mongod
重新上线后,你还需要重新配置副本集。举个例子,假设节点arete:40001的数据无法恢复,你在foobar:40000启动了一个新节点。你可以重新配置副本集,只需抓取配置文档,修改第二个节点的host
属性,随后将其传给rs.reconfig
方法:
> use local> config = db.system.replset.findOne{ "_id" : "myapp", "version" : 1, "members" : [ { "_id" : 0, "host" : "arete:30000" }, { "_id" : 1, "host" : "arete:30001" }, { "_id" : 2, "host" : "arete:30002" } ]}> config.members[1].host = "foobar:40000"arete:40000> rs.reconfig(config)
现在副本集可以识别新节点了,而新节点应该能从现有节点同步数据了。
除了通过完全重新同步进行恢复,还可以通过最近的备份进行恢复。通常都会使用某个从节点来进行备份8,方法是制作数据文件的快照并离线存储。仅当备份中的oplog不比当前副本集成员的oplog旧时,才能通过备份进行恢复。也就是说,备份的oplog里的最新操作必须仍存在于线上oplog里。可以用db.getReplicationInfo
提供的信息立即确定情况是否如此。在进行恢复时,不要忘记考虑还原备份所需的时间。要是备份里最新的oplog条目在从备份复制到新机器的过程有可能变旧,那么最好还是进行完全重新同步吧。
8. 第10章会详细讨论备份。
但通过备份进行恢复速度更快,部分原因是不用从零开始重新构建索引。要从备份进行恢复,将备份的数据文件复制到mongod
的数据路径里。应该会自动开始重新同步的,你可以检查日志或者运行rs.status
进行验证。
4. 部署策略
你已经知道了副本集最多可以包含12个节点,看过了一组令人眼花缭乱的配置选项表格,以及故障转移与恢复所要考虑的内容。配置副本集的方式有很多,但在本节中,我只会讨论那些适用于大多数情况的配置方式。
提供自动故障转移的最小副本集配置就是先前所构建的那个,包含两个副本和一个仲裁节点。在生产环境中,仲裁节点可以运行在应用服务器上,而副本则运行于自己的机器上。对于多数生产环境中的应用而言,这种配置既经济又高效。
但是对于那些对正常运行时间有严格要求的应用程序而言,副本集中需要包含三个完整的副本。那个额外的副本能带来什么好处呢?请想象这样一个场景:一个节点彻底损坏了。在恢复损坏的节点时,你还有两个正常的节点可用。只要第三个节点在线并正在恢复(这可能需要几个小时),副本集仍能自动故障转移到拥有最新数据的节点上。
一些应用程序要求有两个数据中心来做冗余,三个成员的副本集在这种情况下仍然适用。技巧在于让其中一个数据中心仅用于灾难恢复。图8-2就是一个例子。其中,主数据中心运行了副本集的主节点和一个从节点,备用数据中心里的从节点作为被动节点(优先级为0)。
图8-2 成员分布在两个数据中心的三节点副本集
在这个配置中,副本集的主节点始终是数据中心A里两个节点的其中之一。你可以在损失任意一个节点或者任意一个数据中心的情况下,保持应用程序在线。故障转移通常都是自动的,除非数据中心A的节点都发生了故障。同时损失两个节点的情况很少见,通常表现为数据中心A完全故障或者网络分区。要迅速恢复,可以关闭数据中心B里的节点,不带 --replSet
参数进行重启。除此之外,还可以在数据中心B里启动两个新节点,随后强制进行副本集重新配置。照道理不该在大多数节点无法访问时重新配置副本集,但在紧急情况下可以使用force
选项这么做。例如,假设定义了一个新的配置文档config,可以像下面这样强制进行重新配置:
> rs.reconfig(config, {force: true})
和所有生产系统一样,测试是关键,请确保在类似于生产环境的预发布环境中对所有典型故障转移和恢复场景进行测试。了解副本集在这些故障情况下会有何表现,这会让你在发生紧急情况时更从容不迫、处乱不惊。