事故背景

​ 线上环境有一个MongoDB副本集,由于是部署在客户那边本地机房,客户误操作把部署副本集的另外2个节点的 VM 给删除了(并且VM已经无法恢复了)。所幸的是还有一个节点存活,登录节点后发现这个节点是 SECONDARY,所以可能会有一部分数据丢失,而且此时已经无法对应用提供读写服务。此时只能停服维护,并对集群进行恢复。

​ 基于以上问题,下面对副本集恢复操作步鄹进行了记录。

处理思路

  1. 对mongodb数据进行备份(防止恢复集群时出现意外导致数据丢失)。
  2. 把仅存的 SECONDARY 节点提升为 PRIMARY,删除集群中另外2个不存活的节点,然后重新配置MongoDB副本集。
  3. 新部署2个MongoDB节点,并加入到集群中。
  4. 等待 PRIMARY 节点数据同步到另外2个新节点后,进行数据验证,结束生产环境维护。

注意:

由于原先的集群中只存有 SECONDARY 节点,PRIMARY 节点已经丢失,所以存在部署数据没同步到 SECONDARY 的可能。但由于PRIMARY节点的VM已经被删,这部分未同步的数据的丢失在所难免,想恢复这部分数据只能根据自己的业务、代码逻辑设定才有补上丢失的数据的可能性。

集群恢复

1、在SECONDARY节点删除挂掉的primary节点

1.1 查看当前副本集配置

rs1:SECONDARY> rs.conf()

输出内容:

rs1:SECONDARY> use admin
switched to db admin
rs1:SECONDARY> rs_conf = rs.config()
{
"_id" : "rs1",
"version" : 7,
"protocolVersion" : NumberLong(1),
"members" : [
{
"_id" : 0,
"host" : "192.168.30.207:27017",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : {

},
"slaveDelay" : NumberLong(0),
"votes" : 1
},
{
"_id" : 1,
"host" : "192.168.30.213:27017",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : {

},
"slaveDelay" : NumberLong(0),
"votes" : 1
},
{
"_id" : 2,
"host" : "192.168.30.214:27017",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : {

},
"slaveDelay" : NumberLong(0),
"votes" : 1
}
],
"settings" : {
"chainingAllowed" : true,
"heartbeatIntervalMillis" : 2000,
"heartbeatTimeoutSecs" : 10,
"electionTimeoutMillis" : 10000,
"catchUpTimeoutMillis" : -1,
"catchUpTakeoverDelayMillis" : 30000,
"getLastErrorModes" : {

},
"getLastErrorDefaults" : {
"w" : 1,
"wtimeout" : 0
},
"replicaSetId" : ObjectId("5f5094994a4d5004eae73e2f")
}
}

1.2 删除集群成员

  • 比如要删除members中 host 为 192.168.30.213:27017 的成员,通过rs.conf()找到成员的 _id
{
"_id" : 1,
"host" : "192.168.30.213:27017",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : {

},
"slaveDelay" : NumberLong(0),
"votes" : 1
},
  • 删除 _id 为1的成员

splice的第一个参数表示要删除的数组元素的下标

0 表示集群中成员节点的 "_id"

1 表示删除的个数

rs1:SECONDARY> rs_conf = rs.conf()
rs1:SECONDARY> rs_conf.members.splice(0,1)

输出内容:

rs1:SECONDARY> rs_conf.members.splice(1,1)
[
{
"_id" : 1,
"host" : "192.168.30.213:27017",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : {

},
"slaveDelay" : NumberLong(0),
"votes" : 1
}
]

依照此方法删除副本集中不存活的节点。

注意:

有一点需要注意,由于已经删除了 _id 为1的成员,所以后面的成员的 _id 号都会减小1,与数组中元素的下标相同。

2、重新配置MongoDB副本集

2.1 重置集群配置

rs_conf 就是上面修改后的配置,加force参数是因为 SECONDARY 默认没有执行此命令的权限

rs1:SECONDARY> rs.reconfig(rs_conf, {"force":true})

返回内容:

rs1:SECONDARY> rs.reconfig(rs_conf, {"force":true})
{
"ok" : 1,
"operationTime" : Timestamp(1619586716, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1619588924, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
rs1:PRIMARY>

2.2 查看集群状态

rs1:PRIMARY> rs.status()

返回内容:

{
"set" : "rs1",
"date" : ISODate("2021-04-28T05:51:03.672Z"),
"myState" : 1,
"term" : NumberLong(17),
"syncingTo" : "",
"syncSourceHost" : "",
"syncSourceId" : -1,
"heartbeatIntervalMillis" : NumberLong(2000),
"optimes" : {
"lastCommittedOpTime" : {
"ts" : Timestamp(1619589055, 1),
"t" : NumberLong(17)
},
"readConcernMajorityOpTime" : {
"ts" : Timestamp(1619589055, 1),
"t" : NumberLong(17)
},
"appliedOpTime" : {
"ts" : Timestamp(1619589055, 1),
"t" : NumberLong(17)
},
"durableOpTime" : {
"ts" : Timestamp(1619589055, 1),
"t" : NumberLong(17)
}
},
"members" : [
{
"_id" : 0,
"name" : "192.168.30.207:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 7482,
"optime" : {
"ts" : Timestamp(1619589055, 1),
"t" : NumberLong(17)
},
"optimeDate" : ISODate("2021-04-28T05:50:55Z"),
"syncingTo" : "",
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"electionTime" : Timestamp(1619588924, 1),
"electionDate" : ISODate("2021-04-28T05:48:44Z"),
"configVersion" : 124340,
"self" : true,
"lastHeartbeatMessage" : ""
}
],
"ok" : 1,
"operationTime" : Timestamp(1619589055, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1619589055, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}

此时发现,这个 SECONDARY 节点已经提升为 PRIMARY,并且集群状态中,也就只有我们当前一个节点。

接下来就可以向副本集中添加新的MongoDB节点了。

3、添加新的MongoDB节点

这里省略新节点的部署过程,具体可以参考《MongoDB 单节点升级为副本集高可用集群》文章中MongoDB节点部署的步鄹。

注意:

向mongodb副本集添加实例后,PRIMARY节点数据能够自动同步到新添加的SECONDARY节点,无需人工干预。

3.1 增加实例

登录PRIMARY节点,添加MongoDB实例。

新添加的实例优先级权重默认为1,如需调整,建议等数据同步完成后进行权重更改。

rs1:PRIMARY> use admin
rs1:PRIMARY> rs.add('192.168.30.213:27017')
rs1:PRIMARY> rs.add('192.168.30.214:27017')

添加节点的返回结果如下:

rs1:PRIMARY> rs.add('192.168.30.213:27017')
{
"ok" : 1,
"operationTime" : Timestamp(1619581966, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1619581966, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
rs1:PRIMARY> rs.add('192.168.30.214:27017')
{
"ok" : 1,
"operationTime" : Timestamp(1619581975, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1619581975, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}

3.2 删除实例

如果添加错节点时,可以通过 rs.remove() 来删除错误的节点(因为此时当前实例已经是 PRIMARY 了,所以不需要用 1.2 中方法剔除节点)。

从mongodb副本集中移除实例,不可移除primary

rs1:PRIMARY> use admin
rs1:PRIMARY> rs.remove('192.168.30.214:27017')

返回内容:

rs1:PRIMARY> rs.remove('192.168.30.213:27017')
{
"ok" : 1,
"operationTime" : Timestamp(1619581713, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1619581713, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
rs1:PRIMARY> rs.remove('192.168.30.214:27017')
{
"ok" : 1,
"operationTime" : Timestamp(1619581777, 2),
"$clusterTime" : {
"clusterTime" : Timestamp(1619581777, 2),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}

注意:

副本集经过添加删除后顺序会乱,可以根据需要设置权重来调整。

4、调整节点权重

如果想在集群宕机恢复后,还想让某一节点始终保持为 PRIMARY,可以把此节点的权重设置成最大。

4.1 设置权重

找到对应节点在副本集中成员_id,进行权重设置。

这里以成员0为例,其host为192.168.30.207:27017

rs1:PRIMARY> rs_conf = rs.config()
rs1:PRIMARY> rs_conf.members[0].priority=10

4.2 生效配置

rs1:PRIMARY> rs.reconfig(rs_conf)

返回结果:

rs1:PRIMARY> rs.reconfig(rs_conf)
{
"ok" : 1,
"operationTime" : Timestamp(1619591404, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1619591404, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}

4.3 验证权重配置

  • 查询成员0的权重
rs1:PRIMARY> rs.config()

返回内容:

{
"_id" : 0,
"host" : "192.168.30.207:27017",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 10,
"tags" : {

},
"slaveDelay" : NumberLong(0),
"votes" : 1
}
  • 模拟宕机恢复后的集群状态

关闭三个节点的mongodb服务,再无序恢复,然后连接进节点192.168.30.207:27017,成员0依然还是PRIMARY。(为了必然偶然性,可以进行多次测试)