第9章 创建副本集

本章介绍MongoDB的复制系统:副本集(replica set)。本章主要内容如下:

  • 副本集的概念;
  • 副本集的创建方法;
  • 副本集成员的可用选项 。

9.1 复制简介

从第1章开始,我们使用的一直是单台服务器,一个mongod服务器进程。如果只是用作学习和开发,这是可以的,但是如果用到生产环境中,风险会很高:如果服务器崩溃了或者不可访问了怎么办?数据库至少会有一段时间不可用。如果是硬件出了问题,可能需要将数据转移到另一个机器上。在最坏的情况下,磁盘或者网络问题可能会导致数据损坏或者数据不可访问。

使用复制可以将数据副本保存到多台服务器上,建议在所有的生产环境中都要使用。使用MongoDB的复制功能,即使一台或多台服务器出错,也可以保证应用程序正常运行和数据安全。

在MongoDB中,创建一个副本集之后就可以使用复制功能了。副本集是一组服务器,其中有一个主服务器(primary),用于处理客户端请求;还有多个备份服务器(secondary),用于保存主服务器的数据副本。如果主服务器崩溃了,备份服务器会自动将其中一个成员升级为新的主服务器。

使用复制功能时,如果有一台服务器宕机了,仍然可以从副本集的其他服务器上访问数据。如果服务器上的数据损坏或者不可访问,可以从副本集的某个成员中创建一份新的数据副本。

本章主要介绍副本集以及如何在系统上建立复制功能。

9.2 建立副本集

为了快速入门,本节会指导你在本地机器上建立一个包含三个成员的副本集。这些设置不适用于生产环境,但是可以让你熟悉复制功能以及相关的各种配置。

第9章 创建副本集 - 图1本节例子中的数据保存在/data/db目录下,应该在运行这些代码之前确保这个目录存在,而且当前用户对这个目录拥有写权限。

使用--nodb选项启动一个mongo shell,这样可以启动shell但是不连接到任何mongod:

  1. $ mongo --nodb

通过执行下面的命令就可以创建一个副本集:

  1. > replicaSet = new ReplSetTest({"nodes" : 3})

这行代码可以创建一个包含三个服务器的副本集:一个主服务器和两个备份服务器。但是,在执行下面两个命令之前mongod服务器不会真正启动:

  1. > // 启动3个mongod进程
  2. > replicaSet.startSet()
  3. >
  4. > // 配置复制功能
  5. > replicaSet.initiate()

现在已经有了3个mongod进程,分别运行在31000、31001和31002端口。这3个进程都会把各自的日志输出到当前shell中,这会让人很混乱。所以先把这个shell放在一边,再开启一个新的shell用于工作吧。

在第二个shell中,连接到运行在31000端口的mongod:

  1. > conn1 = new Mongo("localhost:31000")
  2. connection to localhost:31000
  3. testReplSet:PRIMARY>
  4. testReplSet:PRIMARY> primaryDB = conn1.getDB("test")
  5. test

注意,当连接到一个副本集成员时,提示符变成了"testReplSet:PRIMARY>"。其中"PRIMARY"是当前成员的状态,"testReplSet"是副本集的标识符。"testReplSet"ReplSetTest使用的默认名称,之后会讲述如何自定义副本集标识符。

为了简洁和可读性,之后的例子会使用">"代替"testReplSet:PRIMARY>"提示符。

在连接到主节点的连接上执行isMaster命令,可以看到副本集的状态:

  1. > primaryDB.isMaster()
  2. {
  3. "setName" : "testReplSet",
  4. "ismaster" : true,
  5. "secondary" : false,
  6. "hosts" : [
  7. "wooster:31000",
  8. "wooster:31002",
  9. "wooster:31001"
  10. ],
  11. "primary" : "wooster:31000",
  12. "me" : "wooster:31000",
  13. "maxBsonObjectSize" : 16777216,
  14. "localTime" : ISODate("2012-09-28T15:48:11.025Z"),
  15. "ok" : 1
  16. }

isMaster返回的字段有点儿多,其中有一个很重要的字段指明了这是一个主节点("ismaster" : true),副本集中还有一个hosts列表。

