0%

MongoDB副本集

为了解决MongoDB单点故障问题,推出了副本集复制功能,其中主节点用于处理客户端读写请求,从节点用于复制主节点的数据,当主节点故障时,副本集自动切换提升一个成员作为新的主节点继续提供服务。

配置副本集

在设计副本集时,需要考虑到一个概念就是”大多数”。在MongoDB的选举规则中,选择的主节点要求得到大多数节点的支持才能成为主节点,这里的大多数被定义为大于群集成员数量一半以上,因此通常节点数量设计为奇数。假设一个包含5个成员的副本集,满足大多数要求则需要至少3个成员支持,如果副本集中有3个成员不可用,就无法满足大多数要求,无法从中选举出一个主节点,剩下的节点全部退化为从节点无法提供服务。

创建配置文件

1
2
3
4
5
6
7
8
config = {
"_id" : "repl1",
"members" : [
{"_id" : 0, "host" : "10.0.139.161:27017"},
{"_id" : 1, "host" : "10.0.139.162:27017"},
{"_id" : 2, "host" : "10.0.139.163:27017"}
]
}

初始化副本集

1
rs.initiate(config)

需要注意的是:副本集最大只能有12个成员,其中只有7个节点有投票权,超过7个需要将多余成员的votes设置为0,虽然没这些成员不可参加主节点选举,但任然可以投否决票。并不是副本集成员越多越好,副本集越大心跳请求的网络流量和选举花费的时间就越大

优先级

配置副本集时,我们可以对每个节点设置选举优先级,通过选项priority来设置优先级权重。拥有最高优先级的成员会优先选举作为主节点,只要其满足大多数的要求,当其拥有最新的数据,那么当前主节点就会自动退位,提升优先级最高的节点成为新的主节点。

成员角色

对于副本集,我们可以对成员做出一些特殊的角色配置。其中可选择的有:

  • 仲裁者:如果成员被定义为仲裁者,则不保存任何数据,也不对客户端提供服务,它仅作为投票者,具有选举功能,来帮助副本集达到大多数这个选举条件。我们可以通过rs.addArb(node)来添加仲裁节点或者在定义配置文件时对仲裁节点添加arbiterOnly:true来声明该节点是仲裁节点。

  • 隐藏成员:客户端不会向隐藏成员发送请求,隐藏成员也不会作为复制源,因此通常可以作为备份节点。通过配置选项hidden:true来隐藏指定节点,需要注意的是只有优先级为0的节点才能被隐藏。

  • 延迟节点:为防止数据被破坏,可以使用配置选项slaveDelay设置一个延迟的备份节点,延迟节点的数据会比主节点延迟指定的时间,单位为秒。设置延迟备份节点时,需要先将优先级设置为0,隐藏该节点,避免接收客户端请求。

修改副本集配置

有时我们需要对初始副本集配置进行修改,例如添加成员或者删除成员,也可以修改现有成员配置。

添加成员

1
rs.add("10.0.139.164:27017")

删除成员

1
rs.remove("10.0.139.164:27017")

修改现有成员优先级

1
2
3
var config = rs.config()
config.members[0].priority=2
rs.reconfig(config)

需要注意的是,在更新副本集配置时,主节点会关闭所有连接,退化为从节点以便接收新的配置,然后恢复正常。

副本集原理

同步

MongoDB的副本集是基于oplog实现的,oplog包含了主节点的每一次写操作,oplog对应local数据库下的一个固定集合。从节点通过查询oplog集合就能获取到对应操作记录进行复制。每个节点都维护着自己的oplog,因此每个成员都能作为同步复制源提供给其它节点使用,并不总是从主节点进行同步复制。

在从节点重启后,会自动从oplog中的最后一个操作开始进行同步。由于同步复制是先复制数据再写入oplog,存在已同步过的数据再次执行的情况,因此MongoDB设计了幂等性,一个oplog操作无论执行多次和执行一次结果是一样的。

