一个小 Case 总结 MongoDB 与 Redis 的主从同步问题。
作者:徐文梁,爱可生 DBA 成员,一个执着于技术的数据库工程师,主要负责数据库日常运维工作。擅长 MySQL,Redis 及其他常见数据库也有涉猎;喜欢垂钓,看书,看风景,结交新朋友。
爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。
本文约 1500 字,预计阅读需要 5 分钟。
问题背景
现场 MongoDB 版本为 4.4.14,PSS 架构。发现一个从节点的状态不正常,一直处于 STARTUP2 的状态,查看 optime 是 1970,因此无法同步数据也无法提供服务。现场同事将另一台从节点的 data 直接暴力 scp 到了不正常的那台,data 中包含的 oplog 也同步过去了,但同步完重启后,不正常的那台还是在 STARTUP2,optime 也还是 1970。
问题分析
了解现场的情况,直接 scp 从节点的 data 会导致数据不一致,这种情况下同步异常是符合预期的,现场数据量不大(20G 不到),因此可以通过逻辑初始化方式进行主从修复。
让现场同事进行如下操作:先 kill 异常从实例,然后把 data 目录清空,最后重新启动实例。做完这些,告诉现场同事只需要耐心等待就可以了,因为数据量不大,理论上一段时间后即可恢复同步正常,但是结局啪啪打脸了,一段时间后现场同事反馈还是同步失败。
此时只能通过查看日志进行解决了,让现场同事提供对应时间段的 mongo 日志,通过查看日志,发现如下信息:
{"t":{"$date":"2023-08-22T19:01:15.574+08:00"},"s":"I", "c":"INITSYNC", "id":21192, "ctx":"ReplCoordExtern-10","msg":"Initial sync status and statistics","attr":{"status":"failed","statistics":{"failedInitialSyncAttempts":10,"maxFailedInitialSyncAttempts":10,"initialSyncStart":{"$date":"2023-08-22T09:51:56.710Z"},"totalInitialSyncElapsedMillis":4158864,"initialSyncAttempts":[{"durationMillis":418787,"status":"OplogStartMissing: error fetching oplog during initial sync :: caused by :: Our last optime fetched: { ts: Timestamp(1692698292, 26), t: 44 }. source's GTE: { ts: Timestamp(1692698294, 42), t: 44 }","syncSource":"master:27017","rollBackId":8,"operationsRetried":0,"totalTimeUnreachableMillis":0},{"durationMillis":409866,"status":"OplogStartMissing: error fetching oplog during initial sync :: caused by :: Our last optime fetched: { ts: Timestamp(1692698692, 62), t: 44 }. source's GTE: { ts: Timestamp(1692698709, 12), t: 44 }","syncSource":"master:27017","rollBackId":8,"operationsRetried":0,"totalTimeUnreachableMillis":0},{"durationMillis":429278,"status":"OplogStartMissing: error fetching oplog during initial sync :: caused by :: Our last optime fetched: { ts: Timestamp(1692699126, 26), t: 44 }. source's GTE: { ts: Timestamp(1692699137, 18), t: 44 }","syncSource":"master:27017","rollBackId":8,"operationsRetried":0,"totalTimeUnreachableMillis":0},{"durationMillis":427610,"status":"OplogStartMissing: error fetching oplog during initial sync :: caused by :: Our last optime fetched: { ts: Timestamp(1692699549, 44), t: 44 }. source's GTE: { ts: Timestamp(1692699570, 87), t: 44 }",
通过上面信息,可以定位问题。从库从主库读取 oplog 位点时,主库目前的最小位点大于从库所需要读取的位点,因此导致同步失败,一般这种情况有两种可能:
- 全量数据数据量较大,同步时间较长,导致 oplog 之前的位点被覆盖。
- 该段时间内的修改操作较多,导致 oplog 之前的位点被覆盖。
问题处理
无论是上面哪种情况,都可以通过调整 oplog 大小进行解决。让现场同事先临时调大一倍 oplog 的值,然后重新进行同步,一段时间后现场同事反馈主从同步已经恢复。实际生产中 oplog 大小的设置参考
问题思考
问题解决了,但是透过这个小案例,我们是否能收获更多呢?答案是肯定的。
思考如下几个问题:
问题一:除了通过逻辑初始化方式进行主从修复,是否还有其他方式呢?
问题二:MongoDB 使用逻辑初始化方式进行主从修复,与 Redis 的主从修复相比有何异同?
问题一
对于问题一,常见的我们可以选择以下三种方式:
- 通过关闭异常实例,清空数据目录,然后启动 mongo,mongo 会自动进行初始化,从主实例同步数据,即 case 中使用的方式,该种方式操作简单,最安全,推荐。
- 对集群中正常节点进行 LVM 快照,然后复制数据到异常实例,启动异常实例。该方式 也不会阻塞源端读写,操作相对第一种方式稍显复杂。
- 对集群中正常节点执行
db.fsyncLock()
操作加锁后通过 cp 或 scp 等方式将数据目录拷贝到异常实例,然后对正常实例执行db.fsyncUnlock()
操作释放锁,然后启动异常实例。该操作会阻塞源端写操作,不推荐。
问题二
对于 MongoDB 使用逻辑初始化方式和 Redis 进行主从修复时,都是分为 全量数据 + 增量同步 两个阶段。
第一阶段
MongoDB 会克隆除 local 数据库之外的所有数据库。为了进行克隆,mongod 扫描每个源数据库中的各个集合,并将所有数据插入到这些集合各自的副本中。当初始化同步完成后,目标成员会从 STARTUP2 状态转为 SECONDARY 状态。
Redis 从实例会连接主实例,发送 psync
或者 sync
命令(不同版本有差别),主实例收到命令后,会通过执行 bgsave
命令生成 RDB 快照文件。
第二阶段
MongoDB 从节点成员在初始化同步之后会获取在数据复制期间新增的 oplog 记录,oplog 是循环写的,如果过小,可能会被覆盖,从而导致同步失败。oplog 大小可以通过 db.getReplicationInfo()
命令查看。
mgset-919:PRIMARY>db .getReplicationInfo()
{
"logSizeMB" : 512,
"usedMB" : 0.01,
"timeDiff" : 228185,
"timeDiffHours" : 63.38,
"tFirst" :"Thu Jul 27 2023 16:45:11 GMT+0800(CST)",
"tLast" :"sun Jul 30 2023 08:08:16 GMT+0800(CST)",
"now":"Tue Aug 29 2023 16:44:53 GMT+0800 (CST)"
……
可通过
db.adminCommand({replSetResizeOplog: 1, size: (200* 1024)})
命令在线修改,单位为 MB。
Redis 主实例在 bgsave
执行完成后,会向从实例发送快照文件。在此期间写操作命令会记录在缓冲区内,对应的参数是 client-output-buffer-limit
,表示大小限制是 512M,持续性限制是当客户端缓冲区大小持续 120 秒超过 128M,则关闭 SLAVE 客户端连接。如果设置过小,也会导致同步失败。可以通过 config get client-output-buffer-limit
命令查看。
#redis-cli -h 127.0.0.1 -p xxxx
127.0.0.1:xxxx> config get client-output-buffer-limit
1) "client-output-buffer-limit"
2) "noraml 524288000 0 0 slave 536870912 134217728 120 pubsub 33554432 8388608 60"
可通过
config set client-output-buffer-limit ”slave 1073741824 268435456 120"
在线设置,单位为比特。
PS:Redis 从 2.8 版本开始支持断点续传。该技术依赖增量复制缓冲区,称为 repl_backlog_buffer
,是一个定长的环形数组。如果数组内容写满了,则会从头开始覆盖之前的内容,未被覆盖的情况下,从节点与主节点能够在网络连接断开重连后,只从中断处继续进行复制,而不必重新同步。对应的参数为 repl-backlog-size
,如果网络环境较差,可以适当增大该参数值。
问题总结
通过 case,我们将 MongoDB 和 Redis 主从复制原理、修复方式以及常见问题完整的串联起来了,是不是收获不小呢?
留下一个小问题,client-output-buffer-limit
对应的缓冲区和 repl-backlog-size
对应的缓冲区有啥区别呢?