第9章 创建副本集 - 图2如果服务器返回内容"ismaster" : false,也是正常的。可以从"primary"字段获知主节点是哪一个,然后重新连接到主节点所在的主机/端口就可以了。

既然已经连接到主节点,就做一些写入操作看看会有什么发生吧!首先,插入1000个文档:

  1. > for (i=0; i<1000; i++) { primaryDB.coll.insert({count: i}) }
  2. >
  3. > // 检查集合的文档数量,确保真的插入成功了
  4. > primaryDB.coll.count()
  5. 1000

检查其中一个副本集成员,验证一下其中是否有刚刚写入的那些文档的副本。可以连接到任意一个备份节点:

  1. > conn2 = new Mongo("localhost:31001")
  2. connection to localhost:31001
  3. > secondaryDB = conn2.getDB("test")
  4. test

备份节点可能会落后于主节点,可能没有最新写入的数据,所以备份节点在默认情况下会拒绝读取请求,以防止应用程序意外拿到过期的数据。因此,如果在备份节点上做查询,可能会得到一个错误提示,说当前节点不是主节点。

  1. > secondaryDB.coll.find()
  2. error: { "$err" : "not master and slaveok=false", "code" : 13435 }

这是为了保护应用程序,以免意外连接到备份节点,读取到过期数据。如果希望从备份节点读取数据,需要设置“从备份节点读取数据没有问题”标识,如下所示:

  1. > conn2.setSlaveOk()

注意,slaveOk是对连接(例子中是conn2)设置的,不是对数据库(secondaryDB)设置的。

现在就可以从这个备份节点中读取数据了。使用普通的查询:

  1. > secondaryDB.coll.find()
  2. { "_id" : ObjectId("5037cac65f3257931833902b"), "count" : 0 }
  3. { "_id" : ObjectId("5037cac65f3257931833902c"), "count" : 1 }
  4. { "_id" : ObjectId("5037cac65f3257931833902d"), "count" : 2 }
  5. ...
  6. { "_id" : ObjectId("5037cac65f3257931833903c"), "count" : 17 }
  7. { "_id" : ObjectId("5037cac65f3257931833903d"), "count" : 18 }
  8. { "_id" : ObjectId("5037cac65f3257931833903e"), "count" : 19 }
  9. Type "it" for more
  10. >
  11. > secondaryDB.coll.count()
  12. 1000

可以看到刚刚写入的所有文档都出现在备份节点中了。

现在,试着在上执行写入操作:

  1. > secondaryDB.coll.insert({"count" : 1001})
  2. > secondaryDB.runCommand({"getLastError" : 1})
  3. {
  4. "err" : "not master",
  5. "code" : 10058,
  6. "n" : 0,
  7. "lastOp" : Timestamp(0, 0),
  8. "connectionId" : 5,
  9. "ok" : 1
  10. }

可以看到,不能对备份节点执行写操作。备份节点只通过复制功能写入数据,不接受客户端的写入请求。

有一个很有意思的功能你应该试一下:自动故障转移(automatic failover)。如果主节点挂了,其中一个备份节点会自动选举为主节点。为了验证这个功能,先关掉主节点:

  1. > primaryDB.adminCommand({"shutdown" : 1})

在备份节点上执行isMaster,看看新的主节点是哪一个:

  1. > secondaryDB.isMaster()

返回的内容如下所示:

  1. {
  2. "setName" : "testReplSet",
  3. "ismaster" : true,
  4. "secondary" : false,
  5. "hosts" : [
  6. "wooster:31001",
  7. "wooster:31000",
  8. "wooster:31002"
  9. ],
  10. "primary" : "wooster:31001",
  11. "me" : "wooster:31001",
  12. "maxBsonObjectSize" : 16777216,
  13. "localTime" : ISODate("2012-09-28T16:52:07.975Z"),
  14. "ok" : 1
  15. }

新的主节点也可以是其他服务器。第一个检测到主节点挂了的备份节点会成为新的主节点。现在可以向新的主节点发送写入请求了。