副本集成员启动后,就会检查自身状态,确定是否可以从某个节点进行同步。如果不行,则尝试进行完整的数据复制。这个过程就是初始化同步,它主要包含以下步骤:

  1. 选择一个复制源,在local.me中为创建一个标识符,删除已经存在的数据库
  2. 将同步源的数据复制到本地
  3. 记录克隆过程中所有操作到oplog中
  4. 将上述的oplog同步到从节点
  5. 数据复制完成,创建索引
  6. 应用创建索引过程中产生的oplog
  7. 完成初始化同步,切换到secondary正常状态

初始化同步过程中,第二步和第五步都比较耗时,可能会导致节点远远落后同步源,从而导致复制数据被覆盖。另外初始化同步会强制将当前成员的所有数据加载到内存中,导致频繁的访问的数据不能常驻内存,导致请求变慢。因此建议在副本集空闲时间执行并确保oplog足够大

心跳

为了获取到副本集中其它成员的状态,成员每隔两秒会向其它节点发送一个心跳请求,主要用于检查成员状态,判断主节点是否满足大多数要求,不满足则重新选举。成员状态除了primary和secondary,也包含一些其它状态:

  • STARTUP:成员刚启动时处于这个状态,加载完配置后切换到STARTUP2状态
  • STARTUP2:MongoDB会创建几个线程,用于处理复制和选举,然后切换到RECOVERY状态
  • RECOVERY:该状态下暂时无法处理请求,在处理非常耗时的操作时,成员也可能进入该状态。当成员与其它成员脱节时,也会进入该状态,这时可能需要重新同步
  • ARBITER:仲裁者始终处于该状态
  • DOWN:如果成员不可达时,就会处于DOWN状态
  • UNKNOWN:如果成员无法到达其其它任何成员,其它成员就无法知道它处于什么状态,会将其报告为UNKNOWN状态
  • REMOVED:成员被移除副本集时,它处于REMOVED状态
  • ROLLBACK:如果成员正在进行回滚,它就处于ROLLBACK状态,回滚完成切换到RECOVERY状态,然后成为secondary
  • FATAL:如果一个成员发生了不可挽回的错误,也不再尝试恢复正常的话,就处于FATAL状态

数据回滚

如果主节点执行完写操作后就挂了,从节点可能没来得及复制该操作,那么新选举出来的主节点就会遗漏这次写操作。当挂掉的主节点恢复后,就会向其它复制源进行数据复制,当其无法从其它节点获取到最后的写操作就会进行回滚,撤销这次写操作,从共同点继续复制。

回滚的操作会记录在数据目录的rollback目录下,文件名为collectionName.bson。如果需要将回滚记录应用到当前的主节点,我们可以通过mongorestore将其加载到一个临时集合

1
mongorestore --db temp --collection test /mongodb/data/important.test.2020-06-27T15-30-00.0.bson

如果回滚的数据量大于300MB,或者回滚30分钟以上的操作,回滚就会失败,对于回滚失败的节点,必须重新同步。

为避免数据回滚的情况,我们希望不管发生什么都将写操作都确认写入操作复制到了副本集的大多数。我们可以通过getLastError命令配合w选项强制要求getLastError等待,一直到给定数量的成员都执行完最后的写入操作,仅阻塞当前会话。w选项的值可设置为majority来要求大多数成员都执行完成。

当执行该命令时,如果副本集只有一个主节点和仲裁节点,主节点无法将操作复制到副本集任何成员。getLastError无法确定要等待多久,会一直等待下去。因此,我们还应该设置wtimeout选项指定超时时间,如果超时还没有返回则返回错误。

