MongoDB副本集回滚那些事
回滚(rollback)操作是 MongoDB 副本集发生一些异常主备切换后可能发生的现象。回滚操作会撤销在当前节点上已执行的一些修改操作。
什么时候会触发回滚
MongoDB 副本集节点上有个同步线程,负责拉取需要同步的 oplog。被拉取 oplog 的节点称作同步源。那么,要回滚,首先要有一个同步源。
同步源
链式复制
平时我们都说主备同步主备同步,那同步源肯定是主节点了?其实不一定,MongoDB 很早就支持了链式复制,即备节点可以从另外一个备节点拉取 oplog,而不只从主节点拉取。这样一来可以减少主节点的负载,二来各节点可以选择离自己近的节点进行同步。当然,在某些情况下,这可能会导致一些备节点的延迟变大。链式复制可以通过以下命令来打开或关闭:
- cfg = rs.config() cfg.settings.chainingAllowed = true / false rs.reconfig(cfg)
Secondary 节点如何选择同步源
Secondary 节点会根据以下原则选择一个同步源:
- 如果之前有通过命令 replSetSyncFrom 指定了同步源,那么使用此同步源
- 由于后续需要根据到其他节点的 ping 值(通过心跳进行统计)信息进行选择,这里会判断一下是否已有足够的信息,需要等待更多的心跳包,如果不需要,继续,否则直接返回,等下次需要选择时再看
- 如果没有开启 Chained Replication(链式复制),那么选择 Primary
-
通过两轮选择,基于以下规则选择一个 ping 值最低的节点:
- 如果自己可以建索引,那么只能从同样可以建索引的节点同步
- oplog 的时间戳比我新(这里是获取该节点上次心跳包里带的 appliedOpTime 的时间戳进行比较)
- 不在黑名单中 (注:何时将同步节点加进黑名单?1. 连接不上该节点,加 10s 黑名单;2. 落后该节点太多无法继续同步,加 1min 黑名单)
其中在第一轮选择中,会额外考虑以下条件:
1. 拥有投票权的节点只能从同样拥有投票权的节点同步
2. 不能从 hidden 节点同步
3. 不能从落后 Primary 太多 (超过配置的 maxSyncSourceLagSecs)的节点同步
4. 不能从配置了比自己拥有更大 delay 的节点同步
如果第一轮没有选出合适的节点,那么再进行第二轮选择,放宽上述条件的限制。
什么时候会触发回滚(续)
回到回滚触发条件。同步线程已经选择出了一个同步源,它向同步源发起一个 find 请求,查询大于等于其最新的 oplog 时间戳的 oplog。如果发生以下两种情况,那么需要回滚:
1. 在同步源上没有查到比其更新的 oplog(我们刚刚通过一系列麻烦的规则选出它作为同步源,但是我们的 oplog 却比它还新)
2. 返回的的第一条 oplog 和其最新的 oplog 的 OpTime 和 hash 都不同,注意这里是比较整个 OpTime,即除了时间戳之外还包括 term,首先会比较 term,如果 term 不同,那就不同
回滚具体流程
回滚之前会获取 minvalid 集合的数据进行判断当前节点是否处于一致的状态,如果不是则直接 assert 结束进程。关于 minvalid 集合的作用,可参见。如果允许进行回滚,则执行以下步骤:
- 记录日志『rollback 0』
- 进入 ROLLBACK 状态
- 记录日志『rollback 1』
- 向同步源发送一个 replSetGetRBID 的命令获取一个 rollbackId,这个 rollbackId 是用来在后面判断在 rollback 过程中同步源自身是否发生回滚,每个节点如果发生 rollback,会修改自己的 rollbackId。
- 记录日志『rollback 2 FindCommonPoint'
- 查找自己和同步源的 oplog 的 commonPoint,这里是从同步源最新的 oplog 开始逆向查找,比较自己和同步源的最新的 oplog 的时间,计算相差的秒数,如果超过 30 分钟,那么放弃 rollback;如果本地的 oplog 时间戳比对方的更新,往前继续找,直到找到时间戳相等的那条。这里每比较一条本地的 oplog,都会对 oplog 的内容进行解析,从而得到回滚所需执行的操作集(包括需要重新从同步源获取的文档、需要重新同步的集合、需要 drop 的集合、索引等。这里同时也会进行一些判断,如果有发现某条 oplog 的大小大于 512MB,放弃回滚。如果有 dropDatabase 操作,放弃回滚。)。找到了时间戳相等且 hash 一致的 oplog,就找到了 commonPoint。
- 记录日志『rollback 3 fixup'
- 自增 rollbackId
- 接下来根据刚刚解析 oplog 得到的需要重新从同步源获取其最新版本的文档集,从同步源逐个获取,并保存在一个 map 中。这里会对要回滚的数据总大小进行判断,不能超过 300MB。所有文档处理完毕后,从同步源获取其最新的 oplog 的时间备用
- 记录日志『rollback 3.5』
- 再次获取同步源的 rollbackId,如果和刚刚得到的不一样,那说明同步源自身也发生了回滚,放弃这次回滚操作
- 记录日志『rollback 4 n: 需要更新的文档个数』
- 将刚才第 9 步从同步源得到的最新的 oplog 的时间戳作为结束时间,插入一个时间戳区间到 minvalid 集合,表明当前数据处于不一致状态。
- 如果有需要重新同步整个集合数据或元数据的的,逐个处理(重新同步集合数据的,先 drop 然后 copyCollection;重新同步集合元数据的,获取元数据并更新到本地),此处会记录日志『rollback 4.1.1 coll resync'或『rollback 4.1.2 coll metadata resync'。这里由于可能比较费时,记录日志『rollback 4.2』,然后再一次获取同步源最新的 oplog 时间戳记录到 minvalid 集合,并再次判断同步源是否自身发生回滚。如果一切正常,记录日志『rollback 4.3』。
- 记录日志『rollback 4.6』
- 处理需要 drop 的集合(如果有),这里会做 collScan 将要 drop 的集合的文档的内容写到 rollback 目录里的文件中
- 处理需要 drop 的索引(如果有)
- 记录日志『rollback 4.7』
- 处理刚刚从同步源获取的最新版本的文档集,先将本地的文档写到 rollback 目录里的文件中,然后删除或更新
- 记录日志『rollback 5 d: 删除的文档数 u: 更新的文档数』
- 记录日志『rollback 6』
- 清除本地 oplog 集合中在 commonPoint 之后的 oplog
- reload 本地的最新 oplog
- 记录日志『rollback done'
- 再次自增自己的 rollbackId
- 记录日志『rollback finished'
3.2.11 以前回滚的 bug
需要注意的是,当执行完最后一步记录日志『rollback finished',其实回滚还没真正结束。此时节点会进入 RECOVERING 状态,minvalid 集合中还记录了一个时间戳区间。即节点在回滚过程中记录了需要同步到哪个 OpTime,后续等同步线程追上这个时间点后才能变成 SECONDARY 状态。如果在这时候,发生了同步源切换,比如切换到另外一个同样需要回滚的节点,并且又将刚刚已清除掉的 commonPoint 之后的 oplog 给同步回来,那么就可能触发第二次回滚触发 assert 退出。关于这个 bug,MongoDB 官方已在 3.2.11 版本中修复,修复方法是在选择同步源的时候增加是否包含 minvalid 中 OpTime 的判断。我们阿里云数据库 MongoDB 也已 merge 了这个 bugfix。
来源: https://yq.aliyun.com/articles/66132