isMaster是一个非常老的命令了,那时副本集还没有出现,MongoDB只支持主从复制(master-slave replication)。所以它与副本集的术语有些不一致,isMaster中的主节点(master)与副本集中的主节点(primary)是等同的,从节点(slave)则相当于备份节点(secondary)。

在副本集上完成这些操作之后,从第一个shell中将其关闭。这个shell中现在应该充满了大量的副本集成员输出日志,敲几次Enter键之后就可以看到命令提示符了。可以执行下面的命令关闭副本集:

  1. > replicaSet.stopSet()

恭喜!你刚刚已经完成了创建副本集、使用副本集和关闭副本集的操作!

有几个关键的概念需要注意。

  • 客户端在单台服务器上可以执行的请求,都可以发送到主节点执行(读、写、执行命令、创建索引等)。
  • 客户端不能在备份节点上执行写操作。
  • 默认情况下,客户端不能从备份节点中读取数据。在备份节点上显式地执行setSlaveOk之后,客户端就可以从备份节点中读取数据了。

理解这些基本知识之后,本章剩余的部分是集中讲述在各种实际情况下应该如何配置副本集。记住,如果希望在实际中看看某个配置或者选项的效果,随时可以回到ReplSetTest

9.3 配置副本集

在实际的部署中,需要在多台机器之间建立复制功能。本节会完整建立一个真实场景下的副本集,你在自己的应用程序中可以直接使用。

假设你有一个运行在server-1:27017上的单个mongod实例,其中已经有一些数据(如果数据库中现在没有数据也没关系,只是数据目录会为空而已)。首先要为副本集选定一个名字,名字可以是任意的UTF-8字符串。

选好名称之后,使用--replSet name选项重启server-1。例如:

  1. $ mongod --replSet spock -f mongod.conf --fork

现在,使用同样的replSet和标示符(spock)再启动两个mongod服务器作为副本集中的其他成员:

  1. $ ssh server-2
  2. server-2$ mongod --replSet spock -f mongod.conf --fork
  3. server-2$ exit
  4. $
  5. $ ssh server-3
  6. server-3$ mongod --replSet spock -f mongod.conf --fork
  7. server-3$ exit

只有第一个副本集成员拥有数据,其他成员的数据目录都是空的。只要将后两个成员添加到副本集中,它们就会自动克隆第一个成员的数据。

replSet选项添加到每个成员各有自的mongod.conf文件中,以后启动时就会自动使用这个选项。

现在应该有3个分别运行在不同服务器上的mongod实例了。但是,每个mongod都不知道有其他mongod存在。为了让每个mongod能够知道彼此的存在,需要创建一个配置文件,在配置文件中列出每一个成员,并且将配置文件发送给server-1,然后server-1会负责将配置文件传播给其他成员。

首先创建配置文件。在shell中,创建一个如下所示的文档:

  1. > config = {
  2. "_id" : "spock",
  3. "members" : [
  4. {"_id" : 0, "host" : "server-1:27017"},
  5. {"_id" : 1, "host" : "server-2:27017"},
  6. {"_id" : 2, "host" : "server-3:27017"}
  7. ]
  8. }

这个配置文档中有几个重要的部分。"_id"字段的值就是启动时从命令行传递进来的副本集名称(在本例中是"spock")。一定要保证这个名称与启动时传入的名称一致。

这个文档的剩余部分是一个副本集成员数组。其中每个元素都需要两个字段:一个唯一的数值类型的"_id"字段,和一个主机名(将例子中的主机名替换为你自己实际使用的主机地址)。

这个config对象就是副本集的配置,现在需要将其发送给其中一个副本集成员。为此,连接到一个有数据的服务器(server-1:27017),使用config对象对副本集进行初始化:

  1. > // 连接到server-1
  2. > db = (new Mongo("server-1:27017")).getDB("test")
  3. >
  4. > // 初始化副本集
  5. > rs.initiate(config)
  6. {
  7. "info" : "Config now saved locally. Should come online in about a minute.",
  8. "ok" : 1
  9. }

server-1会解析这个配置对象,然后向其他成员发送消息,提醒它们使用新的配置。所有成员都配置完成之后,它们会自动选出一个主节点,然后就可以正常处理读写请求了。