1
db.runCommand({"getLastError : 1, "w" : "majoriry", "wtimeout" : 1000})

自定义复制保证规则

有时我们的副本集成员分散在多个机房中,我们希望确保写操作能复制每个数据中心至少一个节点上。副本集允许我们创建自己的规则,并且可以传递给getLastError

在创建规则前,我们需要对副本集成员进行分类,可以在副本集配置中添加tags字段

1
2
3
4
var config = rs.config()
config.members[0].tags = {"IDC" : "SZ"}
config.members[1].tags = {"IDC" : "SZ"}
config.members[2].tags = {"IDC" " "SH"}

接下来就是创建自己的规则,通过在副本集配置中创建”getLastErrorMode”选项实现。每个规则的形式都是由”name”:{“key” : “number”}。name就是规则名称,key就是标签的值,number需要遵循这条规则分组的数量

1
2
3
config.settings = {}
config.settings.getLastErrorModes = [{"eachIDC" : {"IDC" : 2}}]
rs.reconfig(config)

应用自定义的规则

1
db.runCommand({"getLastError" : 1, "w" : "eachIDC", "wtimeout" : 1000})

副本集维护

修改成员状态

当我们进行副本集维护时,会手动修改副本集成员状态。

1
rs.stepDown()

这个命令会让主节点退化为从节点,并维持60秒,也可以手动设置一个时间值。如果这段时间没有选举出新的主节点,这个节点就会重新参加选举

如果不希望维护期间,其它成员选举为主节点,可以在每个从节点上执行freeze命令,以强制它们处于secondary

1
rs.freeze(10000)

维护完成后,如果想提前释放其它成员,可以再次执行freeze(0)

除此之外,我们也可以认为让成员进入维护模式,成员会变成RECOVERING状态,客户端将不再发送请求到这个成员上,也不能作为复制源。

1
db.adminCommand({"replSetMaintenanceMode" : "true"})

复制链路

在从节点执行rs.status()命令时,输出信息中会有一个”syncingTo”的字段,其用于表示当前成员以哪个节点作为复制源进行复制。如果在所有从节点上执行replSetGetStatus命令就能弄清楚副本集整个的复制链路情况。

MongoDB根据ping时间选择复制源,一个成员向另一个成员发送心跳请求,就可以知道ping所耗费的时间。MongoDB维护着不同节点成员间心跳请求的平均花费时间。选择同步源时,会选择一个离自己比较近而且数据也比自己新的成员。

自动复制链路也存在一些缺点,复制链路越长,将写操作复制到所有节点所花费的时间就越长。极端情况可能会形成一种串行的复制链路,每个从节点都要比前面的从节点要落后,这种情况可以通过rs.syncFrom命令修改成员的复制源。

当然,我们也可以禁用复制链路,要求所有从节点都从主节点进行复制,只需要将allowChaining设置为false

1
2
3
4
var config = rs.config()
config.settings = config.settings || {}
config.settings.allowChaining = false
rs.reconfig(config)

跟踪延迟

延迟是指从节点相对于主节点的落后程度,是主节点最后一次操作的时间戳与从节点最后一次操作时间戳的差值。在主节点执行db.printReplicationInfo或在从节点执行db.printSlaveReplicationInfo能够快速获得同步信息

1
2
3
4
5
6
shard1:PRIMARY> db.printReplicationInfo()
configured oplog size: 2048MB
log length start to end: 8659767secs (2405.49hrs)
oplog first event time: Thu Mar 19 2020 11:39:52 GMT+0800 (CST)
oplog last event time: Sat Jun 27 2020 17:09:19 GMT+0800 (CST)
now: Sat Jun 27 2020 17:09:28 GMT+0800 (CST)

输出信息中包含了oplog的大小,以及oplog包含的操作时间范围,如果log length start to end较小可能就需要对oplog进行扩容了。

1
2
3
4
shard3:SECONDARY> db.printSlaveReplicationInfo()
source: 10.0.139.161:22000
syncedTo: Sat Jun 27 2020 17:12:05 GMT+0800 (CST)
0 secs (0 hrs) behind the primary

从上面的输出可以看出当前成员的复制源以及相对于主节点的复制延迟。