前几天,一个做电商的朋友在群里抛了个问题:「订单数据在 MongoDB 里,现在要实时同步一份到数仓做报表,顺便再往国产库里备一份,这玩意儿到底该怎么搞?」
群里安静了几秒,然后有人甩了个链接------MongoDB 官方文档。朋友回了一句:「看过了,满屏参数,越看越懵。」
我后来琢磨,这问题挺典型的。你搜「MongoDB 数据同步」,出来的大概是三拨东西:官方文档(权威,但像天书)、某个工具的操作手册(只讲它自己,不告诉你该不该用它)、还有几篇散落的技术博客(有用,但零碎)。唯独没人把「你到底该选哪种方案」这件事讲清楚。
所以今天这篇,我把 MongoDB 数据同步的 4 条路线一次性捋清楚,配上选型对比表,再加几个我这些年踩过的坑。看完你应该能对着自己的场景,直接圈出该走哪条。
一、先把「同步」这两个字掰扯清楚
很多人一上来就懵,是因为「MongoDB 数据同步」这个词,指的压根不是一回事。往细了说,至少有两层:
- 副本集内部的复制(replication):主节点和从节点之间保持数据一致。这是 MongoDB 自己干的活,目的是高可用和读写分离。
- 跨实例、跨库的同步(sync / migration):把 Mongo 的数据搬到另一个 Mongo、关系型库、数仓,或者反向。这得靠外部工具或者自己写代码。
不把这两层分清,你就没法解释为什么官方文档和工具教程讲的完全是两码事。下面 4 条路线,第一条是「内」,后三条是「外」。
二、路线一:副本集 oplog 复制(MongoDB 自带的)
如果只是想让同一个集群里的几个节点数据一致,那 MongoDB 自己就搞定了,核心就是 oplog(operation log,操作日志)。
原理一句话:主节点每执行一次写操作,就往本地的 oplog 集合里记一条;从节点不停地从这个集合里拉最新记录,再照着重放一遍。因为记录的是「操作」而不是「结果」,所以天然是增量、且顺序严格。
从节点刚加入副本集的时候,会先做一次初始同步(initial sync) ,把全量数据拉过来;之后才进入持续复制,追 oplog 增量。怎么看追得怎么样了?两条命令:
javascript
// 看整体副本集状态
rs.status()
// 看每个从节点的复制落后情况
rs.printSecondaryReplicationInfo()
rs.status() 里重点看每个成员的 stateStr:PRIMARY、SECONDARY、STARTUP2(正在初始同步)。要是从节点一直卡在 STARTUP2 不动,多半是数据量大或者网络不行。
这条路我最想提醒的坑,是 oplog 窗口(oplog window)。 oplog 是个有上限的集合,满了就把最旧的挤掉。如果从节点追得太慢,等它再想追的时候,它还没同步到的那些操作已经被主节点清掉了------这个从节点就「stale」(陈旧)了。到了这一步,增量追不回来了,只能删数据重做一次初始同步。所以生产上,oplog 的大小、从节点的复制延迟,都得盯着。
顺带说一句 oplog 大小怎么估:它至少要能覆盖「你最长的复制中断窗口 + 一次完整初始同步的时间 + 预留维护窗口」。三个数一加,再乘个安全系数,基本就不会在关键时刻掉链子。
小结:路线一解决的是「同一个副本集内部」的一致性问题,纯内部机制,不涉及「搬到别的库」。
三、路线二:Change Streams(自己写消费者)
如果你要把 Mongo 的变化实时推出去------比如推到另一个 Mongo、推到消息队列、或者触发业务逻辑------MongoDB 官方给了一套 Change Streams(变更流) 的 API。
它其实还是建立在 oplog 之上,只不过把「读 oplog」这件事包装成了订阅接口,你写个消费者挂着就能收变化。拿 Node.js 举个例子:
javascript
const { MongoClient } = require('mongodb');
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('orders');
const changeStream = db.collection('order_info').watch([
{ $match: { operationType: { $in: ['insert', 'update', 'delete'] } } }
]);
changeStream.on('change', (next) => {
// next 里就是这次变更的详情,含 _id、operationType、fullDocument 等
console.log(next.operationType, next.fullDocument);
});
思路很干净,能力也强。但我要说句实在话:Change Streams 是「半成品」,不是「成品方案」。 它只负责把变化「吐」给你,后面的事全得你自己扛:
- 断点恢复 :消费者挂了,重启后得用
resumeToken(恢复令牌)接着上次的位置继续,这个 token 怎么持久化、怎么处理过期,全是你自己的活。 - 目标端映射:Mongo 的文档结构落到关系型表里怎么对应、嵌套文档怎么拆、类型怎么转,自己写。
- 消费监控:一个消费线程悄悄死了、没人发现,漏的数据能让你半夜惊醒。
所以这条路适合「有开发资源、需求比较定制」的场景;想省心的话,接着往下看。
四、路线三:同步工具(MongoShake / mongosync / 云 DTS)
不想自己写代码,那就找现成工具。搜「MongoDB 数据同步」大概率会碰到这几个名字,我按场景帮你划个边界:
- MongoShake:阿里云开源的工具,本质也是读 oplog 做转发。强项是 Mongo → Mongo、Mongo → Kafka 这类链路,配置简单,社区活跃。适合「Mongo 生态内部」的实时同步。
- mongosync:MongoDB 官方出的工具,定位是「集群间的一次性数据迁移」,适合搬一次就完事的那种,不适合当长期实时同步通道。
- 云厂商 DTS :各家云自带的传输服务,开箱即用、有控制台。但缺点是绑死自家云------目标端要是自建库、私有化环境或者信创环境,基本用不上。
这三个的共同边界是:它们主要围着 MongoDB 生态打转,或者围着某朵云打转。 一旦你的目标端是「关系型库 / 数仓 / 国产库」,它们就有点使不上劲了。
五、路线四:异构 CDC 工具(MongoDB → 关系型/国产库)
前面三条路线,其实共享一个盲区:目标端不是 MongoDB 生态的时候,都差点意思。
- Change Streams:要自己写映射、自己管断点;
- MongoShake:到关系型库还得再接一层;
- 云 DTS:绑云,出不了圈。
所以就有了第四条路线------专门的异构 CDC(Change Data Capture)工具。它的定位就是「在不同类型的数据库之间做增量同步」,把读日志、类型映射、断点续传、一致性校验这些脏活累活打包干掉。
我在信创项目里接触比较多的是电科金仓的 KFS(Kingbase FlySync)。它走的路子和 Oracle GoldenGate 是一个思路:挂在源库上解析日志(Redo Log、Binlog、MongoDB 的 oplog 这类),把变化实时捕获下来,再翻译成目标端能懂的语句写进去。官方口径里,它内置 20 余种源库适配器,MongoDB、Oracle、MySQL、SQL Server 都在里面,目标端可以落到 KingbaseES 这类关系型库。
几个我印象比较深的点,都是它跟「自己写 Change Streams」拉开差距的地方:
- 秒级延迟:做的是日志级 CDC,不是定时轮询,增量同步延迟能压到秒级(官方标称 1 秒以内)。
- 断点续传:任务中断了,按位点(LSN/SCN 那套思路)记着搬到哪了,恢复后接着跑,不用从头再来。
- 在线数据比对:同步完还能帮你做数据比对,行数、内容对不上会告警,不用自己写脚本去数。
当然,客观说一句:这类商业化工具的代价是得部署管理端、要付费,比你自己写 Change Streams 成本高。所以它的适用场景也很明确------目标是关系型/国产库、要长期实时同步、又不想养一套同步代码。如果你的目标端就是另一个 MongoDB 集群,那路线三里更轻的选项可能更合适。
六、选型对比表
不想看长篇的,直接看这张表:
| 方案 | 同步方向 | 是否实时 | 维护成本 | 最适合的场景 |
|---|---|---|---|---|
| 副本集 oplog 复制 | Mongo 集群内部(主↔从) | 是(近实时) | 低(MongoDB 自带) | 高可用、读写分离、副本集扩容 |
| Change Streams | Mongo → Mongo / 应用 / 消息 | 是 | 高(自己写消费者) | 有开发能力、需求定制 |
| MongoShake / mongosync / 云 DTS | 主要在 Mongo 生态内 | 是(MongoShake)/ 否(mongosync 一次性) | 中 | Mongo→Mongo、Mongo→Kafka、一次性迁移 |
| 异构 CDC 工具(如金仓 KFS) | Mongo → 关系型 / 数仓 / 国产库 | 是 | 中(商用工具) | 目标端非 Mongo 生态、要长期实时同步 |
一句话总结选型逻辑:先看目标端是谁。 目标端还是 Mongo,走前三条;目标端是关系型/国产库,直接看第四条。
七、实操演练:给副本集加个从节点
光说不练假把式。这里补一个最常用的操作------给副本集扩容一个从节点,并观察它的同步进度。这也是官方文档往往一笔带过、但生产上总要手把手做的一步。
前提:新节点的 mongod 已经用和副本集相同的 keyfile 启动,配置文件里 replication.replSetName 一致。
在主节点上执行:
javascript
rs.add("mongodb://新节点IP:27017")
加进去之后,新节点会进入 STARTUP2 状态开始初始同步。想看它同步到哪了,别只看 rs.status() 的状态,再开一个窗口看它的实际操作:
javascript
// 在任意节点上,看正在跑的同步相关操作
db.currentOp({
"desc": { $regex: "initial sync|repl" }
})
或者直接去新节点的日志里找 initial sync 关键字,能看到类似「synced from」和分阶段进度。判断同步完成的标准 :rs.status() 里该成员的 stateStr 从 STARTUP2 变成 SECONDARY,并且 rs.printSecondaryReplicationInfo() 里它的落后时间趋近于 0。
一个小提醒:初始同步会从源节点拉全量数据,多少会占源节点的 IO 和带宽,尽量避开业务高峰期做扩容;数据量大的话,先评估一下 oplog 够不够大,别同步到一半 oplog 窗口不够又得重来。
八、初始同步的两种姿势:逻辑同步 vs 文件拷贝
上面说的「初始同步」,从 MongoDB 5.2 版本起其实有两种玩法,很多人在这一步吃了亏,我单独拎出来说。
逻辑同步(默认) :一边从源节点扫描集合、复制文档,一边同步建索引;这期间新产生的写操作,先暂存在本机 local 库的 oplog 里,全量拷完再追增量。好处是通用、不用停任何东西;坏处是慢------数据量大、索引多的时候,光建索引就能磨很久。
文件拷贝同步 :直接把源节点的数据文件(WiredTiger 文件)拷过去。速度明显快,但限制一大堆,而且是 Enterprise 版本才有的:
- 会替换掉目标节点的
local库; - 同步期间不能做备份;
- 不能并发从多个源同步;
- 拷贝完
count()结果可能不准确(文件快照的元数据和实际行数有出入)。
所以我的建议很粗暴:能走逻辑同步就走逻辑同步,除非数据量巨大、你又用的是企业版、而且清楚上面几个坑。文件拷贝那套,更像是「官方给超大集群开的后门」,不是给你图省事用的。
还有个参数值得记住:initialSyncSourceReadPreference。它决定新节点从哪个成员拉数据------默认优先主节点,主节点不合适再放宽条件筛一轮。生产上如果不想让主节点扛这个 IO,可以把它指到一个专用同步源(比如 hidden 节点,后面第十节会讲)。
小知识:初始同步是有容错的。遇到临时网络错误会自己恢复,默认给 24 小时恢复窗口、最多重试 10 次;过了窗口还没救回来,就换一个健康源从头再来。所以偶尔看到它「重来」,别慌,先看是不是网络抖了。
九、同步进度怎么盯、一致性怎么验
「同步完了」得有客观标准。我的习惯是几件套一起上:
① 看进度。 rs.status() 看 stateStr 是粗颗粒的,再补一个更细的:
javascript
db.currentOp({
"desc": { $regex: "initial sync|repl|sync" }
})
新节点自己的日志里搜 initial sync,能看到分阶段进度(建索引、复制集合、追 oplog)。判断「追平」的金标准:rs.printSecondaryReplicationInfo() 里落后时间稳定在个位数毫秒,且不再扩大。
② 数行数。 全量同步完,源端和目标端分别跑一遍:
javascript
db.order_info.countDocuments({})
行数对上了,才过第一关。
③ 抽数据比对。 行数一样不代表内容一样。Mongo 原生没有跨集群的「一键校验」,要么自己写脚本抽样比对,要么直接用同步工具自带的数据比对能力(这也是前面 KFS 那类工具的价值之一------把比对做成内置能力,对不上会告警)。自己写的话,挑几个关键字段做抽样哈希比对就够用。
④ 看延迟趋势。 生产上最好给复制延迟挂监控,落后时间超过阈值就告警。等肉眼发现数据不对,往往已经晚了。
十、几个容易忽略的特殊拓扑
前面讲的都是「规规矩矩的副本集」,生产上总有些特殊配置,同步行为会不一样,逐个点一下:
- 链式复制(chained replication) :默认情况下,从节点可以从别的从节点拉 oplog,而不是必须连主节点。好处是给主节点减负;代价是一层链上的延迟会叠加。某个中间节点慢了,下游跟着慢,排查时要先搞清楚「谁在给谁供数据」。
- 延迟节点(delayed secondary) :故意让某个从节点落后一段时间 (比如 1 小时)再应用 oplog。它不是为了同步快,而是当「后悔药」------误删了数据,还能从这个节点把 1 小时前的状态捞回来。这种节点的「落后」是人为的、正常的,别一看落后就报警。
- hidden 节点(hidden member) :不参与选举、也不对外提供读服务,但照样同步。特别适合当备份源或同步源 ------把
initialSyncSourceReadPreference指到 hidden 节点,加新节点时就能绕开主节点。 - 分片集群:每个分片本质上是独立副本集,同步按分片各自进行。给某个分片扩容、重同步,只影响那一个分片,别把整个集群当成一个整体去操作。
十一、踩坑清单与速查
这些年我在「MongoDB 数据同步」这件事上栽过的跟头,挑几个高频的列出来:
- 从节点突然 stale :主节点 oplog 被覆盖,从节点追不上。判断信号:
rs.printSecondaryReplicationInfo()落后时间越来越大,或者日志报too stale to catch up。解法:删数据重做初始同步(第七节那套),并同步把 oplog 调大。 - 「复制落后多少算 stale」心里没数:别等它报错才看。给复制延迟挂告警,落后时间超过业务容忍线(比如 30 秒)就触发,比事后救火强。
- Change Streams 的 resumeToken 丢了:消费者重启后拿不到 token,只能从当前时间点重放,中间变化就漏了。解法:把 token 落到持久化存储里,随消费进度一起更新。
- 同步完成后没校验:同步「跑完」不等于「跑对」。用工具自带的数据比对,或者自己写脚本抽几条对一对,别等业务发现数据少了再回头查。
- 拿 mongodump 给副本集成员做种子 :不行。副本集成员的种子要保证一致性快照、还要带
local库,mongodump 满足不了这个前提,容易出「看着好了、实际不一致」的暗病。 - 忘了账号权限这茬 :读 oplog、订阅 Change Streams 都需要相应权限,账号没配好,工具会报
not authorized。源端给个能读 oplog、目标端给个能写的账号,别用 admin 一把梭。 - 低估了读 oplog 对源库的影响:无论自建消费者还是工具,都是靠持续读源端 oplog 干活。高频写入场景下,这个读取是有开销的,源端 oplog 保留时长、抓取频率都要调好,别把「同步」变成了「拖累」。
顺手附一张速查表,出问题先对着找:
| 现象 / 报错信号 | 大概原因 | 先查什么 |
|---|---|---|
too stale to catch up |
oplog 被覆盖,从节点落后太多 | rs.printSecondaryReplicationInfo() 看落后时间 |
成员卡在 STARTUP2 |
初始同步没完成 / 反复重试 | 新节点日志搜 initial sync,看网络与源节点 IO |
日志出现 OplogStartMissing |
要找的 oplog 起点已被覆盖 | 重做初始同步,评估 oplog 大小 |
| Change Streams 中断后连不上 | resumeToken 失效 | 检查 token 持久化、oplog 窗口是否够 |
| 同步「跑完」但数据对不上 | 缺一致性校验 / 文件拷贝的 count 偏差 | 行数比对 + 抽样哈希比对 |
工具报 not authorized |
账号权限不足 | 核对源端读 oplog、目标端写权限 |
十二、复盘与 Checklist
把这事想通之后,你会发现 MongoDB 数据同步没那么玄乎,难就难在「选对方案」。送你一份 Checklist,动手前先过一遍:
- 目标端是 Mongo 还是关系型/数仓/国产库?(决定走内还是走外)
- 要实时同步,还是一次性迁移?(决定长期通道还是搬一次)
- 有没有开发资源维护自研消费者?(决定 Change Streams 还是工具)
- 是否私有化/信创环境?(决定能不能用云 DTS)
- 数据量大不大、要不要用文件拷贝同步?(决定初始同步姿势)
- 有没有盯着复制延迟、oplog 窗口?(决定会不会半夜被叫醒)
- 同步完成后有没有做一致性校验?(决定数据错不错)
MongoDB 数据同步这件事,一半功夫在选对路线,另一半在你有没有把延迟、断点和一致性这三件事提前想清楚。 这句话送给你,能省不少半夜救火的工夫。
我是DBA小马哥,十年一线数据库运维。写的东西都是生产环境里趟出来的,关注我,少踩坑。