第9章 创建副本集 - 图3可惜,无法将单机服务器转换为副本集,除非停机重启并进行初始化。即使只有一个服务器,可能你也想将它配置为一个只有一个成员的副本集。有了这样一个副本集之后,继续添加更多的成员时就不需要停机了。

如果正在创建一个全新的副本集,可以将配置文件发送给副本集的任何一个成员。如果副本集中已经有一个有数据的成员,那就必须将配置对象发送给这个拥有数据的成员。如果拥有数据的成员不止一个,那么就无法初始化副本集。

第9章 创建副本集 - 图4必须使用mongo shell来配置副本集。没有其他方法可以基于文件对副本集进行配置。

9.3.1 rs辅助函数

注意上面的rs.initiate()命令中的rsrs是一个全局变量,其中包含与复制相关的辅助函数(可以执行rs.help()查看可用的辅助函数)。这些函数大多只是数据库命令的包装器。例如,下面的数据库命令与rs.initiate(config)是等价的:

  1. > db.adminCommand({"replSetInitiate" : config})

对辅助函数和底层的数据库命令都做些了解是非常好的,有时直接使用数据库命令比使用辅助函数要简单。

9.3.2 网络注意事项

副本集内的每个成员都必须能够连接到其他所有成员(包括自身)。如果遇到某些成员不能到达其他运行中成员的错误,就需要更改网络配置以便各个成员能够相互连通。

另外,副本集的配置中不应该使用localhost作为主机名。如果所有副本集成员都运行在同一台机器上,那么localhost可以被正确解析,但是运行在一台机器上的副本集意义不大;如果副本集是运行在多台机器上的,那么localhost就无法被解析为正确的主机名。MongoDB允许副本集的所有成员都运行在同一台机器上,这样可以方便在本地测试,但是如果在配置中混用localhost和非localhost主机名的话,MongoDB会给出警告。

9.4 修改副本集配置

可以随时修改副本集的配置:可以添加或者删除成员,也可以修改已有的成员。很多常用操作都有对应的shell辅助函数,比如,可以使用rs.add为副本集添加新成员:

  1. > rs.add("server-4:27017")

类似地,也可以从副本集中删除成员:

  1. > rs.remove("server-1:27017")
  2. Fri Sep 28 16:44:46 DBClientCursor::init call() failed
  3. Fri Sep 28 16:44:46 query failed : admin.$cmd { replSetReconfig: {
  4. _id: "testReplSet", version: 2, members: [ { _id: 0, host: "ubuntu:31000" },
  5. { _id: 2, host: "ubuntu:31002" } ] } } to: localhost:31000
  6. Fri Sep 28 16:44:46 Error: error doing query:
  7. failed src/mongo/shell/collection.js:155
  8. Fri Sep 28 16:44:46 trying reconnect to localhost:31000
  9. Fri Sep 28 16:44:46 reconnect localhost:31000 ok

注意,删除成员时(或者是除添加成员之外的其他改变副本集配置的行为),会在shell中得到很多无法连接数据库的错误信息。这是正常的,这实际上说明配置修改成功了。重新配置副本集时,作为重新配置过程的最后一步,主节点会关闭所有连接。因此,shell中的连接会短暂断开,然后重新自动建立连接。

重新配置副本集时,主节点需要先退化为普通的备份节点,以便接受新的配置,然后会恢复。要注意,重新配置副本集之后会,副本集中会暂时没有主节点,之后会一切恢复正常。

可以在shell中执行rs.config()来查看配置修改是否成功。这个命令可以打印出副本集当前使用的配置信息:

  1. > rs.config()
  2. {
  3. "_id" : "testReplSet",
  4. "version" : 2,
  5. "members" : [
  6. {
  7. "_id" : 1,
  8. "host" : "server-2:27017"
  9. },
  10. {
  11. "_id" : 2,
  12. "host" : "server-3:27017"
  13. },
  14. {
  15. "_id" : 3,
  16. "host" : "server-4:27017"
  17. }
  18. ]
  19. }

每次修改副本集配置时,"version"字段都会自增,它的初始值为1。

除了对副本集添加或者删除成员,也可以修改现有的成员。为了修改副本集成员,可以在shell中创建新的配置文档,然后调用rs.reconfig。假设有如下所示的配置:

  1. > rs.config()
  2. {
  3. "_id" : "testReplSet",
  4. "version" : 2,
  5. "members" : [
  6. {
  7. "_id" : 0,
  8. "host" : "server-1:27017"
  9. },
  10. {
  11. "_id" : 1,
  12. "host" : "10.1.1.123:27017"
  13. },
  14. {
  15. "_id" : 2,
  16. "host" : "server-3:27017"
  17. }
  18. ]
  19. }

其中"_id"为1的成员地址用IP而不是主机名表示,需要将其改为主机名表示的地址。首先在shell中得到当前使用的配置,然后修改相应的字段:

  1. > var config = rs.config()
  2. > config.members[1].host = "server-2:27017"

现在配置文件修改完成了,需要使用rs.reconfig辅助函数将新的配置文件发送给数据库:

  1. > rs.reconfig(config)

对于复杂的数据集配置修改,rs.reconfig通常比rs.addrs.remove更有用,比如修改成员配置或者是一次性添加或者删除多个成员。可以使用这个命令做任何合法的副本集配置修改:只需创建想要的配置文档然后将其传给rs.reconfig

9.5 设计副本集

为了能够设计自己的副本集,有一些特定的副本集相关概念需要熟悉。下一章会详细讲述这些内容。副本集中很重要的一个概念是“大多数”(majority):选择主节点时需要由大多数决定,主节点只有在得到大多数支持时才能继续作为主节点,写操作被复制到大多数成员时这个写操作就是安全的。这里的大多数被定义为“副本中一半以上的成员”,如表9-1所示。

表9-1 怎样才算大多数

副本集中的成员总数 副本集中的大多数
1 1
2 2
3 2
4 3
5 3
6 4
7 4

注意,如果副本集中有些成员挂了或者是不可用,并不会影响“大多数”。因为“大多数”是基于副本集的配置来计算的。

假设有一个包含5个成员的副本集,其中3个成员不可用,仍然有2个可以正常工作,如图9-1所示。剩余的2个成员已经无法达到副本集“大多数”的要求(在这个例子中,至少要有3个成员才算“大多数”),所以它们无法选举主节点。如果这两个成员中有一个是主节点,当它注意到它无法得到“大多数”成员支持时,就会从主节点上退位。几秒钟之后,这个副本集中会包含2个备份节点和3个不可达成员。

第9章 创建副本集 - 图5

图9-1 由于副本集中只有少数成员可用,所有成员都会变为备份节点

可能会有很多人觉得这样的规则弱爆了:为什么剩余的两个成员不能选举出主节点呢?问题在于,3个不可达的成员并不一定是真的挂了,可能只是由于网络问题造成不可达,如图9-2所示。在这种情况下,左边的3个成员可以选举出一个主节点,因为3个成员可以达到副本集成员的大多数(总共5个成员)。

第9章 创建副本集 - 图6

图9-2 对于成员来说,左边的服务器会觉得右边的服务器挂了,右边的服务器也会觉得左边的服务器挂了

在这种情况下,我们不希望两边的网络各自选举出一个主节点:那样的话副本集就会拥有两个主节点了!两个主节点都可以写入数据,这样整个副本集的数据就会发生混乱。只有达到“大多数”的情况下才能选举或者维持主节点,这样要求是为了避免出现多个主节点。

通常只能有一个主节点,这对于副本集的配置是很重要的。例如,对于上面描述的5个成员来说,如果1、2、3位于同一个数据中心,而4、5位于另一个数据中心。这样,在第1个数据中心里,几乎总是可以满足“大多数”这个条件(这样就可以比较容易地判断出很可能是数据中心之间的网络错误,而不是数据中心内部的错误)。

一种常见的设置是使用2个成员的副本集(这通常不是你想要的):一个主节点和一个备份节点。假如其中一个成员不可用,另一个成员就看不到它了,如图9-3所示。在这种情况下,网络任何一端都无法达到“大多数”的条件,所以这个副本集会退化为拥有两个备份节点(没有主节点)的副本集。因此,通常不建议使用这样的配置。

第9章 创建副本集 - 图7

图9-3 如果成员总数是偶数,成员平均分配到不同的网络中,任何一边都无法满足“大多数”的条件

下面是两种推荐的配置方式。

  • 将“大多数”成员放在同一个数据中心,如图9-2所示。如果有一个主数据中心,而且你希望副本集的主节点总是位于主数据中心的话,这样的配置会比较好。只要主数据中心能够正常运转,就会有一个主节点。但是,如果主数据中心不可用了,那么备份数据中心的成员无法选举出主节点。

  • 在两个数据中心各自放置数量相等的成员,在第三个地方放置一个用于决定胜负的副本集成员。如果两个数据中心同等重要,那么这种配置会比较好。因为任意一个数据中心的服务器都可以找到另一台服务器以达到“大多数”。但是,这样就需要将服务器分散到三个地方。

更复杂的需求需要使用不同的配置,一定要考虑清楚,出现不利情况时,副本集要如何达到“大多数”的要求。

如果MongoDB的一个副本集可以拥有多个主节点,上面这些复杂问题就迎刃而解了。但是,多个主节点会带来其他的复杂性。拥有两个主节点的情况下,就需要处理写入冲突(例如,A在第一个主节点上更新了一个文档,而B在另一个主节点上删除了这个文档)。在支持多线程写入的系统中有两种常见的冲突处理方式:手工解决冲突或者是让系统任选一个作为“赢家”。但是这两种方式对于开发者来说都不容易实现,因为无法确保写入的数据不会被其他节点修改。因此,MongoDB选择只支持单一主节点。这样可以使开发更容易,但是当副本集被设为只读时,将导致程序暂时无法写入数据。

选举机制

当一个备份节点无法与主节点连通时,它就会联系并请求其他的副本集成员将自己选举为主节点。其他成员会做几项理性的检查:自身是否能够与主节点连通?希望被选举为主节点的备份节点的数据是否最新?有没有其他更高优先级的成员可以被选举为主节点?

如果要求被选举为主节点的成员能够得到副本集中“大多数”成员的投票,它就会成为主节点。即使“大多数”成员中只有一个否决了本次选举,选举就会取消。如果成员发现任何原因,表明当前希望成为主节点的成员不应该成为主节点,那么它就会否决此次选举。

在日志中可以看到得票数为比较大的负数的情况,因为一张否决票相当于10 000张赞成票。如果某个成员投赞成票,另一个成员投否决票,那么就可以在消息中看到选举结果为-9999或者是比较相近的负数值。

  1. Wed Jun 20 17:44:02 [rsMgr] replSet info electSelf 1
  2. Wed Jun 20 17:44:02 [rsMgr] replSet couldn't elect self, only received -9999 votes

如果有两个成员投了否决票,一个成员投了赞成票,那么选举结果就是-19999,依次类推。这些消息是很正常的,不必担心。

希望成为主节点的成员(候选人)必须使用复制将自己的数据更新为最新,副本集中的其他成员会对此进行检查。复制操作是严格按照时间排序的,所以候选人的最后一条操作要比它能连通的其他所有成员更晚(或者与其他成员相等)。

假设候选人执行的最后一个复制操作是123。它能连通的其他成员中有一个的最后复制操作是124,那么这个成员就会否决候选人的选举。这时候选人会继续进行数据同步,等它同步到124时,它会重新请求选举(如果那时整个副本集中仍然没有主节点的话)。在新一轮的选举中,假如候选人没有其他不合规之处,之前否决它的成员就会为它投赞成票。

假如候选人得到了“大多数”的赞成票,它就会成为主节点。

还有一点需要注意:每个成员都只能要求自己被选举为主节点。简单起见,不能推荐其他成员被选举为主节点,只能为申请成为主节点的候选人投票。

9.6 成员配置选项

到目前为止,我们建立的副本集中所有成员都拥有同样的配置。但是,有时我们并不希望每个成员都完全一样。你可能希望让某个成员拥有优先成为主节点的权力,或者是让某个成员对客户端不可见,这样便不会有读写请求发送给它。在副本集配置的子文档中可以为每个成员指定这些选项(甚至更多选项)。本节介绍可以对成员使用的选项。

9.6.1 选举仲裁者

上面的例子显示了具有两个成员的副本集在“大多数”要求上的缺点。但是,很多人的应用程序使用量比较小,并不想保存三份数据副本。两份副本已经足够了,保存第三份副本的话纯粹是浪费人力、物力和财力。

对于这种部署,MongoDB支持一种特殊类型的成员,称为仲裁者(arbiter)。仲裁者的唯一作用就是参与选举。仲裁者并不保存数据,也不会为客户端提供服务:它只是为了帮助具有两个成员的副本集能够满足“大多数”这个条件。

由于仲裁者并不需要履行传统mongod服务器的责任,所以可以将仲裁者作为轻量级进程,运行在配置比较差的服务器上。如果可能,应该将仲裁者放在单独的故障域(failure domain)中,与其他成员分开。这样它就可以以“外部视角”来看待副本集中的成员了,如9.5节在部署列表中推荐的一样。

启动仲裁者与启动普通mongod的方式相同,使用"--replSet 副本集名称"和空的数据目录。可以使用rs.addArb()辅助函数将仲裁者添加到副本集中:

  1. > rs.addArb("server-5:27017")

也可以在成员配置中指定arbiterOnly选项,这与上面的效果是一样的:

  1. > rs.add({"_id" : 4, "host" : "server-5:27017", "arbiterOnly" : true})

成员一旦以仲裁者的身份添加到副本集中,它就永远只能是仲裁者:无法将仲裁者重新配置为非仲裁者,反之亦然。

使用仲裁者的另一个好处是:如果你拥有的节点数是偶数,那么可能会出现一半节点投票给A,但是另一半成员投票给B的情况。仲裁者这时就可以投出决定胜负的关键一票。

1. 最多只能使用一个仲裁者

注意,在上面的例子中,最多只需要一个仲裁者。如果节点数量是奇数,那就不需要仲裁者。一种错误的理解是:为了“以防万一”,总是应该添加额外的仲裁者。但是,添加额外的仲裁者,并不能加快选举速度,也不能提供更好的数据安全性。

假设有一个3成员的副本集。需要两个成员才能组成“大多数”,才能选举主节点。如果这时添加了一个仲裁者,副本集中总共就有4个成员了,要有3个成员才能组成“大多数”。因此,副本集的稳定性其实是降低了:原本只需要67%的成员可用,副本集就可用;现在必须要有75%的成员可用,副本集才可用。

添加额外成员也会导致选举耗时变长。由于添加了仲裁者,现在副本集一共拥有偶数个成员,这样就可能出现两个成员票数相同的情况。仲裁者的目的应该是避免出现平票,而不是导致出现平票。

2. 仲裁者的缺点

不知道应该将一个成员作为数据节点还是作为仲裁者时,应该将其作为数据节点。在小副本集中使用仲裁者而不是数据节点会导致一些操作性的任务变困难。假设有一个副本集,它有两个“普通”成员,还有一个仲裁者成员,其中一个数据成员挂了。如果这个数据成员真的挂了(数据无法恢复),另一个数据成员成为主节点。这时整个副本集中只有一个数据成员和一个仲裁者成员。为了保证数据安全,就需要一个新的备份节点,并且将主节点的数据副本复制到备份节点。复制数据会对服务器造成很大的压力,会拖慢应用程序。通常,将几GB的数据复制到新服务器可以很快完成,不会对服务器和应用程序造成显著影响,但是如果要复制100 GB以上的数据,问题就会很严重了。

相反,如果拥有三个数据成员,一个服务器挂掉时,副本集中仍然有一个主节点和一个备份节点,不会影响正常运作。这时,可以用剩余的那个备份节点来初始化一个新的备份节点服务器,而不必依赖于主节点。

在上面两个数据成员+一个仲裁者成员的情景中,主节点是仅剩的一份完好的数据,它不仅要处理应用程序请求,还要将数据复制到另一个新的服务器上。

如果可能,尽可能在副本集中使用奇数个数据成员,而不要使用仲裁者。

9.6.2 优先级

优先级用于表示一个成员渴望成为主节点的程度。优先级的取值范围可以是0~100,默认是1。将优先级设为0有特殊含义:优先级为0的成员永远不能够成为主节点。这样的成员称为被动成员(passive member)。

拥有最高优先级的成员会优先选举为主节点(只要它能够得到集合中“大多数”的赞成票,并且数据是最新的)。假如在副本集中添加了一个优先级为1.5的成员:

  1. > rs.add({"_id" : 4, "host" : "server-4:27017", "priority" : 1.5})

假设其他成员的优先级都是1,只要server-4拥有最新的数据,那么当前的主节点就会自动退位,server-4会被选举为新的主节点。如果server-4的数据不够新,那么当前主节点就会保持不变。设置优先级并不会导致副本集中选不出主节点,也不会使数据不够新的成员成为主节点(一直到它的数据更新到最新)。

使用优先级时有一点需要注意:修改副本集配置时,新的配置必须要发送给在新配置下可能成为主节点的成员。因此,无法在一次reconfig操作中将当前主节点的优先级设置为0,也不能对所有成员优先级都为0的副本集执行reconfig

优先值的值只会影响副本集成员间相对优先级大小关系。如果某个副本集3个成员的优先级是500、1、1,另一个副本集3个成员的优先级是2、1、1,那么它们的行为是一样的。

9.6.3 隐藏成员

客户端不会向隐藏成员发送请求,隐藏成员也不会作为复制源(尽管当其他复制源不可用时隐藏成员也会被使用)。因此,很多人会将不够强大的服务器或者备份服务器隐藏起来。

假设有一副本集如下所示:

  1. > rs.isMaster()
  2. {
  3. ...
  4. "hosts" : [
  5. "server-1:27107",
  6. "server-2:27017",
  7. "server-3:27017"
  8. ],
  9. ...
  10. }

为了隐藏server-3,可以在它的配置中指定hidden : true。只有优先级为0的成员才能被隐藏(不能将主节点隐藏):

  1. > var config = rs.config()
  2. > config.members[2].hidden = 0
  3. 0
  4. > config.members[2].priority = 0
  5. 0
  6. > rs.reconfig(config)

现在,执行isMaster()可以看到:

  1. > rs.isMaster()
  2. {
  3. ...
  4. "hosts" : [
  5. "server-1:27107",
  6. "server-2:27017"
  7. ],
  8. ...
  9. }

使用rs.status()rs.config()能够看到隐藏成员,隐藏成员只对isMaster()不可见。客户端连接到副本集时,会调用isMaster()来查看可用成员。因此,隐藏成员不会收到客户端的读请求。

要将隐藏成员设为非隐藏,只需将配置中的hidden设为false就可以了,或者删除hidden选项。

9.6.4 延迟备份节点

数据可能会因为人为错误而遭受毁灭性的破坏:可能有人不小心删除了主数据库,或者刚上线的新版应用程序有一个严重bug,把所有数据都变成了垃圾。为了防止这类问题,可以使用slaveDelay设置一个延迟的备份节点。

延迟备份节点的数据会比主节点延迟指定的时间(单位是秒),这是有意为之。这样,如果有人不小心摧毁了你的主集合,还可以将数据从先前的备份中恢复过来。12.4.7节有详细介绍。

slaveDelay要求成员的优先级是0。如果你的应用会将读请求路由到备份节点,应该将延迟备份节点隐藏掉,以免读请求被路由到延迟备份节点。

9.6.5 创建索引

有时,备份节点并不需要与主节点拥有相同的索引,甚至可以没有索引。如果某个备份节点的用途仅仅是处理数据备份或者是离线的批量任务,那么你可能希望在它的成员配置中指定"buildIndexs" : false。这个选项可以阻止备份节点创建索引。

这是一个永久选项,指定了"buildIndexes" : false的成员永远无法恢复为可以创建索引的“正常”成员。如果确实需要将不创建索引的成员修改为可以创建索引的成员,那么必须将这个成员从副本集中移除,再删除它的所有数据,最后再将它重新添加到副本集中,并且允许它重新进行数据同步。

另外,这个选项也要求成员的优先级为0。